├── .eslintrc.yaml ├── LICENSE ├── README.md ├── _locales ├── de │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── fr │ └── messages.json ├── pl │ └── messages.json ├── pt_PT │ └── messages.json └── ru │ └── messages.json ├── api ├── api.js ├── app.js ├── event.js ├── installer.js ├── management.js ├── webstore.js └── webstore_private.js ├── background ├── app.js └── content_proxy.js ├── content ├── callback_registry.js ├── channel.js ├── inject │ ├── page.js │ ├── proxy_element.js │ └── store_page.js ├── proxy.js ├── store.css └── store_proxy.js ├── icons ├── 128.png ├── 16.png ├── 24.png ├── 32.png └── 64.png └── manifest.json /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | browser: true 4 | parserOptions: 5 | ecmaVersion: 2017 6 | extends: 'eslint:recommended' 7 | rules: 8 | # Possible Errors 9 | no-constant-condition: 10 | - error 11 | - checkLoops: false 12 | 13 | # Best practices 14 | curly: error 15 | eqeqeq: error 16 | no-console: 17 | - error 18 | - allow: 19 | - assert 20 | - error 21 | - warn 22 | no-else-return: error 23 | no-extra-boolean-cast: off # expressiveness beats it 24 | no-fallthrough: 25 | - error 26 | - commentPattern: 'falls?(\s|\-)?through' 27 | no-invalid-this: error 28 | no-multi-spaces: error 29 | no-undef: off 30 | no-unused-vars: 31 | - error 32 | - vars: local 33 | args: none 34 | 35 | # Stylistic 36 | array-bracket-spacing: error 37 | block-spacing: error 38 | brace-style: 39 | - error 40 | - 1tbs 41 | - allowSingleLine: true 42 | # camelcase: error, # triggers on opt_foo 43 | comma-dangle: 44 | - error 45 | - always-multiline 46 | comma-spacing: error 47 | comma-style: error 48 | computed-property-spacing: error 49 | eol-last: error 50 | key-spacing: error 51 | keyword-spacing: error 52 | linebreak-style: error 53 | max-len: error 54 | new-cap: 55 | - error 56 | - capIsNew: false 57 | new-parens: error 58 | no-multiple-empty-lines: error 59 | no-spaced-func: error 60 | no-trailing-spaces: error 61 | no-unneeded-ternary: error 62 | no-whitespace-before-property: error 63 | object-curly-spacing: error 64 | object-property-newline: 65 | - error 66 | - allowMultiplePropertiesPerLine: true 67 | one-var-declaration-per-line: error 68 | operator-linebreak: 69 | - error 70 | - after 71 | quotes: 72 | - error 73 | - single 74 | semi-spacing: error 75 | space-before-blocks: error 76 | space-before-function-paren: 77 | - error 78 | - never 79 | space-in-parens: error 80 | space-infix-ops: error 81 | space-unary-ops: error 82 | # spaced-comment: error, # triggers on fn(/*foo=*/false) 83 | 84 | # Ecmascript 6 85 | arrow-body-style: 86 | - error 87 | - as-needed 88 | arrow-parens: 89 | - error 90 | - as-needed 91 | arrow-spacing: error 92 | generator-star-spacing: error 93 | no-useless-computed-key: error 94 | no-useless-constructor: error 95 | no-var: error 96 | prefer-arrow-callback: error 97 | prefer-template: error 98 | rest-spread-spacing: error 99 | template-curly-spacing: error 100 | yield-star-spacing: error 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Opera Software AS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chrome-webstore-extension 2 | 3 | Contains the code of [Install Chrome Extensions](https://addons.opera.com/en-gb/extensions/details/download-chrome-extension-9/) hosted at https://addons.opera.com which allows users to install extensions from Google Chrome Web Store directly in Opera browser. 4 | -------------------------------------------------------------------------------- /_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message": "Install Chrome Extensions", 4 | "description": "" 5 | }, 6 | 7 | "description": { 8 | "message": "Installiere Extensions vom Chrome Web Store.", 9 | "description": "" 10 | }, 11 | 12 | "buttonTitle": { 13 | "message": "Zu Opera hinzufügen", 14 | "description": "" 15 | }, 16 | 17 | "installerErrorNetwork": { 18 | "message": "Die Extension konnte aufgrund eines Netzwerkfehlers nicht heruntergeladen werden.\n\n Überprüfe deine Internetverbindung und versuche es erneut.", 19 | "description": "" 20 | }, 21 | 22 | "installerErrorTimeout": { 23 | "message": "Download-Timeout erreicht. Meistens wird es durch langsame oder instabile Internetverbindung verursacht.\n\n Warte einen Moment oder versuche es noch einmal.", 24 | "description": "" 25 | }, 26 | 27 | "installerFinalize": { 28 | "message": "Um die Installation abzuschließen, geh zum Erweiterungsmanager und bestätige die Installation, indem du auf die Schaltfläche Installieren klickst.", 29 | "description": "" 30 | }, 31 | 32 | "installerFinalizeMayWork": { 33 | "message": "Um die Installation abzuschließen, geh zum Erweiterungsmanager und bestätige die Installation, indem du auf die Schaltfläche Installieren klickst.\n\n[Kompatibilitätsanzeige]\nBitte beachte, dass diese Erweiterung APIs erfordert, die in Opera nicht vollständig unterstützt werden.\nEs kann immerhin in Opera funktionieren. Beende die Installation um es zu überprüfen.", 34 | "description": "" 35 | }, 36 | 37 | "installerFinalizeNotSupported": { 38 | "message": "Um die Installation abzuschließen, geh zum Erweiterungsmanager und bestätige die Installation, indem du auf die Schaltfläche Installieren klickst.\n\n[Kompatibilitätsanzeige]\nBitte beachte, dass diese Erweiterung APIs erfordert, die in Opera nicht vollständig unterstützt werden.\nEs kann immerhin in Opera funktionieren. Beende die Installation um es zu überprüfen.", 39 | "description": "" 40 | }, 41 | 42 | "installerNotSupportedApp": { 43 | "message": "Du versuchst, eine APP zu installieren.\n\nOpera unterstützt keine Apps. Es können nur EXTENSIONS installiert werden.", 44 | "description": "" 45 | }, 46 | 47 | "installerNotSupportedTheme": { 48 | "message": "Du versuchst, eine THEME zu installieren.\n\nOpera unterstützt den Chromium Theme Format nicht. Es können nur EXTENSIONS installiert werden.", 49 | "description": "" 50 | }, 51 | 52 | "welcomeYandex": { 53 | "message": "Vielen Dank für die Installation dieser Erweiterung, aber Yandex Benutzer brauchen es nicht. Dein Browser unterstützt Chrome Web Store standardmäßig. Probier es selbst aus.", 54 | "description": "" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message": "Install Chrome Extensions", 4 | "description": "" 5 | }, 6 | 7 | "description": { 8 | "message": "Install extensions from Chrome Web Store.", 9 | "description": "" 10 | }, 11 | 12 | "buttonTitle": { 13 | "message": "Add to Opera", 14 | "description": "" 15 | }, 16 | 17 | "installerErrorNetwork": { 18 | "message": "Unable to download extension package due the network error.\n\nCheck your internet connection and try again.", 19 | "description": "" 20 | }, 21 | 22 | "installerErrorTimeout": { 23 | "message": "Downloading timeout reached. It is most often caused by slow or unstable internet connection.\n\nWait a while or try one more time.", 24 | "description": "" 25 | }, 26 | 27 | "installerFinalize": { 28 | "message": "To complete installation, go to extensions manager and confirm installation by clicking install button.", 29 | "description": "" 30 | }, 31 | 32 | "installerFinalizeMayWork": { 33 | "message": "To complete installation, go to extensions manager and confirm installation by clicking install button.\n\n[Compatibility notice]\nPlease, be aware that this extension requires APIs that aren't fully supported in Opera.\nIt still can work in Opera, so complete installation to verify.", 34 | "description": "" 35 | }, 36 | 37 | "installerFinalizeNotSupported": { 38 | "message": "To complete installation, go to extensions manager and confirm installation by clicking install button.\n\n[Compatibility notice]\nPlease, be aware that this extension requires APIs that are not supported in Opera.\nIt still can work in Opera, so complete installation to verify.", 39 | "description": "" 40 | }, 41 | 42 | "installerNotSupportedApp": { 43 | "message": "You are trying to install an APP.\n\nOpera doesn't support apps. Only EXTENSIONS can be installed.", 44 | "description": "" 45 | }, 46 | 47 | "installerNotSupportedTheme": { 48 | "message": "You are trying to install a THEME.\n\nOpera doesn't support chromium theme format. Only EXTENSIONS can be installed.", 49 | "description": "" 50 | }, 51 | 52 | "welcomeYandex": { 53 | "message": "Thank you for installing this extension but Yandex browser users don't need it. Your browser supports chrome web store by default. Just go there and try yourself.", 54 | "description": "" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message": "Instalar Extensiones de Chrome", 4 | "description": "" 5 | }, 6 | 7 | "description": { 8 | "message": "Instalar extensiones de Chrome Web Store.", 9 | "description": "" 10 | }, 11 | 12 | "buttonTitle": { 13 | "message": "Añadir a Opera", 14 | "description": "" 15 | }, 16 | 17 | "installerErrorNetwork": { 18 | "message": "No se puede descargar la extension por un fallo en la red.\n\nComprueba tu conexión e inténtalo de nuevo.", 19 | "description": "" 20 | }, 21 | 22 | "installerErrorTimeout": { 23 | "message": "Se ha agotado el tiempo de espera. Puede ser debido a una conexión a Internet lenta o inestable.\n\nEspera un poco o inténtalo de nuevo.", 24 | "description": "" 25 | }, 26 | 27 | "installerFinalize": { 28 | "message": "Para finalizar la instalación, abre el Gestor de Extensiones y haz click en el botón de \"Instalar\" para confirmar.", 29 | "description": "" 30 | }, 31 | 32 | "installerFinalizeMayWork": { 33 | "message": "Para finalizar la instalación, abre el Gestor de Extensiones y haz click en el botón de \"Instalar\" para confirmar.\n\n[Nota de compatibilidad]\nPor favor, ten en cuenta que esta extensión requiere APIs que no están completamente soportados en Opera.\nSin embargo, es posible que funcione. Completa la instalación para comprobarlo.", 34 | "description": "" 35 | }, 36 | 37 | "installerFinalizeNotSupported": { 38 | "message": "Para finalizar la instalación, abre el Gestor de Extensiones y haz click en el botón de \"Instalar\" para confirmar.\n\n[Nota de compatibilidad]\nPor favor, ten en cuenta que esta extensión requiere APIs que no son soportados en Opera.\nSin embargo, es posible que funcione. Completa la instalación para comprobarlo.", 39 | "description": "" 40 | }, 41 | 42 | "installerNotSupportedApp": { 43 | "message": "Intentas instalar una APLICACIÓN.\n\nOpera no es compatible con aplicaciones. Solo se puede instalar EXTENSIONES.", 44 | "description": "" 45 | }, 46 | 47 | "installerNotSupportedTheme": { 48 | "message": "Intentas instalar un TEMA.\n\nOpera no es compatible con formato de temas de Chromium. Solo se puede instalar EXTENSIONES.", 49 | "description": "" 50 | }, 51 | 52 | "welcomeYandex": { 53 | "message": "Gracias por instalar esta extensión, pero usuarios de Yandex browser no lo necesitan. Tu navegador soporta Chrome Web Store por defecto. Abrelo para ver por ti mismo.", 54 | "description": "" 55 | } 56 | } -------------------------------------------------------------------------------- /_locales/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message": "Installer des extensions Chrome", 4 | "description": "" 5 | }, 6 | 7 | "description": { 8 | "message": "Installer des extensions du Chrome Web Store.", 9 | "description": "" 10 | }, 11 | 12 | "buttonTitle": { 13 | "message": "Ajouter à Opera", 14 | "description": "" 15 | }, 16 | 17 | "installerErrorNetwork": { 18 | "message": "Impossible de télécharger l’extension à cause d’une erreur réseau.\n\nVeuillez vérifier votre connexion internet et réessayer.", 19 | "description": "" 20 | }, 21 | 22 | "installerErrorTimeout": { 23 | "message": "Délai de téléchargement expiré. Cela est le plus souvent dû à une connexion internet lente ou instable.\n\nVeuillez patienter un instant ou réessayer.", 24 | "description": "" 25 | }, 26 | 27 | "installerFinalize": { 28 | "message": "Afin de terminer l’installation, veuillez ouvrir le gestionnaire d’extensions et confirmer l’installation en cliquant sur le bouton Installer.", 29 | "description": "" 30 | }, 31 | 32 | "installerFinalizeMayWork": { 33 | "message": "Afin de terminer l’installation, veuillez ouvrir le gestionnaire d’extensions et confirmer l’installation en cliquant sur le bouton Installer.\n\n[Notice de compatibilité]\nMerci de bien vouloir noter que cette extension requiert des APIs qui ne sont pas complètement supportées par Opera.\nCela peut toutefois fonctionner dans Opera, veuillez donc terminer l’installation pour voir.", 34 | "description": "" 35 | }, 36 | 37 | "installerFinalizeNotSupported": { 38 | "message": "Afin de terminer l’installation, veuillez ouvrir le gestionnaire d’extensions et confirmer l’installation en cliquant sur le bouton Installer.\n\n[Notice de compatibilité]\nMerci de bien vouloir noter que cette extension requiert des APIs qui ne sont pas supportées par Opera.\nCela peut toutefois fonctionner dans Opera, veuillez donc terminer l’installation pour voir.", 39 | "description": "" 40 | }, 41 | 42 | "installerNotSupportedApp": { 43 | "message": "Vous êtes en train d’essayer d’installer une APP.\n\nOpera ne supporte pas les apps. Seules les EXTENSIONS peuvent être installées.", 44 | "description": "" 45 | }, 46 | 47 | "installerNotSupportedTheme": { 48 | "message": "Vous êtes en train d’installer un THEME.\n\nOpera ne supporte pas le format de thèmes de chromium. Seules les EXTENSIONS peuvent être installées.", 49 | "description": "" 50 | }, 51 | 52 | "welcomeYandex": { 53 | "message": "Merci d’avoir installé cette extension mais les utilisateurs du navigateur Yandex n’en ont pas besoin. Votre navigateur supporte le Chrome Web Store par défaut. Allez-y et essayez par vous-même.", 54 | "description": "" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /_locales/pl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "buttonTitle": { 3 | "description": "", 4 | "message": "Dodaj do Opery" 5 | }, 6 | "description": { 7 | "description": "", 8 | "message": "Zainstaluj rozszerzenia ze sklepu Chrome Web Store." 9 | }, 10 | "installerErrorNetwork": { 11 | "description": "", 12 | "message": "Nie można pobrać pakietu rozszerzenia z powodu błędu sieci. Sprawdź połączenie internetowe i spróbuj ponownie." 13 | }, 14 | "installerErrorTimeout": { 15 | "description": "", 16 | "message": "Przekroczono czas oczekiwania. Jest to najczęściej spowodowane powolnym lub niestabilnym połączeniem sieciowym. Poczekaj jeszcze chwilę lub spróbuj jeszcze raz." 17 | }, 18 | "installerFinalize": { 19 | "description": "", 20 | "message": "Aby zakończyć instalację przejdź do managera rozszerzeń i potwierdź instalację klikając na guzik \"Zainstaluj\"." 21 | }, 22 | "installerFinalizeMayWork": { 23 | "description": "", 24 | "message": "Aby zakończyć instalację przejdź do managera rozszerzeń i potwierdź instalację klikając na guzik \"Zainstaluj\". [Informacja o zgodności] To rozszerzenie wymaga API, które nie są w pełni wspierane w Operze. Mimo to rozszerzenie może działać w Operze, więc zakończ instalację aby to sprawdzić." 25 | }, 26 | "installerFinalizeNotSupported": { 27 | "description": "", 28 | "message": "Aby zakończyć instalację przejdź do managera rozszerzeń i potwierdź instalację klikając na guzik \"Zainstaluj\". [Informacja o zgodności] To rozszerzenie wymaga API, które nie są w wspierane w Operze. Mimo to rozszerzenie może działać w Operze, więc zakończ instalację aby to sprawdzić." 29 | }, 30 | "installerNotSupportedApp": { 31 | "description": "", 32 | "message": "Próbujesz zainstalować aplikację. Opera nie wspiera aplikacji, jedynie rozszerzenia mogą być zainstalowane." 33 | }, 34 | "installerNotSupportedTheme": { 35 | "description": "", 36 | "message": "Próbujesz zainstalować motyw. Opera nie wspiera motywów, jedynie rozszerzenia mogą być zainstalowane." 37 | }, 38 | "name": { 39 | "description": "", 40 | "message": "Zainstaluj rozszerzenia Chrome" 41 | }, 42 | "welcomeYandex": { 43 | "description": "", 44 | "message": "Dziękujemy za zainstalowanie tego rozszerzenia ale użytkownicy przeglądarki Yandex nie potrzebują go. Twoja przeglądarka domyślnie wspiera sklep Chrome Web Store. Po prostu zajrzyj tam i przekonaj się sam." 45 | } 46 | } -------------------------------------------------------------------------------- /_locales/pt_PT/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message": "Instalar Extensões Chrome", 4 | "description": "" 5 | }, 6 | 7 | "description": { 8 | "message": "Instalar extensões da Loja Online Chrome.", 9 | "description": "" 10 | }, 11 | 12 | "buttonTitle": { 13 | "message": "Adicionar ao Browser Opera", 14 | "description": "" 15 | }, 16 | 17 | "installerErrorNetwork": { 18 | "message": "Não foi possível fazer a transferência da extensão devido a um erro de rede.\n\nVerifique a sua ligacão à Internet e tente outra vez.", 19 | "description": "" 20 | }, 21 | 22 | "installerErrorTimeout": { 23 | "message": "O tempo de transferência expirou. A causa mais comum é uma ligação lenta ou instável à Internet.\n\nVolte a tentar mais tarde.", 24 | "description": "" 25 | }, 26 | 27 | "installerFinalize": { 28 | "message": "Para completar a instalação, dirija-se ao gestor de instalações e confirme clicando no botão de instalação.", 29 | "description": "" 30 | }, 31 | 32 | "installerFinalizeMayWork": { 33 | "message": "Para completar a instalação, dirija-se ao gestor de instalações e confirme clicando no botão de instalação.\n\n[Nota de compatibilidade]\nAtenção que esta extensão requer APIs que não são totalmente suportadas pelo browser Opera.\nAinda assim, pode funcionar no browser Opera. Por favor, complete a instalação para confirmar.", 34 | "description": "" 35 | }, 36 | 37 | "installerFinalizeNotSupported": { 38 | "message": "Para completar a instalação, dirija-se ao gestor de instalações e confirme clicando no botão de instalação.\n\n[Nota de Compatibilidade]\nAtenção que esta extensão requer APIs que não são suportadas pelo browser Opera.\nAinda assim, pode funcionar no browser Opera. Por favor, complete a instalação para confirmar.", 39 | "description": "" 40 | }, 41 | 42 | "installerNotSupportedApp": { 43 | "message": "Está a tentar instalar uma APP.\n\nO browser Opera não suporta apps. Apenas extensões podem ser instaladas.", 44 | "description": "" 45 | }, 46 | 47 | "installerNotSupportedTheme": { 48 | "message": "Está a tentar instalar um THEME.\n\nO browser Opera não suporta themes no formato chromium. Apenas extensões podem ser instaladas.", 49 | "description": "" 50 | }, 51 | 52 | "welcomeYandex": { 53 | "message": "Obrigado por instalar esta extensão mas utilizadores do browser Yandex não a necessitam. O browser Yandex já suporta a Chrome Web Store the maneira nativa.", 54 | "description": "" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message": "Установка расширений Chrome", 4 | "description": "" 5 | }, 6 | 7 | "description": { 8 | "message": "Устанавливайте расширения из интернет-магазина Chrome", 9 | "description": "" 10 | }, 11 | 12 | "buttonTitle": { 13 | "message": "Добавить в Opera", 14 | "description": "" 15 | }, 16 | 17 | "installerErrorNetwork": { 18 | "message": "Не удалось загрузить расширение из-за ошибки сети.\n\nПроверьте Ваше соединение и попробуйте ещё раз.", 19 | "description": "" 20 | }, 21 | 22 | "installerErrorTimeout": { 23 | "message": "Максимальное время ожидания скачивания достигнуто. Это может быть вызвано плохим или медленным интернет соединением.\n\nПодождите немного и попробуйте ещё раз.", 24 | "description": "" 25 | }, 26 | 27 | "installerFinalize": { 28 | "message": "Для завершения установки перейдите в менеджер расширений и подтвердите установку расширения нажав на кнопку установки.", 29 | "description": "" 30 | }, 31 | 32 | "installerFinalizeMayWork": { 33 | "message": "Для завершения установки перейдите в менеджер расширений и подтвердите установку расширения нажав на кнопку установки.\n\n[Уведомление о совместимости]\nПожалуйста, помните, что для этого расширения требуется API, который не полностью поддерживается в Opera.\nНо оно может заработать в Opera, поэтому завершите установку чтобы в этом убедиться.", 34 | "description": "" 35 | }, 36 | 37 | "installerFinalizeNotSupported": { 38 | "message": "Для завершения установки перейдите в менеджер расширений и подтвердите установку расширения нажав на кнопку установки.\n\n[Уведомление о совместимости]\nПожалуйста, помните, что для этого расширения требуется API, который не поддерживается в Opera, поэтому завершите установку чтобы в этом убедиться.", 39 | "description": "" 40 | }, 41 | 42 | "installerNotSupportedApp": { 43 | "message": "Вы пытаетесь установить ПРИЛОЖЕНИЕ.\n\nOpera не поддерживает приложения. Только РАСШИРЕНИЯ могут быть установлены.", 44 | "description": "" 45 | }, 46 | 47 | "installerNotSupportedTheme": { 48 | "message": "Вы пытаетесь установить ТЕМУ.\n\nOpera не поддерживает формат тем chromium. Только РАСШИРЕНИЯ могут быть установлены.", 49 | "description": "" 50 | }, 51 | 52 | "welcomeYandex": { 53 | "message": "Спасибо за установку расширения, но пользователям Yandex Браузера это не требуется. Ваш браузер поддерживает интернет-магазин Chrome по умолчанию. Просто зайдите туда и проверьте.", 54 | "description": "" 55 | } 56 | } -------------------------------------------------------------------------------- /api/api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Api { 4 | static get EXTENSION_MANIFEST() { 5 | return { 6 | 'author': this.FEATURE_SUPPORTED, 7 | 'automation': this.FEATURE_NOT_SUPPORTED, 8 | 'background': this.FEATURE_SUPPORTED, 9 | 'background_page': this.FEATURE_NOT_SUPPORTED, 10 | 'browser_action': this.FEATURE_SUPPORTED, 11 | 'chrome_settings_overrides': this.FEATURE_NOT_SUPPORTED, // no effect 12 | 'chrome_ui_overrides': this.FEATURE_MAY_WORK_WITHOUT, // no effect 13 | 'chrome_url_overrides': this.FEATURE_NOT_SUPPORTED, 14 | 'commands': this.FEATURE_SUPPORTED, 15 | 'content_capabilities': this.FEATURE_NOT_SUPPORTED, // need more tests 16 | 'content_scripts': this.FEATURE_SUPPORTED, 17 | 'content_security_policy': this.FEATURE_SUPPORTED, 18 | 'converted_from_user_script': this.FEATURE_SUPPORTED, 19 | 'current_locale': this.FEATURE_SUPPORTED, 20 | 'default_locale': this.FEATURE_SUPPORTED, 21 | 'description': this.FEATURE_SUPPORTED, 22 | 'devtools_page': this.FEATURE_SUPPORTED, 23 | 'event_rules': this.FEATURE_SUPPORTED, 24 | 'export': this.FEATURE_MAY_WORK, // need more tests 25 | 'externally_connectable': this.FEATURE_SUPPORTED, 26 | 'file_browser_handlers': this.FEATURE_MAY_WORK_WITHOUT, 27 | 'file_system_provider_capabilities': this.FEATURE_NOT_SUPPORTED, 28 | 'homepage_url': this.FEATURE_SUPPORTED, 29 | 'icons': this.FEATURE_SUPPORTED, 30 | 'import': this.FEATURE_MAY_WORK, // need more tests 31 | 'incognito': this.FEATURE_SUPPORTED, 32 | 'input_components': this.FEATURE_NOT_SUPPORTED, 33 | 'key': this.FEATURE_SUPPORTED, 34 | 'manifest_version': this.FEATURE_SUPPORTED, 35 | 'minimum_chrome_version': this.FEATURE_SUPPORTED, // no effect 36 | 'nacl_modules': this.FEATURE_NOT_SUPPORTED, // no effect 37 | 'name': this.FEATURE_SUPPORTED, 38 | 'oauth2': this.FEATURE_MAY_WORK, 39 | 'offline_enabled': this.FEATURE_MAY_WORK_WITHOUT, // no effect 40 | 'omnibox': this.FEATURE_SUPPORTED, 41 | 'optional_permissions': this.FEATURE_SUPPORTED, 42 | 'options_page': this.FEATURE_SUPPORTED, 43 | 'options_ui': this.FEATURE_MAY_WORK, 44 | 'page_action': this.FEATURE_SUPPORTED, 45 | 'permissions': { 46 | 'activeTab': this.FEATURE_SUPPORTED, 47 | 'alarms': this.FEATURE_SUPPORTED, 48 | 'background': this.FEATURE_MAY_WORK_WITHOUT, // can work without 49 | 'bookmarks': this.FEATURE_SUPPORTED, 50 | 'browsingData': this.FEATURE_SUPPORTED, 51 | 'certificateProvider': this.FEATURE_NOT_SUPPORTED, 52 | 'clipboardRead': this.FEATURE_SUPPORTED, 53 | 'clipboardWrite': this.FEATURE_SUPPORTED, 54 | 'contentSettings': this.FEATURE_SUPPORTED, 55 | 'contextMenus': this.FEATURE_SUPPORTED, 56 | 'cookies': this.FEATURE_SUPPORTED, 57 | 'debugger': this.FEATURE_SUPPORTED, 58 | 'declarativeContent': this.FEATURE_SUPPORTED, 59 | 'declarativeWebRequest': this.FEATURE_SUPPORTED, 60 | 'desktopCapture': this.FEATURE_SUPPORTED, 61 | 'displaySource': this.FEATURE_NOT_SUPPORTED, 62 | 'dns': this.FEATURE_NOT_SUPPORTED, 63 | 'documentScan': this.FEATURE_NOT_SUPPORTED, 64 | 'downloads': this.FEATURE_SUPPORTED, 65 | 'enterprise.deviceAttributes': this.FEATURE_NOT_SUPPORTED, 66 | 'enterprise.platformKeys': this.FEATURE_NOT_SUPPORTED, 67 | 'experimental': this.FEATURE_NOT_SUPPORTED, 68 | 'fileBrowserHandler': this.FEATURE_MAY_WORK_WITHOUT, 69 | 'fileSystemProvider': this.FEATURE_NOT_SUPPORTED, 70 | 'fontSettings': this.FEATURE_SUPPORTED, 71 | 'gcm': this.FEATURE_SUPPORTED, 72 | 'geolocation': this.FEATURE_SUPPORTED, 73 | 'history': this.FEATURE_SUPPORTED, 74 | 'identity': this.FEATURE_NOT_SUPPORTED, // 90% not supported 75 | 'idle': this.FEATURE_SUPPORTED, 76 | 'idltest': this.FEATURE_NOT_SUPPORTED, 77 | 'management': this.FEATURE_SUPPORTED, 78 | 'nativeMessaging': this.FEATURE_SUPPORTED, 79 | 'networking.config': this.FEATURE_NOT_SUPPORTED, 80 | 'notificationProvider': this.FEATURE_NOT_SUPPORTED, 81 | 'notifications': this.FEATURE_SUPPORTED, 82 | 'pageCapture': this.FEATURE_SUPPORTED, 83 | 'platformKeys': this.FEATURE_NOT_SUPPORTED, 84 | 'power': this.FEATURE_SUPPORTED, 85 | 'printerProvider': this.FEATURE_SUPPORTED, 86 | 'privacy': this.FEATURE_SUPPORTED, 87 | 'processes': this.FEATURE_NOT_SUPPORTED, // dev only 88 | 'proxy': this.FEATURE_SUPPORTED, 89 | 'sessions': this.FEATURE_SUPPORTED, 90 | 'signedInDevices': this.FEATURE_NOT_SUPPORTED, // dev only 91 | 'storage': this.FEATURE_MAY_WORK, // sync ? 92 | 'system.cpu': this.FEATURE_SUPPORTED, 93 | 'system.display': this.FEATURE_SUPPORTED, 94 | 'system.memory': this.FEATURE_SUPPORTED, 95 | 'system.storage': this.FEATURE_SUPPORTED, 96 | 'tabCapture': this.FEATURE_SUPPORTED, 97 | 'tabs': this.FEATURE_SUPPORTED, 98 | 'topSites': this.FEATURE_SUPPORTED, 99 | 'tts': this.FEATURE_SUPPORTED, 100 | 'ttsEngine': this.FEATURE_SUPPORTED, 101 | 'unlimitedStorage': this.FEATURE_SUPPORTED, 102 | 'vpnProvider': this.FEATURE_NOT_SUPPORTED, 103 | 'wallpaper': this.FEATURE_NOT_SUPPORTED, 104 | 'webNavigation': this.FEATURE_SUPPORTED, 105 | 'webRequest': this.FEATURE_SUPPORTED, 106 | 'webRequestBlocking': this.FEATURE_SUPPORTED, 107 | 'webview': this.FEATURE_MAY_WORK, // yet 108 | 'windows': this.FEATURE_SUPPORTED, 109 | }, 110 | 'platforms': this.FEATURE_NOT_SUPPORTED, // for NaCL 111 | 'plugins': this.FEATURE_SUPPORTED, 112 | 'requirements': this.FEATURE_SUPPORTED, 113 | 'sandbox': this.FEATURE_SUPPORTED, 114 | 'short_name': this.FEATURE_SUPPORTED, 115 | 'signature': this.FEATURE_NOT_SUPPORTED, 116 | 'spellcheck': this.FEATURE_SUPPORTED, 117 | 'storage': this.FEATURE_SUPPORTED, 118 | 'system_indicator': this.FEATURE_MAY_WORK_WITHOUT, // no effect 119 | 'tts_engine': this.FEATURE_SUPPORTED, 120 | 'update_url': this.FEATURE_SUPPORTED, 121 | 'version': this.FEATURE_SUPPORTED, 122 | 'version_name': this.FEATURE_SUPPORTED, 123 | 'web_accessible_resources': this.FEATURE_SUPPORTED, 124 | 'webview': this.FEATURE_MAY_WORK, // yet 125 | }; 126 | } 127 | 128 | static get FEATURE_MAY_WORK() { return 'feature_may_work'; } 129 | static get FEATURE_MAY_WORK_WITHOUT() { return 'feature_may_work_without'; } 130 | static get FEATURE_NOT_SUPPORTED() { return 'feature_not_supported'; } 131 | static get FEATURE_SUPPORTED() { return 'feature_supported'; } 132 | static get TYPE_EVENT() { return 'event'; } 133 | static get TYPE_FUNCTION() { return 'function'; } 134 | static get TYPE_OBJECT() { return 'object'; } 135 | 136 | static areFeaturesSupported_(features, dictionary) { 137 | const states = features.map(feature => { 138 | if (this.isHostPattern_(feature)) { 139 | return this.FEATURE_SUPPORTED; 140 | } 141 | 142 | const state = dictionary[feature] || this.FEATURE_NOT_SUPPORTED; 143 | return state instanceof Object ? this.FEATURE_SUPPORTED : state; 144 | }); 145 | 146 | return this.getComputedFeatureState_(states); 147 | } 148 | 149 | static get base() { return {}; } 150 | 151 | static get() { 152 | let cl = new this(...arguments); 153 | let api = this.base; 154 | let props = Object.getOwnPropertyNames(Object.getPrototypeOf(cl)); 155 | for (let prop of props) { 156 | if (prop !== 'constructor' && !prop.endsWith('_')) { 157 | if (cl[prop] instanceof Function) { 158 | api[prop] = cl[prop].bind(cl); 159 | } else { 160 | api[prop] = cl[prop]; 161 | } 162 | } 163 | } 164 | 165 | return api; 166 | } 167 | 168 | static getComputedFeatureState_(states) { 169 | if (states.some(state => state === this.FEATURE_NOT_SUPPORTED)) { 170 | return this.FEATURE_NOT_SUPPORTED; 171 | } 172 | if (states.some(state => state === this.FEATURE_MAY_WORK)) { 173 | return this.FEATURE_MAY_WORK; 174 | } 175 | if (states.some(state => state === this.FEATURE_MAY_WORK_WITHOUT)) { 176 | return this.FEATURE_MAY_WORK_WITHOUT; 177 | } 178 | return this.FEATURE_SUPPORTED; 179 | } 180 | 181 | static isApp(manifest) { return 'app' in manifest; } 182 | 183 | static isExtension(manifest) { 184 | return !(this.isApp(manifest) || this.isTheme(manifest)); 185 | } 186 | 187 | static isHostPattern_(value) { 188 | return value === '' || value.includes('://'); 189 | } 190 | 191 | static isManifestSupported(manifest) { 192 | const states = []; 193 | const permissions = manifest.permissions; 194 | const optionalPermissions = manifest.optional_permissions; 195 | states.push(this.areFeaturesSupported_( 196 | Object.keys(manifest), this.EXTENSION_MANIFEST)); 197 | if (optionalPermissions) { 198 | states.push(this.areFeaturesSupported_( 199 | optionalPermissions, this.EXTENSION_MANIFEST.permissions)); 200 | } 201 | if (permissions) { 202 | states.push(this.areFeaturesSupported_( 203 | permissions, this.EXTENSION_MANIFEST.permissions)); 204 | } 205 | 206 | return this.getComputedFeatureState_(states); 207 | } 208 | 209 | static isTheme(manifest) { return 'theme' in manifest; } 210 | } 211 | -------------------------------------------------------------------------------- /api/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class App extends Api { 4 | get isInstalled() { return true; } 5 | 6 | getDetails() { 7 | return { 8 | 'app': { 9 | 'launch': { 10 | 'web_url': 'https://chrome.google.com/webstore', 11 | }, 12 | 'urls': [ 13 | 'https://chrome.google.com/webstore', 14 | ], 15 | }, 16 | 'description': 'Discover great apps, games, extensions and themes for Google Chrome.', 17 | 'icons': { 18 | '16': 'webstore_icon_16.png', 19 | '128': 'webstore_icon_128.png', 20 | }, 21 | 'id': 'ahfgeienlihckogmohjhadlkjgocpleb', 22 | 'key': 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCtl3tO0osjuzRsf6xtD2SKxPlTfuoy7AWoObysitBPvH5fE1NaAA1/2JkPWkVDhdLBWLaIBPYeXbzlHp3y4Vv/4XG+aN5qFE3z+1RU/NqkzVYHtIpVScf3DjTYtKVL66mzVGijSoAIwbFCC3LpGdaoe6Q1rSRDp76wR6jjFzsYwQIDAQAB', 23 | 'name': 'Web Store', 24 | 'permissions': [ 25 | 'webstorePrivate', 26 | 'management', 27 | 'system.cpu', 28 | 'system.display', 29 | 'system.memory', 30 | 'system.network', 31 | 'system.storage', 32 | ], 33 | 'version': '0.2', 34 | } 35 | } 36 | 37 | getIsInstalled() { return this.isInstalled; } 38 | 39 | installState(callback) { 40 | if (callback) { 41 | callback('installed'); 42 | } 43 | } 44 | 45 | runningState() { return 'cannot_run'; } 46 | } 47 | -------------------------------------------------------------------------------- /api/event.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Event extends Api { 4 | static get base() { return new (class Event {})(); } 5 | constructor(event, filter) { 6 | super(); 7 | this.event_ = event; 8 | /* 9 | TODO: Add support for rules. 10 | 11 | this.ruleID_ = 0; 12 | this.rules_ = new Map(); 13 | */ 14 | this.listeners_ = new Set(); 15 | 16 | if (filter instanceof Function) { 17 | this.listen_ = data => { 18 | if (filter(data)) { 19 | this.dispatch(data); 20 | } 21 | } 22 | } else { 23 | this.listen_ = data => this.dispatch(data); 24 | } 25 | } 26 | 27 | addListener(listener) { 28 | if (!listener) { 29 | return; 30 | } 31 | if (!this.listeners_.size && this.event_) { 32 | this.event_.addListener(this.listen_); 33 | } 34 | this.listeners_.add(listener); 35 | } 36 | 37 | dispatch(data) { this.listeners_.forEach(listener => listener(data)); } 38 | dispatchToListener(data, listener) { listener(data); } 39 | hasListener(listener) { return this.listeners_.has(listener); } 40 | hasListeners() { return this.listeners_.size > 0; } 41 | 42 | removeListener(listener) { 43 | if (this.hasListener(listener)) { 44 | this.listeners_.delete(listener); 45 | if (!this.listeners_.size && this.event_) { 46 | this.event_.removeListener(this.listen_); 47 | } 48 | } 49 | } 50 | 51 | addRules(rules, callback) { 52 | /* 53 | rules = rules.map(rule => { 54 | let id = rule.id; 55 | if (!id) { 56 | this.ruleID_ = (this.ruleID_ + 1) % 10000; 57 | id = `${this.ruleID_}${Date.now()}`; 58 | } 59 | rule.id = id; 60 | rule.priority = rule.priority || 100; 61 | this.rules_.set(id, rule); 62 | return rule; 63 | }); 64 | 65 | if (callback) { 66 | callback(rules); 67 | } 68 | */ 69 | } 70 | 71 | getRules(ruleIdentifiers, callback) { 72 | /* 73 | if (callback) { 74 | callback(ruleIdentifiers.map(id => this.rules_.get(id))); 75 | } 76 | */ 77 | } 78 | 79 | removeRules(ruleIdentifiers, callback) { 80 | /* 81 | ruleIdentifiers.forEach(id => this.rules_.delete(id)); 82 | if (callback) { 83 | callback(); 84 | } 85 | */ 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /api/installer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Installer extends Api { 4 | static get CRX_HOST() { 5 | return 'https://clients2.google.com/service/update2/crx'; 6 | } 7 | 8 | static get MAX_DOWNLOADING_TIME() { return 45 * 1000; } 9 | 10 | constructor() { 11 | super(); 12 | this.onDownloadProgress_ = Event.get(); 13 | this.onInstallRequest_ = Event.get(); 14 | this.onInstallStageChanged_ = Event.get(); 15 | 16 | this.jobs_ = new Map(); 17 | this.browserVersion_ = navigator.userAgent.match(/chrome\/([0-9.]*)\s/i)[1]; 18 | chrome.runtime.getPlatformInfo( 19 | platformInfo => this.platformInfo_ = platformInfo); 20 | 21 | chrome.management.onInstalled.addListener(details => { 22 | if (details && details.id) { 23 | this.jobComplete_(details.id, this.Result.SUCCESS); 24 | } 25 | }); 26 | chrome.management.onUninstalled.addListener(id => { 27 | if (id) { 28 | this.jobComplete_(id, this.Result.USER_CANCELLED); 29 | } 30 | }); 31 | } 32 | 33 | get InstallStage() { 34 | return { 35 | DOWNLOADING: 'downloading', 36 | INSTALLING: 'installing', 37 | }; 38 | } 39 | 40 | get Result() { 41 | return { 42 | ALREADY_INSTALLED: 'already_installed', 43 | BLACKLISTED: 'blacklisted', 44 | BLOCKED_BY_POLICY: 'blocked_by_policy', 45 | FEATURE_DISABLED: 'feature_disabled', 46 | ICON_ERROR: 'icon_error', 47 | INSTALL_ERROR: 'install_error', 48 | INSTALL_IN_PROGRESS: 'install_in_progress', 49 | INVALID_ICON_URL: 'invalid_icon_url', 50 | INVALID_ID: 'invalid_id', 51 | LAUNCH_IN_PROGRESS: 'launch_in_progress', 52 | MANIFEST_ERROR: 'manifest_error', 53 | MISSING_DEPENDENCIES: 'missing_dependencies', 54 | SUCCESS: 'success', 55 | UNKNOWN_ERROR: 'unknown_error', 56 | UNSUPPORTED_EXTENSION_TYPE: 'unsupported_extension_type', 57 | USER_CANCELLED: 'user_cancelled', 58 | USER_GESTURE_REQUIRED: 'user_gesture_required', 59 | }; 60 | } 61 | 62 | get onDownloadProgress() { return this.onDownloadProgress_; } 63 | get onInstallRequest() { return this.onInstallRequest_; } 64 | get onInstallStageChanged() { return this.onInstallStageChanged_; } 65 | 66 | isInstalled(id) { 67 | return this.getExtension_(id).then( 68 | extension => Boolean(extension) && extension.enabled); 69 | } 70 | 71 | isTypeSupported_(manifest) { 72 | if (Api.isTheme(manifest)) { 73 | alert(chrome.i18n.getMessage('installerNotSupportedTheme')); 74 | return false; 75 | } 76 | 77 | if (Api.isApp(manifest)) { 78 | alert(chrome.i18n.getMessage('installerNotSupportedApp')); 79 | return false; 80 | } 81 | 82 | return true; 83 | } 84 | 85 | getDownload_(url) { 86 | return new Promise(resolve => { 87 | chrome.downloads.search({}, items => { 88 | items = items.filter(item => item.url === url); 89 | if (items) { 90 | const item = items[0]; 91 | chrome.downloads.erase({url}); 92 | resolve(item); 93 | } else { 94 | resolve(null); 95 | } 96 | }); 97 | }); 98 | } 99 | 100 | getDownloadData_(id) { 101 | const params = new URLSearchParams(''); 102 | params.append('response', 'redirect'); 103 | params.append('os', this.platformInfo_.os); 104 | params.append('arch', this.platformInfo_.arch); 105 | params.append('nacl_arch', this.platformInfo_.nacl_arch); 106 | params.append('prod', 'chromiumcrx'); 107 | params.append('prodchannel', 'unknown'); 108 | params.append('prodversion', this.browserVersion_); 109 | params.append('acceptformat', 'crx2,crx3'); 110 | params.append('x', `id=${id}&uc`); 111 | 112 | return { 113 | 'filename': `${id}.nex`, 114 | 'url': `${this.constructor.CRX_HOST}?${params}`, 115 | }; 116 | } 117 | 118 | getExtension_(id) { 119 | return new Promise(resolve => { 120 | chrome.management.get( 121 | id, details => resolve( 122 | (chrome.runtime.lastError || !details) ? null : details)); 123 | }); 124 | } 125 | 126 | getIdFromUrl(url) { 127 | url = new URL(url); 128 | const id = url.pathname.split('/').reduceRight( 129 | (id, value) => (id || (/^[a-z]{32}$/.test(value) && value)), null); 130 | if (id) { 131 | return id; 132 | } 133 | } 134 | 135 | accept_(id, manifestState) { 136 | this.onInstallStageChanged.dispatch( 137 | {id, stage: this.InstallStage.INSTALLING}); 138 | if (manifestState === Api.FEATURE_NOT_SUPPORTED) { 139 | alert(chrome.i18n.getMessage('installerFinalizeNotSupported')); 140 | } else if ( 141 | manifestState === Api.FEATURE_MAY_WORK || 142 | manifestState === Api.FEATURE_MAY_WORK_WITHOUT) { 143 | alert(chrome.i18n.getMessage('installerFinalizeMayWork')); 144 | } else { 145 | alert(chrome.i18n.getMessage('installerFinalize')); 146 | } 147 | 148 | this.navigateToExtensionDetails(id); 149 | } 150 | 151 | download_(id) { 152 | const options = this.getDownloadData_(id); 153 | const maxTime = Date.now() + this.constructor.MAX_DOWNLOADING_TIME; 154 | this.onDownloadProgress.dispatch({id, percentDownloaded: 0}); 155 | this.onInstallStageChanged.dispatch( 156 | {id, stage: this.InstallStage.DOWNLOADING}); 157 | return new Promise((resolve, reject) => { 158 | 159 | chrome.downloads.download(options, (downloadId) => { 160 | if (downloadId) { 161 | this.onDownloadProgress.dispatch({id, percentDownloaded: 1}); 162 | resolve(); 163 | } else { 164 | this.getDownload_(options.url).then((download) => { 165 | if (download && download.error) { 166 | alert(chrome.i18n.getMessage('installerErrorNetwork')); 167 | } else if (Date.now() > maxTime) { 168 | alert(chrome.i18n.getMessage('installerErrorTimeout')); 169 | } 170 | reject(); 171 | }).catch(() => { 172 | reject(); 173 | }); 174 | } 175 | }); 176 | 177 | }); 178 | } 179 | 180 | install({manifest, id}, onComplete = () => {}, onError = () => {}) { 181 | this.onInstallRequest.dispatch(id); 182 | this.jobs_.set(id, status => { 183 | this.jobs_.delete(id); 184 | if (status === this.Result.SUCCESS) { 185 | onComplete(); 186 | } else { 187 | onError(status); 188 | } 189 | }); 190 | 191 | if (!id) { 192 | this.jobComplete_(id, this.Result.INVALID_ID); 193 | return; 194 | } 195 | 196 | if (manifest && !this.isTypeSupported_(manifest)) { 197 | this.jobComplete_(id, this.Result.UNSUPPORTED_EXTENSION_TYPE); 198 | return; 199 | } 200 | 201 | const manifestState = 202 | manifest ? Api.isManifestSupported(manifest) : Api.FEATURE_SUPPORTED; 203 | 204 | this.getExtension_(id).then(extension => { 205 | if (extension) { 206 | if (extension.enabled) { 207 | this.navigateToExtensionDetails(id); 208 | this.jobComplete_(id, this.Result.SUCCESS); 209 | } else { 210 | this.accept_(id, manifestState); 211 | } 212 | } else { 213 | this.download_(id) 214 | .then(() => this.accept_(id, manifestState)) 215 | .catch(() => this.jobComplete_(id, this.Result.UNKNOWN_ERROR)); 216 | } 217 | }); 218 | } 219 | 220 | jobComplete_(id, status) { 221 | const complete = this.jobs_.get(id); 222 | if (complete) { 223 | complete(status); 224 | } 225 | } 226 | 227 | navigateToExtensionDetails(id) { 228 | chrome.tabs.create({ 229 | 'url': `chrome://extensions?id=${id}`, 230 | 'active': true, 231 | }); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /api/management.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Management extends Api { 4 | static get UPDATE_URL() { return 'clients2.google.com'; } 5 | 6 | constructor() { 7 | super(); 8 | this.api_ = chrome.management; 9 | this.onDisabled_ = Event.get(this.api_.onDisabled); 10 | this.onEnabled_ = Event.get(this.api_.onEnabled); 11 | this.onInstalled_ = Event.get(this.api_.onInstalled); 12 | this.onUninstalled_ = Event.get(this.api_.onUninstalled); 13 | } 14 | 15 | get ExtensionDisabledReason() { return this.api_.ExtensionDisabledReason; } 16 | get ExtensionInstallType() { return this.api_.ExtensionInstallType; } 17 | get ExtensionType() { return this.api_.ExtensionType; } 18 | get LaunchType() { return this.api_.LaunchType; } 19 | get onDisabled() { return this.onDisabled_; } 20 | get onEnabled() { return this.onEnabled_; } 21 | get onInstalled() { return this.onInstalled_; } 22 | get onUninstalled() { return this.onUninstalled_; } 23 | 24 | isFromStore_(item) { 25 | return item && item.updateUrl && 26 | item.updateUrl.indexOf(this.constructor.UPDATE_URL) !== -1; 27 | } 28 | 29 | createAppShortcut() { return this.api_.createAppShortcut(...arguments); } 30 | generateAppForLink() { return this.api_.generateAppForLink(...arguments); } 31 | 32 | get(id, callback = () => {}) { 33 | return this.api_.get( 34 | id, item => { callback(this.isFromStore_(item) ? item : null); }); 35 | } 36 | 37 | getAll(callback = () => {}) { 38 | return this.api_.getAll( 39 | items => { callback(items.filter(item => this.isFromStore_(item))); }); 40 | } 41 | 42 | getPermissionWarningsById(id, callback = () => {}) { 43 | this.get(id, item => { 44 | if (!item) { 45 | callback([]); 46 | } else { 47 | this.api_.getPermissionWarningsById(...arguments); 48 | } 49 | }); 50 | } 51 | 52 | launchApp() { return this.api_.launchApp(...arguments); } 53 | 54 | setEnabled(id, enabled, callback = () => {}) { 55 | this.get(id, item => { 56 | if (!item) { 57 | callback(); 58 | } else { 59 | this.api_.setEnabled(...arguments); 60 | } 61 | }); 62 | } 63 | 64 | setLaunchType() { return this.api_.setLaunchType(...arguments); } 65 | 66 | uninstall(id) { 67 | let args = Array.from(arguments); 68 | let callback = () => {}; 69 | if (args[args.length - 1] instanceof Function) { 70 | callback = args[args.length - 1]; 71 | } 72 | 73 | this.get(id, item => { 74 | if (!item) { 75 | callback(); 76 | } else { 77 | this.api_.uninstall(...arguments); 78 | } 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /api/webstore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Webstore extends Api { 4 | constructor() { 5 | super(); 6 | this.onDownloadProgress_ = Event.get(); 7 | this.onInstallStageChanged_ = Event.get(); 8 | } 9 | 10 | get onDownloadProgress() { return this.onDownloadProgress_; } 11 | get onInstallStageChanged() { return this.onInstallStageChanged_; } 12 | 13 | install(url, onSuccess, onFailure) {} 14 | } 15 | -------------------------------------------------------------------------------- /api/webstore_private.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class WebstorePrivate extends Api { 4 | constructor(installer) { 5 | super(); 6 | this.installer_ = installer; 7 | } 8 | 9 | get Result() { return this.installer_.Result; } 10 | 11 | get WebGlStatus() { 12 | return { 13 | WEBGL_ALLOWED: 'webgl_allowed', 14 | WEBGL_BLOCKED: 'webgl_blocked', 15 | }; 16 | } 17 | 18 | beginInstallWithManifest3({id, manifest}, callback) { 19 | manifest = JSON.parse(manifest); 20 | 21 | this.installer_.install( 22 | {id, manifest}, 23 | () => callback(''), 24 | () => callback(this.Result.USER_CANCELLED)); 25 | } 26 | 27 | completeInstall(id, callback) { 28 | if (callback) { 29 | callback([]); 30 | } 31 | } 32 | 33 | enableAppLauncher() {} 34 | 35 | getBrowserLogin(callback) { 36 | this.getStoreLogin(login => callback({'login': login})); 37 | } 38 | 39 | getEphemeralAppsEnabled(callback) { 40 | if (callback) { 41 | callback(false); 42 | } 43 | } 44 | 45 | getIsLauncherEnabled(callback) { 46 | if (callback) { 47 | callback(false); 48 | } 49 | } 50 | 51 | getStoreLogin(callback) { callback(localStorage.getItem('login') || ''); } 52 | 53 | getWebGLStatus(callback) { callback(this.WebGlStatus.WEBGL_ALLOWED); } 54 | 55 | install() {} 56 | 57 | isInIncognitoMode(callback) { 58 | if (callback) { 59 | callback(false); 60 | } 61 | } 62 | 63 | isPendingCustodianApproval(id, callback) { 64 | if (callback) { 65 | callback(false); 66 | } 67 | } 68 | 69 | launchEphemeralApp(id, callback) { 70 | if (callback) { 71 | callback(); 72 | } 73 | } 74 | 75 | setStoreLogin(login, callback) { 76 | localStorage.setItem('login', login); 77 | callback(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /background/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const WEBSTORE_HOST_PATTERN = '*://*.chrome.google.com/*'; 5 | const YANDEX_UA_CODE = 'YaBrowser'; 6 | const YANDEX_ERROR_MESSAGE = 'welcomeYandex'; 7 | 8 | class EventPageExtension { 9 | constructor() { 10 | if (!this.isSupportedBrowser_()) { 11 | chrome.management.uninstallSelf(); 12 | return; 13 | } 14 | 15 | this.registerListeners_(); 16 | } 17 | 18 | isSupportedBrowser_() { 19 | if (navigator.userAgent.includes(YANDEX_UA_CODE)) { 20 | alert(chrome.i18n.getMessage(YANDEX_ERROR_MESSAGE)); 21 | return false; 22 | } 23 | return true; 24 | } 25 | 26 | registerListeners_() { 27 | chrome.runtime.onConnect.addListener(port => this.createProxy_(port)); 28 | chrome.runtime.onInstalled.addListener(({reason}) => { 29 | const {INSTALL, UPDATE} = chrome.runtime.OnInstalledReason; 30 | if (reason === INSTALL || reason === UPDATE) { 31 | this.reloadWebstoreTabs_(); 32 | } 33 | }); 34 | } 35 | 36 | createProxy_(port) { 37 | const installer = Installer.get(); 38 | const management = Management.get(); 39 | const webstorePrivate = WebstorePrivate.get(installer); 40 | 41 | new ContentProxy(port, installer, {management, webstorePrivate}); 42 | } 43 | 44 | reloadWebstoreTabs_() { 45 | chrome.tabs.query({url: WEBSTORE_HOST_PATTERN}, tabs => { 46 | for (let {id} of tabs) { 47 | chrome.tabs.reload(id); 48 | } 49 | }); 50 | } 51 | } 52 | 53 | new EventPageExtension(); 54 | } 55 | -------------------------------------------------------------------------------- /background/content_proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class ContentProxy { 4 | static get ACTION_ADD_TO_OPERA() { return 'add-to-opera'; } 5 | 6 | constructor(port, installer, chromeWhitelist, operaWhitelist) { 7 | this.port_ = port; 8 | const tab = this.port_.sender.tab; 9 | this.tabId_ = tab.id; 10 | this.installer_ = installer; 11 | this.opr_ = operaWhitelist || {}; 12 | this.chrome_ = chromeWhitelist || {}; 13 | 14 | this.registeredListeners_ = []; 15 | this.registerPortListeners_(); 16 | this.onTabUpdate_(tab); 17 | } 18 | 19 | get opr() { return this.opr_; } 20 | get chrome() { return this.chrome_; } 21 | 22 | registerPortListeners_() { 23 | const onDisconnect = () => { 24 | for (let listener of this.registeredListeners_) { 25 | listener(); 26 | } 27 | }; 28 | this.port_.onDisconnect.addListener(onDisconnect); 29 | this.callOnDisconnect_( 30 | () => this.port_.onDisconnect.removeListener(onDisconnect)); 31 | 32 | const onMessage = message => this.onMessage_(message); 33 | this.port_.onMessage.addListener(onMessage); 34 | this.callOnDisconnect_( 35 | () => this.port_.onMessage.removeListener(onMessage)); 36 | 37 | const onActivated = info => { 38 | chrome.tabs.get(info.tabId, tab => this.onTabUpdate_(tab)); 39 | }; 40 | chrome.tabs.onActivated.addListener(onActivated); 41 | this.callOnDisconnect_( 42 | () => chrome.tabs.onActivated.removeListener(onActivated)); 43 | 44 | const onUpdated = (id, changes, tab) => this.onTabUpdate_(tab); 45 | chrome.tabs.onUpdated.addListener(onUpdated); 46 | this.callOnDisconnect_( 47 | () => chrome.tabs.onUpdated.removeListener(onUpdated)); 48 | 49 | const onClicked = tab => { 50 | if (!tab || tab.id !== this.tabId_ || !tab.url) { 51 | return; 52 | } 53 | this.onButtonClicked_(tab.url); 54 | }; 55 | chrome.pageAction.onClicked.addListener(onClicked); 56 | this.callOnDisconnect_( 57 | () => chrome.pageAction.onClicked.removeListener(onClicked)); 58 | } 59 | 60 | callOnDisconnect_(method) { this.registeredListeners_.push(method); } 61 | 62 | addListener_(message) { 63 | const id = message.id; 64 | const add = this.getMethod_(message.add); 65 | if (!add) { 66 | return; 67 | } 68 | 69 | const listener = (...data) => this.port_.postMessage({data, id}); 70 | add(listener); 71 | 72 | if (!message.remove) { 73 | return; 74 | } 75 | const remove = this.getMethod_(message.remove); 76 | if (!remove) { 77 | return; 78 | } 79 | 80 | this.callOnDisconnect_(() => remove(listener)); 81 | } 82 | 83 | setButtonVisibility_(isVisible) { 84 | if (isVisible) { 85 | chrome.pageAction.show(this.tabId_); 86 | } else { 87 | chrome.pageAction.hide(this.tabId_); 88 | } 89 | } 90 | 91 | callMethod_(message) { 92 | const id = message.id; 93 | const method = this.getMethod_(message.type); 94 | const callback = data => this.port_.postMessage({data, id}); 95 | 96 | if (!method) { 97 | callback(); 98 | return; 99 | } 100 | 101 | const args = message.data; 102 | try { 103 | if (message.isSync) { 104 | callback([method(...args)]); 105 | } else { 106 | args.push((...data) => callback(data)); 107 | method(...args); 108 | } 109 | } catch (e) { 110 | callback(); 111 | } 112 | } 113 | 114 | fetch(url, callback) { 115 | const xhr = new XMLHttpRequest(); 116 | xhr.onload = () => callback(xhr.response); 117 | xhr.open('GET', chrome.extension.getURL(url)); 118 | xhr.send(); 119 | } 120 | 121 | getDefinition(name, callback) { 122 | let api = this; 123 | let defs; 124 | const path = name.split('.'); 125 | 126 | try { 127 | while (path.length) { 128 | api = api[path.shift()]; 129 | } 130 | 131 | if (api.constructor.name !== 'Object') { 132 | if (api.constructor.name === 'Function') { 133 | defs = api.prototype; 134 | } else { 135 | defs = Object.getPrototypeOf(api); 136 | } 137 | } else { 138 | defs = api; 139 | } 140 | 141 | const items = Object.getOwnPropertyNames(defs).map(name => { 142 | const data = {name}; 143 | const item = api[name]; 144 | data.type = this.getType_(item); 145 | 146 | if (data.type === Api.TYPE_OBJECT) { 147 | data.value = Object.assign({}, item); 148 | } 149 | return data; 150 | }).filter(entry => entry !== null); 151 | 152 | callback(items); 153 | } catch (e) { 154 | callback([]); 155 | } 156 | } 157 | 158 | getMethod_(path) { 159 | path = path.split('.'); 160 | let root = this; 161 | let method = root; 162 | try { 163 | while (path.length) { 164 | root = method; 165 | method = root[path.shift()]; 166 | } 167 | 168 | return method.bind(root); 169 | } catch (e) { 170 | return null; 171 | } 172 | } 173 | 174 | getType_(item) { 175 | if (item.constructor.name.toLowerCase().endsWith('event')) { 176 | return Api.TYPE_EVENT; 177 | } 178 | if (item instanceof Function) { 179 | return Api.TYPE_FUNCTION; 180 | } 181 | if (item instanceof Object) { 182 | return Api.TYPE_OBJECT; 183 | } 184 | } 185 | 186 | onMessage_(message) { 187 | const {id} = message; 188 | if (String(id).startsWith('_')) { 189 | this.addListener_(message); 190 | } else { 191 | this.callMethod_(message); 192 | } 193 | } 194 | 195 | onTabUpdate_(tab) { 196 | if (!tab || tab.id !== this.tabId_) { 197 | return; 198 | } 199 | const url = tab.url; 200 | const isDetailsPage = 201 | url && url.includes('chrome.google.com/webstore/detail/'); 202 | this.setButtonVisibility_(isDetailsPage); 203 | } 204 | 205 | onButtonClicked_(url) { 206 | const id = this.installer_.getIdFromUrl(url); 207 | if (!id) { 208 | return; 209 | } 210 | 211 | this.installer_.isInstalled(id).then(isInstalled => { 212 | this.setButtonVisibility_(false); 213 | if (isInstalled) { 214 | this.installer_.navigateToExtensionDetails(id); 215 | return; 216 | } 217 | 218 | let onInstallRequest; 219 | new Promise((resolve, reject) => { 220 | onInstallRequest = extensionId => { 221 | if (id === extensionId) { 222 | resolve(); 223 | } 224 | }; 225 | this.installer_.onInstallRequest.addListener(onInstallRequest); 226 | chrome.tabs.sendMessage( 227 | this.tabId_, this.constructor.ACTION_ADD_TO_OPERA); 228 | setTimeout(reject, 3000); 229 | }).then(() => { 230 | this.installer_.onInstallRequest.removeListener(onInstallRequest); 231 | }).catch(() => { 232 | this.installer_.onInstallRequest.removeListener(onInstallRequest); 233 | this.installer_.install({id}); 234 | }); 235 | }); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /content/callback_registry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class CallbackRegistry { 4 | constructor() { 5 | this.normalCallbacks_ = new Map(); 6 | this.normalLastID_ = 0; 7 | this.persistentCallbacks_ = new Map(); 8 | this.persistentlLastID_ = 0; 9 | } 10 | 11 | add(callback, isPersistent) { 12 | let id; 13 | if (isPersistent) { 14 | id = this.persistentlLastID_++ % 1000000; 15 | id = `_${id}`; 16 | this.persistentCallbacks_.set(id, data => callback(data)); 17 | } else { 18 | id = this.normalLastID_++ % 1000000; 19 | this.normalCallbacks_.set(id, data => callback(data)); 20 | } 21 | 22 | return id; 23 | } 24 | 25 | exec(id, data) { 26 | if (this.isPersistent(id)) { 27 | if (this.persistentCallbacks_.has(id)) { 28 | this.persistentCallbacks_.get(id)(data); 29 | } 30 | } else { 31 | if (this.normalCallbacks_.has(id)) { 32 | this.normalCallbacks_.get(id)(data); 33 | this.remove(id); 34 | } 35 | } 36 | } 37 | 38 | remove(id) { 39 | if (this.isPersistent(id)) { 40 | this.persistentCallbacks_.delete(id); 41 | } else { 42 | this.normalCallbacks_.delete(id); 43 | } 44 | } 45 | 46 | isPersistent(id) { return String(id).startsWith('_'); } 47 | } 48 | -------------------------------------------------------------------------------- /content/channel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Channel { 4 | constructor() { 5 | this.callbacks_ = new CallbackRegistry(); 6 | this.port_ = chrome.runtime.connect(); 7 | this.port_.onMessage.addListener(message => this.onMessage_(message)); 8 | } 9 | 10 | fetch() { return this.send('fetch', Array.from(arguments)); } 11 | 12 | forward(message, callback) { 13 | let id = message.id; 14 | let isPersistent = this.callbacks_.isPersistent(id); 15 | message.id = 16 | this.callbacks_.add(data => callback({id, data}), isPersistent); 17 | 18 | this.port_.postMessage(message); 19 | } 20 | 21 | send(type, data, isSync) { 22 | return new Promise(resolve => { 23 | let message = {}; 24 | message.data = data; 25 | message.type = type; 26 | message.isSync = isSync; 27 | message.id = this.callbacks_.add(data => resolve(data), false); 28 | this.port_.postMessage(message); 29 | }); 30 | } 31 | 32 | onMessage_(message) { this.callbacks_.exec(message.id, message.data); } 33 | } 34 | -------------------------------------------------------------------------------- /content/inject/page.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/chrome-webstore-extension/39dafce477ec2cbcfb5183929d02a81afba3d64b/content/inject/page.js -------------------------------------------------------------------------------- /content/inject/proxy_element.js: -------------------------------------------------------------------------------- 1 | class ProxyElement extends HTMLElement { 2 | static get ELEMENT_NAME() { return ID; } 3 | static get EVENT_API_DEF() { return 'api-def'; } 4 | static get EVENT_API_RESPONSE() { return 'api-response'; } 5 | static get EVENT_MESSAGE() { return 'message'; } 6 | createdCallback() { 7 | this.callbacks_ = new CallbackRegistry(); 8 | this.addEventListener( 9 | this.constructor.EVENT_API_DEF, e => this.build_(e.detail)); 10 | this.addEventListener( 11 | this.constructor.EVENT_API_RESPONSE, e => this.response_(e.detail)); 12 | } 13 | 14 | createEvent_(path) { 15 | let pid; 16 | return Event.get({ 17 | addListener: callback => { 18 | pid = this.callbacks_.add(data => { 19 | if (callback) { 20 | callback(...data); 21 | } 22 | }, true); 23 | 24 | this.dispatchEvent(new CustomEvent(this.constructor.EVENT_MESSAGE, { 25 | detail: { 26 | id: pid, 27 | add: `${path}.addListener`, 28 | remove: `${path}.removeListener`, 29 | }, 30 | })); 31 | }, 32 | 33 | removeListener: () => { 34 | if (pid) { 35 | this.callbacks_.remove(pid); 36 | } 37 | }, 38 | }); 39 | } 40 | 41 | createMethod_(path) { 42 | return function() { 43 | let callback = null; 44 | let data = Array.from(arguments); 45 | if (data[data.length - 1] instanceof Function) { 46 | callback = data.pop(); 47 | } 48 | 49 | let id = this.callbacks_.add(data => { 50 | if (callback) { 51 | callback(...data); 52 | } 53 | }, false); 54 | 55 | this.dispatchEvent(new CustomEvent(this.constructor.EVENT_MESSAGE, { 56 | detail: { 57 | type: path, 58 | data: data, 59 | isSync: !Boolean(callback), 60 | id: id, 61 | }, 62 | })); 63 | }.bind(this); 64 | } 65 | 66 | response_({id, data}) { 67 | if (id) { 68 | this.callbacks_.exec(id, data); 69 | } 70 | } 71 | 72 | build_(def) { 73 | let path = def.name.split('.'); 74 | let api = window; 75 | 76 | while (path.length) { 77 | let obj = path.shift(); 78 | api[obj] = api[obj] || {}; 79 | api = api[obj]; 80 | } 81 | 82 | for (let item of def.data) { 83 | if (item.type === Api.TYPE_OBJECT) { 84 | api[item.name] = item.value; 85 | } else if (item.type === Api.TYPE_FUNCTION) { 86 | api[item.name] = this.createMethod_(`${def.name}.${item.name}`); 87 | } else if (item.type === Api.TYPE_EVENT) { 88 | api[item.name] = this.createEvent_(`${def.name}.${item.name}`); 89 | } 90 | } 91 | } 92 | } 93 | 94 | document.registerElement(ProxyElement.ELEMENT_NAME, ProxyElement); 95 | -------------------------------------------------------------------------------- /content/inject/store_page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Page { 4 | static get ADD_BUTTON_CLASS_NAME() { return 'webstore-test-button-label'; } 5 | constructor() { 6 | this.adaptButtons_(); 7 | this.hideOpera_(); 8 | this.injectProxyElement_(); 9 | this.addApis_(); 10 | } 11 | 12 | adaptButtons_() { 13 | let observer = new MutationObserver(records => { 14 | records.forEach(record => { 15 | if (!record.addedNodes) { 16 | return; 17 | } 18 | 19 | Array.from(record.addedNodes).forEach(node => { 20 | if (!node.classList || !node.querySelectorAll) { 21 | return; 22 | } 23 | 24 | let items = []; 25 | 26 | if (node.classList && 27 | node.classList.contains(this.constructor.ADD_BUTTON_CLASS_NAME)) { 28 | items.push(node); 29 | } else if (node.querySelectorAll) { 30 | items = Array.from(node.querySelectorAll( 31 | `.${this.constructor.ADD_BUTTON_CLASS_NAME}`)); 32 | } 33 | 34 | items.forEach(element => { 35 | let root = element; 36 | let text = element.textContent; 37 | 38 | if (element.shadowRoot) { 39 | root = element.shadowRoot; 40 | text = root.textContent; 41 | } else { 42 | root = element.createShadowRoot(); 43 | } 44 | 45 | root.textContent = text.replace(/chrome/ig, 'Opera'); 46 | }); 47 | }); 48 | }); 49 | }); 50 | 51 | observer.observe( 52 | document.documentElement, {childList: true, subtree: true}); 53 | } 54 | 55 | addApis_() { 56 | Reflect.defineProperty(chrome, 'app', {value: App.get()}); 57 | } 58 | 59 | hideOpera_() { 60 | let userAgent = navigator.userAgent.split('OPR')[0].trim(); 61 | Reflect.defineProperty(navigator, 'userAgent', {value: userAgent}); 62 | let appVersion = navigator.appVersion.split('OPR')[0].trim(); 63 | Reflect.defineProperty(navigator, 'appVersion', {value: appVersion}); 64 | Reflect.deleteProperty(window, 'opr'); 65 | } 66 | 67 | injectProxyElement_() { 68 | document.documentElement.appendChild( 69 | document.createElement(ProxyElement.ELEMENT_NAME)); 70 | } 71 | } 72 | 73 | new Page(); 74 | -------------------------------------------------------------------------------- /content/proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Proxy { 4 | static get APIs() { return []; } 5 | 6 | static get EVENT_API_DEF() { return 'api-def'; } 7 | static get EVENT_API_RESPONSE() { return 'api-response'; } 8 | static get EVENT_MESSAGE() { return 'message'; } 9 | 10 | static get SCRIPTS() { 11 | return [ 12 | '/api/api.js', 13 | '/api/event.js', 14 | '/content/callback_registry.js', 15 | '/content/inject/proxy_element.js', 16 | ]; 17 | } 18 | 19 | constructor() { 20 | this.channel_ = new Channel(); 21 | this.id_ = `proxy-${Date.now()}`; 22 | this.proxy_ = null; 23 | 24 | Promise 25 | .all([ 26 | this.loadApis_(), 27 | this.loadScripts_().then(scripts => { 28 | let completed = this.watch_(); 29 | this.inject_(scripts); 30 | return completed; 31 | }), 32 | ]) 33 | .then(result => this.initialize_(result[0])); 34 | } 35 | 36 | inject_(scripts) { 37 | let script = document.createElement('script'); 38 | script.textContent = `'use strict'; 39 | (function() { 40 | let ID = '${this.id_}'; 41 | ${scripts.join('\n\n')} 42 | })();`; 43 | document.head.insertBefore(script, document.head.firstChild); 44 | script.remove(); 45 | } 46 | 47 | loadScripts_() { 48 | return Promise.all(this.constructor.SCRIPTS.map( 49 | url => this.channel_.fetch(url).then(data => data[0]))); 50 | } 51 | 52 | loadApis_() { 53 | return Promise.all(this.constructor.APIs.map( 54 | name => this.channel_.send('getDefinition', [name]) 55 | .then(data => ({name: name, data: data[0]})))); 56 | } 57 | 58 | watch_() { 59 | return new Promise(resolve => { 60 | this.proxy_ = null; 61 | let observer = new MutationObserver(records => { 62 | if (this.proxy_) { 63 | return; 64 | } 65 | this.proxy_ = document.querySelector(this.id_); 66 | if (this.proxy_) { 67 | observer.disconnect(); 68 | this.proxy_.remove(); 69 | resolve(); 70 | } 71 | }); 72 | 73 | observer.observe(document.documentElement, {childList: true}); 74 | }); 75 | } 76 | 77 | initialize_(apis) { 78 | this.proxy_.addEventListener(this.constructor.EVENT_MESSAGE, e => { 79 | this.channel_.forward(e.detail, message => { 80 | let event = new CustomEvent( 81 | this.constructor.EVENT_API_RESPONSE, {detail: message}); 82 | this.proxy_.dispatchEvent(event); 83 | }); 84 | }); 85 | apis.forEach(api => { 86 | let event = 87 | new CustomEvent(this.constructor.EVENT_API_DEF, {detail: api}); 88 | this.proxy_.dispatchEvent(event); 89 | }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /content/store.css: -------------------------------------------------------------------------------- 1 | div[role="button"], 2 | div[role="button"]:hover { 3 | border-color: #22a502 !important; 4 | border-radius: 2px !important; 5 | background-color: #26b700 !important; 6 | background-image: -webkit-linear-gradient(top,#28bd00 0%,#21a100 100%) !important; 7 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.15) !important; 8 | } 9 | 10 | div[role="button"]:hover { 11 | background-image: -webkit-linear-gradient(top,#21a100 0%,#1e9200 100%) !important; 12 | } 13 | 14 | div[role="button"]:active { 15 | box-shadow: none !important; 16 | } -------------------------------------------------------------------------------- /content/store_proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class StoreProxy extends Proxy { 4 | static get ACTION_ADD_TO_OPERA() { return 'add-to-opera'; } 5 | static get APIs() { 6 | return super.APIs.concat([ 7 | 'chrome.management', 8 | 'chrome.webstorePrivate', 9 | ]); 10 | } 11 | 12 | constructor() { 13 | super(); 14 | chrome.runtime.onMessage.addListener(action => this.onMessage_(action)); 15 | } 16 | 17 | static get SCRIPTS() { 18 | return super.SCRIPTS.concat([ 19 | '/api/app.js', 20 | '/content/inject/store_page.js', 21 | ]); 22 | } 23 | 24 | dispatchAddToOpera_() { 25 | try { 26 | const buttonLabel = document.querySelector( 27 | '[role=dialog] [role=button] .webstore-test-button-label'); 28 | const button = buttonLabel.closest('[role=button]'); 29 | button.click(); 30 | } catch (e) { 31 | // if any error then wait for fallback 32 | } 33 | } 34 | 35 | onMessage_(action) { 36 | if (action === this.constructor.ACTION_ADD_TO_OPERA) { 37 | this.dispatchAddToOpera_(); 38 | } 39 | } 40 | } 41 | 42 | new StoreProxy(); 43 | -------------------------------------------------------------------------------- /icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/chrome-webstore-extension/39dafce477ec2cbcfb5183929d02a81afba3d64b/icons/128.png -------------------------------------------------------------------------------- /icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/chrome-webstore-extension/39dafce477ec2cbcfb5183929d02a81afba3d64b/icons/16.png -------------------------------------------------------------------------------- /icons/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/chrome-webstore-extension/39dafce477ec2cbcfb5183929d02a81afba3d64b/icons/24.png -------------------------------------------------------------------------------- /icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/chrome-webstore-extension/39dafce477ec2cbcfb5183929d02a81afba3d64b/icons/32.png -------------------------------------------------------------------------------- /icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/operasoftware/chrome-webstore-extension/39dafce477ec2cbcfb5183929d02a81afba3d64b/icons/64.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "content_scripts": [ 3 | { 4 | "matches": [ 5 | "http://chrome.google.com/webstore/*", 6 | "https://chrome.google.com/webstore/*" 7 | ], 8 | "run_at": "document_start", 9 | "css": ["content/store.css"], 10 | "js": [ 11 | "content/callback_registry.js", 12 | "content/channel.js", 13 | "content/proxy.js", 14 | "content/store_proxy.js" 15 | ] 16 | } 17 | ], 18 | "name": "__MSG_name__", 19 | "default_locale": "en", 20 | "icons": { 21 | "128": "icons/128.png", 22 | "64": "icons/64.png" 23 | }, 24 | "description": "__MSG_description__", 25 | "background": { 26 | "persistent": false, 27 | "scripts": [ 28 | "/api/api.js", 29 | "/api/installer.js", 30 | "/api/event.js", 31 | "/api/management.js", 32 | "/api/webstore.js", 33 | "/api/webstore_private.js", 34 | "/background/content_proxy.js", 35 | "/background/app.js" 36 | ] 37 | }, 38 | "version": "2.5.8", 39 | "manifest_version": 2, 40 | "page_action": { 41 | "default_icon": { 42 | "16": "icons/16.png", 43 | "24": "icons/24.png", 44 | "32": "icons/32.png" 45 | }, 46 | "default_title": "__MSG_buttonTitle__" 47 | }, 48 | "permissions": [ 49 | "tabs", 50 | "management", 51 | "downloads", 52 | "https://clients2.google.com/service/update2/*", 53 | "*://chrome.google.com/webstore/*" 54 | ] 55 | } 56 | --------------------------------------------------------------------------------