Prechádzať zdrojové kódy

Merge remote-tracking branch 'upstream/master' into feat/vue3

Gerald 3 rokov pred
rodič
commit
20bc623a0a

+ 1 - 1
package.json

@@ -79,5 +79,5 @@
       "./test/mock/index.js"
     ]
   },
-  "beta": 24
+  "beta": 25
 }

+ 3 - 0
scripts/webpack.conf.js

@@ -70,8 +70,11 @@ const defsObj = {
     { key: 'VM_VER', val: VM_VER },
     { key: 'SYNC_GOOGLE_CLIENT_ID' },
     { key: 'SYNC_GOOGLE_CLIENT_SECRET' },
+    { key: 'SYNC_GOOGLE_DESKTOP_ID' },
+    { key: 'SYNC_GOOGLE_DESKTOP_SECRET' },
     { key: 'SYNC_ONEDRIVE_CLIENT_ID' },
     { key: 'SYNC_ONEDRIVE_CLIENT_SECRET' },
+    { key: 'SYNC_DROPBOX_CLIENT_ID' },
   ]),
   'process.env.INIT_FUNC_NAME': JSON.stringify(INIT_FUNC_NAME),
   'process.env.VAULT_ID': VAULT_ID,

+ 14 - 5
src/_locales/ru/messages.yml

@@ -212,7 +212,7 @@ filterScopeName:
   message: Имя
 filterSize:
   description: Label for option to sort scripts by size.
-  message: ''
+  message: размер
 genericError:
   description: Label for generic error.
   message: Ошибка
@@ -512,10 +512,17 @@ labelXhrInject:
   description: >-
     (`$1` will be shown as `page`) Option in Advanced settings to enable
     synchronous injection.
-  message: ''
+  message: Режим синхронизации $1
 labelXhrInjectHint:
   description: Tooltip of the `Synchronous page mode` option in Advanced settings.
-  message: ''
+  message: >-
+    Включите только в случае, если у вас есть сценарий, который должен быть
+    запущен до начала загрузки страницы, и в настоящее время он выполняется
+    слишком поздно. Как и режим Instant injection в Tampermonkey, этот вариант
+    использует устаревший синхронный XHR, поэтому в Chrome/Chromium будут
+    предупреждения в консоли devtools, но вы можете игнорировать их, поскольку
+    негативные последствия незначительны. Скрыть предупреждения можно щелкнув
+    ПКМ на одном из них
 lastSync:
   description: Label for last sync timestamp.
   message: Последняя синхронизация в $1
@@ -579,7 +586,9 @@ msgCheckingForUpdate:
   message: Проверка наличия обновлений...
 msgDateFormatInfo:
   description: Help text of the info icon in VM settings for the export file name.
-  message: ''
+  message: >-
+    Нажмите, чтобы открыть документацию MomentJS. Разрешенные маркеры:
+    $1Используйте [квадратные скобки] для защиты буквального текста.
 msgErrorFetchingResource:
   description: >-
     Message shown when Violentmonkey fails fetching a resource/require/icon of
@@ -707,7 +716,7 @@ optionEditorWindowSimple:
   message: Скрыть омнибокс
 optionPopup:
   description: Label of the popup menu section in settings.
-  message: ''
+  message: Всплывающее меню и значок
 optionPopupEnabledFirst:
   description: Option to show enabled scripts first in popup.
   message: Сначала включенные

+ 32 - 1
src/background/sync/base.js

@@ -1,5 +1,5 @@
 import {
-  debounce, normalizeKeys, request, noop, makePause, ensureArray, sendCmd,
+  debounce, normalizeKeys, request, noop, makePause, ensureArray, sendCmd, blob2base64, getUniqId,
 } from '@/common';
 import { TIMEOUT_HOUR } from '@/common/consts';
 import {
@@ -620,3 +620,34 @@ hookOptions((data) => {
   const value = data?.['sync.current'];
   if (value) initialize();
 });
+
+const base64urlMapping = {
+  '+': '-',
+  '/': '_',
+};
+
+async function sha256b64url(code) {
+  const bin = new TextEncoder().encode(code);
+  const buffer = await crypto.subtle.digest('SHA-256', bin);
+  const blob = new Blob([buffer], { type: 'application/octet-binary' });
+  const b64 = await blob2base64(blob);
+  return b64.replace(/[+/=]/g, m => base64urlMapping[m] || '');
+}
+
+/**
+ * Create a unique string between 43 and 128 characters long.
+ *
+ * Ref: RFC 7636
+ */
+export function getCodeVerifier() {
+  return getUniqId(getUniqId(getUniqId()));
+}
+
+export async function getCodeChallenge(codeVerifier) {
+  const method = 'S256';
+  const challenge = await sha256b64url(codeVerifier);
+  return {
+    code_challenge: challenge,
+    code_challenge_method: method,
+  };
+}

+ 65 - 18
src/background/sync/dropbox.js

@@ -1,11 +1,14 @@
+import { getUniqId } from '@/common';
 import { loadQuery, dumpQuery } from '../utils';
 import {
   getURI, getItemFilename, BaseService, isScriptFile, register,
   openAuthPage,
+  getCodeVerifier,
+  getCodeChallenge,
 } from './base';
 
 const config = {
-  client_id: 'f0q12zup2uys5w8',
+  client_id: process.env.SYNC_DROPBOX_CLIENT_ID,
   redirect_uri: 'https://violentmonkey.github.io/auth_dropbox.html',
 };
 
@@ -20,10 +23,25 @@ function jsonStringifySafe(obj) {
 const Dropbox = BaseService.extend({
   name: 'dropbox',
   displayName: 'Dropbox',
+  refreshToken() {
+    const refreshToken = this.config.get('refresh_token');
+    return this.authorized({
+      grant_type: 'refresh_token',
+      refresh_token: refreshToken,
+    })
+    .then(() => this.prepare());
+  },
   user() {
-    return this.loadData({
+    const requestUser = () => this.loadData({
       method: 'POST',
       url: 'https://api.dropboxapi.com/2/users/get_current_account',
+    });
+    return requestUser()
+    .catch((res) => {
+      if (res.status === 401) {
+        return this.refreshToken().then(requestUser);
+      }
+      throw res;
     })
     .catch((err) => {
       if (err.status === 401) {
@@ -94,36 +112,65 @@ const Dropbox = BaseService.extend({
     })
     .then(normalize);
   },
-  authorize() {
+  async authorize() {
+    this.session = {
+      state: getUniqId(),
+      codeVerifier: getCodeVerifier(),
+    };
     const params = {
-      response_type: 'token',
+      response_type: 'code',
+      token_access_type: 'offline',
       client_id: config.client_id,
       redirect_uri: config.redirect_uri,
+      state: this.session.state,
+      ...await getCodeChallenge(this.session.codeVerifier),
     };
     const url = `https://www.dropbox.com/oauth2/authorize?${dumpQuery(params)}`;
     openAuthPage(url, config.redirect_uri);
   },
-  authorized(raw) {
-    const data = loadQuery(raw);
-    if (data.access_token) {
-      this.config.set({
-        uid: data.uid,
-        token: data.access_token,
-      });
-    }
+  async authorized(params) {
+    delete this.headers.Authorization;
+    this.authState.set('authorizing');
+    const data = await this.loadData({
+      method: 'POST',
+      url: 'https://api.dropbox.com/oauth2/token',
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded',
+      },
+      body: dumpQuery({
+        client_id: config.client_id,
+        ...params,
+      }),
+      responseType: 'json',
+    });
+    if (!data.access_token) throw data;
+    this.config.set({
+      uid: data.account_id,
+      token: data.access_token,
+      refresh_token: data.refresh_token || params.refresh_token,
+    });
   },
   checkAuth(url) {
-    const redirectUri = `${config.redirect_uri}#`;
-    if (url.startsWith(redirectUri)) {
-      this.authorized(url.slice(redirectUri.length));
-      this.checkSync();
-      return true;
-    }
+    const redirectUri = `${config.redirect_uri}?`;
+    if (!url.startsWith(redirectUri)) return;
+    const query = loadQuery(url.slice(redirectUri.length));
+    const { state, codeVerifier } = this.session || {};
+    this.session = null;
+    if (query.state !== state || !query.code) return;
+    this.authorized({
+      code: query.code,
+      code_verifier: codeVerifier,
+      grant_type: 'authorization_code',
+      redirect_uri: config.redirect_uri,
+    })
+    .then(() => this.checkSync());
+    return true;
   },
   revoke() {
     this.config.set({
       uid: null,
       token: null,
+      refresh_token: null,
     });
     return this.prepare();
   },

+ 29 - 35
src/background/sync/googledrive.js

@@ -3,16 +3,19 @@
 // - https://github.com/google/google-api-nodejs-client
 import { getUniqId, noop } from '@/common';
 import { objectGet } from '@/common/object';
-import { dumpQuery, notify } from '../utils';
+import { loadQuery, dumpQuery } from '../utils';
 import {
   getURI, getItemFilename, BaseService, register, isScriptFile,
   openAuthPage,
+  getCodeVerifier,
+  getCodeChallenge,
 } from './base';
 
 const config = {
-  client_id: process.env.SYNC_GOOGLE_CLIENT_ID,
-  client_secret: process.env.SYNC_GOOGLE_CLIENT_SECRET,
-  redirect_uri: 'https://violentmonkey.github.io/auth_googledrive.html',
+  client_id: process.env.SYNC_GOOGLE_DESKTOP_ID,
+  client_secret: process.env.SYNC_GOOGLE_DESKTOP_SECRET,
+  redirect_uri: 'http://127.0.0.1:45678/',
+  // redirect_uri: 'https://violentmonkey.github.io/auth_googledrive.html',
   scope: 'https://www.googleapis.com/auth/drive.appdata',
 };
 const UNAUTHORIZED = { status: 'UNAUTHORIZED' };
@@ -39,24 +42,6 @@ const GoogleDrive = BaseService.extend({
     });
     return requestUser()
     .then((info) => {
-      // If access was granted with access_type=online, revoke it.
-      if (info.access_type === 'online') {
-        return this.loadData({
-          method: 'POST',
-          url: `https://accounts.google.com/o/oauth2/revoke?token=${this.config.get('token')}`,
-          prefix: '',
-          headers: {
-            'Content-Type': 'application/x-www-form-urlencoded',
-          },
-        })
-        .then(() => {
-          notify({
-            title: 'Sync Upgraded',
-            body: 'Please reauthorize access to your Google Drive to complete the upgradation.',
-          });
-          return Promise.reject('Online access revoked.');
-        });
-      }
       if (info.scope !== config.scope) return Promise.reject(UNAUTHORIZED);
     })
     .catch((res) => {
@@ -109,28 +94,39 @@ const GoogleDrive = BaseService.extend({
       return Promise.all([gotMeta, remoteData, this.getLocalData()]);
     });
   },
-  authorize() {
+  async authorize() {
+    this.session = {
+      state: getUniqId(),
+      codeVerifier: getCodeVerifier(),
+    };
     const params = {
       response_type: 'code',
-      access_type: 'offline',
       client_id: config.client_id,
       redirect_uri: config.redirect_uri,
       scope: config.scope,
+      state: this.session.state,
+      ...await getCodeChallenge(this.session.codeVerifier),
     };
     if (!this.config.get('refresh_token')) params.prompt = 'consent';
     const url = `https://accounts.google.com/o/oauth2/v2/auth?${dumpQuery(params)}`;
     openAuthPage(url, config.redirect_uri);
   },
   checkAuth(url) {
-    const redirectUri = `${config.redirect_uri}?code=`;
-    if (url.startsWith(redirectUri)) {
-      this.authState.set('authorizing');
-      this.authorized({
-        code: decodeURIComponent(url.split('#')[0].slice(redirectUri.length)),
-      })
-      .then(() => this.checkSync());
-      return true;
-    }
+    const redirectUri = `${config.redirect_uri}?`;
+    if (!url.startsWith(redirectUri)) return;
+    const query = loadQuery(url.slice(redirectUri.length));
+    const { state, codeVerifier } = this.session || {};
+    this.session = null;
+    if (query.state !== state || !query.code) return;
+    this.authState.set('authorizing');
+    this.authorized({
+      code: query.code,
+      code_verifier: codeVerifier,
+      grant_type: 'authorization_code',
+      redirect_uri: config.redirect_uri,
+    })
+    .then(() => this.checkSync());
+    return true;
   },
   revoke() {
     this.config.set({
@@ -150,8 +146,6 @@ const GoogleDrive = BaseService.extend({
       body: dumpQuery(Object.assign({}, {
         client_id: config.client_id,
         client_secret: config.client_secret,
-        redirect_uri: config.redirect_uri,
-        grant_type: 'authorization_code',
       }, params)),
       responseType: 'json',
     })

+ 3 - 2
src/common/util.js

@@ -73,9 +73,10 @@ export function noop() {}
 
 export function getUniqId(prefix = 'VM') {
   const now = performance.now();
+  // `rnd + 1` to make sure the number is large enough and the string is long enough
   return prefix
-    + Math.floor((now - Math.floor(now)) * 1e12).toString(36)
-    + Math.floor(Math.random() * 1e12).toString(36);
+    + Math.floor((now - Math.floor(now) + 1) * 1e12).toString(36)
+    + Math.floor((Math.random() + 1) * 1e12).toString(36);
 }
 
 /**