├── .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 | - DropboxDatasource ⇐
TextDatasource
8 | Datasource for Dropbox archives
9 |
10 | - EntryFinder
11 |
12 | - LocalStorageInterface ⇐
StorageInterface
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 | [](https://buttercup.pw) [](https://gitter.im/buttercup-pw/buttercup-core-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://travis-ci.org/buttercup/buttercup-core-web)
5 |
6 | [](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 |
--------------------------------------------------------------------------------