├── .github └── ISSUE_TEMPLATE │ ├── 01_bug_report.yml │ ├── 02_feature_request.yml │ └── config.yml ├── .gitignore ├── .gitlab-ci.yml ├── .gitmodules ├── Authors.md ├── Changelog.md ├── Contributing.md ├── LICENSE ├── Readme.md ├── firefox └── approval_notes.md ├── package-lock.json ├── package.json ├── scripts ├── firefox-amo-metadata.mjs ├── message-description-sync.mjs └── ms-edge-publish.mjs ├── src ├── fonts │ ├── Lato │ │ ├── Lato-Light.woff2 │ │ └── Lato-Medium.woff2 │ └── OpenDyslexic │ │ ├── OpenDyslexic-Bold.woff │ │ └── OpenDyslexic-Regular.woff ├── js │ ├── App │ │ ├── Background.js │ │ ├── Client.js │ │ ├── Options.js │ │ ├── Passlink.js │ │ ├── Popup.js │ │ └── Preview.js │ ├── Client │ │ └── PasswordPaste │ │ │ ├── AbstractPasswordPaste.js │ │ │ ├── AliExpressPasswordPaste.js │ │ │ ├── BlueskyPasswordPaste.js │ │ │ ├── GenericPasswordPaste.js │ │ │ ├── GrowattPasswordPaste.js │ │ │ ├── RedditPasswordPaste.js │ │ │ ├── TumblrPasswordPaste.js │ │ │ └── TwitterPasswordPaste.js │ ├── Controller │ │ ├── AbstractController.js │ │ ├── Client │ │ │ ├── Debug │ │ │ │ └── ShowFields.js │ │ │ └── FillPassword.js │ │ ├── Clipboard │ │ │ └── WriteClipboard.js │ │ ├── Folder │ │ │ ├── List.js │ │ │ └── Show.js │ │ ├── Options │ │ │ ├── Debug │ │ │ │ ├── ClearLog.js │ │ │ │ ├── ClearStatistics.js │ │ │ │ ├── ExtensionInfo.js │ │ │ │ ├── FetchLog.js │ │ │ │ └── Reset.js │ │ │ └── Status.js │ │ ├── PassLink │ │ │ ├── Action.js │ │ │ ├── Analyze.js │ │ │ ├── Connect │ │ │ │ ├── Apply.js │ │ │ │ ├── Codes.js │ │ │ │ └── Theme.js │ │ │ ├── Open.js │ │ │ └── Status.js │ │ ├── Password │ │ │ ├── AddBlank.js │ │ │ ├── Delete.js │ │ │ ├── Favicon.js │ │ │ ├── Fill.js │ │ │ ├── Generate.js │ │ │ ├── Mine.js │ │ │ ├── Related.js │ │ │ ├── Search.js │ │ │ └── Update.js │ │ ├── Popup │ │ │ ├── DebugLoginForms.js │ │ │ ├── FirstRun │ │ │ │ ├── GetSettings.js │ │ │ │ ├── OpenGuide.js │ │ │ │ └── SaveSettings.js │ │ │ ├── OpenSettings.js │ │ │ ├── Status │ │ │ │ ├── Get.js │ │ │ │ └── Set.js │ │ │ └── UpdateMiningItem.js │ │ ├── Server │ │ │ ├── Create.js │ │ │ ├── Delete.js │ │ │ ├── Info.js │ │ │ ├── List.js │ │ │ ├── Reload.js │ │ │ ├── Theme.js │ │ │ └── Update.js │ │ ├── Setting │ │ │ ├── Get.js │ │ │ ├── Reset.js │ │ │ └── Set.js │ │ ├── Tab │ │ │ ├── Close.js │ │ │ ├── Create.js │ │ │ └── Popout.js │ │ ├── Theme │ │ │ ├── Delete.js │ │ │ ├── Import.js │ │ │ ├── List.js │ │ │ ├── Save.js │ │ │ └── Show.js │ │ └── Token │ │ │ └── Request.js │ ├── Converter │ │ ├── FolderConverter.js │ │ ├── PasswordConverter.js │ │ ├── PasteRequestConverter.js │ │ ├── ServerConverter.js │ │ └── ThemeConverter.js │ ├── Database │ │ ├── Database.js │ │ ├── MemoryDatabase.js │ │ ├── MemoryTable.js │ │ └── Table.js │ ├── Definition │ │ ├── Server.json │ │ ├── Theme.json │ │ └── Toast.json │ ├── Event │ │ ├── EventQueue.js │ │ └── Events.js │ ├── Exception │ │ ├── ClientNotAuthorizedError.js │ │ ├── CouldNotOpenIndexedDbError.js │ │ ├── ServerDisabledError.js │ │ ├── ServerNotFoundError.js │ │ └── UnknownItemTypeError.js │ ├── Helper │ │ ├── AutofillRequestHelper.js │ │ ├── BlobToBase64Helper.js │ │ ├── ConnectionErrorHelper.js │ │ ├── HashingHelper.js │ │ ├── HiddenFolderHelper.js │ │ ├── ServerRequirementCheck.js │ │ ├── ServerThemeHelper.js │ │ ├── SessionAuthorizationHelper.js │ │ ├── ThemeCssVarsHelper.js │ │ └── UuidHelper.js │ ├── Manager │ │ ├── AutofillManager.js │ │ ├── BadgeManager.js │ │ ├── BasicAuthAutofillManager.js │ │ ├── ClientControllerManager.js │ │ ├── ClipboardManager.js │ │ ├── ContextMenuManager.js │ │ ├── ControllerManager.js │ │ ├── ConverterManager.js │ │ ├── ErrorManager.js │ │ ├── MigrationManager.js │ │ ├── MiningManager.js │ │ ├── PassLinkManager.js │ │ ├── PasswordSettingsManager.js │ │ ├── RecommendationManager.js │ │ ├── SearchManager.js │ │ ├── ServerManager.js │ │ ├── ServerTimeoutManager.js │ │ └── TabManager.js │ ├── Migrations │ │ ├── Migration20000.js │ │ ├── Migration20001.js │ │ ├── Migration20002.js │ │ ├── Migration20003.js │ │ ├── Migration20004.js │ │ ├── Migration20005.js │ │ └── Migration20006.js │ ├── Miner │ │ └── DomMiner.js │ ├── Models │ │ ├── Client │ │ │ └── PasswordPasteRequest.js │ │ ├── Message │ │ │ └── Message.js │ │ ├── Notification │ │ │ ├── AbstractNotification.js │ │ │ ├── NewPasswordNotification.js │ │ │ └── UpdatePasswordNotification.js │ │ ├── Queue │ │ │ ├── AuthorisationItem.js │ │ │ ├── FeedbackItem.js │ │ │ ├── MiningItem.js │ │ │ └── QueueItem.js │ │ ├── Search │ │ │ ├── Index.js │ │ │ ├── IndexEntry.js │ │ │ └── Match.js │ │ ├── Server │ │ │ └── Server.js │ │ ├── Setting │ │ │ └── Setting.js │ │ ├── Theme │ │ │ └── Theme.js │ │ └── Toast │ │ │ └── Toast.js │ ├── Platform │ │ └── BrowserApi.js │ ├── Prototype │ │ └── prototype.js │ ├── Queue │ │ ├── Client │ │ │ ├── AuthorisationClient.js │ │ │ ├── FeedbackClient.js │ │ │ ├── MiningClient.js │ │ │ └── QueueClient.js │ │ ├── FeedbackQueue.js │ │ └── Queue.js │ ├── Repositories │ │ ├── ApiRepository.js │ │ ├── ServerRepository.js │ │ └── ThemeRepository.js │ ├── Schema │ │ ├── Theme.schema.json │ │ └── ThemeSchema.js │ ├── Search │ │ ├── Indexer │ │ │ ├── AbstractIndexer.js │ │ │ ├── FolderIndexer.js │ │ │ ├── PasswordIndexer.js │ │ │ └── TagIndexer.js │ │ └── Query │ │ │ ├── Boost │ │ │ ├── AbstractBoost.js │ │ │ └── MultiplyBoost.js │ │ │ ├── Builder │ │ │ └── QueryBuilder.js │ │ │ ├── Condition │ │ │ ├── AbstractCondition.js │ │ │ ├── AndCondition.js │ │ │ ├── OrCondition.js │ │ │ └── XorCondition.js │ │ │ ├── Field │ │ │ ├── AbstractField.js │ │ │ ├── FieldContains.js │ │ │ ├── FieldEquals.js │ │ │ ├── FieldGreater.js │ │ │ ├── FieldGreaterOrEquals.js │ │ │ ├── FieldIn.js │ │ │ ├── FieldMatches.js │ │ │ ├── FieldNotContains.js │ │ │ ├── FieldNotEquals.js │ │ │ ├── FieldNotIn.js │ │ │ ├── FieldNotMatches.js │ │ │ ├── FieldSmaller.js │ │ │ ├── FieldSmallerOrEquals.js │ │ │ └── FieldStartsWith.js │ │ │ ├── Pagination │ │ │ ├── AbstractPagination.js │ │ │ ├── Limit.js │ │ │ └── Pagination.js │ │ │ └── Sort │ │ │ ├── AbstractSort.js │ │ │ ├── CustomSort.js │ │ │ ├── SortAscending.js │ │ │ └── SortDescending.js │ ├── Services │ │ ├── DatabaseService.js │ │ ├── FaviconService.js │ │ ├── FormService.js │ │ ├── LocalisationService.js │ │ ├── MessageService.js │ │ ├── NotificationService.js │ │ ├── PasswordStatisticsService.js │ │ ├── PopupStateService.js │ │ ├── QueueService.js │ │ ├── RegistryService.js │ │ ├── SearchService.js │ │ ├── SettingsService.js │ │ ├── StorageService.js │ │ ├── SystemService.js │ │ ├── ThemeService.js │ │ └── ToastService.js │ ├── Settings │ │ ├── ClientSettingsProvider.js │ │ └── MasterSettingsProvider.js │ ├── Themes │ │ ├── AdaptaLight.json │ │ ├── AdaptaTeal.json │ │ ├── ArcDark.json │ │ ├── ArcLight.json │ │ ├── Dark.json │ │ ├── Hacker.json │ │ ├── Light.json │ │ ├── OledDark.json │ │ ├── RGB.json │ │ └── Server.json │ ├── Validation │ │ └── Server.js │ ├── background.js │ ├── client.js │ ├── options.js │ ├── passlink.js │ ├── popup.js │ └── preview.js ├── platform │ ├── chrome │ │ ├── js │ │ │ └── Platform │ │ │ │ └── BrowserApi.js │ │ ├── manifest.json │ │ └── scss │ │ │ └── browser.scss │ ├── fenix │ │ ├── manifest.json │ │ ├── scss │ │ │ └── browser.scss │ │ └── updates.json │ ├── firefox │ │ ├── manifest.json │ │ ├── scss │ │ │ └── browser.scss │ │ └── updates.json │ └── generic │ │ ├── _locales │ │ ├── ar │ │ │ └── messages.json │ │ ├── bg │ │ │ └── messages.json │ │ ├── cs │ │ │ └── messages.json │ │ ├── da │ │ │ └── messages.json │ │ ├── de │ │ │ └── messages.json │ │ ├── el │ │ │ └── messages.json │ │ ├── en │ │ │ └── messages.json │ │ ├── es │ │ │ └── messages.json │ │ ├── fi │ │ │ └── messages.json │ │ ├── fr │ │ │ └── messages.json │ │ ├── gl │ │ │ └── messages.json │ │ ├── hu │ │ │ └── messages.json │ │ ├── it │ │ │ └── messages.json │ │ ├── lt │ │ │ └── messages.json │ │ ├── nl │ │ │ └── messages.json │ │ ├── pl │ │ │ └── messages.json │ │ ├── pt │ │ │ └── messages.json │ │ ├── ru │ │ │ └── messages.json │ │ ├── sk │ │ │ └── messages.json │ │ ├── sl │ │ │ └── messages.json │ │ ├── sv │ │ │ └── messages.json │ │ ├── tr │ │ │ └── messages.json │ │ ├── uk │ │ │ └── messages.json │ │ └── zh_CN │ │ │ └── messages.json │ │ ├── css │ │ └── themes │ │ │ ├── hacker.css │ │ │ ├── rgb.css │ │ │ └── server.css │ │ ├── html │ │ ├── build.html │ │ ├── options.html │ │ ├── passlink.html │ │ ├── popup.html │ │ └── preview.html │ │ └── img │ │ ├── angle-down-solid.svg │ │ ├── background.png │ │ ├── passwords-128.png │ │ ├── passwords-16.png │ │ ├── passwords-32.png │ │ ├── passwords-48.png │ │ ├── passwords-96.png │ │ ├── passwords-dark.png │ │ ├── passwords-dark.svg │ │ ├── passwords-light.png │ │ ├── passwords-light.svg │ │ ├── passwords-new-dark.png │ │ ├── passwords-new-dark.svg │ │ ├── passwords-new-light.png │ │ ├── passwords-new-light.svg │ │ ├── passwords-new.png │ │ ├── passwords-new.svg │ │ ├── passwords-outline-dark.png │ │ ├── passwords-outline-dark.svg │ │ ├── passwords-outline-light.png │ │ ├── passwords-outline-light.svg │ │ ├── passwords.png │ │ └── passwords.svg ├── scss │ ├── _base.scss │ ├── _fonts.scss │ ├── _theme.scss │ └── includes.scss └── vue │ ├── App │ ├── Options.vue │ ├── Passlink.vue │ └── Popup.vue │ └── Components │ ├── Accounts │ ├── Account.vue │ ├── AccountList.vue │ └── NewAccount.vue │ ├── Browse │ ├── ServerBrowser.vue │ ├── ServerInfo.vue │ ├── ServerProperty.vue │ └── ServerUrlProperty.vue │ ├── Demo │ └── DemoMenu.vue │ ├── Firstrun │ ├── FirstRunWizard.vue │ └── Steps │ │ ├── CommonSettings.vue │ │ ├── ServerSetup.vue │ │ └── SetupComplete.vue │ ├── Foldout.vue │ ├── Form │ ├── ButtonField.vue │ ├── InputField.vue │ ├── SelectField.vue │ └── SliderField.vue │ ├── Icon.vue │ ├── List │ ├── FolderList.vue │ ├── Item │ │ ├── Favicon.vue │ │ ├── Folder.vue │ │ ├── Menu │ │ │ └── PasswordMenu.vue │ │ ├── ParentFolder.vue │ │ └── Password.vue │ └── PasswordList.vue │ ├── Options │ ├── Accounts.vue │ ├── Debug.vue │ ├── Setting │ │ └── HelpText.vue │ ├── Settings.vue │ └── Theming.vue │ ├── Passlink │ └── Action │ │ ├── Connect.vue │ │ ├── Connect │ │ ├── Codes.vue │ │ └── Result.vue │ │ ├── Error.vue │ │ └── ScanQr.vue │ ├── Password │ ├── CustomProperty.vue │ ├── Mining.vue │ ├── Property.vue │ └── View.vue │ ├── Popup │ ├── Authorisation.vue │ ├── Browse.vue │ ├── Collected.vue │ ├── Related.vue │ ├── Search.vue │ └── Tools.vue │ ├── Tabs.vue │ ├── Theming │ ├── BadgeIcon.vue │ ├── CustomColorInherit.vue │ ├── CustomColorSet.vue │ ├── CustomColorToast.vue │ ├── CustomFontFamily.vue │ ├── CustomFontSize.vue │ ├── CustomTheme.vue │ ├── PreviewTheme.vue │ └── ThemeOptions.vue │ ├── Toast │ └── Toast.vue │ ├── Toasts.vue │ ├── Tools │ ├── Debug.vue │ └── Generate.vue │ └── Translate.vue └── webpack.config.js /.github/ISSUE_TEMPLATE/02_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: "Feature Request" 2 | description: "Suggest a change or something new" 3 | title: "[FEATURE]: " 4 | labels: ["feature"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | ### 👍 Thank you for contributing to our project! 10 | Please note this is a **free and open-source** project. Most people take on their own time to review new feature requests, so please, be patient. 11 | - type: checkboxes 12 | id: before-posting 13 | attributes: 14 | label: "⚠️ This issue respects the following points: ⚠️" 15 | description: All conditions are **required**. Your issue can be closed if these are checked incorrectly. 16 | options: 17 | - label: This is a **feature request** for _one_ feature, not a question, discussion or multiple features. 18 | required: true 19 | - label: This is not a feature request for the [Nextcloud App](https://github.com/marius-wieschollek/passwords) or another client. 20 | required: true 21 | - label: This feature is **not** already requested on Github _(I've searched it)_. 22 | required: true 23 | - label: This feature does not already exist _(I checked the [wiki](https://git.mdns.eu/nextcloud/passwords/-/wikis/Users/Browser-Extension))_. 24 | required: true 25 | - type: textarea 26 | id: current-status 27 | attributes: 28 | label: Current Status 29 | description: | 30 | Describe the current situation and resulting problems. 31 | validations: 32 | required: true 33 | - type: textarea 34 | id: feature-description 35 | attributes: 36 | label: Feature Description 37 | description: | 38 | Describe your feature and how it should work. 39 | Be as detailed as possible and try to give a step-by-step example of how the feature is used and what happens when. 40 | placeholder: | 41 | 1. I open the app 42 | 2. I do X 43 | 3. Now Y happens 44 | validations: 45 | required: true 46 | - type: textarea 47 | id: additional-context 48 | attributes: 49 | label: Additional Context 50 | description: | 51 | Add any other context or screenshots about the feature request here. 52 | validations: 53 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community Forum 4 | url: https://help.nextcloud.com/c/apps/passwords 5 | about: Get support with problems you have 6 | - name: Telegram Chat 7 | url: https://t.me/nc_passwords 8 | about: Ask questions and discuss with other users 9 | - name: User Handbook 10 | url: https://git.mdns.eu/nextcloud/passwords/-/wikis/Users/Browser-Extension 11 | about: Learn how to use the extension and all the features 12 | - name: Official Nextcloud App Bugtracker 13 | url: https://github.com/marius-wieschollek/passwords/issues 14 | about: Report issues or request a feature for the Nextcloud app -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | build 4 | dist 5 | metadata.json 6 | source.zip -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule ".node_modules/passwords-client"] 2 | path = .node_modules/passwords-client 3 | url = https://git.mdns.eu/nextcloud/passwords-client.git 4 | -------------------------------------------------------------------------------- /Authors.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | - Marius Wieschollek 3 | 4 | ## Contributors 5 | - [Pafcioooo](https://github.com/pafcioooo) 6 | - [Riscue](https://github.com/Riscue) 7 | - [Edoardo Codeglia](https://github.com/edocod1) 8 | -------------------------------------------------------------------------------- /firefox/approval_notes.md: -------------------------------------------------------------------------------- 1 | ## Test server 2 | A test server is provided at [test.passwordsapp.org](https://test.passwordsapp.org/) with the login data user "firefox" and password "PasswordsApp". 3 | 4 | ## Build from repository 5 | Instructions to build this version directly from the repository can be found in the file html/build.html 6 | or in the extension settings under "Debug" -> "Build instructions and source code download". 7 | 8 | ## Build from source 9 | To build this version from the supplied source, run the following commands: 10 | 11 | ```bash 12 | npm ci 13 | npm run build:firefox 14 | rm ./build/updates.json 15 | ``` 16 | 17 | After this, the files in the "build" folder should match the uploaded extension. 18 | The manifest.json file is reformatted by the AMO release process which can cause differences in spacing in the file. -------------------------------------------------------------------------------- /scripts/firefox-amo-metadata.mjs: -------------------------------------------------------------------------------- 1 | import packageJson from '../package.json' with {type: 'json'}; 2 | import parseChangelog from "changelog-parser"; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | async function getChangelog() { 7 | let changelog = await parseChangelog('./Changelog.md'), 8 | version = packageJson.version, 9 | unreleasedText = ''; 10 | 11 | for(let item of changelog.versions) { 12 | if(item.version === version) { 13 | return item.body; 14 | } 15 | if(!item.version && !unreleasedText) { 16 | unreleasedText = item.body; 17 | } 18 | } 19 | 20 | return unreleasedText; 21 | } 22 | 23 | function getApprovalNotes() { 24 | let approvalNotesText = path.resolve(`firefox/approval_notes.md`); 25 | 26 | return fs.readFileSync(approvalNotesText, {encoding: 'utf8', flag: 'r'}); 27 | } 28 | 29 | async function main() { 30 | let changelog = await getChangelog(), 31 | approval_notes = getApprovalNotes(), 32 | release_notes = {"en-US": changelog}; 33 | 34 | let filePath = path.resolve(`metadata.json`), 35 | data = {version: {release_notes, approval_notes}}; 36 | fs.writeFileSync(filePath, JSON.stringify(data)); 37 | } 38 | 39 | await main(); -------------------------------------------------------------------------------- /scripts/ms-edge-publish.mjs: -------------------------------------------------------------------------------- 1 | import {EdgeAddonsAPI} from '@plasmohq/edge-addons-api'; 2 | import json from '../dist/chrome-extension/manifest.json' with {type: 'json'}; 3 | 4 | if(!process.env.EDGE_PRODUCT_ID || !process.env.EDGE_CLIENT_ID || !process.env.EDGE_API_KEY) { 5 | throw new Error('Could not find environment variables'); 6 | } 7 | 8 | const client = new EdgeAddonsAPI( 9 | { 10 | productId : process.env.EDGE_PRODUCT_ID, 11 | clientId : process.env.EDGE_CLIENT_ID, 12 | apiKey : process.env.EDGE_API_KEY, 13 | } 14 | ); 15 | 16 | let response = await client.submit( 17 | { 18 | filePath: './edge-extension.zip', 19 | notes : `Passwords for Nextcloud Browser Extension ${json.version}` 20 | } 21 | ); 22 | 23 | console.log('Update pushed', response); -------------------------------------------------------------------------------- /src/fonts/Lato/Lato-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/fonts/Lato/Lato-Light.woff2 -------------------------------------------------------------------------------- /src/fonts/Lato/Lato-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/fonts/Lato/Lato-Medium.woff2 -------------------------------------------------------------------------------- /src/fonts/OpenDyslexic/OpenDyslexic-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/fonts/OpenDyslexic/OpenDyslexic-Bold.woff -------------------------------------------------------------------------------- /src/fonts/OpenDyslexic/OpenDyslexic-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/fonts/OpenDyslexic/OpenDyslexic-Regular.woff -------------------------------------------------------------------------------- /src/js/App/Client.js: -------------------------------------------------------------------------------- 1 | import SystemService from '@js/Services/SystemService'; 2 | import ErrorManager from '@js/Manager/ErrorManager'; 3 | import MessageService from '@js/Services/MessageService'; 4 | import ConverterManager from '@js/Manager/ConverterManager'; 5 | import ClientControllerManager from '@js/Manager/ClientControllerManager'; 6 | import DomMiner from '@js/Miner/DomMiner'; 7 | 8 | class Client { 9 | async init() { 10 | SystemService.setArea('client'); 11 | ErrorManager.init(); 12 | try { 13 | await SystemService.waitReady(); 14 | SystemService.connect(); 15 | await MessageService.init(true, 'background'); 16 | ConverterManager.init(); 17 | ClientControllerManager.init(); 18 | let miner = new DomMiner(); 19 | miner.init(); 20 | this._initAutofill(); 21 | } catch(e) { 22 | ErrorManager.logError(e); 23 | } 24 | } 25 | 26 | 27 | /** 28 | * 29 | */ 30 | _initAutofill() { 31 | if(document.readyState === "complete" 32 | || document.readyState === "loaded" 33 | || document.readyState === "interactive") { 34 | this._sendAutofillReadyMessage(); 35 | } else { 36 | window.addEventListener( 37 | 'DOMContentLoaded', 38 | () => { 39 | this._sendAutofillReadyMessage(); 40 | } 41 | ); 42 | } 43 | } 44 | 45 | /** 46 | * 47 | */ 48 | _sendAutofillReadyMessage() { 49 | MessageService.send( 50 | { 51 | type : 'autofill.page.ready', 52 | payload : { 53 | url: window.location.href 54 | }, 55 | receiver: 'background' 56 | } 57 | ); 58 | } 59 | } 60 | 61 | export default new Client(); -------------------------------------------------------------------------------- /src/js/Client/PasswordPaste/AliExpressPasswordPaste.js: -------------------------------------------------------------------------------- 1 | import AbstractPasswordPaste from "@js/Client/PasswordPaste/AbstractPasswordPaste"; 2 | 3 | export default class AliExpressPasswordPaste extends AbstractPasswordPaste { 4 | 5 | canHandle() { 6 | return location.origin.endsWith('aliexpress.com'); 7 | } 8 | 9 | async handle() { 10 | let {userField, passwordField} = this._getLoginFields(); 11 | 12 | if(!userField || !passwordField) { 13 | if(this._passwordPasteRequest.isAutofill() || !(await this._openLoginForm())) { 14 | return false; 15 | } 16 | let fields = this._getLoginFields(); 17 | userField = fields.userField; 18 | passwordField = fields.passwordField; 19 | } 20 | 21 | this._insertTextIntoField(userField, this._passwordPasteRequest.getUser()); 22 | this._insertTextIntoField(passwordField, this._passwordPasteRequest.getPassword()); 23 | 24 | if(!this._passwordPasteRequest.isAutofill() && this._passwordPasteRequest.isSubmit()) { 25 | let submitButton = document.querySelector('button.login-submit[type="submit"]'); 26 | if(submitButton) { 27 | this._simulateClick(submitButton); 28 | } 29 | } 30 | 31 | return true; 32 | } 33 | 34 | _getLoginFields() { 35 | let userField = document.getElementById('fm-login-id'), 36 | passwordField = document.getElementById('fm-login-password'); 37 | return {userField, passwordField}; 38 | } 39 | 40 | async _openLoginForm() { 41 | try { 42 | let loginMenu = await this._waitForElement('div[class^="my-account--menuItem"]'); 43 | this._simulateMouseMove(loginMenu); 44 | } catch(e) { 45 | return false; 46 | } 47 | 48 | let loginButton; 49 | try { 50 | loginButton = await this._waitForElement('button[class^="my-account--signin"]'); 51 | } catch(e) { 52 | return false; 53 | } 54 | 55 | this._simulateClick(loginButton); 56 | try { 57 | await this._waitForElement('#fm-login-id'); 58 | return true; 59 | } catch(e) { 60 | return false; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/js/Client/PasswordPaste/GenericPasswordPaste.js: -------------------------------------------------------------------------------- 1 | import AbstractPasswordPaste from "@js/Client/PasswordPaste/AbstractPasswordPaste"; 2 | import FormService from "@js/Services/FormService"; 3 | 4 | export default class GenericPasswordPaste extends AbstractPasswordPaste { 5 | 6 | canHandle() { 7 | let forms = new FormService().getLoginFields(); 8 | return forms.length !== 0; 9 | } 10 | 11 | async handle() { 12 | let forms = new FormService().getLoginFields(); 13 | if (forms.length === 0) return false; 14 | 15 | this._fillFormFields(this._passwordPasteRequest.getFormFields()); 16 | 17 | for (let i = 0; i < forms.length; i++) { 18 | let form = forms[i]; 19 | if (this._passwordPasteRequest.isAutofill() && form.pass.value.length !== 0) { 20 | continue; 21 | } 22 | 23 | if (form.user) this._insertTextIntoField(form.user, this._passwordPasteRequest.getUser()); 24 | this._insertTextIntoField(form.pass, this._passwordPasteRequest.getPassword()); 25 | 26 | if (!this._passwordPasteRequest.isSubmit() || forms.length !== 1) continue; 27 | if (form.submit) { 28 | this._simulateClick(form.submit); 29 | } else if (form.secure) { 30 | this._simulateEnter(form.pass); 31 | } 32 | } 33 | 34 | return true; 35 | } 36 | } -------------------------------------------------------------------------------- /src/js/Client/PasswordPaste/GrowattPasswordPaste.js: -------------------------------------------------------------------------------- 1 | import AbstractPasswordPaste from "@js/Client/PasswordPaste/AbstractPasswordPaste"; 2 | 3 | export default class GrowattPasswordPaste extends AbstractPasswordPaste { 4 | 5 | canHandle() { 6 | return location.origin === 'https://server.growatt.com'; 7 | } 8 | 9 | async handle() { 10 | let userField = document.querySelector('input#val_loginAccount'); 11 | if(!userField) { 12 | return false; 13 | } 14 | 15 | this._insertTextIntoField(userField, this._passwordPasteRequest.getUser()); 16 | let passwordField = await this._waitForElement('input#val_loginPwd'); 17 | this._insertTextIntoField(passwordField, this._passwordPasteRequest.getPassword()); 18 | 19 | if(this._passwordPasteRequest.isSubmit()) { 20 | this._simulateEnter(passwordField); 21 | } 22 | 23 | return true; 24 | } 25 | } -------------------------------------------------------------------------------- /src/js/Client/PasswordPaste/RedditPasswordPaste.js: -------------------------------------------------------------------------------- 1 | import AbstractPasswordPaste from "@js/Client/PasswordPaste/AbstractPasswordPaste"; 2 | 3 | export default class RedditPasswordPaste extends AbstractPasswordPaste { 4 | 5 | #loginUsernameSelector = '#login-username'; 6 | #loginPasswordSelector = '#login-password'; 7 | #loginButtonSelector = '#login-button'; 8 | 9 | canHandle() { 10 | return location.origin === 'https://www.reddit.com' && 11 | ( 12 | document.querySelector(this.#loginButtonSelector) !== null || 13 | document.querySelector(this.#loginUsernameSelector) !== null 14 | ); 15 | } 16 | 17 | async handle() { 18 | let userInput = document.querySelector(this.#loginUsernameSelector); 19 | if(userInput === null) { 20 | if(this._passwordPasteRequest.isAutofill()) { 21 | return false; 22 | } 23 | 24 | let loginButton = document.querySelector(this.#loginButtonSelector); 25 | if(loginButton === null) { 26 | return false; 27 | } 28 | this._simulateClick(loginButton); 29 | try { 30 | userInput = await this._waitForElement(this.#loginUsernameSelector, 10000); 31 | } catch(e) { 32 | return false; 33 | } 34 | } 35 | 36 | this._insertTextIntoField(userInput?.shadowRoot.querySelector('input'), this._passwordPasteRequest.getUser()); 37 | let passwordInput = document.querySelector(this.#loginPasswordSelector)?.shadowRoot.querySelector('input'); 38 | this._insertTextIntoField(passwordInput, this._passwordPasteRequest.getPassword()); 39 | if(!this._passwordPasteRequest.isAutofill() && this._passwordPasteRequest.isSubmit()) { 40 | await this._wait(500); 41 | // Something in reddits own code seems to reset the value of the field to a previous state in some cases 42 | this._insertTextIntoField(passwordInput, this._passwordPasteRequest.getPassword()); 43 | this._simulateEnter(passwordInput); 44 | } 45 | 46 | return true; 47 | } 48 | } -------------------------------------------------------------------------------- /src/js/Client/PasswordPaste/TumblrPasswordPaste.js: -------------------------------------------------------------------------------- 1 | import AbstractPasswordPaste from "@js/Client/PasswordPaste/AbstractPasswordPaste"; 2 | 3 | export default class TumblrPasswordPaste extends AbstractPasswordPaste { 4 | 5 | canHandle() { 6 | return location.origin === 'https://www.tumblr.com' && 7 | !this._passwordPasteRequest.isAutofill(); 8 | } 9 | 10 | async handle() { 11 | let emailField = document.querySelector('input[name="email"]'); 12 | if(!emailField) { 13 | let loginButton = document.querySelector('button.zn53i, button.CKAFB, [style="background-color: rgb(124, 92, 255); color: rgb(255, 255, 255);"] button:nth-child(2)'); 14 | this._simulateClick(loginButton); 15 | 16 | try { 17 | let continueButton = await this._waitForElement('button.dKGjO'); 18 | this._simulateClick(continueButton); 19 | emailField = await this._waitForElement('input[name="email"]'); 20 | } catch (e) { 21 | return false; 22 | } 23 | } 24 | 25 | this._insertTextIntoField(emailField, this._passwordPasteRequest.getUser()); 26 | this._simulateEnter(emailField); 27 | let passwordField = await this._waitForElement('input[name="password"]'); 28 | this._insertTextIntoField(passwordField, this._passwordPasteRequest.getPassword()); 29 | 30 | if(this._passwordPasteRequest.isSubmit()) { 31 | this._simulateEnter(passwordField); 32 | } 33 | 34 | return true; 35 | } 36 | } -------------------------------------------------------------------------------- /src/js/Controller/AbstractController.js: -------------------------------------------------------------------------------- 1 | export default class AbstractController { 2 | 3 | /** 4 | * 5 | * @param {Message} message 6 | * @param {Message} reply 7 | */ 8 | async execute(message, reply) { 9 | console.trace('controller.execute() not implemented') 10 | } 11 | } -------------------------------------------------------------------------------- /src/js/Controller/Clipboard/WriteClipboard.js: -------------------------------------------------------------------------------- 1 | import ClipboardManager from '@js/Manager/ClipboardManager'; 2 | import AbstractController from '@js/Controller/AbstractController'; 3 | import ErrorManager from "@js/Manager/ErrorManager"; 4 | 5 | export default class WriteClipboard extends AbstractController { 6 | 7 | /** 8 | * 9 | * @param {Message} message 10 | * @param {Message} reply 11 | */ 12 | async execute(message, reply) { 13 | try { 14 | let result = ClipboardManager.write(message.getPayload().type, message.getPayload().value); 15 | 16 | if(result) reply.setPayload(true); 17 | } catch(e) { 18 | ErrorManager.logError(e) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/js/Controller/Folder/List.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import TabManager from "@js/Manager/TabManager"; 3 | import SearchService from "@js/Services/SearchService"; 4 | 5 | export default class List extends AbstractController { 6 | 7 | async execute(message, reply) { 8 | let payload = message.getPayload(); 9 | 10 | let query = SearchService 11 | .find(['password', 'folder']) 12 | .where('server', payload.server) 13 | .orWhere((qb) => { 14 | qb.andWhere( 15 | [ 16 | ['type', '=', 'folder'], 17 | ['parent', '=', payload.folder] 18 | ] 19 | ).andWhere( 20 | [ 21 | ['type', '=', 'password'], 22 | ['folder', '=', payload.folder] 23 | ] 24 | ); 25 | }) 26 | .sortBy('favorite') 27 | .sortBy('label', true) 28 | .transform((results) => { 29 | return results.reduce((items, model) => { 30 | items[`${model.MODEL_TYPE}s`].push(model); 31 | return items; 32 | }, {passwords: [], folders: []}); 33 | }); 34 | 35 | if(TabManager.get().tab.incognito) { 36 | query.withHidden(true); 37 | } 38 | 39 | let items = query.execute(); 40 | 41 | reply.setType('folder.items').setPayload(items); 42 | } 43 | } -------------------------------------------------------------------------------- /src/js/Controller/Folder/Show.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import {Folder} from 'passwords-client/models'; 3 | import SearchService from "@js/Services/SearchService"; 4 | 5 | export default class Show extends AbstractController { 6 | async execute(message, reply) { 7 | let folderId = message.getPayload(), 8 | folder; 9 | 10 | if(folderId !== '00000000-0000-0000-0000-000000000000') { 11 | folder = SearchService.get(message.getPayload()); 12 | } else { 13 | folder = new Folder({ 14 | label: 'Home', 15 | id: '00000000-0000-0000-0000-000000000000', 16 | parent: '00000000-0000-0000-0000-000000000000', 17 | revision: '00000000-0000-0000-0000-000000000000' 18 | }) 19 | } 20 | 21 | reply 22 | .setType('folder.item') 23 | .setPayload(folder); 24 | } 25 | } -------------------------------------------------------------------------------- /src/js/Controller/Options/Debug/ClearLog.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import ErrorManager from "@js/Manager/ErrorManager"; 3 | 4 | export default class FetchLog extends AbstractController { 5 | 6 | /** 7 | * 8 | * @param {Message} message 9 | * @param {Message} reply 10 | */ 11 | async execute(message, reply) { 12 | ErrorManager.clearLog(); 13 | reply.setPayload({success: true}); 14 | } 15 | } -------------------------------------------------------------------------------- /src/js/Controller/Options/Debug/ClearStatistics.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import SystemService from "@js/Services/SystemService"; 3 | import StorageService from "@js/Services/StorageService"; 4 | import DatabaseService from "@js/Services/DatabaseService"; 5 | import ToastService from "@js/Services/ToastService"; 6 | import ErrorManager from "@js/Manager/ErrorManager"; 7 | 8 | export default class Reset extends AbstractController { 9 | 10 | /** 11 | * 12 | * @param {Message} message 13 | * @param {Message} reply 14 | */ 15 | async execute(message, reply) { 16 | try { 17 | let database = await DatabaseService.get(DatabaseService.STATISTICS_DATABASE); 18 | await database.unload(); 19 | window.indexedDB.deleteDatabase(DatabaseService.STATISTICS_DATABASE); 20 | } catch(e) { 21 | ErrorManager.catch(e); 22 | } 23 | 24 | await SystemService.requestUpdateCheck(); 25 | SystemService.getBrowserApi().runtime.reload(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/js/Controller/Options/Debug/FetchLog.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import HiddenFolderHelper from "@js/Helper/HiddenFolderHelper"; 3 | import ErrorManager from "@js/Manager/ErrorManager"; 4 | import ServerManager from "@js/Manager/ServerManager"; 5 | import SettingsService from "@js/Services/SettingsService"; 6 | import SystemService from "@js/Services/SystemService"; 7 | 8 | export default class FetchLog extends AbstractController { 9 | 10 | /** 11 | * 12 | * @param {Message} message 13 | * @param {Message} reply 14 | */ 15 | async execute(message, reply) { 16 | let errors = []; 17 | 18 | for(let error of ErrorManager.errors) { 19 | errors.unshift(error); 20 | } 21 | 22 | reply 23 | .setType('debug.errors') 24 | .setPayload(errors); 25 | } 26 | } -------------------------------------------------------------------------------- /src/js/Controller/Options/Debug/Reset.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import SystemService from "@js/Services/SystemService"; 3 | import StorageService from "@js/Services/StorageService"; 4 | import DatabaseService from "@js/Services/DatabaseService"; 5 | import ErrorManager from "@js/Manager/ErrorManager"; 6 | 7 | export default class Reset extends AbstractController { 8 | 9 | /** 10 | * 11 | * @param {Message} message 12 | * @param {Message} reply 13 | */ 14 | async execute(message, reply) { 15 | 16 | try { 17 | let database = await DatabaseService.get(DatabaseService.STATISTICS_DATABASE); 18 | await database.unload(); 19 | window.indexedDB.deleteDatabase(DatabaseService.STATISTICS_DATABASE); 20 | } catch(e) { 21 | ErrorManager.catch(e); 22 | } 23 | 24 | StorageService.stop(); 25 | await SystemService.getBrowserApi().storage.local.clear(); 26 | await SystemService.getBrowserApi().storage.sync.clear(); 27 | await StorageService.init(); 28 | 29 | await SystemService.requestUpdateCheck() 30 | SystemService.getBrowserApi().runtime.reload(); 31 | 32 | reply 33 | .setType('success') 34 | .setPayload(true); 35 | } 36 | } -------------------------------------------------------------------------------- /src/js/Controller/Options/Status.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import SystemService from '@js/Services/SystemService'; 3 | 4 | export default class Status extends AbstractController { 5 | 6 | /** 7 | * 8 | * @param {Message} message 9 | * @param {Message} reply 10 | */ 11 | async execute(message, reply) { 12 | let info = await SystemService.getBrowserInfo(), 13 | status = {device: info.device, browser: info.name.replace(/\W+/g, '-').toLowerCase() }; 14 | 15 | reply 16 | .setType('options.data') 17 | .setPayload(status); 18 | } 19 | } -------------------------------------------------------------------------------- /src/js/Controller/PassLink/Action.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import {PassLink} from 'passwords-client'; 3 | import ErrorManager from '@js/Manager/ErrorManager'; 4 | import RegistryService from '@js/Services/RegistryService'; 5 | 6 | export default class Action extends AbstractController { 7 | 8 | /** 9 | * 10 | * @param {Message} message 11 | * @param {Message} reply 12 | */ 13 | async execute(message, reply) { 14 | let {action, data} = message.getPayload(); 15 | 16 | try { 17 | let handler = PassLink.getAction(action, data); 18 | 19 | if(!RegistryService.has(`passlink.action.${action}`) || RegistryService.get(`passlink.action.${action}`).getParameter('id') !== handler.getParameter('id')) { 20 | RegistryService.set(`passlink.action.${action}`, handler); 21 | } 22 | 23 | reply.setPayload({success: true}); 24 | } catch(e) { 25 | ErrorManager.logError(e); 26 | reply.setPayload({success: false, message: e.message}); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/js/Controller/PassLink/Analyze.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import {PassLink} from 'passwords-client'; 3 | import ErrorManager from '@js/Manager/ErrorManager'; 4 | 5 | export default class Analyze extends AbstractController { 6 | 7 | /** 8 | * 9 | * @param {Message} message 10 | * @param {Message} reply 11 | */ 12 | async execute(message, reply) { 13 | let link = message.getPayload(); 14 | 15 | reply.setType('passlink.link'); 16 | try { 17 | let info = PassLink.analyzeLink(link); 18 | reply.setPayload({action: info.action, data: info.parameters}); 19 | } catch(e) { 20 | reply.setPayload({action: 'error', data: {message: e.message, link}}); 21 | ErrorManager.logError(e); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/js/Controller/PassLink/Connect/Codes.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import RegistryService from '@js/Services/RegistryService'; 3 | 4 | export default class Codes extends AbstractController { 5 | 6 | /** 7 | * 8 | * @param {Message} message 9 | * @param {Message} reply 10 | */ 11 | async execute(message, reply) { 12 | if(!RegistryService.has('passlink.action.connect')) { 13 | reply.setPayload({success: false, message: 'PasslinkNoActiveAction'}); 14 | } 15 | 16 | /** @type Connect **/ 17 | let action = RegistryService.get('passlink.action.connect'); 18 | reply.setPayload({success: true, codes: action.getCodes()}); 19 | } 20 | } -------------------------------------------------------------------------------- /src/js/Controller/PassLink/Connect/Theme.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import ThemeCssVarsHelper from '@js/Helper/ThemeCssVarsHelper'; 3 | import RegistryService from '@js/Services/RegistryService'; 4 | 5 | export default class Theme extends AbstractController { 6 | 7 | /** 8 | * 9 | * @param {Message} message 10 | * @param {Message} reply 11 | */ 12 | async execute(message, reply) { 13 | if(!RegistryService.has('passlink.action.connect')) { 14 | reply.setPayload({success: false, message: 'PasslinkNoActiveAction'}); 15 | } 16 | 17 | /** @type Connect **/ 18 | let action = RegistryService.get('passlink.action.connect'), 19 | theme = await action.getTheme(), 20 | vars = ThemeCssVarsHelper.processTheme(theme); 21 | reply.setPayload({success: true, theme, vars}); 22 | } 23 | } -------------------------------------------------------------------------------- /src/js/Controller/PassLink/Open.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import SystemService from '@js/Services/SystemService'; 3 | 4 | export default class Open extends AbstractController{ 5 | 6 | /** 7 | * 8 | * @param {Message} message 9 | * @param {Message} reply 10 | */ 11 | async execute(message, reply) { 12 | let {action, data} = message.getPayload(); 13 | 14 | let url = await SystemService.getBrowserApi().runtime.getURL('html/passlink.html'); 15 | url += '?action=' + encodeURIComponent(action); 16 | if(data) url += '&data=' + encodeURIComponent(JSON.stringify(data)); 17 | 18 | SystemService.getBrowserApi().tabs.create({url}) 19 | } 20 | } -------------------------------------------------------------------------------- /src/js/Controller/PassLink/Status.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import SystemService from '@js/Services/SystemService'; 3 | 4 | export default class Status extends AbstractController { 5 | 6 | /** 7 | * 8 | * @param {Message} message 9 | * @param {Message} reply 10 | */ 11 | async execute(message, reply) { 12 | let info = await SystemService.getBrowserInfo(), 13 | status = {device: info.device}; 14 | 15 | reply 16 | .setType('passlink.data') 17 | .setPayload(status); 18 | } 19 | } -------------------------------------------------------------------------------- /src/js/Controller/Password/AddBlank.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import MiningManager from '@js/Manager/MiningManager'; 3 | import TabManager from "@js/Manager/TabManager"; 4 | import ErrorManager from "@js/Manager/ErrorManager"; 5 | 6 | export default class AddBlank extends AbstractController { 7 | async execute(message, reply) { 8 | let url = TabManager.get('url'), 9 | title = TabManager.get('tab').title; 10 | 11 | MiningManager 12 | .createItem({title, url, manual: true, user: {value: ''}, password: {value: ''}}) 13 | .catch(ErrorManager.catch); 14 | } 15 | } -------------------------------------------------------------------------------- /src/js/Controller/Password/Favicon.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import FaviconService from "@js/Services/FaviconService"; 3 | 4 | export default class Favicon extends AbstractController { 5 | 6 | async execute(message, reply) { 7 | let {password, size} = message.getPayload(); 8 | 9 | reply.setPayload(await FaviconService.getFaviconForPassword(password, size)) 10 | } 11 | } -------------------------------------------------------------------------------- /src/js/Controller/Password/Fill.js: -------------------------------------------------------------------------------- 1 | import MessageService from '@js/Services/MessageService'; 2 | import AbstractController from '@js/Controller/AbstractController'; 3 | import TabManager from '@js/Manager/TabManager'; 4 | import SettingsService from '@js/Services/SettingsService'; 5 | import ErrorManager from '@js/Manager/ErrorManager'; 6 | import Message from "@js/Models/Message/Message"; 7 | import PasswordStatisticsService from "@js/Services/PasswordStatisticsService"; 8 | import Url from "url-parse"; 9 | import SearchService from "@js/Services/SearchService"; 10 | import AutofillRequestHelper from "@js/Helper/AutofillRequestHelper"; 11 | 12 | export default class Fill extends AbstractController { 13 | 14 | /** 15 | * 16 | * @param {Message} message 17 | * @param {Message} reply 18 | */ 19 | async execute(message, reply) { 20 | /** @type {EnhancedPassword} **/ 21 | let password = SearchService.get(message.getPayload()); 22 | 23 | let ids = TabManager.get('autofill.ids', []); 24 | if(ids.indexOf(password.getId()) === -1) { 25 | ids.push(password.getId()); 26 | TabManager.set('autofill.ids', ids); 27 | } 28 | 29 | let url = Url(TabManager.get('url')); 30 | PasswordStatisticsService.registerPasswordUse(password.getId(), url.host); 31 | 32 | try { 33 | let response = await MessageService.send( 34 | { 35 | type : 'autofill.password', 36 | receiver: 'client', 37 | channel : 'tabs', 38 | tab : TabManager.currentTabId, 39 | silent : true, 40 | payload : await this._createPasteRequest(password) 41 | } 42 | ); 43 | 44 | let success = response instanceof Message ? response.getPayload() === true:false; 45 | reply.setPayload({success}); 46 | } catch(e) { 47 | ErrorManager.logError(e); 48 | reply.setPayload({success: false}); 49 | } 50 | } 51 | 52 | async _createPasteRequest(password) { 53 | return AutofillRequestHelper 54 | .createPasteRequest( 55 | password, 56 | await SettingsService.getValue('paste.form.submit') 57 | ); 58 | } 59 | } -------------------------------------------------------------------------------- /src/js/Controller/Password/Generate.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import ServerManager from '@js/Manager/ServerManager'; 3 | import ToastService from "@js/Services/ToastService"; 4 | import ErrorManager from "@js/Manager/ErrorManager"; 5 | 6 | export default class Generate extends AbstractController { 7 | 8 | /** 9 | * 10 | * @param {Message} message 11 | * @param {Message} reply 12 | */ 13 | async execute(message, reply) { 14 | let api = await ServerManager.getDefaultApi(), 15 | /** @type {PasswordService} **/ 16 | passwordService = api.getInstance('service.password'), 17 | params = message.getPayload(); 18 | 19 | try { 20 | let data = await passwordService.generate(params.numbers, params.special, params.strength); 21 | 22 | reply.setPayload({success: true, password: data.password, words: data.words}); 23 | } catch(e) { 24 | ErrorManager.logError(e); 25 | ToastService.error(['PasswordGenerateError', e.message]) 26 | .catch(ErrorManager.catch); 27 | reply.setPayload({success: false}); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/js/Controller/Password/Mine.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import MiningManager from '@js/Manager/MiningManager'; 3 | import ErrorManager from "@js/Manager/ErrorManager"; 4 | 5 | export default class Mine extends AbstractController { 6 | 7 | async execute(message, reply) { 8 | MiningManager 9 | .addPassword(message.getPayload()) 10 | .catch(ErrorManager.catch); 11 | } 12 | } -------------------------------------------------------------------------------- /src/js/Controller/Password/Related.js: -------------------------------------------------------------------------------- 1 | import RecommendationManager from '@js/Manager/RecommendationManager'; 2 | import AbstractController from '@js/Controller/AbstractController'; 3 | import TabManager from "@js/Manager/TabManager"; 4 | 5 | export default class Related extends AbstractController { 6 | 7 | /** 8 | * 9 | * @param {Message} message 10 | * @param {Message} reply 11 | */ 12 | async execute(message, reply) { 13 | let payload = { 14 | passwords: RecommendationManager.getSuggestions(), 15 | tab : TabManager.currentTabId 16 | }; 17 | 18 | reply.setType('password.suggestions') 19 | .setPayload(payload); 20 | } 21 | } -------------------------------------------------------------------------------- /src/js/Controller/Popup/DebugLoginForms.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import MessageService from "@js/Services/MessageService"; 3 | import TabManager from "@js/Manager/TabManager"; 4 | import ErrorManager from "@js/Manager/ErrorManager"; 5 | 6 | export default class DebugLoginForms extends AbstractController { 7 | async execute(message, reply) { 8 | MessageService.send({type: 'debug.form.fields', receiver: 'client', channel: 'tabs', tab: TabManager.currentTabId}) 9 | .catch(ErrorManager.catch); 10 | } 11 | } -------------------------------------------------------------------------------- /src/js/Controller/Popup/FirstRun/GetSettings.js: -------------------------------------------------------------------------------- 1 | import AbstractController from "@js/Controller/AbstractController"; 2 | import SettingsService from "@js/Services/SettingsService"; 3 | 4 | export default class GetSettings extends AbstractController { 5 | async execute(message, reply) { 6 | reply.setPayload( 7 | { 8 | autofill : await SettingsService.getValue('paste.autofill'), 9 | quicksave: await SettingsService.getValue('notification.password.quicksave'), 10 | incognito: await SettingsService.getValue('mining.incognito.hide') 11 | } 12 | ); 13 | } 14 | } -------------------------------------------------------------------------------- /src/js/Controller/Popup/FirstRun/OpenGuide.js: -------------------------------------------------------------------------------- 1 | import AbstractController from "@js/Controller/AbstractController"; 2 | import SystemService from "@js/Services/SystemService"; 3 | import ErrorManager from "@js/Manager/ErrorManager"; 4 | 5 | export default class OpenGuide extends AbstractController { 6 | 7 | /** 8 | * 9 | * @param {Message} message 10 | * @param {Message} reply 11 | */ 12 | async execute(message, reply) { 13 | let api = SystemService.getBrowserApi(), 14 | url = await this._getGuideUrl(); 15 | 16 | try { 17 | let payload = message.getPayload(); 18 | await api.tabs.create({url}); 19 | reply.setPayload({success: true}); 20 | } catch(e) { 21 | ErrorManager.logError(e); 22 | reply.setPayload({success: false}); 23 | } 24 | } 25 | 26 | async _getGuideUrl() { 27 | let browser = await SystemService.getBrowserInfo(); 28 | 29 | if(browser.name === 'Firefox') { 30 | return 'https://git.mdns.eu/nextcloud/passwords/-/wikis/Users/Browser-Extension/Setup/Setup-with-Firefox'; 31 | } 32 | if(browser.name === 'Chrome') { 33 | return 'https://git.mdns.eu/nextcloud/passwords/-/wikis/Users/Browser-Extension/Setup/Setup-with-Chrome'; 34 | } 35 | if(browser.name === 'Edge') { 36 | return 'https://git.mdns.eu/nextcloud/passwords/-/wikis/Users/Browser-Extension/Setup/Setup-with-Edge'; 37 | } 38 | 39 | return 'https://git.mdns.eu/nextcloud/passwords/-/wikis/Users/Browser-Extension#setup-guides'; 40 | } 41 | } -------------------------------------------------------------------------------- /src/js/Controller/Popup/FirstRun/SaveSettings.js: -------------------------------------------------------------------------------- 1 | import AbstractController from "@js/Controller/AbstractController"; 2 | import SettingsService from "@js/Services/SettingsService"; 3 | 4 | export default class GetSettings extends AbstractController { 5 | async execute(message, reply) { 6 | let payload = message.getPayload(); 7 | 8 | await SettingsService.set('paste.autofill', payload?.autofill === true); 9 | await SettingsService.set('notification.password.quicksave', payload?.quicksave === true); 10 | await SettingsService.set('mining.incognito.hide', payload?.incognito === true); 11 | await SettingsService.set('password.folder.private', 'd1145b70-a731-42da-b3b1-163c3113751f'); 12 | await SettingsService.set('setup.initialized', true); 13 | reply.setType('success'); 14 | } 15 | } -------------------------------------------------------------------------------- /src/js/Controller/Popup/OpenSettings.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import SystemService from "@js/Services/SystemService"; 3 | import ErrorManager from "@js/Manager/ErrorManager"; 4 | 5 | export default class OpenSettings extends AbstractController { 6 | 7 | async execute(message, reply) { 8 | let info = await SystemService.getBrowserInfo(), 9 | api = SystemService.getBrowserApi(); 10 | 11 | if(info.name === 'Kiwi' || SystemService.isCompatible(SystemService.PLATFORM_FENIX)) { 12 | let url = api.runtime.getURL('html/options.html') + '?newtab'; 13 | api.tabs 14 | .create({url, active: true}) 15 | .catch(ErrorManager.catch); 16 | } else { 17 | api.runtime.openOptionsPage(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/js/Controller/Popup/Status/Set.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import RegistryService from '@js/Services/RegistryService'; 3 | 4 | export default class Set extends AbstractController { 5 | 6 | async execute(message, reply) { 7 | let payload = message.getPayload(); 8 | 9 | if(payload.hasOwnProperty('service')) { 10 | if(!payload.hasOwnProperty('tab')) return; 11 | RegistryService.set('popup.tab', payload.tab); 12 | 13 | if(payload.hasOwnProperty('data')) { 14 | RegistryService.set('popup.data', payload.data); 15 | } 16 | } else { 17 | if(!payload.hasOwnProperty('tab')) return; 18 | RegistryService.set('popup.tab', payload.tab); 19 | 20 | if(payload.hasOwnProperty('status')) { 21 | RegistryService.set(`popup.${payload.tab}.status`, payload.status); 22 | } 23 | } 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/js/Controller/Popup/UpdateMiningItem.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import QueueService from '@js/Services/QueueService'; 3 | import MiningItem from '@js/Models/Queue/MiningItem'; 4 | 5 | export default class UpdateMiningItem extends AbstractController { 6 | 7 | async execute(message, reply) { 8 | let data = message.getPayload(), 9 | miningQueue = QueueService.getFeedbackQueue('mining', null, MiningItem), 10 | items = miningQueue.getItems(); 11 | 12 | if(!data.hasOwnProperty('id') || !data.hasOwnProperty('result')) return; 13 | for(let item of items) { 14 | if(item.getId() === data.id) { 15 | item.setResult(data.result); 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/js/Controller/Server/Create.js: -------------------------------------------------------------------------------- 1 | import ServerRepository from '@js/Repositories/ServerRepository'; 2 | import ServerManager from '@js/Manager/ServerManager'; 3 | import ErrorManager from '@js/Manager/ErrorManager'; 4 | import AbstractController from '@js/Controller/AbstractController'; 5 | import ServerValidation from '@js/Validation/Server'; 6 | 7 | export default class Create extends AbstractController { 8 | 9 | /** 10 | * 11 | * @param {Message} message 12 | * @param {Message} reply 13 | */ 14 | async execute(message, reply) { 15 | let data = message.getPayload(), 16 | validation = new ServerValidation(), 17 | result = await validation.validate(data); 18 | 19 | if(!result.ok) { 20 | reply.setType('validation.error').setPayload(result); 21 | return; 22 | } 23 | 24 | let server = result.server; 25 | if(await this._createServer(server, result)) { 26 | ServerManager 27 | .addServer(server) 28 | .catch(ErrorManager.catch); 29 | reply.setType('server.item').setPayload(server); 30 | } else { 31 | reply.setType('validation.error').setPayload(result); 32 | } 33 | } 34 | 35 | /** 36 | * 37 | * @param {Server} server 38 | * @param {Object} result 39 | * @returns {Promise} 40 | * @private 41 | */ 42 | async _createServer(server, result) { 43 | try { 44 | server 45 | .setEnabled(true) 46 | .setTimeout(0); 47 | await ServerRepository.create(server); 48 | return true; 49 | } catch(e) { 50 | ErrorManager.logError(e); 51 | result.ok = false; 52 | result.message = e.message; 53 | return false; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/js/Controller/Server/Delete.js: -------------------------------------------------------------------------------- 1 | import ServerRepository from '@js/Repositories/ServerRepository'; 2 | import ServerManager from '@js/Manager/ServerManager'; 3 | import AbstractController from '@js/Controller/AbstractController'; 4 | import ErrorManager from '@js/Manager/ErrorManager'; 5 | 6 | export default class Delete extends AbstractController { 7 | 8 | /** 9 | * 10 | * @param {Message} message 11 | * @param {Message} reply 12 | */ 13 | async execute(message, reply) { 14 | try { 15 | let server = await ServerRepository.findById(message.getPayload().server); 16 | await ServerManager.deleteServer(server); 17 | reply.setType('delete.success'); 18 | } catch(e) { 19 | ErrorManager.logError(e); 20 | reply.setType('delete.failed') 21 | .setPayload({message: e.message}); 22 | } 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/js/Controller/Server/List.js: -------------------------------------------------------------------------------- 1 | import ServerRepository from '@js/Repositories/ServerRepository'; 2 | import AbstractController from '@js/Controller/AbstractController'; 3 | 4 | export default class List extends AbstractController { 5 | 6 | /** 7 | * 8 | * @param {Message} message 9 | * @param {Message} reply 10 | */ 11 | async execute(message, reply) { 12 | let payload = message.getPayload(), 13 | servers = await ServerRepository.findAll(), 14 | all = payload && payload.all, 15 | results = []; 16 | for(let server of servers) { 17 | if(all || server.getEnabled()) results.push(server); 18 | } 19 | 20 | reply.setType('server.items') 21 | .setPayload(results); 22 | } 23 | } -------------------------------------------------------------------------------- /src/js/Controller/Server/Reload.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import ServerRepository from '@js/Repositories/ServerRepository'; 3 | import ServerManager from '@js/Manager/ServerManager'; 4 | 5 | export default class Reload extends AbstractController { 6 | 7 | async execute(message, reply) { 8 | let server = await ServerRepository.findById(message.getPayload()); 9 | await ServerManager.reloadServer(server); 10 | reply.setPayload(true); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/js/Controller/Server/Update.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import ServerValidation from '@js/Validation/Server'; 3 | import ErrorManager from '@js/Manager/ErrorManager'; 4 | import ServerRepository from '@js/Repositories/ServerRepository'; 5 | import ServerManager from '@js/Manager/ServerManager'; 6 | import ToastService from '@js/Services/ToastService'; 7 | 8 | export default class Update extends AbstractController { 9 | 10 | /** 11 | * 12 | * @param {Message} message 13 | * @param {Message} reply 14 | */ 15 | async execute(message, reply) { 16 | let data = message.getPayload(), 17 | validation = new ServerValidation(), 18 | result = await validation.validate(data); 19 | 20 | if(!result.ok) { 21 | reply.setType('validation.error').setPayload(result); 22 | return; 23 | } 24 | 25 | let server = await this._createServer(result.server, result); 26 | if(server) { 27 | ServerManager.reloadServer(server) 28 | .catch(ErrorManager.catch); 29 | ToastService.closeByTags(server.getId(), 'server-error'); 30 | reply.setType('server.item').setPayload(server); 31 | } else { 32 | reply.setType('validation.error').setPayload(result); 33 | } 34 | } 35 | 36 | /** 37 | * 38 | * @param {Server} server 39 | * @param {Object} result 40 | * @returns {Promise<(Server|Boolean)>} 41 | * @private 42 | */ 43 | async _createServer(server, result) { 44 | try { 45 | let realServer = await ServerRepository.findById(server.getId()); 46 | realServer 47 | .setEnabled(true) 48 | .setUser(server.getUser()) 49 | .setToken(server.getToken()) 50 | .setLabel(server.getLabel()) 51 | .setBaseUrl(server.getBaseUrl()) 52 | .setTimeout(server.getTimeout()); 53 | await ServerRepository.update(realServer); 54 | 55 | return realServer; 56 | } catch(e) { 57 | ErrorManager.logError(e); 58 | result.ok = false; 59 | result.message = e.message; 60 | return false; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/js/Controller/Setting/Reset.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import SettingsService from '@js/Services/SettingsService'; 3 | 4 | export default class Reset extends AbstractController { 5 | 6 | constructor() { 7 | super(); 8 | this._resettableSettings = [ 9 | 'paste.form.submit', 10 | 'paste.popup.close', 11 | 'paste.compromised.warning', 12 | 'paste.autofill', 13 | 'paste.basic-auth', 14 | 'popup.related.search', 15 | 'notification.password.new', 16 | 'notification.password.update', 17 | 'notification.password.quicksave', 18 | 'server.default', 19 | 'theme.current', 20 | 'theme.custom', 21 | 'debug.localisation.enabled', 22 | 'search.recommendation.mode', 23 | 'search.recommendation.maxRows', 24 | 'clipboard.clear.passwords', 25 | 'clipboard.clear.delay', 26 | 'password.list.show.user', 27 | 'mining.ignored-domains', 28 | 'mining.incognito.hide', 29 | 'autofill.ignored-domains' 30 | ]; 31 | } 32 | 33 | /** 34 | * 35 | * @param {Message} message 36 | * @param {Message} reply 37 | * @return {Promise} 38 | */ 39 | async execute(message, reply) { 40 | let setting = message.getPayload(); 41 | 42 | try { 43 | if(this._resettableSettings.indexOf(setting) !== -1) { 44 | let value = await SettingsService.reset(setting); 45 | reply.setType('setting.value').setPayload(value); 46 | } else { 47 | reply.setPayload( 48 | { 49 | success: false, 50 | message: 'Unknown setting' 51 | } 52 | ); 53 | } 54 | } catch(e) { 55 | reply.setPayload( 56 | { 57 | success: false, 58 | message: e.message 59 | } 60 | ); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/js/Controller/Tab/Close.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import SystemService from '@js/Services/SystemService'; 3 | import TabManager from '@js/Manager/TabManager'; 4 | import ErrorManager from '@js/Manager/ErrorManager'; 5 | 6 | export default class Close extends AbstractController { 7 | 8 | /** 9 | * 10 | * @param {Message} message 11 | * @param {Message} reply 12 | */ 13 | async execute(message, reply) { 14 | let api = SystemService.getBrowserApi(); 15 | 16 | try { 17 | let tabId = await this.getTabId(message); 18 | await api.tabs.remove(tabId); 19 | reply.setPayload({success: true}); 20 | } catch(e) { 21 | ErrorManager.logError(e); 22 | reply.setPayload({success: false}); 23 | } 24 | } 25 | 26 | /** 27 | * 28 | * @param {Message} message 29 | * @return {Promise} 30 | */ 31 | async getTabId(message) { 32 | let payload = message.getPayload(); 33 | 34 | if(payload === null || !payload.hasOwnProperty('url')) return TabManager.get('id'); 35 | 36 | let tabs = await SystemService.getBrowserApi().tabs.query({url: payload.url}); 37 | if(tabs.length === 0) return TabManager.get('id'); 38 | 39 | return tabs[0].id; 40 | } 41 | } -------------------------------------------------------------------------------- /src/js/Controller/Tab/Create.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import SystemService from '@js/Services/SystemService'; 3 | import ErrorManager from '@js/Manager/ErrorManager'; 4 | 5 | export default class Create extends AbstractController { 6 | 7 | /** 8 | * 9 | * @param {Message} message 10 | * @param {Message} reply 11 | */ 12 | async execute(message, reply) { 13 | let api = SystemService.getBrowserApi(); 14 | 15 | try { 16 | let payload = message.getPayload(); 17 | await api.tabs.create({url:payload.url}); 18 | reply.setPayload({success: true}); 19 | } catch(e) { 20 | ErrorManager.logError(e); 21 | reply.setPayload({success: false}); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/js/Controller/Tab/Popout.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import SystemService from '@js/Services/SystemService'; 3 | import TabManager from '@js/Manager/TabManager'; 4 | 5 | export default class Popout extends AbstractController { 6 | 7 | /** 8 | * 9 | * @param {Message} message 10 | * @param {Message} reply 11 | */ 12 | async execute(message, reply) { 13 | let info = await SystemService.getBrowserInfo(); 14 | if(info.device === 'desktop') { 15 | let id = await this.getTabId(message); 16 | this.tabToWindow(id); 17 | } 18 | reply.setPayload({success: true}); 19 | } 20 | 21 | /** 22 | * 23 | * @return {Promise} 24 | */ 25 | async tabToWindow(tabId) { 26 | let api = SystemService.getBrowserApi(), 27 | parent = await api.windows.getLastFocused({populate: true}), 28 | offset = {width: 14, height: SystemService.isCompatible('chrome') ? 106:82, left: 25, top: 74}, 29 | width = 360 + offset.width, 30 | height = 360 + offset.height, 31 | left = Math.floor(parent.left + parent.width - width - offset.left), 32 | top = Math.floor(parent.top + offset.top); 33 | 34 | if(parent.tabs.length === 1 && parent.tabs[0].id === tabId) { 35 | await api.windows.update(tabId, {top, left, width, height, focused: true, drawAttention: true}); 36 | return; 37 | } 38 | 39 | let info = await api.windows.create({type: 'panel', tabId, top, left, width, height}); 40 | 41 | if(SystemService.isCompatible(SystemService.PLATFORM_FIREFOX)) { 42 | await api.windows.update(info.id, {top, left}); 43 | } 44 | } 45 | 46 | /** 47 | * 48 | * @param {Message} message 49 | * @return {Promise} 50 | */ 51 | async getTabId(message) { 52 | let payload = message.getPayload(); 53 | 54 | if(payload === null || !payload.hasOwnProperty('url')) return TabManager.get('id'); 55 | 56 | let tabs = await SystemService.getBrowserApi().tabs.query({url: payload.url}); 57 | if(tabs.length === 0) return TabManager.get('id'); 58 | 59 | return tabs[0].id; 60 | } 61 | } -------------------------------------------------------------------------------- /src/js/Controller/Theme/Delete.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import ThemeRepository from '@js/Repositories/ThemeRepository'; 3 | import ThemeService from "@js/Services/ThemeService"; 4 | import SettingsService from "@js/Services/SettingsService"; 5 | 6 | export default class Delete extends AbstractController { 7 | 8 | async execute(message, reply) { 9 | let themeId = message.getPayload().id; 10 | 11 | let currentTheme = await ThemeService.getCurrentTheme(); 12 | if(currentTheme.getId() === themeId) { 13 | let defaultTheme = await ThemeRepository.findById('light'); 14 | ThemeService.applyTheme(defaultTheme); 15 | await SettingsService.reset('theme.current'); 16 | } 17 | 18 | await ThemeRepository.delete(themeId); 19 | reply.setPayload({success: true}); 20 | } 21 | } -------------------------------------------------------------------------------- /src/js/Controller/Theme/Import.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import ThemeRepository from '@js/Repositories/ThemeRepository'; 3 | import ThemeSchema from '@js/Schema/ThemeSchema'; 4 | import Theme from "@js/Models/Theme/Theme"; 5 | import LocalisationService from "@js/Services/LocalisationService"; 6 | 7 | export default class Import extends AbstractController { 8 | 9 | async execute(message, reply) { 10 | let theme = message.getPayload(); 11 | theme.type = 'custom'; 12 | 13 | let validationResult = ThemeSchema(theme); 14 | if(!validationResult) { 15 | reply.setPayload({success: false, message: LocalisationService.translate('ThemeImportError', [this.#formatErrors(ThemeSchema)])}); 16 | return; 17 | } 18 | 19 | await ThemeRepository.update(new Theme(theme)); 20 | reply.setPayload({success: true}); 21 | } 22 | 23 | /** 24 | * 25 | * @param ThemeSchema 26 | * @return {string} 27 | */ 28 | #formatErrors(ThemeSchema) { 29 | let message = []; 30 | 31 | for(let error of ThemeSchema.errors) { 32 | message.push(error.message.capitalize()); 33 | } 34 | 35 | return message.join(' '); 36 | } 37 | } -------------------------------------------------------------------------------- /src/js/Controller/Theme/List.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import ThemeRepository from '@js/Repositories/ThemeRepository'; 3 | 4 | export default class List extends AbstractController { 5 | 6 | async execute(message, reply) { 7 | let themes = await ThemeRepository.findAll(); 8 | 9 | reply.setType('theme.items').setPayload(themes); 10 | } 11 | } -------------------------------------------------------------------------------- /src/js/Controller/Theme/Save.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import ThemeRepository from '@js/Repositories/ThemeRepository'; 3 | import ErrorManager from '@js/Manager/ErrorManager'; 4 | 5 | export default class Save extends AbstractController { 6 | 7 | async execute(message, reply) { 8 | try { 9 | await ThemeRepository.update(message.getPayload()); 10 | reply.setPayload({success: true}); 11 | } catch(e) { 12 | ErrorManager.logError(e); 13 | reply.setType('error').setPayload({success: false, message: e.message}); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/js/Controller/Theme/Show.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import ThemeRepository from '@js/Repositories/ThemeRepository'; 3 | 4 | export default class Show extends AbstractController { 5 | async execute(message, reply) { 6 | try { 7 | let theme = await ThemeRepository.findById(message.getPayload()); 8 | reply.setType('theme.item').setPayload(theme); 9 | } catch(e) { 10 | reply.setType('error'); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/js/Controller/Token/Request.js: -------------------------------------------------------------------------------- 1 | import AbstractController from '@js/Controller/AbstractController'; 2 | import ApiRepository from '@js/Repositories/ApiRepository'; 3 | import ErrorManager from '@js/Manager/ErrorManager'; 4 | 5 | export default class Request extends AbstractController { 6 | 7 | async execute(message, reply) { 8 | let api = await ApiRepository.findById(message.getPayload().server), 9 | provider = message.getPayload().provider, 10 | authRequest = api.getSessionAuthorization(); 11 | 12 | if(!authRequest.isLoaded()) await authRequest.load(); 13 | authRequest.setActiveToken(provider); 14 | let token = authRequest.getActiveToken(); 15 | if(token && token.requiresRequest()) { 16 | try { 17 | await token.sendRequest(); 18 | } catch(e) { 19 | ErrorManager.logError(e); 20 | reply.setPayload({success: false, message: e.message}); 21 | } 22 | } 23 | 24 | reply.setPayload({success: true}); 25 | } 26 | } -------------------------------------------------------------------------------- /src/js/Converter/FolderConverter.js: -------------------------------------------------------------------------------- 1 | import {Folder} from 'passwords-client/models'; 2 | 3 | export default class FolderConverter { 4 | 5 | /** 6 | * 7 | * @param {Message} message 8 | * @returns {Message} 9 | */ 10 | convert(message) { 11 | if(message.getType() === 'folder.items') { 12 | this._processFolderItems(message); 13 | } 14 | if(message.getType() === 'folder.item') { 15 | this._convertFolderItem(message); 16 | } 17 | 18 | return message; 19 | } 20 | 21 | /** 22 | * 23 | * @param {Message} message 24 | */ 25 | _processFolderItems(message) { 26 | let payload = message.getPayload(); 27 | payload.folders = this._convertFolders(payload.folders); 28 | message.setPayload(payload); 29 | } 30 | 31 | /** 32 | * 33 | * @param {Message} message 34 | */ 35 | _convertFolderItem(message) { 36 | let payload = message.getPayload(); 37 | 38 | if(payload === null) return; 39 | message.setPayload(new Folder(payload)); 40 | } 41 | 42 | /** 43 | * 44 | * @param {Array} items 45 | * @return {Password[]} 46 | * @private 47 | */ 48 | _convertFolders(items) { 49 | let folders = []; 50 | 51 | if(items !== null) { 52 | for(let data of items) { 53 | if(data !== null) folders.push(new Folder(data)); 54 | } 55 | } 56 | 57 | return folders; 58 | } 59 | } -------------------------------------------------------------------------------- /src/js/Converter/PasswordConverter.js: -------------------------------------------------------------------------------- 1 | import {Password} from 'passwords-client/models'; 2 | 3 | export default class PasswordConverter { 4 | 5 | /** 6 | * 7 | * @param {Message} message 8 | * @returns {Message} 9 | */ 10 | convert(message) { 11 | if(message.getType() === 'password.items') { 12 | this._processPasswordItems(message); 13 | } 14 | if(message.getType() === 'password.suggestions') { 15 | this._processPasswordSuggestions(message); 16 | } 17 | if(message.getType() === 'folder.items') { 18 | this._processFolderItems(message); 19 | } 20 | 21 | return message; 22 | } 23 | 24 | /** 25 | * 26 | * @param {Message} message 27 | */ 28 | _processPasswordItems(message) { 29 | let payload = message.getPayload(), 30 | passwords = this._convertPasswords(payload); 31 | message.setPayload(passwords); 32 | } 33 | 34 | /** 35 | * 36 | * @param {Message} message 37 | */ 38 | _processPasswordSuggestions(message) { 39 | let payload = message.getPayload(); 40 | payload.passwords = this._convertPasswords(payload.passwords); 41 | 42 | message.setPayload(payload); 43 | } 44 | 45 | /** 46 | * 47 | * @param {Message} message 48 | */ 49 | _processFolderItems(message) { 50 | let payload = message.getPayload(); 51 | payload.passwords = this._convertPasswords(payload.passwords); 52 | message.setPayload(payload); 53 | } 54 | 55 | /** 56 | * 57 | * @param {Array} items 58 | * @return {Password[]} 59 | * @private 60 | */ 61 | _convertPasswords(items) { 62 | let passwords = []; 63 | 64 | if(items !== null) { 65 | for(let data of items) { 66 | passwords.push(new Password(data)); 67 | } 68 | } 69 | 70 | return passwords; 71 | } 72 | } -------------------------------------------------------------------------------- /src/js/Converter/PasteRequestConverter.js: -------------------------------------------------------------------------------- 1 | import PasswordPasteRequest from "@js/Models/Client/PasswordPasteRequest"; 2 | 3 | export default class PasteRequestConverter { 4 | 5 | /** 6 | * 7 | * @param {Message} message 8 | * @returns {Message} 9 | */ 10 | convert(message) { 11 | if (message.getType() === 'autofill.password') { 12 | this._convertPasteRequestItem(message); 13 | } 14 | 15 | return message; 16 | } 17 | 18 | /** 19 | * 20 | * @param {Message} message 21 | * @return {Password[]} 22 | * @private 23 | */ 24 | _convertPasteRequestItem(message) { 25 | let payload = message.getPayload(); 26 | message.setPayload( 27 | new PasswordPasteRequest( 28 | payload.user, 29 | payload.password, 30 | payload.email, 31 | payload.formFields, 32 | payload.submit, 33 | payload.autofill 34 | ) 35 | ); 36 | } 37 | } -------------------------------------------------------------------------------- /src/js/Converter/ServerConverter.js: -------------------------------------------------------------------------------- 1 | import Server from '@js/Models/Server/Server'; 2 | 3 | export default class ServerConverter { 4 | 5 | /** 6 | * 7 | * @param {Message} message 8 | * @returns {Message} 9 | */ 10 | convert(message) { 11 | if(message.getType() === 'server.items') { 12 | return this._covertItems(message); 13 | } else { 14 | return this._covertItem(message); 15 | } 16 | } 17 | 18 | /** 19 | * 20 | * @param {Message} message 21 | * @returns {Message} 22 | */ 23 | _covertItems(message) { 24 | let payload = message.getPayload(), 25 | servers = []; 26 | 27 | if(payload !== null) { 28 | for(let data of payload) { 29 | servers.push(new Server(data)); 30 | } 31 | } 32 | 33 | return message.setPayload(servers); 34 | } 35 | 36 | /** 37 | * 38 | * @param {Message} message 39 | * @returns {Message} 40 | */ 41 | _covertItem(message) { 42 | let payload = message.getPayload(); 43 | 44 | return message.setPayload(new Server(payload)); 45 | } 46 | } -------------------------------------------------------------------------------- /src/js/Converter/ThemeConverter.js: -------------------------------------------------------------------------------- 1 | import Theme from '@js/Models/Theme/Theme'; 2 | 3 | export default class ThemeConverter { 4 | /** 5 | * 6 | * @param {Message} message 7 | * @returns {Message} 8 | */ 9 | convert(message) { 10 | if(message.getType() === 'theme.items') { 11 | return this._covertItems(message); 12 | } else { 13 | return this._covertItem(message); 14 | } 15 | } 16 | 17 | /** 18 | * 19 | * @param {Message} message 20 | * @returns {Message} 21 | */ 22 | _covertItems(message) { 23 | let payload = message.getPayload(), 24 | servers = []; 25 | 26 | if(payload !== null) { 27 | for(let data of payload) { 28 | servers.push(new Theme(data)); 29 | } 30 | } 31 | 32 | return message.setPayload(servers); 33 | } 34 | 35 | /** 36 | * 37 | * @param {Message} message 38 | * @returns {Message} 39 | */ 40 | _covertItem(message) { 41 | let payload = message.getPayload(); 42 | 43 | return message.setPayload(new Theme(payload)); 44 | } 45 | } -------------------------------------------------------------------------------- /src/js/Database/Database.js: -------------------------------------------------------------------------------- 1 | import BooleanState from 'passwords-client/boolean-state'; 2 | import ErrorManager from "@js/Manager/ErrorManager"; 3 | import Table from "@js/Database/Table"; 4 | import CouldNotOpenIndexedDbError from "@js/Exception/CouldNotOpenIndexedDbError"; 5 | 6 | export default class Database { 7 | 8 | get ready() { 9 | return this._ready; 10 | } 11 | 12 | constructor(name, version, upgradeCallback = null) { 13 | this._name = name; 14 | this._version = version; 15 | this._upgradeCallback = upgradeCallback; 16 | this._ready = new BooleanState(false); 17 | this._db = null; 18 | this._tables = {}; 19 | } 20 | 21 | load() { 22 | let request = window.indexedDB.open(this._name, this._version); 23 | 24 | request.onerror = (event) => { 25 | ErrorManager.logError(new CouldNotOpenIndexedDbError(this._name, this._version, request.error), {event}); 26 | }; 27 | 28 | request.onupgradeneeded = (event) => { 29 | let db = event.target.result; 30 | 31 | if (this._upgradeCallback) { 32 | this._upgradeCallback(db, event); 33 | } 34 | }; 35 | 36 | request.onsuccess = (event) => { 37 | this._db = event.target.result; 38 | this._ready.set(true); 39 | }; 40 | 41 | return this._ready.awaitTrue(); 42 | } 43 | 44 | async unload() { 45 | if (!this._ready.get()) { 46 | await this._ready.awaitTrue(); 47 | } 48 | this._tables = {}; 49 | this._db.close(); 50 | this._ready.set(false); 51 | } 52 | 53 | /** 54 | * @param {String} name 55 | * @return {Promise} 56 | */ 57 | async use(name) { 58 | if (!this._ready.get()) { 59 | await this._ready.awaitTrue(); 60 | } 61 | 62 | if(this._tables.hasOwnProperty(name)) { 63 | return this._tables[name]; 64 | } 65 | 66 | let table = new Table(this._db, name); 67 | await table.load(); 68 | this._tables[name] = table; 69 | return table; 70 | } 71 | } -------------------------------------------------------------------------------- /src/js/Database/MemoryDatabase.js: -------------------------------------------------------------------------------- 1 | import Database from "@js/Database/Database"; 2 | import MemoryTable from "@js/Database/MemoryTable"; 3 | 4 | export default class MemoryDatabase extends Database { 5 | 6 | async load() { 7 | this._db = {}; 8 | this._tables = {}; 9 | this._ready.set(true); 10 | } 11 | 12 | async unload() { 13 | if (!this._ready.get()) { 14 | await this._ready.awaitTrue(); 15 | } 16 | this._db = null; 17 | this._tables = {}; 18 | this._ready.set(false); 19 | } 20 | /** 21 | * @param {String} name 22 | * @return {Promise
} 23 | */ 24 | async use(name) { 25 | if (!this._ready.get()) { 26 | await this._ready.awaitTrue(); 27 | } 28 | 29 | if(this._tables.hasOwnProperty(name)) { 30 | return this._tables[name]; 31 | } 32 | 33 | let table = new MemoryTable(this._db, name); 34 | await table.load(); 35 | this._tables[name] = table; 36 | return table; 37 | } 38 | } -------------------------------------------------------------------------------- /src/js/Database/MemoryTable.js: -------------------------------------------------------------------------------- 1 | import Table from "@js/Database/Table"; 2 | 3 | export default class MemoryTable extends Table { 4 | 5 | load() { 6 | this._entries = {}; 7 | } 8 | 9 | async flush() { 10 | } 11 | 12 | get(id) { 13 | if (this._entries.hasOwnProperty(id)) { 14 | return this._entries[id]; 15 | } 16 | 17 | return null; 18 | } 19 | 20 | set(id, value) { 21 | this._entries[id] = value; 22 | } 23 | } -------------------------------------------------------------------------------- /src/js/Database/Table.js: -------------------------------------------------------------------------------- 1 | import ErrorManager from "@js/Manager/ErrorManager"; 2 | 3 | export default class Table { 4 | 5 | constructor(db, name) { 6 | this._db = db; 7 | this._name = name; 8 | this._entries = {}; 9 | } 10 | 11 | load() { 12 | return new Promise((resolve, reject) => { 13 | let transaction = this._db.transaction([this._name], 'readonly'), 14 | request = transaction.objectStore(this._name).getAll(); 15 | 16 | request.onerror = reject; 17 | request.onsuccess = (e) => { 18 | let entries = e.target.result; 19 | for (let entry of entries) { 20 | this._entries[entry.key] = JSON.parse(entry.value); 21 | } 22 | resolve(); 23 | }; 24 | }); 25 | } 26 | 27 | async flush() { 28 | for (let id in this._entries) { 29 | await this._writeEntry(id, this._entries[id]); 30 | } 31 | } 32 | 33 | get(id) { 34 | if (this._entries.hasOwnProperty(id)) { 35 | return this._entries[id]; 36 | } 37 | 38 | return null; 39 | } 40 | 41 | set(id, value) { 42 | this._entries[id] = value; 43 | 44 | this._writeEntry(id, value) 45 | .catch(ErrorManager.catch); 46 | } 47 | 48 | _writeEntry(key, value) { 49 | return new Promise((resolve, reject) => { 50 | let transaction = this._db.transaction([this._name], 'readwrite'), 51 | request = transaction.objectStore(this._name).put({key, value: JSON.stringify(value)}); 52 | 53 | request.onsuccess = resolve; 54 | request.onerror = reject; 55 | }); 56 | } 57 | } -------------------------------------------------------------------------------- /src/js/Definition/Server.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : { 3 | "type": "string" 4 | }, 5 | "label" : { 6 | "type": "string" 7 | }, 8 | "enabled" : { 9 | "type": "boolean" 10 | }, 11 | "lockable" : { 12 | "type": "boolean" 13 | }, 14 | "rootFolder" : { 15 | "type": "string" 16 | }, 17 | "inboxFolder" : { 18 | "type": "string" 19 | }, 20 | "privateFolder": { 21 | "type": "string" 22 | }, 23 | "timeout": { 24 | "type": "number" 25 | }, 26 | "inboxTag" : { 27 | "type": "string" 28 | }, 29 | "flags" : { 30 | "type": "array" 31 | } 32 | } -------------------------------------------------------------------------------- /src/js/Definition/Theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": { 3 | "type": "string" 4 | }, 5 | "label": { 6 | "type": "string" 7 | }, 8 | "type": { 9 | "type": "string" 10 | }, 11 | "font": { 12 | "type": "object" 13 | }, 14 | "variables": { 15 | "type": "object" 16 | }, 17 | "badge": { 18 | "type": "object" 19 | }, 20 | "colors": { 21 | "type": "object" 22 | }, 23 | "style": { 24 | "type": "boolean" 25 | } 26 | } -------------------------------------------------------------------------------- /src/js/Definition/Toast.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": { 3 | "type": "string" 4 | }, 5 | "type": { 6 | "type": "string" 7 | }, 8 | "title": { 9 | "type": "string" 10 | }, 11 | "titleVars": { 12 | "type": "array" 13 | }, 14 | "message": { 15 | "type": "string" 16 | }, 17 | "default": { 18 | "type": "string" 19 | }, 20 | "messageVars": { 21 | "type": "array" 22 | }, 23 | "ttl": { 24 | "type": "number" 25 | }, 26 | "visible": { 27 | "type": "boolean" 28 | }, 29 | "closeable": { 30 | "type": "boolean" 31 | }, 32 | "options": { 33 | "type": "object" 34 | }, 35 | "tags": { 36 | "type": "array" 37 | }, 38 | "modal": { 39 | "type": "boolean" 40 | } 41 | } -------------------------------------------------------------------------------- /src/js/Event/EventQueue.js: -------------------------------------------------------------------------------- 1 | import ErrorManager from '@js/Manager/ErrorManager'; 2 | 3 | export default class EventQueue { 4 | 5 | constructor() { 6 | this._listeners = []; 7 | this._once = []; 8 | } 9 | 10 | /** 11 | * 12 | * @param {Object} data 13 | * @return {Promise} 14 | */ 15 | async emit(data) { 16 | await this._notifyListeners(data); 17 | await this._notifyOnce(data); 18 | } 19 | 20 | /** 21 | * 22 | * @param {Function} callback 23 | */ 24 | on(callback) { 25 | this._listeners.push(callback); 26 | } 27 | 28 | /** 29 | * 30 | * @param {Function} callback 31 | */ 32 | off(callback) { 33 | for(let i = 0; i < this._listeners.length; i++) { 34 | if(this._listeners[i] === callback) { 35 | this._listeners.splice(i, 1); 36 | i--; 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * 43 | * @return {Promise} 44 | */ 45 | once() { 46 | return new Promise((resolve) => { 47 | this._once.push(resolve); 48 | }); 49 | } 50 | 51 | /** 52 | * 53 | * @param {Object} data 54 | * @return {Promise} 55 | * @private 56 | */ 57 | async _notifyListeners(data) { 58 | for(let callback of this._listeners) { 59 | try { 60 | await callback(data); 61 | } catch(e) { 62 | ErrorManager.logError(e); 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * 69 | * @param {Object} data 70 | * @return {Promise} 71 | * @private 72 | */ 73 | async _notifyOnce(data) { 74 | let callback; 75 | while(callback = this._once.pop()) { 76 | try { 77 | await callback(data); 78 | } catch(e) { 79 | ErrorManager.logError(e); 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/js/Event/Events.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'passwords-client/event-emitter'; 2 | import ErrorManager from "@js/Manager/ErrorManager"; 3 | 4 | const eventEmitter = new EventEmitter(); 5 | 6 | const subscribe = (event, callback) => { 7 | eventEmitter.on(event, callback); 8 | }; 9 | const unsubscribe = (event, callback) => { 10 | eventEmitter.off(event, callback); 11 | }; 12 | const once = (event, callback) => { 13 | eventEmitter.once(event, callback); 14 | }; 15 | const emit = (event, data) => { 16 | eventEmitter.emit(event, data) 17 | .catch(ErrorManager.catch); 18 | }; 19 | const emitAsync = async (event, data) => { 20 | await eventEmitter.emit(event, data); 21 | }; 22 | 23 | export { 24 | subscribe, 25 | unsubscribe, 26 | once, 27 | emit, 28 | emitAsync 29 | }; -------------------------------------------------------------------------------- /src/js/Exception/ClientNotAuthorizedError.js: -------------------------------------------------------------------------------- 1 | export default class ClientNotAuthorizedError extends Error { 2 | 3 | /** 4 | * @returns {String} 5 | */ 6 | get name() { 7 | return 'ClientNotAuthorizedError'; 8 | } 9 | 10 | /** 11 | * @returns {PasswordsClient} 12 | */ 13 | get client() { 14 | return this._client; 15 | } 16 | 17 | /** 18 | * @param {PasswordsClient} client 19 | */ 20 | constructor(client) { 21 | let name = 'Client'; 22 | if(client.getServer()) name += ' ' + client.getServer().getLabel(); 23 | 24 | super(`${name} is not authorized`); 25 | this._client = client; 26 | } 27 | } -------------------------------------------------------------------------------- /src/js/Exception/CouldNotOpenIndexedDbError.js: -------------------------------------------------------------------------------- 1 | export default class CouldNotOpenIndexedDbError extends Error { 2 | 3 | /** 4 | * @returns {String} 5 | */ 6 | get name() { 7 | return 'CouldNotOpenIndexedDbError'; 8 | } 9 | 10 | /** 11 | * @param {String} name 12 | * @param {Number} version 13 | * @param {Error} error 14 | */ 15 | constructor(name, version, error = null) { 16 | super(`Could not open database ${name}@${version}${error ? `: ${error.message}`:''}`); 17 | this.previousError = error; 18 | } 19 | } -------------------------------------------------------------------------------- /src/js/Exception/ServerDisabledError.js: -------------------------------------------------------------------------------- 1 | export default class ServerDisabledError extends Error { 2 | 3 | /** 4 | * @returns {String} 5 | */ 6 | get name() { 7 | return 'ServerDisabledError'; 8 | } 9 | 10 | constructor() { 11 | super('Server disabled'); 12 | } 13 | } -------------------------------------------------------------------------------- /src/js/Exception/ServerNotFoundError.js: -------------------------------------------------------------------------------- 1 | export default class ServerNotFoundError extends Error { 2 | 3 | /** 4 | * @returns {String} 5 | */ 6 | get name() { 7 | return 'ServerNotFoundError'; 8 | } 9 | 10 | /** 11 | * @param {String} id 12 | */ 13 | constructor(id) { 14 | super(`The server with the id ${id} does not exist`); 15 | } 16 | } -------------------------------------------------------------------------------- /src/js/Exception/UnknownItemTypeError.js: -------------------------------------------------------------------------------- 1 | export default class UnknownItemTypeError extends Error { 2 | 3 | /** 4 | * @returns {String} 5 | */ 6 | get name() { 7 | return 'UnknownItemTypeError'; 8 | } 9 | 10 | /** 11 | * 12 | * @returns {Object} 13 | */ 14 | get item() { 15 | return this._item; 16 | } 17 | 18 | /** 19 | * @param {Object} item 20 | */ 21 | constructor(item) { 22 | super('Unable to determine item type'); 23 | this._item = item; 24 | } 25 | } -------------------------------------------------------------------------------- /src/js/Helper/AutofillRequestHelper.js: -------------------------------------------------------------------------------- 1 | import PasswordPasteRequest from "@js/Models/Client/PasswordPasteRequest"; 2 | 3 | export default new class AutofillRequestHelper { 4 | 5 | /** 6 | * 7 | * @param {Password} password 8 | * @param {Boolean} submit 9 | * @param {Boolean} autofill 10 | * @returns {PasswordPasteRequest} 11 | */ 12 | createPasteRequest(password, submit = true, autofill = false) { 13 | return new PasswordPasteRequest( 14 | password.getUserName(), 15 | password.getPassword(), 16 | null, 17 | this.getCustomFormFields(password), 18 | submit, 19 | autofill 20 | ); 21 | } 22 | 23 | /** 24 | * 25 | * @param {Password} password 26 | * @returns {Array} 27 | */ 28 | getCustomFormFields(password) { 29 | return password 30 | .getCustomFields() 31 | .getClone() 32 | .reduce((formFields, e) => { 33 | if(e.getType() === 'data' && e.getLabel().startsWith('ext:field/')) { 34 | formFields.push( 35 | { 36 | id : e.getLabel().replace('ext:field/', ''), 37 | value: e.getValue() 38 | } 39 | ); 40 | } 41 | 42 | return formFields; 43 | }, []); 44 | } 45 | }; -------------------------------------------------------------------------------- /src/js/Helper/BlobToBase64Helper.js: -------------------------------------------------------------------------------- 1 | export default new class BlobToBase64Helper { 2 | /** 3 | * @public 4 | * @param {Blob} blob 5 | * @returns {Promise} 6 | */ 7 | async convert(blob) { 8 | return new Promise((resolve, reject) => { 9 | let reader = new FileReader(); 10 | reader.addEventListener('loadend', () => { 11 | resolve(reader.result); 12 | }); 13 | reader.addEventListener('error', (e) => { 14 | reject(e); 15 | }); 16 | reader.readAsDataURL(blob); 17 | }); 18 | } 19 | } -------------------------------------------------------------------------------- /src/js/Helper/HashingHelper.js: -------------------------------------------------------------------------------- 1 | export default new class HashingHelper { 2 | async sha1(str) { 3 | const textEncoder = new TextEncoder(), 4 | hash = await crypto.subtle.digest('SHA-1', textEncoder.encode(str)); 5 | 6 | return Array.from(new Uint8Array(hash)) 7 | .map(v => v.toString(16).padStart(2, '0')) 8 | .join(''); 9 | } 10 | }; -------------------------------------------------------------------------------- /src/js/Helper/ThemeCssVarsHelper.js: -------------------------------------------------------------------------------- 1 | class ThemeCssVarsHelper { 2 | processTheme(theme) { 3 | let vars = { 4 | '--color-primary' : '#0082c9', 5 | '--color-text' : '#ffffff', 6 | '--image-background' : 'linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)', 7 | '--image-logo' : '', 8 | '--border-radius' : '3px', 9 | '--border-radius-large': '10px', 10 | '--border-radius-pill' : '100px' 11 | }; 12 | 13 | if(theme === null) return vars; 14 | if(theme.hasOwnProperty('colors')) { 15 | if(theme.colors.hasOwnProperty('primary')) { 16 | vars['--color-primary'] = theme.colors.primary; 17 | } 18 | if(theme.colors.hasOwnProperty('text')) { 19 | vars['--color-text'] = theme.colors.text; 20 | } 21 | } 22 | if(theme.hasOwnProperty('color.primary')) { 23 | vars['--color-primary'] = theme['color.primary']; 24 | } 25 | if(theme.hasOwnProperty('color.text')) { 26 | vars['--color-text'] = theme['color.text']; 27 | } 28 | if(theme.hasOwnProperty('background')) { 29 | if(vars['--color-primary'] !== '#0082c9') { 30 | vars['--image-background'] = `linear-gradient(40deg,${vars['--color-primary']} 0%,${vars['--color-text']} 320%)`; 31 | } 32 | 33 | vars['--image-background'] = 34 | `url(${theme.background}), ${vars['--image-background']}`; 35 | } 36 | if(theme.hasOwnProperty('logo')) { 37 | vars['--image-logo'] = `url(${theme.logo})`; 38 | } 39 | 40 | return vars; 41 | } 42 | } 43 | 44 | export default new ThemeCssVarsHelper(); -------------------------------------------------------------------------------- /src/js/Helper/UuidHelper.js: -------------------------------------------------------------------------------- 1 | export default new class UuidHelper { 2 | 3 | generate() { 4 | let uuidFunc = crypto?.randomUUID; 5 | 6 | if(uuidFunc) { 7 | return crypto.randomUUID(); 8 | } 9 | 10 | return this._fallbackUuid(); 11 | } 12 | 13 | _fallbackUuid() { 14 | return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => 15 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 16 | ); 17 | } 18 | } -------------------------------------------------------------------------------- /src/js/Manager/ClientControllerManager.js: -------------------------------------------------------------------------------- 1 | import MessageService from '@js/Services/MessageService'; 2 | import FillPassword from '@js/Controller/Client/FillPassword'; 3 | import ErrorManager from '@js/Manager/ErrorManager'; 4 | import ShowFields from "@js/Controller/Client/Debug/ShowFields"; 5 | 6 | class ClientControllerManager { 7 | init() { 8 | this._initClientControllers(); 9 | } 10 | 11 | _initClientControllers() { 12 | MessageService.listen( 13 | 'autofill.password', 14 | async (message, reply) => { 15 | try { 16 | let controller = new FillPassword(); 17 | await controller.execute(message, reply); 18 | } catch(e) { 19 | ErrorManager.logError(e, {message, reply}); 20 | } 21 | } 22 | ); 23 | MessageService.listen( 24 | 'debug.form.fields', 25 | async (message, reply) => { 26 | try { 27 | let controller = new ShowFields(); 28 | await controller.execute(message, reply); 29 | } catch(e) { 30 | ErrorManager.logError(e, {message, reply}); 31 | } 32 | } 33 | ); 34 | } 35 | } 36 | 37 | export default new ClientControllerManager(); -------------------------------------------------------------------------------- /src/js/Manager/ConverterManager.js: -------------------------------------------------------------------------------- 1 | import ErrorManager from '@js/Manager/ErrorManager'; 2 | import MessageService from '@js/Services/MessageService'; 3 | import ThemeConverter from '@js/Converter/ThemeConverter'; 4 | import FolderConverter from '@js/Converter/FolderConverter'; 5 | import ServerConverter from '@js/Converter/ServerConverter'; 6 | import PasswordConverter from '@js/Converter/PasswordConverter'; 7 | import PasteRequestConverter from "@js/Converter/PasteRequestConverter"; 8 | 9 | class ConverterManager { 10 | 11 | init() { 12 | MessageService.convert( 13 | ['password.items', 'password.suggestions', 'folder.items'], 14 | async (message) => { 15 | await this._executeConverter(PasswordConverter, message); 16 | } 17 | ); 18 | MessageService.convert( 19 | ['server.items', 'server.item'], 20 | async (message) => { 21 | await this._executeConverter(ServerConverter, message); 22 | } 23 | ); 24 | MessageService.convert( 25 | ['folder.items', 'folder.item'], 26 | async (message) => { 27 | await this._executeConverter(FolderConverter, message); 28 | } 29 | ); 30 | MessageService.convert( 31 | ['theme.items', 'theme.item', 'theme.save', 'theme.preview'], 32 | async (message) => { 33 | await this._executeConverter(ThemeConverter, message); 34 | } 35 | ); 36 | MessageService.convert( 37 | ['autofill.password'], 38 | async (message) => { 39 | await this._executeConverter(PasteRequestConverter, message); 40 | } 41 | ); 42 | } 43 | 44 | 45 | /** 46 | * 47 | * @param {Object} module 48 | * @param {Message} message 49 | * @private 50 | */ 51 | async _executeConverter(module, message) { 52 | try { 53 | let controller = new module(); 54 | await controller.convert(message); 55 | } catch(e) { 56 | ErrorManager.logError(e, {module, message}); 57 | } 58 | } 59 | } 60 | 61 | export default new ConverterManager(); -------------------------------------------------------------------------------- /src/js/Manager/PassLinkManager.js: -------------------------------------------------------------------------------- 1 | import SystemService from '@js/Services/SystemService'; 2 | 3 | export default new class PassLinkManager { 4 | 5 | /** 6 | * 7 | */ 8 | constructor() { 9 | this._beforeRequestListener = 10 | (d) => { return this._redirectRequest(d); }; 11 | } 12 | 13 | /** 14 | * 15 | */ 16 | init() { 17 | SystemService.getBrowserApi().webRequest.onBeforeRequest.addListener( 18 | this._beforeRequestListener, 19 | {urls: ['https://link.passwordsapp.org/open/*']}, 20 | ['blocking'] 21 | ); 22 | 23 | SystemService.getBrowserApi().tabs.onCreated.addListener( 24 | (tab) => { 25 | if(tab.url && tab.url.substr(0, 35) === 'https://link.passwordsapp.org/open/') { 26 | this._processEventUrl(tab.url, tab.id); 27 | } 28 | } 29 | ); 30 | 31 | SystemService.getBrowserApi().tabs.onUpdated.addListener( 32 | (tabId, changeInfo) => { 33 | if(changeInfo.url && changeInfo.url.substr(0, 35) === 'https://link.passwordsapp.org/open/') { 34 | this._processEventUrl(changeInfo.url, tabId); 35 | } 36 | } 37 | ); 38 | } 39 | 40 | /** 41 | * 42 | * @param requestDetails 43 | * @return {{cancel: boolean}|{redirectUrl: *}} 44 | * @private 45 | */ 46 | _redirectRequest(requestDetails) { 47 | this._processEventUrl(requestDetails.url, requestDetails.tabId); 48 | return {cancel: true}; 49 | } 50 | 51 | _processEventUrl(eventUrl, tabId) { 52 | let passlinkUrl = SystemService.getBrowserApi().runtime.getURL('html/passlink.html'); 53 | 54 | eventUrl = eventUrl.replace('https://link.passwordsapp.org/open/', 'ext+passlink:'); 55 | 56 | passlinkUrl += '?link=' + encodeURIComponent(eventUrl); 57 | SystemService.getBrowserApi().tabs.update(tabId, {url: passlinkUrl}); 58 | } 59 | }; -------------------------------------------------------------------------------- /src/js/Manager/PasswordSettingsManager.js: -------------------------------------------------------------------------------- 1 | import SettingsService from '@js/Services/SettingsService'; 2 | 3 | export default new class PasswordSettingsManager { 4 | 5 | /** 6 | * 7 | */ 8 | constructor() { 9 | this._showUsernameInList = null; 10 | } 11 | 12 | /** 13 | * 14 | */ 15 | async init() { 16 | this._showUsernameInList = await SettingsService.get('password.list.show.user') 17 | } 18 | 19 | /** 20 | * 21 | */ 22 | getShowUserInList() { 23 | return this._showUsernameInList.getValue(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/js/Migrations/Migration20001.js: -------------------------------------------------------------------------------- 1 | import SettingsService from "@js/Services/SettingsService"; 2 | import ServerRepository from "@js/Repositories/ServerRepository"; 3 | 4 | export default class Migration20001 { 5 | /** 6 | * Store detfault server as setting 7 | * 8 | * @returns {Promise} 9 | */ 10 | async sync() { 11 | if(await SettingsService.getValue('server.default') === null) { 12 | let servers = ServerRepository.findAll(); 13 | 14 | if(servers.length > 0) { 15 | await SettingsService.set('server.default', servers[0].getId()); 16 | } 17 | } 18 | } 19 | 20 | async local() {} 21 | } -------------------------------------------------------------------------------- /src/js/Migrations/Migration20002.js: -------------------------------------------------------------------------------- 1 | import ServerRepository from "@js/Repositories/ServerRepository"; 2 | 3 | export default class Migration20002 { 4 | /** 5 | * Initialize server enabled flag 6 | * 7 | * @returns {Promise} 8 | */ 9 | async sync() { 10 | let servers = ServerRepository.findAll(); 11 | for(let server of servers) { 12 | server.setEnabled(true); 13 | await ServerRepository.update(server); 14 | } 15 | } 16 | 17 | async local() {} 18 | } -------------------------------------------------------------------------------- /src/js/Migrations/Migration20003.js: -------------------------------------------------------------------------------- 1 | import ServerRepository from "@js/Repositories/ServerRepository"; 2 | 3 | export default class Migration20003 { 4 | 5 | /** 6 | * Initialize server lock timeout feature 7 | * 8 | * @returns {Promise} 9 | */ 10 | async sync() { 11 | let servers = ServerRepository.findAll(); 12 | for(let server of servers) { 13 | server.setTimeout(0); 14 | server.setLockable(false); 15 | await ServerRepository.update(server); 16 | } 17 | } 18 | 19 | async local() {} 20 | } -------------------------------------------------------------------------------- /src/js/Migrations/Migration20005.js: -------------------------------------------------------------------------------- 1 | import ServerRepository from "@js/Repositories/ServerRepository"; 2 | 3 | export default class Migration20005 { 4 | 5 | /** 6 | * Store tokens and servers separately. 7 | * It's done automatically, so the servers just need to be saved once. 8 | * 9 | * @returns {Promise} 10 | */ 11 | async sync() { 12 | let servers = ServerRepository.findAll(); 13 | for(let server of servers) { 14 | await ServerRepository.update(server); 15 | } 16 | } 17 | 18 | async local() {} 19 | } -------------------------------------------------------------------------------- /src/js/Models/Client/PasswordPasteRequest.js: -------------------------------------------------------------------------------- 1 | export default class PasswordPasteRequest { 2 | _user; 3 | _password; 4 | _email; 5 | _formFields; 6 | _submit; 7 | _autofill; 8 | 9 | constructor(user, password, email, formFields, submit, autofill = false) { 10 | this._user = user; 11 | this._password = password; 12 | this._email = email; 13 | this._formFields = formFields; 14 | this._submit = submit; 15 | this._autofill = autofill; 16 | } 17 | 18 | getUser() { 19 | return this._user; 20 | } 21 | 22 | getPassword() { 23 | return this._password; 24 | } 25 | 26 | getEmail() { 27 | return this._email; 28 | } 29 | 30 | getFormFields() { 31 | return this._formFields; 32 | } 33 | 34 | isSubmit() { 35 | return this._submit; 36 | } 37 | 38 | isAutofill() { 39 | return this._autofill; 40 | } 41 | 42 | toJSON() { 43 | return { 44 | user : this._user, 45 | password : this._password, 46 | email : this._email, 47 | formFields: this._formFields, 48 | submit : this._submit, 49 | autofill : this._autofill 50 | }; 51 | } 52 | } -------------------------------------------------------------------------------- /src/js/Models/Queue/FeedbackItem.js: -------------------------------------------------------------------------------- 1 | import QueueItem from '@js/Models/Queue/QueueItem'; 2 | 3 | export default class FeedbackItem extends QueueItem { 4 | 5 | /** 6 | * 7 | * @param {{}} data 8 | */ 9 | constructor(data = {}) { 10 | super(data); 11 | this._feedback = data.hasOwnProperty('feedback') ? data.feedback:{}; 12 | this._accepted = data.hasOwnProperty('accepted') ? data.accepted:false; 13 | } 14 | 15 | /** 16 | * 17 | * @returns {{}|*} 18 | */ 19 | getFeedback() { 20 | return this._feedback; 21 | } 22 | 23 | /** 24 | * 25 | * @param value 26 | * @returns {FeedbackItem} 27 | */ 28 | setFeedback(value) { 29 | this._feedback = value; 30 | 31 | return this; 32 | } 33 | 34 | /** 35 | * 36 | * @returns {Boolean} 37 | */ 38 | getAccepted() { 39 | return this._accepted; 40 | } 41 | 42 | /** 43 | * 44 | * @param {Boolean} value 45 | * @returns {FeedbackItem} 46 | */ 47 | setAccepted(value) { 48 | this._accepted = value === true; 49 | 50 | return this; 51 | } 52 | 53 | toJSON() { 54 | return { 55 | id : this._id, 56 | task : this._task, 57 | result : this._result, 58 | success : this._success, 59 | accepted: this._accepted, 60 | feedback: this._feedback 61 | }; 62 | } 63 | } -------------------------------------------------------------------------------- /src/js/Models/Search/Index.js: -------------------------------------------------------------------------------- 1 | import ErrorManager from "@js/Manager/ErrorManager"; 2 | 3 | export default class Index { 4 | 5 | constructor(indexer) { 6 | this._indexer = indexer; 7 | this._index = []; 8 | } 9 | 10 | getAll() { 11 | return this._index; 12 | } 13 | 14 | /** 15 | * 16 | * @param {AbstractRevisionModel} item 17 | */ 18 | add(item) { 19 | try { 20 | let index = this._indexer.indexItem(item); 21 | this._index.push(index); 22 | } catch(e) { 23 | ErrorManager.logError(e); 24 | } 25 | } 26 | 27 | /** 28 | * 29 | * @param {String} id 30 | */ 31 | remove(id) { 32 | let index = this._index.findIndex((element) => element.getId() === id); 33 | if(index !== -1) { 34 | this._index.splice(index, 1); 35 | } 36 | } 37 | 38 | /** 39 | * 40 | * @param {AbstractRevisionModel} item 41 | */ 42 | update(item) { 43 | try { 44 | let index = this._indexer.indexItem(item); 45 | this.remove(item.getId()); 46 | this._index.push(index); 47 | } catch(e) { 48 | ErrorManager.logError(e); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/js/Models/Search/Match.js: -------------------------------------------------------------------------------- 1 | export default class Match { 2 | 3 | /** 4 | * 5 | * @param {IndexEntry} indexEntry 6 | * @param {Number} score 7 | */ 8 | constructor(indexEntry, score) { 9 | this._indexEntry = indexEntry; 10 | this._score = score; 11 | } 12 | 13 | 14 | getId() { 15 | return this._indexEntry.getId(); 16 | } 17 | 18 | getType() { 19 | return this._indexEntry.getType(); 20 | } 21 | 22 | isHidden() { 23 | return this._indexEntry.isHidden(); 24 | } 25 | 26 | /** 27 | * 28 | * @param {String} name 29 | * @return {null|*[]} 30 | */ 31 | getField(name) { 32 | if (name === 'score') { 33 | return [this._score]; 34 | } 35 | 36 | return this._indexEntry.getField(name); 37 | } 38 | 39 | getFields() { 40 | let fields = this._indexEntry.getFields(); 41 | fields.score = [this._score]; 42 | 43 | return fields; 44 | } 45 | 46 | /** 47 | * 48 | * @param {String} name 49 | * @returns {Number} 50 | */ 51 | getBoost(name) { 52 | return this._indexEntry.getBoost(name); 53 | } 54 | } -------------------------------------------------------------------------------- /src/js/Models/Setting/Setting.js: -------------------------------------------------------------------------------- 1 | import EventQueue from '@js/Event/EventQueue'; 2 | import ErrorManager from '@js/Manager/ErrorManager'; 3 | import {Setting as SettingModel} from 'passwords-client/models'; 4 | import {InvalidScopeError} from 'passwords-client/errors'; 5 | 6 | export default class Setting extends SettingModel { 7 | 8 | /** 9 | * @return {String} 10 | */ 11 | static get SCOPE_LOCAL() { 12 | return 'local'; 13 | } 14 | 15 | /** 16 | * @return {String} 17 | */ 18 | static get SCOPE_SYNC() { 19 | return 'sync'; 20 | } 21 | 22 | /** 23 | * 24 | * @returns {String[]} 25 | * @constructor 26 | */ 27 | static get SCOPES() { 28 | let scopes = SettingModel.SCOPES; 29 | scopes.push(this.SCOPE_LOCAL); 30 | scopes.push(this.SCOPE_SYNC); 31 | return scopes; 32 | } 33 | 34 | /** 35 | * 36 | * @param value 37 | */ 38 | set value(value) { 39 | let oldValue = this._value; 40 | this._value = value; 41 | 42 | if(oldValue === value) return; 43 | this._change 44 | .emit({value: value, name: this._name, oldValue}) 45 | .catch(ErrorManager.catch); 46 | } 47 | 48 | /** 49 | * 50 | * @returns {EventQueue} 51 | */ 52 | get change() { 53 | return this._change; 54 | } 55 | 56 | /** 57 | * 58 | * @param name 59 | * @param value 60 | * @param scope 61 | */ 62 | constructor(name, value, scope = 'client') { 63 | super(name, value, scope); 64 | this._change = new EventQueue(); 65 | } 66 | 67 | /** 68 | * @param {String} scope 69 | * @private 70 | */ 71 | _checkScope(scope) { 72 | if(Setting.SCOPES.indexOf(scope) === -1) { 73 | throw new InvalidScopeError(scope); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/js/Platform/BrowserApi.js: -------------------------------------------------------------------------------- 1 | import SystemService from "@js/Services/SystemService"; 2 | 3 | class BrowserApi { 4 | 5 | /** 6 | * @return {browser} 7 | */ 8 | getBrowserApi() { 9 | return browser; 10 | } 11 | 12 | /** 13 | * @return {Promise<{os: String, vendor: String, name: String, arch: String, device: String, version: String}>} 14 | */ 15 | async getBrowserInfo() { 16 | let app = await browser.runtime.getBrowserInfo(), 17 | os = await browser.runtime.getPlatformInfo(), 18 | device = os.os === 'android' ? 'mobile':'desktop'; 19 | 20 | return { 21 | device, 22 | os : os.os, 23 | arch : os.arch, 24 | name : app.name, 25 | vendor : app.vendor, 26 | version: app.version 27 | }; 28 | } 29 | 30 | /** 31 | * 32 | * @return {browser.contextMenus} 33 | */ 34 | getContextMenu() { 35 | return browser.menus; 36 | } 37 | 38 | /** 39 | * 40 | * @return {Boolean} 41 | */ 42 | hasContextMenu() { 43 | return browser.hasOwnProperty('menus'); 44 | } 45 | 46 | /** 47 | * 48 | * @return {Boolean} 49 | */ 50 | hasBadgeText() { 51 | return browser.browserAction.hasOwnProperty('getBadgeText'); 52 | } 53 | 54 | /** 55 | * 56 | * @return {Boolean} 57 | */ 58 | hasNotificationButtons() { 59 | return false; 60 | } 61 | 62 | /** 63 | * 64 | * @return {Promise} 65 | */ 66 | async requestUpdateCheck() { 67 | return false; 68 | } 69 | 70 | /** 71 | * @param {String} platform 72 | * @returns {Boolean} 73 | */ 74 | isCompatible(platform) { 75 | if(platform === 'firefox') return true; 76 | return platform === process.env.APP_PLATFORM; 77 | } 78 | 79 | /** 80 | * @returns {boolean} 81 | */ 82 | usesDarkMode() { 83 | let matcher = window.matchMedia('(prefers-color-scheme: dark)'); 84 | return matcher.matches; 85 | } 86 | 87 | /** 88 | * @returns {string} 89 | */ 90 | getDefaultIcon() { 91 | return `passwords${SystemService.usesDarkMode() ? '-light':'-dark'}.svg`; 92 | } 93 | } 94 | 95 | export default new BrowserApi(); -------------------------------------------------------------------------------- /src/js/Prototype/prototype.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @returns {string} 3 | */ 4 | String.prototype.capitalize = function() { 5 | if(this.length === 0) return this; 6 | if(this.length === 1) return this.toUpperCase(); 7 | 8 | return this.charAt(0).toUpperCase() + this.slice(1); 9 | }; 10 | 11 | /** 12 | * @returns {*} 13 | */ 14 | Array.prototype.first = function() { 15 | let keys = Object.keys(this); 16 | 17 | if(keys.length === 0) return undefined; 18 | 19 | return this[keys[0]]; 20 | }; -------------------------------------------------------------------------------- /src/js/Queue/Client/FeedbackClient.js: -------------------------------------------------------------------------------- 1 | import QueueClient from '@js/Queue/Client/QueueClient'; 2 | import FeedbackItem from '@js/Models/Queue/FeedbackItem'; 3 | 4 | export default class FeedbackClient extends QueueClient { 5 | 6 | /** 7 | * 8 | * @param {String} name 9 | * @param {Function} callback 10 | * @param {Function} feedback 11 | * @param {FeedbackItem} type 12 | */ 13 | constructor(name, callback, feedback = null, type = FeedbackItem) { 14 | super(name, callback, type); 15 | this._feedback = feedback; 16 | } 17 | 18 | /** 19 | * 20 | * @param {FeedbackItem} item 21 | * @param {Function} callback 22 | * @returns {Promise} 23 | * @private 24 | */ 25 | async _executeCallback(item, callback) { 26 | if(item.getSuccess() !== null && this._feedback) { 27 | callback = this._feedback; 28 | } 29 | 30 | await super._executeCallback(item, callback); 31 | 32 | if(!item.getAccepted()) { 33 | item.setFeedback({}); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/js/Queue/FeedbackQueue.js: -------------------------------------------------------------------------------- 1 | import Queue from '@js/Queue/Queue'; 2 | import FeedbackItem from '@js/Models/Queue/FeedbackItem'; 3 | 4 | export default class FeedbackQueue extends Queue { 5 | 6 | /** 7 | * 8 | * @param {String} name 9 | * @param {(String|null)} [area=null] 10 | * @param {QueueItem} [type=FeedbackItem] 11 | */ 12 | constructor(name, area, type = FeedbackItem) { 13 | super(name, area, type); 14 | } 15 | 16 | 17 | /** 18 | * 19 | * @param {FeedbackItem|{}} item 20 | */ 21 | push(item) { 22 | item = this._validateItem(item); 23 | if(!item.getSuccess() || !item.getAccepted()) { 24 | return super.push(item); 25 | } 26 | 27 | return new Promise((resolve) => { 28 | this._sendItem(item); 29 | 30 | resolve(); 31 | }); 32 | } 33 | } -------------------------------------------------------------------------------- /src/js/Search/Indexer/AbstractIndexer.js: -------------------------------------------------------------------------------- 1 | import shoetest from "shoetest"; 2 | 3 | export default class AbstractIndexer { 4 | 5 | constructor() { 6 | this._textIndexFields = ['label']; 7 | this._genericIndexFields = ['favorite', 'hidden']; 8 | this._defaultBoostFields = ['favorite']; 9 | } 10 | 11 | /** 12 | * 13 | * @param {(Password|Folder|Tag)} model 14 | * @param {IndexEntry} index 15 | * @protected 16 | */ 17 | _indexServer(model, index) { 18 | let server = model.getServer(); 19 | index.addFieldValue('server', server.getId()) 20 | .addFieldValue('server', server.getLabel()); 21 | } 22 | 23 | /** 24 | * 25 | * @param {(Password|Folder|Tag)} model 26 | * @param {IndexEntry} index 27 | * @protected 28 | */ 29 | _indexTextFields(model, index) { 30 | for(let field of this._textIndexFields) { 31 | let value = model.getProperty(field); 32 | if(value === undefined) return; 33 | 34 | value = value.toLowerCase(); 35 | if(value.length !== 0) { 36 | index.addFieldValue('text', shoetest.simplify(value)) 37 | .addFieldValue(field, value); 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * 44 | * @param {(Password|Folder|Tag)} model 45 | * @param {IndexEntry} index 46 | * @protected 47 | */ 48 | _indexFields(model, index) { 49 | for(let field of this._genericIndexFields) { 50 | index.addFieldValue(field, model.getProperty(field)); 51 | } 52 | 53 | for(let field of this._defaultBoostFields) { 54 | let value = model.getProperty(field); 55 | if(!isNaN(value)) { 56 | index.setBoost(field, value*1); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/js/Search/Indexer/FolderIndexer.js: -------------------------------------------------------------------------------- 1 | import AbstractIndexer from '@js/Search/Indexer/AbstractIndexer'; 2 | import IndexEntry from "@js/Models/Search/IndexEntry"; 3 | 4 | export default class FolderIndexer extends AbstractIndexer { 5 | 6 | /** 7 | * 8 | * @param {Folder} folder 9 | * @return {IndexEntry} 10 | */ 11 | indexItem(folder) { 12 | return this._createIndex(folder); 13 | } 14 | 15 | /** 16 | * 17 | * @param {Folder} folder 18 | * @return {IndexEntry} 19 | */ 20 | _createIndex(folder) { 21 | let entry = new IndexEntry(folder.getId(), 'folder', folder.isHidden()); 22 | this._indexServer(folder, entry); 23 | this._indexTextFields(folder, entry); 24 | this._indexFields(folder, entry); 25 | 26 | entry.addFieldValue('folder', folder.getId()) 27 | .addFieldValue('folder', folder.getLabel()); 28 | 29 | let value = folder.getParent(); 30 | if(value && value.length !== 0) { 31 | entry.addFieldValue('parent', value); 32 | } 33 | 34 | entry.clean(); 35 | 36 | return entry; 37 | } 38 | } -------------------------------------------------------------------------------- /src/js/Search/Indexer/TagIndexer.js: -------------------------------------------------------------------------------- 1 | import AbstractIndexer from '@js/Search/Indexer/AbstractIndexer'; 2 | import IndexEntry from "@js/Models/Search/IndexEntry"; 3 | 4 | export default class TagIndexer extends AbstractIndexer { 5 | 6 | /** 7 | * 8 | * @param {Tag} tag 9 | * @return {IndexEntry} 10 | */ 11 | indexItem(tag) { 12 | return this._createIndex(tag); 13 | } 14 | 15 | /** 16 | * 17 | * @param {Tag} tag 18 | * @return {IndexEntry} 19 | */ 20 | _createIndex(tag) { 21 | let entry = new IndexEntry(tag.getId(), 'tag', tag.isHidden()); 22 | 23 | this._indexServer(tag, entry); 24 | this._indexTextFields(tag, entry); 25 | this._indexFields(tag, entry); 26 | 27 | entry.clean(); 28 | 29 | return entry; 30 | } 31 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Boost/AbstractBoost.js: -------------------------------------------------------------------------------- 1 | export default class AbstractBoost { 2 | 3 | 4 | constructor(name, amount = null) { 5 | this._name = name; 6 | this._amount = amount; 7 | } 8 | 9 | /** 10 | * @param {IndexEntry} entry 11 | * @param {Number} score 12 | * @return {Number} 13 | */ 14 | boost(entry, score) { 15 | return score; 16 | } 17 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Boost/MultiplyBoost.js: -------------------------------------------------------------------------------- 1 | import AbstractBoost from "@js/Search/Query/Boost/AbstractBoost"; 2 | 3 | export default class MultiplyBoost extends AbstractBoost { 4 | 5 | 6 | boost(entry, score) { 7 | let boost = entry.getBoost(this._name); 8 | if(boost === 0) { 9 | return score; 10 | } 11 | return score * (this._amount === null ? boost:this._amount); 12 | } 13 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Condition/AbstractCondition.js: -------------------------------------------------------------------------------- 1 | export default class AbstractCondition { 2 | 3 | constructor(...conditions) { 4 | if(!Array.isArray(conditions)) conditions = []; 5 | 6 | this._conditions = conditions; 7 | } 8 | 9 | /** 10 | * 11 | * @param {(AbstractField|AbstractCondition)} conditions 12 | * @return {AbstractCondition} 13 | */ 14 | append(...conditions) { 15 | this._conditions.push(...conditions); 16 | 17 | return this; 18 | } 19 | 20 | prepend(...conditions) { 21 | this._conditions.unshift(...conditions); 22 | 23 | return this; 24 | } 25 | 26 | /** 27 | * 28 | * @param {Object} item 29 | * @return {({checks: number, passed: boolean, matches: number}|{passed: false})} 30 | */ 31 | evaluate(item) { 32 | return {matches: 0, checks: 0, passed: false} 33 | } 34 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Condition/AndCondition.js: -------------------------------------------------------------------------------- 1 | import AbstractCondition from '@js/Search/Query/Condition/AbstractCondition'; 2 | 3 | export default class AndCondition extends AbstractCondition { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let result = {matches: 0, checks: 0, score: 0, passed: true}; 10 | 11 | for(let condition of this._conditions) { 12 | let partialResult = condition.evaluate(item); 13 | 14 | if(!partialResult.passed) return {matches: 0, checks: 0, score: 0, passed: false}; 15 | result.matches += partialResult.matches; 16 | result.checks += partialResult.checks; 17 | result.score += partialResult.score; 18 | } 19 | 20 | return result; 21 | } 22 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Condition/OrCondition.js: -------------------------------------------------------------------------------- 1 | import AbstractCondition from '@js/Search/Query/Condition/AbstractCondition'; 2 | 3 | export default class OrCondition extends AbstractCondition { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let result = {matches: 0, checks: 0, score: 0, passed: false}; 10 | 11 | for(let condition of this._conditions) { 12 | let partialResult = condition.evaluate(item); 13 | 14 | if(partialResult.passed) { 15 | result.passed = true; 16 | result.matches += partialResult.matches; 17 | result.checks += partialResult.checks; 18 | result.score += partialResult.score; 19 | } else { 20 | if(partialResult.checks) { 21 | result.checks += partialResult.checks; 22 | } else { 23 | result.checks++; 24 | } 25 | } 26 | } 27 | 28 | return result; 29 | } 30 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Condition/XorCondition.js: -------------------------------------------------------------------------------- 1 | import AbstractCondition from '@js/Search/Query/Condition/AbstractCondition'; 2 | 3 | export default class XorCondition extends AbstractCondition { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let result = {matches: 0, checks: 0, score:0, passed: false}; 10 | 11 | for(let condition of this._conditions) { 12 | let partialResult = condition.evaluate(item); 13 | 14 | if(partialResult.passed) { 15 | if(result.passed) return {matches: 0, checks: 0, score:0, passed: false}; 16 | 17 | result.passed = true; 18 | result.matches = partialResult.matches; 19 | result.checks = partialResult.checks; 20 | result.score = partialResult.score; 21 | } 22 | } 23 | 24 | return result; 25 | } 26 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/AbstractField.js: -------------------------------------------------------------------------------- 1 | export default class AbstractField { 2 | 3 | get NO_MATCH_RESULT() { 4 | return {matches: 0, checks: 1, score: 0, passed: false}; 5 | } 6 | 7 | /** 8 | * 9 | * @param {String} name 10 | * @param {*} value 11 | * @param {Number} weight 12 | */ 13 | constructor(name, value, weight) { 14 | this._name = name; 15 | this._value = value; 16 | this._weight = weight; 17 | } 18 | 19 | /** 20 | * 21 | * @return {String} 22 | */ 23 | getName() { 24 | return this._name; 25 | } 26 | 27 | /** 28 | * 29 | * @return {*} 30 | */ 31 | getValue() { 32 | return this._value; 33 | } 34 | 35 | /** 36 | * 37 | * @param {IndexEntry} item 38 | * @return {({checks: number, passed: boolean, matches: number}|{passed: false})} 39 | */ 40 | evaluate(item) { 41 | return this.NO_MATCH_RESULT; 42 | } 43 | 44 | _createResult(checks, matches) { 45 | let passed = matches > 0, 46 | score = passed ? (checks / matches) * this._weight:0; 47 | return {matches, checks, score, passed}; 48 | } 49 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldContains.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldContains extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this.NO_MATCH_RESULT; 12 | 13 | let search = this._value.toLowerCase(); 14 | 15 | let matches = values.reduce((matches, value) => { 16 | return matches + this._countOccurrencesInString(value, search); 17 | }, 0); 18 | 19 | return this._createResult(values.length, matches); 20 | } 21 | 22 | _countOccurrencesInString(string, search) { 23 | if(search.length > string.length || search.length === 0) return 0; 24 | 25 | let matches = 0, 26 | position = 0; 27 | 28 | while(true) { 29 | position = string.indexOf(search, position); 30 | if(position >= 0) { 31 | matches++; 32 | position += search.length; 33 | } else { 34 | break; 35 | } 36 | } 37 | 38 | return matches; 39 | } 40 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldEquals.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldEquals extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this.NO_MATCH_RESULT; 12 | 13 | let search = this._value; 14 | if(typeof search === 'string' && this._name !== 'password') { 15 | search = this._value.toLowerCase(); 16 | } 17 | 18 | for(let value of values) { 19 | if(value === search) return this._createResult(1, 1); 20 | } 21 | 22 | return this.NO_MATCH_RESULT; 23 | } 24 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldGreater.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldGreater extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this.NO_MATCH_RESULT; 12 | 13 | let search = this._value; 14 | if(typeof search === 'string' && this._name !== 'password') { 15 | search = this._value.toLowerCase(); 16 | } 17 | 18 | for(let value of values) { 19 | if(value > search) return this._createResult(1, 1); 20 | } 21 | 22 | return this.NO_MATCH_RESULT; 23 | } 24 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldGreaterOrEquals.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldGreaterOrEquals extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this.NO_MATCH_RESULT; 12 | 13 | let search = this._value; 14 | if(typeof search === 'string' && this._name !== 'password') { 15 | search = this._value.toLowerCase(); 16 | } 17 | 18 | for(let value of values) { 19 | if(value >= search) return this._createResult(1, 1); 20 | } 21 | 22 | return this.NO_MATCH_RESULT; 23 | } 24 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldIn.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldIn extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this.NO_MATCH_RESULT; 12 | 13 | for(let value of values) { 14 | if(this._value.indexOf(value) !== -1) { 15 | return this._createResult(1, 1); 16 | } 17 | } 18 | 19 | return this.NO_MATCH_RESULT; 20 | } 21 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldMatches.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldMatches extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this.NO_MATCH_RESULT; 12 | 13 | let regexp = new RegExp(this._value, 'g'); 14 | for(let value of values) { 15 | let result = regexp.exec(value); 16 | if(result.length > 1) return this._createResult(1, 1); 17 | } 18 | 19 | return this.NO_MATCH_RESULT; 20 | } 21 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldNotContains.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldNotContains extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this._createResult(1, 1); 12 | 13 | let search = this._value.toLowerCase(); 14 | for(let value of values) { 15 | if(value.indexOf(search) !== -1) { 16 | return this.NO_MATCH_RESULT; 17 | } 18 | } 19 | 20 | return this._createResult(1, 1); 21 | } 22 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldNotEquals.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldNotEquals extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this._createResult(1, 1); 12 | 13 | let search = this._value.toLowerCase(); 14 | for(let value of values) { 15 | if(value === search) return this.NO_MATCH_RESULT; 16 | } 17 | 18 | return this._createResult(1, 1); 19 | } 20 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldNotIn.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldNotIn extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this._createResult(1, 1); 12 | 13 | for(let value of values) { 14 | if(this._value.indexOf(value) !== -1) { 15 | return this.NO_MATCH_RESULT; 16 | } 17 | } 18 | 19 | return this._createResult(1, 1); 20 | } 21 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldNotMatches.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldNotMatches extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this._createResult(1, 1); 12 | 13 | let regexp = new RegExp(this._value, 'g'); 14 | for(let value of values) { 15 | let matches = regexp.exec(value); 16 | 17 | if(matches.length > 1) { 18 | return this.NO_MATCH_RESULT; 19 | } 20 | } 21 | 22 | return this._createResult(1, 1); 23 | } 24 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldSmaller.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldSmaller extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this.NO_MATCH_RESULT; 12 | 13 | let search = this._value; 14 | if(typeof search === 'string' && this._name !== 'password') { 15 | search = this._value.toLowerCase(); 16 | } 17 | 18 | for(let value of values) { 19 | if(value < search) return this._createResult(1, 1); 20 | } 21 | 22 | return this.NO_MATCH_RESULT; 23 | } 24 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldSmallerOrEquals.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldSmallerOrEquals extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this.NO_MATCH_RESULT; 12 | 13 | let search = this._value; 14 | if(typeof search === 'string' && this._name !== 'password') { 15 | search = this._value.toLowerCase(); 16 | } 17 | 18 | for(let value of values) { 19 | if(value <= search) return this._createResult(1, 1); 20 | } 21 | 22 | return this.NO_MATCH_RESULT; 23 | } 24 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Field/FieldStartsWith.js: -------------------------------------------------------------------------------- 1 | import AbstractField from '@js/Search/Query/Field/AbstractField'; 2 | 3 | export default class FieldStartsWith extends AbstractField { 4 | 5 | /** 6 | * @inheritDoc 7 | */ 8 | evaluate(item) { 9 | let values = item.getField(this._name); 10 | 11 | if(values === null) return this.NO_MATCH_RESULT; 12 | 13 | let search = this._value.toLowerCase(); 14 | for(let value of values) { 15 | if(value.startsWith(search)) { 16 | return this._createResult(1, 1); 17 | } 18 | } 19 | 20 | return this.NO_MATCH_RESULT; 21 | } 22 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Pagination/AbstractPagination.js: -------------------------------------------------------------------------------- 1 | export default class AbstractPagination { 2 | apply(items) { 3 | return items; 4 | 5 | } 6 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Pagination/Limit.js: -------------------------------------------------------------------------------- 1 | import AbstractPagination from "@js/Search/Query/Pagination/AbstractPagination"; 2 | 3 | export default class Limit extends AbstractPagination { 4 | 5 | constructor(size) { 6 | super(); 7 | this._size = size; 8 | } 9 | 10 | apply(items) { 11 | if(this._size > items.length) { 12 | return items; 13 | } 14 | 15 | return items.splice(0, this._size); 16 | } 17 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Pagination/Pagination.js: -------------------------------------------------------------------------------- 1 | import AbstractPagination from "@js/Search/Query/Pagination/AbstractPagination"; 2 | 3 | export default class Pagination extends AbstractPagination { 4 | 5 | constructor(page, size) { 6 | super(); 7 | this._page = page; 8 | this._size = size; 9 | } 10 | 11 | apply(items) { 12 | let start = this._page * this._size; 13 | 14 | if(start > items.length) { 15 | return []; 16 | } 17 | 18 | return items.splice(start, this._size); 19 | } 20 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Sort/AbstractSort.js: -------------------------------------------------------------------------------- 1 | export default class AbstractSort { 2 | 3 | /** 4 | * 5 | * @param {String} field 6 | */ 7 | constructor(field) { 8 | this._field = field; 9 | } 10 | 11 | /** 12 | * 13 | * @param {Match} a 14 | * @param {Match} b 15 | * @return {Number} 16 | */ 17 | compare(a, b) { 18 | let valueA = this._getFieldValue(a), 19 | valueB = this._getFieldValue(b); 20 | 21 | return this._compareValues(valueA, valueB); 22 | } 23 | 24 | /** 25 | * 26 | * @param {*} a 27 | * @param {*} b 28 | * @return {Number} 29 | * @private 30 | */ 31 | _compareValues(a, b) { 32 | return 0; 33 | } 34 | 35 | /** 36 | * 37 | * @param {Match} item 38 | * @return {*} 39 | * @private 40 | */ 41 | _getFieldValue(item) { 42 | let values = item.getField(this._field); 43 | if (values === null || values.length === 0) return null; 44 | if (values.length === 1) return values.first(); 45 | 46 | return typeof values[0] === 'number' ? values.reduce((a, c) => a + c, 0) : values.join(); 47 | } 48 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Sort/CustomSort.js: -------------------------------------------------------------------------------- 1 | export default class CustomSort { 2 | 3 | /** 4 | * 5 | * @param {Function} callback 6 | */ 7 | constructor(callback) { 8 | this._callback = callback; 9 | } 10 | 11 | /** 12 | * 13 | * @param {Match} a 14 | * @param {Match} b 15 | * @return {Number} 16 | */ 17 | compare(a, b) { 18 | return this._callback(a, b); 19 | } 20 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Sort/SortAscending.js: -------------------------------------------------------------------------------- 1 | import AbstractSort from '@js/Search/Query/Sort/AbstractSort'; 2 | 3 | export default class SortAscending extends AbstractSort { 4 | 5 | _compareValues(a, b) { 6 | if(a === b) return 0; 7 | if(typeof a === 'string') { 8 | if(b === null) return -1; 9 | return a.localeCompare(b, undefined, {numeric: true, sensitivity: 'base'}); 10 | } 11 | return a < b ? -1:1; 12 | } 13 | } -------------------------------------------------------------------------------- /src/js/Search/Query/Sort/SortDescending.js: -------------------------------------------------------------------------------- 1 | import AbstractSort from '@js/Search/Query/Sort/AbstractSort'; 2 | 3 | export default class SortDescending extends AbstractSort { 4 | 5 | _compareValues(a, b) { 6 | if(a === b) return 0; 7 | if(typeof a === 'string') { 8 | if(b === null) return 1; 9 | return b.localeCompare(a, undefined, {numeric: true, sensitivity: 'base'}); 10 | } 11 | return b < a ? -1:1; 12 | } 13 | } -------------------------------------------------------------------------------- /src/js/Services/LocalisationService.js: -------------------------------------------------------------------------------- 1 | import SystemService from "@js/Services/SystemService"; 2 | import SettingsService from "@js/Services/SettingsService"; 3 | 4 | class LocalisationService { 5 | 6 | constructor() { 7 | this._browser = SystemService.getBrowserApi(); 8 | this._debug = null; 9 | } 10 | 11 | /** 12 | * 13 | */ 14 | init() { 15 | SettingsService 16 | .get('debug.localisation.enabled') 17 | .then((s) => { 18 | this._debug = s; 19 | }); 20 | } 21 | 22 | /** 23 | * 24 | * @param {(String|String[])} key 25 | * @param {(String|String[])} [variables] 26 | * @returns {String} 27 | */ 28 | translate(key, ...variables) { 29 | if(this._debug && !this._debug.getValue()) return key; 30 | if(Array.isArray(key)) { 31 | if(key.length < 0) return ''; 32 | 33 | variables = key.slice(1); 34 | key = key.slice(0, 1)[0]; 35 | } 36 | if(Array.isArray(variables[0])) variables = variables[0]; 37 | let translation = this._browser.i18n.getMessage(key, variables); 38 | 39 | return translation ? translation:key; 40 | } 41 | 42 | /** 43 | * @returns {String} 44 | */ 45 | getLocale() { 46 | return this._browser.i18n.getMessage('locale'); 47 | } 48 | } 49 | 50 | export default new LocalisationService(); -------------------------------------------------------------------------------- /src/js/Services/PasswordStatisticsService.js: -------------------------------------------------------------------------------- 1 | import DatabaseService from "@js/Services/DatabaseService"; 2 | import {emit} from "@js/Event/Events"; 3 | import AutofillManager from "@js/Manager/AutofillManager"; 4 | 5 | export default new class PasswordStatisticsService { 6 | 7 | constructor() { 8 | this._table = null; 9 | } 10 | 11 | /** 12 | * @public 13 | * @return {Promise} 14 | */ 15 | async init() { 16 | let database = await DatabaseService.get(DatabaseService.STATISTICS_DATABASE); 17 | this._table = await database.use(DatabaseService.STATISTICS_PASSWORD_USAGE_TABLE); 18 | } 19 | 20 | /** 21 | * @public 22 | * @param {String} id 23 | * @param {(String|null)} domain 24 | * @return {void} 25 | */ 26 | registerPasswordUse(id, domain = null) { 27 | let entry = this._table.get(id); 28 | 29 | if (entry === null) { 30 | entry = {total: 0, domains: {}}; 31 | } 32 | entry.total++; 33 | 34 | if (domain !== null) { 35 | if (!entry.domains.hasOwnProperty(domain)) { 36 | entry.domains[domain] = 0; 37 | } 38 | entry.domains[domain]++; 39 | } 40 | 41 | this._table.set(id, entry); 42 | AutofillManager.toggle(false); 43 | emit('password:statistics:updated', {id, entry}); 44 | AutofillManager.toggle(true); 45 | } 46 | 47 | /** 48 | * @public 49 | * @param {String} id 50 | * @return {Object} 51 | */ 52 | getPasswordUses(id) { 53 | let entry = this._table.get(id); 54 | 55 | if (entry === null) { 56 | return {total: 0, domains: {}}; 57 | } 58 | 59 | return entry; 60 | } 61 | }; -------------------------------------------------------------------------------- /src/js/Services/QueueService.js: -------------------------------------------------------------------------------- 1 | import Queue from '@js/Queue/Queue'; 2 | import FeedbackQueue from '@js/Queue/FeedbackQueue'; 3 | import QueueItem from "@js/Models/Queue/QueueItem"; 4 | import FeedbackItem from "@js/Models/Queue/FeedbackItem"; 5 | 6 | class QueueService { 7 | 8 | constructor() { 9 | this._queues = {}; 10 | } 11 | 12 | /** 13 | * 14 | * @param {String} name 15 | * @param {(String|null)} [area=null] 16 | * @param type 17 | * @returns {Queue} 18 | */ 19 | getQueue(name, area = null, type = QueueItem) { 20 | if(!this._queues.hasOwnProperty(name)) { 21 | this._queues[name] = new Queue(name, area, type); 22 | } 23 | 24 | return this._queues[name]; 25 | } 26 | 27 | /** 28 | * 29 | * @param {String} name 30 | * @param {(String|null)} [area=null] 31 | * @param {FeedbackItem} [type=FeedbackItem] 32 | * @returns {FeedbackQueue} 33 | */ 34 | getFeedbackQueue(name, area = null, type = FeedbackItem) { 35 | if(!this._queues.hasOwnProperty(name)) { 36 | this._queues[name] = new FeedbackQueue(name, area, type); 37 | } 38 | 39 | return this._queues[name]; 40 | } 41 | 42 | /** 43 | * 44 | * @param {String} name 45 | * @return {Boolean} 46 | */ 47 | hasQueue(name) { 48 | return this._queues.hasOwnProperty(name); 49 | } 50 | } 51 | 52 | export default new QueueService(); -------------------------------------------------------------------------------- /src/js/Services/RegistryService.js: -------------------------------------------------------------------------------- 1 | class RegistryService { 2 | 3 | constructor() { 4 | this.registry = []; 5 | } 6 | 7 | /** 8 | * @param {String} key 9 | * @returns {boolean} 10 | */ 11 | has(key) { 12 | return this.registry.hasOwnProperty(key); 13 | } 14 | 15 | /** 16 | * @param {String} key 17 | * @returns {*} 18 | */ 19 | get(key) { 20 | if(this.registry.hasOwnProperty(key)) { 21 | return this.registry[key]; 22 | } 23 | return undefined; 24 | } 25 | 26 | /** 27 | * @param {String} key 28 | * @param {*} value 29 | */ 30 | set(key, value) { 31 | this.registry[key] = value; 32 | } 33 | 34 | /** 35 | * @param {String} key 36 | */ 37 | remove(key) { 38 | if(this.has(key)) { 39 | delete this.registry[key]; 40 | } 41 | } 42 | } 43 | 44 | export default new RegistryService(); -------------------------------------------------------------------------------- /src/js/Settings/ClientSettingsProvider.js: -------------------------------------------------------------------------------- 1 | import MessageService from '@js/Services/MessageService'; 2 | import Error from "@vue/Components/Passlink/Action/Error.vue"; 3 | 4 | class ClientSettingsProvider { 5 | 6 | get withServer() { 7 | return false; 8 | } 9 | 10 | /** 11 | * @param {String} name 12 | * @return {Promise<{value:*, scope:String}>} 13 | */ 14 | async get(name) { 15 | /** @type {Message} **/ 16 | let reply = await MessageService.send({type: 'setting.get', payload: name}); 17 | if(reply.getType() !== 'setting.value') { 18 | throw new Error(reply.getPayload().message); 19 | } 20 | 21 | return reply.getPayload(); 22 | } 23 | 24 | /** 25 | * 26 | * @param {String} name 27 | * @param {*} value 28 | * @return {Promise} 29 | */ 30 | async set(name, value) { 31 | let reply = await MessageService.send({type: 'setting.set', payload: {setting: name, value}}); 32 | if(!reply.getPayload().success) { 33 | throw new Error(reply.getPayload().message); 34 | } 35 | } 36 | 37 | /** 38 | * @param {String} name 39 | * @return {Promise} 40 | */ 41 | async reset(name) { 42 | let reply = await MessageService.send({type: 'setting.reset', payload: name}); 43 | 44 | if(reply.getType() !== 'setting.value') { 45 | throw new Error(reply.getPayload().message); 46 | } 47 | 48 | return reply.getPayload(); 49 | } 50 | 51 | async setForServer() { 52 | throw new Error('setForServer is not supported'); 53 | } 54 | 55 | async getForServer() { 56 | throw new Error('getForServer is not supported'); 57 | } 58 | 59 | async resetForServer() { 60 | throw new Error('resetForServer is not supported'); 61 | } 62 | } 63 | 64 | export default new ClientSettingsProvider(); -------------------------------------------------------------------------------- /src/js/Themes/AdaptaLight.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "adapta-light", 3 | "label": "ThemeAdaptaLight", 4 | "type" : "system", 5 | "badge" : { 6 | "color-bg": "#5294E2", 7 | "color-fg": "#FFFFFF", 8 | "icon" : "passwords-new-dark" 9 | }, 10 | "font" : { 11 | "family": "default", 12 | "size" : "d" 13 | }, 14 | "colors": { 15 | "element-bg" : "#F5F6F7", 16 | "element-fg" : "#5C616C", 17 | "element-hover-bg" : "#FFFFFF", 18 | "element-hover-fg" : "#5C616C", 19 | "element-active-bg" : "#FFFFFF", 20 | "element-active-fg" : "#5294E2", 21 | "element-active-hover-bg": "#FFFFFF", 22 | "element-active-hover-fg": "#5294E2", 23 | "button-bg" : "inherit", 24 | "button-fg" : "inherit", 25 | "button-hover-bg" : "#5294E2", 26 | "button-hover-fg" : "#FFFFFF", 27 | "slider-bg" : "#FFFFFF", 28 | "slider-fg" : "#5294E2", 29 | "slider-br" : "#5294E2", 30 | "slider-active-bg" : "#5294E2", 31 | "slider-active-fg" : "#FFFFFF", 32 | "slider-active-br" : "#5294E2", 33 | "info-bg" : "#0c63db", 34 | "info-fg" : "#ffffff", 35 | "warning-bg" : "#eca700", 36 | "warning-fg" : "#ffffff", 37 | "error-bg" : "#e9322d", 38 | "error-fg" : "#ffffff", 39 | "success-bg" : "#46ba61", 40 | "success-fg" : "#ffffff" 41 | } 42 | } -------------------------------------------------------------------------------- /src/js/Themes/AdaptaTeal.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "adapta-teal", 3 | "label": "ThemeAdaptaTeal", 4 | "type" : "system", 5 | "badge" : { 6 | "color-bg": "#37EE7C", 7 | "color-fg": "#F9F9FA", 8 | "icon" : "passwords-new-light" 9 | }, 10 | "font" : { 11 | "family": "default", 12 | "size" : "d" 13 | }, 14 | "colors": { 15 | "element-bg" : "#202023", 16 | "element-fg" : "#858586", 17 | "element-hover-bg" : "#263238", 18 | "element-hover-fg" : "#F9F9FA", 19 | "element-active-bg" : "#263238", 20 | "element-active-fg" : "#37EE7C", 21 | "element-active-hover-bg": "#263238", 22 | "element-active-hover-fg": "#37EE7C", 23 | "button-bg" : "inherit", 24 | "button-fg" : "inherit", 25 | "button-hover-bg" : "#37EE7C", 26 | "button-hover-fg" : "#F9F9FA", 27 | "slider-bg" : "#263238", 28 | "slider-fg" : "#37EE7C", 29 | "slider-br" : "#37EE7C", 30 | "slider-active-bg" : "#37EE7C", 31 | "slider-active-fg" : "#263238", 32 | "slider-active-br" : "#37EE7C", 33 | "info-bg" : "#0c63db", 34 | "info-fg" : "#ffffff", 35 | "warning-bg" : "#eca700", 36 | "warning-fg" : "#ffffff", 37 | "error-bg" : "#e9322d", 38 | "error-fg" : "#ffffff", 39 | "success-bg" : "#46ba61", 40 | "success-fg" : "#ffffff", 41 | "modal-bg" : "#00000040" 42 | } 43 | } -------------------------------------------------------------------------------- /src/js/Themes/ArcDark.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "arc-darker", 3 | "label": "ThemeArcDark", 4 | "type" : "system", 5 | "badge" : { 6 | "color-bg": "#5296E5", 7 | "color-fg": "#D3DAE3", 8 | "icon" : "passwords-new-light" 9 | }, 10 | "font" : { 11 | "family": "default", 12 | "size" : "d" 13 | }, 14 | "colors": { 15 | "element-bg" : "#2F343F", 16 | "element-fg" : "#ABB4C2", 17 | "element-hover-bg" : "#383C4A", 18 | "element-hover-fg" : "#ABB4C2", 19 | "element-active-bg" : "#383C4A", 20 | "element-active-fg" : "#5296E5", 21 | "element-active-hover-bg": "#383C4A", 22 | "element-active-hover-fg": "#5296E5", 23 | "button-bg" : "inherit", 24 | "button-fg" : "inherit", 25 | "button-hover-bg" : "#5296E5", 26 | "button-hover-fg" : "#D3DAE3", 27 | "slider-bg" : "#383C4A", 28 | "slider-fg" : "#ABB4C2", 29 | "slider-br" : "#ABB4C2", 30 | "slider-active-bg" : "#5296E5", 31 | "slider-active-fg" : "#2F343F", 32 | "slider-active-br" : "#5296E5", 33 | "info-bg" : "#0c63db", 34 | "info-fg" : "#ffffff", 35 | "warning-bg" : "#eca700", 36 | "warning-fg" : "#ffffff", 37 | "error-bg" : "#e9322d", 38 | "error-fg" : "#ffffff", 39 | "success-bg" : "#46ba61", 40 | "success-fg" : "#ffffff", 41 | "modal-bg" : "#00000040" 42 | } 43 | } -------------------------------------------------------------------------------- /src/js/Themes/ArcLight.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "arc-light", 3 | "label": "ThemeArcLight", 4 | "type" : "system", 5 | "badge" : { 6 | "color-bg": "#5294E2", 7 | "color-fg": "#F5F6F7", 8 | "icon" : "passwords-new-dark" 9 | }, 10 | "font" : { 11 | "family": "default", 12 | "size" : "d" 13 | }, 14 | "colors": { 15 | "element-bg" : "#F5F6F7", 16 | "element-fg" : "#5C616C", 17 | "element-hover-bg" : "#E7E8EB", 18 | "element-hover-fg" : "#5C616C", 19 | "element-active-bg" : "#E7E8EB", 20 | "element-active-fg" : "#5294E2", 21 | "element-active-hover-bg": "#E7E8EB", 22 | "element-active-hover-fg": "#5294E2", 23 | "button-bg" : "inherit", 24 | "button-fg" : "inherit", 25 | "button-hover-bg" : "#5294E2", 26 | "button-hover-fg" : "#F5F6F7", 27 | "slider-bg" : "#E7E8EB", 28 | "slider-fg" : "#5C616C", 29 | "slider-br" : "#5C616C", 30 | "slider-active-bg" : "#5294E2", 31 | "slider-active-fg" : "#F5F6F7", 32 | "slider-active-br" : "#5294E2", 33 | "info-bg" : "#0c63db", 34 | "info-fg" : "#ffffff", 35 | "warning-bg" : "#eca700", 36 | "warning-fg" : "#ffffff", 37 | "error-bg" : "#e9322d", 38 | "error-fg" : "#ffffff", 39 | "success-bg" : "#46ba61", 40 | "success-fg" : "#ffffff" 41 | } 42 | } -------------------------------------------------------------------------------- /src/js/Themes/Dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "dark", 3 | "label" : "ThemeDark", 4 | "type" : "system", 5 | "badge" : { 6 | "color-bg": "#0c63db", 7 | "color-fg": "#d8d8d8", 8 | "icon" : null 9 | }, 10 | "font" : { 11 | "family": "default", 12 | "size" : "d" 13 | }, 14 | "colors": { 15 | "element-bg" : "#181818", 16 | "element-fg" : "#d8d8d8", 17 | "element-hover-bg" : "#222222", 18 | "element-hover-fg" : "#d8d8d8", 19 | "element-active-bg" : "#181818", 20 | "element-active-fg" : "#0c63db", 21 | "element-active-hover-bg": "#222222", 22 | "element-active-hover-fg": "#0c63db", 23 | "button-bg" : "inherit", 24 | "button-fg" : "inherit", 25 | "button-hover-bg" : "#0c63db", 26 | "button-hover-fg" : "#181818", 27 | "slider-bg" : "#363639", 28 | "slider-fg" : "#ffffff", 29 | "slider-br" : "#323335", 30 | "slider-active-bg" : "#0060df", 31 | "slider-active-fg" : "#ffffff", 32 | "slider-active-br" : "#003eaa", 33 | "info-bg" : "#0c63db", 34 | "info-fg" : "#ffffff", 35 | "warning-bg" : "#eca700", 36 | "warning-fg" : "#ffffff", 37 | "error-bg" : "#e9322d", 38 | "error-fg" : "#ffffff", 39 | "success-bg" : "#46ba61", 40 | "success-fg" : "#ffffff", 41 | "modal-bg" : "#00000040" 42 | } 43 | } -------------------------------------------------------------------------------- /src/js/Themes/Hacker.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "hacker", 3 | "label" : "ThemeHacker", 4 | "style" : true, 5 | "type" : "system", 6 | "badge" : { 7 | "color-bg": "#00ff00", 8 | "color-fg": "#000000", 9 | "icon" : "passwords-light" 10 | }, 11 | "font" : { 12 | "family": "mono", 13 | "size" : "d" 14 | }, 15 | "colors" : { 16 | "element-bg" : "#000000", 17 | "element-fg" : "#00ff00", 18 | "element-hover-bg" : "#00ff00", 19 | "element-hover-fg" : "#000000", 20 | "element-active-bg" : "#00ff00", 21 | "element-active-fg" : "#000000", 22 | "element-active-hover-bg": "#00ff00", 23 | "element-active-hover-fg": "#000000", 24 | "button-bg" : "inherit", 25 | "button-fg" : "inherit", 26 | "button-hover-bg" : "#00ff00", 27 | "button-hover-fg" : "#000000", 28 | "slider-bg" : "#000000", 29 | "slider-fg" : "#00ff00", 30 | "slider-br" : "#00ff00", 31 | "slider-active-bg" : "#00ff00", 32 | "slider-active-fg" : "#000000", 33 | "slider-active-br" : "#00ff00", 34 | "info-bg" : "#0000ff", 35 | "info-fg" : "#ffffff", 36 | "warning-bg" : "#ffff00", 37 | "warning-fg" : "#000000", 38 | "error-bg" : "#ff0000", 39 | "error-fg" : "#000000", 40 | "success-bg" : "#00ff00", 41 | "success-fg" : "#000000", 42 | "modal-bg" : "#00000040" 43 | }, 44 | "variables": { 45 | "tab-active-border": "var(--tab-border)" 46 | } 47 | } -------------------------------------------------------------------------------- /src/js/Themes/Light.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "light", 3 | "label" : "ThemeLight", 4 | "type" : "system", 5 | "badge" : { 6 | "color-bg": "#0652dd", 7 | "color-fg": "#ffffff", 8 | "icon" : null 9 | }, 10 | "font" : { 11 | "family": "default", 12 | "size" : "d" 13 | }, 14 | "colors": { 15 | "element-bg" : "#ffffff", 16 | "element-fg" : "#000000", 17 | "element-hover-bg" : "#ededed", 18 | "element-hover-fg" : "#000000", 19 | "element-active-bg" : "#ffffff", 20 | "element-active-fg" : "#0996f8", 21 | "element-active-hover-bg": "#ededed", 22 | "element-active-hover-fg": "#0670cc", 23 | "button-bg" : "inherit", 24 | "button-fg" : "inherit", 25 | "button-hover-bg" : "#0996f8", 26 | "button-hover-fg" : "#ffffff", 27 | "slider-bg" : "#eeeeee", 28 | "slider-fg" : "#ffffff", 29 | "slider-br" : "#dcdddd", 30 | "slider-active-bg" : "#0060df", 31 | "slider-active-fg" : "#ffffff", 32 | "slider-active-br" : "#003eaa", 33 | "info-bg" : "#0652dd", 34 | "info-fg" : "#ffffff", 35 | "warning-bg" : "#ffc312", 36 | "warning-fg" : "#ffffff", 37 | "error-bg" : "#ff3f34", 38 | "error-fg" : "#ffffff", 39 | "success-bg" : "#05c46b", 40 | "success-fg" : "#ffffff" 41 | } 42 | } -------------------------------------------------------------------------------- /src/js/Themes/OledDark.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "oled-dark", 3 | "label": "ThemeOledDark", 4 | "type" : "system", 5 | "badge" : { 6 | "color-bg": "#0c63db", 7 | "color-fg": "#d8d8d8", 8 | "icon" : null 9 | }, 10 | "font" : { 11 | "family": "default", 12 | "size" : "d" 13 | }, 14 | "colors": { 15 | "element-bg" : "#000000", 16 | "element-fg" : "#d8d8d8", 17 | "element-hover-bg" : "#000000", 18 | "element-hover-fg" : "#ffffff", 19 | "element-active-bg" : "#000000", 20 | "element-active-fg" : "#0c63db", 21 | "element-active-hover-bg": "#000000", 22 | "element-active-hover-fg": "#0c63db", 23 | "button-bg" : "inherit", 24 | "button-fg" : "inherit", 25 | "button-hover-bg" : "#0c63db", 26 | "button-hover-fg" : "#000000", 27 | "slider-bg" : "#363639", 28 | "slider-fg" : "#ffffff", 29 | "slider-br" : "#323335", 30 | "slider-active-bg" : "#0060df", 31 | "slider-active-fg" : "#ffffff", 32 | "slider-active-br" : "#003eaa", 33 | "info-bg" : "#0c63db", 34 | "info-fg" : "#ffffff", 35 | "warning-bg" : "#eca700", 36 | "warning-fg" : "#ffffff", 37 | "error-bg" : "#e9322d", 38 | "error-fg" : "#ffffff", 39 | "success-bg" : "#46ba61", 40 | "success-fg" : "#ffffff", 41 | "modal-bg" : "#00000040" 42 | } 43 | } -------------------------------------------------------------------------------- /src/js/Themes/RGB.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "rgb", 3 | "label" : "ThemeRGB", 4 | "style" : true, 5 | "type" : "system", 6 | "badge" : { 7 | "color-bg": "#0c63db", 8 | "color-fg": "#d8d8d8", 9 | "icon" : "passwords-light" 10 | }, 11 | "font" : { 12 | "family": "default", 13 | "size" : "d" 14 | }, 15 | "colors": { 16 | "element-bg" : "#181818", 17 | "element-fg" : "#d8d8d8", 18 | "element-hover-bg" : "#222222", 19 | "element-hover-fg" : "#d8d8d8", 20 | "element-active-bg" : "#181818", 21 | "element-active-fg" : "#0c63db", 22 | "element-active-hover-bg": "#222222", 23 | "element-active-hover-fg": "#0c63db", 24 | "button-bg" : "inherit", 25 | "button-fg" : "inherit", 26 | "button-hover-bg" : "#0c63db", 27 | "button-hover-fg" : "#181818", 28 | "slider-bg" : "#363639", 29 | "slider-fg" : "#ffffff", 30 | "slider-br" : "#323335", 31 | "slider-active-bg" : "#0060df", 32 | "slider-active-fg" : "#ffffff", 33 | "slider-active-br" : "#003eaa", 34 | "info-bg" : "#0c63db", 35 | "info-fg" : "#ffffff", 36 | "warning-bg" : "#eca700", 37 | "warning-fg" : "#ffffff", 38 | "error-bg" : "#e9322d", 39 | "error-fg" : "#ffffff", 40 | "success-bg" : "#46ba61", 41 | "success-fg" : "#ffffff", 42 | "modal-bg" : "#00000040" 43 | } 44 | } -------------------------------------------------------------------------------- /src/js/Themes/Server.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : "", 3 | "label" : "", 4 | "type" : "server", 5 | "badge" : { 6 | "color-bg": "#0082c9", 7 | "color-fg": "#ffffff" 8 | }, 9 | "font" : { 10 | "family": "default", 11 | "size" : "d" 12 | }, 13 | "colors": { 14 | "tabs-start" : "#0082c9", 15 | "tabs-end" : "#30b6ff", 16 | "tabs-fg" : "#ffffff80", 17 | "tabs-active-fg" : "#ffffff", 18 | "border" : "#dbdbdb", 19 | "element-bg" : "#ffffff", 20 | "element-fg" : "#000000C0", 21 | "element-hover-bg" : "#ededed", 22 | "element-hover-fg" : "#000000", 23 | "element-active-bg" : "#ededed", 24 | "element-active-fg" : "#0082c9", 25 | "element-active-hover-bg": "#ededed", 26 | "element-active-hover-fg": "#0082c9", 27 | "button-bg" : "inherit", 28 | "button-fg" : "inherit", 29 | "button-hover-bg" : "#0082c9", 30 | "button-hover-fg" : "#ffffff", 31 | "slider-bg" : "#eeeeee", 32 | "slider-fg" : "#ffffff", 33 | "slider-br" : "#dcdddd", 34 | "slider-active-bg" : "#0082c9", 35 | "slider-active-fg" : "#ffffff", 36 | "slider-active-br" : "#0082c9", 37 | "info-bg" : "#0097e6", 38 | "info-fg" : "#ffffff", 39 | "warning-bg" : "#ffc312", 40 | "warning-fg" : "#ffffff", 41 | "error-bg" : "#ff3f34", 42 | "error-fg" : "#ffffff", 43 | "success-bg" : "#05c46b", 44 | "success-fg" : "#ffffff" 45 | } 46 | } -------------------------------------------------------------------------------- /src/js/background.js: -------------------------------------------------------------------------------- 1 | import Background from '@js/App/Background'; 2 | import '@js/Prototype/prototype'; 3 | 4 | Background.init() 5 | .catch(console.error); -------------------------------------------------------------------------------- /src/js/client.js: -------------------------------------------------------------------------------- 1 | import Client from '@js/App/Client'; 2 | 3 | Client.init() 4 | .catch(console.error); 5 | -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | import Options from '@js/App/Options'; 2 | import '@js/Prototype/prototype'; 3 | 4 | // noinspection JSUnresolvedVariable 5 | __webpack_public_path__ = `/`; 6 | 7 | Options.init() 8 | .catch(console.error); 9 | -------------------------------------------------------------------------------- /src/js/passlink.js: -------------------------------------------------------------------------------- 1 | import Passlink from '@js/App/Passlink'; 2 | 3 | // noinspection JSUnresolvedVariable 4 | __webpack_public_path__ = `/`; 5 | 6 | Passlink.init() 7 | .catch(console.error); 8 | -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | import Popup from '@js/App/Popup'; 2 | import '@js/Prototype/prototype'; 3 | 4 | // noinspection JSUnresolvedVariable 5 | __webpack_public_path__ = `/`; 6 | 7 | Popup.init() 8 | .catch(console.error); 9 | -------------------------------------------------------------------------------- /src/js/preview.js: -------------------------------------------------------------------------------- 1 | import Preview from '@js/App/Preview'; 2 | import '@js/Prototype/prototype'; 3 | 4 | // noinspection JSUnresolvedVariable 5 | __webpack_public_path__ = `/`; 6 | 7 | Preview.init() 8 | .catch(console.error); -------------------------------------------------------------------------------- /src/platform/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version" : 2, 3 | "name" : "__MSG_extensionName__", 4 | "version" : "X.X.X.99999", 5 | "version_name" : "X.X.X", 6 | "description" : "__MSG_extensionDescription__", 7 | "author" : "Marius Wieschollek", 8 | "default_locale" : "en", 9 | "minimum_chrome_version" : "92", 10 | "icons" : { 11 | "16" : "img/passwords-16.png", 12 | "32" : "img/passwords-32.png", 13 | "48" : "img/passwords-48.png", 14 | "96" : "img/passwords-96.png", 15 | "128": "img/passwords-128.png" 16 | }, 17 | "browser_action" : { 18 | "default_icon" : "img/passwords-16.png", 19 | "default_title": "__MSG_browserActionTitle__", 20 | "default_popup": "html/popup.html", 21 | "browser_style": true, 22 | "theme_icons" : [ 23 | { 24 | "light": "img/passwords-light.png", 25 | "dark" : "img/passwords-dark.png", 26 | "size" : 16 27 | } 28 | ] 29 | }, 30 | "background" : { 31 | "scripts": [ 32 | "js/background.js" 33 | ] 34 | }, 35 | "options_ui" : { 36 | "page": "html/options.html" 37 | }, 38 | "content_security_policy": "script-src 'self' 'wasm-eval' blob:; object-src 'self'", 39 | "content_scripts" : [ 40 | { 41 | "matches" : [ 42 | "http://*/*", 43 | "https://*/*" 44 | ], 45 | "js" : [ "js/client.js" ], 46 | "all_frames": true 47 | } 48 | ], 49 | "permissions" : [ 50 | "*://*/*", 51 | "tabs", 52 | "storage", 53 | "clipboardWrite", 54 | "contextMenus", 55 | "notifications", 56 | "webRequest", 57 | "webRequestBlocking" 58 | ], 59 | "optional_permissions" : [ 60 | "clipboardRead" 61 | ], 62 | "commands": { 63 | "_execute_browser_action": { 64 | "suggested_key": { 65 | "default": "Alt+Shift+0" 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/platform/chrome/scss/browser.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-size : 11pt; 3 | 4 | &.options { 5 | max-width : 800px; 6 | width : 800px; 7 | 8 | &.edge { 9 | max-width : 375px; 10 | width : 375px; 11 | } 12 | } 13 | 14 | &.popup { 15 | min-width : 360px; 16 | min-height : 360px; 17 | } 18 | 19 | &.mobile { 20 | &.popup { 21 | min-width : 360px; 22 | min-height : 360px; 23 | width : 100vw; 24 | margin : 0 auto 25 | } 26 | 27 | &.options { 28 | max-width : 360px; 29 | width : 100vw; 30 | margin : 0 auto; 31 | 32 | #options .theming { 33 | grid-template-columns : 1fr; 34 | } 35 | } 36 | } 37 | 38 | input:focus, 39 | input:active, 40 | button:focus, 41 | button:active { 42 | outline : none; 43 | } 44 | } 45 | 46 | #options { 47 | .account-form fieldset { 48 | display : block; 49 | 50 | input { 51 | padding : .5rem; 52 | width : 100%; 53 | } 54 | } 55 | 56 | .theming { 57 | grid-template-columns : auto 360px; 58 | 59 | .theme-preview { 60 | max-width : 360px; 61 | width : 100%; 62 | } 63 | 64 | .theme-colors { 65 | .color-setting { 66 | input { 67 | -webkit-appearance : none; 68 | height : 1.5rem; 69 | background-color : transparent; 70 | 71 | &::-webkit-color-swatch { 72 | border : none; 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | 81 | body.edge #options .theming { 82 | grid-template-columns : 1fr; 83 | grid-row-gap : 1rem; 84 | grid-column-gap : 0; 85 | padding : 0; 86 | 87 | .theme-settings { 88 | padding : 1rem; 89 | } 90 | 91 | .theme-preview { 92 | margin-left: .5rem; 93 | } 94 | } -------------------------------------------------------------------------------- /src/platform/fenix/scss/browser.scss: -------------------------------------------------------------------------------- 1 | .panel-section-tabs-button.browser-style { 2 | height : 100%; 3 | cursor : pointer; 4 | } 5 | 6 | .foldout-container .panel-section-tabs-button.browser-style { 7 | text-align : left; 8 | } 9 | 10 | button::-moz-focus-inner, 11 | input::-moz-focus-inner { 12 | border : 0; 13 | } 14 | 15 | body #passlink, 16 | body #passlink .passlink-scan-qr, 17 | body #passlink .passlink-connect { 18 | height: 92vh; 19 | } -------------------------------------------------------------------------------- /src/platform/fenix/updates.json: -------------------------------------------------------------------------------- 1 | { 2 | "addons": { 3 | "ncpasswords@mdns.eu": { 4 | "updates": [ 5 | { 6 | "version": "2.0.0.BUILD", 7 | "update_link": "ARTIFACT_URL", 8 | "browser_specific_settings": { 9 | "gecko": { "strict_min_version": "57" } 10 | } 11 | } 12 | ] 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/platform/firefox/scss/browser.scss: -------------------------------------------------------------------------------- 1 | .panel-section-tabs-button.browser-style { 2 | height : 100%; 3 | cursor : pointer; 4 | } 5 | 6 | .foldout-container .panel-section-tabs-button.browser-style { 7 | text-align : left; 8 | } 9 | 10 | button::-moz-focus-inner, 11 | input::-moz-focus-inner { 12 | border : 0; 13 | } -------------------------------------------------------------------------------- /src/platform/firefox/updates.json: -------------------------------------------------------------------------------- 1 | { 2 | "addons": { 3 | "ncpasswords@mdns.eu": { 4 | "updates": [ 5 | { 6 | "version": "VERSION", 7 | "update_link": "ARTIFACT_URL", 8 | "update_hash": "sha512:SHA_512", 9 | "update_info_url": "CHANGELOG_URL", 10 | "browser_specific_settings": { 11 | "gecko": { "strict_min_version": "57" } 12 | } 13 | } 14 | ] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/platform/generic/css/themes/hacker.css: -------------------------------------------------------------------------------- 1 | .mining-item { 2 | --element-active-fg-color : var(--element-fg-color); 3 | } 4 | 5 | .input-select, 6 | .input-select select { 7 | color : var(--element-hover-fg-color); 8 | } 9 | 10 | .input-select::after { 11 | opacity: 1; 12 | } 13 | 14 | .input-slider.on .input-slider-button { 15 | border-color : var(--slider-active-fg-color); 16 | } -------------------------------------------------------------------------------- /src/platform/generic/css/themes/server.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --tab-border : 0 -1px 0 var(--border-color) inset; 3 | --tab-active-border : 0 -1px 0 var(--border-color) inset; 4 | --tab-button-active-border : 0 -1px 0 var(--border-color) inset; 5 | --main-tab-border : none; 6 | --main-tab-active-border : 0 4px 0 var(--tabs-fg-color) inset; 7 | --main-tab-mobile-active-border : 0 -1px 0 var(--element-hover-bg-color) inset, 4px 0 0 var(--tabs-fg-color) inset; 8 | 9 | } 10 | 11 | body, 12 | body #manager > .tab-container > .tab-content { 13 | scrollbar-color : var(--tabs-end-color) var(--element-active-bg-color); 14 | } 15 | 16 | body #manager > .tab-container > .tabs { 17 | background-image : linear-gradient(40deg, var(--tabs-start-color) 0%, var(--tabs-end-color) 100%); 18 | } 19 | 20 | body #manager > .tab-container > .tabs > .tab { 21 | background : none; 22 | color : var(--tabs-fg-color); 23 | } 24 | 25 | body #manager > .tab-container > .tabs > .tab.active { 26 | background : none; 27 | color : var(--tabs-active-fg-color); 28 | } 29 | 30 | body .input-slider.off .input-slider-button { 31 | border-color : var(--slider-bg-color); 32 | } 33 | 34 | body .input-slider.on .input-slider-button { 35 | border-color : var(--slider-active-bg-color); 36 | } -------------------------------------------------------------------------------- /src/platform/generic/html/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nextcloud Password Client Settings 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/platform/generic/html/passlink.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nextcloud Password Client Popup 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/platform/generic/html/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nextcloud Password Client Popup 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/platform/generic/html/preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nextcloud Password Client Theme Preview 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/platform/generic/img/angle-down-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/platform/generic/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/background.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords-128.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords-16.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords-32.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords-48.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords-96.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords-dark.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords-light.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-new-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords-new-dark.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-new-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-new-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords-new-light.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-new-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords-new.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-new.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-outline-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords-outline-dark.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-outline-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-outline-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords-outline-light.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords-outline-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/platform/generic/img/passwords.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marius-wieschollek/passwords-webextension/049de1766f04a91e1238a17b38a65233e19acfdc/src/platform/generic/img/passwords.png -------------------------------------------------------------------------------- /src/platform/generic/img/passwords.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/scss/_base.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family : var(--font-family); 3 | font-size : var(--font-size); 4 | } 5 | 6 | body { 7 | font-family : var(--font-family); 8 | font-size : var(--font-size); 9 | line-height : 1.1em; 10 | background-color : var(--element-bg-color); 11 | color : var(--element-fg-color); 12 | margin : 0; 13 | display : block; 14 | box-sizing : border-box; 15 | scrollbar-width : thin; 16 | scrollbar-color : var(--element-active-fg-color) var(--element-active-bg-color); 17 | 18 | input, 19 | select, 20 | button, { 21 | font-family : var(--font-family); 22 | font-size : 1rem; 23 | } 24 | } -------------------------------------------------------------------------------- /src/scss/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family : "OpenDyslexic"; 3 | src : url('../../fonts/OpenDyslexic/OpenDyslexic-Regular.woff'); 4 | font-weight : normal; 5 | font-style : normal; 6 | } 7 | 8 | @font-face { 9 | font-family : "OpenDyslexic"; 10 | src : url('../../fonts/OpenDyslexic/OpenDyslexic-Bold.woff'); 11 | font-weight : bold; 12 | font-style : normal; 13 | } 14 | 15 | @font-face { 16 | font-family : "Lato Light"; 17 | src : url('../../fonts/Lato/Lato-Light.woff2'); 18 | font-weight : normal; 19 | font-style : normal; 20 | } 21 | 22 | @font-face { 23 | font-family : "Lato Light"; 24 | src : url('../../fonts/Lato/Lato-Medium.woff2'); 25 | font-weight : bold; 26 | font-style : normal; 27 | } -------------------------------------------------------------------------------- /src/scss/includes.scss: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | @import "fonts"; 3 | @import "theme"; -------------------------------------------------------------------------------- /src/vue/App/Passlink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | 33 | -------------------------------------------------------------------------------- /src/vue/Components/Browse/ServerInfo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/vue/Components/Browse/ServerProperty.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | 30 | -------------------------------------------------------------------------------- /src/vue/Components/Browse/ServerUrlProperty.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/vue/Components/Firstrun/Steps/SetupComplete.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | 26 | -------------------------------------------------------------------------------- /src/vue/Components/List/FolderList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | -------------------------------------------------------------------------------- /src/vue/Components/List/Item/Favicon.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/vue/Components/List/Item/Folder.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 27 | 28 | -------------------------------------------------------------------------------- /src/vue/Components/List/Item/ParentFolder.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 62 | 63 | -------------------------------------------------------------------------------- /src/vue/Components/List/PasswordList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | 29 | -------------------------------------------------------------------------------- /src/vue/Components/Passlink/Action/Connect.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 48 | 49 | -------------------------------------------------------------------------------- /src/vue/Components/Theming/BadgeIcon.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/vue/Components/Theming/CustomFontFamily.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/vue/Components/Theming/CustomFontSize.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/vue/Components/Tools/Debug.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 | -------------------------------------------------------------------------------- /src/vue/Components/Translate.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | --------------------------------------------------------------------------------