This repository has been archived by the owner on Aug 22, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
/
safari.go
480 lines (383 loc) · 12.9 KB
/
safari.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
//
// Copyright (c) 2016 Dean Jackson <deanishe@deanishe.net>
//
// MIT Licence. See http://opensource.org/licenses/MIT
//
// Created on 2016-05-29
//
// TODO: Add iCloud devices & tabs as Folders and Bookmarks
/*
Package safari provides access to Safari's windows, tabs, bookmarks etc. on the Mac.
Package-level functions call the corresponding methods on the default Parser, which
reads the standard Safari bookmarks file with the default options.
The history subpackage provides access to Safari's history.
The safari command is a simple command-line program that implements some of the
library's features.
Tested on Sierra and High Sierra.
*/
package safari
import (
"errors"
"io/ioutil"
"log"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"howett.net/plist"
)
// Types of entries in Bookmarks.plist.
const (
WebBookmarkTypeLeaf = "WebBookmarkTypeLeaf"
WebBookmarkTypeList = "WebBookmarkTypeList"
WebBookmarkTypeProxy = "WebBookmarkTypeProxy"
)
// Names of special folders.
const (
NameBookmarksBar = "BookmarksBar"
NameBookmarksMenu = "BookmarksMenu"
NameReadingList = "com.apple.ReadingList"
)
// Types of objects with UIDs
const (
TypeFolder = "folder"
TypeBookmark = "bookmark"
)
// Default options.
var (
DefaultBookmarksPath = filepath.Join(os.Getenv("HOME"), "Library/Safari/Bookmarks.plist")
DefaultIgnoreBookmarklets = false
// DefaultCloudTabsPath = filepath.Join(os.Getenv("HOME"), "Library/SyncedPreferences/com.apple.Safari.plist")
parser *Parser // Default parser
)
// Item is implemented by Folder and Bookmark.
type Item interface {
Title() string
UID() string
}
// rawRL contains the reading list metadata for a RawBookmark.
type rawRL struct {
DateAdded time.Time
DateLastFetched time.Time
DateLastViewed time.Time
PreviewText string
}
// rawBookmark is the data model used in the Bookmarks.plist file.
type rawBookmark struct {
RawTitle string `plist:"Title"`
Type string `plist:"WebBookmarkType"`
URL string `plist:"URLString"`
UUID string `plist:"WebBookmarkUUID"`
ReadingList *rawRL `plist:"ReadingList"`
URIDict map[string]string `plist:"URIDictionary"`
Children []*rawBookmark
}
// Title returns either RawTitle (if set) or the title from URIDict.
func (rb *rawBookmark) Title() string {
if rb.RawTitle != "" {
return rb.RawTitle
}
return rb.URIDict["title"]
}
// Folder contains Bookmarks and other Folders.
type Folder struct {
title string
Ancestors []*Folder // Last element is this Folder's parent. May be empty.
Bookmarks []*Bookmark // Bookmarks within this folder
Folders []*Folder // Child folders
uid string
isReadingList bool
isBookmarksBar bool
isBookmarksMenu bool
}
// Title returns Folder title and implements Item.
func (f *Folder) Title() string { return f.title }
// UID returns Folder UID and implements Item.
func (f *Folder) UID() string { return f.uid }
// IsReadingList returns true if this Folder is the user's Reading List.
func (f *Folder) IsReadingList() bool { return f.isReadingList }
// IsBookmarksBar returns true if this Folder is the users's BookmarksBar.
func (f *Folder) IsBookmarksBar() bool { return f.isBookmarksBar }
// IsBookmarksMenu returns true if this Folder is the users's BookmarksMenu.
func (f *Folder) IsBookmarksMenu() bool { return f.isBookmarksMenu }
// Bookmark is a Safari bookmark.
type Bookmark struct {
title string
URL string
Ancestors []*Folder // Last element is this Bookmark's parent
Preview string
uid string
}
// Title returns Bookmark title and implements Item.
func (bm *Bookmark) Title() string { return bm.title }
// UID returns Bookmark UID and implements Item.
func (bm *Bookmark) UID() string { return bm.uid }
// Folder returns Folder containing Bookmark. May be nil.
func (bm *Bookmark) Folder() *Folder {
if len(bm.Ancestors) == 0 {
return nil
}
return bm.Ancestors[len(bm.Ancestors)-1]
}
// InReadingList returns true if Bookmark is from the Reading List.
func (bm *Bookmark) InReadingList() bool {
f := bm.Folder()
if f == nil {
return false
}
return f.IsReadingList()
}
// IsBookmarklet returns true if Bookmark is a bookmarklet.
func (bm *Bookmark) IsBookmarklet() bool {
return strings.HasPrefix(bm.URL, "javascript:")
}
// Hostname returns the hostname (without port) of Bookmark's URL.
func (bm *Bookmark) Hostname() (string, error) {
u, err := url.Parse(bm.URL)
if err != nil {
return "", err
}
return u.Hostname(), nil
}
// ToJS returns JavaScript embedded in the URL. Returns an error if the
// bookmark isn't a bookmarklet or can't be parsed.
func (bm *Bookmark) ToJS() (string, error) {
if !bm.IsBookmarklet() {
return "", errors.New("not a bookmarklet")
}
return url.PathUnescape(bm.URL[11:])
}
// Option sets a Parser option.
type Option func(*Parser)
// BookmarksPath sets the path to the Safari bookmarks plist.
func BookmarksPath(path string) Option {
return func(p *Parser) { p.BookmarksPath = path }
}
/*
// CloudTabsPath sets the path to the Safari iCloud tabs plist.
func CloudTabsPath(path string) Option {
return func(p *Parser) { p.CloudTabsPath = path }
}
*/
// IgnoreBookmarklets tells parser whether to ignore bookmarklets.
func IgnoreBookmarklets(v bool) Option {
return func(p *Parser) { p.IgnoreBookmarklets = v }
}
// Parser unmarshals a Bookmarks.plist file.
type Parser struct {
BookmarksPath string
IgnoreBookmarklets bool // Whether to ignore bookmarklets
Bookmarks []*Bookmark // Flat list of all bookmarks (excl. Reading List)
BookmarksRL []*Bookmark // Flat list of all Reading List bookmarks
Folders []*Folder // Flat list of all folders
BookmarksBar *Folder // Folder for user's Bookmarks Bar
BookmarksMenu *Folder // Folder for user's Bookmarks Menu
ReadingList *Folder // Folder for user's Reading List
raw *rawBookmark // Bookmarks.plist data in "native" format
uid2Folder map[string]*Folder
uid2Bookmark map[string]*Bookmark
uid2Type map[string]string
}
// New creates a new Parser with the specified options and calls Parser.Parse().
func New(opts ...Option) (*Parser, error) {
p := &Parser{
BookmarksPath: DefaultBookmarksPath,
IgnoreBookmarklets: DefaultIgnoreBookmarklets,
uid2Folder: map[string]*Folder{},
uid2Bookmark: map[string]*Bookmark{},
uid2Type: map[string]string{},
}
p.Configure(opts...)
if err := p.Parse(); err != nil {
return nil, err
}
return p, nil
}
// Configure applies an Option to Parser.
func (p *Parser) Configure(opts ...Option) {
for _, opt := range opts {
opt(p)
}
}
// Parse unmarshals a Bookmarks.plist.
func (p *Parser) Parse() error {
// TODO: Make Bookmarks.plist optional and add iCloud tabs
data, err := ioutil.ReadFile(p.BookmarksPath)
if err != nil {
return err
}
return p.parseData(data)
}
// parseData does the actual parsing.
func (p *Parser) parseData(data []byte) error {
p.raw = &rawBookmark{}
p.Bookmarks = []*Bookmark{}
p.BookmarksRL = []*Bookmark{}
if _, err := plist.Unmarshal(data, p.raw); err != nil {
return err
}
if err := p.parseRaw(p.raw, []*Folder{}); err != nil {
return err
}
return nil
}
// parse flattens the raw tree and parses the RawBookmarks into Bookmarks.
func (p *Parser) parseRaw(root *rawBookmark, ancestors []*Folder) error {
for _, rb := range root.Children {
switch rb.Type {
case WebBookmarkTypeProxy: // Ignore. Only History, which is empty
continue
case WebBookmarkTypeList: // Folder
f := &Folder{
title: rb.Title(),
Ancestors: ancestors,
uid: rb.UUID,
}
// Add all folders to Parser
p.Folders = append(p.Folders, f)
p.uid2Folder[rb.UUID] = f
p.uid2Type[rb.UUID] = TypeFolder
if len(ancestors) == 0 { // Check if it's a special folder
switch f.Title() {
case NameBookmarksBar:
f.title = "Favorites"
f.isBookmarksBar = true
p.BookmarksBar = f
case NameBookmarksMenu:
f.title = "Bookmarks Menu"
f.isBookmarksMenu = true
p.BookmarksMenu = f
case NameReadingList:
f.title = "Reading List"
f.isReadingList = true
p.ReadingList = f
// default:
// log.Printf("Unknown top-Level folder: %s", f.Title())
}
} else { // Just some normal folder
par := ancestors[len(ancestors)-1]
par.Folders = append(par.Folders, f)
}
if err := p.parseRaw(rb, append(ancestors, f)); err != nil {
return err
}
case WebBookmarkTypeLeaf: // Bookmark
if p.IgnoreBookmarklets && strings.HasPrefix(rb.URL, "javascript:") {
continue
}
bm := &Bookmark{
title: rb.Title(),
URL: rb.URL,
Ancestors: ancestors,
uid: rb.UUID,
}
p.uid2Bookmark[rb.UUID] = bm
p.uid2Type[rb.UUID] = TypeBookmark
if rb.ReadingList != nil {
bm.Preview = rb.ReadingList.PreviewText
}
if len(ancestors) > 0 {
par := ancestors[len(ancestors)-1]
par.Bookmarks = append(par.Bookmarks, bm)
if ancestors[0].isReadingList {
// log.Printf("[ReadingList] + %s", bm.Title)
p.BookmarksRL = append(p.BookmarksRL, bm)
} else {
// log.Printf("%v %s", parents, bm.Title)
p.Bookmarks = append(p.Bookmarks, bm)
}
} else { // Top-level bookmark
p.Bookmarks = append(p.Bookmarks, bm)
}
default:
log.Printf("%v %s", ancestors, rb.Type)
}
}
return nil
}
// BookmarkForUID returns Bookmark with given UID (or nil if no such bookmark is found).
func (p *Parser) BookmarkForUID(uid string) *Bookmark { return p.uid2Bookmark[uid] }
// FilterBookmarks returns all Bookmarks for which accept(bm) returns true.
func (p *Parser) FilterBookmarks(accept func(bm *Bookmark) bool) []*Bookmark {
r := []*Bookmark{}
for _, bm := range p.Bookmarks {
if accept(bm) {
r = append(r, bm)
}
}
return r
}
// FindBookmark returns the first Bookmark for which accept(bm) returns true.
func (p *Parser) FindBookmark(accept func(bm *Bookmark) bool) *Bookmark {
for _, bm := range p.Bookmarks {
if accept(bm) {
return bm
}
}
return nil
}
// FilterFolders returns all Folders for which accept(bm) returns true.
func (p *Parser) FilterFolders(accept func(f *Folder) bool) []*Folder {
r := []*Folder{}
for _, f := range p.Folders {
if accept(f) {
r = append(r, f)
}
}
return r
}
// FindFolder returns the first Folder for which accept(bm) returns true.
func (p *Parser) FindFolder(accept func(f *Folder) bool) *Folder {
for _, f := range p.Folders {
if accept(f) {
return f
}
}
return nil
}
// FolderForUID returns Folder with given UID (or nil if no such folder is found).
func (p *Parser) FolderForUID(uid string) *Folder { return p.uid2Folder[uid] }
// TypeForUID returns the type of item that UID refers to ("bookmark" or "folder").
func (p *Parser) TypeForUID(uid string) string { return p.uid2Type[uid] }
// getParser returns the default Parser, creating it if necessary.
func getParser() *Parser {
if parser != nil {
return parser
}
parser, err := New()
if err != nil {
panic(err)
}
return parser
}
// Configure sets options on the default parser.
func Configure(opts ...Option) { getParser().Configure(opts...) }
// Bookmarks returns all of the user's bookmarks.
func Bookmarks() []*Bookmark { return getParser().Bookmarks }
// BookmarksRL returns bookmarks for the user's Reading List.
func BookmarksRL() []*Bookmark { return getParser().BookmarksRL }
// FilterBookmarks calls Bookmarks() and returns the elements for which accept(bm) returns true.
func FilterBookmarks(accept func(bm *Bookmark) bool) []*Bookmark {
return getParser().FilterBookmarks(accept)
}
// FindBookmark returns the first Bookmark for which accept(bm) returns true.
// Returns nil if no match is found.
func FindBookmark(accept func(bm *Bookmark) bool) *Bookmark { return getParser().FindBookmark(accept) }
// BookmarkForUID returns Bookmark with specified UID or nil.
func BookmarkForUID(uid string) *Bookmark { return getParser().uid2Bookmark[uid] }
// BookmarksBar returns user's Bookmarks Bar folder.
func BookmarksBar() *Folder { return getParser().BookmarksBar }
// BookmarksMenu returns user's Bookmarks Menu folder.
func BookmarksMenu() *Folder { return getParser().BookmarksMenu }
// Folders returns all of a user's bookmark folders.
func Folders() []*Folder { return getParser().Folders }
// ReadingList returns user's Reading List folder.
func ReadingList() *Folder { return getParser().ReadingList }
// FindFolder returns the first Folder for which accept(f) returns true.
// Returns nil if no match is found.
func FindFolder(accept func(f *Folder) bool) *Folder { return getParser().FindFolder(accept) }
// FilterFolders returns all Folders for which accept(f) returns true.
func FilterFolders(accept func(f *Folder) bool) []*Folder { return getParser().FilterFolders(accept) }
// FolderForUID returns Folder with UID uid or nil.
func FolderForUID(uid string) *Folder { return getParser().uid2Folder[uid] }