├── .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
├── SendAndroid.iml
├── android.js
├── app
│ ├── .gitignore
│ ├── build.gradle
│ ├── buildAssets.sh
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── ic_launcher-web.png
│ │ ├── java
│ │ └── org
│ │ │ └── mozilla
│ │ │ └── firefoxsend
│ │ │ └── MainActivity.kt
│ │ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_foreground.xml
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.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
├── assets
├── add.svg
├── addfiles.svg
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── blue_file.svg
├── close-16.svg
├── completed.svg
├── copy-16.svg
├── dl.svg
├── error.svg
├── favicon-16x16.png
├── favicon-32x32.png
├── feedback.svg
├── firefox_logo-only.svg
├── icon.svg
├── intro.svg
├── lock.svg
├── master-logo.svg
├── notFound.svg
├── safari-pinned-tab.svg
├── select-arrow.svg
├── share-24.svg
├── user.svg
└── wordmark.svg
├── 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-action-extension
│ ├── ActionViewController.swift
│ ├── Base.lproj
│ │ └── MainInterface.storyboard
│ └── Info.plist
├── send-ios.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ ├── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcuserdata
│ │ │ └── donovan.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcuserdata
│ │ └── donovan.xcuserdatad
│ │ ├── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── send-ios
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── Info.plist
│ ├── ViewController.swift
│ ├── assets
│ ├── background_1.jpg
│ ├── index.css
│ └── index.html
│ └── help.html
├── l10n.toml
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── Inter-Black.woff
├── Inter-Black.woff2
├── Inter-BlackItalic.woff
├── Inter-BlackItalic.woff2
├── Inter-Bold.woff
├── Inter-Bold.woff2
├── Inter-BoldItalic.woff
├── Inter-BoldItalic.woff2
├── Inter-ExtraBold.woff
├── Inter-ExtraBold.woff2
├── Inter-ExtraBoldItalic.woff
├── Inter-ExtraBoldItalic.woff2
├── Inter-ExtraLight.woff
├── Inter-ExtraLight.woff2
├── Inter-ExtraLightItalic.woff
├── Inter-ExtraLightItalic.woff2
├── Inter-Italic.woff
├── Inter-Italic.woff2
├── Inter-Light.woff
├── Inter-Light.woff2
├── Inter-LightItalic.woff
├── Inter-LightItalic.woff2
├── Inter-Medium.woff
├── Inter-Medium.woff2
├── Inter-MediumItalic.woff
├── Inter-MediumItalic.woff2
├── Inter-Regular.woff
├── Inter-Regular.woff2
├── Inter-SemiBold.woff
├── Inter-SemiBold.woff2
├── Inter-SemiBoldItalic.woff
├── Inter-SemiBoldItalic.woff2
├── Inter-Thin.woff
├── Inter-Thin.woff2
├── Inter-ThinItalic.woff
├── Inter-ThinItalic.woff2
├── Inter-italic.var.woff2
├── Inter-upright.var.woff2
├── Inter.var.woff2
├── contribute.json
├── favicon.ico
├── 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
/.dockerignore:
--------------------------------------------------------------------------------
1 | .circleci
2 | .nyc_output
3 | .vscode
4 | .DS_Store
5 | coverage
6 | docs
7 | firefox
8 | node_modules
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 |
8 | [*.{js,html,yml,json,handlebars}]
9 | indent_style = space
10 | indent_size = 2
11 |
12 | [*.toml]
13 | indent_style = space
14 | indent_size = 4
15 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | assets
3 | firefox
4 | coverage
5 | android/app/build
6 | app/locale.js
7 | app/capabilities.js
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | env:
2 | es6: true
3 | node: true
4 |
5 | extends:
6 | - eslint:recommended
7 | - prettier
8 | - plugin:node/recommended
9 | - plugin:security/recommended
10 |
11 | plugins:
12 | - node
13 | - security
14 |
15 | root: true
16 |
17 | rules:
18 | node/no-deprecated-api: off
19 | node/no-unsupported-features/es-syntax: off
20 | node/no-unsupported-features/node-builtins: off
21 | node/no-unpublished-require: off
22 | node/no-unpublished-import: off
23 |
24 | security/detect-non-literal-fs-filename: off
25 | security/detect-object-injection: off
26 |
27 | no-unused-vars: [error, {argsIgnorePattern: "^_|err|event|next|reject"}]
28 | require-atomic-updates: warn
29 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | public/locales/* linguist-documentation
2 | docs/* linguist-documentation
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 | .idea
5 | .DS_Store
6 | .nyc_output
7 | .tox
8 | .pytest_cache
9 | *.iml
10 | android/app/src/main/assets
11 | ios/send-ios/assets/ios.js
12 | ios/send-ios/assets/vendor.js
13 | ios/send-ios.xcodeproj/project.xcworkspace/xcuserdata/*
14 | ios/send-ios.xcodeproj/xcuserdata/*
15 | test/integration/downloads
16 |
--------------------------------------------------------------------------------
/.htmllintrc:
--------------------------------------------------------------------------------
1 | {
2 | "attr-name-style": "dash",
3 | "id-class-style": "dash",
4 | "indent-width": 2
5 | }
6 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | android/app/src/main/assets
3 | android/app/build
4 | coverage
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | extends: stylelint-config-standard
2 |
3 | plugins:
4 | - stylelint-no-unsupported-browser-features
5 |
6 | rules:
7 | plugin/no-unsupported-browser-features: [true, {severity: warning}]
8 |
9 | color-hex-case: lower
10 | declaration-colon-newline-after: null
11 | selector-list-comma-newline-after: null
12 | value-list-comma-newline-after: null
13 | at-rule-no-unknown: null
14 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | }
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Community Participation Guidelines
2 |
3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines.
4 | For more details, please read the
5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
6 |
7 | ## How to Report
8 | 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.
9 |
10 |
16 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ##
2 | # Firefox Send - Mozilla
3 | #
4 | # License https://github.com/mozilla/send/blob/master/LICENSE
5 | ##
6 |
7 |
8 | # Build project
9 | FROM node:12 AS builder
10 | RUN set -x \
11 | # Add user
12 | && addgroup --gid 10001 app \
13 | && adduser --disabled-password \
14 | --gecos '' \
15 | --gid 10001 \
16 | --home /app \
17 | --uid 10001 \
18 | app
19 | COPY --chown=app:app . /app
20 | USER app
21 | WORKDIR /app
22 | RUN set -x \
23 | # Build
24 | && PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm ci \
25 | && npm run build
26 |
27 |
28 | # Main image
29 | FROM node:12-slim
30 | RUN set -x \
31 | # Add user
32 | && addgroup --gid 10001 app \
33 | && adduser --disabled-password \
34 | --gecos '' \
35 | --gid 10001 \
36 | --home /app \
37 | --uid 10001 \
38 | app
39 | RUN apt-get update && apt-get -y install \
40 | git-core \
41 | && rm -rf /var/lib/apt/lists/*
42 | USER app
43 | WORKDIR /app
44 | COPY --chown=app:app package*.json ./
45 | COPY --chown=app:app app app
46 | COPY --chown=app:app common common
47 | COPY --chown=app:app public/locales public/locales
48 | COPY --chown=app:app server server
49 | COPY --chown=app:app --from=builder /app/dist dist
50 |
51 | RUN npm ci --production && npm cache clean --force
52 | RUN mkdir -p /app/.config/configstore
53 | RUN ln -s dist/version.json version.json
54 |
55 | ENV PORT=1443
56 |
57 | EXPOSE ${PORT}
58 |
59 | CMD ["node", "server/bin/prod.js"]
60 |
--------------------------------------------------------------------------------
/android/.eslintrc.yaml:
--------------------------------------------------------------------------------
1 | env:
2 | browser: true
3 |
4 | parserOptions:
5 | sourceType: module
6 |
7 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | local.properties
2 | .gradle
3 | build
4 |
5 |
--------------------------------------------------------------------------------
/android/README.md:
--------------------------------------------------------------------------------
1 | Readme
2 | =====
3 |
4 | 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.
5 |
6 | Building the Send Android app.
7 | =====
8 |
9 | First, install Android Studio. Open the `android` directory in Android Studio, plug in your android phone, and press the run button.
--------------------------------------------------------------------------------
/android/SendAndroid.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion 27
7 | defaultConfig {
8 | applicationId "org.mozilla.firefoxsend"
9 | minSdkVersion 26
10 | targetSdkVersion 27
11 | versionCode 1
12 | versionName "1.0"
13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
14 | }
15 | buildTypes {
16 | release {
17 | minifyEnabled false
18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
19 | }
20 | }
21 | }
22 |
23 | dependencies {
24 | implementation fileTree(dir: 'libs', include: ['*.jar'])
25 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
26 | implementation 'com.android.support:appcompat-v7:27.1.1'
27 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
28 | testImplementation 'junit:junit:4.12'
29 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
30 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
31 | implementation 'com.github.delight-im:Android-AdvancedWebView:v3.0.0'
32 | implementation "org.mozilla.components:service-firefox-accounts:$android_components_version"
33 | }
34 |
35 | task generateAndLinkBundle(type: Exec, description: 'Generate the android.js bundle and link it into the assets directory') {
36 | commandLine './buildAssets.sh'
37 | }
38 |
39 | tasks.withType(JavaCompile) {
40 | compileTask -> compileTask.dependsOn generateAndLinkBundle
41 | }
42 |
--------------------------------------------------------------------------------
/android/app/buildAssets.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | if [ -d "../../node_modules" ]
3 | then
4 | echo "node_modules already present."
5 | else
6 | echo "node_modules not present, running npm install."
7 | npm install
8 | fi
9 | npm run build
10 | rm -rf src/main/assets
11 | mkdir -p src/main/assets
12 | cp -R ../../dist/* src/main/assets
13 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/android/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/android/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/android/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #220033
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Send
3 |
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.3.21'
5 | ext.android_components_version = '0.26.0'
6 | repositories {
7 | google()
8 | jcenter()
9 | }
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:3.3.2'
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.21"
13 | }
14 | }
15 |
16 | allprojects {
17 | repositories {
18 | google()
19 | maven { url "https://maven.mozilla.org/maven2" }
20 | jcenter()
21 | maven { url "https://jitpack.io" }
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
28 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Feb 19 08:34:25 EST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
7 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/android/pages/.eslintrc.yaml:
--------------------------------------------------------------------------------
1 | env:
2 | browser: true
3 |
4 | parserOptions:
5 | sourceType: module
6 |
7 |
--------------------------------------------------------------------------------
/android/pages/error.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 |
3 | export default function error(_state, _emit) {
4 | return html`
5 |
6 |
37 |
38 |
The card contents will be here.
39 |
Expires after: exp
40 | ${input}
41 |
42 |
})
43 | ${copyText}
44 |
45 |
48 |
49 |
50 | `;
51 | }
52 |
--------------------------------------------------------------------------------
/android/pages/upload.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 |
3 | export default function progressBar(state, emit) {
4 | let percent = 0;
5 | if (state.transfer && state.transfer.progress) {
6 | percent = Math.floor(state.transfer.progressRatio * 100);
7 | }
8 | function onclick(e) {
9 | e.preventDefault();
10 | if (state.uploading) {
11 | emit('cancel');
12 | }
13 | emit('pushState', '/');
14 | }
15 | return html`
16 |
17 |
18 |
19 |
${percent}%
20 |
.
21 |
CANCEL
22 |
23 |
24 |
25 | `;
26 | }
27 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/android/stores/intents.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | export default function intentHandler(state, emitter) {
4 | window.addEventListener(
5 | 'message',
6 | event => {
7 | if (typeof event.data !== 'string' || !event.data.startsWith('data:')) {
8 | return;
9 | }
10 | fetch(event.data)
11 | .then(res => res.blob())
12 | .then(blob => {
13 | emitter.emit('addFiles', { files: [blob] });
14 | emitter.emit('upload', {});
15 | })
16 | .catch(e => console.error('ERROR ' + e + ' ' + e.stack));
17 | },
18 | false
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/android/stores/state.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import User from '../user';
4 | import storage from '../../app/storage';
5 |
6 | export default function initialState(state, emitter) {
7 | const files = [];
8 |
9 | Object.assign(state, {
10 | prefix: '/android_asset',
11 | user: new User(storage),
12 | getAsset(name) {
13 | return `${state.prefix}/${name}`;
14 | },
15 | sentry: {
16 | captureException: e => {
17 | console.error('ERROR ' + e + ' ' + e.stack);
18 | }
19 | },
20 | storage: {
21 | files,
22 | remove: function(fileId) {
23 | console.log('REMOVE FILEID', fileId);
24 | },
25 | writeFile: function(file) {
26 | console.log('WRITEFILE', file);
27 | },
28 | addFile: function(file) {
29 | console.log('addfile' + JSON.stringify(file));
30 | files.push(file);
31 | emitter.emit('pushState', `/share/${file.id}`);
32 | },
33 | totalUploads: 0
34 | },
35 | transfer: null,
36 | uploading: false,
37 | settingPassword: false,
38 | passwordSetError: null,
39 | route: '/'
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/android/user.js:
--------------------------------------------------------------------------------
1 | /* global Android */
2 | import User from '../app/user';
3 | import { deriveFileListKey } from '../app/fxa';
4 |
5 | export default class AndroidUser extends User {
6 | constructor(storage, limits) {
7 | super(storage, limits);
8 | }
9 |
10 | async login() {
11 | Android.beginOAuthFlow();
12 | }
13 |
14 | startAuthFlow() {
15 | return Promise.resolve();
16 | }
17 |
18 | async finishLogin(accountInfo) {
19 | const jwks = JSON.parse(accountInfo.keys);
20 | const ikm = jwks['https://identity.mozilla.com/apps/send'].k;
21 | const profile = {
22 | displayName: accountInfo.displayName,
23 | email: accountInfo.email,
24 | avatar: accountInfo.avatar,
25 | access_token: accountInfo.accessToken
26 | };
27 | profile.fileListKey = await deriveFileListKey(ikm);
28 | this.info = profile;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | env:
2 | browser: true
3 | node: true
4 |
5 | parserOptions:
6 | sourceType: module
7 |
8 | rules:
9 | node/no-unsupported-features: off
10 |
--------------------------------------------------------------------------------
/app/archive.js:
--------------------------------------------------------------------------------
1 | import { blobStream, concatStream } from './streams';
2 |
3 | function isDupe(newFile, array) {
4 | for (const file of array) {
5 | if (
6 | newFile.name === file.name &&
7 | newFile.size === file.size &&
8 | newFile.lastModified === file.lastModified
9 | ) {
10 | return true;
11 | }
12 | }
13 | return false;
14 | }
15 |
16 | export default class Archive {
17 | constructor(files = [], defaultTimeLimit = 86400) {
18 | this.files = Array.from(files);
19 | this.defaultTimeLimit = defaultTimeLimit;
20 | this.timeLimit = defaultTimeLimit;
21 | this.dlimit = 1;
22 | this.password = null;
23 | }
24 |
25 | get name() {
26 | return this.files.length > 1 ? 'Send-Archive.zip' : this.files[0].name;
27 | }
28 |
29 | get type() {
30 | return this.files.length > 1 ? 'send-archive' : this.files[0].type;
31 | }
32 |
33 | get size() {
34 | return this.files.reduce((total, file) => total + file.size, 0);
35 | }
36 |
37 | get numFiles() {
38 | return this.files.length;
39 | }
40 |
41 | get manifest() {
42 | return {
43 | files: this.files.map(file => ({
44 | name: file.name,
45 | size: file.size,
46 | type: file.type
47 | }))
48 | };
49 | }
50 |
51 | get stream() {
52 | return concatStream(this.files.map(file => blobStream(file)));
53 | }
54 |
55 | addFiles(files, maxSize, maxFiles) {
56 | if (this.files.length + files.length > maxFiles) {
57 | throw new Error('tooManyFiles');
58 | }
59 | const newFiles = files.filter(
60 | file => file.size > 0 && !isDupe(file, this.files)
61 | );
62 | const newSize = newFiles.reduce((total, file) => total + file.size, 0);
63 | if (this.size + newSize > maxSize) {
64 | throw new Error('fileTooBig');
65 | }
66 | this.files = this.files.concat(newFiles);
67 | return true;
68 | }
69 |
70 | remove(file) {
71 | const index = this.files.indexOf(file);
72 | if (index > -1) {
73 | this.files.splice(index, 1);
74 | }
75 | }
76 |
77 | clear() {
78 | this.files = [];
79 | this.dlimit = 1;
80 | this.timeLimit = this.defaultTimeLimit;
81 | this.password = null;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/app/dragManager.js:
--------------------------------------------------------------------------------
1 | export default function(state, emitter) {
2 | emitter.on('DOMContentLoaded', () => {
3 | document.body.addEventListener('dragover', event => {
4 | if (state.route === '/') {
5 | event.preventDefault();
6 | }
7 | });
8 | document.body.addEventListener('drop', event => {
9 | if (
10 | state.route === '/' &&
11 | !state.uploading &&
12 | event.dataTransfer &&
13 | event.dataTransfer.files
14 | ) {
15 | event.preventDefault();
16 | emitter.emit('addFiles', {
17 | files: Array.from(event.dataTransfer.files)
18 | });
19 | }
20 | });
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/app/experiments.js:
--------------------------------------------------------------------------------
1 | import hash from 'string-hash';
2 | import Account from './ui/account';
3 |
4 | const experiments = {
5 | signin_button_color: {
6 | eligible: function() {
7 | return true;
8 | },
9 | variant: function() {
10 | return ['white-blue', 'blue', 'white-violet', 'violet'][
11 | Math.floor(Math.random() * 4)
12 | ];
13 | },
14 | run: function(variant, state) {
15 | const account = state.cache(Account, 'account');
16 | account.buttonClass = variant;
17 | }
18 | }
19 | };
20 |
21 | //Returns a number between 0 and 1
22 | // eslint-disable-next-line no-unused-vars
23 | function luckyNumber(str) {
24 | return hash(str) / 0xffffffff;
25 | }
26 |
27 | function checkExperiments(state, emitter) {
28 | const all = Object.keys(experiments);
29 | const id = all.find(id => experiments[id].eligible(state));
30 | if (id) {
31 | const variant = experiments[id].variant(state);
32 | state.storage.enroll(id, variant);
33 | experiments[id].run(variant, state, emitter);
34 | }
35 | }
36 |
37 | export default function initialize(state, emitter) {
38 | emitter.on('DOMContentLoaded', () => {
39 | const xp = experiments[state.query.x];
40 | if (xp) {
41 | xp.run(+state.query.v, state, emitter);
42 | }
43 | });
44 | const enrolled = state.storage.enrolled;
45 | // single experiment per session for now
46 | const id = Object.keys(enrolled)[0];
47 | if (Object.keys(experiments).includes(id)) {
48 | experiments[id].run(enrolled[id], state, emitter);
49 | } else {
50 | checkExperiments(state, emitter);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/locale.js:
--------------------------------------------------------------------------------
1 | import { FluentBundle } from '@fluent/bundle';
2 |
3 | function makeBundle(locale, ftl) {
4 | const bundle = new FluentBundle(locale, { useIsolating: false });
5 | bundle.addMessages(ftl);
6 | return bundle;
7 | }
8 |
9 | export async function getTranslator(locale) {
10 | const bundles = [];
11 | const { default: en } = await import('../public/locales/en-US/send.ftl');
12 | if (locale !== 'en-US') {
13 | const { default: ftl } = await import(
14 | `../public/locales/${locale}/send.ftl`
15 | );
16 | bundles.push(makeBundle(locale, ftl));
17 | }
18 | bundles.push(makeBundle('en-US', en));
19 | return function(id, data) {
20 | for (let bundle of bundles) {
21 | if (bundle.hasMessage(id)) {
22 | return bundle.format(bundle.getMessage(id), data);
23 | }
24 | }
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/app/main.js:
--------------------------------------------------------------------------------
1 | /* global DEFAULTS LIMITS PREFS */
2 | import 'core-js';
3 | import 'fast-text-encoding'; // MS Edge support
4 | import 'intl-pluralrules';
5 | import choo from 'choo';
6 | import nanotiming from 'nanotiming';
7 | import routes from './routes';
8 | import getCapabilities from './capabilities';
9 | import controller from './controller';
10 | import dragManager from './dragManager';
11 | import pasteManager from './pasteManager';
12 | import storage from './storage';
13 | import metrics from './metrics';
14 | import experiments from './experiments';
15 | import * as Sentry from '@sentry/browser';
16 | import './main.css';
17 | import User from './user';
18 | import { getTranslator } from './locale';
19 | import Archive from './archive';
20 | import { setTranslate, locale } from './utils';
21 |
22 | if (navigator.doNotTrack !== '1' && window.SENTRY_CONFIG) {
23 | Sentry.init(window.SENTRY_CONFIG);
24 | }
25 |
26 | if (process.env.NODE_ENV === 'production') {
27 | nanotiming.disabled = true;
28 | }
29 |
30 | (async function start() {
31 | const capabilities = await getCapabilities();
32 | if (
33 | !capabilities.crypto &&
34 | window.location.pathname !== '/unsupported/crypto'
35 | ) {
36 | return window.location.assign('/unsupported/crypto');
37 | }
38 | if (capabilities.serviceWorker) {
39 | try {
40 | await navigator.serviceWorker.register('/serviceWorker.js');
41 | await navigator.serviceWorker.ready;
42 | } catch (e) {
43 | // continue but disable streaming downloads
44 | capabilities.streamDownload = false;
45 | }
46 | }
47 |
48 | const translate = await getTranslator(locale());
49 | setTranslate(translate);
50 | // eslint-disable-next-line require-atomic-updates
51 | window.initialState = {
52 | LIMITS,
53 | DEFAULTS,
54 | PREFS,
55 | archive: new Archive([], DEFAULTS.EXPIRE_SECONDS),
56 | capabilities,
57 | translate,
58 | storage,
59 | sentry: Sentry,
60 | user: new User(storage, LIMITS, window.AUTH_CONFIG),
61 | transfer: null,
62 | fileInfo: null,
63 | locale: locale()
64 | };
65 |
66 | const app = routes(choo({ hash: true }));
67 | // eslint-disable-next-line require-atomic-updates
68 | window.app = app;
69 | app.use(experiments);
70 | app.use(metrics);
71 | app.use(controller);
72 | app.use(dragManager);
73 | app.use(pasteManager);
74 | app.mount('body');
75 | })();
76 |
--------------------------------------------------------------------------------
/app/pasteManager.js:
--------------------------------------------------------------------------------
1 | function getString(item) {
2 | return new Promise(resolve => {
3 | item.getAsString(resolve);
4 | });
5 | }
6 |
7 | export default function(state, emitter) {
8 | window.addEventListener('paste', async event => {
9 | if (state.route !== '/' || state.uploading) return;
10 | if (['password', 'text', 'email'].includes(event.target.type)) return;
11 |
12 | const items = Array.from(event.clipboardData.items);
13 | const transferFiles = items.filter(item => item.kind === 'file');
14 | const strings = items.filter(item => item.kind === 'string');
15 | if (transferFiles.length) {
16 | const promises = transferFiles.map(async (f, i) => {
17 | const blob = f.getAsFile();
18 | if (!blob) {
19 | return null;
20 | }
21 | const name = await getString(strings[i]);
22 | const file = new File([blob], name, { type: blob.type });
23 | return file;
24 | });
25 | const files = (await Promise.all(promises)).filter(f => !!f);
26 | if (files.length) {
27 | emitter.emit('addFiles', { files });
28 | }
29 | } else if (strings.length) {
30 | strings[0].getAsString(s => {
31 | const file = new File([s], 'pasted.txt', { type: 'text/plain' });
32 | emitter.emit('addFiles', { files: [file] });
33 | });
34 | }
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/app/readme.md:
--------------------------------------------------------------------------------
1 | # Application Code
2 |
3 | `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.
4 |
5 | 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)
6 |
7 | - `pages` contains display logic an markup for pages
8 | - `routes` contains route definitions and logic
9 | - `templates` contains ui elements smaller than pages
10 |
--------------------------------------------------------------------------------
/app/routes.js:
--------------------------------------------------------------------------------
1 | const choo = require('choo');
2 | const download = require('./ui/download');
3 | const body = require('./ui/body');
4 |
5 | module.exports = function(app = choo({ hash: true })) {
6 | app.route('/', body(require('./ui/home')));
7 | app.route('/download/:id', body(download));
8 | app.route('/download/:id/:key', body(download));
9 | app.route('/unsupported/:reason', body(require('./ui/unsupported')));
10 | app.route('/error', body(require('./ui/error')));
11 | app.route('/blank', body(require('./ui/blank')));
12 | app.route('/oauth', function(state, emit) {
13 | emit('authenticate', state.query.code, state.query.state);
14 | });
15 | app.route('/login', function(state, emit) {
16 | emit('replaceState', '/');
17 | setTimeout(() => emit('render'));
18 | });
19 | app.route('/report', body(require('./ui/report')));
20 | app.route('*', body(require('./ui/notFound')));
21 | return app;
22 | };
23 |
--------------------------------------------------------------------------------
/app/ui/blank.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 |
3 | module.exports = function() {
4 | return html`
5 |
6 |
12 |
13 | `;
14 | };
15 |
--------------------------------------------------------------------------------
/app/ui/body.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 | const Header = require('./header');
3 | const Footer = require('./footer');
4 |
5 | module.exports = function body(main) {
6 | return function(state, emit) {
7 | const b = html`
8 |
11 | ${state.cache(Header, 'header').render()} ${main(state, emit)}
12 | ${state.cache(Footer, 'footer').render()}
13 |
14 | `;
15 | if (state.layout) {
16 | // server side only
17 | return state.layout(state, b);
18 | }
19 | return b;
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/app/ui/copyDialog.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 | const { copyToClipboard } = require('../utils');
3 |
4 | module.exports = function(name, url) {
5 | const dialog = function(state, emit, close) {
6 | return html`
7 |
10 |
11 | ${state.translate('notifyUploadEncryptDone')}
12 |
13 |
14 | ${state.translate('copyLinkDescription')}
15 | ${name}
16 |
17 |
24 |
31 |
38 |
39 | `;
40 |
41 | function copy(event) {
42 | event.stopPropagation();
43 | copyToClipboard(url);
44 | event.target.textContent = state.translate('copiedUrl');
45 | setTimeout(close, 1000);
46 | }
47 | };
48 | dialog.type = 'copy';
49 | return dialog;
50 | };
51 |
--------------------------------------------------------------------------------
/app/ui/downloadCompleted.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 | const assets = require('../../common/assets');
3 |
4 | module.exports = function(state) {
5 | const btnText = state.user.loggedIn ? 'okButton' : 'sendYourFilesLink';
6 | return html`
7 |
32 | `;
33 | };
34 |
--------------------------------------------------------------------------------
/app/ui/downloadDialog.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 |
3 | module.exports = function() {
4 | return function(state, emit, close) {
5 | const archive = state.fileInfo;
6 | return html`
7 |
10 |
11 | ${state.translate('downloadConfirmTitle')}
12 |
13 |
16 | ${state.translate('downloadConfirmDescription')}
17 |
18 |
19 |
25 |
28 |
29 |
38 | ${state.translate('reportFile')}
41 |
42 | `;
43 |
44 | function toggleDownloadEnabled(event) {
45 | event.stopPropagation();
46 | const checked = event.target.checked;
47 | const btn = document.getElementById('download-btn');
48 | btn.disabled = !checked;
49 | }
50 |
51 | function download(event) {
52 | event.preventDefault();
53 | close();
54 | event.target.disabled = true;
55 | emit('download', archive);
56 | }
57 | };
58 | };
59 |
--------------------------------------------------------------------------------
/app/ui/error.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 | const assets = require('../../common/assets');
3 | const modal = require('./modal');
4 |
5 | module.exports = function(state, emit) {
6 | const btnText = state.user.loggedIn ? 'okButton' : 'sendYourFilesLink';
7 | return html`
8 |
9 | ${state.modal && modal(state, emit)}
10 |
13 |
14 | ${state.translate('errorPageHeader')}
15 |
16 |
17 |
23 | ${state.translate('trySendDescription')}
24 |
25 |
26 | ${state.translate(btnText)}
29 |
30 |
31 |
32 | `;
33 | };
34 |
--------------------------------------------------------------------------------
/app/ui/expiryOptions.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 | const raw = require('choo/html/raw');
3 | const { secondsToL10nId } = require('../utils');
4 | const selectbox = require('./selectbox');
5 |
6 | module.exports = function(state, emit) {
7 | const el = html`
8 |
9 | ${raw(
10 | state.translate('archiveExpiryInfo', {
11 | downloadCount:
12 | '',
13 | timespan: ''
14 | })
15 | )}
16 |
17 | `;
18 | if (el.__encoded) {
19 | // we're rendering on the server
20 | return el;
21 | }
22 |
23 | const counts = state.DEFAULTS.DOWNLOAD_COUNTS.filter(
24 | i => state.capabilities.account || i <= state.user.maxDownloads
25 | );
26 |
27 | const dlCountSelect = el.querySelector('#dlCount');
28 | el.replaceChild(
29 | selectbox(
30 | state.archive.dlimit,
31 | counts,
32 | num => state.translate('downloadCount', { num }),
33 | value => {
34 | const max = state.user.maxDownloads;
35 | state.archive.dlimit = Math.min(value, max);
36 | if (value > max) {
37 | emit('signup-cta', 'count');
38 | } else {
39 | emit('render');
40 | }
41 | },
42 | 'expire-after-dl-count-select'
43 | ),
44 | dlCountSelect
45 | );
46 |
47 | const expires = state.DEFAULTS.EXPIRE_TIMES_SECONDS.filter(
48 | i => state.capabilities.account || i <= state.user.maxExpireSeconds
49 | );
50 |
51 | const timeSelect = el.querySelector('#timespan');
52 | el.replaceChild(
53 | selectbox(
54 | state.archive.timeLimit,
55 | expires,
56 | num => {
57 | const l10n = secondsToL10nId(num);
58 | return state.translate(l10n.id, l10n);
59 | },
60 | value => {
61 | const max = state.user.maxExpireSeconds;
62 | state.archive.timeLimit = Math.min(value, max);
63 | if (value > max) {
64 | emit('signup-cta', 'time');
65 | } else {
66 | emit('render');
67 | }
68 | },
69 | 'expire-after-time-select'
70 | ),
71 | timeSelect
72 | );
73 |
74 | return el;
75 | };
76 |
--------------------------------------------------------------------------------
/app/ui/footer.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 | const Component = require('choo/component');
3 |
4 | class Footer extends Component {
5 | constructor(name, state) {
6 | super(name);
7 | this.state = state;
8 | }
9 |
10 | update() {
11 | return false;
12 | }
13 |
14 | createElement() {
15 | return html`
16 |
19 | `;
20 | }
21 | }
22 |
23 | module.exports = Footer;
24 |
--------------------------------------------------------------------------------
/app/ui/header.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 | const Component = require('choo/component');
3 | const Account = require('./account');
4 | const assets = require('../../common/assets');
5 | const { platform } = require('../utils');
6 |
7 | class Header extends Component {
8 | constructor(name, state, emit) {
9 | super(name);
10 | this.state = state;
11 | this.emit = emit;
12 | this.account = state.cache(Account, 'account');
13 | }
14 |
15 | update() {
16 | this.account.render();
17 | return false;
18 | }
19 | createElement() {
20 | const title =
21 | platform() === 'android'
22 | ? html`
23 |
24 |
25 |
28 |
29 | `
30 | : html`
31 |
32 |
36 |
39 |
40 | `;
41 | return html`
42 |
45 | ${title} ${this.account.render()}
46 |
47 | `;
48 | }
49 | }
50 |
51 | module.exports = Header;
52 |
--------------------------------------------------------------------------------
/app/ui/home.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 | const { list } = require('../utils');
3 | const archiveTile = require('./archiveTile');
4 | const modal = require('./modal');
5 | const intro = require('./intro');
6 |
7 | module.exports = function(state, emit) {
8 | if (state.user.loginRequired && !state.user.loggedIn) {
9 | emit('signup-cta', 'required');
10 | }
11 | const archives = state.storage.files
12 | .filter(archive => !archive.expired)
13 | .map(archive => archiveTile(state, emit, archive));
14 | let left = '';
15 | if (state.uploading) {
16 | left = archiveTile.uploading(state, emit);
17 | } else if (state.archive.numFiles > 0) {
18 | left = archiveTile.wip(state, emit);
19 | } else {
20 | left = archiveTile.empty(state, emit);
21 | }
22 | archives.reverse();
23 | const right =
24 | archives.length === 0
25 | ? intro(state)
26 | : list(archives, 'p-2 h-full overflow-y-auto w-full', 'mb-4 w-full');
27 |
28 | return html`
29 |
30 | ${state.modal && modal(state, emit)}
31 |
34 | ${left}
35 | ${right}
36 |
37 |
38 | `;
39 | };
40 |
--------------------------------------------------------------------------------
/app/ui/intro.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 | const assets = require('../../common/assets');
3 |
4 | module.exports = function intro(state) {
5 | return html`
6 |
9 |
10 |
11 | ${state.translate('introTitle')}
12 |
13 |
14 | ${state.translate('introDescription')}
15 |
16 |
})
17 |
18 |
19 | `;
20 | };
21 |
--------------------------------------------------------------------------------
/app/ui/modal.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 |
3 | module.exports = function(state, emit) {
4 | return html`
5 |
8 |
11 |
12 | ${state.modal(state, emit, close)}
13 |
14 |
15 |
16 | `;
17 |
18 | function close(event) {
19 | if (event) {
20 | event.preventDefault();
21 | event.stopPropagation();
22 | }
23 | emit('closeModal');
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/app/ui/notFound.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 | const assets = require('../../common/assets');
3 | const modal = require('./modal');
4 |
5 | module.exports = function(state, emit) {
6 | const btnText = state.user.loggedIn ? 'okButton' : 'sendYourFilesLink';
7 | return html`
8 |
9 | ${state.modal && modal(state, emit)}
10 |
36 |
37 | `;
38 | };
39 |
--------------------------------------------------------------------------------
/app/ui/okDialog.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 |
3 | module.exports = function(message) {
4 | return function(state, emit, close) {
5 | return html`
6 |
7 |
8 | ${message}
9 |
10 |
17 |
18 | `;
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/app/ui/selectbox.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 |
3 | module.exports = function(selected, options, translate, changed, htmlId) {
4 | let x = selected;
5 |
6 | return html`
7 |
21 | `;
22 |
23 | function choose(event) {
24 | const target = event.target;
25 | const value = +target.value;
26 |
27 | if (x !== value) {
28 | x = value;
29 | changed(value);
30 | }
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/app/ui/shareDialog.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 |
3 | module.exports = function(name, url) {
4 | const dialog = function(state, emit, close) {
5 | return html`
6 |
9 |
10 | ${state.translate('notifyUploadEncryptDone')}
11 |
12 |
13 | ${state.translate('shareLinkDescription')}
14 | ${name}
15 |
16 |
23 |
30 |
37 |
38 | `;
39 |
40 | async function share(event) {
41 | event.stopPropagation();
42 | try {
43 | await navigator.share({
44 | title: state.translate('-send-brand'),
45 | text: state.translate('shareMessage', { name }),
46 | url
47 | });
48 | } catch (e) {
49 | if (e.code === e.ABORT_ERR) {
50 | return;
51 | }
52 | console.error(e);
53 | }
54 | close();
55 | }
56 | };
57 | dialog.type = 'share';
58 | return dialog;
59 | };
60 |
--------------------------------------------------------------------------------
/app/ui/unsupported.js:
--------------------------------------------------------------------------------
1 | const html = require('choo/html');
2 | const modal = require('./modal');
3 |
4 | module.exports = function(state, emit) {
5 | let strings = {};
6 | let why = '';
7 | let url = '';
8 |
9 | if (state.params.reason !== 'outdated') {
10 | strings = unsupportedStrings(state);
11 | why = html`
12 |
16 | ${state.translate('notSupportedLink')}
17 |
18 | `;
19 | url =
20 | 'https://www.mozilla.org/firefox/new/?utm_campaign=send-acquisition&utm_medium=referral&utm_source=send.firefox.com';
21 | } else {
22 | strings = outdatedStrings(state);
23 | url = 'https://support.mozilla.org/kb/update-firefox-latest-version';
24 | }
25 |
26 | return html`
27 |
28 | ${state.modal && modal(state, emit)}
29 |
32 | ${strings.header}
33 | ${strings.description}
34 | ${why}
35 |
36 | ${strings.button}
37 |
38 |
39 |
40 | `;
41 | };
42 |
43 | function outdatedStrings(state) {
44 | return {
45 | header: state.translate('notSupportedHeader'),
46 | description: state.translate('notSupportedOutdatedDetail'),
47 | button: state.translate('updateFirefox')
48 | };
49 | }
50 |
51 | function unsupportedStrings(state) {
52 | return {
53 | header: state.translate('notSupportedHeader'),
54 | description: state.translate('notSupportedDescription'),
55 | button: state.translate('downloadFirefox')
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/assets/add.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/addfiles.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/assets/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/assets/android-chrome-192x192.png
--------------------------------------------------------------------------------
/assets/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/assets/android-chrome-512x512.png
--------------------------------------------------------------------------------
/assets/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/assets/apple-touch-icon.png
--------------------------------------------------------------------------------
/assets/blue_file.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/close-16.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/completed.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/copy-16.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/dl.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/error.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/assets/favicon-16x16.png
--------------------------------------------------------------------------------
/assets/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/assets/favicon-32x32.png
--------------------------------------------------------------------------------
/assets/feedback.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/intro.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
--------------------------------------------------------------------------------
/assets/lock.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/master-logo.svg:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/assets/notFound.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
--------------------------------------------------------------------------------
/assets/select-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/share-24.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/wordmark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/browserslist:
--------------------------------------------------------------------------------
1 | last 2 chrome versions
2 | last 2 firefox versions
3 | last 2 safari versions
4 | last 2 edge versions
5 | edge 18
6 | firefox esr
7 |
--------------------------------------------------------------------------------
/build/android_index_plugin.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const html = require('choo/html');
3 | const NAME = 'AndroidIndexPlugin';
4 |
5 | function chunkFileNames(compilation) {
6 | const names = {};
7 | for (const chunk of compilation.chunks) {
8 | for (const file of chunk.files) {
9 | if (!/\.map$/.test(file)) {
10 | names[`${chunk.name}${path.extname(file)}`] = file;
11 | }
12 | }
13 | }
14 | return names;
15 | }
16 | class AndroidIndexPlugin {
17 | apply(compiler) {
18 | compiler.hooks.emit.tap(NAME, compilation => {
19 | const files = chunkFileNames(compilation);
20 | const page = html`
21 |
22 |
23 |
Send
24 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | `
36 | .toString()
37 | .replace(/\n\s{6}/g, '\n');
38 | compilation.assets['android.html'] = {
39 | source() {
40 | return page;
41 | },
42 | size() {
43 | return page.length;
44 | }
45 | };
46 | });
47 | }
48 | }
49 |
50 | module.exports = AndroidIndexPlugin;
51 |
--------------------------------------------------------------------------------
/build/readme.md:
--------------------------------------------------------------------------------
1 | # Custom Loaders
2 |
3 | ## Android Index Plugin
4 |
5 | Generates the `index.html` page for the native android client
6 |
7 | ## Version Plugin
8 |
9 | Creates a `version.json` file that gets exposed by the `/__version__` route from the `package.json` file and current git commit hash.
10 |
11 | # See Also
12 |
13 | - [docs/build.md](../docs/build.md)
14 | - [webpack.config.js](../webpack.config.js)
--------------------------------------------------------------------------------
/build/version_plugin.js:
--------------------------------------------------------------------------------
1 | const gitRevSync = require('git-rev-sync');
2 | const pkg = require('../package.json');
3 |
4 | let commit = 'unknown';
5 |
6 | try {
7 | commit = gitRevSync.short();
8 | } catch (e) {
9 | console.warn('Error fetching current git commit: ' + e);
10 | }
11 |
12 | const version = JSON.stringify({
13 | commit,
14 | source: pkg.homepage,
15 | version: process.env.CIRCLE_TAG || `v${pkg.version}`
16 | });
17 |
18 | class VersionPlugin {
19 | apply(compiler) {
20 | compiler.hooks.emit.tap('VersionPlugin', compilation => {
21 | compilation.assets['version.json'] = {
22 | source() {
23 | return version;
24 | },
25 | size() {
26 | return version.length;
27 | }
28 | };
29 | });
30 | }
31 | }
32 |
33 | module.exports = VersionPlugin;
34 |
--------------------------------------------------------------------------------
/common/assets.js:
--------------------------------------------------------------------------------
1 | const genmap = require('./generate_asset_map');
2 | const isServer = typeof genmap === 'function';
3 | let prefix = '';
4 | let manifest = {};
5 | try {
6 | //eslint-disable-next-line node/no-missing-require
7 | manifest = require('../dist/manifest.json');
8 | } catch (e) {
9 | // use middleware
10 | }
11 |
12 | const assets = isServer ? manifest : genmap;
13 |
14 | function getAsset(name) {
15 | return prefix + assets[name];
16 | }
17 |
18 | function setPrefix(name) {
19 | prefix = name;
20 | }
21 |
22 | function getMatches(match) {
23 | return Object.keys(assets)
24 | .filter(k => match.test(k))
25 | .map(getAsset);
26 | }
27 |
28 | const instance = {
29 | setPrefix: setPrefix,
30 | get: getAsset,
31 | match: getMatches,
32 | setMiddleware: function(middleware) {
33 | function getManifest() {
34 | return JSON.parse(
35 | middleware.fileSystem.readFileSync(
36 | middleware.getFilenameFromUrl('/manifest.json')
37 | )
38 | );
39 | }
40 | if (middleware) {
41 | instance.get = function getAssetWithMiddleware(name) {
42 | const m = getManifest();
43 | return prefix + m[name];
44 | };
45 | instance.match = function matchAssetWithMiddleware(match) {
46 | const m = getManifest();
47 | return Object.keys(m)
48 | .filter(k => match.test(k))
49 | .map(k => prefix + m[k]);
50 | };
51 | }
52 | }
53 | };
54 |
55 | module.exports = instance;
56 |
--------------------------------------------------------------------------------
/common/generate_asset_map.js:
--------------------------------------------------------------------------------
1 | /*
2 | This code is included by both the server and frontend via
3 | common/assets.js
4 |
5 | When included from the server the export will be the function.
6 |
7 | When included from the frontend (via webpack) the export will
8 | be an object mapping file names to hashed file names. Example:
9 | "send_logo.svg": "send_logo.5fcfdf0e.svg"
10 | */
11 |
12 | const fs = require('fs');
13 | const path = require('path');
14 |
15 | function kv(f) {
16 | return `"${f}": require('../assets/${f}')`;
17 | }
18 |
19 | module.exports = function() {
20 | const files = fs.readdirSync(path.join(__dirname, '..', 'assets'));
21 | const code = `module.exports = {
22 | ${files.map(kv).join(',\n')}
23 | };`;
24 | return {
25 | code,
26 | dependencies: files.map(f => require.resolve('../assets/' + f)),
27 | cacheable: true
28 | };
29 | };
30 |
--------------------------------------------------------------------------------
/common/readme.md:
--------------------------------------------------------------------------------
1 | # Common Code
2 |
3 | This directory contains code loaded by both the frontend `app` and backend `server`. The code here can be challenging to understand at first because the contexts for the two (three counting the dev server) environments that include them are quite different, but the purpose of these modules are quite simple, to provide mappings from the source assets (`copy-16.png`) to the concrete production assets (`copy-16.db66e0bf.svg`).
4 |
5 | ## Generate Asset Map
6 |
7 | This loader enumerates all the files in `assets/` so that `common/assets.js` can provide mappings from the source filename to the hashed filename used on the site.
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | web:
4 | build: .
5 | links:
6 | - redis
7 | ports:
8 | - "1443:1443"
9 | environment:
10 | - REDIS_HOST=redis
11 | redis:
12 | image: redis:alpine
13 | selenium-firefox:
14 | image: b4handjr/selenium-firefox
15 | ports:
16 | - "${VNC_PORT:-5900}:5900"
17 | shm_size: 2g
18 | volumes:
19 | - .:/code
20 |
--------------------------------------------------------------------------------
/docs/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # flod as main contact for string changes
2 | public/locales/en-US/*.ftl @flodolo
3 |
--------------------------------------------------------------------------------
/docs/build.md:
--------------------------------------------------------------------------------
1 | Send has two build configurations, development and production. Both can be run via `npm` scripts, `npm start` for development and `npm run build` for production. Webpack is our only build tool and all configuration lives in [webpack.config.js](../webpack.config.js).
2 |
3 | # Development
4 |
5 | `npm start` launches a `webpack-dev-server` on port 8080 that compiles the assets and watches files for changes. It also serves the backend API and frontend unit tests via the `server/bin/dev.js` entrypoint. The frontend tests can be run in the browser by navigating to http://localhost:8080/test and will rerun automatically as the watched files are saved with changes.
6 |
7 | # Production
8 |
9 | `npm run build` compiles the assets and writes the files to the `dist/` directory. `npm run prod` launches an Express server on port 1443 that serves the backend API and frontend static assets from `dist/` via the `server/bin/prod.js` entrypoint.
10 |
11 | # Notable differences
12 |
13 | - Development compiles assets in memory, so no `dist/` directory is generated
14 | - Development does not enable CSP headers
15 | - Development frontend source is instrumented for code coverage
16 | - Only development includes sourcemaps
17 | - Only development exposes the `/test` route
18 | - Production sets Cache-Control immutable headers on the hashed static assets
19 |
20 | # Custom Loaders
21 |
22 | The `build/` directory contains custom webpack loaders specific to Send. See [build/readme.md](../build/readme.md) for details on each loader.
--------------------------------------------------------------------------------
/docs/deployment.md:
--------------------------------------------------------------------------------
1 | ## Requirements
2 | This document describes how to do a full deployment of Firefox Send on your own Linux server. You will need:
3 |
4 | * A working (and ideally somewhat recent) installation of NodeJS and NPM
5 | * GIT
6 | * An Apache webserver
7 | * Optionally telnet, to be able to quickly check your installation
8 |
9 | For Debian/Ubuntu systems this probably just means something like this:
10 |
11 | * apt install git apache2 nodejs npm telnet
12 |
13 | ## Building
14 | * We assume an already configured virtual-host on your webserver with an existing empty htdocs folder
15 | * First, remove that htdocs folder - we will replace it with Firefox Send's version now
16 | * git clone https://github.com/mozilla/send.git htdocs
17 | * Make now sure you are NOT root but rather the user your webserver is serving files under (e.g. "su www-data" or whoever the owner of your htdocs folder is)
18 | * npm install
19 | * npm run build
20 |
21 | ## Running
22 | To have a permanently running version of Firefox Send as a background process:
23 |
24 | * Create a file "run.sh" with:
25 | ```
26 | #!/bin/bash
27 | nohup su www-data -c "npm run prod" 2>/dev/null &
28 | ```
29 | * chmod +x run.sh
30 | * ./run.sh
31 |
32 | Now the Firefox Send backend should be running on port 1443. You can check with:
33 | * telnet localhost 1443
34 |
35 | ## Reverse Proxy
36 | Of course, we don't want to expose the service on port 1443. Instead we want our normal webserver to forward all requests to Firefox send ("Reverse proxy").
37 |
38 | # Apache webserver
39 |
40 | * a2enmod proxy
41 | * a2enmod proxy_http
42 | * a2enmod proxy_wstunnel
43 |
44 | In your Apache virtual host configuration file, insert this:
45 |
46 | ```
47 | # Enable rewrite engine
48 | RewriteEngine on
49 |
50 | # Make sure the original domain name is forwarded to Send
51 | # Otherwise the generated URLs will be wrong
52 | ProxyPreserveHost on
53 |
54 | # Make sure the generated URL is https://
55 | RequestHeader set X-Forwarded-Proto https
56 |
57 | # If it's a normal file (e.g. PNG, CSS) just return it
58 | RewriteCond %{REQUEST_FILENAME} -f
59 | RewriteRule .* - [L]
60 |
61 | # If it's a websocket connection, redirect it to a Send WS connection
62 | RewriteCond %{HTTP:Upgrade} =websocket [NC]
63 | RewriteRule /(.*) ws://127.0.0.1:1443/$1 [P,L]
64 |
65 | # Otherwise redirect it to a normal HTTP connection
66 | RewriteRule ^/(.*)$ http://127.0.0.1:1443/$1 [P,QSA]
67 | ProxyPassReverse "/" "http://127.0.0.1:1443"
68 | ```
69 |
--------------------------------------------------------------------------------
/docs/docker.md:
--------------------------------------------------------------------------------
1 | ## Setup
2 |
3 | Run `docker build -t send:latest .` to create an image or `docker-compose up` to run a full testable stack. *We don't recommend using docker-compose for production.*
4 |
5 | ## Environment variables:
6 |
7 | | Name | Description
8 | |------------------|-------------|
9 | | `PORT` | Port the server will listen on (defaults to 1443).
10 | | `S3_BUCKET` | The S3 bucket name.
11 | | `REDIS_HOST` | Host name of the Redis server.
12 | | `SENTRY_CLIENT` | Sentry Client ID
13 | | `SENTRY_DSN` | Sentry DSN
14 | | `MAX_FILE_SIZE` | in bytes (defaults to 2147483648)
15 | | `NODE_ENV` | "production"
16 | | `BASE_URL` | The HTTPS URL where traffic will be served (e.g. `https://send.firefox.com`)
17 |
18 | ## Example:
19 |
20 | ```sh
21 | $ docker run --net=host -e 'NODE_ENV=production' \
22 | -e 'S3_BUCKET=testpilot-p2p-dev' \
23 | -e 'REDIS_HOST=dyf9s2r4vo3.bolxr4.0001.usw2.cache.amazonaws.com' \
24 | -e 'SENTRY_CLIENT=https://51e23d7263e348a7a3b90a5357c61cb2@sentry.prod.mozaws.net/168' \
25 | -e 'SENTRY_DSN=https://51e23d7263e348a7a3b90a5357c61cb2:65e23d7263e348a7a3b90a5357c61c44@sentry.prod.mozaws.net/168' \
26 | -e 'BASE_URL=https://send.firefox.com' \
27 | mozilla/send:latest
28 | ```
29 |
--------------------------------------------------------------------------------
/docs/encryption.md:
--------------------------------------------------------------------------------
1 | # File Encryption
2 |
3 | Send use 128-bit AES-GCM encryption via the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) to encrypt files in the browser before uploading them to the server. The code is in [app/keychain.js](../app/keychain.js).
4 |
5 | ## Steps
6 |
7 | ### Uploading
8 |
9 | 1. A new secret key is generated with `crypto.getRandomValues`
10 | 2. The secret key is used to derive more keys via HKDF SHA-256
11 | - a series of encryption keys for the file, via [ECE](https://tools.ietf.org/html/rfc8188) (AES-GCM)
12 | - an encryption key for the file metadata (AES-GCM)
13 | - a signing key for request authentication (HMAC SHA-256)
14 | 3. The file and metadata are encrypted with their corresponding keys
15 | 4. The encrypted data and signing key are uploaded to the server
16 | 5. An owner token and the share url are returned by the server and stored in local storage
17 | 6. The secret key is appended to the share url as a [#fragment](https://en.wikipedia.org/wiki/Fragment_identifier) and presented to the UI
18 |
19 | ### Downloading
20 |
21 | 1. The browser loads the share url page, which includes an authentication nonce
22 | 2. The browser imports the secret key from the url fragment
23 | 3. The same 3 keys as above are derived
24 | 4. The browser signs the nonce with its signing key and requests the metadata
25 | 5. The encrypted metadata is decrypted and presented on the page
26 | 6. The browser makes another authenticated request to download the encrypted file
27 | 7. The browser downloads and decrypts the file
28 | 8. The file prompts the save dialog or automatically saves depending on the browser settings
29 |
30 | ### Passwords
31 |
32 | A password may optionally be set to authenticate the download request. When a password is set the following steps occur.
33 |
34 | #### Sender
35 |
36 | 1. The original signing key derived from the secret key is discarded
37 | 2. A new signing key is generated via PBKDF2 from the user entered password and the full share url (including secret key fragment)
38 | 3. The new key is sent to the server, authenticated by the owner token
39 | 4. The server stores the new key and marks the record as needing a password
40 |
41 | #### Downloader
42 |
43 | 1. The browser loads the share url page, which includes an authentication nonce and indicator that the file requires a password
44 | 2. The user is prompted for the password and the signing key is derived
45 | 3. The browser requests the metadata using the key to sign the nonce
46 | 4. If the password was correct the metadata is returned, otherwise a 401
47 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | ## How big of a file can I transfer with Firefox Send?
2 |
3 | There is a 2.5GB file size limit built in to Send(1GB for non-signed in users), however, in practice you may
4 | be unable to send files that large. Send encrypts and decrypts the files in
5 | the browser which is great for security but will tax your system resources. In
6 | particular you can expect to see your memory usage go up by at least the size
7 | of the file when the transfer is processing. You can see [the results of some
8 | testing](https://github.com/mozilla/send/issues/170#issuecomment-314107793).
9 | For the most reliable operation on common computers, it’s probably best to stay
10 | under a few hundred megabytes.
11 |
12 | ## Why is my browser not supported?
13 |
14 | We’re using the [Web Cryptography JavaScript API with the AES-GCM
15 | algorithm](https://www.w3.org/TR/WebCryptoAPI/#aes-gcm) for our encryption.
16 | Many browsers support this standard and should work fine, but some have not
17 | implemented it yet (mobile browsers lag behind on this, in
18 | particular).
19 |
20 | ## Why does Firefox Send require JavaScript?
21 |
22 | Firefox Send uses JavaScript to:
23 |
24 | - Encrypt and decrypt files locally on the client instead of the server.
25 | - Render the user interface.
26 | - Manage translations on the website into [various different languages](https://github.com/mozilla/send#localization).
27 | - Collect data to help us improve Send in accordance with our [Terms & Privacy](https://send.firefox.com/legal).
28 |
29 | Since Send is an open source project, you can see all of the cool ways we use JavaScript by [examining our code](https://github.com/mozilla/send/).
30 |
31 | ## How long are files available for?
32 |
33 | Files are available to be downloaded for 24 hours, after which they are removed
34 | from the server. They are also removed immediately once the download limit is reached.
35 |
36 | ## Can a file be downloaded more than once?
37 |
38 | Yes, once a file is submitted to Send you can select the download limit.
39 |
40 |
41 | *Disclaimer: Send is an experiment and under active development. The answers
42 | here may change as we get feedback from you and the project matures.*
43 |
--------------------------------------------------------------------------------
/docs/localization.md:
--------------------------------------------------------------------------------
1 | # Localization
2 |
3 | Send is localized in over 50 languages. We use the [fluent](http://projectfluent.org/) library and store our translations in [FTL](http://projectfluent.org/fluent/guide/) files in `public/locales/`. `en-US` is our base language, and other languages are managed by [pontoon](https://pontoon.mozilla.org/projects/test-pilot-firefox-send/).
4 |
5 | ## Process
6 |
7 | Strings are added or removed from [public/locales/en-US/send.ftl] as needed. Strings **MUST NOT** be *changed* after they've been commited and pushed to master. Changing a string requires creating a new ID with a new name (preferably descriptive instead of incremented) and deletion of the obsolete ID. It's often useful to add a comment above the string with info about how and where the string is used.
8 |
9 | Once new strings are commited to master they are available for translators in Pontoon. All languages other than `en-US` should be edited via Pontoon. Translations get automatically commited to the github master branch.
10 |
11 | ### Activation
12 |
13 | The development environment includes all locales in `public/locales` via the `L10N_DEV` environment variable. Production uses `package.json` as the list of locales to use. Once a locale has enough string coverage it should be added to `package.json`.
14 |
15 | ## Code
16 |
17 | In `app/` we use the `state.translate()` function to translate strings to the best matching language base on the user's `Accept-Language` header. It's a wrapper around fluent's [FluentBundle.format](http://projectfluent.org/fluent.js/fluent/FluentBundle.html). It works the same for both server and client side rendering.
18 |
19 | ### Examples
20 |
21 | ```js
22 | // simple string
23 | const finishedString = state.translate('downloadFinish')
24 | // with parameters
25 | const progressString = state.translate('downloadingPageProgress', {
26 | filename: state.fileInfo.name,
27 | size: bytes(state.fileInfo.size)
28 | })
29 | ```
30 |
--------------------------------------------------------------------------------
/docs/notes/streams.md:
--------------------------------------------------------------------------------
1 | # Web Streams
2 |
3 | - API
4 | - https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
5 | - Reference Implementation
6 | - https://github.com/whatwg/streams/tree/master/reference-implementation
7 | - Examples
8 | - https://github.com/mdn/dom-examples/tree/master/streams
9 | - Polyfill
10 | - https://github.com/MattiasBuelens/web-streams-polyfill
11 |
12 | # Encrypted Content Encoding
13 |
14 | - Spec
15 | - https://trac.tools.ietf.org/html/rfc8188
16 | - node.js implementation
17 | - https://github.com/web-push-libs/encrypted-content-encoding/tree/master/nodejs
18 |
19 | # Other APIs
20 |
21 | - Blobs
22 | - https://developer.mozilla.org/en-US/docs/Web/API/Blob
23 | - ArrayBuffers, etc
24 | - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
25 | - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
26 | - FileReader
27 | - https://developer.mozilla.org/en-US/docs/Web/API/FileReader
28 |
29 | # Other
30 |
31 | - node.js Buffer browser library
32 | - https://github.com/feross/buffer
33 | - StreamSaver
34 | - https://github.com/jimmywarting/StreamSaver.js
35 |
--------------------------------------------------------------------------------
/docs/takedowns.md:
--------------------------------------------------------------------------------
1 | ## Take-down process
2 |
3 | In cases of a DMCA notice, or other abuse yet to be determined, a file has to be removed from the service.
4 |
5 | Files can be delisted and made inaccessible by removing their record from Redis.
6 |
7 | Send share links contain the `id` of the file, for example `https://send.firefox.com/download/3d9d2bb9a1`
8 |
9 | From a host with access to the Redis server run a `DEL` command with the file id.
10 |
11 | For example:
12 |
13 | ```sh
14 | redis-cli DEL 3d9d2bb9a1
15 | ```
16 |
17 | Other redis-cli parameters like `-h` may also be required. See [redis-cli docs](https://redis.io/topics/rediscli) for more info.
18 |
19 | The encrypted file resides on S3 as the same `id` under the bucket that the app was configured with as `S3_BUCKET`. The file can be managed if it has not already expired with the [AWS cli](https://docs.aws.amazon.com/cli/latest/reference/s3/index.html) or AWS web console.
--------------------------------------------------------------------------------
/ios/generate-bundle.js:
--------------------------------------------------------------------------------
1 | const child_process = require('child_process');
2 | const fs = require('fs');
3 | const path = require('path');
4 |
5 | child_process.execSync('npm run build');
6 |
7 | const prefix = path.join('..', 'dist');
8 | const json_string = fs.readFileSync(path.join(prefix, 'manifest.json'));
9 | const manifest = JSON.parse(json_string);
10 |
11 | const ios_filename = manifest['ios.js'];
12 | fs.writeFileSync(
13 | 'send-ios/assets/ios.js',
14 | fs.readFileSync(`${prefix}${ios_filename}`)
15 | );
16 |
17 | const vendor_filename = manifest['vendor.js'];
18 | fs.writeFileSync(
19 | 'send-ios/assets/vendor.js',
20 | fs.readFileSync(`${prefix}${vendor_filename}`)
21 | );
22 |
--------------------------------------------------------------------------------
/ios/send-ios-action-extension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | send-ios-action-extension
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | XPC!
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | NSExtension
24 |
25 | NSExtensionAttributes
26 |
27 | NSExtensionActivationRule
28 | SUBQUERY (
29 | extensionItems,
30 | $extensionItem,
31 | SUBQUERY (
32 | $extensionItem.attachments,
33 | $attachment,
34 | (
35 | ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf"
36 | || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image"
37 | || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text"
38 | || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png"
39 | || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg"
40 | || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg-2000"
41 | || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.compuserve.gif"
42 | || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.microsoft.bmp"
43 | )
44 | ).@count == 1
45 | ).@count == 1
46 |
47 | NSExtensionMainStoryboard
48 | MainInterface
49 | NSExtensionPointIdentifier
50 | com.apple.ui-services
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/ios/send-ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/send-ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/send-ios.xcodeproj/project.xcworkspace/xcuserdata/donovan.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/ios/send-ios.xcodeproj/project.xcworkspace/xcuserdata/donovan.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
8 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/ios/send-ios.xcodeproj/xcuserdata/donovan.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | send-ios-action-extension.xcscheme
8 |
9 | orderHint
10 | 1
11 |
12 | send-ios.xcscheme
13 |
14 | orderHint
15 | 0
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/ios/send-ios/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // send-ios
4 | //
5 | // Created by Donovan Preston on 7/19/18.
6 | //
7 |
8 | import UIKit
9 |
10 | @UIApplicationMain
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | var window: UIWindow?
14 |
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
17 | // Override point for customization after application launch.
18 | return true
19 | }
20 |
21 | func applicationWillResignActive(_ application: UIApplication) {
22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
23 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
24 | }
25 |
26 | func applicationDidEnterBackground(_ application: UIApplication) {
27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
29 | }
30 |
31 | func applicationWillEnterForeground(_ application: UIApplication) {
32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
33 | }
34 |
35 | func applicationDidBecomeActive(_ application: UIApplication) {
36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
37 | }
38 |
39 | func applicationWillTerminate(_ application: UIApplication) {
40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
41 | }
42 |
43 |
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/ios/send-ios/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/ios/send-ios/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/ios/send-ios/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/ios/send-ios/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/ios/send-ios/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // send-ios
4 | //
5 | // Created by Donovan Preston on 7/19/18.
6 | //
7 |
8 | import UIKit
9 | import WebKit
10 |
11 | class ViewController: UIViewController, WKScriptMessageHandler {
12 | @IBOutlet var webView: WKWebView!
13 |
14 | override func viewDidLoad() {
15 | super.viewDidLoad()
16 | self.webView.frame = self.view.bounds
17 | self.webView?.configuration.userContentController.add(self, name: "loaded")
18 | self.webView?.configuration.userContentController.add(self, name: "copy")
19 | if let url = Bundle.main.url(
20 | forResource: "index",
21 | withExtension: "html",
22 | subdirectory: "assets") {
23 | webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
24 | }
25 | }
26 |
27 | public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
28 | print("Message received: \(message.name) with body: \(message.body)")
29 | UIPasteboard.general.string = "\(message.body)"
30 | }
31 |
32 | override func didReceiveMemoryWarning() {
33 | super.didReceiveMemoryWarning()
34 | // Dispose of any resources that can be recreated.
35 | }
36 |
37 |
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/ios/send-ios/assets/background_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/send/ade10e496c064d3b29191dd33b1066bf99607d74/ios/send-ios/assets/background_1.jpg
--------------------------------------------------------------------------------
/ios/send-ios/assets/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: url('background_1.jpg');
3 | display: flex;
4 | flex-direction: row;
5 | flex: auto;
6 | justify-content: center;
7 | align-items: center;
8 | padding: 0 20px;
9 | box-sizing: border-box;
10 | position: fixed;
11 | top: 0;
12 | left: 0;
13 | right: 0;
14 | bottom: 0;
15 | }
16 |
17 | #striped {
18 | background-image: repeating-linear-gradient(
19 | 45deg,
20 | white,
21 | white 5px,
22 | #ea000e 5px,
23 | #ea000e 25px,
24 | white 25px,
25 | white 30px,
26 | #0083ff 30px,
27 | #0083ff 50px
28 | );
29 | height: 350px;
30 | width: 480px;
31 | }
32 |
33 | #white {
34 | display: flex;
35 | justify-content: center;
36 | align-items: center;
37 | flex-direction: column;
38 | height: 100%;
39 | background-color: white;
40 | margin: 0 10px;
41 | padding: 1px 10px 0 10px;
42 | }
43 |
44 | #label {
45 | background: #0297f8;
46 | border: 1px solid #0297f8;
47 | color: white;
48 | font-size: 24px;
49 | font-weight: 500;
50 | height: 60px;
51 | width: 200px;
52 | display: flex;
53 | justify-content: center;
54 | align-items: center;
55 | }
56 |
57 | #input {
58 | display: none;
59 | }
60 |
61 | #url {
62 | flex: 1;
63 | width: 100%;
64 | height: 32px;
65 | font-size: 24px;
66 | margin-top: 1em;
67 | }
68 |
69 | .button {
70 | flex: 1;
71 | display: block;
72 | background: #0297f8;
73 | border: 1px solid #0297f8;
74 | color: white;
75 | font-size: 24px;
76 | font-weight: 500;
77 | width: 95%;
78 | height: 32px;
79 | margin-top: 1em;
80 | }
81 |
82 | #send-another {
83 | margin-bottom: 1em;
84 | }
85 |
--------------------------------------------------------------------------------
/ios/send-ios/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Send
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/ios/send-ios/help.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | HELLO WORLD
4 |
5 |