Showing preview only (1,236K chars total). Download the full file or copy to clipboard to get everything.
Repository: mozilla/send
Branch: master
Commit: ade10e496c06
Files: 319
Total size: 1.1 MB
Directory structure:
gitextract_4or1ul2c/
├── .circleci/
│ └── config.yml
├── .dockerignore
├── .editorconfig
├── .eslintignore
├── .eslintrc.yml
├── .gitattributes
├── .gitignore
├── .htmllintrc
├── .prettierignore
├── .stylelintrc
├── .vscode/
│ └── settings.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTORS
├── Dockerfile
├── LICENSE
├── README.md
├── android/
│ ├── .eslintrc.yaml
│ ├── .gitignore
│ ├── README.md
│ ├── android.js
│ ├── app/
│ │ ├── .gitignore
│ │ ├── build.gradle
│ │ ├── buildAssets.sh
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── mozilla/
│ │ │ └── firefoxsend/
│ │ │ └── MainActivity.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── drawable-v24/
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── layout/
│ │ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ └── values/
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ ├── build.gradle
│ ├── gradle/
│ │ └── wrapper/
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ ├── gradlew
│ ├── gradlew.bat
│ ├── pages/
│ │ ├── .eslintrc.yaml
│ │ ├── error.js
│ │ ├── home.js
│ │ ├── preferences.js
│ │ ├── share.js
│ │ └── upload.js
│ ├── settings.gradle
│ ├── stores/
│ │ ├── intents.js
│ │ └── state.js
│ └── user.js
├── app/
│ ├── .eslintrc.yml
│ ├── api.js
│ ├── archive.js
│ ├── capabilities.js
│ ├── controller.js
│ ├── crc32.js
│ ├── dragManager.js
│ ├── ece.js
│ ├── experiments.js
│ ├── fileReceiver.js
│ ├── fileSender.js
│ ├── fxa.js
│ ├── keychain.js
│ ├── locale.js
│ ├── main.css
│ ├── main.js
│ ├── metrics.js
│ ├── ownedFile.js
│ ├── pasteManager.js
│ ├── readme.md
│ ├── routes.js
│ ├── serviceWorker.js
│ ├── storage.js
│ ├── streams.js
│ ├── ui/
│ │ ├── account.js
│ │ ├── archiveTile.js
│ │ ├── blank.js
│ │ ├── body.js
│ │ ├── copyDialog.js
│ │ ├── download.js
│ │ ├── downloadCompleted.js
│ │ ├── downloadDialog.js
│ │ ├── downloadPassword.js
│ │ ├── error.js
│ │ ├── expiryOptions.js
│ │ ├── footer.js
│ │ ├── header.js
│ │ ├── home.js
│ │ ├── intro.js
│ │ ├── modal.js
│ │ ├── noStreams.js
│ │ ├── notFound.js
│ │ ├── okDialog.js
│ │ ├── report.js
│ │ ├── selectbox.js
│ │ ├── shareDialog.js
│ │ └── unsupported.js
│ ├── user.js
│ ├── utils.js
│ └── zip.js
├── browserslist
├── build/
│ ├── android_index_plugin.js
│ ├── readme.md
│ └── version_plugin.js
├── common/
│ ├── assets.js
│ ├── generate_asset_map.js
│ └── readme.md
├── docker-compose.yml
├── docs/
│ ├── CODEOWNERS
│ ├── acceptance-mobile.md
│ ├── acceptance-web.md
│ ├── build.md
│ ├── deployment.md
│ ├── docker.md
│ ├── encryption.md
│ ├── experiments.md
│ ├── faq.md
│ ├── localization.md
│ ├── metrics.md
│ ├── notes/
│ │ └── streams.md
│ └── takedowns.md
├── ios/
│ ├── generate-bundle.js
│ ├── ios.js
│ ├── send-ios/
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets/
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── Base.lproj/
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ ├── Info.plist
│ │ ├── ViewController.swift
│ │ ├── assets/
│ │ │ ├── index.css
│ │ │ └── index.html
│ │ └── help.html
│ ├── send-ios-action-extension/
│ │ ├── ActionViewController.swift
│ │ ├── Base.lproj/
│ │ │ └── MainInterface.storyboard
│ │ └── Info.plist
│ └── send-ios.xcodeproj/
│ ├── project.pbxproj
│ └── project.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcshareddata/
│ └── IDEWorkspaceChecks.plist
├── l10n.toml
├── package.json
├── postcss.config.js
├── public/
│ ├── contribute.json
│ ├── inter.css
│ └── locales/
│ ├── an/
│ │ └── send.ftl
│ ├── ar/
│ │ └── send.ftl
│ ├── ast/
│ │ └── send.ftl
│ ├── az/
│ │ └── send.ftl
│ ├── azz/
│ │ └── send.ftl
│ ├── be/
│ │ └── send.ftl
│ ├── bn/
│ │ └── send.ftl
│ ├── br/
│ │ └── send.ftl
│ ├── bs/
│ │ └── send.ftl
│ ├── ca/
│ │ └── send.ftl
│ ├── cak/
│ │ └── send.ftl
│ ├── ckb/
│ │ └── send.ftl
│ ├── cs/
│ │ └── send.ftl
│ ├── cy/
│ │ └── send.ftl
│ ├── da/
│ │ └── send.ftl
│ ├── de/
│ │ └── send.ftl
│ ├── dsb/
│ │ └── send.ftl
│ ├── el/
│ │ └── send.ftl
│ ├── en-CA/
│ │ └── send.ftl
│ ├── en-GB/
│ │ └── send.ftl
│ ├── en-US/
│ │ └── send.ftl
│ ├── es-AR/
│ │ └── send.ftl
│ ├── es-CL/
│ │ └── send.ftl
│ ├── es-ES/
│ │ └── send.ftl
│ ├── es-MX/
│ │ └── send.ftl
│ ├── et/
│ │ └── send.ftl
│ ├── eu/
│ │ └── send.ftl
│ ├── fa/
│ │ └── send.ftl
│ ├── fi/
│ │ └── send.ftl
│ ├── fr/
│ │ └── send.ftl
│ ├── fy-NL/
│ │ └── send.ftl
│ ├── gn/
│ │ └── send.ftl
│ ├── gor/
│ │ └── send.ftl
│ ├── he/
│ │ └── send.ftl
│ ├── hr/
│ │ └── send.ftl
│ ├── hsb/
│ │ └── send.ftl
│ ├── hu/
│ │ └── send.ftl
│ ├── hus/
│ │ └── send.ftl
│ ├── hy-AM/
│ │ └── send.ftl
│ ├── ia/
│ │ └── send.ftl
│ ├── id/
│ │ └── send.ftl
│ ├── ig/
│ │ └── send.ftl
│ ├── it/
│ │ └── send.ftl
│ ├── ixl/
│ │ └── send.ftl
│ ├── ja/
│ │ └── send.ftl
│ ├── ka/
│ │ └── send.ftl
│ ├── kab/
│ │ └── send.ftl
│ ├── ko/
│ │ └── send.ftl
│ ├── lt/
│ │ └── send.ftl
│ ├── lus/
│ │ └── send.ftl
│ ├── meh/
│ │ └── send.ftl
│ ├── mix/
│ │ └── send.ftl
│ ├── ml/
│ │ └── send.ftl
│ ├── ms/
│ │ └── send.ftl
│ ├── nb-NO/
│ │ └── send.ftl
│ ├── nl/
│ │ └── send.ftl
│ ├── nn-NO/
│ │ └── send.ftl
│ ├── oc/
│ │ └── send.ftl
│ ├── pa-IN/
│ │ └── send.ftl
│ ├── pai/
│ │ └── send.ftl
│ ├── pl/
│ │ └── send.ftl
│ ├── ppl/
│ │ └── send.ftl
│ ├── pt-BR/
│ │ └── send.ftl
│ ├── pt-PT/
│ │ └── send.ftl
│ ├── quc/
│ │ └── send.ftl
│ ├── ro/
│ │ └── send.ftl
│ ├── ru/
│ │ └── send.ftl
│ ├── sk/
│ │ └── send.ftl
│ ├── sl/
│ │ └── send.ftl
│ ├── sn/
│ │ └── send.ftl
│ ├── sq/
│ │ └── send.ftl
│ ├── sr/
│ │ └── send.ftl
│ ├── su/
│ │ └── send.ftl
│ ├── sv-SE/
│ │ └── send.ftl
│ ├── te/
│ │ └── send.ftl
│ ├── th/
│ │ └── send.ftl
│ ├── tl/
│ │ └── send.ftl
│ ├── tr/
│ │ └── send.ftl
│ ├── trs/
│ │ └── send.ftl
│ ├── uk/
│ │ └── send.ftl
│ ├── vi/
│ │ └── send.ftl
│ ├── yo/
│ │ └── send.ftl
│ ├── yua/
│ │ └── send.ftl
│ ├── zgh/
│ │ └── send.ftl
│ ├── zh-CN/
│ │ └── send.ftl
│ └── zh-TW/
│ └── send.ftl
├── scripts/
│ ├── .eslintrc.yml
│ ├── bin/
│ │ └── run-integration-test-circleci.sh
│ ├── get-prod-locales.js
│ ├── lint-locales.js
│ └── sync-npm-dependencies.sh
├── server/
│ ├── amplitude.js
│ ├── bin/
│ │ ├── dev.js
│ │ ├── prod.js
│ │ └── test.js
│ ├── clientConstants.js
│ ├── config.js
│ ├── fxa.js
│ ├── initScript.js
│ ├── keychain.js
│ ├── layout.js
│ ├── limiter.js
│ ├── locale.js
│ ├── log.js
│ ├── metadata.js
│ ├── middleware/
│ │ ├── auth.js
│ │ └── language.js
│ ├── readme.md
│ ├── routes/
│ │ ├── delete.js
│ │ ├── done.js
│ │ ├── download.js
│ │ ├── exists.js
│ │ ├── filelist.js
│ │ ├── index.js
│ │ ├── info.js
│ │ ├── metadata.js
│ │ ├── metrics.js
│ │ ├── pages.js
│ │ ├── params.js
│ │ ├── password.js
│ │ ├── report.js
│ │ ├── token.js
│ │ ├── upload.js
│ │ ├── webmanifest.js
│ │ └── ws.js
│ ├── state.js
│ └── storage/
│ ├── fs.js
│ ├── gcs.js
│ ├── index.js
│ ├── redis.js
│ └── s3.js
├── tailwind.config.js
├── test/
│ ├── .eslintrc.yml
│ ├── backend/
│ │ ├── auth-tests.js
│ │ ├── delete-tests.js
│ │ ├── info-tests.js
│ │ ├── language-tests.js
│ │ ├── metadata-tests.js
│ │ ├── owner-tests.js
│ │ ├── params-tests.js
│ │ ├── password-tests.js
│ │ ├── s3-tests.js
│ │ └── storage-tests.js
│ ├── frontend/
│ │ ├── .eslintrc.yml
│ │ ├── index.js
│ │ ├── routes.js
│ │ ├── runner.js
│ │ └── tests/
│ │ ├── api-tests.js
│ │ ├── auth-tests.js
│ │ ├── crypto-tests.js
│ │ ├── fileSender-tests.js
│ │ ├── keychain-tests.js
│ │ ├── streaming-tests.js
│ │ └── workflow-tests.js
│ ├── integration/
│ │ ├── README.md
│ │ ├── download-tests.js
│ │ ├── fixtures/
│ │ │ ├── txt-larger-testfile.txt
│ │ │ └── txt-small-testfile.txt
│ │ ├── homepage-tests.js
│ │ ├── pages/
│ │ │ └── desktop/
│ │ │ ├── download_page.js
│ │ │ ├── home_page.js
│ │ │ └── page.js
│ │ ├── progress-tests.js
│ │ └── send-test.html
│ ├── readme.md
│ ├── testServer.js
│ ├── wdio.circleci.conf.js
│ ├── wdio.common.conf.js
│ ├── wdio.docker.conf.js
│ ├── wdio.local.conf.js
│ ├── wdio.remote.config.js
│ └── wdio.saucelabs.config.js
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
version: 2.0
jobs:
test:
docker:
- image: circleci/node:12-browsers
steps:
- checkout
- run: npm ci
- run: npm run lint
- run: npm test
- store_artifacts:
path: coverage
integration_tests:
docker:
- image: circleci/node:12-browsers
steps:
- checkout
- run: npm ci
- run:
name: Run integration test
command: ./scripts/bin/run-integration-test-circleci.sh
deploy_dev:
docker:
- image: circleci/node:12
steps:
- checkout
- setup_remote_docker
- run: docker login -u $DOCKER_USER -p $DOCKER_PASS
- run: docker build -t mozilla/send:latest .
- run: docker push mozilla/send:latest
deploy_vnext:
docker:
- image: circleci/node:12
steps:
- checkout
- setup_remote_docker
- run: docker login -u $DOCKER_USER -p $DOCKER_PASS
- run: docker build -t mozilla/send:vnext .
- run: docker push mozilla/send:vnext
deploy_stage:
docker:
- image: circleci/node:12
steps:
- checkout
- setup_remote_docker
- run: docker login -u $DOCKER_USER -p $DOCKER_PASS
- run: docker build -t mozilla/send:$CIRCLE_TAG .
- run: docker push mozilla/send:$CIRCLE_TAG
workflows:
version: 2
test_pr:
jobs:
- test:
filters:
branches:
ignore:
- master
- vnext
- integration_tests:
filters:
branches:
ignore: master
build_and_deploy_dev:
jobs:
- deploy_dev:
filters:
branches:
only: master
tags:
ignore: /^v.*/
- deploy_vnext:
filters:
branches:
only: vnext
tags:
ignore: /^v.*/
build_and_deploy_stage:
jobs:
- test:
filters:
branches:
ignore: /.*/
tags:
only: /^v.*/
- integration_tests:
filters:
branches:
ignore: /.*/
tags:
only: /^v.*/
- deploy_stage:
requires:
- test
- integration_tests
filters:
branches:
ignore: /.*/
tags:
only: /^v.*/
================================================
FILE: .dockerignore
================================================
.circleci
.nyc_output
.vscode
.DS_Store
coverage
docs
firefox
node_modules
================================================
FILE: .editorconfig
================================================
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
[*.{js,html,yml,json,handlebars}]
indent_style = space
indent_size = 2
[*.toml]
indent_style = space
indent_size = 4
================================================
FILE: .eslintignore
================================================
dist
assets
firefox
coverage
android/app/build
app/locale.js
app/capabilities.js
================================================
FILE: .eslintrc.yml
================================================
env:
es6: true
node: true
extends:
- eslint:recommended
- prettier
- plugin:node/recommended
- plugin:security/recommended
plugins:
- node
- security
root: true
rules:
node/no-deprecated-api: off
node/no-unsupported-features/es-syntax: off
node/no-unsupported-features/node-builtins: off
node/no-unpublished-require: off
node/no-unpublished-import: off
security/detect-non-literal-fs-filename: off
security/detect-object-injection: off
no-unused-vars: [error, {argsIgnorePattern: "^_|err|event|next|reject"}]
require-atomic-updates: warn
================================================
FILE: .gitattributes
================================================
public/locales/* linguist-documentation
docs/* linguist-documentation
================================================
FILE: .gitignore
================================================
node_modules
coverage
dist
.idea
.DS_Store
.nyc_output
.tox
.pytest_cache
*.iml
android/app/src/main/assets
ios/send-ios/assets/ios.js
ios/send-ios/assets/vendor.js
ios/send-ios.xcodeproj/project.xcworkspace/xcuserdata/*
ios/send-ios.xcodeproj/xcuserdata/*
test/integration/downloads
================================================
FILE: .htmllintrc
================================================
{
"attr-name-style": "dash",
"id-class-style": "dash",
"indent-width": 2
}
================================================
FILE: .prettierignore
================================================
dist
android/app/src/main/assets
android/app/build
coverage
================================================
FILE: .stylelintrc
================================================
extends: stylelint-config-standard
plugins:
- stylelint-no-unsupported-browser-features
rules:
plugin/no-unsupported-browser-features: [true, {severity: warning}]
color-hex-case: lower
declaration-colon-newline-after: null
selector-list-comma-newline-after: null
value-list-comma-newline-after: null
at-rule-no-unknown: null
================================================
FILE: .vscode/settings.json
================================================
{
}
================================================
FILE: CHANGELOG.md
================================================
## Change Log
### v2.5.1 (2018/03/12 19:26 +00:00)
- [#789](https://github.com/mozilla/send/pull/789) Fixed #775 : Made text not-selectable (@RCMainak)
### v2.5.0 (2018/03/08 19:31 +00:00)
- [#782](https://github.com/mozilla/send/pull/782) updated docs (@dannycoates)
- [#781](https://github.com/mozilla/send/pull/781) Don't translate URL-safe chars, b64 is doing it for us (@timvisee)
- [#779](https://github.com/mozilla/send/pull/779) implemented crypto polyfills for ms edge (@dannycoates)
### v2.4.1 (2018/02/28 17:05 +00:00)
- [#777](https://github.com/mozilla/send/pull/777) use a separate circle in the progress svg for indefinite progress (@dannycoates)
### v2.4.0 (2018/02/27 01:55 +00:00)
- [#769](https://github.com/mozilla/send/pull/769) removed unsafe-inline styles via svgo-loader (@dannycoates)
- [#767](https://github.com/mozilla/send/pull/767) added coverage artifact to circleci (@dannycoates)
- [#766](https://github.com/mozilla/send/pull/766) Some frontend unit tests [WIP] (@dannycoates)
- [#761](https://github.com/mozilla/send/pull/761) added maxPasswordLength and passwordError messages (@dannycoates)
- [#764](https://github.com/mozilla/send/pull/764) added indefinite progress mode (@dannycoates)
- [#760](https://github.com/mozilla/send/pull/760) refactored css: phase 1 (@dannycoates)
- [#759](https://github.com/mozilla/send/pull/759) Switch en-US FTL file to new syntax (@flodolo)
- [#758](https://github.com/mozilla/send/pull/758) refactored server (@dannycoates)
- [#757](https://github.com/mozilla/send/pull/757) Update to fluent 0.4.3 (@stasm)
### v2.3.0 (2018/02/01 23:27 +00:00)
- [#536](https://github.com/mozilla/send/pull/536) use redis expire event to delete stored data immediately (@ehuggett)
- [#744](https://github.com/mozilla/send/pull/744) Gradient experiment (@dannycoates)
- [#739](https://github.com/mozilla/send/pull/739) added /api/info/:id route (@dannycoates)
- [#737](https://github.com/mozilla/send/pull/737) big refactor (@dannycoates)
- [#722](https://github.com/mozilla/send/pull/722) Add localization note to 'Time' and 'Downloads' string (@flodolo)
- [#721](https://github.com/mozilla/send/pull/721) show download Limits on page; Fixes #661 (@shikhar-scs)
- [#694](https://github.com/mozilla/send/pull/694) Passwords can now be changed (#687) (@himanish-star)
- [#702](https://github.com/mozilla/send/pull/702) Restricted the banner from showing on unsupported browsers (@himanish-star)
- [#701](https://github.com/mozilla/send/pull/701) improved popup for mobile display; Fixes #699 (@shikhar-scs)
- [#683](https://github.com/mozilla/send/pull/683) API changes to accommodate 3rd party clients (@ehuggett)
- [#698](https://github.com/mozilla/send/pull/698) Popup for delete button attached (@himanish-star)
- [#695](https://github.com/mozilla/send/pull/695) Show Warning, Cancel and Redirect on size > 2GB ; fixes #578 (@shikhar-scs)
- [#684](https://github.com/mozilla/send/pull/684) delete btn popup attached (@himanish-star)
- [#686](https://github.com/mozilla/send/pull/686) Hide password while Typing and after Entering: Fixes #670 (@shikhar-scs)
- [#679](https://github.com/mozilla/send/pull/679) changed font to sans sherif: Solves #676 (@shikhar-scs)
- [#693](https://github.com/mozilla/send/pull/693) README: Fix query link for "good first bugs" (@jspam)
- [#685](https://github.com/mozilla/send/pull/685) checkbox now has a hover effect: fixes #635 (@himanish-star)
- [#668](https://github.com/mozilla/send/pull/668) Add possibility to bind to a specific IP address (@TwizzyDizzy)
- [#682](https://github.com/mozilla/send/pull/682) [Docs] - README.md - minor spelling fixes (@tmm2018)
- [#672](https://github.com/mozilla/send/pull/672) Use EXPIRE_SECONDS to calculate file ttl for static content (@derektamsen)
- [#680](https://github.com/mozilla/send/pull/680) adjusted line height of label : fixes #609 (@himanish-star)
### v2.2.2 (2017/12/19 18:06 +00:00)
- [#667](https://github.com/mozilla/send/pull/667) Make develop the default NODE_ENV (@claudijd)
### v2.2.1 (2017/12/08 18:00 +00:00)
- [#665](https://github.com/mozilla/send/pull/665) stop drag target from flickering when dragging over children (@ericawright)
### v2.2.0 (2017/12/06 23:57 +00:00)
- [#654](https://github.com/mozilla/send/pull/654) Multiple download UI (@dannycoates)
- [#650](https://github.com/mozilla/send/pull/650) #634: overwrite appearance of password submit input (@ovlb)
- [#649](https://github.com/mozilla/send/pull/649) #609 share interface: align text in input and button (@ovlb)
### v2.1.2 (2017/11/16 19:03 +00:00)
- [#645](https://github.com/mozilla/send/pull/645) Remove the leak of the password into the console (@laurentj)
### v2.1.0 (2017/11/15 03:07 +00:00)
- [#641](https://github.com/mozilla/send/pull/641) Added experiment for firefox download promo (@dannycoates)
- [#640](https://github.com/mozilla/send/pull/640) use fluent-langneg for subtag support (@dannycoates)
- [#639](https://github.com/mozilla/send/pull/639) wrap number localization in try/catch (@dannycoates)
### v2.0.0 (2017/11/08 05:31 +00:00)
- [#633](https://github.com/mozilla/send/pull/633) Keyboard navigation/visual feedback regression (@ehuggett)
- [#632](https://github.com/mozilla/send/pull/632) display the 'add password' button only when the input field isn't empty (@dannycoates)
- [#626](https://github.com/mozilla/send/pull/626) Partial fix for #623 (@ehuggett)
- [#624](https://github.com/mozilla/send/pull/624) set a default MIME type in file metadata (@ehuggett)
- [#612](https://github.com/mozilla/send/pull/612) Password UI nits (@dannycoates, @ericawright)
- [#617](https://github.com/mozilla/send/pull/617) allow drag and drop if navigating from shared page (@ericawright)
- [#608](https://github.com/mozilla/send/pull/608) disable copying link when password not completed (@ericawright)
- [#605](https://github.com/mozilla/send/pull/605) align the "Password" and "Copy to clipboard" fields. (@ericawright)
- [#582](https://github.com/mozilla/send/pull/582) Add optional password to the download url (@dannycoates)
### v1.2.4 (2017/10/10 17:34 +00:00)
- [#583](https://github.com/mozilla/send/pull/583) Promote the beefy UI to default (@dannycoates)
- [#581](https://github.com/mozilla/send/pull/581) introducing ToC to README.md (@tmm2018)
- [#579](https://github.com/mozilla/send/pull/579) Hide cancel button when upload reaches 100% (@ericawright)
- [#580](https://github.com/mozilla/send/pull/580) Change Favicon in to look better in a variety of cases (@ericawright)
- [#571](https://github.com/mozilla/send/pull/571) Centre logo (@ehuggett)
- [#574](https://github.com/mozilla/send/pull/574) Make upload button focusable (accessibility/tab navigation) (@ehuggett)
### v1.2.0 (2017/09/12 22:42 +00:00)
- [#559](https://github.com/mozilla/send/pull/559) added first A/B experiment (@dannycoates)
- [#542](https://github.com/mozilla/send/pull/542) fix docker link typo (@ehuggett)
- [#541](https://github.com/mozilla/send/pull/541) removed .title and .alt attributes from ftl (@dannycoates)
- [#537](https://github.com/mozilla/send/pull/537) a few changes to make A/B testing easier (@dannycoates)
- [#533](https://github.com/mozilla/send/pull/533) minor UI fixes (@youwenliang)
- [#531](https://github.com/mozilla/send/pull/531) Add CHANGELOG script (@pdehaan)
- [#535](https://github.com/mozilla/send/pull/535) Fixed minimum NodeJS version in README (@LuFlo)
- [#528](https://github.com/mozilla/send/pull/528) adding separators to README (@tmm2018)
### v1.1.1 (2017/08/17 01:29 +00:00)
- [#516](https://github.com/mozilla/send/pull/516) cache assets (@dannycoates)
- [#520](https://github.com/mozilla/send/pull/520) fix drag & drop (@dannycoates)
- [#515](https://github.com/mozilla/send/pull/515) removed jquery from upload.js (@dannycoates)
- [#514](https://github.com/mozilla/send/pull/514) use async and removed jquery from download.js (@dannycoates)
- [#513](https://github.com/mozilla/send/pull/513) use svg for progress (@dannycoates)
- [#510](https://github.com/mozilla/send/pull/510) added precommit hook for format (@dannycoates)
- [#502](https://github.com/mozilla/send/pull/502) extracted filelist into its own file (@dannycoates)
- [#428](https://github.com/mozilla/send/pull/428) add twitter and open graph cards (@dannycoates, @johngruen)
- [#506](https://github.com/mozilla/send/pull/506) 404 page (@varghesethomase)
- [#508](https://github.com/mozilla/send/pull/508) fixes 478 (@abhinadduri)
- [#504](https://github.com/mozilla/send/pull/504) fix japanese browse button (@johngruen)
- [#503](https://github.com/mozilla/send/pull/503) Added editorconfig (@skystar-p)
- [#499](https://github.com/mozilla/send/pull/499) use import/export in the frontend code (@dannycoates)
- [#500](https://github.com/mozilla/send/pull/500) fixed build:css on windows (@dannycoates)
- [#481](https://github.com/mozilla/send/pull/481) Cater for mobile and desktop (@pdehaan, @hubdotcom)
- [#493](https://github.com/mozilla/send/pull/493) added webpack-dev-middleware (@dannycoates)
- [#491](https://github.com/mozilla/send/pull/491) added missing exit event cases (@dannycoates)
- [#492](https://github.com/mozilla/send/pull/492) make the site mostly work when cookies (localStorage) are disabled (@dannycoates)
- [#490](https://github.com/mozilla/send/pull/490) set the mime type in the download blob (@dannycoates)
- [#485](https://github.com/mozilla/send/pull/485) added progress to tab title when not in focus (@dannycoates)
- [#474](https://github.com/mozilla/send/pull/474) Fixing bug #438 by adding role attribute to anchor tags and alt attribute images (@varghesethomase)
- [#480](https://github.com/mozilla/send/pull/480) Increase font weight to 500 on <button>s and <label>s (@pdehaan)
- [#419](https://github.com/mozilla/send/pull/419) Add autoprefixer and cssnano support (@pdehaan)
### v1.1.0 (2017/08/08 03:59 +00:00)
- [#473](https://github.com/mozilla/send/pull/473) Sort contributors alphabetically to prevent churn (@pdehaan)
- [#472](https://github.com/mozilla/send/pull/472) removed references to checksums in frontend tests (@abhinadduri)
- [#470](https://github.com/mozilla/send/pull/470) removed the file sha256 hash (@dannycoates)
- [#469](https://github.com/mozilla/send/pull/469) Increase mimimum node version to 8.2.0 (@ehuggett)
- [#468](https://github.com/mozilla/send/pull/468) attach delete-file handler only after upload (@dannycoates)
- [#466](https://github.com/mozilla/send/pull/466) added webpack (@dannycoates)
- [#427](https://github.com/mozilla/send/pull/427) Extended system font list fixes:#408 (@gautamkrishnar)
- [#448](https://github.com/mozilla/send/pull/448) Migrate width attribute to CSS (Fixes #436) (@nskins)
- [#457](https://github.com/mozilla/send/pull/457) factored out progress into progress.js (@dannycoates)
- [#452](https://github.com/mozilla/send/pull/452) refactored metrics (@dannycoates)
- [#455](https://github.com/mozilla/send/pull/455) Add a few missing strings from es-CL and tr locales (@pdehaan)
- [#444](https://github.com/mozilla/send/pull/444) Chain jQuery calls, do not use events alias and store selectors (@Johann-S)
- [#416](https://github.com/mozilla/send/pull/416) WIP: use webcrypto-liner to support Safari 10 (@dannycoates)
- [#451](https://github.com/mozilla/send/pull/451) Add rel noopener noreferrer to target='_blank' anchor elements (Fixes #439) (@boopeshmahendran)
- [#449](https://github.com/mozilla/send/pull/449) Add X-UA-Compatible meta tag (@kenrick95)
- [#433](https://github.com/mozilla/send/pull/433) Prevent download button from being clicked multiple times (@pdehaan)
- [#432](https://github.com/mozilla/send/pull/432) Add contributors script (@pdehaan)
- [#409](https://github.com/mozilla/send/pull/409) Handle copy clipboard disabled (@Johann-S)
### v1.0.4 (2017/08/03 23:05 +00:00)
- [#418](https://github.com/mozilla/send/pull/418) _blank all footer links (@dannycoates)
- [#386](https://github.com/mozilla/send/pull/386) fix percentage view on mobile layout (@ariestiyansyah)
- [#414](https://github.com/mozilla/send/pull/414) Add link to FAQ in unsupported view (@pdehaan)
- [#415](https://github.com/mozilla/send/pull/415) Only include Fira CSS on /unsupported/* route (@pdehaan)
- [#412](https://github.com/mozilla/send/pull/412) throw key errors before download begins (@dannycoates)
- [#404](https://github.com/mozilla/send/pull/404) Use async function instead of promise (#325) (@weihanglo)
- [#406](https://github.com/mozilla/send/pull/406) Add noscript tag (@pdehaan)
- [#325](https://github.com/mozilla/send/pull/325) Use async function instead of promise (#325) (@weihanglo)
- [#325](https://github.com/mozilla/send/pull/325) Use async function instead of promise (#325) (@weihanglo)
### v1.0.3 (2017/08/02 23:59 +00:00)
- [#402](https://github.com/mozilla/send/pull/402) filter the hash from error reports (@dannycoates)
- [#400](https://github.com/mozilla/send/pull/400) fix link that breaks download by opening in new tab (@johngruen)
- [#369](https://github.com/mozilla/send/pull/369) Add ESLint no-alert shame rule (@pdehaan)
- [#396](https://github.com/mozilla/send/pull/396) add babel-polyfill (@dannycoates)
- [#394](https://github.com/mozilla/send/pull/394) catch JSON.parse errors of storage metadata (@dannycoates)
- [#367](https://github.com/mozilla/send/pull/367) Generate production locales using 'compare-locales' (@pdehaan)
- [#392](https://github.com/mozilla/send/pull/392) Adjust hover behavior on send-logo (#382)
Fixes: #382. (@weihanglo)
- [#382](https://github.com/mozilla/send/pull/382) Adjust hover behavior on send-logo (#382) (@weihanglo)
- [#382](https://github.com/mozilla/send/pull/382) Adjust hover behavior on send-logo (#382) (@weihanglo)
- [#380](https://github.com/mozilla/send/pull/380) Add Pontoon URL to README (@pdehaan)
### v1.0.2 (2017/07/31 18:58 +00:00)
- [#365](https://github.com/mozilla/send/pull/365) revert the IE fix to fix footer on chrome (@dannycoates)
### v1.0.1 (2017/07/31 17:28 +00:00)
- [#353](https://github.com/mozilla/send/pull/353) redirect ie to /unsupported (@abhinadduri, @dannycoates)
- [#360](https://github.com/mozilla/send/pull/360) Fix some linting nits (@pdehaan)
- [#362](https://github.com/mozilla/send/pull/362) Adjusts category of unsupported event (fixes #350). (@chuckharmston)
- [#355](https://github.com/mozilla/send/pull/355) Make order of uploaded files in list consistent (@pdehaan)
- [#356](https://github.com/mozilla/send/pull/356) Get rid of console.log statements (@pdehaan)
- [#358](https://github.com/mozilla/send/pull/358) Fix some missing .title attributes in dev-only locales (@pdehaan)
- [#354](https://github.com/mozilla/send/pull/354) Remove /en-US/ from cookies link in footer (@pdehaan)
- [#339](https://github.com/mozilla/send/pull/339) Show error page on firefox v49 and below (@ericawright, @abhinadduri)
- [#346](https://github.com/mozilla/send/pull/346) Add docs/CODEOWNERS file (@pdehaan)
- [#345](https://github.com/mozilla/send/pull/345) wrap long file names (@dnarcese)
- [#344](https://github.com/mozilla/send/pull/344) don't wrap file list headers (@dnarcese)
- [#327](https://github.com/mozilla/send/pull/327) Modify popup delete dialog (@youwenliang)
- [#341](https://github.com/mozilla/send/pull/341) center percentage text on all browser versions (@dnarcese)
- [#340](https://github.com/mozilla/send/pull/340) Remove duplicate entities in localized FTL files (@flodolo)
- [#337](https://github.com/mozilla/send/pull/337) support v 50 and 51 by not allowing const in loops (@ericawright)
- [#338](https://github.com/mozilla/send/pull/338) Remove duplicated strings in en-US, fix nn-NO file (@flodolo)
- [#336](https://github.com/mozilla/send/pull/336) German(de): Fixed missing value for deleteFileButton (#336) (@flodolo)
- [#334](https://github.com/mozilla/send/pull/334) fix functionality on firefox 50 and 51 (@dnarcese)
### v1.0.0 (2017/07/26 19:08 +00:00)
- [#323](https://github.com/mozilla/send/pull/323) disable upload/download notifications (@dannycoates)
- [#322](https://github.com/mozilla/send/pull/322) fix feedback button jump (@dnarcese)
- [#320](https://github.com/mozilla/send/pull/320) fix German footer (@dnarcese)
### v0.2.2 (2017/07/26 04:50 +00:00)
- [#314](https://github.com/mozilla/send/pull/314) added L10N_DEV environment variable for making all languages available (@dannycoates)
- [#313](https://github.com/mozilla/send/pull/313) removing timeout limit for front end tests (@abhinadduri)
- [#311](https://github.com/mozilla/send/pull/311) expired ids should reject instead of returning null (@dannycoates)
- [#302](https://github.com/mozilla/send/pull/302) UX Refine WIP (@youwenliang)
- [#310](https://github.com/mozilla/send/pull/310) if the download card is pressed, the expired card shows up properly (@abhinadduri)
- [#269](https://github.com/mozilla/send/pull/269) refactored ftl file (@abhinadduri)
- [#291](https://github.com/mozilla/send/pull/291) added legal page (@dannycoates)
- [#307](https://github.com/mozilla/send/pull/307) don't show error page on upload cancel (@dnarcese)
- [#299](https://github.com/mozilla/send/pull/299) use CIRCLE_TAG as version.json version if present (@dannycoates)
### v0.2.1 (2017/07/24 23:34 +00:00)
- [#296](https://github.com/mozilla/send/pull/296) restyle delete popup (@dnarcese)
- [#295](https://github.com/mozilla/send/pull/295) renamed environment variables to remove P2P_ prefix (@dannycoates)
- [#294](https://github.com/mozilla/send/pull/294) dealing with invalid drag and drops (@abhinadduri)
- [#297](https://github.com/mozilla/send/pull/297) added environment variable for expire time (@dannycoates)
- [#292](https://github.com/mozilla/send/pull/292) Fixes289 (@abhinadduri)
- [#288](https://github.com/mozilla/send/pull/288) fix: Don`t allow upload when not on the upload page. (@ericawright)
- [#285](https://github.com/mozilla/send/pull/285) added messages for processing phases (@dannycoates)
- [#267](https://github.com/mozilla/send/pull/267) make site responsive and add feedback link (@johngruen)
- [#286](https://github.com/mozilla/send/pull/286) Update download progress bar color (@pdehaan)
- [#281](https://github.com/mozilla/send/pull/281) Stop ESLint from linting the /public/ directory (@pdehaan)
- [#280](https://github.com/mozilla/send/pull/280) created /unsupported page and added gcmCompliant to /download page (@dannycoates)
- [#279](https://github.com/mozilla/send/pull/279) create separate js bundles for upload/download pages (@dannycoates)
- [#268](https://github.com/mozilla/send/pull/268) Testpilot ga (@abhinadduri)
### v0.2.0 (2017/07/21 19:27 +00:00)
- [#266](https://github.com/mozilla/send/pull/266) abort uploads over maxfilesize (@dannycoates)
- [#264](https://github.com/mozilla/send/pull/264) Remove duplicate custom metric. (@chuckharmston)
- [#259](https://github.com/mozilla/send/pull/259) add alert when uploading multiple files (@dnarcese)
- [#262](https://github.com/mozilla/send/pull/262) sync download progress bar with percentage (@dnarcese)
- [#258](https://github.com/mozilla/send/pull/258) better sync percent with progress bar (@dnarcese)
- [#257](https://github.com/mozilla/send/pull/257) add a dynamic js script for page config (@dannycoates)
- [#256](https://github.com/mozilla/send/pull/256) add file size limit message (@dnarcese)
- [#253](https://github.com/mozilla/send/pull/253) Add favicon.ico version of the Send logo (@pdehaan)
- [#254](https://github.com/mozilla/send/pull/254) Add nsp check to circle ci (@pdehaan)
- [#245](https://github.com/mozilla/send/pull/245) Localization (@abhinadduri)
- [#252](https://github.com/mozilla/send/pull/252) only allow drag and drop on upload page (@dnarcese)
- [#250](https://github.com/mozilla/send/pull/250) make footer not overlap (@dnarcese)
- [#251](https://github.com/mozilla/send/pull/251) minify all images (@ericawright)
- [#249](https://github.com/mozilla/send/pull/249) change how the file upload box expands (@dnarcese)
- [#246](https://github.com/mozilla/send/pull/246) remove P2P references. Fixes #224 (@clouserw)
- [#242](https://github.com/mozilla/send/pull/242) Make only icons clickable in file list (@dnarcese)
- [#236](https://github.com/mozilla/send/pull/236) add FAQ. Fixes #186 (@clouserw)
- [#235](https://github.com/mozilla/send/pull/235) allow send another file link to open in new tab (@dnarcese)
- [#234](https://github.com/mozilla/send/pull/234) fix download svg (@dnarcese)
- [#232](https://github.com/mozilla/send/pull/232) escape filename in the ui (@dannycoates)
- [#226](https://github.com/mozilla/send/pull/226) added functionality to cancel uploads (@abhinadduri)
- [#231](https://github.com/mozilla/send/pull/231) move head and html tags to main template (@dnarcese)
- [#228](https://github.com/mozilla/send/pull/228) add send logo (@dnarcese)
- [#229](https://github.com/mozilla/send/pull/229) change learn more and github links (@dnarcese)
- [#201](https://github.com/mozilla/send/pull/201) Adds metrics documentation (closes #5). (@chuckharmston)
- [#223](https://github.com/mozilla/send/pull/223) change size of send another file links (@dnarcese)
- [#222](https://github.com/mozilla/send/pull/222) add footer (@dnarcese)
- [#197](https://github.com/mozilla/send/pull/197) fixes issues 195 and 192 (@abhinadduri)
- [#204](https://github.com/mozilla/send/pull/204) added HSTS header (@dannycoates)
- [#193](https://github.com/mozilla/send/pull/193) Frontend tests (@abhinadduri)
- [#191](https://github.com/mozilla/send/pull/191) New ui! (@dnarcese)
### v0.1.4 (2017/07/12 18:21 +00:00)
- [#189](https://github.com/mozilla/send/pull/189) Add CSP directives (@dannycoates)
- [#188](https://github.com/mozilla/send/pull/188) fixes delete button error (@abhinadduri)
- [#185](https://github.com/mozilla/send/pull/185) added loading, hashing, and encrypting events for uploader; decryptin… (@abhinadduri)
- [#183](https://github.com/mozilla/send/pull/183) rename to 'Send' (@dannycoates)
- [#184](https://github.com/mozilla/send/pull/184) Server tests (@abhinadduri)
- [#178](https://github.com/mozilla/send/pull/178) fixed issues in branch title (@abhinadduri)
- [#177](https://github.com/mozilla/send/pull/177) Gcm compliance (@abhinadduri)
- [#106](https://github.com/mozilla/send/pull/106) Gcm (@abhinadduri, @dannycoates)
- [#168](https://github.com/mozilla/send/pull/168) Show error page if upload fails (@dnarcese)
- [#148](https://github.com/mozilla/send/pull/148) WIP: Add basic contribute.json (@pdehaan)
- [#162](https://github.com/mozilla/send/pull/162) Fix dev server URL in README.md file (@pdehaan)
- [#167](https://github.com/mozilla/send/pull/167) build docker image with new name (@relud)
- [#164](https://github.com/mozilla/send/pull/164) Add word wraps to table (@dnarcese)
- [#149](https://github.com/mozilla/send/pull/149) Add robots.txt (@pdehaan)
- [#161](https://github.com/mozilla/send/pull/161) Hide table header on empty list (@dnarcese)
- [#154](https://github.com/mozilla/send/pull/154) Remove expired uploads (@dnarcese)
- [#146](https://github.com/mozilla/send/pull/146) Update README with some more details (@pdehaan)
### v0.1.2 (2017/06/24 03:38 +00:00)
- [#138](https://github.com/mozilla/send/pull/138) remove notLocalHost (@dannycoates)
### v0.1.0 (2017/06/24 01:24 +00:00)
- [#137](https://github.com/mozilla/send/pull/137) refactored docker build (@dannycoates)
- [#132](https://github.com/mozilla/send/pull/132) Add /__version__ route (@pdehaan)
- [#135](https://github.com/mozilla/send/pull/135) make dockerfile more dockerflowy (@dannycoates)
- [#134](https://github.com/mozilla/send/pull/134) Load previous uploads (@dannycoates, @dnarcese)
- [#131](https://github.com/mozilla/send/pull/131) added __heartbeat__ (@dannycoates)
- [#133](https://github.com/mozilla/send/pull/133) Add LICENSE file (@pdehaan)
- [#130](https://github.com/mozilla/send/pull/130) added sentry to server code (@abhinadduri)
- [#124](https://github.com/mozilla/send/pull/124) Remove unused [dev]dependencies (@pdehaan)
- [#119](https://github.com/mozilla/send/pull/119) Move cross-env to a dep (@pdehaan)
- [#123](https://github.com/mozilla/send/pull/123) removed bitly integration (@abhinadduri)
- [#122](https://github.com/mozilla/send/pull/122) fix docker build (@dannycoates)
- [#121](https://github.com/mozilla/send/pull/121) added docker service to circle.yml (@dannycoates)
- [#120](https://github.com/mozilla/send/pull/120) added sentry (@abhinadduri)
- [#118](https://github.com/mozilla/send/pull/118) change docker image name and add builds for tags (@relud)
- [#116](https://github.com/mozilla/send/pull/116) add /__lbheartbeat__ endpoint (@relud)
- [#79](https://github.com/mozilla/send/pull/79) Optimize/minimize bundle.js for production (@pdehaan)
- [#104](https://github.com/mozilla/send/pull/104) Fix a bunch of ESLint and HTMLLint errors (@pdehaan)
- [#105](https://github.com/mozilla/send/pull/105) Progress bars (@dnarcese)
- [#111](https://github.com/mozilla/send/pull/111) added in anonmyized ip google analytics (@abhinadduri)
- [#110](https://github.com/mozilla/send/pull/110) added notifications (@abhinadduri)
- [#103](https://github.com/mozilla/send/pull/103) added Dockerfile (@dannycoates)
- [#100](https://github.com/mozilla/send/pull/100) Added Helmet Middleware (@abhinadduri)
- [#99](https://github.com/mozilla/send/pull/99) Testing (@abhinadduri)
- [#77](https://github.com/mozilla/send/pull/77) Fix the linter errors (@pdehaan)
- [#54](https://github.com/mozilla/send/pull/54) Adding basic ESLint config (@pdehaan)
- [#71](https://github.com/mozilla/send/pull/71) Drag & drop (@dnarcese)
- [#72](https://github.com/mozilla/send/pull/72) Logging (@abhinadduri, @dannycoates)
- [#45](https://github.com/mozilla/send/pull/45) S3 integration (@abhinadduri, @dannycoates)
- [#46](https://github.com/mozilla/send/pull/46) Download page and share link UI (@dnarcese)
- [#41](https://github.com/mozilla/send/pull/41) Added upload page and file list UI (@dnarcese)
- [#40](https://github.com/mozilla/send/pull/40) Tweak the package.json file (@pdehaan)
- [#43](https://github.com/mozilla/send/pull/43) added return (@abhinadduri)
- [#42](https://github.com/mozilla/send/pull/42) changed to handle 404 during download, also removing progress listene… (@abhinadduri)
- [#39](https://github.com/mozilla/send/pull/39) Refactor riff (@abhinadduri, @dannycoates)
- [#36](https://github.com/mozilla/send/pull/36) added prettier for js formatting (@dannycoates)
- [#28](https://github.com/mozilla/send/pull/28) Added a UI for the uploader end, made stylistic changes, implemented deleting (@abhinadduri)
- [#25](https://github.com/mozilla/send/pull/25) Changed naming for some pages, no longer stores files by name on server (@abhinadduri)
- [#17](https://github.com/mozilla/send/pull/17) changed from using input fields for keys to getting from url (#17) (@abhinadduri)
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Community Participation Guidelines
This repository is governed by Mozilla's code of conduct and etiquette guidelines.
For more details, please read the
[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
## How to Report
For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.
<!--
## Project Specific Etiquette
In some cases, there will be additional project etiquette i.e.: (https://bugzilla.mozilla.org/page.cgi?id=etiquette.html).
Please update for your project.
-->
================================================
FILE: CONTRIBUTORS
================================================
Abdalrahman Hwoij
Abhinav Adduri
Adnan Kičin
Adolfo Jayme Barrientos
Alberto Castro
Alexander Slovesnik
Alfredos-Panagiotis Damkalis
Aman Alam
Amin Mahmudian
Ander Elortondo
Andreas Pettersson
Anesu Chiodza
Anika Dorn
Anish Sheela
Arash Mousavi
Artem Polivanchuk
Ashikur Rahman
Ashok kumar
Balasankar C
Balázs Meskó
Belayet Hossain
Benjamin Forehand Jr
Besnik Bleta
Björn I
Bjørn I
Boopesh Mahendran
Brahim Essaidi
Brainlulz
Breana Gonzales
Christian Elbrianno
Christoph Kührer
Christopher Ramírez
Chuck Harmston
Cloney 173741
Cláudio Esperança
Cristian Silaghi
Cynthia Pereira
Daniel Thorn
Daniela Arcese
Danny Coates
Davide
Derek Tamsen
Dhyey Thakore
Donovan Preston
Edi Santoso
Edmund Huggett
Elisa X
Emily
Emily Hou
Emin Mastizada
Enol
Erica
Erica Wright
Fauzan Alfi
Filip Hruška
Fjoerfoks
Francesco Lodolo
Francesco Lodolo [:flod]
Frederick Villaluna
G12r
Gabriela
Gautam krishna.R
George Raptis
Georgianizator
Gonçalo Matos
Gwenn
Hampus
Hugo
Hugo Abreu
Hyeonseok Shin
Håvar Henriksen
Ian Neal
ItielMaN
Jae Hyeon Park
Jakob Kappel
Jakub Rychlý
Jamie
Jarmo
Jim Spentzos
Jiri Grönroos
Jobava
Joe Becher
Joe ST
Joergen
Johann-S
John Gruen
Jon Buckley
Jon Vadillo
Jonathan Claudius
Jordi Cuevas
Jordi Serratosa
Juan Esteban Ajsivinac Sián
Juan Sián
Juraj Cigáň
Kerim Kalamujić
Khaled Hosny
Kim Ludvigsen
Kim Younggeon
Kohei Yoshino
Lan Glad
Lasse Liehu
Laurent Jouanneau
Lobodzets
LuFlo
Luis A. Sánchez
Luiz Carlos de Morais
Luiz Felipe F M Costa
Luna Jernberg
Mahay Alam Khan
Marcelo Ghelman
Marcelo Poli
Marco Aurélio
Mark Heijl
Mark Liang
Mark Liang (You-Wen)
Marko Andrejić
Martijn Dekker
Marwan Mohamad
Matjaž Horvat
Maykon Chagas
Melo46
Merike Sell
Michael Köhler
Michael Wolf
Michal Stanke
Michal Vašíček
Mikeyy
Miro Rauhala
Mozilla Pontoon
Mozilla-GitHub-Standards
Mozinet
Moḥend Belqasem
Muhend Belkacem
Muḥend Belqasem
Myungjae Won
Nicholas Skinsacos
Nihad
Nihad Suljić
Niksend Mizuhara
Oscar
Paulius
Pedro Burlamaqui Bendahan
Peter deHaan
Pierre Neter
Pin-guang Chen
Piotr Drąg
Quentí
Quế Tùng
Rachel Tublitz
Radu Popescu
Rhoslyn Prys
RickieES
Rimas Kudelis
Rizky Ariestiyansyah
Rob Powell
Robert
Roberto Alvarado
Rodrigo
Rodrigo Guerra
Rok Žerdin
Romi Hardiyanto
Rongjian Zhang
Ruba
Sahithi
Sairam Raavi
Sander Lepik
Sandro
Sara Todaro
Sav22999
Schieck :)
Selim Şumlu
Selyan Sliman Amiri
Sidak Singh Aulakh
Slimane Amiri
Slimane Selyan AMIRI
Soumya Himanish Mohapatra
Staś Małolepszy
Suriyaa ✌️️
Tema
Thomas Dalichow
Théo Chevalier
Tiago Morais Morgado
Tim Visée
Tomer Cohen
Tomáš Zelina
Ton
Top
Tymur Faradzhev
Uccen Marzuq
Varghese Thomas
Victor Bychek
Vimal Raghubir
Vitaliy Krutko
Weihang Lo
Wiktor Furman
Wil Clouser
YFdyh000
Yassine Aït-El-Mouden
Yongmin H
You-Wen Liang (Mark)
aaaaalbert
aefgh39622
alamanda
albertdcastro
alex_mayorga
ariestiyansyah
avelper
chilledfrogs
clouserw-mozilla-owner
dgadelha
dskmori
ehuggett
eljuno
emily-hou1
erdem cobanoglu
gautamkrishnar
gmontagu
goofy
hello
hi
ivan.pompa
jesferman1993
jlG
josotrix
jspam
julen
julenx
kenrick95
kumincir
leo.toneff
m4hdi.pdroid
mail
manxmensch
marigalicer
marsf
merianosnikos
mirzet.omerovic.1992
mujeebcpy
p.sanroman.bengoetxea
passionforlife
paul.trevor
pyup.io bot
ravmn
rcmainak
reza.habibi2008
rgpublic
risger
robbp
ruikunai
savemore99.sm
sergio
shikhar-scs
siparon
skystar-p
stripTM
tatalmondmush
tiagomoraismorgado
timvisee
victor.gonzalezro
xcffl
ybouhamam
yoshimitsu002
yusup.ramdani
Μιχάλης
Марко Костић (Marko Kostić)
Ратко Вујановић
صفا الفليج
వీవెన్
ജോയ്സ്
张无忌
新垣结衣松冈茉优长泽雅美门胁麦上野树里石原里美
莫非前世那一眼
================================================
FILE: Dockerfile
================================================
##
# Firefox Send - Mozilla
#
# License https://github.com/mozilla/send/blob/master/LICENSE
##
# Build project
FROM node:12 AS builder
RUN set -x \
# Add user
&& addgroup --gid 10001 app \
&& adduser --disabled-password \
--gecos '' \
--gid 10001 \
--home /app \
--uid 10001 \
app
COPY --chown=app:app . /app
USER app
WORKDIR /app
RUN set -x \
# Build
&& PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm ci \
&& npm run build
# Main image
FROM node:12-slim
RUN set -x \
# Add user
&& addgroup --gid 10001 app \
&& adduser --disabled-password \
--gecos '' \
--gid 10001 \
--home /app \
--uid 10001 \
app
RUN apt-get update && apt-get -y install \
git-core \
&& rm -rf /var/lib/apt/lists/*
USER app
WORKDIR /app
COPY --chown=app:app package*.json ./
COPY --chown=app:app app app
COPY --chown=app:app common common
COPY --chown=app:app public/locales public/locales
COPY --chown=app:app server server
COPY --chown=app:app --from=builder /app/dist dist
RUN npm ci --production && npm cache clean --force
RUN mkdir -p /app/.config/configstore
RUN ln -s dist/version.json version.json
ENV PORT=1443
EXPOSE ${PORT}
CMD ["node", "server/bin/prod.js"]
================================================
FILE: LICENSE
================================================
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
================================================
FILE: README.md
================================================
# Firefox Send
[](https://circleci.com/gh/mozilla/send)
## NOTICE - May 2021
Mozilla discontinued the Firefox Send service in September 2021. For more information about this, please see the [Mozilla Blog](https://blog.mozilla.org/blog/2020/09/17/update-on-firefox-send-and-firefox-notes/).
Please note that the [Mozilla Public License 2.0](https://www.mozilla.org/en-US/MPL/2.0/) does not "grant any rights in the trademarks, service marks, or logos of any Contributor." You may fork and modify the source code for Firefox Send pursuant to the Mozilla Public License, but you may not create a version of the service that uses Mozilla trademarks or logos.
This repository is archived. In May 2021, Mozilla removed Mozilla trademarks from some of the files in this repository so that developers using this code are less likely to inadvertently infringe Mozilla's trademarks and confuse users. You are welcome to copy and modify this code under its open source license, but please ensure that all use complies with [Mozilla's trademark policy](https://www.mozilla.org/en-US/foundation/trademarks/policy/). In other words, if you create a new version of Firefox Send you must remove all "Mozilla" and "Firefox" branding to ensure that users are not confused about who is providing the service.
**Docs:** [FAQ](docs/faq.md), [Encryption](docs/encryption.md), [Build](docs/build.md), [Docker](docs/docker.md), [Metrics](docs/metrics.md), [More](docs/)
---
## Table of Contents
* [What it does](#what-it-does)
* [Requirements](#requirements)
* [Development](#development)
* [Commands](#commands)
* [Configuration](#configuration)
* [Localization](#localization)
* [Contributing](#contributing)
* [Testing](#testing)
* [Deployment](#deployment)
* [Android](#android)
* [License](#license)
---
## What it does
A file sharing experiment which allows you to send encrypted files to other users.
---
## Requirements
- [Node.js 12.x](https://nodejs.org/)
- [Redis server](https://redis.io/) (optional for development)
- [AWS S3](https://aws.amazon.com/s3/) or compatible service (optional)
---
## Development
To start an ephemeral development server, run:
```sh
npm install
npm start
```
Then, browse to http://localhost:8080
---
## Commands
| Command | Description |
|------------------|-------------|
| `npm run format` | Formats the frontend and server code using **prettier**.
| `npm run lint` | Lints the CSS and JavaScript code.
| `npm test` | Runs the suite of mocha tests.
| `npm start` | Runs the server in development configuration.
| `npm run build` | Builds the production assets.
| `npm run prod` | Runs the server in production configuration.
---
## Configuration
The server is configured with environment variables. See [server/config.js](server/config.js) for all options and [docs/docker.md](docs/docker.md) for examples.
---
## Localization
Firefox Send localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language or Mozilla’s [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance.
see also [docs/localization.md](docs/localization.md)
---
## Contributing
Pull requests are always welcome! Feel free to check out the list of ["good first issues"](https://github.com/mozilla/send/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
---
## Testing
| ENVIRONMENT | URL
|-------------|-----
| Production | <https://send.firefox.com/>
| Stage | <https://stage.send.nonprod.cloudops.mozgcp.net/>
| Development | <https://send2.dev.lcip.org/>
---
## Deployment
see also [docs/deployment.md](docs/deployment.md)
---
## Android
The android implementation is contained in the `android` directory, and can be viewed locally for easy testing and editing by running `ANDROID=1 npm start` and then visiting <http://localhost:8080>. CSS and image files are located in the `android/app/src/main/assets` directory.
---
## License
[Mozilla Public License Version 2.0](LICENSE)
---
================================================
FILE: android/.eslintrc.yaml
================================================
env:
browser: true
parserOptions:
sourceType: module
================================================
FILE: android/.gitignore
================================================
local.properties
.gradle
build
================================================
FILE: android/README.md
================================================
Readme
=====
The Send Android app allows you to choose any file from your android device, encrypt it with a password, and get a URL which will allow secure download of the file. By default, this URL will expire after one download or 24 hours.
Building the Send Android app.
=====
First, install Android Studio. Open the `android` directory in Android Studio, plug in your android phone, and press the run button.
================================================
FILE: android/android.js
================================================
import 'intl-pluralrules';
import choo from 'choo';
import html from 'choo/html';
import * as Sentry from '@sentry/browser';
import { setApiUrlPrefix, getConstants } from '../app/api';
import metrics from '../app/metrics';
//import assets from '../common/assets';
import Archive from '../app/archive';
import Header from '../app/ui/header';
import storage from '../app/storage';
import controller from '../app/controller';
import User from './user';
import intents from './stores/intents';
import home from './pages/home';
import upload from './pages/upload';
import share from './pages/share';
import preferences from './pages/preferences';
import error from './pages/error';
import { getTranslator } from '../app/locale';
import { setTranslate } from '../app/utils';
import { delay } from '../app/utils';
if (navigator.userAgent === 'Send Android') {
setApiUrlPrefix('https://send.firefox.com');
}
const app = choo();
//app.use(state);
app.use(controller);
app.use(intents);
window.finishLogin = async function(accountInfo) {
while (!(app.state && app.state.user)) {
await delay();
}
await app.state.user.finishLogin(accountInfo);
await app.state.user.syncFileList();
app.emitter.emit('replaceState', '/');
};
function body(main) {
return function(state, emit) {
/*
Disable the preferences menu for now since it is ugly and isn't
relevant to the beta
function clickPreferences(event) {
event.preventDefault();
emit('pushState', '/preferences');
}
const menu = html`<a
id="hamburger"
class="absolute top-0 right-0 z-50"
href="#"
onclick="${clickPreferences}"
>
<img src="${assets.get('preferences.png')}" />
</a>`;
*/
return html`
<body class="flex flex-col items-center font-sans bg-grey-10 h-screen">
${state.cache(Header, 'header').render()} ${main(state, emit)}
</body>
`;
};
}
(async function start() {
const translate = await getTranslator('en-US');
setTranslate(translate);
const { LIMITS, DEFAULTS } = await getConstants();
app.use(state => {
state.LIMITS = LIMITS;
state.DEFAULTS = DEFAULTS;
state.translate = translate;
state.capabilities = {
account: true
}; //TODO
state.archive = new Archive([], DEFAULTS.EXPIRE_SECONDS);
state.storage = storage;
state.user = new User(storage, LIMITS);
state.sentry = Sentry;
});
app.use(metrics);
app.route('/', body(home));
app.route('/upload', upload);
app.route('/share/:id', share);
app.route('/preferences', preferences);
app.route('/error', error);
//app.route('/debugging', require('./pages/debugging').default);
// add /api/filelist
app.mount('body');
})();
window.app = app;
================================================
FILE: android/app/.gitignore
================================================
/build
================================================
FILE: android/app/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
defaultConfig {
applicationId "org.mozilla.firefoxsend"
minSdkVersion 26
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'com.github.delight-im:Android-AdvancedWebView:v3.0.0'
implementation "org.mozilla.components:service-firefox-accounts:$android_components_version"
}
task generateAndLinkBundle(type: Exec, description: 'Generate the android.js bundle and link it into the assets directory') {
commandLine './buildAssets.sh'
}
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn generateAndLinkBundle
}
================================================
FILE: android/app/buildAssets.sh
================================================
#!/usr/bin/env bash
if [ -d "../../node_modules" ]
then
echo "node_modules already present."
else
echo "node_modules not present, running npm install."
npm install
fi
npm run build
rm -rf src/main/assets
mkdir -p src/main/assets
cp -R ../../dist/* src/main/assets
================================================
FILE: android/app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: android/app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.mozilla.firefoxsend">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing" android:value="false" />
<activity android:name="org.mozilla.firefoxsend.MainActivity" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</application>
</manifest>
================================================
FILE: android/app/src/main/java/org/mozilla/firefoxsend/MainActivity.kt
================================================
package org.mozilla.firefoxsend
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.util.Base64
import android.util.Log
import android.view.View
import android.webkit.*
import im.delight.android.webview.AdvancedWebView
import kotlinx.android.synthetic.main.activity_main.*
import mozilla.components.service.fxa.Config
import mozilla.components.service.fxa.FirefoxAccount
import mozilla.components.service.fxa.FxaResult
import org.json.JSONObject
internal class LoggingWebChromeClient : WebChromeClient() {
override fun onConsoleMessage(cm: ConsoleMessage): Boolean {
Log.d(TAG, String.format("%s @ %d: %s",
cm.message(), cm.lineNumber(), cm.sourceId()))
return true
}
companion object {
private const val TAG = "CONTENT"
}
}
class WebAppInterface(private val mContext: MainActivity) {
@JavascriptInterface
fun beginOAuthFlow() {
mContext.beginOAuthFlow()
}
@JavascriptInterface
fun shareUrl(url: String) {
mContext.shareUrl(url)
}
}
class MainActivity : AppCompatActivity(), AdvancedWebView.Listener {
private var mToShare: String? = null
private var mToCall: String? = null
private var mAccount: FirefoxAccount? = null
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
webView.apply {
setListener(this@MainActivity, this@MainActivity)
addJavascriptInterface(WebAppInterface(this@MainActivity), JS_INTERFACE_NAME)
setLayerType(View.LAYER_TYPE_HARDWARE, null)
webChromeClient = LoggingWebChromeClient()
settings.apply {
userAgentString = "Send Android"
allowUniversalAccessFromFileURLs = true
javaScriptEnabled = true
}
}
val type = intent.type
if (Intent.ACTION_SEND == intent.action && type != null) {
if (type == "text/plain") {
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
// Log.d(TAG_INTENT, "text/plain $sharedText")
mToShare = "data:text/plain;base64," + Base64.encodeToString(sharedText.toByteArray(), 16).trim()
} else if (type.startsWith("image/")) {
val imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) as Uri
// Log.d(TAG_INTENT, "image/ $imageUri")
mToShare = "data:text/plain;base64," + Base64.encodeToString(imageUri.path.toByteArray(), 16).trim()
}
}
webView.loadUrl("file:///android_asset/android.html")
}
fun beginOAuthFlow() {
Config.release().then { value ->
mAccount = FirefoxAccount(value, "20f7931c9054d833", "https://send.firefox.com/fxa/android-redirect.html")
mAccount?.beginOAuthFlow(arrayOf("profile", "https://identity.mozilla.com/apps/send"), true)
?.then { url ->
// Log.d(TAG_CONFIG, "GOT A URL $url")
this@MainActivity.runOnUiThread {
webView.loadUrl(url)
}
FxaResult.fromValue(Unit)
}
// Log.d(TAG_CONFIG, "CREATED FIREFOXACCOUNT")
FxaResult.fromValue(Unit)
}
}
fun shareUrl(url: String) {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, url)
}
val components = arrayOf(ComponentName(applicationContext, MainActivity::class.java))
val chooser = Intent.createChooser(shareIntent, "")
.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, components)
startActivity(chooser)
}
override fun onResume() {
super.onResume()
webView.onResume()
}
override fun onPause() {
webView.onPause()
super.onPause()
}
override fun onDestroy() {
webView.onDestroy()
super.onDestroy()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
webView.onActivityResult(requestCode, resultCode, intent)
}
override fun onBackPressed() {
if (!webView.onBackPressed()) {
return
}
super.onBackPressed()
}
override fun onPageStarted(url: String, favicon: Bitmap?) {
if (url.startsWith("https://send.firefox.com/fxa/android-redirect.html")) {
// We load this here so the user doesn't see the android-redirect.html page
webView.loadUrl("file:///android_asset/android.html")
val uri = Uri.parse(url)
uri.getQueryParameter("code")?.let { code ->
uri.getQueryParameter("state")?.let { state ->
mAccount?.completeOAuthFlow(code, state)?.whenComplete { info ->
mAccount?.getProfile(false)?.then { profile ->
val profileJsonPayload = JSONObject()
.put("accessToken", info.accessToken)
.put("keys", info.keys)
.put("avatar", profile.avatar)
.put("displayName", profile.displayName)
.put("email", profile.email)
.put("uid", profile.uid).toString()
mToCall = "finishLogin($profileJsonPayload)"
this@MainActivity.runOnUiThread {
// Clear the history so that the user can't use the back button to see broken pages
// that were inserted into the history by the login process.
webView.clearHistory()
// We also reload this here because we need to make sure onPageFinished runs after mToCall has been set.
// We can't guarantee that onPageFinished wasn't already called at this point.
webView.loadUrl("file:///android_asset/android.html")
}
FxaResult.fromValue(Unit)
}
}
}
}
}
if (!url.startsWith("file:///android_asset/") && !url.startsWith("https://accounts.firefox.com/")) {
// Don't allow loading anything other than the app in our webview
// It should be possible to do this with shouldOverrideUrlLoading
// but it didn't seem to be working, so this works as a hack.
webView.loadUrl("file:///android_asset/android.html")
Log.d(TAG_MAIN, "BAD URL " + url)
} else {
// Log.d(TAG_MAIN, "onPageStarted " + url)
}
}
override fun onPageFinished(url: String) {
// Log.d(TAG_MAIN, "onPageFinished")
if (mToShare != null) {
// Log.d(TAG_INTENT, mToShare)
webView.postWebMessage(WebMessage(mToShare), Uri.EMPTY)
mToShare = null
}
if (mToCall != null) {
this@MainActivity.runOnUiThread {
webView.evaluateJavascript(mToCall) {
mToCall = null
}
}
}
}
override fun onPageError(errorCode: Int, description: String, failingUrl: String) {
Log.d(TAG_MAIN, "onPageError($errorCode, $description, $failingUrl)")
}
override fun onDownloadRequested(url: String,
suggestedFilename: String,
mimeType: String,
contentLength: Long,
contentDisposition: String,
userAgent: String) {
// Log.d(TAG_MAIN, "onDownloadRequested")
}
override fun onExternalPageRequest(url: String) {
// Log.d(TAG_MAIN, "onExternalPageRequest($url)")
}
companion object {
private const val TAG_MAIN = "MAIN"
private const val TAG_INTENT = "INTENT"
private const val TAG_CONFIG = "CONFIG"
private const val JS_INTERFACE_NAME = "Android"
}
}
================================================
FILE: android/app/src/main/res/drawable/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="92.5"
android:viewportHeight="92.5">
<group android:translateX="27.75"
android:translateY="28.25">
<path
android:pathData="M18.1313,0.0003C8.1363,0.0003 0.0003,7.9743 0.0003,17.7673C0.0003,18.8133 0.8523,19.6643 1.8983,19.6643L16.2333,19.6643L16.2333,29.1093L11.7773,24.6963C11.0413,23.9613 9.8403,23.9613 9.0653,24.6963C8.3293,25.4323 8.3293,26.6323 9.0653,27.4063L16.7753,35.1093C16.8143,35.1483 16.8533,35.1873 16.9303,35.2253L16.9693,35.2253C17.0083,35.2643 17.0463,35.3033 17.0853,35.3033L17.1243,35.3033C17.1633,35.3423 17.2013,35.3423 17.2403,35.3803L17.2793,35.3803C17.3183,35.4193 17.3953,35.4193 17.4343,35.4583C17.4733,35.4963 17.5503,35.4963 17.5893,35.4963C17.6283,35.4963 17.7053,35.5353 17.7443,35.5353L17.7823,35.5353C17.8213,35.5353 17.8603,35.5353 17.9373,35.5743L18.3253,35.5743C18.3643,35.5743 18.4023,35.5743 18.4803,35.5353L18.5193,35.5353C18.5573,35.5353 18.6353,35.4963 18.6743,35.4963C18.7123,35.4963 18.7903,35.4583 18.8293,35.4193C18.8673,35.3803 18.9453,35.3803 18.9843,35.3423C19.0223,35.3033 19.0613,35.3033 19.1003,35.2643L19.1383,35.2643C19.1773,35.2253 19.2163,35.1873 19.2553,35.1873L19.2933,35.1873C19.3323,35.1483 19.3713,35.1093 19.4483,35.0713L27.1583,27.3673C27.8943,26.6323 27.8943,25.4323 27.1583,24.6583C26.4223,23.9223 25.2213,23.9223 24.4463,24.6583L20.0303,29.1093L20.0303,19.7033L34.3643,19.7033C35.4103,19.7033 36.2633,18.8513 36.2633,17.8063C36.2633,7.9743 28.1273,0.0003 18.1313,0.0003ZM3.9133,15.8713C4.8813,9.0963 10.8863,3.8323 18.1313,3.8323C25.3763,3.8323 31.3423,9.0963 32.3113,15.8713L3.9133,15.8713Z"
android:fillType="nonZero">
<aapt:attr name="android:fillColor">
<gradient
android:startY="2.9809632"
android:startX="25.805717"
android:endY="31.687763"
android:endX="8.569217"
android:type="linear">
<item android:offset="0" android:color="#FFFF980E"/>
<item android:offset="0.21" android:color="#FFFF7139"/>
<item android:offset="0.36" android:color="#FFFF5854"/>
<item android:offset="0.46" android:color="#FFFF4F5E"/>
<item android:offset="0.69" android:color="#FFFF3750"/>
<item android:offset="0.86" android:color="#FFF92261"/>
<item android:offset="1" android:color="#FFF5156C"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M18.1313,0.0003C8.1363,0.0003 0.0003,7.9743 0.0003,17.7673C0.0003,18.8133 0.8523,19.6643 1.8983,19.6643L16.2333,19.6643L16.2333,29.1093L11.7773,24.6963C11.0413,23.9613 9.8403,23.9613 9.0653,24.6963C8.3293,25.4323 8.3293,26.6323 9.0653,27.4063L16.7753,35.1093C16.8143,35.1483 16.8533,35.1873 16.9303,35.2253L16.9693,35.2253C17.0083,35.2643 17.0463,35.3033 17.0853,35.3033L17.1243,35.3033C17.1633,35.3423 17.2013,35.3423 17.2403,35.3803L17.2793,35.3803C17.3183,35.4193 17.3953,35.4193 17.4343,35.4583C17.4733,35.4963 17.5503,35.4963 17.5893,35.4963C17.6283,35.4963 17.7053,35.5353 17.7443,35.5353L17.7823,35.5353C17.8213,35.5353 17.8603,35.5353 17.9373,35.5743L18.3253,35.5743C18.3643,35.5743 18.4023,35.5743 18.4803,35.5353L18.5193,35.5353C18.5573,35.5353 18.6353,35.4963 18.6743,35.4963C18.7123,35.4963 18.7903,35.4583 18.8293,35.4193C18.8673,35.3803 18.9453,35.3803 18.9843,35.3423C19.0223,35.3033 19.0613,35.3033 19.1003,35.2643L19.1383,35.2643C19.1773,35.2253 19.2163,35.1873 19.2553,35.1873L19.2933,35.1873C19.3323,35.1483 19.3713,35.1093 19.4483,35.0713L27.1583,27.3673C27.8943,26.6323 27.8943,25.4323 27.1583,24.6583C26.4223,23.9223 25.2213,23.9223 24.4463,24.6583L20.0303,29.1093L20.0303,19.7033L34.3643,19.7033C35.4103,19.7033 36.2633,18.8513 36.2633,17.8063C36.2633,7.9743 28.1273,0.0003 18.1313,0.0003ZM3.9133,15.8713C4.8813,9.0963 10.8863,3.8323 18.1313,3.8323C25.3763,3.8323 31.3423,9.0963 32.3113,15.8713L3.9133,15.8713Z"
android:fillType="nonZero">
<aapt:attr name="android:fillColor">
<gradient
android:startY="2.9809632"
android:startX="25.805717"
android:endY="31.687763"
android:endX="8.569217"
android:type="linear">
<item android:offset="0" android:color="#CCFFF44F"/>
<item android:offset="0.75" android:color="#00FFF44F"/>
<item android:offset="1" android:color="#00FFF44F"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M20.0303,3.9483C26.3833,4.8003 31.4203,9.6773 32.3113,15.8713L23.8653,15.8713C21.7733,15.8713 20.0683,17.5743 20.0683,19.6643L34.3643,19.6643C35.4103,19.6643 36.2633,18.8133 36.2633,17.7673C36.2633,10.9933 31.4593,7.6643 27.3913,5.7673C23.6333,4.0253 20.0303,3.9483 20.0303,3.9483Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="20.534323"
android:startX="22.366518"
android:endY="7.772023"
android:endX="30.234228"
android:type="linear">
<item android:offset="0" android:color="#FF3A8EE6"/>
<item android:offset="0.24" android:color="#FF5C79F0"/>
<item android:offset="0.63" android:color="#FF9059FF"/>
<item android:offset="1" android:color="#FFC139E6"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M32.2333,15.4453C33.5123,16.4903 34.8293,17.4963 36.0693,18.5803C36.1853,18.3483 36.2633,18.0773 36.2633,17.7673C36.2633,10.9933 31.4593,7.6643 27.3913,5.7673C23.6333,4.0253 20.0303,3.9483 20.0303,3.9483C26.2283,4.7613 31.1873,9.4843 32.2333,15.4453Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="8.195093"
android:startX="30.235817"
android:endY="12.836453"
android:endX="26.934916"
android:type="linear">
<item android:offset="0" android:color="#7E6E008B"/>
<item android:offset="0.5" android:color="#00C846CB"/>
<item android:offset="1" android:color="#00C846CB"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M32.0013,15.8713L23.8653,15.8713C21.7733,15.8713 20.0683,17.5743 20.0683,19.6643L34.3643,19.6643C34.9453,19.6643 35.4883,19.3933 35.8373,18.9673C34.5583,17.9223 33.2793,16.9163 32.0013,15.8713Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="18.076923"
android:startX="31.69962"
android:endY="17.594997"
android:endX="23.366179"
android:type="linear">
<item android:offset="0" android:color="#006A2BEA"/>
<item android:offset="0.14" android:color="#006A2BEA"/>
<item android:offset="0.3" android:color="#15662CE6"/>
<item android:offset="0.47" android:color="#2C592FDB"/>
<item android:offset="0.64" android:color="#424534C9"/>
<item android:offset="0.82" android:color="#59283BAF"/>
<item android:offset="0.99" android:color="#7003448D"/>
<item android:offset="1" android:color="#7200458B"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>
================================================
FILE: android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="92.5"
android:viewportHeight="92.5">
<group android:translateX="27.75"
android:translateY="28.25">
<path
android:pathData="M18.1313,0.0003C8.1363,0.0003 0.0003,7.9743 0.0003,17.7673C0.0003,18.8133 0.8523,19.6643 1.8983,19.6643L16.2333,19.6643L16.2333,29.1093L11.7773,24.6963C11.0413,23.9613 9.8403,23.9613 9.0653,24.6963C8.3293,25.4323 8.3293,26.6323 9.0653,27.4063L16.7753,35.1093C16.8143,35.1483 16.8533,35.1873 16.9303,35.2253L16.9693,35.2253C17.0083,35.2643 17.0463,35.3033 17.0853,35.3033L17.1243,35.3033C17.1633,35.3423 17.2013,35.3423 17.2403,35.3803L17.2793,35.3803C17.3183,35.4193 17.3953,35.4193 17.4343,35.4583C17.4733,35.4963 17.5503,35.4963 17.5893,35.4963C17.6283,35.4963 17.7053,35.5353 17.7443,35.5353L17.7823,35.5353C17.8213,35.5353 17.8603,35.5353 17.9373,35.5743L18.3253,35.5743C18.3643,35.5743 18.4023,35.5743 18.4803,35.5353L18.5193,35.5353C18.5573,35.5353 18.6353,35.4963 18.6743,35.4963C18.7123,35.4963 18.7903,35.4583 18.8293,35.4193C18.8673,35.3803 18.9453,35.3803 18.9843,35.3423C19.0223,35.3033 19.0613,35.3033 19.1003,35.2643L19.1383,35.2643C19.1773,35.2253 19.2163,35.1873 19.2553,35.1873L19.2933,35.1873C19.3323,35.1483 19.3713,35.1093 19.4483,35.0713L27.1583,27.3673C27.8943,26.6323 27.8943,25.4323 27.1583,24.6583C26.4223,23.9223 25.2213,23.9223 24.4463,24.6583L20.0303,29.1093L20.0303,19.7033L34.3643,19.7033C35.4103,19.7033 36.2633,18.8513 36.2633,17.8063C36.2633,7.9743 28.1273,0.0003 18.1313,0.0003ZM3.9133,15.8713C4.8813,9.0963 10.8863,3.8323 18.1313,3.8323C25.3763,3.8323 31.3423,9.0963 32.3113,15.8713L3.9133,15.8713Z"
android:fillType="nonZero">
<aapt:attr name="android:fillColor">
<gradient
android:startY="2.9809632"
android:startX="25.805717"
android:endY="31.687763"
android:endX="8.569217"
android:type="linear">
<item android:offset="0" android:color="#FFFF980E"/>
<item android:offset="0.21" android:color="#FFFF7139"/>
<item android:offset="0.36" android:color="#FFFF5854"/>
<item android:offset="0.46" android:color="#FFFF4F5E"/>
<item android:offset="0.69" android:color="#FFFF3750"/>
<item android:offset="0.86" android:color="#FFF92261"/>
<item android:offset="1" android:color="#FFF5156C"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M18.1313,0.0003C8.1363,0.0003 0.0003,7.9743 0.0003,17.7673C0.0003,18.8133 0.8523,19.6643 1.8983,19.6643L16.2333,19.6643L16.2333,29.1093L11.7773,24.6963C11.0413,23.9613 9.8403,23.9613 9.0653,24.6963C8.3293,25.4323 8.3293,26.6323 9.0653,27.4063L16.7753,35.1093C16.8143,35.1483 16.8533,35.1873 16.9303,35.2253L16.9693,35.2253C17.0083,35.2643 17.0463,35.3033 17.0853,35.3033L17.1243,35.3033C17.1633,35.3423 17.2013,35.3423 17.2403,35.3803L17.2793,35.3803C17.3183,35.4193 17.3953,35.4193 17.4343,35.4583C17.4733,35.4963 17.5503,35.4963 17.5893,35.4963C17.6283,35.4963 17.7053,35.5353 17.7443,35.5353L17.7823,35.5353C17.8213,35.5353 17.8603,35.5353 17.9373,35.5743L18.3253,35.5743C18.3643,35.5743 18.4023,35.5743 18.4803,35.5353L18.5193,35.5353C18.5573,35.5353 18.6353,35.4963 18.6743,35.4963C18.7123,35.4963 18.7903,35.4583 18.8293,35.4193C18.8673,35.3803 18.9453,35.3803 18.9843,35.3423C19.0223,35.3033 19.0613,35.3033 19.1003,35.2643L19.1383,35.2643C19.1773,35.2253 19.2163,35.1873 19.2553,35.1873L19.2933,35.1873C19.3323,35.1483 19.3713,35.1093 19.4483,35.0713L27.1583,27.3673C27.8943,26.6323 27.8943,25.4323 27.1583,24.6583C26.4223,23.9223 25.2213,23.9223 24.4463,24.6583L20.0303,29.1093L20.0303,19.7033L34.3643,19.7033C35.4103,19.7033 36.2633,18.8513 36.2633,17.8063C36.2633,7.9743 28.1273,0.0003 18.1313,0.0003ZM3.9133,15.8713C4.8813,9.0963 10.8863,3.8323 18.1313,3.8323C25.3763,3.8323 31.3423,9.0963 32.3113,15.8713L3.9133,15.8713Z"
android:fillType="nonZero">
<aapt:attr name="android:fillColor">
<gradient
android:startY="2.9809632"
android:startX="25.805717"
android:endY="31.687763"
android:endX="8.569217"
android:type="linear">
<item android:offset="0" android:color="#CCFFF44F"/>
<item android:offset="0.75" android:color="#00FFF44F"/>
<item android:offset="1" android:color="#00FFF44F"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M20.0303,3.9483C26.3833,4.8003 31.4203,9.6773 32.3113,15.8713L23.8653,15.8713C21.7733,15.8713 20.0683,17.5743 20.0683,19.6643L34.3643,19.6643C35.4103,19.6643 36.2633,18.8133 36.2633,17.7673C36.2633,10.9933 31.4593,7.6643 27.3913,5.7673C23.6333,4.0253 20.0303,3.9483 20.0303,3.9483Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="20.534323"
android:startX="22.366518"
android:endY="7.772023"
android:endX="30.234228"
android:type="linear">
<item android:offset="0" android:color="#FF3A8EE6"/>
<item android:offset="0.24" android:color="#FF5C79F0"/>
<item android:offset="0.63" android:color="#FF9059FF"/>
<item android:offset="1" android:color="#FFC139E6"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M32.2333,15.4453C33.5123,16.4903 34.8293,17.4963 36.0693,18.5803C36.1853,18.3483 36.2633,18.0773 36.2633,17.7673C36.2633,10.9933 31.4593,7.6643 27.3913,5.7673C23.6333,4.0253 20.0303,3.9483 20.0303,3.9483C26.2283,4.7613 31.1873,9.4843 32.2333,15.4453Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="8.195093"
android:startX="30.235817"
android:endY="12.836453"
android:endX="26.934916"
android:type="linear">
<item android:offset="0" android:color="#7E6E008B"/>
<item android:offset="0.5" android:color="#00C846CB"/>
<item android:offset="1" android:color="#00C846CB"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M32.0013,15.8713L23.8653,15.8713C21.7733,15.8713 20.0683,17.5743 20.0683,19.6643L34.3643,19.6643C34.9453,19.6643 35.4883,19.3933 35.8373,18.9673C34.5583,17.9223 33.2793,16.9163 32.0013,15.8713Z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="18.076923"
android:startX="31.69962"
android:endY="17.594997"
android:endX="23.366179"
android:type="linear">
<item android:offset="0" android:color="#006A2BEA"/>
<item android:offset="0.14" android:color="#006A2BEA"/>
<item android:offset="0.3" android:color="#15662CE6"/>
<item android:offset="0.47" android:color="#2C592FDB"/>
<item android:offset="0.64" android:color="#424534C9"/>
<item android:offset="0.82" android:color="#59283BAF"/>
<item android:offset="0.99" android:color="#7003448D"/>
<item android:offset="1" android:color="#7200458B"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>
================================================
FILE: android/app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<im.delight.android.webview.AdvancedWebView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" />
================================================
FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
================================================
FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
================================================
FILE: android/app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>
================================================
FILE: android/app/src/main/res/values/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#220033</color>
</resources>
================================================
FILE: android/app/src/main/res/values/strings.xml
================================================
<resources>
<string name="app_name">Send</string>
</resources>
================================================
FILE: android/app/src/main/res/values/styles.xml
================================================
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
================================================
FILE: android/build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.21'
ext.android_components_version = '0.26.0'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.21"
}
}
allprojects {
repositories {
google()
maven { url "https://maven.mozilla.org/maven2" }
jcenter()
maven { url "https://jitpack.io" }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: android/gradle/wrapper/gradle-wrapper.properties
================================================
#Tue Feb 19 08:34:25 EST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
================================================
FILE: android/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
================================================
FILE: android/gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: android/gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: android/pages/.eslintrc.yaml
================================================
env:
browser: true
parserOptions:
sourceType: module
================================================
FILE: android/pages/error.js
================================================
const html = require('choo/html');
export default function error(_state, _emit) {
return html`
<body>
<div id="white">
<h1>Error</h1>
<p>Sorry, an error occurred.</p>
</div>
</body>
`;
}
================================================
FILE: android/pages/home.js
================================================
const html = require('choo/html');
const { list } = require('../../app/utils');
const archiveTile = require('../../app/ui/archiveTile');
const modal = require('../../app/ui/modal');
const intro = require('../../app/ui/intro');
const assets = require('../../common/assets');
module.exports = function(state, emit) {
function onchange(event) {
event.preventDefault();
const newFiles = Array.from(event.target.files);
emit('addFiles', { files: newFiles });
}
function onclick() {
document.getElementById('file-upload').click();
}
const archives = state.storage.files
.filter(archive => !archive.expired)
.map(archive => archiveTile(state, emit, archive))
.reverse();
let content = '';
let button = html`
<div
class="bg-blue-50 rounded-full m-4 flex items-center justify-center shadow-lg"
style="width: 56px; height: 56px"
onclick="${onclick}"
>
<img src="${assets.get('add.svg')}" />
</div>
`;
if (state.uploading) {
content = archiveTile.uploading(state, emit);
button = '';
} else if (state.archive.numFiles > 0) {
content = archiveTile.wip(state, emit);
button = '';
} else {
content =
archives.length < 1
? intro(state)
: list(archives, 'h-full overflow-y-auto w-full', 'mb-3 w-full');
}
return html`
<main class="main">
${state.modal && modal(state, emit)}
<section
class="h-full w-full p-6 z-10 overflow-hidden md:flex md:flex-row md:rounded-lg md:shadow-big"
>
${content}
</section>
<div class="fixed right-0 bottom-0 z-20">
${button}
<input
id="file-upload"
class="hidden"
type="file"
multiple
onchange="${onchange}"
onclick="${e => e.stopPropagation()}"
/>
</div>
</main>
`;
};
================================================
FILE: android/pages/preferences.js
================================================
const html = require('choo/html');
import { setFileProtocolWssUrl, getFileProtocolWssUrl } from '../../app/api';
export default function preferences(state, emit) {
const wssURL = getFileProtocolWssUrl();
function updateWssUrl(event) {
state.wssURL = event.target.value;
setFileProtocolWssUrl(state.wssURL);
emit('render');
}
function clickDone(event) {
event.preventDefault();
emit('pushState', '/');
}
return html`
<body>
<div id="white">
<div id="preferences">
<a onclick="${clickDone}" href="#"> done </a>
<dl>
<dt>wss url:</dt>
<dd>
<input type="text" onchange="${updateWssUrl}" value="${wssURL}" />
</dd>
</dl>
</div>
</div>
</body>
`;
}
================================================
FILE: android/pages/share.js
================================================
const html = require('choo/html');
export default function uploadComplete(state, emit) {
const file = state.storage.files[state.storage.files.length - 1];
function onclick(e) {
e.preventDefault();
input.select();
document.execCommand('copy');
input.selectionEnd = input.selectionStart;
copyText.textContent = 'Copied!';
setTimeout(function() {
copyText.textContent = 'Copy link';
}, 2000);
}
function uploadFile(event) {
event.preventDefault();
const target = event.target;
const file = target.files[0];
if (file.size === 0) {
return;
}
emit('pushState', '/upload');
emit('addFiles', { files: [file] });
emit('upload', {});
}
const input = html`
<input id="url" value="${file.url}" readonly="true" />
`;
const copyText = html`
<span>Copy link</span>
`;
return html`<body>
<div id="white">
<div class="card">
<div>The card contents will be here.</div>
<div>Expires after: <span class="expires-after">exp</span></div>
${input}
<div id="copy-link" onclick=${onclick}>
<img id="copy-image" src=${state.getAsset('copy-link.png')} />
${copyText}
</div>
<label id="label" for="input">
<img src=${state.getAsset('cloud-upload.png')} />
</label>
<input id="input" name="input" type="file" onchange=${uploadFile} />
</div>
</body>`;
}
================================================
FILE: android/pages/upload.js
================================================
const html = require('choo/html');
export default function progressBar(state, emit) {
let percent = 0;
if (state.transfer && state.transfer.progress) {
percent = Math.floor(state.transfer.progressRatio * 100);
}
function onclick(e) {
e.preventDefault();
if (state.uploading) {
emit('cancel');
}
emit('pushState', '/');
}
return html`
<body>
<div id="white">
<div class="card">
<div>${percent}%</div>
<span class="progress" style="width: ${percent}%">.</span>
<div class="cancel" onclick="${onclick}">CANCEL</div>
</div>
</div>
</body>
`;
}
================================================
FILE: android/settings.gradle
================================================
include ':app'
================================================
FILE: android/stores/intents.js
================================================
/* eslint-disable no-console */
export default function intentHandler(state, emitter) {
window.addEventListener(
'message',
event => {
if (typeof event.data !== 'string' || !event.data.startsWith('data:')) {
return;
}
fetch(event.data)
.then(res => res.blob())
.then(blob => {
emitter.emit('addFiles', { files: [blob] });
emitter.emit('upload', {});
})
.catch(e => console.error('ERROR ' + e + ' ' + e.stack));
},
false
);
}
================================================
FILE: android/stores/state.js
================================================
/* eslint-disable no-console */
import User from '../user';
import storage from '../../app/storage';
export default function initialState(state, emitter) {
const files = [];
Object.assign(state, {
prefix: '/android_asset',
user: new User(storage),
getAsset(name) {
return `${state.prefix}/${name}`;
},
sentry: {
captureException: e => {
console.error('ERROR ' + e + ' ' + e.stack);
}
},
storage: {
files,
remove: function(fileId) {
console.log('REMOVE FILEID', fileId);
},
writeFile: function(file) {
console.log('WRITEFILE', file);
},
addFile: function(file) {
console.log('addfile' + JSON.stringify(file));
files.push(file);
emitter.emit('pushState', `/share/${file.id}`);
},
totalUploads: 0
},
transfer: null,
uploading: false,
settingPassword: false,
passwordSetError: null,
route: '/'
});
}
================================================
FILE: android/user.js
================================================
/* global Android */
import User from '../app/user';
import { deriveFileListKey } from '../app/fxa';
export default class AndroidUser extends User {
constructor(storage, limits) {
super(storage, limits);
}
async login() {
Android.beginOAuthFlow();
}
startAuthFlow() {
return Promise.resolve();
}
async finishLogin(accountInfo) {
const jwks = JSON.parse(accountInfo.keys);
const ikm = jwks['https://identity.mozilla.com/apps/send'].k;
const profile = {
displayName: accountInfo.displayName,
email: accountInfo.email,
avatar: accountInfo.avatar,
access_token: accountInfo.accessToken
};
profile.fileListKey = await deriveFileListKey(ikm);
this.info = profile;
}
}
================================================
FILE: app/.eslintrc.yml
================================================
env:
browser: true
node: true
parserOptions:
sourceType: module
rules:
node/no-unsupported-features: off
================================================
FILE: app/api.js
================================================
import { arrayToB64, b64ToArray, delay } from './utils';
import { ECE_RECORD_SIZE } from './ece';
let fileProtocolWssUrl = null;
try {
fileProtocolWssUrl = localStorage.getItem('wssURL');
} catch (e) {
// NOOP
}
if (!fileProtocolWssUrl) {
fileProtocolWssUrl = 'wss://send.firefox.com/api/ws';
}
export class ConnectionError extends Error {
constructor(cancelled, duration, size) {
super(cancelled ? '0' : 'connection closed');
this.cancelled = cancelled;
this.duration = duration;
this.size = size;
}
}
export function setFileProtocolWssUrl(url) {
localStorage && localStorage.setItem('wssURL', url);
fileProtocolWssUrl = url;
}
export function getFileProtocolWssUrl() {
return fileProtocolWssUrl;
}
let apiUrlPrefix = '';
export function getApiUrl(path) {
return apiUrlPrefix + path;
}
export function setApiUrlPrefix(prefix) {
apiUrlPrefix = prefix;
}
function post(obj, bearerToken) {
const h = {
'Content-Type': 'application/json'
};
if (bearerToken) {
h['Authentication'] = `Bearer ${bearerToken}`;
}
return {
method: 'POST',
headers: new Headers(h),
body: JSON.stringify(obj)
};
}
export function parseNonce(header) {
header = header || '';
return header.split(' ')[1];
}
async function fetchWithAuth(url, params, keychain) {
const result = {};
params = params || {};
const h = await keychain.authHeader();
params.headers = new Headers({
Authorization: h,
'Content-Type': 'application/json'
});
const response = await fetch(url, params);
result.response = response;
result.ok = response.ok;
const nonce = parseNonce(response.headers.get('WWW-Authenticate'));
result.shouldRetry = response.status === 401 && nonce !== keychain.nonce;
keychain.nonce = nonce;
return result;
}
async function fetchWithAuthAndRetry(url, params, keychain) {
const result = await fetchWithAuth(url, params, keychain);
if (result.shouldRetry) {
return fetchWithAuth(url, params, keychain);
}
return result;
}
export async function del(id, owner_token) {
const response = await fetch(
getApiUrl(`/api/delete/${id}`),
post({ owner_token })
);
return response.ok;
}
export async function setParams(id, owner_token, bearerToken, params) {
const response = await fetch(
getApiUrl(`/api/params/${id}`),
post(
{
owner_token,
dlimit: params.dlimit
},
bearerToken
)
);
return response.ok;
}
export async function fileInfo(id, owner_token) {
const response = await fetch(
getApiUrl(`/api/info/${id}`),
post({ owner_token })
);
if (response.ok) {
const obj = await response.json();
return obj;
}
throw new Error(response.status);
}
export async function metadata(id, keychain) {
const result = await fetchWithAuthAndRetry(
getApiUrl(`/api/metadata/${id}`),
{ method: 'GET' },
keychain
);
if (result.ok) {
const data = await result.response.json();
const meta = await keychain.decryptMetadata(b64ToArray(data.metadata));
return {
size: meta.size,
ttl: data.ttl,
name: meta.name,
type: meta.type,
manifest: meta.manifest,
flagged: data.flagged
};
}
throw new Error(result.response.status);
}
export async function setPassword(id, owner_token, keychain) {
const auth = await keychain.authKeyB64();
const response = await fetch(
getApiUrl(`/api/password/${id}`),
post({ owner_token, auth })
);
return response.ok;
}
function asyncInitWebSocket(server) {
return new Promise((resolve, reject) => {
try {
const ws = new WebSocket(server);
ws.addEventListener('open', () => resolve(ws), { once: true });
} catch (e) {
reject(new ConnectionError(false));
}
});
}
function listenForResponse(ws, canceller) {
return new Promise((resolve, reject) => {
function handleClose(event) {
// a 'close' event before a 'message' event means the request failed
ws.removeEventListener('message', handleMessage);
reject(new ConnectionError(canceller.cancelled));
}
function handleMessage(msg) {
ws.removeEventListener('close', handleClose);
try {
const response = JSON.parse(msg.data);
if (response.error) {
throw new Error(response.error);
} else {
resolve(response);
}
} catch (e) {
reject(e);
}
}
ws.addEventListener('message', handleMessage, { once: true });
ws.addEventListener('close', handleClose, { once: true });
});
}
async function upload(
stream,
metadata,
verifierB64,
timeLimit,
dlimit,
bearerToken,
onprogress,
canceller
) {
let size = 0;
const start = Date.now();
const host = window.location.hostname;
const port = window.location.port;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const endpoint =
window.location.protocol === 'file:'
? fileProtocolWssUrl
: `${protocol}//${host}${port ? ':' : ''}${port}/api/ws`;
const ws = await asyncInitWebSocket(endpoint);
try {
const metadataHeader = arrayToB64(new Uint8Array(metadata));
const fileMeta = {
fileMetadata: metadataHeader,
authorization: `send-v1 ${verifierB64}`,
bearer: bearerToken,
timeLimit,
dlimit
};
const uploadInfoResponse = listenForResponse(ws, canceller);
ws.send(JSON.stringify(fileMeta));
const uploadInfo = await uploadInfoResponse;
const completedResponse = listenForResponse(ws, canceller);
const reader = stream.getReader();
let state = await reader.read();
while (!state.done) {
if (canceller.cancelled) {
ws.close();
}
if (ws.readyState !== WebSocket.OPEN) {
break;
}
const buf = state.value;
ws.send(buf);
onprogress(size);
size += buf.length;
state = await reader.read();
while (
ws.bufferedAmount > ECE_RECORD_SIZE * 2 &&
ws.readyState === WebSocket.OPEN &&
!canceller.cancelled
) {
await delay();
}
}
if (ws.readyState === WebSocket.OPEN) {
ws.send(new Uint8Array([0])); //EOF
}
await completedResponse;
uploadInfo.duration = Date.now() - start;
return uploadInfo;
} catch (e) {
e.size = size;
e.duration = Date.now() - start;
throw e;
} finally {
if (![WebSocket.CLOSED, WebSocket.CLOSING].includes(ws.readyState)) {
ws.close();
}
}
}
export function uploadWs(
encrypted,
metadata,
verifierB64,
timeLimit,
dlimit,
bearerToken,
onprogress
) {
const canceller = { cancelled: false };
return {
cancel: function() {
canceller.cancelled = true;
},
result: upload(
encrypted,
metadata,
verifierB64,
timeLimit,
dlimit,
bearerToken,
onprogress,
canceller
)
};
}
////////////////////////
async function _downloadStream(id, dlToken, signal) {
const response = await fetch(getApiUrl(`/api/download/${id}`), {
signal: signal,
method: 'GET',
headers: { Authorization: `Bearer ${dlToken}` }
});
if (response.status !== 200) {
throw new Error(response.status);
}
return response.body;
}
async function tryDownloadStream(id, dlToken, signal, tries = 2) {
try {
const result = await _downloadStream(id, dlToken, signal);
return result;
} catch (e) {
if (e.message === '401' && --tries > 0) {
return tryDownloadStream(id, dlToken, signal, tries);
}
if (e.name === 'AbortError') {
throw new Error('0');
}
throw e;
}
}
export function downloadStream(id, dlToken) {
const controller = new AbortController();
function cancel() {
controller.abort();
}
return {
cancel,
result: tryDownloadStream(id, dlToken, controller.signal)
};
}
//////////////////
async function download(id, dlToken, onprogress, canceller) {
const xhr = new XMLHttpRequest();
canceller.oncancel = function() {
xhr.abort();
};
return new Promise(function(resolve, reject) {
xhr.addEventListener('loadend', function() {
canceller.oncancel = function() {};
if (xhr.status !== 200) {
return reject(new Error(xhr.status));
}
const blob = new Blob([xhr.response]);
resolve(blob);
});
xhr.addEventListener('progress', function(event) {
if (event.target.status === 200) {
onprogress(event.loaded);
}
});
xhr.open('get', getApiUrl(`/api/download/blob/${id}`));
xhr.setRequestHeader('Authorization', `Bearer ${dlToken}`);
xhr.responseType = 'blob';
xhr.send();
onprogress(0);
});
}
async function tryDownload(id, dlToken, onprogress, canceller, tries = 2) {
try {
const result = await download(id, dlToken, onprogress, canceller);
return result;
} catch (e) {
if (e.message === '401' && --tries > 0) {
return tryDownload(id, dlToken, onprogress, canceller, tries);
}
throw e;
}
}
export function downloadFile(id, dlToken, onprogress) {
const canceller = {
oncancel: function() {} // download() sets this
};
function cancel() {
canceller.oncancel();
}
return {
cancel,
result: tryDownload(id, dlToken, onprogress, canceller)
};
}
export async function getFileList(bearerToken, kid) {
const headers = new Headers({ Authorization: `Bearer ${bearerToken}` });
const response = await fetch(getApiUrl(`/api/filelist/${kid}`), { headers });
if (response.ok) {
const encrypted = await response.blob();
return encrypted;
}
throw new Error(response.status);
}
export async function setFileList(bearerToken, kid, data) {
const headers = new Headers({ Authorization: `Bearer ${bearerToken}` });
const response = await fetch(getApiUrl(`/api/filelist/${kid}`), {
headers,
method: 'POST',
body: data
});
return response.ok;
}
export function sendMetrics(blob) {
if (!navigator.sendBeacon) {
return;
}
try {
navigator.sendBeacon(getApiUrl('/api/metrics'), blob);
} catch (e) {
console.error(e);
}
}
export async function getConstants() {
const response = await fetch(getApiUrl('/config'));
if (response.ok) {
const obj = await response.json();
return obj;
}
throw new Error(response.status);
}
export async function reportLink(id, keychain, reason) {
const result = await fetchWithAuthAndRetry(
getApiUrl(`/api/report/${id}`),
{
method: 'POST',
body: JSON.stringify({ reason })
},
keychain
);
if (result.ok) {
return;
}
throw new Error(result.response.status);
}
export async function getDownloadToken(id, keychain) {
const result = await fetchWithAuthAndRetry(
getApiUrl(`/api/download/token/${id}`),
{
method: 'GET'
},
keychain
);
if (result.ok) {
return (await result.response.json()).token;
}
throw new Error(result.response.status);
}
export async function downloadDone(id, dlToken) {
const headers = new Headers({ Authorization: `Bearer ${dlToken}` });
const response = await fetch(getApiUrl(`/api/download/done/${id}`), {
headers,
method: 'POST'
});
return response.ok;
}
================================================
FILE: app/archive.js
================================================
import { blobStream, concatStream } from './streams';
function isDupe(newFile, array) {
for (const file of array) {
if (
newFile.name === file.name &&
newFile.size === file.size &&
newFile.lastModified === file.lastModified
) {
return true;
}
}
return false;
}
export default class Archive {
constructor(files = [], defaultTimeLimit = 86400) {
this.files = Array.from(files);
this.defaultTimeLimit = defaultTimeLimit;
this.timeLimit = defaultTimeLimit;
this.dlimit = 1;
this.password = null;
}
get name() {
return this.files.length > 1 ? 'Send-Archive.zip' : this.files[0].name;
}
get type() {
return this.files.length > 1 ? 'send-archive' : this.files[0].type;
}
get size() {
return this.files.reduce((total, file) => total + file.size, 0);
}
get numFiles() {
return this.files.length;
}
get manifest() {
return {
files: this.files.map(file => ({
name: file.name,
size: file.size,
type: file.type
}))
};
}
get stream() {
return concatStream(this.files.map(file => blobStream(file)));
}
addFiles(files, maxSize, maxFiles) {
if (this.files.length + files.length > maxFiles) {
throw new Error('tooManyFiles');
}
const newFiles = files.filter(
file => file.size > 0 && !isDupe(file, this.files)
);
const newSize = newFiles.reduce((total, file) => total + file.size, 0);
if (this.size + newSize > maxSize) {
throw new Error('fileTooBig');
}
this.files = this.files.concat(newFiles);
return true;
}
remove(file) {
const index = this.files.indexOf(file);
if (index > -1) {
this.files.splice(index, 1);
}
}
clear() {
this.files = [];
this.dlimit = 1;
this.timeLimit = this.defaultTimeLimit;
this.password = null;
}
}
================================================
FILE: app/capabilities.js
================================================
/* global AUTH_CONFIG */
import { browserName, locale } from './utils';
async function checkCrypto() {
try {
const key = await crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 128
},
true,
['encrypt', 'decrypt']
);
await crypto.subtle.exportKey('raw', key);
await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: crypto.getRandomValues(new Uint8Array(12)),
tagLength: 128
},
key,
new ArrayBuffer(8)
);
await crypto.subtle.importKey(
'raw',
crypto.getRandomValues(new Uint8Array(16)),
'PBKDF2',
false,
['deriveKey']
);
await crypto.subtle.importKey(
'raw',
crypto.getRandomValues(new Uint8Array(16)),
'HKDF',
false,
['deriveKey']
);
await crypto.subtle.generateKey(
{
name: 'ECDH',
namedCurve: 'P-256'
},
true,
['deriveBits']
);
return true;
} catch (err) {
try {
window.asmCrypto = await import('asmcrypto.js');
await import('@dannycoates/webcrypto-liner/build/shim');
return true;
} catch (e) {
return false;
}
}
}
function checkStreams() {
try {
new ReadableStream({
pull() {}
});
return true;
} catch (e) {
return false;
}
}
async function polyfillStreams() {
try {
await import('@mattiasbuelens/web-streams-polyfill');
return true;
} catch (e) {
return false;
}
}
export default async function getCapabilities() {
const browser = browserName();
const isMobile = /mobi|android/i.test(navigator.userAgent);
const serviceWorker = 'serviceWorker' in navigator && browser !== 'edge';
let crypto = await checkCrypto();
const nativeStreams = checkStreams();
let polyStreams = false;
if (!nativeStreams) {
polyStreams = await polyfillStreams();
}
let account = typeof AUTH_CONFIG !== 'undefined';
try {
account = account && !!localStorage;
} catch (e) {
account = false;
}
const share =
isMobile &&
typeof navigator.share === 'function' &&
locale().startsWith('en'); // en until strings merge
const standalone =
window.matchMedia('(display-mode: standalone)').matches ||
navigator.standalone;
const mobileFirefox = browser === 'firefox' && isMobile;
return {
account,
crypto,
serviceWorker,
streamUpload: nativeStreams || polyStreams,
streamDownload:
nativeStreams && serviceWorker && browser !== 'safari' && !mobileFirefox,
multifile: nativeStreams || polyStreams,
share,
standalone
};
}
================================================
FILE: app/controller.js
================================================
import FileSender from './fileSender';
import FileReceiver from './fileReceiver';
import { copyToClipboard, delay, openLinksInNewTab, percent } from './utils';
import * as metrics from './metrics';
import { bytes, locale } from './utils';
import okDialog from './ui/okDialog';
import copyDialog from './ui/copyDialog';
import shareDialog from './ui/shareDialog';
export default function(state, emitter) {
let lastRender = 0;
let updateTitle = false;
function render() {
emitter.emit('render');
}
async function checkFiles() {
const changes = await state.user.syncFileList();
const rerender = changes.incoming || changes.downloadCount;
if (rerender) {
render();
}
}
function updateProgress() {
if (updateTitle) {
emitter.emit('DOMTitleChange', percent(state.transfer.progressRatio));
}
render();
}
emitter.on('DOMContentLoaded', () => {
document.addEventListener('blur', () => (updateTitle = true));
document.addEventListener('focus', () => {
updateTitle = false;
emitter.emit('DOMTitleChange', 'Send');
});
checkFiles();
});
emitter.on('render', () => {
lastRender = Date.now();
});
emitter.on('login', email => {
state.user.login(email);
});
emitter.on('logout', async () => {
await state.user.logout();
metrics.loggedOut({ trigger: 'button' });
emitter.emit('pushState', '/');
});
emitter.on('removeUpload', file => {
state.archive.remove(file);
if (state.archive.numFiles === 0) {
state.archive.clear();
}
render();
});
emitter.on('delete', async ownedFile => {
try {
metrics.deletedUpload({
size: ownedFile.size,
time: ownedFile.time,
speed: ownedFile.speed,
type: ownedFile.type,
ttl: ownedFile.expiresAt - Date.now(),
location
});
state.storage.remove(ownedFile.id);
await ownedFile.del();
} catch (e) {
state.sentry.captureException(e);
}
render();
});
emitter.on('cancel', () => {
state.transfer.cancel();
});
emitter.on('addFiles', async ({ files }) => {
if (files.length < 1) {
return;
}
const maxSize = state.user.maxSize;
try {
state.archive.addFiles(
files,
maxSize,
state.LIMITS.MAX_FILES_PER_ARCHIVE
);
} catch (e) {
if (e.message === 'fileTooBig' && maxSize < state.LIMITS.MAX_FILE_SIZE) {
return emitter.emit('signup-cta', 'size');
}
state.modal = okDialog(
state.translate(e.message, {
size: bytes(maxSize),
count: state.LIMITS.MAX_FILES_PER_ARCHIVE
})
);
}
render();
});
emitter.on('authenticate', async (code, oauthState) => {
try {
await state.user.finishLogin(code, oauthState);
await state.user.syncFileList();
emitter.emit('replaceState', '/');
} catch (e) {
emitter.emit('replaceState', '/error');
setTimeout(render);
}
});
emitter.on('upload', async () => {
if (state.storage.files.length >= state.LIMITS.MAX_ARCHIVES_PER_USER) {
state.modal = okDialog(
state.translate('tooManyArchives', {
count: state.LIMITS.MAX_ARCHIVES_PER_USER
})
);
return render();
}
const archive = state.archive;
const sender = new FileSender();
sender.on('progress', updateProgress);
sender.on('encrypting', render);
sender.on('complete', render);
state.transfer = sender;
state.uploading = true;
render();
const links = openLinksInNewTab();
await delay(200);
const start = Date.now();
try {
const ownedFile = await sender.upload(archive, state.user.bearerToken);
state.storage.totalUploads += 1;
const duration = Date.now() - start;
metrics.completedUpload(archive, duration);
state.storage.addFile(ownedFile);
// TODO integrate password into /upload request
if (archive.password) {
emitter.emit('password', {
password: archive.password,
file: ownedFile
});
}
state.modal = state.capabilities.share
? shareDialog(ownedFile.name, ownedFile.url)
: copyDialog(ownedFile.name, ownedFile.url);
} catch (err) {
if (err.message === '0') {
//cancelled. do nothing
metrics.cancelledUpload(archive, err.duration);
render();
} else if (err.message === '401') {
const refreshed = await state.user.refresh();
if (refreshed) {
return emitter.emit('upload');
}
emitter.emit('pushState', '/error');
} else {
// eslint-disable-next-line no-console
console.error(err);
state.sentry.withScope(scope => {
scope.setExtra('duration', err.duration);
scope.setExtra('size', err.size);
state.sentry.captureException(err);
});
metrics.stoppedUpload(archive, err.duration);
emitter.emit('pushState', '/error');
}
} finally {
openLinksInNewTab(links, false);
archive.clear();
state.uploading = false;
state.transfer = null;
await state.user.syncFileList();
render();
}
});
emitter.on('password', async ({ password, file }) => {
try {
state.settingPassword = true;
render();
await file.setPassword(password);
state.storage.writeFile(file);
await delay(1000);
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
state.passwordSetError = err;
} finally {
state.settingPassword = false;
}
render();
});
emitter.on('getMetadata', async () => {
const file = state.fileInfo;
const receiver = new FileReceiver(file);
try {
await receiver.getMetadata();
state.transfer = receiver;
} catch (e) {
if (e.message === '401' || e.message === '404') {
file.password = null;
file.dead = e.message === '404';
} else {
console.error(e);
return emitter.emit('pushState', '/error');
}
}
render();
});
emitter.on('download', async file => {
state.transfer.on('progress', updateProgress);
state.transfer.on('decrypting', render);
state.transfer.on('complete', render);
const links = openLinksInNewTab();
const size = file.size;
const start = Date.now();
try {
const dl = state.transfer.download({
stream: state.capabilities.streamDownload,
storage: state.storage
});
render();
await dl;
state.storage.totalDownloads += 1;
const duration = Date.now() - start;
metrics.completedDownload({
size,
duration,
password_protected: file.requiresPassword
});
} catch (err) {
if (err.message === '0') {
// download cancelled
state.transfer.reset();
render();
} else {
// eslint-disable-next-line no-console
state.transfer = null;
const location = ['404', '403'].includes(err.message)
? '/404'
: '/error';
if (location === '/error') {
state.sentry.withScope(scope => {
scope.setExtra('duration', err.duration);
scope.setExtra('size', err.size);
scope.setExtra('progress', err.progress);
state.sentry.captureException(err);
});
const duration = Date.now() - start;
metrics.stoppedDownload({
size,
duration,
password_protected: file.requiresPassword
});
}
emitter.emit('pushState', location);
}
} finally {
openLinksInNewTab(links, false);
}
});
emitter.on('copy', ({ url }) => {
copyToClipboard(url);
// metrics.copiedLink({ location });
});
emitter.on('closeModal', () => {
if (
state.PREFS.surveyUrl &&
['copy', 'share'].includes(state.modal.type) &&
locale().startsWith('en') &&
(state.storage.totalUploads > 1 || state.storage.totalDownloads > 0) &&
!state.user.surveyed
) {
state.user.surveyed = true;
// state.modal = surveyDialog();
} else {
state.modal = null;
}
render();
});
emitter.on('report', async ({ reason }) => {
try {
const receiver = state.transfer || new FileReceiver(state.fileInfo);
await receiver.reportLink(reason);
render();
} catch (err) {
console.error(err);
if (err.message === '404') {
state.fileInfo = { reported: true };
return render();
}
emitter.emit('pushState', '/error');
}
});
setInterval(() => {
// poll for updates of the upload list
if (!state.modal && state.route === '/') {
checkFiles();
}
}, 2 * 60 * 1000);
setInterval(() => {
// poll for rerendering the file list countdown timers
if (
!state.modal &&
state.route === '/' &&
state.storage.files.length > 0 &&
Date.now() - lastRender > 30000
) {
render();
}
}, 60000);
}
================================================
FILE: app/crc32.js
================================================
const LOOKUP = Int32Array.from([
0x00000000,
0x77073096,
0xee0e612c,
0x990951ba,
0x076dc419,
0x706af48f,
0xe963a535,
0x9e6495a3,
0x0edb8832,
0x79dcb8a4,
0xe0d5e91e,
0x97d2d988,
0x09b64c2b,
0x7eb17cbd,
0xe7b82d07,
0x90bf1d91,
0x1db71064,
0x6ab020f2,
0xf3b97148,
0x84be41de,
0x1adad47d,
0x6ddde4eb,
0xf4d4b551,
0x83d385c7,
0x136c9856,
0x646ba8c0,
0xfd62f97a,
0x8a65c9ec,
0x14015c4f,
0x63066cd9,
0xfa0f3d63,
0x8d080df5,
0x3b6e20c8,
0x4c69105e,
0xd56041e4,
0xa2677172,
0x3c03e4d1,
0x4b04d447,
0xd20d85fd,
0xa50ab56b,
0x35b5a8fa,
0x42b2986c,
0xdbbbc9d6,
0xacbcf940,
0x32d86ce3,
0x45df5c75,
0xdcd60dcf,
0xabd13d59,
0x26d930ac,
0x51de003a,
0xc8d75180,
0xbfd06116,
0x21b4f4b5,
0x56b3c423,
0xcfba9599,
0xb8bda50f,
0x2802b89e,
0x5f058808,
0xc60cd9b2,
0xb10be924,
0x2f6f7c87,
0x58684c11,
0xc1611dab,
0xb6662d3d,
0x76dc4190,
0x01db7106,
0x98d220bc,
0xefd5102a,
0x71b18589,
0x06b6b51f,
0x9fbfe4a5,
0xe8b8d433,
0x7807c9a2,
0x0f00f934,
0x9609a88e,
0xe10e9818,
0x7f6a0dbb,
0x086d3d2d,
0x91646c97,
0xe6635c01,
0x6b6b51f4,
0x1c6c6162,
0x856530d8,
0xf262004e,
0x6c0695ed,
0x1b01a57b,
0x8208f4c1,
0xf50fc457,
0x65b0d9c6,
0x12b7e950,
0x8bbeb8ea,
0xfcb9887c,
0x62dd1ddf,
0x15da2d49,
0x8cd37cf3,
0xfbd44c65,
0x4db26158,
0x3ab551ce,
0xa3bc0074,
0xd4bb30e2,
0x4adfa541,
0x3dd895d7,
0xa4d1c46d,
0xd3d6f4fb,
0x4369e96a,
0x346ed9fc,
0xad678846,
0xda60b8d0,
0x44042d73,
0x33031de5,
0xaa0a4c5f,
0xdd0d7cc9,
0x5005713c,
0x270241aa,
0xbe0b1010,
0xc90c2086,
0x5768b525,
0x206f85b3,
0xb966d409,
0xce61e49f,
0x5edef90e,
0x29d9c998,
0xb0d09822,
0xc7d7a8b4,
0x59b33d17,
0x2eb40d81,
0xb7bd5c3b,
0xc0ba6cad,
0xedb88320,
0x9abfb3b6,
0x03b6e20c,
0x74b1d29a,
0xead54739,
0x9dd277af,
0x04db2615,
0x73dc1683,
0xe3630b12,
0x94643b84,
0x0d6d6a3e,
0x7a6a5aa8,
0xe40ecf0b,
0x9309ff9d,
0x0a00ae27,
0x7d079eb1,
0xf00f9344,
0x8708a3d2,
0x1e01f268,
0x6906c2fe,
0xf762575d,
0x806567cb,
0x196c3671,
0x6e6b06e7,
0xfed41b76,
0x89d32be0,
0x10da7a5a,
0x67dd4acc,
0xf9b9df6f,
0x8ebeeff9,
0x17b7be43,
0x60b08ed5,
0xd6d6a3e8,
0xa1d1937e,
0x38d8c2c4,
0x4fdff252,
0xd1bb67f1,
0xa6bc5767,
0x3fb506dd,
0x48b2364b,
0xd80d2bda,
0xaf0a1b4c,
0x36034af6,
0x41047a60,
0xdf60efc3,
0xa867df55,
0x316e8eef,
0x4669be79,
0xcb61b38c,
0xbc66831a,
0x256fd2a0,
0x5268e236,
0xcc0c7795,
0xbb0b4703,
0x220216b9,
0x5505262f,
0xc5ba3bbe,
0xb2bd0b28,
0x2bb45a92,
0x5cb36a04,
0xc2d7ffa7,
0xb5d0cf31,
0x2cd99e8b,
0x5bdeae1d,
0x9b64c2b0,
0xec63f226,
0x756aa39c,
0x026d930a,
0x9c0906a9,
0xeb0e363f,
0x72076785,
0x05005713,
0x95bf4a82,
0xe2b87a14,
0x7bb12bae,
0x0cb61b38,
0x92d28e9b,
0xe5d5be0d,
0x7cdcefb7,
0x0bdbdf21,
0x86d3d2d4,
0xf1d4e242,
0x68ddb3f8,
0x1fda836e,
0x81be16cd,
0xf6b9265b,
0x6fb077e1,
0x18b74777,
0x88085ae6,
0xff0f6a70,
0x66063bca,
0x11010b5c,
0x8f659eff,
0xf862ae69,
0x616bffd3,
0x166ccf45,
0xa00ae278,
0xd70dd2ee,
0x4e048354,
0x3903b3c2,
0xa7672661,
0xd06016f7,
0x4969474d,
0x3e6e77db,
0xaed16a4a,
0xd9d65adc,
0x40df0b66,
0x37d83bf0,
0xa9bcae53,
0xdebb9ec5,
0x47b2cf7f,
0x30b5ffe9,
0xbdbdf21c,
0xcabac28a,
0x53b39330,
0x24b4a3a6,
0xbad03605,
0xcdd70693,
0x54de5729,
0x23d967bf,
0xb3667a2e,
0xc4614ab8,
0x5d681b02,
0x2a6f2b94,
0xb40bbe37,
0xc30c8ea1,
0x5a05df1b,
0x2d02ef8d
]);
module.exports = function crc32(uint8Array, previous) {
let crc = previous === 0 ? 0 : ~~previous ^ -1;
for (let i = 0; i < uint8Array.byteLength; i++) {
crc = LOOKUP[(crc ^ uint8Array[i]) & 0xff] ^ (crc >>> 8);
}
return (crc ^ -1) >>> 0;
};
================================================
FILE: app/dragManager.js
================================================
export default function(state, emitter) {
emitter.on('DOMContentLoaded', () => {
document.body.addEventListener('dragover', event => {
if (state.route === '/') {
event.preventDefault();
}
});
document.body.addEventListener('drop', event => {
if (
state.route === '/' &&
!state.uploading &&
event.dataTransfer &&
event.dataTransfer.files
) {
event.preventDefault();
emitter.emit('addFiles', {
files: Array.from(event.dataTransfer.files)
});
}
});
});
}
================================================
FILE: app/ece.js
================================================
import { transformStream } from './streams';
import { concat } from './utils';
const NONCE_LENGTH = 12;
const TAG_LENGTH = 16;
const KEY_LENGTH = 16;
const MODE_ENCRYPT = 'encrypt';
const MODE_DECRYPT = 'decrypt';
export const ECE_RECORD_SIZE = 1024 * 64;
const encoder = new TextEncoder();
function generateSalt(len) {
const randSalt = new Uint8Array(len);
crypto.getRandomValues(randSalt);
return randSalt.buffer;
}
class ECETransformer {
constructor(mode, ikm, rs, salt) {
this.mode = mode;
this.prevChunk;
this.seq = 0;
this.firstchunk = true;
this.rs = rs;
this.ikm = ikm.buffer;
this.salt = salt;
}
async generateKey() {
const inputKey = await crypto.subtle.importKey(
'raw',
this.ikm,
'HKDF',
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: this.salt,
info: encoder.encode('Content-Encoding: aes128gcm\0'),
hash: 'SHA-256'
},
inputKey,
{
name: 'AES-GCM',
length: 128
},
true, // Edge polyfill requires key to be extractable to encrypt :/
['encrypt', 'decrypt']
);
}
async generateNonceBase() {
const inputKey = await crypto.subtle.importKey(
'raw',
this.ikm,
'HKDF',
false,
['deriveKey']
);
const base = await crypto.subtle.exportKey(
'raw',
await crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: this.salt,
info: encoder.encode('Content-Encoding: nonce\0'),
hash: 'SHA-256'
},
inputKey,
{
name: 'AES-GCM',
length: 128
},
true,
['encrypt', 'decrypt']
)
);
return base.slice(0, NONCE_LENGTH);
}
generateNonce(seq) {
if (seq > 0xffffffff) {
throw new Error('record sequence number exceeds limit');
}
const nonce = new DataView(this.nonceBase.slice());
const m = nonce.getUint32(nonce.byteLength - 4);
const xor = (m ^ seq) >>> 0; //forces unsigned int xor
nonce.setUint32(nonce.byteLength - 4, xor);
return new Uint8Array(nonce.buffer);
}
pad(data, isLast) {
const len = data.length;
if (len + TAG_LENGTH >= this.rs) {
throw new Error('data too large for record size');
}
if (isLast) {
return concat(data, Uint8Array.of(2));
} else {
const padding = new Uint8Array(this.rs - len - TAG_LENGTH);
padding[0] = 1;
return concat(data, padding);
}
}
unpad(data, isLast) {
for (let i = data.length - 1; i >= 0; i--) {
if (data[i]) {
if (isLast) {
if (data[i] !== 2) {
throw new Error('delimiter of final record is not 2');
}
} else {
if (data[i] !== 1) {
throw new Error('delimiter of not final record is not 1');
}
}
return data.slice(0, i);
}
}
throw new Error('no delimiter found');
}
createHeader() {
const nums = new DataView(new ArrayBuffer(5));
nums.setUint32(0, this.rs);
return concat(new Uint8Array(this.salt), new Uint8Array(nums.buffer));
}
readHeader(buffer) {
if (buffer.length < 21) {
throw new Error('chunk too small for reading header');
}
const header = {};
const dv = new DataView(buffer.buffer);
header.salt = buffer.slice(0, KEY_LENGTH);
header.rs = dv.getUint32(KEY_LENGTH);
const idlen = dv.getUint8(KEY_LENGTH + 4);
header.length = idlen + KEY_LENGTH + 5;
return header;
}
async encryptRecord(buffer, seq, isLast) {
const nonce = this.generateNonce(seq);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: nonce },
this.key,
this.pad(buffer, isLast)
);
return new Uint8Array(encrypted);
}
async decryptRecord(buffer, seq, isLast) {
const nonce = this.generateNonce(seq);
const data = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: nonce,
tagLength: 128
},
this.key,
buffer
);
return this.unpad(new Uint8Array(data), isLast);
}
async start(controller) {
if (this.mode === MODE_ENCRYPT) {
this.key = await this.generateKey();
this.nonceBase = await this.generateNonceBase();
controller.enqueue(this.createHeader());
} else if (this.mode !== MODE_DECRYPT) {
throw new Error('mode must be either encrypt or decrypt');
}
}
async transformPrevChunk(isLast, controller) {
if (this.mode === MODE_ENCRYPT) {
controller.enqueue(
await this.encryptRecord(this.prevChunk, this.seq, isLast)
);
this.seq++;
} else {
if (this.seq === 0) {
//the first chunk during decryption contains only the header
const header = this.readHeader(this.prevChunk);
this.salt = header.salt;
this.rs = header.rs;
this.key = await this.generateKey();
this.nonceBase = await this.generateNonceBase();
} else {
controller.enqueue(
await this.decryptRecord(this.prevChunk, this.seq - 1, isLast)
);
}
this.seq++;
}
}
async transform(chunk, controller) {
if (!this.firstchunk) {
await this.transformPrevChunk(false, controller);
}
this.firstchunk = false;
this.prevChunk = new Uint8Array(chunk.buffer);
}
async flush(controller) {
//console.log('ece stream ends')
if (this.prevChunk) {
await this.transformPrevChunk(true, controller);
}
}
}
class StreamSlicer {
constructor(rs, mode) {
this.mode = mode;
this.rs = rs;
this.chunkSize = mode === MODE_ENCRYPT ? rs - 17 : 21;
this.partialChunk = new Uint8Array(this.chunkSize); //where partial chunks are saved
this.offset = 0;
}
send(buf, controller) {
controller.enqueue(buf);
if (this.chunkSize === 21 && this.mode === MODE_DECRYPT) {
this.chunkSize = this.rs;
}
this.partialChunk = new Uint8Array(this.chunkSize);
this.offset = 0;
}
//reslice input into record sized chunks
transform(chunk, controller) {
//console.log('Received chunk with %d bytes.', chunk.byteLength)
let i = 0;
if (this.offset > 0) {
const len = Math.min(chunk.byteLength, this.chunkSize - this.offset);
this.partialChunk.set(chunk.slice(0, len), this.offset);
this.offset += len;
i += len;
if (this.offset === this.chunkSize) {
this.send(this.partialChunk, controller);
}
}
while (i < chunk.byteLength) {
const remainingBytes = chunk.byteLength - i;
if (remainingBytes >= this.chunkSize) {
const record = chunk.slice(i, i + this.chunkSize);
i += this.chunkSize;
this.send(record, controller);
} else {
const end = chunk.slice(i, i + remainingBytes);
i += end.byteLength;
this.partialChunk.set(end);
this.offset = end.byteLength;
}
}
}
flush(controller) {
if (this.offset > 0) {
controller.enqueue(this.partialChunk.slice(0, this.offset));
}
}
}
/*
input: a ReadableStream containing data to be transformed
key: Uint8Array containing key of size KEY_LENGTH
rs: int containing record size, optional
salt: ArrayBuffer containing salt of KEY_LENGTH length, optional
*/
export function encryptStream(
input,
key,
rs = ECE_RECORD_SIZE,
salt = generateSalt(KEY_LENGTH)
) {
const mode = 'encrypt';
const inputStream = transformStream(input, new StreamSlicer(rs, mode));
return transformStream(inputStream, new ECETransformer(mode, key, rs, salt));
}
/*
input: a ReadableStream containing data to be transformed
key: Uint8Array containing key of size KEY_LENGTH
rs: int containing record size, optional
*/
export function decryptStream(input, key, rs = ECE_RECORD_SIZE) {
const mode = 'decrypt';
const inputStream = transformStream(input, new StreamSlicer(rs, mode));
return transformStream(inputStream, new ECETransformer(mode, key, rs));
}
================================================
FILE: app/experiments.js
================================================
import hash from 'string-hash';
import Account from './ui/account';
const experiments = {
signin_button_color: {
eligible: function() {
return true;
},
variant: function() {
return ['white-blue', 'blue', 'white-violet', 'violet'][
Math.floor(Math.random() * 4)
];
},
run: function(variant, state) {
const account = state.cache(Account, 'account');
account.buttonClass = variant;
}
}
};
//Returns a number between 0 and 1
// eslint-disable-next-line no-unused-vars
function luckyNumber(str) {
return hash(str) / 0xffffffff;
}
function checkExperiments(state, emitter) {
const all = Object.keys(experiments);
const id = all.find(id => experiments[id].eligible(state));
if (id) {
const variant = experiments[id].variant(state);
state.storage.enroll(id, variant);
experiments[id].run(variant, state, emitter);
}
}
export default function initialize(state, emitter) {
emitter.on('DOMContentLoaded', () => {
const xp = experiments[state.query.x];
if (xp) {
xp.run(+state.query.v, state, emitter);
}
});
const enrolled = state.storage.enrolled;
// single experiment per session for now
const id = Object.keys(enrolled)[0];
if (Object.keys(experiments).includes(id)) {
experiments[id].run(enrolled[id], state, emitter);
} else {
checkExperiments(state, emitter);
}
}
================================================
FILE: app/fileReceiver.js
================================================
import Nanobus from 'nanobus';
import Keychain from './keychain';
import { delay, bytes, streamToArrayBuffer } from './utils';
import {
downloadFile,
downloadDone,
metadata,
getApiUrl,
reportLink,
getDownloadToken
} from './api';
import { blobStream } from './streams';
import Zip from './zip';
export default class FileReceiver extends Nanobus {
constructor(fileInfo) {
super('FileReceiver');
this.keychain = new Keychain(fileInfo.secretKey, fileInfo.nonce);
if (fileInfo.requiresPassword) {
this.keychain.setPassword(fileInfo.password, fileInfo.url);
}
this.fileInfo = fileInfo;
this.dlToken = null;
this.reset();
}
get id() {
return this.fileInfo.id;
}
get progressRatio() {
return this.progress[0] / this.progress[1];
}
get progressIndefinite() {
return this.state !== 'downloading';
}
get sizes() {
return {
partialSize: bytes(this.progress[0]),
totalSize: bytes(this.progress[1])
};
}
cancel() {
if (this.downloadRequest) {
this.downloadRequest.cancel();
}
}
reset() {
this.msg = 'fileSizeProgress';
this.state = 'initialized';
this.progress = [0, 1];
}
async getMetadata() {
const meta = await metadata(this.fileInfo.id, this.keychain);
this.fileInfo.name = meta.name;
this.fileInfo.type = meta.type;
this.fileInfo.size = +meta.size;
this.fileInfo.manifest = meta.manifest;
this.fileInfo.flagged = meta.flagged;
this.state = 'ready';
}
async reportLink(reason) {
await reportLink(this.fileInfo.id, this.keychain, reason);
}
sendMessageToSw(msg) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = function(event) {
if (event.data === undefined) {
reject('bad response from serviceWorker');
} else if (event.data.error !== undefined) {
reject(event.data.error);
} else {
resolve(event.data);
}
};
navigator.serviceWorker.controller.postMessage(msg, [channel.port2]);
});
}
async downloadBlob(noSave = false) {
this.state = 'downloading';
this.downloadRequest = await downloadFile(
this.fileInfo.id,
this.dlToken,
p => {
this.progress = [p, this.fileInfo.size];
this.emit('progress');
}
);
try {
const ciphertext = await this.downloadRequest.result;
this.downloadRequest = null;
this.msg = 'decryptingFile';
this.state = 'decrypting';
this.emit('decrypting');
let size = this.fileInfo.size;
let plainStream = this.keychain.decryptStream(blobStream(ciphertext));
if (this.fileInfo.type === 'send-archive') {
const zip = new Zip(this.fileInfo.manifest, plainStream);
plainStream = zip.stream;
size = zip.size;
}
const plaintext = await streamToArrayBuffer(plainStream, size);
if (!noSave) {
await saveFile({
plaintext,
name: decodeURIComponent(this.fileInfo.name),
type: this.fileInfo.type
});
}
this.msg = 'downloadFinish';
this.emit('complete');
this.state = 'complete';
} catch (e) {
this.downloadRequest = null;
throw e;
}
}
async downloadStream(noSave = false) {
const start = Date.now();
const onprogress = p => {
this.progress = [p, this.fileInfo.size];
this.emit('progress');
};
this.downloadRequest = {
cancel: () => {
this.sendMessageToSw({ request: 'cancel', id: this.fileInfo.id });
}
};
try {
this.state = 'downloading';
const info = {
request: 'init',
id: this.fileInfo.id,
filename: this.fileInfo.name,
type: this.fileInfo.type,
manifest: this.fileInfo.manifest,
key: this.fileInfo.secretKey,
requiresPassword: this.fileInfo.requiresPassword,
password: this.fileInfo.password,
url: this.fileInfo.url,
size: this.fileInfo.size,
nonce: this.keychain.nonce,
dlToken: this.dlToken,
noSave
};
await this.sendMessageToSw(info);
onprogress(0);
if (noSave) {
const res = await fetch(getApiUrl(`/api/download/${this.fileInfo.id}`));
if (res.status !== 200) {
throw new Error(res.status);
}
} else {
const downloadPath = `/api/download/${this.fileInfo.id}`;
let downloadUrl = getApiUrl(downloadPath);
if (downloadUrl === downloadPath) {
downloadUrl = `${location.protocol}//${location.host}${downloadPath}`;
}
const a = document.createElement('a');
a.href = downloadUrl;
document.body.appendChild(a);
a.click();
}
let prog = 0;
let hangs = 0;
while (prog < this.fileInfo.size) {
const msg = await this.sendMessageToSw({
request: 'progress',
id: this.fileInfo.id
});
if (msg.progress === prog) {
hangs++;
} else {
hangs = 0;
}
if (hangs > 30) {
// TODO: On Chrome we don't get a cancel
// signal so one is indistinguishable from
// a hang. We may be able to detect
// which end is hung in the service worker
// to improve on this.
const e = new Error('hung download');
e.duration = Date.now() - start;
e.size = this.fileInfo.size;
e.progress = prog;
throw e;
}
prog = msg.progress;
onprogress(prog);
await delay(1000);
}
this.downloadRequest = null;
this.msg = 'downloadFinish';
this.emit('complete');
this.state = 'complete';
} catch (e) {
this.downloadRequest = null;
if (e === 'cancelled' || e.message === '400') {
throw new Error(0);
}
throw e;
}
}
async download({ stream, storage, noSave }) {
this.dlToken = storage.getDownloadToken(this.id);
if (!this.dlToken) {
this.dlToken = await getDownloadToken(this.id, this.keychain);
storage.setDownloadToken(this.id, this.dlToken);
}
if (stream) {
await this.downloadStream(noSave);
} else {
await this.downloadBlob(noSave);
}
await downloadDone(this.id, this.dlToken);
storage.setDownloadToken(this.id);
}
}
async function saveFile(file) {
return new Promise(function(resolve, reject) {
const dataView = new DataView(file.plaintext);
const blob = new Blob([dataView], { type: file.type });
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, file.name);
return resolve();
} else if (/iPhone|fxios/i.test(navigator.userAgent)) {
// This method is much slower but createObjectURL
// is buggy on iOS
const reader = new FileReader();
reader.addEventListener('loadend', function() {
if (reader.error) {
return reject(reader.error);
}
if (reader.result) {
const a = document.createElement('a');
a.href = reader.result;
a.download = file.name;
document.body.appendChild(a);
a.click();
}
resolve();
});
reader.readAsDataURL(blob);
} else {
const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = file.name;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(downloadUrl);
setTimeout(resolve, 100);
}
});
}
================================================
FILE: app/fileSender.js
================================================
import Nanobus from 'nanobus';
import OwnedFile from './ownedFile';
import Keychain from './keychain';
import { arrayToB64, bytes } from './utils';
import { uploadWs } from './api';
import { encryptedSize } from './utils';
export default class FileSender extends Nanobus {
constructor() {
super('FileSender');
this.keychain = new Keychain();
this.reset();
}
get progressRatio() {
return this.progress[0] / this.progress[1];
}
get progressIndefinite() {
return (
['fileSizeProgress', 'notifyUploadEncryptDone'].indexOf(this.msg) === -1
);
}
get sizes() {
return {
partialSize: bytes(this.progress[0]),
totalSize: bytes(this.progress[1])
};
}
reset() {
this.uploadRequest = null;
this.msg = 'importingFile';
this.progress = [0, 1];
this.cancelled = false;
}
cancel() {
this.cancelled = true;
if (this.uploadRequest) {
this.uploadRequest.cancel();
}
}
async upload(archive, bearerToken) {
if (this.cancelled) {
throw new Error(0);
}
this.msg = 'encryptingFile';
this.emit('encrypting');
const totalSize = encryptedSize(archive.size);
const encStream = await this.keychain.encryptStream(archive.stream);
const metadata = await this.keychain.encryptMetadata(archive);
const authKeyB64 = await this.keychain.authKeyB64();
this.uploadRequest = uploadWs(
encStream,
metadata,
authKeyB64,
archive.timeLimit,
archive.dlimit,
bearerToken,
p => {
this.progress = [p, totalSize];
this.emit('progress');
}
);
if (this.cancelled) {
throw new Error(0);
}
this.msg = 'fileSizeProgress';
this.emit('progress'); // HACK to kick MS Edge
try {
const result = await this.uploadRequest.result;
this.msg = 'notifyUploadEncryptDone';
this.uploadRequest = null;
this.progress = [1, 1];
const secretKey = arrayToB64(this.keychain.rawSecret);
const ownedFile = new OwnedFile({
id: result.id,
url: `${result.url}#${secretKey}`,
name: archive.name,
size: archive.size,
manifest: archive.manifest,
time: result.duration,
speed: archive.size / (result.duration / 1000),
createdAt: Date.now(),
expiresAt: Date.now() + archive.timeLimit * 1000,
secretKey: secretKey,
nonce: this.keychain.nonce,
ownerToken: result.ownerToken,
dlimit: archive.dlimit,
timeLimit: archive.timeLimit
});
return ownedFile;
} catch (e) {
this.msg = 'errorPageHeader';
this.uploadRequest = null;
throw e;
}
}
}
================================================
FILE: app/fxa.js
================================================
/* global AUTH_CONFIG */
import { arrayToB64, b64ToArray, concat } from './utils';
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function getOtherInfo(enc) {
const name = encoder.encode(enc);
const length = 256;
const buffer = new ArrayBuffer(name.length + 16);
const dv = new DataView(buffer);
const result = new Uint8Array(buffer);
let i = 0;
dv.setUint32(i, name.length);
i += 4;
result.set(name, i);
i += name.length;
dv.setUint32(i, 0);
i += 4;
dv.setUint32(i, 0);
i += 4;
dv.setUint32(i, length);
return result;
}
async function concatKdf(key, enc) {
if (key.length !== 32) {
throw new Error('unsupported key length');
}
const otherInfo = getOtherInfo(enc);
const buffer = new ArrayBuffer(4 + key.length + otherInfo.length);
const dv = new DataView(buffer);
const concat = new Uint8Array(buffer);
dv.setUint32(0, 1);
concat.set(key, 4);
concat.set(otherInfo, key.length + 4);
const result = await crypto.subtle.digest('SHA-256', concat);
return new Uint8Array(result);
}
export async function prepareScopedBundleKey(storage) {
const keys = await crypto.subtle.generateKey(
{
name: 'ECDH',
namedCurve: 'P-256'
},
true,
['deriveBits']
);
const privateJwk = await crypto.subtle.exportKey('jwk', keys.privateKey);
const publicJwk = await crypto.subtle.exportKey('jwk', keys.publicKey);
const kid = await crypto.subtle.digest(
'SHA-256',
encoder.encode(JSON.stringify(publicJwk))
);
privateJwk.kid = kid;
publicJwk.kid = kid;
storage.set('scopedBundlePrivateKey', JSON.stringify(privateJwk));
return arrayToB64(encoder.encode(JSON.stringify(publicJwk)));
}
export async function decryptBundle(storage, bundle) {
const privateJwk = JSON.parse(storage.get('scopedBundlePrivateKey'));
storage.remove('scopedBundlePrivateKey');
const privateKey = await crypto.subtle.importKey(
'jwk',
privateJwk,
{
name: 'ECDH',
namedCurve: 'P-256'
},
false,
['deriveBits']
);
const jweParts = bundle.split('.');
if (jweParts.length !== 5) {
throw new Error('invalid jwe');
}
const header = JSON.parse(decoder.decode(b64ToArray(jweParts[0])));
const additionalData = encoder.encode(jweParts[0]);
const iv = b64ToArray(jweParts[2]);
const ciphertext = b64ToArray(jweParts[3]);
const tag = b64ToArray(jweParts[4]);
if (header.alg !== 'ECDH-ES' || header.enc !== 'A256GCM') {
throw new Error('unsupported jwe type');
}
const publicKey = await crypto.subtle.importKey(
'jwk',
header.epk,
{
name: 'ECDH',
namedCurve: 'P-256'
},
false,
[]
);
const sharedBits = await crypto.subtle.deriveBits(
{
name: 'ECDH',
public: publicKey
},
privateKey,
256
);
const rawSharedKey = await concatKdf(new Uint8Array(sharedBits), header.enc);
const sharedKey = await crypto.subtle.importKey(
'raw',
rawSharedKey,
{
name: 'AES-GCM'
},
false,
['decrypt']
);
const plaintext = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv,
additionalData: additionalData,
tagLength: tag.length * 8
},
sharedKey,
concat(ciphertext, tag)
);
return JSON.parse(decoder.decode(plaintext));
}
export async function preparePkce(storage) {
const verifier = arrayToB64(crypto.getRandomValues(new Uint8Array(64)));
storage.set('pkceVerifier', verifier);
const challenge = await crypto.subtle.digest(
'SHA-256',
encoder.encode(verifier)
);
return arrayToB64(new Uint8Array(challenge));
}
export async function deriveFileListKey(ikm) {
const baseKey = await crypto.subtle.importKey(
'raw',
b64ToArray(ikm),
{ name: 'HKDF' },
false,
['deriveKey']
);
const fileListKey = await crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('fileList'),
hash: 'SHA-256'
},
baseKey,
{
name: 'AES-GCM',
length: 128
},
true,
['encrypt', 'decrypt']
);
const rawFileListKey = await crypto.subtle.exportKey('raw', fileListKey);
return arrayToB64(new Uint8Array(rawFileListKey));
}
export async function getFileListKey(storage, bundle) {
const jwks = await decryptBundle(storage, bundle);
const jwk = jwks[AUTH_CONFIG.key_scope];
return deriveFileListKey(jwk.k);
}
================================================
FILE: app/keychain.js
================================================
import { arrayToB64, b64ToArray } from './utils';
import { decryptStream, encryptStream } from './ece.js';
const encoder = new TextEncoder();
const decoder = new TextDecoder();
export default class Keychain {
constructor(secretKeyB64, nonce) {
this._nonce = nonce || 'yRCdyQ1EMSA3mo4rqSkuNQ==';
if (secretKeyB64) {
this.rawSecret = b64ToArray(secretKeyB64);
} else {
this.rawSecret = crypto.getRandomValues(new Uint8Array(16));
}
this.secretKeyPromise = crypto.subtle.importKey(
'raw',
this.rawSecret,
'HKDF',
false,
['deriveKey']
);
this.metaKeyPromise = this.secretKeyPromise.then(function(secretKey) {
return crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('metadata'),
hash: 'SHA-256'
},
secretKey,
{
name: 'AES-GCM',
length: 128
},
false,
['encrypt', 'decrypt']
);
});
this.authKeyPromise = this.secretKeyPromise.then(function(secretKey) {
return crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('authentication'),
hash: 'SHA-256'
},
secretKey,
{
name: 'HMAC',
hash: { name: 'SHA-256' }
},
true,
['sign']
);
});
}
get nonce() {
return this._nonce;
}
set nonce(n) {
if (n && n !== this._nonce) {
this._nonce = n;
}
}
setPassword(password, shareUrl) {
this.authKeyPromise = crypto.subtle
.importKey('raw', encoder.encode(password), { name: 'PBKDF2' }, false, [
'deriveKey'
])
.then(passwordKey =>
crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode(shareUrl),
iterations: 100,
hash: 'SHA-256'
},
passwordKey,
{
name: 'HMAC',
hash: 'SHA-256'
},
true,
['sign']
)
);
}
setAuthKey(authKeyB64) {
this.authKeyPromise = crypto.subtle.importKey(
'raw',
b64ToArray(authKeyB64),
{
name: 'HMAC',
hash: 'SHA-256'
},
true,
['sign']
);
}
async authKeyB64() {
const authKey = await this.authKeyPromise;
const rawAuth = await crypto.subtle.exportKey('raw', authKey);
return arrayToB64(new Uint8Array(rawAuth));
}
async authHeader() {
const authKey = await this.authKeyPromise;
const sig = await crypto.subtle.sign(
{
name: 'HMAC'
},
authKey,
b64ToArray(this.nonce)
);
return `send-v1 ${arrayToB64(new Uint8Array(sig))}`;
}
async encryptMetadata(metadata) {
const metaKey = await this.metaKeyPromise;
const ciphertext = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: new Uint8Array(12),
tagLength: 128
},
metaKey,
encoder.encode(
JSON.stringify({
name: metadata.name,
size: metadata.size,
type: metadata.type || 'application/octet-stream',
manifest: metadata.manifest || {}
})
)
);
return ciphertext;
}
encryptStream(plainStream) {
return encryptStream(plainStream, this.rawSecret);
}
decryptStream(cryptotext) {
return decryptStream(cryptotext, this.rawSecret);
}
async decryptMetadata(ciphertext) {
const metaKey = await this.metaKeyPromise;
const plaintext = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: new Uint8Array(12),
tagLength: 128
},
metaKey,
ciphertext
);
return JSON.parse(decoder.decode(plaintext));
}
}
================================================
FILE: app/locale.js
================================================
import { FluentBundle } from '@fluent/bundle';
function makeBundle(locale, ftl) {
const bundle = new FluentBundle(locale, { useIsolating: false });
bundle.addMessages(ftl);
return bundle;
}
export async function getTranslator(locale) {
const bundles = [];
const { default: en } = await import('../public/locales/en-US/send.ftl');
if (locale !== 'en-US') {
const { default: ftl } = await import(
`../public/locales/${locale}/send.ftl`
);
bundles.push(makeBundle(locale, ftl));
}
bundles.push(makeBundle('en-US', en));
return function(id, data) {
for (let bundle of bundles) {
if (bundle.hasMessage(id)) {
return bundle.format(bundle.getMessage(id), data);
}
}
};
}
================================================
FILE: app/main.css
================================================
@tailwind base;
html {
line-height: 1.15;
}
@tailwind components;
:not(input) {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
:root {
--violet-gradient: linear-gradient(
-180deg,
rgba(144, 89, 255, 0.8) 0%,
rgba(144, 89, 255, 0.4) 100%
);
}
a {
color: inherit;
text-decoration: none;
}
a:focus {
outline: 1px dotted grey;
}
body {
overflow-x: hidden;
}
.btn {
@apply bg-blue-60;
@apply text-white;
@apply cursor-pointer;
@apply py-4;
@apply px-6;
@apply font-semibold;
}
.btn:hover {
@apply bg-blue-70;
}
.btn:focus {
@apply bg-blue-70;
}
.btn:disabled {
@apply bg-grey-transparent;
cursor: not-allowed;
}
.checkbox {
@apply leading-normal;
@apply select-none;
}
.checkbox > input[type='checkbox'] {
@apply absolute;
@apply opacity-0;
}
.checkbox > label {
@apply cursor-pointer;
}
.checkbox > label::before {
/* @apply bg-grey-10; */
@apply border;
@apply rounded-sm;
content: '';
height: 1.5rem;
width: 1.5rem;
margin-right: 0.5rem;
float: left;
}
.checkbox > label:hover::before {
@apply border-blue-50;
}
.checkbox > input:focus + label::before {
@apply border-blue-50;
}
.checkbox > input:checked + label::before {
@apply bg-blue-50;
@apply border-blue-50;
background-image: url('../assets/lock.svg');
background-position: center;
background-size: 1.25rem;
background-repeat: no-repeat;
}
.checkbox > input:disabled + label {
cursor: auto;
}
.checkbox > input:disabled + label::before {
@apply bg-blue-50;
@apply border-blue-50;
background-image: url('../assets/lock.svg');
background-position: center;
background-size: 1.25rem;
background-repeat: no-repeat;
cursor: auto;
}
details {
overflow: hidden;
}
details > summary::-webkit-details-marker {
display: none;
}
details > summary > svg {
transition: all 0.25s cubic-bezier(0.07, 0.95, 0, 1);
}
details[open] {
overflow-y: auto;
}
details[open] > summary > svg {
transform: rotate(90deg);
}
footer li:hover {
text-decoration: underline;
}
.link-blue {
@apply text-blue-60;
}
.link-blue:hover {
@apply text-blue-70;
}
.link-blue:focus {
@apply text-blue-70;
}
.main-header img {
height: 32px;
width: auto;
}
.intro {
max-width: 100%;
height: unset;
}
.dl-bg {
filter: grayscale(1) opacity(0.15);
}
.main {
display: flex;
position: relative;
max-width: 64rem;
width: 100%;
height: 100%;
}
.main > section {
@apply bg-white;
}
#password-msg::after {
content: '\200b';
}
progress {
@apply bg-grey-30;
@apply rounded-sm;
@apply w-full;
@apply h-1;
}
progress::-webkit-progress-bar {
@apply bg-grey-30;
@apply rounded-sm;
@apply w-full;
@apply h-1;
}
progress::-webkit-progress-value {
/* stylelint-disable */
background-image: -webkit-linear-gradient(
-45deg,
transparent 20%,
rgba(255, 255, 255, 0.4) 20%,
rgba(255, 255, 255, 0.4) 40%,
transparent 40%,
transparent 60%,
rgba(255, 255, 255, 0.4) 60%,
rgba(255, 255, 255, 0.4) 80%,
transparent 80%
),
-webkit-linear-gradient(left, #0a84ff, #0a84ff);
/* stylelint-enable */
border-radius: 2px;
background-size: 21px 20px, 100% 100%, 100% 100%;
-webkit-animation: animate-stripes 1s linear infinite;
}
progress::-moz-progress-bar {
/* stylelint-disable */
background-image: -moz-linear-gradient(
135deg,
transparent 20%,
rgba(255, 255, 255, 0.4) 20%,
rgba(255, 255, 255, 0.4) 40%,
transparent 40%,
transparent 60%,
rgba(255, 255, 255, 0.4) 60%,
rgba(255, 255, 255, 0.4) 80%,
transparent 80%
),
-moz-linear-gradient(left, #0a84ff, #0a84ff);
/* stylelint-enable */
border-radius: 2px;
background-size: 21px 20px, 100% 100%, 100% 100%;
animation: animate-stripes 1s linear infinite;
}
@-webkit-keyframes animate-stripes {
100% {
background-position: -21px 0;
}
}
@keyframes animate-stripes {
100% {
background-position: -21px 0;
}
}
select {
background-image: url('../assets/select-arrow.svg');
background-position: calc(100% - 0.75rem);
background-repeat: no-repeat;
}
@screen md {
.main-header img {
height: 48px;
width: auto;
}
.intro {
max-width: unset;
height: unset;
margin-bottom: -3rem;
margin-right: -7rem;
}
.main {
@apply flex-1;
@apply self-center;
@apply items-center;
@apply m-auto;
@apply py-8;
min-height: 42rem;
max-height: 42rem;
width: calc(100% - 3rem);
}
}
@screen dark {
body {
@apply text-grey-10;
background-image: unset;
}
.btn {
@apply bg-blue-40;
@apply text-white;
}
.btn:hover {
@apply bg-blue-50;
}
.btn:focus {
@apply bg-blue-50;
}
.btn:disabled {
@apply bg-grey-80;
}
.link-blue {
@apply text-blue-40;
}
.link-blue:hover {
@apply text-blue-50;
}
.link-blue:focus {
@apply text-blue-50;
}
.main > section {
@apply bg-grey-90;
}
@screen md {
.main > section {
@apply border;
@apply border-grey-80;
}
}
}
@tailwind utilities;
@responsive {
.shadow-light {
box-shadow: 0 0 8px 0 rgba(12, 12, 13, 0.1);
}
.shadow-big {
box-shadow: 0 12px 18px 2px rgba(34, 0, 51, 0.04),
0 6px 22px 4px rgba(7, 48, 114, 0.12),
0 6px 10px -4px rgba(14, 13, 26, 0.12);
}
}
@variants focus {
.outline {
outline: 1px dotted grey;
}
}
.word-break-all {
word-break: break-all;
line-break: anywhere;
}
.signin {
backface-visibility: hidden;
border-radius: 6px;
transition-property: transform, background-color;
transition-duration: 250ms;
transition-timing-function: cubic-bezier(0.07, 0.95, 0, 1);
}
.signin:hover,
.signin:focus {
transform: scale(1.0625);
}
.signin:hover:active {
transform: scale(0.9375);
}
================================================
FILE: app/main.js
================================================
/* global DEFAULTS LIMITS PREFS */
import 'core-js';
import 'fast-text-encoding'; // MS Edge support
import 'intl-pluralrules';
import choo from 'choo';
import nanotiming from 'nanotiming';
import routes from './routes';
import getCapabilities from './capabilities';
import controller from './controller';
import dragManager from './dragManager';
import pasteManager from './pasteManager';
import storage from './storage';
import metrics from './metrics';
import experiments from './experiments';
import * as Sentry from '@sentry/browser';
import './main.css';
import User from './user';
import { getTranslator } from './locale';
import Archive from './archive';
import { setTranslate, locale } from './utils';
if (navigator.doNotTrack !== '1' && window.SENTRY_CONFIG) {
Sentry.init(window.SENTRY_CONFIG);
}
if (process.env.NODE_ENV === 'production') {
nanotiming.disabled = true;
}
(async function start() {
const capabilities = await getCapabilities();
if (
!capabilities.crypto &&
window.location.pathname !== '/unsupported/crypto'
) {
return window.location.assign('/unsupported/crypto');
}
if (capabilities.serviceWorker) {
try {
await navigator.serviceWorker.register('/serviceWorker.js');
await navigator.serviceWorker.ready;
} catch (e) {
// continue but disable streaming downloads
capabilities.streamDownload = false;
}
}
const translate = await getTranslator(locale());
setTranslate(translate);
// eslint-disable-next-line require-atomic-updates
window.initialState = {
LIMITS,
DEFAULTS,
PREFS,
archive: new Archive([], DEFAULTS.EXPIRE_SECONDS),
capabilities,
translate,
storage,
sentry: Sentry,
user: new User(storage, LIMITS, window.AUTH_CONFIG),
transfer: null,
fileInfo: null,
locale: locale()
};
const app = routes(choo({ hash: true }));
// eslint-disable-next-line require-atomic-updates
window.app = app;
app.use(experiments);
app.use(metrics);
app.use(controller);
app.use(dragManager);
app.use(pasteManager);
app.mount('body');
})();
================================================
FILE: app/metrics.js
================================================
import storage from './storage';
import { platform, locale } from './utils';
import { sendMetrics } from './api';
let appState = null;
let experiment = null;
const HOUR = 1000 * 60 * 60;
const events = [];
let session_id = Date.now();
const lang = locale();
export default function initialize(state, emitter) {
appState = state;
emitter.on('DOMContentLoaded', () => {
experiment = storage.enrolled;
if (!appState.user.firstAction) {
appState.user.firstAction =
appState.route === '/' ? 'upload' : 'download';
}
const query = appState.query;
addEvent('client_visit', {
entrypoint: appState.route === '/' ? 'upload' : 'download',
referrer: document.referrer,
utm_campaign: query.utm_campaign,
utm_content: query.utm_content,
utm_medium: query.utm_medium,
utm_source: query.utm_source,
utm_term: query.utm_term
});
});
emitter.on('experiment', experimentEvent);
window.addEventListener('unload', submitEvents);
}
function sizeOrder(n) {
return Math.floor(Math.log10(n));
}
function submitEvents() {
if (navigator.doNotTrack === '1') {
return;
}
sendMetrics(
new Blob(
[
JSON.stringify({
now: Date.now(),
session_id,
lang,
platform: platform(),
events
})
],
{ type: 'text/plain' } // see http://crbug.com/490015
)
);
events.splice(0);
}
async function addEvent(event_type, event_properties) {
const user_id = await appState.user.metricId();
const device_id = await appState.user.deviceId();
const ab_id = Object.keys(experiment)[0];
if (ab_id) {
event_properties.experiment = ab_id;
event_properties.variant = experiment[ab_id];
}
events.push({
device_id,
event_properties,
event_type,
time: Date.now(),
user_id,
user_properties: {
anonymous: !appState.user.loggedIn,
first_action: appState.user.firstAction,
active_count: storage.files.length
}
});
if (events.length === 25) {
submitEvents();
}
}
function cancelledUpload(archive, duration) {
return addEvent('client_upload', {
download_limit: archive.dlimit,
duration: sizeOrder(duration),
file_count: archive.numFiles,
password_protected: !!archive.password,
size: sizeOrder(archive.size),
status: 'cancel',
time_limit: archive.timeLimit
});
}
function completedUpload(archive, duration) {
return addEvent('client_upload', {
download_limit: archive.dlimit,
duration: sizeOrder(duration),
file_count: archive.numFiles,
password_protected: !!archive.password,
size: sizeOrder(archive.size),
status: 'ok',
time_limit: archive.timeLimit
});
}
function stoppedUpload(archive, duration = 0) {
return addEvent('client_upload', {
download_limit: archive.dlimit,
duration: sizeOrder(duration),
file_count: archive.numFiles,
password_protected: !!archive.password,
size: sizeOrder(archive.size),
status: 'error',
time_limit: archive.timeLimit
});
}
function stoppedDownload(params) {
return addEvent('client_download', {
duration: sizeOrder(params.duration),
password_protected: params.password_protected,
size: sizeOrder(params.size),
status: 'error'
});
}
function completedDownload(params) {
return addEvent('client_download', {
duration: sizeOrder(params.duration),
password_protected: params.password_protected,
size: sizeOrder(params.size),
status: 'ok'
});
}
function deletedUpload(ownedFile) {
return addEvent('client_delete', {
age: Math.floor((Date.now() - ownedFile.createdAt) / HOUR),
downloaded: ownedFile.dtotal > 0,
status: 'ok'
});
}
function experimentEvent(params) {
return addEvent('client_experiment', params);
}
function submittedSignup(params) {
return addEvent('client_login', {
status: 'ok',
trigger: params.trigger
});
}
function canceledSignup(params) {
return addEvent('client_login', {
status: 'cancel',
trigger: params.trigger
});
}
function loggedOut(params) {
addEvent('client_logout', {
status: 'ok',
trigger: params.trigger
});
// flush events and start new anon session
submitEvents();
session_id = Date.now();
}
export {
cancelledUpload,
stoppedUpload,
completedUpload,
deletedUpload,
stoppedDownload,
completedDownload,
submittedSignup,
canceledSignup,
loggedOut
};
================================================
FILE: app/ownedFile.js
================================================
import Keychain from './keychain';
import { arrayToB64 } from './utils';
import { del, fileInfo, setParams, setPassword } from './api';
export default class OwnedFile {
constructor(obj) {
if (!obj.manifest) {
throw new Error('invalid file object');
}
this.id = obj.id;
this.url = obj.url;
this.name = obj.name;
this.size = obj.size;
this.manifest = obj.manifest;
this.time = obj.time;
this.speed = obj.speed;
this.createdAt = obj.createdAt;
this.expiresAt = obj.expiresAt;
this.ownerToken = obj.ownerToken;
this.dlimit = obj.dlimit || 1;
this.dtotal = obj.dtotal || 0;
this.keychain = new Keychain(obj.secretKey, obj.nonce);
this._hasPassword = !!obj.hasPassword;
this.timeLimit = obj.timeLimit;
}
get hasPassword() {
return !!this._hasPassword;
}
get expired() {
return this.dlimit === this.dtotal || Date.now() > this.expiresAt;
}
async setPassword(password) {
try {
this.password = password;
this._hasPassword = true;
this.keychain.setPassword(password, this.url);
const result = await setPassword(this.id, this.ownerToken, this.keychain);
return result;
} catch (e) {
this.password = null;
this._hasPassword = false;
throw e;
}
}
del() {
return del(this.id, this.ownerToken);
}
changeLimit(dlimit, user = {}) {
if (this.dlimit !== dlimit) {
this.dlimit = dlimit;
return setParams(this.id, this.ownerToken, user.bearerToken, { dlimit });
}
return Promise.resolve(true);
}
async updateDownloadCount() {
const oldTotal = this.dtotal;
const oldLimit = this.dlimit;
try {
const result = await fileInfo(this.id, this.ownerToken);
this.dtotal = result.dtotal;
this.dlimit = result.dlimit;
} catch (e) {
if (e.message === '404') {
this.dtotal = this.dlimit;
}
// ignore other errors
}
return oldTotal !== this.dtotal || oldLimit !== this.dlimit;
}
toJSON() {
return {
id: this.id,
url: this.url,
name: this.name,
size: this.size,
manifest: this.manifest,
time: this.time,
speed: this.speed,
createdAt: this.createdAt,
expiresAt: this.expiresAt,
secretKey: arrayToB64(this.keychain.rawSecret),
ownerToken: this.ownerToken,
dlimit: this.dlimit,
dtotal: this.dtotal,
hasPassword: this.hasPassword,
timeLimit: this.timeLimit
};
}
}
================================================
FILE: app/pasteManager.js
================================================
function getString(item) {
return new Promise(resolve => {
item.getAsString(resolve);
});
}
export default function(state, emitter) {
window.addEventListener('paste', async event => {
if (state.route !== '/' || state.uploading) return;
if (['password', 'text', 'email'].includes(event.target.type)) return;
const items = Array.from(event.clipboardData.items);
const transferFiles = items.filter(item => item.kind === 'file');
const strings = items.filter(item => item.kind === 'string');
if (transferFiles.length) {
const promises = transferFiles.map(async (f, i) => {
const blob = f.getAsFile();
if (!blob) {
return null;
}
const name = await getString(strings[i]);
const file = new File([blob], name, { type: blob.type });
return file;
});
const files = (await Promise.all(promises)).filter(f => !!f);
if (files.length) {
emitter.emit('addFiles', { files });
}
} else if (strings.length) {
strings[0].getAsString(s => {
const file = new File([s], 'pasted.txt', { type: 'text/plain' });
emitter.emit('addFiles', { files: [file] });
});
}
});
}
================================================
FILE: app/readme.md
================================================
# Application Code
`app/` contains the browser code that gets bundled into `app.[hash].js`. It's got all the logic, crypto, and UI. All of it gets used in the browser, and some of it by the server for server side rendering.
The main entrypoint for the browser is [main.js](./main.js) and on the server [routes.js](./routes.js) is imported by [/server/routes/pages.js](../server/routes/pages.js)
- `pages` contains display logic an markup for pages
- `routes` contains route definitions and logic
- `templates` contains ui elements smaller than pages
================================================
FILE: app/routes.js
================================================
const choo = require('choo');
const download = require('./ui/download');
const body = require('./ui/body');
module.exports = function(app = choo({ hash: true })) {
app.route('/', body(require('./ui/home')));
app.route('/download/:id', body(download));
app.route('/download/:id/:key', body(download));
app.route('/unsupported/:reason', body(require('./ui/unsupported')));
app.route('/error', body(require('./ui/error')));
app.route('/blank', body(require('./ui/blank')));
app.route('/oauth', function(state, emit) {
emit('authenticate', state.query.code, state.query.state);
});
app.route('/login', function(state, emit) {
emit('replaceState', '/');
setTimeout(() => emit('render'));
});
app.route('/report', body(require('./ui/report')));
app.route('*', body(require('./ui/notFound')));
return app;
};
================================================
FILE: app/serviceWorker.js
================================================
import assets from '../common/assets';
import { version } from '../package.json';
import Keychain from './keychain';
import { downloadStream } from './api';
import { transformStream } from './streams';
import Zip from './zip';
import contentDisposition from 'content-disposition';
let noSave = false;
const map = new Map();
const IMAGES = /.*\.(png|svg|jpg)$/;
const VERSIONED_ASSET = /\.[A-Fa-f0-9]{8}\.(js|css|png|svg|jpg)(#\w+)?$/;
const DOWNLOAD_URL = /\/api\/download\/([A-Fa-f0-9]{4,})/;
const FONT = /\.woff2?$/;
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim().then(precache));
});
async function decryptStream(id) {
const file = map.get(id);
if (!file) {
return new Response(null, { status: 400 });
}
try {
let size = file.size;
let type = file.type;
const keychain = new Keychain(file.key, file.nonce);
if (file.requiresPassword) {
keychain.setPassword(file.password, file.url);
}
file.download = downloadStream(id, file.dlToken);
const body = await file.download.result;
const decrypted = keychain.decryptStream(body);
let zipStream = null;
if (file.type === 'send-archive') {
const zip = new Zip(file.manifest, decrypted);
zipStream = zip.stream;
type = 'application/zip';
size = zip.size;
}
const responseStream = transformStream(
zipStream || decrypted,
{
transform(chunk, controller) {
file.progress += chunk.length;
controller.enqueue(chunk);
}
},
function oncancel() {
// NOTE: cancel doesn't currently fire on chrome
// https://bugs.chromium.org/p/chromium/issues/detail?id=638494
file.download.cancel();
map.delete(id);
}
);
const headers = {
'Content-Disposition': contentDisposition(file.filename),
'Content-Type': type,
'Content-Length': size
};
return new Response(responseStream, { headers });
} catch (e) {
if (noSave) {
return new Response(null, { status: e.message });
}
return new Response(null, {
status: 302,
headers: {
Location: `/download/${id}/#${file.key}`
}
});
}
}
async function precache() {
try {
await cleanCache();
const cache = await caches.open(version);
const images = assets.match(IMAGES);
await cache.addAll(images);
} catch (e) {
console.error(e);
// cache will get populated on demand
}
}
async function cleanCache() {
const oldCaches = await caches.keys();
for (const c of oldCaches) {
if (c !== version) {
await caches.delete(c);
}
}
}
function cacheable(url) {
return VERSIONED_ASSET.test(url) || FONT.test(url);
}
async function cachedOrFetched(req) {
const cache = await caches.open(version);
const cached = await cache.match(req);
if (cached) {
return cached;
}
const fetched = await fetch(req);
if (fetched.ok && cacheable(req.url)) {
cache.put(req, fetched.clone());
}
return fetched;
}
self.onfetch = event => {
const req = event.request;
if (req.method !== 'GET') return;
const url = new URL(req.url);
const dlmatch = DOWNLOAD_URL.exec(url.pathname);
if (dlmatch) {
event.respondWith(decryptStream(dlmatch[1]));
} else if (cacheable(url.pathname)) {
event.respondWith(cachedOrFetched(req));
}
};
self.onmessage = event => {
if (event.data.request === 'init') {
noSave = event.data.noSave;
const info = {
key: event.data.key,
nonce: event.data.nonce,
filename: event.data.filename,
requiresPassword: event.data.requiresPassword,
password: event.data.password,
url: event.data.url,
type: event.data.type,
manifest: event.data.manifest,
size: event.data.size,
dlToken: event.data.dlToken,
progress: 0
};
map.set(event.data.id, info);
event.ports[0].postMessage('file info received');
} else if (event.data.request === 'progress') {
const file = map.get(event.data.id);
if (!file) {
event.ports[0].postMessage({ error: 'cancelled' });
} else {
if (file.progress === file.size) {
map.delete(event.data.id);
}
event.ports[0].postMessage({ progress: file.progress });
}
} else if (event.data.request === 'cancel') {
const file = map.get(event.data.id);
if (file) {
if (file.download) {
file.download.cancel();
}
map.delete(event.data.id);
}
event.ports[0].postMessage('download cancelled');
}
};
================================================
FILE: app/storage.js
================================================
import { arrayToB64, isFile } from './utils';
import OwnedFile from './ownedFile';
class Mem {
constructor() {
this.items = new Map();
}
get length() {
return this.items.size;
}
getItem(key) {
return this.items.get(key);
}
setItem(key, value) {
return this.items.set(key, value);
}
removeItem(key) {
return this.items.delete(key);
}
key(i) {
return this.items.keys()[i];
}
}
class Storage {
constructor() {
try {
this.engine = localStorage || new Mem();
} catch (e) {
this.engine = new Mem();
}
this._files = this.loadFiles();
this.pruneTokens();
}
loadFiles() {
const fs = new Map();
for (let i = 0; i < this.engine.length; i++) {
const k = this.engine.key(i);
if (isFile(k)) {
try {
const f = new OwnedFile(JSON.parse(this.engine.getItem(k)));
if (!f.id) {
f.id = f.fileId;
}
fs.set(f.id, f);
} catch (err) {
// obviously you're not a golfer
this.engine.removeItem(k);
}
}
}
return fs;
}
get id() {
let id = this.engine.getItem('device_id');
if (!id) {
id = arrayToB64(crypto.getRandomValues(new Uint8Array(16)));
this.engine.setItem('device_id', id);
}
return id;
}
get totalDownloads() {
return Number(this.engine.getItem('totalDownloads'));
}
set totalDownloads(n) {
this.engine.setItem('totalDownloads', n);
}
get totalUploads() {
return Number(this.engine.getItem('totalUploads'));
}
set totalUploads(n) {
this.engine.setItem('totalUploads', n);
}
get referrer() {
return this.engine.getItem('referrer');
}
set referrer(str) {
this.engine.setItem('referrer', str);
}
get enrolled() {
return JSON.parse(this.engine.getItem('ab_experiments') || '{}');
}
enroll(id, variant) {
const enrolled = {};
enrolled[id] = variant;
this.engine.setItem('ab_experiments', JSON.stringify(enrolled));
}
get files() {
return Array.from(this._files.values()).sort(
(a, b) => a.createdAt - b.createdAt
);
}
get user() {
try {
return JSON.parse(this.engine.getItem('user'));
} catch (e) {
return null;
}
}
set user(info) {
return this.engine.setItem('user', JSON.stringify(info));
}
getFileById(id) {
return this._files.get(id);
}
get(id) {
return this.engine.getItem(id);
}
set(id, value) {
return this.engine.setItem(id, value);
}
remove(property) {
if (isFile(property)) {
this._files.delete(property);
}
this.engine.removeItem(property);
}
addFile(file) {
this._files.set(file.id, file);
this.writeFile(file);
}
writeFile(file) {
this.engine.setItem(file.id, JSON.stringify(file));
}
writeFiles() {
this._files.forEach(f => this.writeFile(f));
}
clearLocalFiles() {
this._files.forEach(f => this.engine.removeItem(f.id));
this._files = new Map();
}
async merge(files = []) {
let incoming = false;
let outgoing = false;
let downloadCount = false;
for (const f of files) {
if (!this.getFileById(f.id)) {
this.addFile(new OwnedFile(f));
incoming = true;
}
}
const workingFiles = this.files.slice();
for (const f of workingFiles) {
const cc = await f.updateDownloadCount();
if (cc) {
await this.writeFile(f);
}
downloadCount = downloadCount || cc;
outgoing = outgoing || f.expired;
if (f.expired) {
gitextract_4or1ul2c/ ├── .circleci/ │ └── config.yml ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .gitattributes ├── .gitignore ├── .htmllintrc ├── .prettierignore ├── .stylelintrc ├── .vscode/ │ └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTORS ├── Dockerfile ├── LICENSE ├── README.md ├── android/ │ ├── .eslintrc.yaml │ ├── .gitignore │ ├── README.md │ ├── android.js │ ├── app/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── buildAssets.sh │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── org/ │ │ │ └── mozilla/ │ │ │ └── firefoxsend/ │ │ │ └── MainActivity.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── values/ │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── pages/ │ │ ├── .eslintrc.yaml │ │ ├── error.js │ │ ├── home.js │ │ ├── preferences.js │ │ ├── share.js │ │ └── upload.js │ ├── settings.gradle │ ├── stores/ │ │ ├── intents.js │ │ └── state.js │ └── user.js ├── app/ │ ├── .eslintrc.yml │ ├── api.js │ ├── archive.js │ ├── capabilities.js │ ├── controller.js │ ├── crc32.js │ ├── dragManager.js │ ├── ece.js │ ├── experiments.js │ ├── fileReceiver.js │ ├── fileSender.js │ ├── fxa.js │ ├── keychain.js │ ├── locale.js │ ├── main.css │ ├── main.js │ ├── metrics.js │ ├── ownedFile.js │ ├── pasteManager.js │ ├── readme.md │ ├── routes.js │ ├── serviceWorker.js │ ├── storage.js │ ├── streams.js │ ├── ui/ │ │ ├── account.js │ │ ├── archiveTile.js │ │ ├── blank.js │ │ ├── body.js │ │ ├── copyDialog.js │ │ ├── download.js │ │ ├── downloadCompleted.js │ │ ├── downloadDialog.js │ │ ├── downloadPassword.js │ │ ├── error.js │ │ ├── expiryOptions.js │ │ ├── footer.js │ │ ├── header.js │ │ ├── home.js │ │ ├── intro.js │ │ ├── modal.js │ │ ├── noStreams.js │ │ ├── notFound.js │ │ ├── okDialog.js │ │ ├── report.js │ │ ├── selectbox.js │ │ ├── shareDialog.js │ │ └── unsupported.js │ ├── user.js │ ├── utils.js │ └── zip.js ├── browserslist ├── build/ │ ├── android_index_plugin.js │ ├── readme.md │ └── version_plugin.js ├── common/ │ ├── assets.js │ ├── generate_asset_map.js │ └── readme.md ├── docker-compose.yml ├── docs/ │ ├── CODEOWNERS │ ├── acceptance-mobile.md │ ├── acceptance-web.md │ ├── build.md │ ├── deployment.md │ ├── docker.md │ ├── encryption.md │ ├── experiments.md │ ├── faq.md │ ├── localization.md │ ├── metrics.md │ ├── notes/ │ │ └── streams.md │ └── takedowns.md ├── ios/ │ ├── generate-bundle.js │ ├── ios.js │ ├── send-ios/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── ViewController.swift │ │ ├── assets/ │ │ │ ├── index.css │ │ │ └── index.html │ │ └── help.html │ ├── send-ios-action-extension/ │ │ ├── ActionViewController.swift │ │ ├── Base.lproj/ │ │ │ └── MainInterface.storyboard │ │ └── Info.plist │ └── send-ios.xcodeproj/ │ ├── project.pbxproj │ └── project.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── IDEWorkspaceChecks.plist ├── l10n.toml ├── package.json ├── postcss.config.js ├── public/ │ ├── contribute.json │ ├── inter.css │ └── locales/ │ ├── an/ │ │ └── send.ftl │ ├── ar/ │ │ └── send.ftl │ ├── ast/ │ │ └── send.ftl │ ├── az/ │ │ └── send.ftl │ ├── azz/ │ │ └── send.ftl │ ├── be/ │ │ └── send.ftl │ ├── bn/ │ │ └── send.ftl │ ├── br/ │ │ └── send.ftl │ ├── bs/ │ │ └── send.ftl │ ├── ca/ │ │ └── send.ftl │ ├── cak/ │ │ └── send.ftl │ ├── ckb/ │ │ └── send.ftl │ ├── cs/ │ │ └── send.ftl │ ├── cy/ │ │ └── send.ftl │ ├── da/ │ │ └── send.ftl │ ├── de/ │ │ └── send.ftl │ ├── dsb/ │ │ └── send.ftl │ ├── el/ │ │ └── send.ftl │ ├── en-CA/ │ │ └── send.ftl │ ├── en-GB/ │ │ └── send.ftl │ ├── en-US/ │ │ └── send.ftl │ ├── es-AR/ │ │ └── send.ftl │ ├── es-CL/ │ │ └── send.ftl │ ├── es-ES/ │ │ └── send.ftl │ ├── es-MX/ │ │ └── send.ftl │ ├── et/ │ │ └── send.ftl │ ├── eu/ │ │ └── send.ftl │ ├── fa/ │ │ └── send.ftl │ ├── fi/ │ │ └── send.ftl │ ├── fr/ │ │ └── send.ftl │ ├── fy-NL/ │ │ └── send.ftl │ ├── gn/ │ │ └── send.ftl │ ├── gor/ │ │ └── send.ftl │ ├── he/ │ │ └── send.ftl │ ├── hr/ │ │ └── send.ftl │ ├── hsb/ │ │ └── send.ftl │ ├── hu/ │ │ └── send.ftl │ ├── hus/ │ │ └── send.ftl │ ├── hy-AM/ │ │ └── send.ftl │ ├── ia/ │ │ └── send.ftl │ ├── id/ │ │ └── send.ftl │ ├── ig/ │ │ └── send.ftl │ ├── it/ │ │ └── send.ftl │ ├── ixl/ │ │ └── send.ftl │ ├── ja/ │ │ └── send.ftl │ ├── ka/ │ │ └── send.ftl │ ├── kab/ │ │ └── send.ftl │ ├── ko/ │ │ └── send.ftl │ ├── lt/ │ │ └── send.ftl │ ├── lus/ │ │ └── send.ftl │ ├── meh/ │ │ └── send.ftl │ ├── mix/ │ │ └── send.ftl │ ├── ml/ │ │ └── send.ftl │ ├── ms/ │ │ └── send.ftl │ ├── nb-NO/ │ │ └── send.ftl │ ├── nl/ │ │ └── send.ftl │ ├── nn-NO/ │ │ └── send.ftl │ ├── oc/ │ │ └── send.ftl │ ├── pa-IN/ │ │ └── send.ftl │ ├── pai/ │ │ └── send.ftl │ ├── pl/ │ │ └── send.ftl │ ├── ppl/ │ │ └── send.ftl │ ├── pt-BR/ │ │ └── send.ftl │ ├── pt-PT/ │ │ └── send.ftl │ ├── quc/ │ │ └── send.ftl │ ├── ro/ │ │ └── send.ftl │ ├── ru/ │ │ └── send.ftl │ ├── sk/ │ │ └── send.ftl │ ├── sl/ │ │ └── send.ftl │ ├── sn/ │ │ └── send.ftl │ ├── sq/ │ │ └── send.ftl │ ├── sr/ │ │ └── send.ftl │ ├── su/ │ │ └── send.ftl │ ├── sv-SE/ │ │ └── send.ftl │ ├── te/ │ │ └── send.ftl │ ├── th/ │ │ └── send.ftl │ ├── tl/ │ │ └── send.ftl │ ├── tr/ │ │ └── send.ftl │ ├── trs/ │ │ └── send.ftl │ ├── uk/ │ │ └── send.ftl │ ├── vi/ │ │ └── send.ftl │ ├── yo/ │ │ └── send.ftl │ ├── yua/ │ │ └── send.ftl │ ├── zgh/ │ │ └── send.ftl │ ├── zh-CN/ │ │ └── send.ftl │ └── zh-TW/ │ └── send.ftl ├── scripts/ │ ├── .eslintrc.yml │ ├── bin/ │ │ └── run-integration-test-circleci.sh │ ├── get-prod-locales.js │ ├── lint-locales.js │ └── sync-npm-dependencies.sh ├── server/ │ ├── amplitude.js │ ├── bin/ │ │ ├── dev.js │ │ ├── prod.js │ │ └── test.js │ ├── clientConstants.js │ ├── config.js │ ├── fxa.js │ ├── initScript.js │ ├── keychain.js │ ├── layout.js │ ├── limiter.js │ ├── locale.js │ ├── log.js │ ├── metadata.js │ ├── middleware/ │ │ ├── auth.js │ │ └── language.js │ ├── readme.md │ ├── routes/ │ │ ├── delete.js │ │ ├── done.js │ │ ├── download.js │ │ ├── exists.js │ │ ├── filelist.js │ │ ├── index.js │ │ ├── info.js │ │ ├── metadata.js │ │ ├── metrics.js │ │ ├── pages.js │ │ ├── params.js │ │ ├── password.js │ │ ├── report.js │ │ ├── token.js │ │ ├── upload.js │ │ ├── webmanifest.js │ │ └── ws.js │ ├── state.js │ └── storage/ │ ├── fs.js │ ├── gcs.js │ ├── index.js │ ├── redis.js │ └── s3.js ├── tailwind.config.js ├── test/ │ ├── .eslintrc.yml │ ├── backend/ │ │ ├── auth-tests.js │ │ ├── delete-tests.js │ │ ├── info-tests.js │ │ ├── language-tests.js │ │ ├── metadata-tests.js │ │ ├── owner-tests.js │ │ ├── params-tests.js │ │ ├── password-tests.js │ │ ├── s3-tests.js │ │ └── storage-tests.js │ ├── frontend/ │ │ ├── .eslintrc.yml │ │ ├── index.js │ │ ├── routes.js │ │ ├── runner.js │ │ └── tests/ │ │ ├── api-tests.js │ │ ├── auth-tests.js │ │ ├── crypto-tests.js │ │ ├── fileSender-tests.js │ │ ├── keychain-tests.js │ │ ├── streaming-tests.js │ │ └── workflow-tests.js │ ├── integration/ │ │ ├── README.md │ │ ├── download-tests.js │ │ ├── fixtures/ │ │ │ ├── txt-larger-testfile.txt │ │ │ └── txt-small-testfile.txt │ │ ├── homepage-tests.js │ │ ├── pages/ │ │ │ └── desktop/ │ │ │ ├── download_page.js │ │ │ ├── home_page.js │ │ │ └── page.js │ │ ├── progress-tests.js │ │ └── send-test.html │ ├── readme.md │ ├── testServer.js │ ├── wdio.circleci.conf.js │ ├── wdio.common.conf.js │ ├── wdio.docker.conf.js │ ├── wdio.local.conf.js │ ├── wdio.remote.config.js │ └── wdio.saucelabs.config.js └── webpack.config.js
SYMBOL INDEX (475 symbols across 81 files)
FILE: android/android.js
function body (line 43) | function body(main) {
FILE: android/pages/error.js
function error (line 3) | function error(_state, _emit) {
FILE: android/pages/home.js
function onchange (line 9) | function onchange(event) {
function onclick (line 16) | function onclick() {
FILE: android/pages/preferences.js
function preferences (line 5) | function preferences(state, emit) {
FILE: android/pages/share.js
function uploadComplete (line 3) | function uploadComplete(state, emit) {
FILE: android/pages/upload.js
function progressBar (line 3) | function progressBar(state, emit) {
FILE: android/stores/intents.js
function intentHandler (line 3) | function intentHandler(state, emitter) {
FILE: android/stores/state.js
function initialState (line 6) | function initialState(state, emitter) {
FILE: android/user.js
class AndroidUser (line 5) | class AndroidUser extends User {
method constructor (line 6) | constructor(storage, limits) {
method login (line 10) | async login() {
method startAuthFlow (line 14) | startAuthFlow() {
method finishLogin (line 18) | async finishLogin(accountInfo) {
FILE: app/api.js
class ConnectionError (line 14) | class ConnectionError extends Error {
method constructor (line 15) | constructor(cancelled, duration, size) {
function setFileProtocolWssUrl (line 23) | function setFileProtocolWssUrl(url) {
function getFileProtocolWssUrl (line 28) | function getFileProtocolWssUrl() {
function getApiUrl (line 33) | function getApiUrl(path) {
function setApiUrlPrefix (line 37) | function setApiUrlPrefix(prefix) {
function post (line 41) | function post(obj, bearerToken) {
function parseNonce (line 55) | function parseNonce(header) {
function fetchWithAuth (line 60) | async function fetchWithAuth(url, params, keychain) {
function fetchWithAuthAndRetry (line 77) | async function fetchWithAuthAndRetry(url, params, keychain) {
function del (line 85) | async function del(id, owner_token) {
function setParams (line 93) | async function setParams(id, owner_token, bearerToken, params) {
function fileInfo (line 107) | async function fileInfo(id, owner_token) {
function metadata (line 121) | async function metadata(id, keychain) {
function setPassword (line 142) | async function setPassword(id, owner_token, keychain) {
function asyncInitWebSocket (line 151) | function asyncInitWebSocket(server) {
function listenForResponse (line 162) | function listenForResponse(ws, canceller) {
function upload (line 187) | async function upload(
function uploadWs (line 264) | function uploadWs(
function _downloadStream (line 295) | async function _downloadStream(id, dlToken, signal) {
function tryDownloadStream (line 309) | async function tryDownloadStream(id, dlToken, signal, tries = 2) {
function downloadStream (line 324) | function downloadStream(id, dlToken) {
function download (line 337) | async function download(id, dlToken, onprogress, canceller) {
function tryDownload (line 366) | async function tryDownload(id, dlToken, onprogress, canceller, tries = 2) {
function downloadFile (line 378) | function downloadFile(id, dlToken, onprogress) {
function getFileList (line 391) | async function getFileList(bearerToken, kid) {
function setFileList (line 401) | async function setFileList(bearerToken, kid, data) {
function sendMetrics (line 411) | function sendMetrics(blob) {
function getConstants (line 422) | async function getConstants() {
function reportLink (line 433) | async function reportLink(id, keychain, reason) {
function getDownloadToken (line 450) | async function getDownloadToken(id, keychain) {
function downloadDone (line 465) | async function downloadDone(id, dlToken) {
FILE: app/archive.js
function isDupe (line 3) | function isDupe(newFile, array) {
class Archive (line 16) | class Archive {
method constructor (line 17) | constructor(files = [], defaultTimeLimit = 86400) {
method name (line 25) | get name() {
method type (line 29) | get type() {
method size (line 33) | get size() {
method numFiles (line 37) | get numFiles() {
method manifest (line 41) | get manifest() {
method stream (line 51) | get stream() {
method addFiles (line 55) | addFiles(files, maxSize, maxFiles) {
method remove (line 70) | remove(file) {
method clear (line 77) | clear() {
FILE: app/capabilities.js
function checkCrypto (line 4) | async function checkCrypto() {
function checkStreams (line 58) | function checkStreams() {
function polyfillStreams (line 69) | async function polyfillStreams() {
function getCapabilities (line 78) | async function getCapabilities() {
FILE: app/controller.js
function render (line 14) | function render() {
function checkFiles (line 18) | async function checkFiles() {
function updateProgress (line 26) | function updateProgress() {
FILE: app/crc32.js
constant LOOKUP (line 1) | const LOOKUP = Int32Array.from([
FILE: app/ece.js
constant NONCE_LENGTH (line 4) | const NONCE_LENGTH = 12;
constant TAG_LENGTH (line 5) | const TAG_LENGTH = 16;
constant KEY_LENGTH (line 6) | const KEY_LENGTH = 16;
constant MODE_ENCRYPT (line 7) | const MODE_ENCRYPT = 'encrypt';
constant MODE_DECRYPT (line 8) | const MODE_DECRYPT = 'decrypt';
constant ECE_RECORD_SIZE (line 9) | const ECE_RECORD_SIZE = 1024 * 64;
function generateSalt (line 13) | function generateSalt(len) {
class ECETransformer (line 19) | class ECETransformer {
method constructor (line 20) | constructor(mode, ikm, rs, salt) {
method generateKey (line 30) | async generateKey() {
method generateNonceBase (line 56) | async generateNonceBase() {
method generateNonce (line 87) | generateNonce(seq) {
method pad (line 98) | pad(data, isLast) {
method unpad (line 113) | unpad(data, isLast) {
method createHeader (line 131) | createHeader() {
method readHeader (line 137) | readHeader(buffer) {
method encryptRecord (line 150) | async encryptRecord(buffer, seq, isLast) {
method decryptRecord (line 160) | async decryptRecord(buffer, seq, isLast) {
method start (line 175) | async start(controller) {
method transformPrevChunk (line 185) | async transformPrevChunk(isLast, controller) {
method transform (line 208) | async transform(chunk, controller) {
method flush (line 216) | async flush(controller) {
class StreamSlicer (line 224) | class StreamSlicer {
method constructor (line 225) | constructor(rs, mode) {
method send (line 233) | send(buf, controller) {
method transform (line 243) | transform(chunk, controller) {
method flush (line 273) | flush(controller) {
function encryptStream (line 286) | function encryptStream(
function decryptStream (line 302) | function decryptStream(input, key, rs = ECE_RECORD_SIZE) {
FILE: app/experiments.js
function luckyNumber (line 23) | function luckyNumber(str) {
function checkExperiments (line 27) | function checkExperiments(state, emitter) {
function initialize (line 37) | function initialize(state, emitter) {
FILE: app/fileReceiver.js
class FileReceiver (line 15) | class FileReceiver extends Nanobus {
method constructor (line 16) | constructor(fileInfo) {
method id (line 27) | get id() {
method progressRatio (line 31) | get progressRatio() {
method progressIndefinite (line 35) | get progressIndefinite() {
method sizes (line 39) | get sizes() {
method cancel (line 46) | cancel() {
method reset (line 52) | reset() {
method getMetadata (line 58) | async getMetadata() {
method reportLink (line 68) | async reportLink(reason) {
method sendMessageToSw (line 72) | sendMessageToSw(msg) {
method downloadBlob (line 90) | async downloadBlob(noSave = false) {
method downloadStream (line 130) | async downloadStream(noSave = false) {
method download (line 224) | async download({ stream, storage, noSave }) {
function saveFile (line 240) | async function saveFile(file) {
FILE: app/fileSender.js
class FileSender (line 8) | class FileSender extends Nanobus {
method constructor (line 9) | constructor() {
method progressRatio (line 15) | get progressRatio() {
method progressIndefinite (line 19) | get progressIndefinite() {
method sizes (line 25) | get sizes() {
method reset (line 32) | reset() {
method cancel (line 39) | cancel() {
method upload (line 46) | async upload(archive, bearerToken) {
FILE: app/fxa.js
function getOtherInfo (line 7) | function getOtherInfo(enc) {
function concatKdf (line 26) | async function concatKdf(key, enc) {
function prepareScopedBundleKey (line 41) | async function prepareScopedBundleKey(storage) {
function decryptBundle (line 62) | async function decryptBundle(storage, bundle) {
function preparePkce (line 133) | async function preparePkce(storage) {
function deriveFileListKey (line 143) | async function deriveFileListKey(ikm) {
function getFileListKey (line 170) | async function getFileListKey(storage, bundle) {
FILE: app/keychain.js
class Keychain (line 6) | class Keychain {
method constructor (line 7) | constructor(secretKeyB64, nonce) {
method nonce (line 57) | get nonce() {
method nonce (line 61) | set nonce(n) {
method setPassword (line 67) | setPassword(password, shareUrl) {
method setAuthKey (line 91) | setAuthKey(authKeyB64) {
method authKeyB64 (line 104) | async authKeyB64() {
method authHeader (line 110) | async authHeader() {
method encryptMetadata (line 122) | async encryptMetadata(metadata) {
method encryptStream (line 143) | encryptStream(plainStream) {
method decryptStream (line 147) | decryptStream(cryptotext) {
method decryptMetadata (line 151) | async decryptMetadata(ciphertext) {
FILE: app/locale.js
function makeBundle (line 3) | function makeBundle(locale, ftl) {
function getTranslator (line 9) | async function getTranslator(locale) {
FILE: app/metrics.js
constant HOUR (line 7) | const HOUR = 1000 * 60 * 60;
function initialize (line 12) | function initialize(state, emitter) {
function sizeOrder (line 36) | function sizeOrder(n) {
function submitEvents (line 40) | function submitEvents() {
function addEvent (line 61) | async function addEvent(event_type, event_properties) {
function cancelledUpload (line 86) | function cancelledUpload(archive, duration) {
function completedUpload (line 98) | function completedUpload(archive, duration) {
function stoppedUpload (line 110) | function stoppedUpload(archive, duration = 0) {
function stoppedDownload (line 122) | function stoppedDownload(params) {
function completedDownload (line 131) | function completedDownload(params) {
function deletedUpload (line 140) | function deletedUpload(ownedFile) {
function experimentEvent (line 148) | function experimentEvent(params) {
function submittedSignup (line 152) | function submittedSignup(params) {
function canceledSignup (line 159) | function canceledSignup(params) {
function loggedOut (line 166) | function loggedOut(params) {
FILE: app/ownedFile.js
class OwnedFile (line 5) | class OwnedFile {
method constructor (line 6) | constructor(obj) {
method hasPassword (line 27) | get hasPassword() {
method expired (line 31) | get expired() {
method setPassword (line 35) | async setPassword(password) {
method del (line 49) | del() {
method changeLimit (line 53) | changeLimit(dlimit, user = {}) {
method updateDownloadCount (line 61) | async updateDownloadCount() {
method toJSON (line 77) | toJSON() {
FILE: app/pasteManager.js
function getString (line 1) | function getString(item) {
FILE: app/serviceWorker.js
constant IMAGES (line 11) | const IMAGES = /.*\.(png|svg|jpg)$/;
constant VERSIONED_ASSET (line 12) | const VERSIONED_ASSET = /\.[A-Fa-f0-9]{8}\.(js|css|png|svg|jpg)(#\w+)?$/;
constant DOWNLOAD_URL (line 13) | const DOWNLOAD_URL = /\/api\/download\/([A-Fa-f0-9]{4,})/;
constant FONT (line 14) | const FONT = /\.woff2?$/;
function decryptStream (line 24) | async function decryptStream(id) {
function precache (line 86) | async function precache() {
function cleanCache (line 98) | async function cleanCache() {
function cacheable (line 107) | function cacheable(url) {
function cachedOrFetched (line 111) | async function cachedOrFetched(req) {
FILE: app/storage.js
class Mem (line 4) | class Mem {
method constructor (line 5) | constructor() {
method length (line 9) | get length() {
method getItem (line 13) | getItem(key) {
method setItem (line 17) | setItem(key, value) {
method removeItem (line 21) | removeItem(key) {
method key (line 25) | key(i) {
class Storage (line 30) | class Storage {
method constructor (line 31) | constructor() {
method loadFiles (line 41) | loadFiles() {
method id (line 62) | get id() {
method totalDownloads (line 71) | get totalDownloads() {
method totalDownloads (line 74) | set totalDownloads(n) {
method totalUploads (line 77) | get totalUploads() {
method totalUploads (line 80) | set totalUploads(n) {
method referrer (line 83) | get referrer() {
method referrer (line 86) | set referrer(str) {
method enrolled (line 89) | get enrolled() {
method enroll (line 93) | enroll(id, variant) {
method files (line 99) | get files() {
method user (line 105) | get user() {
method user (line 113) | set user(info) {
method getFileById (line 117) | getFileById(id) {
method get (line 121) | get(id) {
method set (line 125) | set(id, value) {
method remove (line 129) | remove(property) {
method addFile (line 136) | addFile(file) {
method writeFile (line 141) | writeFile(file) {
method writeFiles (line 145) | writeFiles() {
method clearLocalFiles (line 149) | clearLocalFiles() {
method merge (line 154) | async merge(files = []) {
method setDownloadToken (line 185) | setDownloadToken(id, token) {
method getDownloadToken (line 200) | getDownloadToken(id) {
method pruneTokens (line 208) | pruneTokens() {
FILE: app/streams.js
function transformStream (line 3) | function transformStream(readable, transformer, oncancel) {
class BlobStreamController (line 43) | class BlobStreamController {
method constructor (line 44) | constructor(blob, size) {
method pull (line 50) | pull(controller) {
function blobStream (line 71) | function blobStream(blob, size) {
class ConcatStreamController (line 75) | class ConcatStreamController {
method constructor (line 76) | constructor(streams) {
method nextReader (line 83) | nextReader() {
method pull (line 88) | async pull(controller) {
function concatStream (line 101) | function concatStream(streams) {
FILE: app/ui/account.js
class Account (line 4) | class Account extends Component {
method constructor (line 5) | constructor(name, state, emit) {
method avatarClick (line 15) | avatarClick(event) {
method hideMenu (line 22) | hideMenu(event) {
method login (line 28) | login(event) {
method logout (line 33) | logout(event) {
method changed (line 38) | changed() {
method setLocal (line 42) | setLocal() {
method update (line 50) | update() {
method createElement (line 54) | createElement() {
FILE: app/ui/archiveTile.js
function expiryInfo (line 16) | function expiryInfo(translate, archive) {
function password (line 28) | function password(state) {
function fileInfo (line 112) | function fileInfo(file, action) {
function archiveInfo (line 128) | function archiveInfo(archive, action) {
function archiveDetails (line 144) | function archiveDetails(translate, archive) {
function copy (line 252) | function copy(event) {
function del (line 263) | function del(event) {
function share (line 268) | async function share(event) {
function focus (line 347) | function focus(event) {
function blur (line 351) | function blur(event) {
function upload (line 357) | function upload(event) {
function add (line 366) | function add(event) {
function remove (line 378) | function remove(file, desc) {
function cancel (line 427) | function cancel(event) {
function focus (line 500) | function focus(event) {
function blur (line 504) | function blur(event) {
function add (line 508) | function add(event) {
function toggleDownloadEnabled (line 561) | function toggleDownloadEnabled(event) {
function download (line 568) | function download(event) {
FILE: app/ui/copyDialog.js
function copy (line 41) | function copy(event) {
FILE: app/ui/download.js
constant BIG_SIZE (line 10) | const BIG_SIZE = 1024 * 1024 * 256;
function createFileInfo (line 12) | function createFileInfo(state) {
function downloading (line 21) | function downloading(state, emit) {
function preview (line 34) | function preview(state, emit) {
FILE: app/ui/downloadDialog.js
function toggleDownloadEnabled (line 44) | function toggleDownloadEnabled(event) {
function download (line 51) | function download(event) {
FILE: app/ui/downloadPassword.js
function inputChanged (line 65) | function inputChanged(event) {
function checkPassword (line 81) | function checkPassword(event) {
FILE: app/ui/footer.js
class Footer (line 4) | class Footer extends Component {
method constructor (line 5) | constructor(name, state) {
method update (line 10) | update() {
method createElement (line 14) | createElement() {
FILE: app/ui/header.js
class Header (line 7) | class Header extends Component {
method constructor (line 8) | constructor(name, state, emit) {
method update (line 15) | update() {
method createElement (line 19) | createElement() {
FILE: app/ui/modal.js
function close (line 18) | function close(event) {
FILE: app/ui/noStreams.js
function optionChanged (line 67) | function optionChanged(event) {
function submit (line 88) | function submit(event) {
FILE: app/ui/report.js
constant REPORTABLES (line 4) | const REPORTABLES = ['Malware', 'Pii', 'Abuse'];
function optionChanged (line 98) | function optionChanged(event) {
function report (line 104) | function report(event) {
FILE: app/ui/selectbox.js
function choose (line 23) | function choose(event) {
FILE: app/ui/shareDialog.js
function share (line 40) | async function share(event) {
FILE: app/ui/unsupported.js
function outdatedStrings (line 43) | function outdatedStrings(state) {
function unsupportedStrings (line 51) | function unsupportedStrings(state) {
FILE: app/user.js
function hashId (line 13) | async function hashId(id) {
class User (line 22) | class User {
method constructor (line 23) | constructor(storage, limits, authConfig) {
method info (line 30) | get info() {
method info (line 34) | set info(data) {
method firstAction (line 39) | get firstAction() {
method firstAction (line 43) | set firstAction(action) {
method surveyed (line 47) | get surveyed() {
method surveyed (line 51) | set surveyed(yes) {
method avatar (line 55) | get avatar() {
method name (line 63) | get name() {
method email (line 67) | get email() {
method loggedIn (line 71) | get loggedIn() {
method bearerToken (line 75) | get bearerToken() {
method refreshToken (line 79) | get refreshToken() {
method maxSize (line 83) | get maxSize() {
method maxExpireSeconds (line 89) | get maxExpireSeconds() {
method maxDownloads (line 95) | get maxDownloads() {
method loginRequired (line 101) | get loginRequired() {
method metricId (line 105) | async metricId() {
method deviceId (line 109) | async deviceId() {
method startAuthFlow (line 113) | async startAuthFlow(trigger, utms = {}) {
method login (line 139) | async login(email) {
method finishLogin (line 178) | async finishLogin(code, state) {
method refresh (line 210) | async refresh() {
method logout (line 239) | async logout() {
method syncFileList (line 271) | async syncFileList() {
method toJSON (line 319) | toJSON() {
FILE: app/utils.js
function arrayToB64 (line 10) | function arrayToB64(array) {
function b64ToArray (line 18) | function b64ToArray(str) {
function locale (line 22) | function locale() {
function loadShim (line 26) | function loadShim(polyfill) {
function isFile (line 36) | function isFile(id) {
function copyToClipboard (line 40) | function copyToClipboard(str) {
constant LOCALIZE_NUMBERS (line 61) | const LOCALIZE_NUMBERS = !!(
constant UNITS (line 68) | const UNITS = ['bytes', 'kb', 'mb', 'gb'];
function bytes (line 69) | function bytes(num) {
function percent (line 93) | function percent(ratio) {
function number (line 104) | function number(n) {
function allowedCopy (line 111) | function allowedCopy() {
function delay (line 116) | function delay(delay = 100) {
function fadeOut (line 120) | function fadeOut(selector) {
function openLinksInNewTab (line 127) | function openLinksInNewTab(links, should = true) {
function browserName (line 143) | function browserName() {
function streamToArrayBuffer (line 173) | async function streamToArrayBuffer(stream, size) {
function list (line 204) | function list(items, ulStyle = '', liStyle = '') {
function secondsToL10nId (line 218) | function secondsToL10nId(seconds) {
function timeLeft (line 228) | function timeLeft(milliseconds) {
function platform (line 258) | function platform() {
constant ECE_RECORD_SIZE (line 265) | const ECE_RECORD_SIZE = 1024 * 64;
constant TAG_LENGTH (line 266) | const TAG_LENGTH = 16;
function encryptedSize (line 267) | function encryptedSize(size, rs = ECE_RECORD_SIZE, tagLength = TAG_LENGT...
function setTranslate (line 275) | function setTranslate(t) {
function concat (line 279) | function concat(b1, b2) {
FILE: app/zip.js
function dosDateTime (line 5) | function dosDateTime(dateTime = new Date()) {
class File (line 18) | class File {
method constructor (line 19) | constructor(info) {
method header (line 27) | get header() {
method dataDescriptor (line 47) | get dataDescriptor() {
method directoryRecord (line 57) | directoryRecord(offset) {
method byteLength (line 83) | get byteLength() {
method append (line 87) | append(data, controller) {
function centralDirectory (line 99) | function centralDirectory(files, controller) {
function eod (line 112) | function eod(fileCount, directorySize, directoryOffset) {
class ZipStreamController (line 126) | class ZipStreamController {
method constructor (line 127) | constructor(files, source) {
method nextFile (line 136) | nextFile() {
method pull (line 140) | async pull(controller) {
class Zip (line 168) | class Zip {
method constructor (line 169) | constructor(manifest, source) {
method stream (line 174) | get stream() {
method size (line 178) | get size() {
FILE: build/android_index_plugin.js
constant NAME (line 3) | const NAME = 'AndroidIndexPlugin';
function chunkFileNames (line 5) | function chunkFileNames(compilation) {
class AndroidIndexPlugin (line 16) | class AndroidIndexPlugin {
method apply (line 17) | apply(compiler) {
FILE: build/version_plugin.js
class VersionPlugin (line 18) | class VersionPlugin {
method apply (line 19) | apply(compiler) {
FILE: common/assets.js
function getAsset (line 14) | function getAsset(name) {
function setPrefix (line 18) | function setPrefix(name) {
function getMatches (line 22) | function getMatches(match) {
function getManifest (line 33) | function getManifest() {
FILE: common/generate_asset_map.js
function kv (line 15) | function kv(f) {
FILE: ios/ios.js
constant MAXFILESIZE (line 3) | const MAXFILESIZE = 1024 * 1024 * 1024 * 2;
function dom (line 8) | function dom(tagName, attributes, children = []) {
function uploadComplete (line 33) | function uploadComplete(file) {
function upload (line 86) | function upload(event) {
function render (line 102) | function render() {
function sendBase64EncodedFromSwift (line 146) | function sendBase64EncodedFromSwift(encoded) {
FILE: postcss.config.js
class TailwindExtractor (line 1) | class TailwindExtractor {
method extract (line 2) | static extract(content) {
FILE: scripts/lint-locales.js
function filterErrors (line 31) | function filterErrors(details) {
function getLocales (line 43) | function getLocales() {
FILE: server/amplitude.js
constant HOUR (line 6) | const HOUR = 1000 * 60 * 60;
function truncateToHour (line 8) | function truncateToHour(timestamp) {
function orderOfMagnitude (line 12) | function orderOfMagnitude(n) {
function userId (line 16) | function userId(fileId, ownerId) {
function statUploadEvent (line 23) | function statUploadEvent(data) {
function statDownloadEvent (line 46) | function statDownloadEvent(data) {
function statDeleteEvent (line 65) | function statDeleteEvent(data) {
function statReportEvent (line 84) | function statReportEvent(data) {
function clientEvent (line 105) | function clientEvent(
function sendBatch (line 166) | async function sendBatch(events, timeout = 1000) {
FILE: server/bin/dev.js
constant ID_REGEX (line 10) | const ID_REGEX = '([0-9a-fA-F]{10, 16})';
function android (line 21) | function android(req, res) {
FILE: server/fxa.js
constant KEY_SCOPE (line 4) | const KEY_SCOPE = config.fxa_key_scope;
function getFxaConfig (line 8) | async function getFxaConfig() {
FILE: server/keychain.js
method constructor (line 8) | constructor(secretKeyB64) {
method decryptMetadata (line 40) | async decryptMetadata(ciphertext) {
FILE: server/limiter.js
class Limiter (line 3) | class Limiter extends Transform {
method constructor (line 4) | constructor(limit) {
method _transform (line 10) | _transform(chunk, encoding, callback) {
FILE: server/locale.js
function makeBundle (line 7) | function makeBundle(locale) {
FILE: server/metadata.js
function makeToken (line 3) | function makeToken(secret, counter) {
class Metadata (line 9) | class Metadata {
method constructor (line 10) | constructor(obj, storage) {
method getDownloadToken (line 26) | async getDownloadToken() {
method verifyDownloadToken (line 38) | async verifyDownloadToken(token) {
FILE: server/middleware/language.js
function allLangs (line 13) | function allLangs() {
FILE: server/routes/filelist.js
function id (line 6) | function id(user, kid) {
method get (line 15) | async get(req, res) {
method post (line 30) | async post(req, res) {
FILE: server/routes/index.js
constant IS_DEV (line 13) | const IS_DEV = config.env === 'development';
constant ID_REGEX (line 14) | const ID_REGEX = '([0-9a-fA-F]{10,16})';
FILE: server/routes/pages.js
function stripEvents (line 5) | function stripEvents(str) {
FILE: server/storage/fs.js
class FSStorage (line 6) | class FSStorage {
method constructor (line 7) | constructor(config, log) {
method length (line 13) | async length(id) {
method getStream (line 18) | getStream(id) {
method set (line 22) | set(id, file) {
method del (line 38) | async del(id) {
method ping (line 46) | ping() {
FILE: server/storage/gcs.js
class GCSStorage (line 4) | class GCSStorage {
method constructor (line 5) | constructor(config, log) {
method length (line 10) | async length(id) {
method getStream (line 15) | getStream(id) {
method set (line 19) | set(id, file) {
method del (line 33) | del(id) {
method ping (line 37) | ping() {
FILE: server/storage/index.js
function getPrefix (line 6) | function getPrefix(seconds) {
class DB (line 10) | class DB {
method constructor (line 11) | constructor(config) {
method ttl (line 30) | async ttl(id) {
method getPrefixedInfo (line 35) | async getPrefixedInfo(id) {
method length (line 49) | async length(id) {
method get (line 54) | async get(id) {
method set (line 63) | async set(id, file, meta, expireSeconds = config.default_expire_second...
method setField (line 75) | setField(id, key, value) {
method incrementField (line 79) | async incrementField(id, key, increment = 1) {
method kill (line 83) | async kill(id) {
method flag (line 91) | async flag(id) {
method del (line 96) | async del(id) {
method ping (line 102) | async ping() {
method metadata (line 107) | async metadata(id) {
FILE: server/storage/s3.js
constant AWS (line 1) | const AWS = require('aws-sdk');
class S3Storage (line 3) | class S3Storage {
method constructor (line 4) | constructor(config, log) {
method length (line 16) | async length(id) {
method getStream (line 23) | getStream(id) {
method set (line 27) | set(id, file) {
method del (line 37) | del(id) {
method ping (line 41) | ping() {
FILE: test/backend/auth-tests.js
function request (line 10) | function request(id, auth) {
function response (line 17) | function response() {
FILE: test/backend/delete-tests.js
function request (line 9) | function request(id) {
function response (line 15) | function response() {
FILE: test/backend/info-tests.js
function request (line 8) | function request(id, meta) {
function response (line 15) | function response() {
FILE: test/backend/language-tests.js
function request (line 12) | function request(acceptLang) {
FILE: test/backend/metadata-tests.js
function request (line 9) | function request(id, meta = {}) {
function response (line 16) | function response() {
FILE: test/backend/owner-tests.js
function request (line 9) | function request(id, owner_token) {
function response (line 16) | function response() {
FILE: test/backend/params-tests.js
function request (line 8) | function request(id) {
function response (line 16) | function response() {
FILE: test/backend/password-tests.js
function request (line 8) | function request(id, body) {
function response (line 15) | function response() {
FILE: test/backend/s3-tests.js
function resolvedPromise (line 5) | function resolvedPromise(val) {
function rejectedPromise (line 11) | function rejectedPromise(err) {
FILE: test/backend/storage-tests.js
class MockStorage (line 5) | class MockStorage {
method length (line 6) | length() {
method getStream (line 9) | getStream() {
method set (line 12) | set() {
method del (line 15) | del() {
method ping (line 18) | ping() {
FILE: test/frontend/index.js
function kv (line 4) | function kv(f) {
FILE: test/frontend/runner.js
function onConsole (line 20) | function onConsole(msg) {
FILE: test/integration/pages/desktop/download_page.js
class DownloadPage (line 4) | class DownloadPage extends Page {
method constructor (line 5) | constructor(path) {
method downloadUsingPassword (line 14) | downloadUsingPassword(password) {
method download (line 21) | download() {
FILE: test/integration/pages/desktop/home_page.js
class HomePage (line 4) | class HomePage extends Page {
method constructor (line 5) | constructor() {
method waitForPageToLoad (line 18) | waitForPageToLoad() {
method showUploadInput (line 25) | showUploadInput() {
FILE: test/integration/pages/desktop/page.js
class Page (line 2) | class Page {
method constructor (line 3) | constructor(path) {
method open (line 7) | open() {
method waitForPageToLoad (line 17) | waitForPageToLoad() {
Condensed preview — 319 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,239K chars).
[
{
"path": ".circleci/config.yml",
"chars": 2362,
"preview": "version: 2.0\njobs:\n test:\n docker:\n - image: circleci/node:12-browsers\n steps:\n - checkout\n - run:"
},
{
"path": ".dockerignore",
"chars": 74,
"preview": ".circleci\n.nyc_output\n.vscode\n.DS_Store\ncoverage\ndocs\nfirefox\nnode_modules"
},
{
"path": ".editorconfig",
"chars": 201,
"preview": "root = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\n\n[*.{js,html,yml,json,handlebars}]\nind"
},
{
"path": ".eslintignore",
"chars": 80,
"preview": "dist\nassets\nfirefox\ncoverage\nandroid/app/build\napp/locale.js\napp/capabilities.js"
},
{
"path": ".eslintrc.yml",
"chars": 578,
"preview": "env:\n es6: true\n node: true\n\nextends:\n - eslint:recommended\n - prettier\n - plugin:node/recommended\n - plugin:secur"
},
{
"path": ".gitattributes",
"chars": 70,
"preview": "public/locales/* linguist-documentation\ndocs/* linguist-documentation\n"
},
{
"path": ".gitignore",
"chars": 284,
"preview": "node_modules\ncoverage\ndist\n.idea\n.DS_Store\n.nyc_output\n.tox\n.pytest_cache\n*.iml\nandroid/app/src/main/assets\nios/send-ios"
},
{
"path": ".htmllintrc",
"chars": 81,
"preview": "{\n \"attr-name-style\": \"dash\",\n \"id-class-style\": \"dash\",\n \"indent-width\": 2\n}\n"
},
{
"path": ".prettierignore",
"chars": 59,
"preview": "dist\nandroid/app/src/main/assets\nandroid/app/build\ncoverage"
},
{
"path": ".stylelintrc",
"chars": 342,
"preview": "extends: stylelint-config-standard\n\nplugins:\n - stylelint-no-unsupported-browser-features\n\nrules:\n plugin/no-unsupport"
},
{
"path": ".vscode/settings.json",
"chars": 3,
"preview": "{\n}"
},
{
"path": "CHANGELOG.md",
"chars": 26930,
"preview": "## Change Log\n\n### v2.5.1 (2018/03/12 19:26 +00:00)\n- [#789](https://github.com/mozilla/send/pull/789) Fixed #775 : Made"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 691,
"preview": "# Community Participation Guidelines\n\nThis repository is governed by Mozilla's code of conduct and etiquette guidelines."
},
{
"path": "CONTRIBUTORS",
"chars": 3491,
"preview": "Abdalrahman Hwoij\nAbhinav Adduri\nAdnan Kičin\nAdolfo Jayme Barrientos\nAlberto Castro\nAlexander Slovesnik\nAlfredos-Panagio"
},
{
"path": "Dockerfile",
"chars": 1268,
"preview": "##\n# Firefox Send - Mozilla\n#\n# License https://github.com/mozilla/send/blob/master/LICENSE\n##\n\n\n# Build project\nFROM no"
},
{
"path": "LICENSE",
"chars": 16726,
"preview": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\""
},
{
"path": "README.md",
"chars": 4356,
"preview": "# Firefox Send\n\n[](https://circleci.com/gh/m"
},
{
"path": "android/.eslintrc.yaml",
"chars": 59,
"preview": "env:\n browser: true\n\nparserOptions:\n sourceType: module\n\n"
},
{
"path": "android/.gitignore",
"chars": 32,
"preview": "local.properties\n.gradle\nbuild\n\n"
},
{
"path": "android/README.md",
"chars": 415,
"preview": "Readme\n=====\n\nThe Send Android app allows you to choose any file from your android device, encrypt it with a password, a"
},
{
"path": "android/android.js",
"chars": 2755,
"preview": "import 'intl-pluralrules';\nimport choo from 'choo';\nimport html from 'choo/html';\nimport * as Sentry from '@sentry/brows"
},
{
"path": "android/app/.gitignore",
"chars": 7,
"preview": "/build\n"
},
{
"path": "android/app/build.gradle",
"chars": 1508,
"preview": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-android-extensions'\n\nandroi"
},
{
"path": "android/app/buildAssets.sh",
"chars": 270,
"preview": "#!/usr/bin/env bash\nif [ -d \"../../node_modules\" ]\nthen\n echo \"node_modules already present.\"\nelse\n echo \"node_modules"
},
{
"path": "android/app/proguard-rules.pro",
"chars": 751,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "android/app/src/main/AndroidManifest.xml",
"chars": 1536,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n package="
},
{
"path": "android/app/src/main/java/org/mozilla/firefoxsend/MainActivity.kt",
"chars": 8740,
"preview": "package org.mozilla.firefoxsend\n\nimport android.annotation.SuppressLint\nimport android.content.ComponentName\nimport andr"
},
{
"path": "android/app/src/main/res/drawable/ic_launcher_foreground.xml",
"chars": 7296,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:aapt=\"http://schemas.android.com/aapt\"\n "
},
{
"path": "android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
"chars": 7495,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:aapt=\"http://schemas.android.com/aapt\"\n"
},
{
"path": "android/app/src/main/res/layout/activity_main.xml",
"chars": 340,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<im.delight.android.webview.AdvancedWebView xmlns:android=\"http://schemas.android"
},
{
"path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 267,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 267,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "android/app/src/main/res/values/colors.xml",
"chars": 208,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"colorPrimary\">#3F51B5</color>\n <color name=\"color"
},
{
"path": "android/app/src/main/res/values/ic_launcher_background.xml",
"chars": 120,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"ic_launcher_background\">#220033</color>\n</resources>"
},
{
"path": "android/app/src/main/res/values/strings.xml",
"chars": 67,
"preview": "<resources>\n <string name=\"app_name\">Send</string>\n</resources>\n"
},
{
"path": "android/app/src/main/res/values/styles.xml",
"chars": 381,
"preview": "<resources>\n\n <!-- Base application theme. -->\n <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">"
},
{
"path": "android/build.gradle",
"chars": 647,
"preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n e"
},
{
"path": "android/gradle/wrapper/gradle-wrapper.properties",
"chars": 233,
"preview": "#Tue Feb 19 08:34:25 EST 2019\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_"
},
{
"path": "android/gradle.properties",
"chars": 726,
"preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
},
{
"path": "android/gradlew",
"chars": 5296,
"preview": "#!/usr/bin/env sh\n\n##############################################################################\n##\n## Gradle start up"
},
{
"path": "android/gradlew.bat",
"chars": 2260,
"preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
},
{
"path": "android/pages/.eslintrc.yaml",
"chars": 59,
"preview": "env:\n browser: true\n\nparserOptions:\n sourceType: module\n\n"
},
{
"path": "android/pages/error.js",
"chars": 228,
"preview": "const html = require('choo/html');\n\nexport default function error(_state, _emit) {\n return html`\n <body>\n <div "
},
{
"path": "android/pages/home.js",
"chars": 1868,
"preview": "const html = require('choo/html');\nconst { list } = require('../../app/utils');\nconst archiveTile = require('../../app/u"
},
{
"path": "android/pages/preferences.js",
"chars": 798,
"preview": "const html = require('choo/html');\n\nimport { setFileProtocolWssUrl, getFileProtocolWssUrl } from '../../app/api';\n\nexpor"
},
{
"path": "android/pages/share.js",
"chars": 1409,
"preview": "const html = require('choo/html');\n\nexport default function uploadComplete(state, emit) {\n const file = state.storage.f"
},
{
"path": "android/pages/upload.js",
"chars": 647,
"preview": "const html = require('choo/html');\n\nexport default function progressBar(state, emit) {\n let percent = 0;\n if (state.tr"
},
{
"path": "android/settings.gradle",
"chars": 15,
"preview": "include ':app'\n"
},
{
"path": "android/stores/intents.js",
"chars": 524,
"preview": "/* eslint-disable no-console */\n\nexport default function intentHandler(state, emitter) {\n window.addEventListener(\n "
},
{
"path": "android/stores/state.js",
"chars": 971,
"preview": "/* eslint-disable no-console */\n\nimport User from '../user';\nimport storage from '../../app/storage';\n\nexport default fu"
},
{
"path": "android/user.js",
"chars": 742,
"preview": "/* global Android */\nimport User from '../app/user';\nimport { deriveFileListKey } from '../app/fxa';\n\nexport default cla"
},
{
"path": "app/.eslintrc.yml",
"chars": 115,
"preview": "env:\n browser: true\n node: true\n\nparserOptions:\n sourceType: module\n\nrules:\n node/no-unsupported-features: off\n"
},
{
"path": "app/api.js",
"chars": 11208,
"preview": "import { arrayToB64, b64ToArray, delay } from './utils';\nimport { ECE_RECORD_SIZE } from './ece';\n\nlet fileProtocolWssUr"
},
{
"path": "app/archive.js",
"chars": 1875,
"preview": "import { blobStream, concatStream } from './streams';\n\nfunction isDupe(newFile, array) {\n for (const file of array) {\n "
},
{
"path": "app/capabilities.js",
"chars": 2622,
"preview": "/* global AUTH_CONFIG */\nimport { browserName, locale } from './utils';\n\nasync function checkCrypto() {\n try {\n cons"
},
{
"path": "app/controller.js",
"chars": 9105,
"preview": "import FileSender from './fileSender';\nimport FileReceiver from './fileReceiver';\nimport { copyToClipboard, delay, openL"
},
{
"path": "app/crc32.js",
"chars": 3875,
"preview": "const LOOKUP = Int32Array.from([\n 0x00000000,\n 0x77073096,\n 0xee0e612c,\n 0x990951ba,\n 0x076dc419,\n 0x706af48f,\n 0"
},
{
"path": "app/dragManager.js",
"chars": 576,
"preview": "export default function(state, emitter) {\n emitter.on('DOMContentLoaded', () => {\n document.body.addEventListener('d"
},
{
"path": "app/ece.js",
"chars": 8054,
"preview": "import { transformStream } from './streams';\nimport { concat } from './utils';\n\nconst NONCE_LENGTH = 12;\nconst TAG_LENGT"
},
{
"path": "app/experiments.js",
"chars": 1393,
"preview": "import hash from 'string-hash';\nimport Account from './ui/account';\n\nconst experiments = {\n signin_button_color: {\n "
},
{
"path": "app/fileReceiver.js",
"chars": 7636,
"preview": "import Nanobus from 'nanobus';\nimport Keychain from './keychain';\nimport { delay, bytes, streamToArrayBuffer } from './u"
},
{
"path": "app/fileSender.js",
"chars": 2698,
"preview": "import Nanobus from 'nanobus';\nimport OwnedFile from './ownedFile';\nimport Keychain from './keychain';\nimport { arrayToB"
},
{
"path": "app/fxa.js",
"chars": 4412,
"preview": "/* global AUTH_CONFIG */\nimport { arrayToB64, b64ToArray, concat } from './utils';\n\nconst encoder = new TextEncoder();\nc"
},
{
"path": "app/keychain.js",
"chars": 3826,
"preview": "import { arrayToB64, b64ToArray } from './utils';\nimport { decryptStream, encryptStream } from './ece.js';\nconst encoder"
},
{
"path": "app/locale.js",
"chars": 732,
"preview": "import { FluentBundle } from '@fluent/bundle';\n\nfunction makeBundle(locale, ftl) {\n const bundle = new FluentBundle(loc"
},
{
"path": "app/main.css",
"chars": 5894,
"preview": "@tailwind base;\n\nhtml {\n line-height: 1.15;\n}\n\n@tailwind components;\n\n:not(input) {\n -webkit-user-select: none;\n -moz"
},
{
"path": "app/main.js",
"chars": 2099,
"preview": "/* global DEFAULTS LIMITS PREFS */\nimport 'core-js';\nimport 'fast-text-encoding'; // MS Edge support\nimport 'intl-plural"
},
{
"path": "app/metrics.js",
"chars": 4446,
"preview": "import storage from './storage';\nimport { platform, locale } from './utils';\nimport { sendMetrics } from './api';\n\nlet a"
},
{
"path": "app/ownedFile.js",
"chars": 2500,
"preview": "import Keychain from './keychain';\nimport { arrayToB64 } from './utils';\nimport { del, fileInfo, setParams, setPassword "
},
{
"path": "app/pasteManager.js",
"chars": 1215,
"preview": "function getString(item) {\n return new Promise(resolve => {\n item.getAsString(resolve);\n });\n}\n\nexport default func"
},
{
"path": "app/readme.md",
"chars": 553,
"preview": "# Application Code\n\n`app/` contains the browser code that gets bundled into `app.[hash].js`. It's got all the logic, cry"
},
{
"path": "app/routes.js",
"chars": 841,
"preview": "const choo = require('choo');\nconst download = require('./ui/download');\nconst body = require('./ui/body');\n\nmodule.expo"
},
{
"path": "app/serviceWorker.js",
"chars": 4621,
"preview": "import assets from '../common/assets';\nimport { version } from '../package.json';\nimport Keychain from './keychain';\nimp"
},
{
"path": "app/storage.js",
"chars": 4827,
"preview": "import { arrayToB64, isFile } from './utils';\nimport OwnedFile from './ownedFile';\n\nclass Mem {\n constructor() {\n th"
},
{
"path": "app/streams.js",
"chars": 2571,
"preview": "/* global TransformStream */\n\nexport function transformStream(readable, transformer, oncancel) {\n try {\n return read"
},
{
"path": "app/ui/account.js",
"chars": 2994,
"preview": "const html = require('choo/html');\nconst Component = require('choo/component');\n\nclass Account extends Component {\n con"
},
{
"path": "app/ui/archiveTile.js",
"chars": 17052,
"preview": "/* global Android */\n\nconst html = require('choo/html');\nconst raw = require('choo/html/raw');\nconst assets = require('."
},
{
"path": "app/ui/blank.js",
"chars": 363,
"preview": "const html = require('choo/html');\n\nmodule.exports = function() {\n return html`\n <main class=\"main\">\n <section\n"
},
{
"path": "app/ui/body.js",
"chars": 570,
"preview": "const html = require('choo/html');\nconst Header = require('./header');\nconst Footer = require('./footer');\n\nmodule.expor"
},
{
"path": "app/ui/copyDialog.js",
"chars": 1580,
"preview": "const html = require('choo/html');\nconst { copyToClipboard } = require('../utils');\n\nmodule.exports = function(name, url"
},
{
"path": "app/ui/download.js",
"chars": 3750,
"preview": "/* global downloadMetadata */\nconst html = require('choo/html');\nconst assets = require('../../common/assets');\nconst ar"
},
{
"path": "app/ui/downloadCompleted.js",
"chars": 1036,
"preview": "const html = require('choo/html');\nconst assets = require('../../common/assets');\n\nmodule.exports = function(state) {\n "
},
{
"path": "app/ui/downloadDialog.js",
"chars": 1737,
"preview": "const html = require('choo/html');\n\nmodule.exports = function() {\n return function(state, emit, close) {\n const arch"
},
{
"path": "app/ui/downloadPassword.js",
"chars": 3039,
"preview": "const html = require('choo/html');\n\nmodule.exports = function(state, emit) {\n const fileInfo = state.fileInfo;\n const "
},
{
"path": "app/ui/error.js",
"chars": 1105,
"preview": "const html = require('choo/html');\nconst assets = require('../../common/assets');\nconst modal = require('./modal');\n\nmod"
},
{
"path": "app/ui/expiryOptions.js",
"chars": 1955,
"preview": "const html = require('choo/html');\nconst raw = require('choo/html/raw');\nconst { secondsToL10nId } = require('../utils')"
},
{
"path": "app/ui/footer.js",
"chars": 508,
"preview": "const html = require('choo/html');\nconst Component = require('choo/component');\n\nclass Footer extends Component {\n cons"
},
{
"path": "app/ui/header.js",
"chars": 1483,
"preview": "const html = require('choo/html');\nconst Component = require('choo/component');\nconst Account = require('./account');\nco"
},
{
"path": "app/ui/home.js",
"chars": 1240,
"preview": "const html = require('choo/html');\nconst { list } = require('../utils');\nconst archiveTile = require('./archiveTile');\nc"
},
{
"path": "app/ui/intro.js",
"chars": 660,
"preview": "const html = require('choo/html');\nconst assets = require('../../common/assets');\n\nmodule.exports = function intro(state"
},
{
"path": "app/ui/modal.js",
"chars": 626,
"preview": "const html = require('choo/html');\n\nmodule.exports = function(state, emit) {\n return html`\n <send-modal\n class="
},
{
"path": "app/ui/noStreams.js",
"chars": 3753,
"preview": "const html = require('choo/html');\nconst { bytes } = require('../utils');\nconst assets = require('../../common/assets');"
},
{
"path": "app/ui/notFound.js",
"chars": 1241,
"preview": "const html = require('choo/html');\nconst assets = require('../../common/assets');\nconst modal = require('./modal');\n\nmod"
},
{
"path": "app/ui/okDialog.js",
"chars": 556,
"preview": "const html = require('choo/html');\n\nmodule.exports = function(message) {\n return function(state, emit, close) {\n ret"
},
{
"path": "app/ui/report.js",
"chars": 3733,
"preview": "const html = require('choo/html');\nconst assets = require('../../common/assets');\n\nconst REPORTABLES = ['Malware', 'Pii'"
},
{
"path": "app/ui/selectbox.js",
"chars": 748,
"preview": "const html = require('choo/html');\n\nmodule.exports = function(selected, options, translate, changed, htmlId) {\n let x ="
},
{
"path": "app/ui/shareDialog.js",
"chars": 1735,
"preview": "const html = require('choo/html');\n\nmodule.exports = function(name, url) {\n const dialog = function(state, emit, close)"
},
{
"path": "app/ui/unsupported.js",
"chars": 1753,
"preview": "const html = require('choo/html');\nconst modal = require('./modal');\n\nmodule.exports = function(state, emit) {\n let str"
},
{
"path": "app/user.js",
"chars": 8575,
"preview": "import assets from '../common/assets';\nimport { getFileList, setFileList } from './api';\nimport { encryptStream, decrypt"
},
{
"path": "app/utils.js",
"chars": 7006,
"preview": "/* global Android */\nlet html;\ntry {\n html = require('choo/html');\n} catch (e) {\n // running in the service worker\n}\nc"
},
{
"path": "app/zip.js",
"chars": 5935,
"preview": "import crc32 from './crc32';\n\nconst encoder = new TextEncoder();\n\nfunction dosDateTime(dateTime = new Date()) {\n const "
},
{
"path": "browserslist",
"chars": 111,
"preview": "last 2 chrome versions\nlast 2 firefox versions\nlast 2 safari versions\nlast 2 edge versions\nedge 18\nfirefox esr\n"
},
{
"path": "build/android_index_plugin.js",
"chars": 1294,
"preview": "const path = require('path');\nconst html = require('choo/html');\nconst NAME = 'AndroidIndexPlugin';\n\nfunction chunkFileN"
},
{
"path": "build/readme.md",
"chars": 351,
"preview": "# Custom Loaders\n\n## Android Index Plugin\n\nGenerates the `index.html` page for the native android client\n\n## Version Plu"
},
{
"path": "build/version_plugin.js",
"chars": 671,
"preview": "const gitRevSync = require('git-rev-sync');\nconst pkg = require('../package.json');\n\nlet commit = 'unknown';\n\ntry {\n co"
},
{
"path": "common/assets.js",
"chars": 1254,
"preview": "const genmap = require('./generate_asset_map');\nconst isServer = typeof genmap === 'function';\nlet prefix = '';\nlet mani"
},
{
"path": "common/generate_asset_map.js",
"chars": 742,
"preview": "/*\n This code is included by both the server and frontend via\n common/assets.js\n\n When included from the server the e"
},
{
"path": "common/readme.md",
"chars": 630,
"preview": "# Common Code\n\nThis directory contains code loaded by both the frontend `app` and backend `server`. The code here can be"
},
{
"path": "docker-compose.yml",
"chars": 321,
"preview": "version: \"3\"\nservices:\n web:\n build: .\n links:\n - redis\n ports:\n - \"1443:1443\"\n environment:\n "
},
{
"path": "docs/CODEOWNERS",
"chars": 78,
"preview": "# flod as main contact for string changes\npublic/locales/en-US/*.ftl @flodolo\n"
},
{
"path": "docs/acceptance-mobile.md",
"chars": 5232,
"preview": "# Send V2 UX Mobile Acceptance and Spec Annotations\n\n`Date Created: 8/20/2018`\n\n## Acceptance Criteria\n\nAdapted from [th"
},
{
"path": "docs/acceptance-web.md",
"chars": 2799,
"preview": "# Send V2 UX Web Acceptance Criteria\n\n## General\n\n- [ ] It should match the spec provided.\n- [ ] It should have a feedba"
},
{
"path": "docs/build.md",
"chars": 1447,
"preview": "Send has two build configurations, development and production. Both can be run via `npm` scripts, `npm start` for develo"
},
{
"path": "docs/deployment.md",
"chars": 2256,
"preview": "## Requirements\nThis document describes how to do a full deployment of Firefox Send on your own Linux server. You will n"
},
{
"path": "docs/docker.md",
"chars": 1138,
"preview": "## Setup\n\nRun `docker build -t send:latest .` to create an image or `docker-compose up` to run a full testable stack. *W"
},
{
"path": "docs/encryption.md",
"chars": 2428,
"preview": "# File Encryption\n\nSend use 128-bit AES-GCM encryption via the [Web Crypto API](https://developer.mozilla.org/en-US/docs"
},
{
"path": "docs/experiments.md",
"chars": 3748,
"preview": "# A/B experiment testing\n\nWe're using Google Analytics Experiments for A/B testing.\n\n## Creating an experiment\n\nNavigate"
},
{
"path": "docs/faq.md",
"chars": 2035,
"preview": "## How big of a file can I transfer with Firefox Send?\n\nThere is a 2.5GB file size limit built in to Send(1GB for non-si"
},
{
"path": "docs/localization.md",
"chars": 1849,
"preview": "# Localization\n\nSend is localized in over 50 languages. We use the [fluent](http://projectfluent.org/) library and store"
},
{
"path": "docs/metrics.md",
"chars": 5008,
"preview": "# Send V2 Metrics Definitions\n\n## Key Value Prop\n\nQuickly and privately transfer large files from any device to any devi"
},
{
"path": "docs/notes/streams.md",
"chars": 1036,
"preview": "# Web Streams\n\n- API\n - https://developer.mozilla.org/en-US/docs/Web/API/Streams_API\n- Reference Implementation\n - htt"
},
{
"path": "docs/takedowns.md",
"chars": 854,
"preview": "## Take-down process\n\nIn cases of a DMCA notice, or other abuse yet to be determined, a file has to be removed from the "
},
{
"path": "ios/generate-bundle.js",
"chars": 588,
"preview": "const child_process = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\n\nchild_process.e"
},
{
"path": "ios/ios.js",
"chars": 3726,
"preview": "/* global window, document, fetch */\n\nconst MAXFILESIZE = 1024 * 1024 * 1024 * 2;\n\nconst EventEmitter = require('events'"
},
{
"path": "ios/send-ios/AppDelegate.swift",
"chars": 2120,
"preview": "//\n// AppDelegate.swift\n// send-ios\n//\n// Created by Donovan Preston on 7/19/18.\n//\n\nimport UIKit\n\n@UIApplicationMain"
},
{
"path": "ios/send-ios/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 1590,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"iphone\",\n \"size\" : \"20x20\",\n \"scale\" : \"2x\"\n },\n {\n \"idiom\""
},
{
"path": "ios/send-ios/Assets.xcassets/Contents.json",
"chars": 62,
"preview": "{\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n }\n}"
},
{
"path": "ios/send-ios/Base.lproj/LaunchScreen.storyboard",
"chars": 1681,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard"
},
{
"path": "ios/send-ios/Base.lproj/Main.storyboard",
"chars": 3025,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3"
},
{
"path": "ios/send-ios/Info.plist",
"chars": 1463,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "ios/send-ios/ViewController.swift",
"chars": 1179,
"preview": "//\n// ViewController.swift\n// send-ios\n//\n// Created by Donovan Preston on 7/19/18.\n//\n\nimport UIKit\nimport WebKit\n\nc"
},
{
"path": "ios/send-ios/assets/index.css",
"chars": 1270,
"preview": "body {\n background: url('background_1.jpg');\n display: flex;\n flex-direction: row;\n flex: auto;\n justify-content: c"
},
{
"path": "ios/send-ios/assets/index.html",
"chars": 391,
"preview": "\n <!DOCTYPE html>\n <html lang=\"en-US\">\n <head>\n <title>Send</title>\n <link href=\"index.css\" rel=\"stylesheet\">\n "
},
{
"path": "ios/send-ios/help.html",
"chars": 66,
"preview": "<doctype html>\n <body>\n HELLO WORLD\n </body>\n</html>\n"
},
{
"path": "ios/send-ios-action-extension/ActionViewController.swift",
"chars": 3150,
"preview": "//\n// ActionViewController.swift\n// send-ios-action-extension\n//\n// Created by Donovan Preston on 7/26/18.\n//\n\nimport"
},
{
"path": "ios/send-ios-action-extension/Base.lproj/MainInterface.storyboard",
"chars": 4766,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3"
},
{
"path": "ios/send-ios-action-extension/Info.plist",
"chars": 2119,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "ios/send-ios.xcodeproj/project.pbxproj",
"chars": 21098,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 50;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "ios/send-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 153,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:send-ios.xcodep"
},
{
"path": "ios/send-ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "l10n.toml",
"chars": 341,
"preview": "# This Source Code Form is subject to the terms of the Mozilla Public\n# License, v. 2.0. If a copy of the MPL was not di"
},
{
"path": "package.json",
"chars": 5867,
"preview": "{\n \"name\": \"firefox-send\",\n \"description\": \"File Sharing Experiment\",\n \"version\": \"3.0.22\",\n \"author\": \"Mozilla (htt"
},
{
"path": "postcss.config.js",
"chars": 770,
"preview": "class TailwindExtractor {\n static extract(content) {\n return content.match(/[A-Za-z0-9-_:/]+/g) || [];\n }\n}\n\nconst "
},
{
"path": "public/contribute.json",
"chars": 725,
"preview": "{\n \"name\": \"firefox-send\",\n \"description\": \"File Sharing Experiment\",\n \"repository\": {\n \"url\": \"https://github.com"
},
{
"path": "public/inter.css",
"chars": 3763,
"preview": "@font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 100;\n font-display: optional;\n src: url('Int"
},
{
"path": "public/locales/an/send.ftl",
"chars": 8966,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Se ye importando…\nencryptingFile = Se y"
},
{
"path": "public/locales/ar/send.ftl",
"chars": 7909,
"preview": "# Send is a brand name and should not be localized.\ntitle = فَيَرفُكس سِنْد\nsiteFeedback = الانطباعات\nimportingFile = يس"
},
{
"path": "public/locales/ast/send.ftl",
"chars": 6903,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importando...\nencryptingFile = Cifrando"
},
{
"path": "public/locales/az/send.ftl",
"chars": 2513,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = Geri dönüş\nimportingFile = İdxal edilir…"
},
{
"path": "public/locales/azz/send.ftl",
"chars": 7955,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = Nikan uelis tikijkuilos tein tiknemilijt"
},
{
"path": "public/locales/be/send.ftl",
"chars": 9391,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Імпартаванне...\nencryptingFile = Зашыфр"
},
{
"path": "public/locales/bn/send.ftl",
"chars": 6941,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = প্রতিক্রিয়া\nimportingFile = ইম্পোর্ট হচ্"
},
{
"path": "public/locales/br/send.ftl",
"chars": 8631,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Oc'h enporzhiañ …\nencryptingFile = Oc'h"
},
{
"path": "public/locales/bs/send.ftl",
"chars": 6530,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteSubtitle = web eksperiment\nsiteFeedback = Povratne "
},
{
"path": "public/locales/ca/send.ftl",
"chars": 9113,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = S'està important…\nencryptingFile = S'es"
},
{
"path": "public/locales/cak/send.ftl",
"chars": 7408,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = Rutzijol\nimportingFile = Tajin nijik…\nen"
},
{
"path": "public/locales/ckb/send.ftl",
"chars": 6955,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = ڕەخنەوپێشنیار\nimportingFile = هاوردەکردن"
},
{
"path": "public/locales/cs/send.ftl",
"chars": 9996,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Probíhá import…\nencryptingFile = Probíh"
},
{
"path": "public/locales/cy/send.ftl",
"chars": 11144,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Mewnforio…\nencryptingFile = Wrthi'n amg"
},
{
"path": "public/locales/da/send.ftl",
"chars": 8543,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importerer…\nencryptingFile = Krypterer…"
},
{
"path": "public/locales/de/send.ftl",
"chars": 9171,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Wird importiert…\nencryptingFile = Wird "
},
{
"path": "public/locales/dsb/send.ftl",
"chars": 9836,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importěrujo se...\nencryptingFile = Kodě"
},
{
"path": "public/locales/el/send.ftl",
"chars": 9289,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Εισαγωγή…\nencryptingFile = Κρυπτογράφησ"
},
{
"path": "public/locales/en-CA/send.ftl",
"chars": 8389,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importing…\nencryptingFile = Encrypting…"
},
{
"path": "public/locales/en-GB/send.ftl",
"chars": 8389,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importing…\nencryptingFile = Encrypting…"
},
{
"path": "public/locales/en-US/send.ftl",
"chars": 8310,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importing…\nencryptingFile = Encrypting…"
},
{
"path": "public/locales/es-AR/send.ftl",
"chars": 8945,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importando…\nencryptingFile = Cifrando…\n"
},
{
"path": "public/locales/es-CL/send.ftl",
"chars": 8924,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importando…\nencryptingFile = Cifrando…\n"
},
{
"path": "public/locales/es-ES/send.ftl",
"chars": 8932,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importando...\nencryptingFile = Cifrando"
},
{
"path": "public/locales/es-MX/send.ftl",
"chars": 6973,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = Comentario\nimportingFile = Importando..."
},
{
"path": "public/locales/et/send.ftl",
"chars": 6806,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = Tagasiside\nimportingFile = Importimine.."
},
{
"path": "public/locales/eu/send.ftl",
"chars": 7134,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = Iritzia\nimportingFile = Inportatzen…\nenc"
},
{
"path": "public/locales/fa/send.ftl",
"chars": 6976,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = بازخورد\nimportingFile = در حال وارد کردن"
},
{
"path": "public/locales/fi/send.ftl",
"chars": 9004,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Tuodaan…\nencryptingFile = Salataan...\nd"
},
{
"path": "public/locales/fr/send.ftl",
"chars": 9355,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importation…\nencryptingFile = Chiffreme"
},
{
"path": "public/locales/fy-NL/send.ftl",
"chars": 8888,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Ymportearje…\nencryptingFile = Fersiferj"
},
{
"path": "public/locales/gn/send.ftl",
"chars": 8681,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Ojegueruhína…\nencryptingFile = Mo’ãmby…"
},
{
"path": "public/locales/gor/send.ftl",
"chars": 2543,
"preview": "# Send is a brand name and should not be localized.\ntitle = Firefox Molawo\nsiteSubtitle = web yimontalo\nsiteFeedback = P"
},
{
"path": "public/locales/he/send.ftl",
"chars": 7904,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = מתבצע ייבוא…\nencryptingFile = מתבצעת הצ"
},
{
"path": "public/locales/hr/send.ftl",
"chars": 9278,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Uvoz…\nencryptingFile = Šifriranje …\ndec"
},
{
"path": "public/locales/hsb/send.ftl",
"chars": 9819,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importuje so...\nencryptingFile = Zakluč"
},
{
"path": "public/locales/hu/send.ftl",
"chars": 8943,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importálás…\nencryptingFile = Titkosítás"
},
{
"path": "public/locales/hus/send.ftl",
"chars": 7274,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = Ka olna' max jant'oj yab u t'ojnal alwa'"
},
{
"path": "public/locales/hy-AM/send.ftl",
"chars": 6967,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = Արձագանք\nimportingFile = Ներմուծում...\ne"
},
{
"path": "public/locales/ia/send.ftl",
"chars": 8884,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importation…\nencryptingFile = Cryptatio"
},
{
"path": "public/locales/id/send.ftl",
"chars": 8390,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Mengimpor…\nencryptingFile = Mengenkrips"
},
{
"path": "public/locales/ig/send.ftl",
"chars": 3778,
"preview": "# Send is a brand name and should not be localized.\ntitle = Firefox Zipu\nimportingFile = Mbubata…\nencryptingFile = ezoro"
},
{
"path": "public/locales/it/send.ftl",
"chars": 8784,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importazione in corso…\nencryptingFile ="
},
{
"path": "public/locales/ixl/send.ftl",
"chars": 2027,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = Aq'a yol sti'\nimportingFile = Eq'otzan\ne"
},
{
"path": "public/locales/ja/send.ftl",
"chars": 6880,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = インポート中...\nencryptingFile = 暗号化中...\ndecr"
},
{
"path": "public/locales/ka/send.ftl",
"chars": 8631,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = გადმოტანა...\nencryptingFile = დაშიფვრა."
},
{
"path": "public/locales/kab/send.ftl",
"chars": 8548,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Akter...\nencryptingFile = Awgelhen...\nd"
},
{
"path": "public/locales/ko/send.ftl",
"chars": 6756,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = 가져오는 중…\nencryptingFile = 암호화 중…\ndecrypt"
},
{
"path": "public/locales/lt/send.ftl",
"chars": 9933,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importuojama…\nencryptingFile = Šifruoja"
},
{
"path": "public/locales/lus/send.ftl",
"chars": 87,
"preview": "encryptingFile = Encrypting...\ndecryptingFile = Decrypting\n\n## Send version 2 strings\n\n"
},
{
"path": "public/locales/meh/send.ftl",
"chars": 6453,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteFeedback = Tu'un jianininu\nimportingFile = Nasia´a…"
},
{
"path": "public/locales/mix/send.ftl",
"chars": 7746,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Ndakiin…\nencryptingFile = Ndasami tu'un"
},
{
"path": "public/locales/ml/send.ftl",
"chars": 7394,
"preview": "# Send is a brand name and should not be localized.\ntitle = ഫയർഫോക്സ് സെൻഡ്\nsiteFeedback = പ്രതികരണം\nimportingFile = ഇറക"
},
{
"path": "public/locales/ms/send.ftl",
"chars": 6488,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nsiteSubtitle = experimen web\nsiteFeedback = Maklum bala"
},
{
"path": "public/locales/nb-NO/send.ftl",
"chars": 8667,
"preview": "# Send is a brand name and should not be localized.\ntitle = Send\nimportingFile = Importerer…\nencryptingFile = Krypterer."
}
]
// ... and 119 more files (download for full content)
About this extraction
This page contains the full source code of the mozilla/send GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 319 files (1.1 MB), approximately 345.2k tokens, and a symbol index with 475 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.