Browse Source

Merge branch `main`

Antoine du Hamel 9 months ago
parent
commit
37ad27d5d1

+ 3 - 0
.env.example

@@ -11,6 +11,7 @@ COMPANION_PORT=3020
 COMPANION_CLIENT_ORIGINS=
 COMPANION_CLIENT_ORIGINS=
 COMPANION_SECRET=development
 COMPANION_SECRET=development
 COMPANION_PREAUTH_SECRET=development2
 COMPANION_PREAUTH_SECRET=development2
+COMPANION_OAUTH_ORIGIN=*
 
 
 # NOTE: Only enable this in development. Enabling it in production is a security risk
 # NOTE: Only enable this in development. Enabling it in production is a security risk
 COMPANION_ALLOW_LOCAL_URLS=true
 COMPANION_ALLOW_LOCAL_URLS=true
@@ -25,6 +26,8 @@ COMPANION_AWS_REGION="AWS REGION"
 COMPANION_AWS_PREFIX="OPTIONAL PREFIX"
 COMPANION_AWS_PREFIX="OPTIONAL PREFIX"
 # to enable S3 Transfer Acceleration (default: false)
 # to enable S3 Transfer Acceleration (default: false)
 # COMPANION_AWS_USE_ACCELERATE_ENDPOINT="false"
 # COMPANION_AWS_USE_ACCELERATE_ENDPOINT="false"
+# to enable S3 path style uploads (default: false), this is useful for localstack support
+# COMPANION_AWS_FORCE_PATH_STYLE="true"
 # to set X-Amz-Expires query param in presigned urls (in seconds, default: 800)
 # to set X-Amz-Expires query param in presigned urls (in seconds, default: 800)
 # COMPANION_AWS_EXPIRES="800"
 # COMPANION_AWS_EXPIRES="800"
 # to set a canned ACL for uploaded objects: https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
 # to set a canned ACL for uploaded objects: https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl

+ 1 - 1
.github/workflows/companion-deploy.yml

@@ -63,7 +63,7 @@ jobs:
           username: ${{secrets.DOCKER_USERNAME}}
           username: ${{secrets.DOCKER_USERNAME}}
           password: ${{secrets.DOCKER_PASSWORD}}
           password: ${{secrets.DOCKER_PASSWORD}}
       - name: Build and push
       - name: Build and push
-        uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6.1.0
+        uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1 # v6.2.0
         with:
         with:
           push: true
           push: true
           context: .
           context: .

+ 1 - 1
.github/workflows/release.yml

@@ -161,7 +161,7 @@ jobs:
           username: ${{secrets.DOCKER_USERNAME}}
           username: ${{secrets.DOCKER_USERNAME}}
           password: ${{secrets.DOCKER_PASSWORD}}
           password: ${{secrets.DOCKER_PASSWORD}}
       - name: Build and push
       - name: Build and push
-        uses: docker/build-push-action@31159d49c0d4756269a0940a750801a1ea5d7003 # v6.1.0
+        uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1 # v6.2.0
         with:
         with:
           push: true
           push: true
           context: .
           context: .

+ 11 - 3
README.md

@@ -16,14 +16,22 @@ about more important problems than building a file uploader.
 **[Read the docs](https://uppy.io/docs)** |
 **[Read the docs](https://uppy.io/docs)** |
 **[Try Uppy](https://uppy.io/examples/dashboard/)**
 **[Try Uppy](https://uppy.io/examples/dashboard/)**
 
 
-<a href="https://transloadit.com" target="_blank"><img width="185" src="https://github.com/transloadit/uppy/raw/main/assets/developed-by-transloadit.png"></a>
+<p>
+  <a href="https://transloadit.com" target="_blank">
+    <picture>
+      <source media="(prefers-color-scheme: dark)" srcset="https://github.com/transloadit/uppy/assets/375537/6651e57e-cb57-4336-8745-6473ae68d0bd">
+      <source media="(prefers-color-scheme: light)" srcset="https://github.com/transloadit/uppy/assets/375537/7f14421d-1e37-464e-8203-ade121216c88">
+      <img src="https://github.com/transloadit/uppy/assets/375537/7f14421d-1e37-464e-8203-ade121216c88" alt="Developed by Transloadit">
+    </picture>
+  </a>
+</p>
 
 
 Uppy is being developed by the folks at [Transloadit](https://transloadit.com),
 Uppy is being developed by the folks at [Transloadit](https://transloadit.com),
 a versatile API to handle any file in your app.
 a versatile API to handle any file in your app.
 
 
 <table>
 <table>
-<tr><th>Tests</th><td><img src="https://github.com/transloadit/uppy/workflows/Tests/badge.svg" alt="CI status for Uppy tests"></td><td><img src="https://github.com/transloadit/uppy/workflows/Companion/badge.svg" alt="CI status for Companion tests"></td><td><img src="https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg" alt="CI status for browser tests"></td></tr>
-<tr><th>Deploys</th><td><img src="https://github.com/transloadit/uppy/workflows/Release/badge.svg" alt="CI status for CDN deployment"></td><td><img src="https://github.com/transloadit/uppy/workflows/Companion%20Deploy/badge.svg" alt="CI status for Companion deployment"></td><td><img src="https://github.com/transloadit/uppy.io/workflows/Deploy%20to%20GitHub%20Pages/badge.svg" alt="CI status for website deployment"></td></tr>
+<tr><th>Tests</th><td><img src="https://github.com/transloadit/uppy/workflows/CI/badge.svg" alt="CI status for Uppy tests"></td><td><img src="https://github.com/transloadit/uppy/workflows/Companion/badge.svg" alt="CI status for Companion tests"></td><td><img src="https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg" alt="CI status for browser tests"></td></tr>
+<tr><th>Deploys</th><td><img src="https://github.com/transloadit/uppy/workflows/Release/badge.svg" alt="CI status for CDN deployment"></td><td><img src="https://github.com/transloadit/uppy/workflows/Companion%20Edge%20Deploy/badge.svg" alt="CI status for Companion deployment"></td><td><img src="https://github.com/transloadit/uppy.io/workflows/Deploy%20to%20GitHub%20Pages/badge.svg" alt="CI status for website deployment"></td></tr>
 </table>
 </table>
 
 
 ## Example
 ## Example

+ 21 - 0
docs/companion.md

@@ -343,6 +343,21 @@ which has only the secret, nothing else.
 
 
 :::
 :::
 
 
+### `oauthOrigin` `COMPANION_OAUTH_ORIGIN`
+
+:::caution
+
+Setting this option is strongly recommended. If left unset (or set to `'*'`),
+your app could be impersonated.
+
+:::
+
+An [origin](https://developer.mozilla.org/en-US/docs/Glossary/Origin) specifying
+allowed origins, or an array of origins (comma-separated origins in
+`COMPANION_OAUTH_ORIGIN`). Any browser request from an origin that is not listed
+will not receive OAuth2 tokens, and the OAuth request won’t complete. Set it to
+`'*'` to allow all origins (not recommended). Default: `'*'`.
+
 #### `uploadUrls` `COMPANION_UPLOAD_URLS`
 #### `uploadUrls` `COMPANION_UPLOAD_URLS`
 
 
 An allowlist (array) of strings (exact URLs) or regular expressions. Companion
 An allowlist (array) of strings (exact URLs) or regular expressions. Companion
@@ -484,6 +499,12 @@ takes one argument which is an object with the following properties:
 - `req`, Express.js `Request` object. Do not use any Companion internals from
 - `req`, Express.js `Request` object. Do not use any Companion internals from
   the req object, as these might change in any minor version of Companion.
   the req object, as these might change in any minor version of Companion.
 
 
+#### `s3.forcePathStyle` `COMPANION_AWS_FORCE_PATH_STYLE`
+
+This adds support for setting the S3 client’s `forcePathStyle` option. That is
+necessary to use Uppy/Companion alongside localstack in development
+environments. **Default**: `false`.
+
 ##### `s3.region` `COMPANION_AWS_REGION`
 ##### `s3.region` `COMPANION_AWS_REGION`
 
 
 The datacenter region where the target bucket is located.
 The datacenter region where the target bucket is located.

+ 1 - 1
docs/framework-integrations/angular.mdx

@@ -11,7 +11,7 @@ import TabItem from '@theme/TabItem';
 
 
 ## When should I use it?
 ## When should I use it?
 
 
-When you are using the Angular framework and you wouldl like to use on of the UI
+When you are using the Angular framework and you would like to use one of the UI
 components.
 components.
 
 
 ## Install
 ## Install

+ 1 - 0
e2e/start-companion-with-load-balancer.mjs

@@ -67,6 +67,7 @@ const startCompanion = ({ name, port }) => {
       COMPANION_ALLOW_LOCAL_URLS: 'true',
       COMPANION_ALLOW_LOCAL_URLS: 'true',
       COMPANION_ENABLE_URL_ENDPOINT: 'true',
       COMPANION_ENABLE_URL_ENDPOINT: 'true',
       COMPANION_LOGGER_PROCESS_NAME: name,
       COMPANION_LOGGER_PROCESS_NAME: name,
+      COMPANION_OAUTH_ORIGIN: '*',
     },
     },
   })
   })
   // Adding a `then` property so the return value is awaitable:
   // Adding a `then` property so the return value is awaitable:

+ 1 - 0
examples/aws-companion/server.cjs

@@ -35,6 +35,7 @@ const options = {
     bucket: process.env.COMPANION_AWS_BUCKET,
     bucket: process.env.COMPANION_AWS_BUCKET,
     region: process.env.COMPANION_AWS_REGION,
     region: process.env.COMPANION_AWS_REGION,
     endpoint: process.env.COMPANION_AWS_ENDPOINT,
     endpoint: process.env.COMPANION_AWS_ENDPOINT,
+    forcePathStyle: process.env.COMPANION_AWS_FORCE_PATH_STYLE === 'true',
   },
   },
   server: { host: 'localhost:3020' },
   server: { host: 'localhost:3020' },
   filePath: DATA_DIR,
   filePath: DATA_DIR,

+ 1 - 0
examples/aws-nodejs/index.js

@@ -58,6 +58,7 @@ function getS3Client() {
       accessKeyId: process.env.COMPANION_AWS_KEY,
       accessKeyId: process.env.COMPANION_AWS_KEY,
       secretAccessKey: process.env.COMPANION_AWS_SECRET,
       secretAccessKey: process.env.COMPANION_AWS_SECRET,
     },
     },
+    forcePathStyle: process.env.COMPANION_AWS_FORCE_PATH_STYLE === 'true',
   })
   })
   return s3Client
   return s3Client
 }
 }

+ 2 - 0
examples/digitalocean-spaces/server.cjs

@@ -14,6 +14,7 @@ const companion = require('../../packages/@uppy/companion')
  *   - COMPANION_AWS_KEY - Your access key ID
  *   - COMPANION_AWS_KEY - Your access key ID
  *   - COMPANION_AWS_SECRET - Your secret access key
  *   - COMPANION_AWS_SECRET - Your secret access key
  *   - COMPANION_AWS_BUCKET - Your space's name.
  *   - COMPANION_AWS_BUCKET - Your space's name.
+ *   - COMPANION_AWS_FORCE_PATH_STYLE - Indicates if s3ForcePathStyle should be used rather than subdomain for S3 buckets.
  */
  */
 
 
 if (!process.env.COMPANION_AWS_REGION) throw new Error('Missing Space region, please set the COMPANION_AWS_REGION environment variable (eg. "COMPANION_AWS_REGION=ams3")')
 if (!process.env.COMPANION_AWS_REGION) throw new Error('Missing Space region, please set the COMPANION_AWS_REGION environment variable (eg. "COMPANION_AWS_REGION=ams3")')
@@ -43,6 +44,7 @@ const { app: companionApp } = companion.app({
     secret: process.env.COMPANION_AWS_SECRET,
     secret: process.env.COMPANION_AWS_SECRET,
     bucket: process.env.COMPANION_AWS_BUCKET,
     bucket: process.env.COMPANION_AWS_BUCKET,
     region: process.env.COMPANION_AWS_REGION,
     region: process.env.COMPANION_AWS_REGION,
+    forcePathStyle: process.env.COMPANION_AWS_FORCE_PATH_STYLE === 'true',
   },
   },
   server: { host },
   server: { host },
   filePath: DATA_DIR,
   filePath: DATA_DIR,

+ 1 - 0
packages/@uppy/companion-client/src/Provider.ts

@@ -166,6 +166,7 @@ export default class Provider<M extends Meta, B extends Body>
   }): string {
   }): string {
     const params = new URLSearchParams({
     const params = new URLSearchParams({
       ...query,
       ...query,
+      // This is only used for Companion instances configured to accept multiple origins.
       state: btoa(JSON.stringify({ origin: getOrigin() })),
       state: btoa(JSON.stringify({ origin: getOrigin() })),
       ...this.authQuery({ authFormData }),
       ...this.authQuery({ authFormData }),
     })
     })

+ 1 - 0
packages/@uppy/companion/env_example

@@ -39,6 +39,7 @@ COMPANION_AWS_BUCKET=
 COMPANION_AWS_ENDPOINT=
 COMPANION_AWS_ENDPOINT=
 COMPANION_AWS_REGION=
 COMPANION_AWS_REGION=
 COMPANION_AWS_PREFIX=
 COMPANION_AWS_PREFIX=
+COMPANION_AWS_FORCE_PATH_STYLE="false"
 
 
 COMPANION_ZOOM_KEY=
 COMPANION_ZOOM_KEY=
 COMPANION_ZOOM_SECRET=
 COMPANION_ZOOM_SECRET=

+ 16 - 3
packages/@uppy/companion/src/server/controllers/connect.js

@@ -12,12 +12,25 @@ const queryString = (params, prefix = '?') => {
  * @param {object} res
  * @param {object} res
  */
  */
 module.exports = function connect(req, res) {
 module.exports = function connect(req, res) {
-  const { secret } = req.companion.options
+  const { secret, oauthOrigin } = req.companion.options
   const stateObj = oAuthState.generateState()
   const stateObj = oAuthState.generateState()
 
 
-  if (req.query.state) {
+  // not sure if we need to store origin in the session state (e.g. we could've just gotten it directly inside send-token)
+  // but we're afraid to change the logic there
+  if (oauthOrigin && !Array.isArray(oauthOrigin)) {
+    // If the server only allows a single origin, we ignore the client-supplied
+    // origin from query because we don't need it.
+    stateObj.origin = oauthOrigin
+  } else if (oauthOrigin && oauthOrigin.length < 2) {
+    // eslint-disable-next-line prefer-destructuring
+    stateObj.origin = oauthOrigin[0]
+  } else {
+    // If we have multiple allowed origins, we need to check the client-supplied origin from query.
+    // If the client provides an untrusted origin,
+    // we want to send `undefined`. `undefined` means `/`, which is the same origin when passed to `postMessage`.
+    // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-window-postmessage-options-dev
     const { origin } = JSON.parse(atob(req.query.state))
     const { origin } = JSON.parse(atob(req.query.state))
-    stateObj.origin = origin
+    stateObj.origin = oauthOrigin ? oauthOrigin.find(o => o === origin) : origin
   }
   }
 
 
   if (req.companion.options.server.oauthDomain) {
   if (req.companion.options.server.oauthDomain) {

+ 1 - 0
packages/@uppy/companion/src/server/s3-client.js

@@ -29,6 +29,7 @@ module.exports = (companionOptions, createPresignedPostMode = false) => {
     /** @type {import('@aws-sdk/client-s3').S3ClientConfig} */
     /** @type {import('@aws-sdk/client-s3').S3ClientConfig} */
     let s3ClientOptions = {
     let s3ClientOptions = {
       region: s3.region,
       region: s3.region,
+      forcePathStyle: Boolean(s3.forcePathStyle)
     }
     }
 
 
     if (s3.useAccelerateEndpoint) {
     if (s3.useAccelerateEndpoint) {

+ 4 - 0
packages/@uppy/companion/src/standalone/helper.js

@@ -133,6 +133,7 @@ const getConfigFromEnv = () => {
       process.env.COMPANION_AWS_USE_ACCELERATE_ENDPOINT === 'true',
       process.env.COMPANION_AWS_USE_ACCELERATE_ENDPOINT === 'true',
       expires: parseInt(process.env.COMPANION_AWS_EXPIRES || '800', 10),
       expires: parseInt(process.env.COMPANION_AWS_EXPIRES || '800', 10),
       acl: process.env.COMPANION_AWS_ACL,
       acl: process.env.COMPANION_AWS_ACL,
+      forcePathStyle: process.env.COMPANION_AWS_FORCE_PATH_STYLE === 'true',
     },
     },
     server: {
     server: {
       host: process.env.COMPANION_DOMAIN,
       host: process.env.COMPANION_DOMAIN,
@@ -182,6 +183,9 @@ const getConfigFromEnv = () => {
     corsOrigins: getCorsOrigins(),
     corsOrigins: getCorsOrigins(),
     testDynamicOauthCredentials: process.env.COMPANION_TEST_DYNAMIC_OAUTH_CREDENTIALS === 'true',
     testDynamicOauthCredentials: process.env.COMPANION_TEST_DYNAMIC_OAUTH_CREDENTIALS === 'true',
     testDynamicOauthCredentialsSecret: process.env.COMPANION_TEST_DYNAMIC_OAUTH_CREDENTIALS_SECRET,
     testDynamicOauthCredentialsSecret: process.env.COMPANION_TEST_DYNAMIC_OAUTH_CREDENTIALS_SECRET,
+    oauthOrigin: process.env.COMPANION_OAUTH_ORIGIN?.includes(',') ?
+      process.env.COMPANION_OAUTH_ORIGIN.split(',') :
+      process.env.COMPANION_OAUTH_ORIGIN,
   }
   }
 }
 }
 
 

+ 1 - 0
packages/@uppy/companion/test/__tests__/uploader.js

@@ -20,6 +20,7 @@ afterAll(() => {
 
 
 process.env.COMPANION_DATADIR = './test/output'
 process.env.COMPANION_DATADIR = './test/output'
 process.env.COMPANION_DOMAIN = 'localhost:3020'
 process.env.COMPANION_DOMAIN = 'localhost:3020'
+process.env.COMPANION_OAUTH_ORIGIN = '*'
 const { companionOptions } = standalone()
 const { companionOptions } = standalone()
 
 
 const mockReq = {}
 const mockReq = {}

+ 2 - 0
packages/@uppy/companion/test/mockserver.js

@@ -45,6 +45,8 @@ const defaultEnv = {
   COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '',
   COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '',
 
 
   COMPANION_ENABLE_URL_ENDPOINT: 'true',
   COMPANION_ENABLE_URL_ENDPOINT: 'true',
+
+  COMPANION_OAUTH_ORIGIN: '*',
 }
 }
 
 
 function updateEnv (env) {
 function updateEnv (env) {

+ 51 - 0
packages/@uppy/core/src/Uppy.test.ts

@@ -1947,6 +1947,57 @@ describe('src/Core', () => {
     })
     })
   })
   })
 
 
+  describe('clearUploadedFiles', () => {
+    it('should reset state to default', () => {
+      const core = new Core()
+      core.addFile({
+        source: 'vi',
+        name: 'foo.jpg',
+        type: 'image/jpeg',
+        data: testImage,
+      })
+      core.addFile({
+        source: 'vi',
+        name: 'foo2.jpg',
+        type: 'image/jpeg',
+        data: testImage,
+      })
+      core.clearUploadedFiles()
+      expect(core.getState()).toMatchObject({
+        totalProgress: 0,
+        allowNewUpload: true,
+        error: null,
+        recoveredState: null,
+        files: {},
+      })
+    })
+
+    it('should throw error if plugin does not allow removing files during an upload', () => {
+      const core = new Core()
+      const newState = {
+        capabilities: {
+          individualCancellation: false,
+          uploadProgress: true,
+          resumableUploads: false,
+        },
+        currentUploads: {
+          upload1: {
+            fileIDs: [
+              'uppy-file1/jpg-1e-image/jpeg',
+              'uppy-file2/jpg-1e-image/jpeg',
+              'uppy-file3/jpg-1e-image/jpeg',
+            ],
+          },
+        },
+      }
+      // @ts-ignore
+      core.setState(newState)
+      expect(() => {
+        core.clearUploadedFiles()
+      }).toThrowError()
+    })
+  })
+
   describe('checkRestrictions', () => {
   describe('checkRestrictions', () => {
     it('should enforce the maxNumberOfFiles rule', () => {
     it('should enforce the maxNumberOfFiles rule', () => {
       const core = new Core({
       const core = new Core({

+ 2 - 3
packages/@uppy/drag-drop/README.md

@@ -9,8 +9,7 @@
 
 
 Droppable zone UI for Uppy. Drag and drop files into it to upload.
 Droppable zone UI for Uppy. Drag and drop files into it to upload.
 
 
-**[Read the docs](https://uppy.io/docs/dragdrop)** |
-**[Try it](https://uppy.io/examples/dragdrop/)**
+**[Read the docs](https://uppy.io/docs/drag-drop/)**
 
 
 Uppy is being developed by the folks at [Transloadit](https://transloadit.com),
 Uppy is being developed by the folks at [Transloadit](https://transloadit.com),
 a versatile file encoding service.
 a versatile file encoding service.
@@ -41,7 +40,7 @@ Transloadit’s CDN: Edgly. In that case `Uppy` will attach itself to the global
 ## Documentation
 ## Documentation
 
 
 Documentation for this plugin can be found on the
 Documentation for this plugin can be found on the
-[Uppy website](https://uppy.io/docs/dragdrop).
+[Uppy website](https://uppy.io/docs/drag-drop/).
 
 
 ## License
 ## License