├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── API.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── karma.conf.js ├── package-lock.json ├── package.json ├── source ├── ArchiveManager.js ├── ArchiveTools.js ├── DropboxDatasource.js ├── EntryFinder.js ├── HashingTools.js ├── LocalStorageInterface.js ├── StorageInterface.js └── index.js ├── tests ├── .eslintrc ├── ArchiveManager.spec.js ├── ArchiveTools.spec.js ├── Buttercup.Archive.spec.js ├── Buttercup.vendor.spec.js ├── EntryFinder.spec.js ├── LocalStorageInterface.spec.js └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["lodash"], 3 | "presets": [ 4 | ["es2015", { "modules": false }] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | max_line_length = 0 13 | trim_trailing_whitespace = false 14 | 15 | [.eslintrc] 16 | [karma.conf.js] 17 | [.travis.yml] 18 | [package.json] 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true, 6 | "mocha": false, 7 | "jasmine": false 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 6, 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": false 14 | } 15 | }, 16 | "rules": { 17 | "comma-dangle": [2,"never"], 18 | "no-cond-assign": [2,"except-parens"], 19 | "no-console": 1, 20 | "no-constant-condition": 2, 21 | "no-control-regex": 2, 22 | "no-debugger": 2, 23 | "no-dupe-args": 2, 24 | "no-dupe-keys": 2, 25 | "no-duplicate-case": 2, 26 | "no-empty-character-class": 2, 27 | "no-empty": 2, 28 | "no-ex-assign": 2, 29 | "no-extra-boolean-cast": 2, 30 | "no-extra-parens": [2,"all"], 31 | "no-extra-semi": 2, 32 | "no-func-assign": 2, 33 | "no-inner-declarations": [2,"both"], 34 | "no-invalid-regexp": 2, 35 | "no-irregular-whitespace": 2, 36 | "no-negated-in-lhs": 2, 37 | "no-obj-calls": 2, 38 | "no-regex-spaces": 2, 39 | "no-sparse-arrays": 2, 40 | "no-unreachable": 2, 41 | "use-isnan": 2, 42 | "valid-jsdoc": [2,{"preferType":{"object":"Object","array":"Array","promise":"Promise","string":"String","number":"Number","boolean":"Boolean","bool":"Boolean","Null":"null"},"requireReturn":false,"requireReturnType":false,"requireParamDescription":true,"requireReturnDescription":true}], 43 | "valid-typeof": 2, 44 | "no-unexpected-multiline": 2, 45 | "accessor-pairs": [2,{"setWithoutGet":true}], 46 | "block-scoped-var": 2, 47 | "complexity": [1,6], 48 | "consistent-return": 2, 49 | "curly": [2,"all"], 50 | "default-case": 2, 51 | "dot-notation": 1, 52 | "dot-location": [2,"property"], 53 | "eqeqeq": 2, 54 | "guard-for-in": 2, 55 | "no-alert": 1, 56 | "no-caller": 2, 57 | "no-div-regex": 1, 58 | "no-else-return": 2, 59 | "no-labels": 2, 60 | "no-eq-null": 2, 61 | "no-eval": 2, 62 | "no-extend-native": 2, 63 | "no-extra-bind": 2, 64 | "no-fallthrough": 2, 65 | "no-floating-decimal": 2, 66 | "no-implicit-coercion": [2,{"boolean":true,"number":true,"string":true}], 67 | "no-implied-eval": 2, 68 | "no-invalid-this": 0, 69 | "no-iterator": 2, 70 | "no-lone-blocks": 2, 71 | "no-loop-func": 1, 72 | "no-multi-spaces": [2,{"exceptions":{"Property":true,"VariableDeclarator":true,"ImportDeclaration":false}}], 73 | "no-multi-str": 2, 74 | "no-native-reassign": 2, 75 | "no-new-func": 2, 76 | "no-new-wrappers": 2, 77 | "no-new": 2, 78 | "no-octal-escape": 2, 79 | "no-octal": 2, 80 | "no-param-reassign": [2,{"props":false}], 81 | "no-process-env": 1, 82 | "no-proto": 2, 83 | "no-redeclare": [2,{"builtinGlobals":true}], 84 | "no-return-assign": [2,"always"], 85 | "no-script-url": 1, 86 | "no-self-compare": 2, 87 | "no-sequences": 2, 88 | "no-throw-literal": 2, 89 | "no-unused-expressions": 2, 90 | "no-useless-call": 2, 91 | "no-void": 2, 92 | "no-warning-comments": [1,{"terms":["todo","fixme","@todo"],"location":"start"}], 93 | "no-with": 2, 94 | "radix": 2, 95 | "vars-on-top": 2, 96 | "wrap-iife": [2,"inside"], 97 | "yoda": [2,"never",{"exceptRange":true}], 98 | "no-catch-shadow": 2, 99 | "no-delete-var": 2, 100 | "no-label-var": 2, 101 | "no-shadow-restricted-names": 2, 102 | "no-shadow": [2,{"builtinGlobals":false,"hoist":"all"}], 103 | "no-undef-init": 2, 104 | "no-undef": 2, 105 | "no-undefined": 2, 106 | "no-unused-vars": 2, 107 | "no-use-before-define": 2, 108 | "callback-return": 2, 109 | "handle-callback-err": [2,"error"], 110 | "no-mixed-requires": 2, 111 | "no-new-require": 2, 112 | "no-path-concat": 2, 113 | "no-process-exit": 1, 114 | "brace-style": 2, 115 | "camelcase": [2,{"properties":"always"}], 116 | "comma-spacing": [2,{"after":true}], 117 | "comma-style": [2,"last"], 118 | "computed-property-spacing": [1,"never"], 119 | "consistent-this": [1,"_this"], 120 | "eol-last": 2, 121 | "func-names": 1, 122 | "indent": [2, 4, {"SwitchCase": 1}], 123 | "key-spacing": [2,{"afterColon":true}], 124 | "linebreak-style": [2,"unix"], 125 | "new-cap": [2,{"newIsCap":true,"capIsNew":true}], 126 | "new-parens": 2, 127 | "no-array-constructor": 2, 128 | "no-lonely-if": 2, 129 | "no-mixed-spaces-and-tabs": 2, 130 | "no-multiple-empty-lines": 2, 131 | "no-nested-ternary": 1, 132 | "no-new-object": 2, 133 | "no-spaced-func": 2, 134 | "no-trailing-spaces": [2,{"skipBlankLines":true}], 135 | "operator-linebreak": [2,"after"], 136 | "padded-blocks": [2,"never"], 137 | "quote-props": [2,"as-needed"], 138 | "quotes": [2,"double","avoid-escape"], 139 | "semi-spacing": [2,{"after":true}], 140 | "semi": [2,"always"], 141 | "keyword-spacing": [2,{"before":true,"after":true}], 142 | "space-before-blocks": [2,"always"], 143 | "space-before-function-paren": [2,"never"], 144 | "space-in-parens": [2,"never"], 145 | "space-infix-ops": 2, 146 | "space-unary-ops": 2, 147 | "spaced-comment": [2,"always",{"block":{"balanced":true,"exceptions":["*"]}}], 148 | "arrow-spacing": [2,{"before":true,"after":true}], 149 | "constructor-super": 2, 150 | "generator-star-spacing": [2,{"before":true,"after":false}], 151 | "no-class-assign": 2, 152 | "no-const-assign": 2, 153 | "no-this-before-super": 2, 154 | "prefer-const": 1 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Dependency directory 6 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 7 | node_modules 8 | 9 | _SpecRunner.html 10 | .history 11 | .vscode 12 | 13 | build 14 | dist 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | source 2 | tests 3 | .babelrc 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | addons: 4 | firefox: latest 5 | apt: 6 | sources: 7 | - google-chrome 8 | packages: 9 | - google-chrome-stable 10 | - google-chrome-beta 11 | #before_install: 12 | before_script: 13 | - export DISPLAY=:99.0 14 | - sh -e /etc/init.d/xvfb start 15 | - sleep 3 # give xvfb some time to start 16 | #before_script: 17 | # - export NODE_PATH=./node_modules/buttercup/node_modules/:./node_modules/:$NODE_PATH 18 | script: 19 | - npm run test:ci 20 | language: node_js 21 | node_js: 22 | - "stable" 23 | notifications: 24 | webhooks: 25 | urls: 26 | - https://webhooks.gitter.im/e/c803777205006fc90eef 27 | on_success: change # options: [always|never|change] default: always 28 | on_failure: always # options: [always|never|change] default: always 29 | on_start: never # options: [always|never|change] default: always 30 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ## Classes 2 | 3 |
4 |
ArchiveManager
5 |

Archive Manager - manages a set of archives for the browser

6 |
7 |
DropboxDatasourceTextDatasource
8 |

Datasource for Dropbox archives

9 |
10 |
EntryFinder
11 |
12 |
LocalStorageInterfaceStorageInterface
13 |

Interface for localStorage

14 |
15 |
16 | 17 | ## Members 18 | 19 |
20 |
StorageInterface : Object
21 |
22 |
23 | 24 | ## Objects 25 | 26 |
27 |
ArchiveTools : object
28 |
29 |
30 | 31 | ## Functions 32 | 33 |
34 |
flattenEntries(archives)Array.<EntrySearchInfo>
35 |

Flatten entries into a searchable structure

36 |
37 |
38 | 39 | ## Typedefs 40 | 41 |
42 |
ArchiveDetailsDisplay : Object
43 |

Archive details for display

44 |
45 |
ManagedArchiveItem : Object
46 |

Stored archive entry

47 |
48 |
EntrySearchInfo : Object
49 |
50 |
51 | 52 | 53 | 54 | ## ArchiveManager 55 | Archive Manager - manages a set of archives for the browser 56 | 57 | **Kind**: global class 58 | 59 | * [ArchiveManager](#ArchiveManager) 60 | * [new ArchiveManager([storage])](#new_ArchiveManager_new) 61 | * _instance_ 62 | * [.archives](#ArchiveManager+archives) : Object 63 | * [.displayList](#ArchiveManager+displayList) : [Array.<ArchiveDetailsDisplay>](#ArchiveDetailsDisplay) 64 | * [.storage](#ArchiveManager+storage) : [StorageInterface](#StorageInterface) 65 | * [.unlockedArchives](#ArchiveManager+unlockedArchives) : [Array.<ManagedArchiveItem>](#ManagedArchiveItem) 66 | * [.addArchive(archiveName, workspace, credentials, masterPassword)](#ArchiveManager+addArchive) 67 | * [.isLocked(archiveName)](#ArchiveManager+isLocked) ⇒ Boolean 68 | * [.loadState()](#ArchiveManager+loadState) 69 | * [.lock(archiveName)](#ArchiveManager+lock) ⇒ Promise 70 | * [.removeArchive(archiveName)](#ArchiveManager+removeArchive) ⇒ Boolean 71 | * [.saveState()](#ArchiveManager+saveState) ⇒ Promise 72 | * [.unlock(archiveName, password)](#ArchiveManager+unlock) ⇒ Promise 73 | * [.updateUnlocked()](#ArchiveManager+updateUnlocked) ⇒ Promise 74 | * _static_ 75 | * [.ArchiveStatus](#ArchiveManager.ArchiveStatus) 76 | * [.getSharedManager()](#ArchiveManager.getSharedManager) ⇒ [ArchiveManager](#ArchiveManager) 77 | 78 | 79 | 80 | ### new ArchiveManager([storage]) 81 | Constructor for the manager 82 | 83 | 84 | | Param | Type | Description | 85 | | --- | --- | --- | 86 | | [storage] | [StorageInterface](#StorageInterface) | Storage interface reference | 87 | 88 | 89 | 90 | ### archiveManager.archives : Object 91 | Archives reference 92 | 93 | **Kind**: instance property of [ArchiveManager](#ArchiveManager) 94 | 95 | 96 | ### archiveManager.displayList : [Array.<ArchiveDetailsDisplay>](#ArchiveDetailsDisplay) 97 | Array of archive details ready for display 98 | 99 | **Kind**: instance property of [ArchiveManager](#ArchiveManager) 100 | 101 | 102 | ### archiveManager.storage : [StorageInterface](#StorageInterface) 103 | Storage reference 104 | 105 | **Kind**: instance property of [ArchiveManager](#ArchiveManager) 106 | 107 | 108 | ### archiveManager.unlockedArchives : [Array.<ManagedArchiveItem>](#ManagedArchiveItem) 109 | Array of unlocked archive items 110 | 111 | **Kind**: instance property of [ArchiveManager](#ArchiveManager) 112 | 113 | 114 | ### archiveManager.addArchive(archiveName, workspace, credentials, masterPassword) 115 | Add an archive to the manager 116 | 117 | **Kind**: instance method of [ArchiveManager](#ArchiveManager) 118 | 119 | | Param | Type | Description | 120 | | --- | --- | --- | 121 | | archiveName | String | A unique name for the item | 122 | | workspace | Workspace | The workspace that holds the archive, datasource etc. | 123 | | credentials | Credentials | The credentials for remote storage etc. (these should also already hold datasource meta information) | 124 | | masterPassword | String | The master password | 125 | 126 | 127 | 128 | ### archiveManager.isLocked(archiveName) ⇒ Boolean 129 | Check if an item is locked 130 | 131 | **Kind**: instance method of [ArchiveManager](#ArchiveManager) 132 | **Returns**: Boolean - True if locked 133 | **Throws**: 134 | 135 | - Error Throws if the item is not found 136 | 137 | 138 | | Param | Type | Description | 139 | | --- | --- | --- | 140 | | archiveName | String | The name of the item | 141 | 142 | 143 | 144 | ### archiveManager.loadState() 145 | Load the manager state 146 | Used when the page loads to restore the archive items list (all are locked at 147 | this stage). 148 | 149 | **Kind**: instance method of [ArchiveManager](#ArchiveManager) 150 | 151 | 152 | ### archiveManager.lock(archiveName) ⇒ Promise 153 | Lock an item 154 | 155 | **Kind**: instance method of [ArchiveManager](#ArchiveManager) 156 | **Returns**: Promise - A promise that resolves when the item is locked 157 | **Throws**: 158 | 159 | - Error Throws if the item is not found 160 | - Error Throws if the item is already locked 161 | - Error Throws if the item is currently being processed 162 | 163 | 164 | | Param | Type | Description | 165 | | --- | --- | --- | 166 | | archiveName | String | The name of the item to lock | 167 | 168 | 169 | 170 | ### archiveManager.removeArchive(archiveName) ⇒ Boolean 171 | Remove an archive by name 172 | 173 | **Kind**: instance method of [ArchiveManager](#ArchiveManager) 174 | **Returns**: Boolean - True if deleted, false if not found 175 | 176 | | Param | Type | Description | 177 | | --- | --- | --- | 178 | | archiveName | String | The name of the archive to remove | 179 | 180 | 181 | 182 | ### archiveManager.saveState() ⇒ Promise 183 | Save the state of the manager to the storage 184 | 185 | **Kind**: instance method of [ArchiveManager](#ArchiveManager) 186 | **Returns**: Promise - A promise that resolves once the state has been saved 187 | 188 | 189 | ### archiveManager.unlock(archiveName, password) ⇒ Promise 190 | Unlock a locked item 191 | 192 | **Kind**: instance method of [ArchiveManager](#ArchiveManager) 193 | **Returns**: Promise - A promise that resolves when the item is unlocked 194 | **Throws**: 195 | 196 | - Error Throws if the item is not locked 197 | 198 | 199 | | Param | Type | Description | 200 | | --- | --- | --- | 201 | | archiveName | String | The name of the item to unlock | 202 | | password | String | The master password of the item to unlock | 203 | 204 | 205 | 206 | ### archiveManager.updateUnlocked() ⇒ Promise 207 | Update workspaces that are unlocked 208 | 209 | **Kind**: instance method of [ArchiveManager](#ArchiveManager) 210 | **Returns**: Promise - A promise that resolves after updating all unlocked workspaces 211 | 212 | 213 | ### ArchiveManager.ArchiveStatus 214 | Stored archive status 215 | 216 | **Kind**: static enum of [ArchiveManager](#ArchiveManager) 217 | 218 | 219 | ### ArchiveManager.getSharedManager() ⇒ [ArchiveManager](#ArchiveManager) 220 | Get the singleton shared instance 221 | 222 | **Kind**: static method of [ArchiveManager](#ArchiveManager) 223 | **Returns**: [ArchiveManager](#ArchiveManager) - The shared instance 224 | 225 | 226 | ## DropboxDatasource ⇐ TextDatasource 227 | Datasource for Dropbox archives 228 | 229 | **Kind**: global class 230 | **Extends:** TextDatasource 231 | 232 | * [DropboxDatasource](#DropboxDatasource) ⇐ TextDatasource 233 | * [new DropboxDatasource(accessToken, resourcePath)](#new_DropboxDatasource_new) 234 | * [.toObject()](#DropboxDatasource+toObject) ⇒ Object 235 | 236 | 237 | 238 | ### new DropboxDatasource(accessToken, resourcePath) 239 | Datasource for Dropbox accounts 240 | 241 | 242 | | Param | Type | Description | 243 | | --- | --- | --- | 244 | | accessToken | String | The dropbox access token | 245 | | resourcePath | String | The file path | 246 | 247 | 248 | 249 | ### dropboxDatasource.toObject() ⇒ Object 250 | Output the datasource as an object 251 | 252 | **Kind**: instance method of [DropboxDatasource](#DropboxDatasource) 253 | **Returns**: Object - An object describing the datasource 254 | 255 | 256 | ## EntryFinder 257 | **Kind**: global class 258 | 259 | * [EntryFinder](#EntryFinder) 260 | * [new EntryFinder(_archives)](#new_EntryFinder_new) 261 | * [.items](#EntryFinder+items) : [Array.<EntrySearchInfo>](#EntrySearchInfo) 262 | * [.lastResult](#EntryFinder+lastResult) : [Array.<EntrySearchInfo>](#EntrySearchInfo) 263 | * [.initSearcher()](#EntryFinder+initSearcher) 264 | * [.search(term)](#EntryFinder+search) ⇒ [Array.<EntrySearchInfo>](#EntrySearchInfo) 265 | 266 | 267 | 268 | ### new EntryFinder(_archives) 269 | 270 | | Param | Type | Description | 271 | | --- | --- | --- | 272 | | _archives | Array.<Archive> | Archive | The archives to search | 273 | 274 | 275 | 276 | ### entryFinder.items : [Array.<EntrySearchInfo>](#EntrySearchInfo) 277 | All items 278 | 279 | **Kind**: instance property of [EntryFinder](#EntryFinder) 280 | 281 | 282 | ### entryFinder.lastResult : [Array.<EntrySearchInfo>](#EntrySearchInfo) 283 | The last result 284 | 285 | **Kind**: instance property of [EntryFinder](#EntryFinder) 286 | 287 | 288 | ### entryFinder.initSearcher() 289 | Initialise the searching mechanism 290 | 291 | **Kind**: instance method of [EntryFinder](#EntryFinder) 292 | 293 | 294 | ### entryFinder.search(term) ⇒ [Array.<EntrySearchInfo>](#EntrySearchInfo) 295 | Search and get results 296 | 297 | **Kind**: instance method of [EntryFinder](#EntryFinder) 298 | **Returns**: [Array.<EntrySearchInfo>](#EntrySearchInfo) - The results 299 | 300 | | Param | Type | Description | 301 | | --- | --- | --- | 302 | | term | String | The search term | 303 | 304 | 305 | 306 | ## LocalStorageInterface ⇐ [StorageInterface](#StorageInterface) 307 | Interface for localStorage 308 | 309 | **Kind**: global class 310 | **Extends:** [StorageInterface](#StorageInterface) 311 | 312 | * [LocalStorageInterface](#LocalStorageInterface) ⇐ [StorageInterface](#StorageInterface) 313 | * [.getAllKeys()](#LocalStorageInterface+getAllKeys) ⇒ Promise.<Array.<String>> 314 | * [.getValue(name)](#LocalStorageInterface+getValue) ⇒ Promise.<String> 315 | * [.setValue(name, value)](#LocalStorageInterface+setValue) ⇒ Promise 316 | 317 | 318 | 319 | ### localStorageInterface.getAllKeys() ⇒ Promise.<Array.<String>> 320 | Get all keys from storage 321 | 322 | **Kind**: instance method of [LocalStorageInterface](#LocalStorageInterface) 323 | **Returns**: Promise.<Array.<String>> - A promise that resolves with an array of keys 324 | 325 | 326 | ### localStorageInterface.getValue(name) ⇒ Promise.<String> 327 | Get the value of a key 328 | 329 | **Kind**: instance method of [LocalStorageInterface](#LocalStorageInterface) 330 | **Returns**: Promise.<String> - A promise that resolves with the value 331 | 332 | | Param | Type | Description | 333 | | --- | --- | --- | 334 | | name | String | The key name | 335 | 336 | 337 | 338 | ### localStorageInterface.setValue(name, value) ⇒ Promise 339 | Set the value for a key 340 | 341 | **Kind**: instance method of [LocalStorageInterface](#LocalStorageInterface) 342 | **Returns**: Promise - A promise that resolves when the value is set 343 | 344 | | Param | Type | Description | 345 | | --- | --- | --- | 346 | | name | String | The key name | 347 | | value | String | The value to set | 348 | 349 | 350 | 351 | ## StorageInterface : Object 352 | **Kind**: global variable 353 | 354 | * [StorageInterface](#StorageInterface) : Object 355 | * [.getData](#StorageInterface.getData) ⇒ \* 356 | * [.setData](#StorageInterface.setData) 357 | 358 | 359 | 360 | ### StorageInterface.getData ⇒ \* 361 | Get data from storage 362 | 363 | **Kind**: static property of [StorageInterface](#StorageInterface) 364 | **Returns**: \* - The fetched data 365 | 366 | | Param | Type | Description | 367 | | --- | --- | --- | 368 | | key | String | The key to fetch for | 369 | | defaultValue | \* | The default value if the key is not found | 370 | 371 | 372 | 373 | ### StorageInterface.setData 374 | Set data for a key 375 | 376 | **Kind**: static property of [StorageInterface](#StorageInterface) 377 | 378 | | Param | Type | Description | 379 | | --- | --- | --- | 380 | | key | String | The key to set for | 381 | | rawData | Object | Array | String | Number | \* | The raw data to set | 382 | 383 | 384 | 385 | ## ArchiveTools : object 386 | **Kind**: global namespace 387 | 388 | * [ArchiveTools](#ArchiveTools) : object 389 | * [.extractDomain(url)](#ArchiveTools.extractDomain) ⇒ String 390 | * [.getEntriesForURL(archive, url)](#ArchiveTools.getEntriesForURL) ⇒ Array.<Entry> 391 | 392 | 393 | 394 | ### ArchiveTools.extractDomain(url) ⇒ String 395 | Extract the domain from a URL 396 | 397 | **Kind**: static method of [ArchiveTools](#ArchiveTools) 398 | **Returns**: String - The domain or an empty string if none found 399 | 400 | | Param | Type | Description | 401 | | --- | --- | --- | 402 | | url | String | The URL to extract from | 403 | 404 | 405 | 406 | ### ArchiveTools.getEntriesForURL(archive, url) ⇒ Array.<Entry> 407 | Get entries for a particular URL 408 | 409 | **Kind**: static method of [ArchiveTools](#ArchiveTools) 410 | **Returns**: Array.<Entry> - An array of entries 411 | 412 | | Param | Type | Description | 413 | | --- | --- | --- | 414 | | archive | Archive | A buttercup archive instance | 415 | | url | String | A URL | 416 | 417 | 418 | 419 | ## flattenEntries(archives) ⇒ [Array.<EntrySearchInfo>](#EntrySearchInfo) 420 | Flatten entries into a searchable structure 421 | 422 | **Kind**: global function 423 | **Returns**: [Array.<EntrySearchInfo>](#EntrySearchInfo) - An array of searchable objects 424 | 425 | | Param | Type | Description | 426 | | --- | --- | --- | 427 | | archives | Array.<Archive> | An array of archives | 428 | 429 | 430 | 431 | ## ArchiveDetailsDisplay : Object 432 | Archive details for display 433 | 434 | **Kind**: global typedef 435 | **Properties** 436 | 437 | | Name | Type | Description | 438 | | --- | --- | --- | 439 | | name | String | The name of the item | 440 | | status | ArchiveStatus | The status of the item | 441 | | type | String | The type of archive connection | 442 | 443 | 444 | 445 | ## ManagedArchiveItem : Object 446 | Stored archive entry 447 | 448 | **Kind**: global typedef 449 | **Properties** 450 | 451 | | Name | Type | Description | 452 | | --- | --- | --- | 453 | | status | ArchiveStatus | The status of the item | 454 | | workspace | Workspace | undefined | Reference to the workspace (undefined if locked) | 455 | | credentials | Credentials | String | Reference to Credentials instance (encrypted string if locked) | 456 | | password | String | undefined | The master password (undefined if locked) | 457 | 458 | 459 | 460 | ## EntrySearchInfo : Object 461 | **Kind**: global typedef 462 | **Properties** 463 | 464 | | Name | Type | Description | 465 | | --- | --- | --- | 466 | | entry | Entry | The entry | 467 | | archive | Archive | The associated archive | 468 | 469 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Buttercup core-web changelog 2 | 3 | ## v0.41.0 4 | _2017-10-16_ 5 | 6 | * Upgrade core to 0.50.0 7 | * Allow overriding of existing datasources 8 | 9 | ## v0.40.4 10 | _2017-09-20_ 11 | 12 | * Add react-native version of the script (compatibility) 13 | * Switch to using `/dist` instead of `/build` 14 | 15 | ## v0.39.0 16 | _2017-09-04_ 17 | 18 | * Upgrade core to 0.49.0 19 | * Expose additional crypto overrides for IV and salt generation 20 | 21 | ## v0.38.0 22 | _2017-09-02_ 23 | 24 | * Upgrade core to 0.48.0 25 | * Expose overridable crypto methods 26 | 27 | ## v0.37.0 28 | _2017-08-29_ 29 | 30 | * Upgrade core to 0.47.0 31 | * Core crypto now async 32 | 33 | ## v0.36.0 34 | _2017-08-26_ 35 | 36 | * Upgrade core to 0.46.0, upgrade iocane to 0.8.0 37 | * Expose override functions for crypto 38 | 39 | ## v0.35.0 40 | _2017-07-45_ 41 | 42 | * Update core to 0.45.0 43 | * Fix `Entry` facade consumption 44 | * `Entry` `getProperty`/`getMeta`/`getAttribute` support for 0 parameters 45 | 46 | ## v0.34.1 47 | _2017-07-16_ 48 | 49 | * Update core to 0.44.1 50 | * Expose `webdav-fs` in vendor props (`fetch` method override support) 51 | 52 | ## v0.33.1 53 | _2017-07-07_ 54 | 55 | * Update core to 0.33.1 56 | * Fix core `ArchiveManager` `unlockedSources` returning incorrect results 57 | 58 | ## v0.33.0 59 | _2017-07-06_ 60 | 61 | * Update core to 0.42.0 62 | * Change event emitters to be asynchronous 63 | 64 | ## v0.32.2 65 | _2017-07-03_ 66 | 67 | * Update core to 0.41.2 68 | * Fix core `ArchiveManager` `unlock` method breaking when wrong password entered 69 | 70 | ## v0.32.1 71 | _2017-06-30_ 72 | 73 | * Update core to 0.41.1 74 | * Fix core `ArchiveManager` `Workspace` creation providing wrong credentials 75 | 76 | ## v0.32.0 77 | _2017-06-24_ 78 | 79 | * Update core to 0.41.0 80 | * `ArchiveManager` `remove` method 81 | * `webdav-fs` to 1.3.0 82 | * Disable native `window.fetch` in browsers for stability 83 | 84 | ## v0.31.0 85 | _2017-06-10_ 86 | 87 | * Update core to 0.40.1 88 | * Add missing event to core's `ArchiveManager` 89 | * Reduce bundle size by using lodash replacement plugins 90 | 91 | ## v0.30.0 92 | _2017-06-07_ 93 | 94 | * Add `ArchiveManager` and `StorageInterface` back for compatibility 95 | 96 | ## v0.29.0 97 | _2017-05-28_ 98 | 99 | * Update core to 0.40.0 100 | * Update webdav-fs to 1.0.0 101 | * Add event emitters to core classes 102 | * **Bugfix**: empty value encoding 103 | 104 | ## v0.28.1 105 | _2017-05-27_ 106 | 107 | * Move SubtleCrypto reference into PBKDF2 function (fixes global reference when not available - eg. not browser) 108 | 109 | ## v0.28.0 110 | _2017-05-24_ 111 | 112 | * Allow for external PBKDF2 functions in patching 113 | 114 | ## v0.27.1 115 | _2017-05-22_ 116 | 117 | * Update core to 0.39.1 118 | * Expose previously created methods for React Native support 119 | 120 | ## v0.27.0 121 | _2017-05-21_ 122 | 123 | * Update core to 0.39.0 124 | * Support setting deferred handlers for `TextDatasource` crypto 125 | 126 | ## v0.26.0 127 | _2017-05-02_ 128 | 129 | _Due to package **deprecation**, this release helps to gradually phase out functionality in core-web._ 130 | 131 | * Remove `ArchiveManager` and `StorageInterface` 132 | * Upgrade core to 0.38.0 133 | * New `ArchiveManager` 134 | * Add `LocalStorageInterface` for use with core's `ArchiveManager` 135 | 136 | ## v0.25.2 137 | _2017-04-16_ 138 | 139 | * Upgrade core to 0.37.1 140 | * Bugfix: Merging deletion commands when remote hasn't changed 141 | 142 | ## v0.25.1 143 | _2017-03-29_ 144 | 145 | * Bugfix: Error in archive unlock process (wrong password) broke state 146 | 147 | ## v0.25.0 148 | _2017-03-27_ 149 | 150 | * Upgrade core to 0.37.0 151 | * Added support for `Group.getGroup()` to access parent groups 152 | 153 | ## v0.24.0 154 | _2017-03-20_ 155 | 156 | * Added `EntryFinder` for fuzzy searching entries 157 | 158 | ## v0.23.0 159 | _2017-03-13_ 160 | 161 | * Upgrade core to 0.35.0 162 | * Entry property serialisation (breaks backwards compatibility with older Buttercup builds) 163 | 164 | ## v0.22.0 165 | _2017-03-09_ 166 | 167 | * Improve URL matching 168 | 169 | ## v0.21.0 170 | _2017-03-07_ 171 | 172 | * **Breaking**: 173 | * Export UMD module instead of default `window` only attachment 174 | * Update core to 0.34.0 (credentials breaking changes) 175 | * Global, shared `archiveManager` reference removed 176 | * `ArchiveManager` gets singleton method 177 | 178 | ## v0.20.0 179 | _2017-01-25_ 180 | 181 | * Add update method for unlocked archives in ArchiveManager 182 | 183 | ## v0.19.0 184 | _2017-01-22_ 185 | 186 | * Add `type` property to `displayList` of `ArchiveManager` (_This contains breaking changes to the save format in local storage._) 187 | 188 | ## v0.18.0 189 | _2017-01-10_ 190 | 191 | * Added `removeArchive` to `ArchiveManager` 192 | 193 | ## v0.17.3 194 | _2017-01-09_ 195 | 196 | * Bugfix: typo in output of ArchiveManager.displayList 197 | 198 | ## v0.17.2 199 | _2017-01-07_ 200 | 201 | * Upgrade core to 0.33.1 202 | * Type checking for `Archive` and `Group` instances 203 | * Better type checking in group moving 204 | 205 | ## v0.17.0 206 | _2017-01-07_ 207 | 208 | * Upgrade core to 0.33.0 209 | * Add `getHistory` and `createFromHistory` `Archive` methods 210 | 211 | ## v0.16.0 212 | _2017-01-06_ 213 | 214 | * Upgrade core to 0.32.0 215 | * Add `findEntryByID` to `Archive` and `Group` classes 216 | * Add `emptyTrash` method to `Archive` 217 | 218 | ## v0.15.1 219 | _2016-12-30_ 220 | 221 | * Fixed OwnCloudDatasource instantiation 222 | 223 | ## v0.15.0 224 | _2016-12-27_ 225 | 226 | * Added [API documentation](API.md) 227 | * Load Archive Manager state on boot 228 | * Added Dropbox datasource 229 | * Filter URL-based entries if they're deleted (trash) 230 | * **Breaking changes:** 231 | * Rewrote `ArchiveManager` 232 | 233 | ## v0.14.1 234 | _2016-12-17_ 235 | 236 | * Update `SubtleCrypto`'s `importKey` to use `extractable: false` (fix for Chrome) 237 | * Run `loadState` on init for archive manager 238 | 239 | ## v0.14.0 240 | _2016-12-11_ 241 | 242 | * Credentials state output 243 | 244 | ## v0.13.0 245 | _2016-12-03_ 246 | 247 | * Credentials update (meta) 248 | 249 | ## v0.12.0 250 | _2016-11-08_ 251 | 252 | * Upgrade Buttercup core to 0.28.0 253 | * Shared archives 254 | * Group moving between archives 255 | * Archive `toObject` 256 | 257 | ## v0.11.1 258 | _2016-11-01_ 259 | 260 | * Upgrade Buttercup core to 0.27.0 261 | * Group & Entry searching decorators for Archives and Groups 262 | * Renamed ManagedGroup to Group 263 | * Renamed ManagedEntry to Entry 264 | * Deprecated Archive.getGroupByID and Group.getGroupByID in favour of findGroupByID 265 | 266 | ## v0.10.4 267 | _2016-10-20_ 268 | 269 | * Fix publishing (no files included / babel not run) 270 | 271 | ## v0.10.0 272 | _2016-10-16_ 273 | 274 | * Upgrade Buttercup core to 0.25.0 275 | * Entry and Group deletion upgrade 276 | * Fixed `toObject` issues 277 | 278 | ## v0.9.0 279 | _2016-10-15_ 280 | 281 | * Upgrade Buttercup core to 0.24.0 282 | * Group `toObject` depth 283 | 284 | ## v0.8.0 285 | _2016-07-18_ 286 | 287 | * Workspace saving asynchronously 288 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Perry Mitchell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Buttercup core library - for the web 2 | Web-based build of the Buttercup core library. 3 | 4 | [![Buttercup](https://cdn.rawgit.com/buttercup-pw/buttercup-assets/6582a033/badge/buttercup-slim.svg)](https://buttercup.pw) [![Join the chat at https://gitter.im/buttercup-pw/buttercup-core-web](https://badges.gitter.im/buttercup-pw/buttercup-core-web.svg)](https://gitter.im/buttercup-pw/buttercup-core-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/buttercup/buttercup-core-web.svg?branch=master)](https://travis-ci.org/buttercup/buttercup-core-web) 5 | 6 | [![Buttercup-web](https://nodei.co/npm/buttercup-web.png?downloads=true&downloadRank=true&stars=true)](https://www.npmjs.com/package/buttercup-web) 7 | 8 | For the most part this library inherits all functionality from [Buttercup core](https://github.com/buttercup-pw/buttercup-core), but it also contains some web-specific functionality in the way of tools and rigs. 9 | 10 | API reference: 11 | 12 | * This API ([core-web](API.md)) 13 | * [Core API](https://github.com/buttercup/buttercup-core/blob/master/doc/api.md) 14 | 15 | ## _**Deprecated**: Core-web will slowly be integrated completely with [Buttercup core](https://github.com/buttercup/buttercup-core/)_ 16 | 17 | This repository will be made obsolete by the task [buttercup/buttercup-core#181](https://github.com/buttercup/buttercup-core/pull/181). 18 | 19 | ## Usage 20 | Buttercup core-web is a UMD module, so you can import it using [AMD](http://requirejs.org/docs/whyamd.html#amd) or [CommonJS](http://requirejs.org/docs/whyamd.html#commonjs) styles, or by simply including it as script on a webpage (exposes `Buttercup` and `Buttercup.Web` on the `window`). 21 | 22 | Importing Buttercup into other projects is easy: 23 | 24 | ```javascript 25 | import { Archive } from "buttercup-web"; 26 | // or 27 | const { Archive } = require("buttercup-web"); 28 | ``` 29 | 30 | When using **react-native**, there's a special version of the script which should be used when Dropbox, for example, may be used: 31 | 32 | ```javascript 33 | import { Archive, Web as ButtercupWeb } from "buttercup-web/dist/react-native-buttercup.min.js"; 34 | const { DropboxDatasource } = ButtercupWeb; 35 | ``` 36 | 37 | ## Cryptography 38 | The core-web library utilises current technology to encrypt and hash and very high speed, and this is supported by only the [newest browsers](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto#Browser_compatibility). 39 | 40 | Buttercup-core-web, like Buttercup-core, uses [iocane](https://github.com/perry-mitchell/iocane) for text encryption and decryption. iocane uses 256bit AES encryption to securely store password archives, and is completely compatible with most modern browsers. 41 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Oct 15 2016 15:57:04 GMT+0300 (EEST) 3 | 4 | const webpackConfig = require("./webpack.config.js").pop(); // minified is second 5 | 6 | module.exports = function(config) { 7 | config.set({ 8 | 9 | // base path that will be used to resolve all patterns (eg. files, exclude) 10 | basePath: "", 11 | 12 | 13 | // frameworks to use 14 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 15 | frameworks: ["mocha", "chai", "sinon"], 16 | 17 | 18 | // list of files / patterns to load in the browser 19 | files: [ 20 | // "build/buttercup.min.js", 21 | "source/index.js", 22 | "tests/index.js", 23 | "tests/**/*.spec.js" 24 | ], 25 | 26 | 27 | // list of files to exclude 28 | exclude: [], 29 | 30 | 31 | // preprocess matching files before serving them to the browser 32 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 33 | preprocessors: { 34 | "source/**/*.js": ["webpack"] 35 | // "tests/index.js": ["webpack"] 36 | }, 37 | 38 | 39 | webpack: webpackConfig, 40 | 41 | 42 | webpackMiddleware: { 43 | stats: "errors-only" 44 | }, 45 | 46 | 47 | // test results reporter to use 48 | // possible values: "dots", "progress" 49 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 50 | reporters: ["progress"], 51 | 52 | 53 | // web server port 54 | port: 9876, 55 | 56 | 57 | // enable / disable colors in the output (reporters and logs) 58 | colors: true, 59 | 60 | 61 | // level of logging 62 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 63 | logLevel: config.LOG_INFO, 64 | 65 | 66 | client: { 67 | captureConsole: false, 68 | mocha: { 69 | timeout: 7500 70 | } 71 | }, 72 | 73 | 74 | // enable / disable watching file and executing tests whenever any file changes 75 | autoWatch: true, 76 | 77 | 78 | // start these browsers 79 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 80 | browsers: ["Chrome"], 81 | 82 | 83 | // Continuous Integration mode 84 | // if true, Karma captures browsers, runs the tests and exits 85 | singleRun: false, 86 | 87 | // Concurrency level 88 | // how many browser should be started simultaneous 89 | concurrency: Infinity 90 | }); 91 | }; 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "buttercup-web", 3 | "version": "0.41.0", 4 | "description": "Buttercup core library for the web.", 5 | "main": "dist/buttercup.min.js", 6 | "scripts": { 7 | "build": "npm run clean && webpack", 8 | "clean": "rimraf dist/*.*", 9 | "dev": "webpack --progress --watch", 10 | "generate:docs": "jsdoc2md 'source/**/*.js' > API.md", 11 | "karma": "karma start", 12 | "prepublish": "npm run build", 13 | "test": "npm run test:lint && npm run karma -- --single-run", 14 | "test:ci": "npm test", 15 | "test:lint": "eslint 'source/**'", 16 | "test:watch": "npm run karma" 17 | }, 18 | "files": [ 19 | "dist/buttercup.js", 20 | "dist/buttercup.min.js", 21 | "dist/react-native-buttercup.min.js" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/buttercup-pw/buttercup-core-web.git" 26 | }, 27 | "keywords": [ 28 | "buttercup", 29 | "password", 30 | "security", 31 | "encryption" 32 | ], 33 | "author": "Perry Mitchell ", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/buttercup-pw/buttercup-core-web/issues" 37 | }, 38 | "homepage": "https://github.com/buttercup-pw/buttercup-core-web#readme", 39 | "devDependencies": { 40 | "babel-core": "^6.24.1", 41 | "babel-loader": "^7.0.0", 42 | "babel-plugin-lodash": "^3.2.11", 43 | "babel-preset-es2015": "^6.24.1", 44 | "chai": "^3.5.0", 45 | "crypto-browserify": "~3.11.0", 46 | "dropbox-fs": "~0.0.4", 47 | "eslint": "^3.13.0", 48 | "fuse.js": "~2.6.2", 49 | "jsdoc-to-markdown": "^2.0.1", 50 | "json-loader": "~0.5.4", 51 | "karma": "^1.3.0", 52 | "karma-chai": "^0.1.0", 53 | "karma-chrome-launcher": "^2.0.0", 54 | "karma-cli": "^1.0.1", 55 | "karma-firefox-launcher": "^1.0.0", 56 | "karma-mocha": "^1.2.0", 57 | "karma-sinon": "^1.0.5", 58 | "karma-webpack": "^1.8.1", 59 | "lodash-webpack-plugin": "^0.11.4", 60 | "mocha": "^3.1.2", 61 | "node-noop": "^1.0.0", 62 | "react-native-dropbox-sdk": "~0.4.0", 63 | "rimraf": "^2.5.4", 64 | "sinon": "^1.17.6", 65 | "uglify-js": "^2.8.22", 66 | "uglifyjs-webpack-plugin": "^0.4.3", 67 | "webpack": "~2.4.1", 68 | "webpack-visualizer-plugin": "^0.1.11" 69 | }, 70 | "dependencies": { 71 | "buttercup": "~0.50.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /source/ArchiveManager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Buttercup = require("buttercup"), 4 | StorageInterface = require("__buttercup_web/StorageInterface.js"); 5 | 6 | var createCredentials = Buttercup.createCredentials, 7 | DatasourceAdapter = Buttercup.DatasourceAdapter, 8 | Workspace = Buttercup.Workspace; 9 | 10 | var __sharedManager = null; 11 | 12 | /** 13 | * Archive Manager - manages a set of archives for the browser 14 | */ 15 | class ArchiveManager { 16 | 17 | /** 18 | * Constructor for the manager 19 | * @param {StorageInterface=} storage Storage interface reference 20 | */ 21 | constructor(storage) { 22 | this._archives = {}; 23 | this._storage = storage || StorageInterface; 24 | } 25 | 26 | /** 27 | * Archives reference 28 | * @type {Object} 29 | */ 30 | get archives() { 31 | return this._archives; 32 | } 33 | 34 | /** 35 | * Archive details for display 36 | * @typedef {Object} ArchiveDetailsDisplay 37 | * @property {String} name The name of the item 38 | * @property {ArchiveStatus} status The status of the item 39 | * @property {String} type The type of archive connection 40 | */ 41 | 42 | /** 43 | * Array of archive details ready for display 44 | * @type {Array.} 45 | */ 46 | get displayList() { 47 | const archives = this.archives; 48 | return Object.keys(archives).map(archiveName => ({ 49 | name: archiveName, 50 | status: archives[archiveName].status, 51 | type: archives[archiveName].type 52 | })); 53 | } 54 | 55 | /** 56 | * Storage reference 57 | * @type {StorageInterface} 58 | */ 59 | get storage() { 60 | return this._storage; 61 | } 62 | 63 | /** 64 | * Stored archive entry 65 | * @typedef {Object} ManagedArchiveItem 66 | * @property {ArchiveStatus} status The status of the item 67 | * @property {Workspace|undefined} workspace Reference to the workspace (undefined if locked) 68 | * @property {Credentials|String} credentials Reference to Credentials instance (encrypted string if locked) 69 | * @property {String|undefined} password The master password (undefined if locked) 70 | */ 71 | 72 | /** 73 | * Array of unlocked archive items 74 | * @type {Array.} 75 | */ 76 | get unlockedArchives() { 77 | const archives = this.archives; 78 | return Object.keys(archives) 79 | .map(archiveName => Object.assign({ name: archiveName }, archives[archiveName])) 80 | .filter(details => details.status === ArchiveManager.ArchiveStatus.UNLOCKED); 81 | } 82 | 83 | /** 84 | * Add an archive to the manager 85 | * @param {String} archiveName A unique name for the item 86 | * @param {Workspace} workspace The workspace that holds the archive, datasource etc. 87 | * @param {Credentials} credentials The credentials for remote storage etc. 88 | * (these should also already hold datasource meta information) 89 | * @param {String} masterPassword The master password 90 | */ 91 | addArchive(archiveName, workspace, credentials, masterPassword) { 92 | if (this._archives[archiveName]) { 93 | throw new Error(`Archive already exists: ${archiveName}`); 94 | } 95 | this.archives[archiveName] = { 96 | status: ArchiveManager.ArchiveStatus.UNLOCKED, 97 | workspace, 98 | credentials, 99 | password: masterPassword, 100 | type: credentials.type 101 | }; 102 | } 103 | 104 | /** 105 | * Check if an item is locked 106 | * @param {String} archiveName The name of the item 107 | * @returns {Boolean} True if locked 108 | * @throws {Error} Throws if the item is not found 109 | */ 110 | isLocked(archiveName) { 111 | if (!this.archives[archiveName]) { 112 | throw new Error(`Archive not found: ${archiveName}`); 113 | } 114 | return this.archives[archiveName].status === ArchiveManager.ArchiveStatus.LOCKED; 115 | } 116 | 117 | /** 118 | * Load the manager state 119 | * Used when the page loads to restore the archive items list (all are locked at 120 | * this stage). 121 | */ 122 | loadState() { 123 | var loadedData = this.storage.getData("archiveManager", { archives: {} }); 124 | this._archives = {}; 125 | for (const archiveName in loadedData.archives) { 126 | if (loadedData.archives.hasOwnProperty(archiveName)) { 127 | const { content, type } = loadedData.archives[archiveName]; 128 | this.archives[archiveName] = { 129 | status: ArchiveManager.ArchiveStatus.LOCKED, 130 | credentials: content, 131 | type 132 | }; 133 | } 134 | } 135 | } 136 | 137 | /** 138 | * Lock an item 139 | * @param {String} archiveName The name of the item to lock 140 | * @throws {Error} Throws if the item is not found 141 | * @throws {Error} Throws if the item is already locked 142 | * @throws {Error} Throws if the item is currently being processed 143 | * @returns {Promise} A promise that resolves when the item is locked 144 | */ 145 | lock(archiveName) { 146 | if (!this.archives[archiveName]) { 147 | throw new Error(`Archive not found: ${archiveName}`); 148 | } 149 | if (this.isLocked(archiveName)) { 150 | throw new Error(`Archive already locked: ${archiveName}`); 151 | } 152 | let details = this.archives[archiveName]; 153 | if (details.status === ArchiveManager.ArchiveStatus.PROCESSING) { 154 | throw new Error(`Archive is in processing state: ${archiveName}`); 155 | } 156 | details.status = ArchiveManager.ArchiveStatus.PROCESSING; 157 | return details.credentials 158 | .toSecureString(details.password) 159 | .then(function(encContent) { 160 | details.credentials = encContent; 161 | delete details.workspace; 162 | delete details.password; 163 | details.status = ArchiveManager.ArchiveStatus.LOCKED; 164 | }); 165 | } 166 | 167 | /** 168 | * Remove an archive by name 169 | * @param {String} archiveName The name of the archive to remove 170 | * @returns {Boolean} True if deleted, false if not found 171 | */ 172 | removeArchive(archiveName) { 173 | if (this._archives.hasOwnProperty(archiveName)) { 174 | delete this._archives[archiveName]; 175 | return true; 176 | } 177 | return false; 178 | } 179 | 180 | /** 181 | * Save the state of the manager to the storage 182 | * @returns {Promise} A promise that resolves once the state has been saved 183 | */ 184 | saveState() { 185 | var packet = { 186 | archives: {} 187 | }, 188 | delayed = [Promise.resolve()]; 189 | Object.keys(this.archives).forEach((archiveName) => { 190 | const archiveDetails = this.archives[archiveName]; 191 | if (archiveDetails.status === ArchiveManager.ArchiveStatus.LOCKED) { 192 | packet.archives[archiveName] = { 193 | content: archiveDetails.credentials, 194 | type: archiveDetails.type 195 | }; 196 | } else { 197 | delayed.push( 198 | archiveDetails.credentials 199 | .toSecureString(archiveDetails.password) 200 | .then(function handledConvertedContent(content) { 201 | packet.archives[archiveName] = { 202 | content, 203 | type: archiveDetails.type 204 | }; 205 | }) 206 | ); 207 | } 208 | }); 209 | return Promise 210 | .all(delayed) 211 | .then(() => { 212 | this.storage.setData("archiveManager", packet); 213 | }); 214 | } 215 | 216 | /** 217 | * Unlock a locked item 218 | * @param {String} archiveName The name of the item to unlock 219 | * @param {String} password The master password of the item to unlock 220 | * @throws {Error} Throws if the item is not locked 221 | * @returns {Promise} A promise that resolves when the item is unlocked 222 | */ 223 | unlock(archiveName, password) { 224 | var archiveDetails = this.archives[archiveName]; 225 | if (!this.isLocked(archiveName)) { 226 | return Promise.resolve(archiveDetails); 227 | } 228 | archiveDetails.status = ArchiveManager.ArchiveStatus.PROCESSING; 229 | return createCredentials 230 | .fromSecureString(archiveDetails.credentials, password) 231 | .then((credentials) => { 232 | if (!credentials) { 233 | return Promise.reject(new Error("Failed unlocking credentials: " + archiveName)); 234 | } 235 | archiveDetails.credentials = credentials; 236 | archiveDetails.password = password; 237 | let datasourceInfo = JSON.parse(credentials.getValueOrFail("datasource")), 238 | ds = DatasourceAdapter.objectToDatasource(datasourceInfo, credentials); 239 | if (!ds) { 240 | throw new Error("Failed creating datasource - possible corrupt credentials"); 241 | } 242 | return Promise.all([ 243 | ds.load(createCredentials.fromPassword(password)), 244 | Promise.resolve(ds) 245 | ]); 246 | }) 247 | .then(([archive, datasource] = []) => { 248 | const workspace = new Workspace(); 249 | workspace.setPrimaryArchive(archive, datasource, createCredentials.fromPassword(password)); 250 | archiveDetails.workspace = workspace; 251 | archiveDetails.status = ArchiveManager.ArchiveStatus.UNLOCKED; 252 | }) 253 | .catch(function(err) { 254 | archiveDetails.status = ArchiveManager.ArchiveStatus.LOCKED; 255 | throw err; 256 | }); 257 | } 258 | 259 | /** 260 | * Update workspaces that are unlocked 261 | * @returns {Promise} A promise that resolves after updating all unlocked workspaces 262 | */ 263 | updateUnlocked() { 264 | return Promise.all( 265 | this.unlockedArchives.map(item => item.workspace 266 | .localDiffersFromRemote() 267 | .then(function(differs) { 268 | return differs ? 269 | item.workspace.mergeSaveablesFromRemote().then(() => true) : 270 | false; 271 | }) 272 | .then(function(save) { 273 | // all up to date 274 | return save ? 275 | item.workspace.save() : 276 | null; 277 | }) 278 | ) 279 | ); 280 | } 281 | 282 | } 283 | 284 | /** 285 | * Stored archive status 286 | * @name ArchiveStatus 287 | * @enum 288 | * @memberof ArchiveManager 289 | * @static 290 | */ 291 | ArchiveManager.ArchiveStatus = { 292 | LOCKED: "locked", 293 | UNLOCKED: "unlocked", 294 | PROCESSING: "processing" 295 | }; 296 | 297 | /** 298 | * Get the singleton shared instance 299 | * @memberof ArchiveManager 300 | * @static 301 | * @returns {ArchiveManager} The shared instance 302 | */ 303 | ArchiveManager.getSharedManager = function getSharedManager() { 304 | if (__sharedManager === null) { 305 | __sharedManager = new ArchiveManager(); 306 | __sharedManager.loadState(); 307 | } 308 | return __sharedManager; 309 | }; 310 | 311 | module.exports = ArchiveManager; 312 | -------------------------------------------------------------------------------- /source/ArchiveTools.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @namespace ArchiveTools 5 | */ 6 | const tools = module.exports = { 7 | 8 | /** 9 | * Extract the domain from a URL 10 | * @param {String} url The URL to extract from 11 | * @returns {String} The domain or an empty string if none found 12 | * @memberof ArchiveTools 13 | */ 14 | extractDomain: function(url) { 15 | let match = url.match(/^(https?:\/\/)?([a-z0-9-]+\.[a-z0-9-]+(\.[a-z0-9-]+)*)/i); 16 | return match ? 17 | match[2] : 18 | ""; 19 | }, 20 | 21 | /** 22 | * Get entries for a particular URL 23 | * @param {Archive} archive A buttercup archive instance 24 | * @param {String} url A URL 25 | * @return {Array.} An array of entries 26 | * @memberof ArchiveTools 27 | */ 28 | getEntriesForURL: function(archive, url) { 29 | return archive 30 | .findEntriesByMeta("url", /.+/) 31 | .filter(function(entry) { 32 | var entryURL = entry.getMeta("url"), 33 | entryDomain = tools.extractDomain(entryURL); 34 | return entryDomain.length > 0 && 35 | entryDomain === tools.extractDomain(url) && 36 | entry.isInTrash() === false; 37 | }); 38 | } 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /source/DropboxDatasource.js: -------------------------------------------------------------------------------- 1 | const dropboxFS = require("dropbox-fs"); 2 | const Buttercup = require("buttercup"); 3 | 4 | const TextDatasource = Buttercup.TextDatasource; 5 | const registerDatasource = Buttercup.DatasourceAdapter.registerDatasource; 6 | 7 | /** 8 | * Datasource for Dropbox archives 9 | * @augments TextDatasource 10 | */ 11 | class DropboxDatasource extends TextDatasource { 12 | 13 | /** 14 | * Datasource for Dropbox accounts 15 | * @param {String} accessToken The dropbox access token 16 | * @param {String} resourcePath The file path 17 | */ 18 | constructor(accessToken, resourcePath) { 19 | super(""); 20 | this.path = resourcePath; 21 | this.token = accessToken; 22 | this.dfs = dropboxFS({ 23 | apiKey: accessToken 24 | }); 25 | } 26 | 27 | load(password) { 28 | return (new Promise((resolve, reject) => { 29 | this.dfs.readFile(this.path, { encoding: "utf8" }, function _readFile(error, data) { 30 | if (error) { 31 | return reject(error); 32 | } 33 | return resolve(data); 34 | }); 35 | })) 36 | .then((content) => { 37 | this.setContent(content); 38 | return super.load(password); 39 | }); 40 | } 41 | 42 | save(archive, password) { 43 | return super 44 | .save(archive, password) 45 | .then((encryptedContent) => { 46 | return new Promise((resolve, reject) => { 47 | this.dfs.writeFile(this.path, encryptedContent, function _writeFile(err) { 48 | if (err) { 49 | return reject(err); 50 | } 51 | return resolve(); 52 | }); 53 | }); 54 | }); 55 | } 56 | 57 | /** 58 | * Output the datasource as an object 59 | * @returns {Object} An object describing the datasource 60 | */ 61 | toObject() { 62 | return { 63 | type: "dropbox", 64 | token: this.token, 65 | path: this.path 66 | }; 67 | } 68 | 69 | } 70 | 71 | DropboxDatasource.fromObject = function fromObject(obj) { 72 | if (obj.type === "dropbox") { 73 | return new DropboxDatasource(obj.token, obj.path); 74 | } 75 | throw new Error(`Unknown or invalid type: ${obj.type}`); 76 | }; 77 | 78 | DropboxDatasource.fromString = function fromString(str, hostCredentials) { 79 | return DropboxDatasource.fromObject(JSON.parse(str), hostCredentials); 80 | }; 81 | 82 | registerDatasource("dropbox", DropboxDatasource); 83 | 84 | module.exports = DropboxDatasource; 85 | -------------------------------------------------------------------------------- /source/EntryFinder.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Fuse = require("fuse.js"); 4 | const Buttercup = require("buttercup"); 5 | 6 | const { getAllEntries } = Buttercup.tools.searching.instance; 7 | 8 | /** 9 | * Flatten entries into a searchable structure 10 | * @param {Array.} archives An array of archives 11 | * @returns {Array.} An array of searchable objects 12 | */ 13 | function flattenEntries(archives) { 14 | return archives.reduce(function _reduceArchiveEntries(items, archive) { 15 | return [ 16 | ...items, 17 | ...getAllEntries(archive.getGroups()).map(function _expandEntry(entry) { 18 | return { 19 | entry, 20 | archive 21 | }; 22 | }) 23 | ]; 24 | }, []); 25 | } 26 | 27 | /** 28 | * @typedef {Object} EntrySearchInfo 29 | * @property {Entry} entry The entry 30 | * @property {Archive} archive The associated archive 31 | */ 32 | 33 | class EntryFinder { 34 | 35 | /** 36 | * @param {Array.|Archive} _archives The archives to search 37 | */ 38 | constructor(_archives) { 39 | let archives = Array.isArray(_archives) ? _archives : [_archives]; 40 | this._items = flattenEntries(archives); 41 | this._fuse = null; 42 | this._lastResult = []; 43 | this.initSearcher(); 44 | } 45 | 46 | /** 47 | * All items 48 | * @type {Array.} 49 | */ 50 | get items() { 51 | return this._items; 52 | } 53 | 54 | /** 55 | * The last result 56 | * @type {Array.} 57 | */ 58 | get lastResult() { 59 | return this._lastResult; 60 | } 61 | 62 | /** 63 | * Initialise the searching mechanism 64 | */ 65 | initSearcher() { 66 | this._fuse = new Fuse(this.items, { 67 | keys: [ 68 | "property.title", 69 | "property.username", 70 | "meta.URL", 71 | "meta.url" 72 | ], 73 | getFn: function _translateEntryForFuse(item, keyPath) { 74 | const entry = item.entry; 75 | const [ type, key ] = keyPath.split("."); 76 | switch (type) { 77 | case "property": { 78 | return entry.getProperty(key); 79 | } 80 | case "meta": { 81 | return entry.getMeta(key); 82 | } 83 | default: 84 | throw new Error(`Unknown entry property type: ${type}`); 85 | } 86 | }, 87 | shouldSort: true, 88 | threshold: 0.5, 89 | tokenSeparator: /\s+/g 90 | }); 91 | } 92 | 93 | /** 94 | * Search and get results 95 | * @param {String} term The search term 96 | * @returns {Array.} The results 97 | */ 98 | search(term) { 99 | this._lastResult = this._fuse.search(term); 100 | return this.lastResult; 101 | } 102 | 103 | } 104 | 105 | module.exports = EntryFinder; 106 | -------------------------------------------------------------------------------- /source/HashingTools.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Buttercup = require("buttercup"); 4 | 5 | function arrayBufferToHexString(arrayBuffer) { 6 | var byteArray = new Uint8Array(arrayBuffer); 7 | var hexString = ""; 8 | var nextHexByte; 9 | 10 | for (let i = 0; i < byteArray.byteLength; i += 1) { 11 | nextHexByte = byteArray[i].toString(16); 12 | if (nextHexByte.length < 2) { 13 | nextHexByte = "0" + nextHexByte; 14 | } 15 | hexString += nextHexByte; 16 | } 17 | return hexString; 18 | } 19 | 20 | function addHexSupportToArrayBuffer(arrayBuffer) { 21 | const _toString = arrayBuffer.toString || function NOOP() {}; 22 | arrayBuffer.toString = function(mode) { 23 | if (mode === "hex") { 24 | return arrayBufferToHexString(arrayBuffer); 25 | } 26 | return _toString.call(arrayBuffer, mode); 27 | }; 28 | return arrayBuffer; 29 | } 30 | 31 | function checkBrowserSupport() { 32 | if (!window.TextEncoder || !window.TextDecoder) { 33 | throw new Error("TextEncoder is not available"); 34 | } 35 | } 36 | 37 | function joinBuffers(buffer1, buffer2) { 38 | var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); 39 | tmp.set(new Uint8Array(buffer1), 0); 40 | tmp.set(new Uint8Array(buffer2), buffer1.byteLength); 41 | return tmp.buffer; 42 | } 43 | 44 | function stringToArrayBuffer(string) { 45 | var encoder = new TextEncoder("utf-8"); 46 | return encoder.encode(string); 47 | } 48 | 49 | const lib = module.exports = { 50 | 51 | /** 52 | * Derive a key from a password 53 | * @param {String} password The password 54 | * @param {String} salt The salt 55 | * @param {Number} rounds The number of derivation rounds 56 | * @param {Number} bits The number of bits for the key 57 | * @see checkBrowserSupport 58 | * @returns {Promise.} A promise that resolves with an ArrayBuffer 59 | */ 60 | deriveKeyFromPassword: function deriveKeyFromPassword(password, salt, rounds, bits /* , algorithm */) { 61 | checkBrowserSupport(); 62 | const subtleCrypto = window.crypto.subtle; 63 | 64 | let params = { 65 | name: "PBKDF2", 66 | hash: "SHA-256", 67 | salt: stringToArrayBuffer(salt), 68 | iterations: rounds 69 | }, 70 | bytes = bits / 8, 71 | keysLen = bytes / 2; 72 | return subtleCrypto.importKey( 73 | "raw", 74 | stringToArrayBuffer(password), 75 | { name: "PBKDF2" }, 76 | false, // not extractable 77 | ["deriveBits"] 78 | ) 79 | .then((keyData) => subtleCrypto.deriveBits(params, keyData, bits)) 80 | .then((derivedData) => Promise.all([ 81 | subtleCrypto.importKey( 82 | "raw", 83 | derivedData.slice(0, keysLen), 84 | "AES-CBC", 85 | true, 86 | ["encrypt", "decrypt"] 87 | ), 88 | subtleCrypto.importKey( 89 | "raw", 90 | derivedData.slice(keysLen, keysLen * 2), 91 | "AES-CBC", 92 | true, 93 | ["encrypt", "decrypt"] 94 | ) 95 | ])) 96 | .then((aesKeys) => Promise.all([ 97 | subtleCrypto.exportKey("raw", aesKeys[0]), 98 | subtleCrypto.exportKey("raw", aesKeys[1]) 99 | ])) 100 | .then((rawKeys) => joinBuffers(rawKeys[0], rawKeys[1])) 101 | .then((arrBuff) => addHexSupportToArrayBuffer(arrBuff)); // HAXOR 102 | }, 103 | 104 | /** 105 | * Perform patching of the PBKDF2 function in iocane 106 | * @param {Function|undefined=} handler Optionally override the internal PBKDF2 engine 107 | */ 108 | patchCorePBKDF: function patchCorePBKDF(handler = lib.deriveKeyFromPassword) { 109 | Buttercup.vendor.iocane.components.setPBKDF2(handler); 110 | } 111 | 112 | }; 113 | -------------------------------------------------------------------------------- /source/LocalStorageInterface.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Buttercup = require("buttercup"); 4 | 5 | const StorageInterface = Buttercup.storage.StorageInterface; 6 | 7 | function getStorage() { 8 | return window.localStorage; 9 | } 10 | 11 | /** 12 | * Interface for localStorage 13 | * @augments StorageInterface 14 | */ 15 | class LocalStorageInterface extends StorageInterface { 16 | 17 | constructor() { 18 | super(); 19 | this._storage = getStorage(); 20 | } 21 | 22 | get storage() { 23 | return this._storage; 24 | } 25 | 26 | /** 27 | * Get all keys from storage 28 | * @returns {Promise.>} A promise that resolves with an array of keys 29 | */ 30 | getAllKeys() { 31 | return Promise.resolve(Object.keys(this.storage)); 32 | } 33 | 34 | /** 35 | * Get the value of a key 36 | * @param {String} name The key name 37 | * @returns {Promise.} A promise that resolves with the value 38 | */ 39 | getValue(name) { 40 | const value = this.storage.getItem(name); 41 | return Promise.resolve(value); 42 | } 43 | 44 | /** 45 | * Set the value for a key 46 | * @param {String} name The key name 47 | * @param {String} value The value to set 48 | * @returns {Promise} A promise that resolves when the value is set 49 | */ 50 | setValue(name, value) { 51 | this.storage.setItem(name, value); 52 | return Promise.resolve(); 53 | } 54 | 55 | } 56 | 57 | module.exports = LocalStorageInterface; 58 | -------------------------------------------------------------------------------- /source/StorageInterface.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @name StorageInterface 5 | * @type {Object} 6 | */ 7 | module.exports = { 8 | 9 | /** 10 | * Get data from storage 11 | * @memberof StorageInterface 12 | * @name getData 13 | * @static 14 | * @param {String} key The key to fetch for 15 | * @param {*} defaultValue The default value if the key is not found 16 | * @returns {*} The fetched data 17 | */ 18 | getData: function(key, defaultValue) { 19 | var value = window.localStorage.getItem(key); 20 | return value ? JSON.parse(value) : defaultValue; 21 | }, 22 | 23 | /** 24 | * Set data for a key 25 | * @memberof StorageInterface 26 | * @name setData 27 | * @static 28 | * @param {String} key The key to set for 29 | * @param {Object|Array|String|Number|*} rawData The raw data to set 30 | */ 31 | setData: function(key, rawData) { 32 | window.localStorage.setItem(key, JSON.stringify(rawData)); 33 | } 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Buttercup = require("buttercup"); 4 | 5 | const ArchiveTools = require("__buttercup_web/ArchiveTools.js"); 6 | const EntryFinder = require("__buttercup_web/EntryFinder.js"); 7 | const HashingTools = require("__buttercup_web/HashingTools.js"); 8 | const DropboxDatasource = require("__buttercup_web/DropboxDatasource.js"); 9 | const LocalStorageInterface = require("__buttercup_web/LocalStorageInterface.js"); 10 | 11 | // Deprecated: 12 | const ArchiveManager = require("__buttercup_web/ArchiveManager.js"); 13 | const StorageInterface = require("__buttercup_web/StorageInterface.js"); 14 | 15 | module.exports = Object.assign( 16 | {}, 17 | Buttercup, 18 | { 19 | Web: { 20 | ArchiveManager, 21 | ArchiveTools, 22 | DropboxDatasource, 23 | EntryFinder, 24 | HashingTools, 25 | LocalStorageInterface, 26 | StorageInterface 27 | } 28 | } 29 | ); 30 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true, 6 | "mocha": true, 7 | "jasmine": true 8 | }, 9 | "rules": { 10 | "func-names": 0, 11 | "no-unused-expressions": 0, 12 | "padded-blocks": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/ArchiveManager.spec.js: -------------------------------------------------------------------------------- 1 | describe("ArchiveManager", function() { 2 | 3 | var Buttercup = window.Buttercup, 4 | Archive = Buttercup.Archive, 5 | createCredentials = Buttercup.createCredentials, 6 | TextDatasource = Buttercup.TextDatasource, 7 | Workspace = Buttercup.Workspace, 8 | ArchiveManager = Buttercup.Web.ArchiveManager; 9 | 10 | beforeEach(function() { 11 | this.archiveManager = new ArchiveManager({ 12 | getData: () => JSON.parse(this.savedData), 13 | setData: (name, data) => { 14 | this.savedData = JSON.stringify(data); 15 | } 16 | }); 17 | this.savedData = '{ "archives": {} }'; 18 | let testArchive = Archive.createWithDefaults(), 19 | testDatasource = new TextDatasource(); 20 | return testDatasource 21 | .save(testArchive, createCredentials.fromPassword("pass")) 22 | .then((archiveEnc) => { 23 | this.datasource = new TextDatasource(archiveEnc); 24 | this.workspace = new Workspace(); 25 | this.workspace.setPrimaryArchive(testArchive, this.datasource, createCredentials.fromPassword("pass")); 26 | let creds = createCredentials("text"); 27 | creds.setValue("datasource", this.datasource.toString()); 28 | this.archiveManager.addArchive( 29 | "test", 30 | this.workspace, 31 | creds, 32 | "pass" 33 | ); 34 | }); 35 | }); 36 | 37 | describe("isLocked", function() { 38 | 39 | it("detects unlocked entries correctly", function() { 40 | expect(this.archiveManager.isLocked("test")).to.be.false; 41 | }); 42 | 43 | it("detects locked entries correctly", function() { 44 | this.archiveManager.archives["test"].status = ArchiveManager.ArchiveStatus.LOCKED; 45 | expect(this.archiveManager.isLocked("test")).to.be.true; 46 | }); 47 | 48 | }); 49 | 50 | describe("loadState", function() { 51 | 52 | it("loads saved archives in locked state", function() { 53 | expect(this.archiveManager.archives["test"].status).to.equal(ArchiveManager.ArchiveStatus.UNLOCKED); 54 | return this.archiveManager 55 | .saveState() 56 | .then(() => this.archiveManager.loadState()) 57 | .then(() => { 58 | expect(this.archiveManager.archives.test.status).to.equal(ArchiveManager.ArchiveStatus.LOCKED); 59 | expect(this.archiveManager.archives.test.type).to.equal("text"); 60 | }); 61 | }); 62 | 63 | }); 64 | 65 | describe("lock", function() { 66 | 67 | it("locks an item successfully", function() { 68 | expect(this.archiveManager.archives["test"].status).to.equal(ArchiveManager.ArchiveStatus.UNLOCKED); 69 | return this.archiveManager 70 | .lock("test") 71 | .then(() => { 72 | let details = this.archiveManager.archives["test"]; 73 | expect(details.status).to.equal(ArchiveManager.ArchiveStatus.LOCKED); 74 | expect(details.workspace).to.be.undefined; 75 | expect(details.password).to.be.undefined; 76 | expect(typeof details.credentials).to.equal("string"); 77 | }); 78 | }); 79 | 80 | }); 81 | 82 | describe("removeArchive", function() { 83 | 84 | it("removes the archive", function() { 85 | let removed = this.archiveManager.removeArchive("test"); 86 | expect(removed).to.be.true; 87 | expect(this.archiveManager.archives.test).to.be.undefined; 88 | }); 89 | 90 | }); 91 | 92 | describe("saveState", function() { 93 | 94 | it("writes archives to storage", function() { 95 | return this.archiveManager 96 | .saveState() 97 | .then(() => { 98 | let storage = JSON.parse(this.savedData); 99 | expect(storage.archives.test.content).to.have.length.above(50); 100 | }); 101 | }); 102 | 103 | it("saves type", function() { 104 | return this.archiveManager 105 | .saveState() 106 | .then(() => { 107 | let storage = JSON.parse(this.savedData); 108 | expect(storage.archives.test.type).to.equal("text"); 109 | }); 110 | }); 111 | 112 | }); 113 | 114 | describe("unlock", function() { 115 | 116 | it("unlocks a locked item", function() { 117 | return this.archiveManager 118 | .lock("test") 119 | .then(() => { 120 | expect(this.archiveManager.archives["test"].status).to.equal(ArchiveManager.ArchiveStatus.LOCKED); 121 | return this.archiveManager.unlock("test", "pass"); 122 | }) 123 | .then(() => { 124 | expect(this.archiveManager.archives["test"].status).to.equal(ArchiveManager.ArchiveStatus.UNLOCKED); 125 | }); 126 | }); 127 | 128 | }); 129 | 130 | }); 131 | -------------------------------------------------------------------------------- /tests/ArchiveTools.spec.js: -------------------------------------------------------------------------------- 1 | describe("ArchiveManager", function() { 2 | 3 | "use strict"; 4 | 5 | var Buttercup = window.Buttercup; 6 | 7 | var ButtercupWeb = window.Buttercup.Web, 8 | ArchiveTools = ButtercupWeb.ArchiveTools; 9 | 10 | describe("extractDomain", function() { 11 | 12 | it("extracts from full URLs", function() { 13 | expect(ArchiveTools.extractDomain( 14 | "http://www.example.com/test-area/index.html" 15 | )).to.equal("www.example.com"); 16 | expect(ArchiveTools.extractDomain( 17 | "sub1.example.website.com/test-area/index.php?abc" 18 | )).to.equal("sub1.example.website.com"); 19 | expect(ArchiveTools.extractDomain( 20 | "https://abc.cn" 21 | )).to.equal("abc.cn"); 22 | }); 23 | 24 | it("extracts from domains only", function() { 25 | expect(ArchiveTools.extractDomain("www.site.com.au")).to.equal("www.site.com.au"); 26 | expect(ArchiveTools.extractDomain("example.org")).to.equal("example.org"); 27 | }); 28 | 29 | }); 30 | 31 | describe("getEntriesForURL", function() { 32 | 33 | var archive; 34 | 35 | beforeEach(function() { 36 | archive = new Buttercup.Archive(); 37 | var group1 = archive.createGroup("Group 1"), 38 | group2 = archive.createGroup("Group 2"); 39 | var entry1 = group1.createEntry("Entry 1"), 40 | entry2 = group2.createEntry("Entry 2"), 41 | entry3 = group1.createEntry("Entry 3"), 42 | entry4 = group2.createEntry("Entry 3"); 43 | entry1.setProperty("username", "entry1"); 44 | entry1.setMeta("URL", "http://www.example.com/test-area/index.html"); 45 | entry2.setProperty("username", "entry2"); 46 | entry2.setMeta("URL", "www.example.com/test-area/"); 47 | entry3.setProperty("username", "entry3"); 48 | entry3.setMeta("URL", "https://login.amazing.com/entry-portal"); 49 | entry4.setProperty("username", "entry4"); 50 | entry4.setMeta("URL", "invalid"); 51 | }); 52 | 53 | it("fetches similar URLs", function() { 54 | var currentURL = "http://www.example.com/test-area/index.html#testing", 55 | entries = ArchiveTools.getEntriesForURL(archive, currentURL); 56 | expect(entries.length).to.equal(2); 57 | }); 58 | 59 | it("fetches a single URL", function() { 60 | var currentURL = "https://login.amazing.com/entry-portal", 61 | entries = ArchiveTools.getEntriesForURL(archive, currentURL); 62 | expect(entries.length).to.equal(1); 63 | expect(entries[0].getProperty("username")).to.equal("entry3"); 64 | }); 65 | 66 | it("ignores empty/non-matching URLs", function() { 67 | var currentURL = "invalid", 68 | entries = ArchiveTools.getEntriesForURL(archive, currentURL); 69 | expect(entries.length).to.equal(0); 70 | }); 71 | 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /tests/Buttercup.Archive.spec.js: -------------------------------------------------------------------------------- 1 | describe("Buttercup.Archive", function() { 2 | 3 | "use strict"; 4 | 5 | it("exists on the window", function() { 6 | expect(window.Buttercup.Archive).to.not.be.undefined; 7 | }); 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /tests/Buttercup.vendor.spec.js: -------------------------------------------------------------------------------- 1 | describe("Buttercup.vendor", function() { 2 | 3 | "use strict"; 4 | 5 | describe("iocane", function() { 6 | 7 | it("derives the correct password", function(done) { 8 | window.Buttercup.vendor.iocane.derivation.deriveFromPassword("p455", "salt", 30000) 9 | .then(function(hash) { 10 | return hash.key.toString("hex"); 11 | }) 12 | .then(function(hash) { 13 | expect(hash).to.equal("3206e36d28a3139def5037d1a0d25b4eb12cd8a70f715517206e7f07f8f8dd2b"); 14 | (done)(); 15 | }) 16 | .catch(function(err) { 17 | console.error(err); 18 | }); 19 | }); 20 | 21 | it("handles a large amount of rounds", function(done) { 22 | window.Buttercup.vendor.iocane.derivation.deriveFromPassword("some-password", "123salt", 250000) 23 | .then((hash) => hash.key.toString("hex")) 24 | .then(function(hash) { 25 | expect(hash).to.equal("c6aabb1f7cf5a74f39d74a72e4b5708407de63177deddf86f930dc0197100acd"); 26 | }) 27 | .then(done) 28 | .catch(function(err) { 29 | console.error(err); 30 | }); 31 | }); 32 | 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /tests/EntryFinder.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("EntryFinder", function() { 4 | 5 | const { Archive } = window.Buttercup; 6 | const { EntryFinder } = window.Buttercup.Web; 7 | 8 | it("supports multiple archives", function() { 9 | const a1 = new Archive(); 10 | const a2 = new Archive(); 11 | const e1 = a1.createGroup("main").createEntry("test"); 12 | const e2 = a2.createGroup("main").createEntry("test"); 13 | const finder = new EntryFinder([a1, a2]); 14 | expect(finder.search("test")).to.eql([ { entry: e1, archive: a1 }, { entry: e2, archive: a2 } ]); 15 | }); 16 | 17 | describe("searching by title", function() { 18 | 19 | beforeEach(function() { 20 | // mock data 21 | this.archive = new Archive(); 22 | let groupA = this.archive.createGroup("GroupA"), 23 | groupB = this.archive.createGroup("GroupB"), 24 | groupC = groupB.createGroup("GroupC"); 25 | this.entry1 = groupA.createEntry("My personal bank"); 26 | this.entry2 = groupB.createEntry("Other banking login"); 27 | this.entry3 = groupC.createEntry("Car ranking site"); 28 | this.entry4 = groupC.createEntry("Unicorns are great"); 29 | this.finder = new EntryFinder(this.archive); 30 | }); 31 | 32 | it("returns no entries for empty search string", function() { 33 | let entries = this.finder.search(""); 34 | expect(entries).to.have.lengthOf(0); 35 | }); 36 | 37 | it("searches all levels for entries", function() { 38 | let entries = this.finder.search("a"); 39 | expect(entries.map(i => i.entry.getProperty("title")).sort()).to.eql([ 40 | "My personal bank", 41 | "Other banking login", 42 | "Car ranking site", 43 | "Unicorns are great" 44 | ].sort()); 45 | }); 46 | 47 | it("returns entries related to a term", function() { 48 | let entries = this.finder.search("bank"); 49 | expect(entries.map(i => i.entry.getProperty("title")).sort()).to.eql([ 50 | "My personal bank", 51 | "Other banking login", 52 | "Car ranking site" 53 | ].sort()); 54 | }); 55 | 56 | }); 57 | 58 | describe("searching by username", function() { 59 | 60 | beforeEach(function() { 61 | // mock data 62 | this.archive = new Archive(); 63 | let groupA = this.archive.createGroup("GroupA"), 64 | groupB = this.archive.createGroup("GroupB"); 65 | this.entry1 = groupA.createEntry("My personal bank"); 66 | this.entry2 = groupB.createEntry("Other banking login"); 67 | this.entry1.setProperty("username", "john@myawesomeblog.org"); 68 | this.entry2.setProperty("username", "danielle487@myawesomeblog.org"); 69 | this.finder = new EntryFinder(this.archive); 70 | }); 71 | 72 | it("returns entries by username searches", function() { 73 | let entries = this.finder.search("john"); 74 | expect(entries.map(i => i.entry.getProperty("username")).sort()).to.eql([ 75 | "john@myawesomeblog.org" 76 | ]); 77 | }); 78 | 79 | it("returns entries for similar usernames", function() { 80 | let entries = this.finder.search("blog"); 81 | expect(entries.map(i => i.entry.getProperty("username")).sort()).to.eql([ 82 | "danielle487@myawesomeblog.org", 83 | "john@myawesomeblog.org" 84 | ]); 85 | }); 86 | 87 | }); 88 | 89 | describe("searching by URL", function() { 90 | 91 | beforeEach(function() { 92 | // mock data 93 | this.archive = new Archive(); 94 | let groupA = this.archive.createGroup("GroupA"), 95 | groupB = this.archive.createGroup("GroupB"); 96 | this.entry1 = groupA.createEntry("My personal bank"); 97 | this.entry2 = groupB.createEntry("Other banking login"); 98 | this.entry1.setMeta("URL", "https://secure.shopping.com/login"); 99 | this.entry2.setMeta("url", "http://www.someplace.org/shopping/entry.php"); 100 | this.finder = new EntryFinder(this.archive); 101 | }); 102 | 103 | it("returns entries by URL searches", function() { 104 | let entries = this.finder.search("secure"); 105 | expect(entries.map(i => i.entry.getMeta("URL") || i.entry.getMeta("url")).sort()).to.eql([ 106 | "https://secure.shopping.com/login" 107 | ]); 108 | }); 109 | 110 | it("returns entries for similar URLs", function() { 111 | let entries = this.finder.search("shopping"); 112 | expect(entries.map(i => i.entry.getMeta("URL") || i.entry.getMeta("url")).sort()).to.eql([ 113 | "http://www.someplace.org/shopping/entry.php", 114 | "https://secure.shopping.com/login" 115 | ]); 116 | }); 117 | 118 | }); 119 | 120 | }); 121 | -------------------------------------------------------------------------------- /tests/LocalStorageInterface.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("LocalStorageInterface", function() { 4 | 5 | const { LocalStorageInterface } = window.Buttercup.Web; 6 | 7 | beforeEach(function() { 8 | this.lsi = new LocalStorageInterface(); 9 | this.storage = { 10 | values: {}, 11 | getItem: key => typeof this.storage.values[key] === "string" ? this.storage.values[key] : null, 12 | setItem: (key, value) => { 13 | this.storage.values[key] = value; 14 | } 15 | }; 16 | this.lsi._storage = this.storage; 17 | }); 18 | 19 | describe("getAllKeys", function() { 20 | 21 | beforeEach(function() { 22 | this.storage.values["some key"] = "test"; 23 | this.storage.values.key2 = "another value"; 24 | this.lsi._storage = this.storage.values; 25 | }); 26 | 27 | it("returns all keys", function() { 28 | return this.lsi.getAllKeys().then(keys => { 29 | expect(keys).to.contain("some key"); 30 | expect(keys).to.contain("key2"); 31 | expect(keys).to.have.lengthOf(2); 32 | }); 33 | }); 34 | 35 | }); 36 | 37 | describe("getValue", function() { 38 | 39 | beforeEach(function() { 40 | this.storage.values["magicValue"] = "test"; 41 | }); 42 | 43 | it("returns the correct value", function() { 44 | return this.lsi.getValue("magicValue").then(function(value) { 45 | expect(value).to.equal("test"); 46 | }); 47 | }); 48 | 49 | it("returns null if no value found", function() { 50 | return this.lsi.getValue("magicValue2").then(function(value) { 51 | expect(value).to.be.null; 52 | }); 53 | }); 54 | 55 | }); 56 | 57 | describe("setValue", function() { 58 | 59 | beforeEach(function() { 60 | this.storage.values["magicValue"] = "test"; 61 | }); 62 | 63 | it("sets new values", function() { 64 | return this.lsi.setValue("item", "1 2 3").then(() => { 65 | expect(this.storage.values.item).to.equal("1 2 3"); 66 | }); 67 | }); 68 | 69 | it("overwrites existing values", function() { 70 | return this.lsi.setValue("magicValue", "[true]").then(() => { 71 | expect(this.storage.values.magicValue).to.equal("[true]"); 72 | }); 73 | }); 74 | 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | window.Buttercup.Web.HashingTools.patchCorePBKDF(); 2 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const webpack = require("webpack"); 4 | const UglifyJSPlugin = require("uglifyjs-webpack-plugin"); 5 | const Visualizer = require("webpack-visualizer-plugin"); 6 | const LodashModuleReplacementPlugin = require("lodash-webpack-plugin"); 7 | 8 | // const defines = { 9 | // "global.GENTLY": false 10 | // }; 11 | 12 | const SOURCE = path.resolve(__dirname, "./source"); 13 | const DIST = path.resolve(__dirname, "./dist"); 14 | const NODE_MODULES = path.resolve(__dirname, "./node_modules"); 15 | const BUTTERCUP_CORE = fs.realpathSync(path.resolve(NODE_MODULES, "./buttercup")); 16 | const IOCANE = fs.realpathSync(path.resolve(NODE_MODULES, "./iocane")); 17 | const WEBDAVFS = fs.realpathSync(path.resolve(NODE_MODULES, "./webdav-fs")); 18 | 19 | const entry = path.resolve(SOURCE, "./index.js"); 20 | const rules = [ 21 | { 22 | test: /\.json$/i, 23 | use: "json-loader" 24 | }, 25 | { 26 | test: /\.js$/, 27 | use: "babel-loader", 28 | include: [ 29 | SOURCE, 30 | BUTTERCUP_CORE, 31 | IOCANE, 32 | WEBDAVFS 33 | ] 34 | } 35 | ]; 36 | const node = { 37 | crypto: false, 38 | fs: "empty" 39 | }; 40 | const resolve = { 41 | alias: { 42 | crypto: require.resolve("crypto-browserify"), 43 | __buttercup_web: SOURCE 44 | }, 45 | extensions: [".js"], 46 | symlinks: false, 47 | modules: [ NODE_MODULES, BUTTERCUP_CORE, IOCANE, WEBDAVFS ] 48 | }; 49 | const resolveRN = Object.assign({}, resolve); 50 | resolveRN.alias.dropbox = "react-native-dropbox-sdk"; 51 | const stats = { colors: true }; 52 | const developmentPlugins = process.env.VIS === "stats" ? 53 | [ new Visualizer() ] : []; 54 | 55 | module.exports = [ 56 | 57 | // Raw 58 | { 59 | entry, 60 | module: { rules }, 61 | node, 62 | output: { 63 | path: DIST, 64 | filename: "buttercup.js", 65 | library: "Buttercup", 66 | libraryTarget: "umd" 67 | }, 68 | plugins: [ 69 | // new webpack.DefinePlugin(defines), 70 | new LodashModuleReplacementPlugin(), 71 | new webpack.NormalModuleReplacementPlugin(/\/iconv-loader$/, "node-noop"), 72 | new webpack.IgnorePlugin(/vertx/), 73 | ...developmentPlugins 74 | ], 75 | resolve, 76 | stats 77 | }, 78 | 79 | // Minified 80 | { 81 | entry, 82 | module: { rules }, 83 | node, 84 | output: { 85 | path: DIST, 86 | filename: "buttercup.min.js", 87 | library: "Buttercup", 88 | libraryTarget: "umd" 89 | }, 90 | plugins: [ 91 | // new webpack.DefinePlugin(defines), 92 | new LodashModuleReplacementPlugin(), 93 | new webpack.NormalModuleReplacementPlugin(/\/iconv-loader$/, "node-noop"), 94 | new webpack.IgnorePlugin(/vertx/), 95 | new UglifyJSPlugin({ 96 | compress: { 97 | warnings: false 98 | }, 99 | mangle: true, 100 | comments: false 101 | }) 102 | ], 103 | resolve, 104 | stats 105 | }, 106 | 107 | // Minified + React-Native compat 108 | { 109 | entry, 110 | module: { rules }, 111 | node, 112 | output: { 113 | path: DIST, 114 | filename: "react-native-buttercup.min.js", 115 | library: "Buttercup", 116 | libraryTarget: "umd" 117 | }, 118 | plugins: [ 119 | // new webpack.DefinePlugin(defines), 120 | new LodashModuleReplacementPlugin(), 121 | new webpack.NormalModuleReplacementPlugin(/\/iconv-loader$/, "node-noop"), 122 | new webpack.IgnorePlugin(/vertx/), 123 | new UglifyJSPlugin({ 124 | compress: { 125 | warnings: false 126 | }, 127 | mangle: true, 128 | comments: false 129 | }) 130 | ], 131 | resolve: resolveRN, 132 | stats 133 | } 134 | 135 | ]; 136 | --------------------------------------------------------------------------------