iphone_backup.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741
  1. const log = require('./log')
  2. const path = require('path')
  3. const sqlite3 = require('sqlite3')
  4. const bplist = require('bplist-parser')
  5. const fs = require('fs')
  6. const plist = require('plist')
  7. // Cookie Parser
  8. const cookieParser = require('./cookies.js')
  9. // Normalize mac addresses in wifi output
  10. const macParse = require('./mac_address_parse')
  11. // Derive filenames based on domain + file path
  12. const fileHash = require('./backup_filehash')
  13. // Manifest.mbdb parser
  14. const manifestMBDBParse = require('./manifest_mbdb_parse')
  15. const databases = {
  16. SMS: fileHash('Library/SMS/sms.db'),
  17. Contacts: fileHash('Library/AddressBook/AddressBook.sqlitedb'),
  18. Calendar: fileHash('Library/Calendar/Calendar.sqlitedb'),
  19. Reminders: fileHash('Library/Calendar/Calendar.sqlitedb'),
  20. Notes: fileHash('Library/Notes/notes.sqlite'),
  21. Notes2: fileHash('NoteStore.sqlite', 'AppDomainGroup-group.com.apple.notes'),
  22. AddressBook: fileHash('Library/AddressBook/AddressBook.sqlitedb'),
  23. 'Cookies.binarycookies': '69b1865768101bacde5b77ccc44445cea9ce1261',
  24. Calls: '2b2b0084a1bc3a5ac8c27afdf14afb42c61a19ca',
  25. Calls2: fileHash('Library/CallHistoryDB/CallHistory.storedata'),
  26. Locations: fileHash('Library/Caches/locationd/consolidated.db', 'RootDomain'),
  27. WebHistory: fileHash('Library/Safari/History.db', 'AppDomain-com.apple.mobilesafari'),
  28. Photos: fileHash('Media/PhotoData/Photos.sqlite', 'CameraRollDomain'),
  29. WiFi: fileHash('SystemConfiguration/com.apple.wifi.plist', 'SystemPreferencesDomain'),
  30. Voicemail: fileHash('Library/Voicemail/voicemail.db'),
  31. SafariBookmarks: fileHash('Library/Safari/Bookmarks.db')
  32. }
  33. var cache = {}
  34. class IPhoneBackup {
  35. constructor (id, status, info, manifest, base) {
  36. this.id = id
  37. this.status = status
  38. this.info = info
  39. this.manifest = manifest
  40. this.base = base
  41. }
  42. // Open a backup with a specified ID
  43. // base is optional and will be computed if not used.
  44. static fromID (id, base) {
  45. // Get the path of the folder.
  46. if (base) {
  47. base = path.join(base, id)
  48. } else {
  49. base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', id)
  50. }
  51. // Parse manifest bplist files
  52. try {
  53. log.verbose('parsing status', base)
  54. var status = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Status.plist')))[0]
  55. } catch (e) {
  56. log.error('Cannot open Status.plist', e)
  57. }
  58. try {
  59. log.verbose('parsing manifest', base)
  60. var manifest = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Manifest.plist')))[0]
  61. } catch (e) {
  62. log.error('Cannot open Manifest.plist', e)
  63. }
  64. try {
  65. log.verbose('parsing status', base)
  66. var info = plist.parse(fs.readFileSync(path.join(base, 'Info.plist'), 'utf8'))
  67. } catch (e) {
  68. log.error('Cannot open Info.plist', e)
  69. }
  70. return new IPhoneBackup(id, status, info, manifest, base)
  71. }
  72. get iOSVersion () {
  73. return this.manifest.Lockdown.ProductVersion
  74. }
  75. getFileName (fileID, isAbsoulte) {
  76. isAbsoulte = isAbsoulte || false
  77. // const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', this.id)
  78. // Return v2 filename
  79. if (this.status.Version < 3 || isAbsoulte) {
  80. return path.join(this.base, fileID)
  81. } else {
  82. // v3 has folders
  83. return path.join(this.base, fileID.substr(0, 2), fileID)
  84. }
  85. }
  86. openDatabase (fileID, isAbsoulte) {
  87. return new Promise((resolve, reject) => {
  88. isAbsoulte = isAbsoulte || false
  89. // Get the backup folder
  90. // Return v2 filename
  91. if (this.status.Version < 3 || isAbsoulte) {
  92. let db = new sqlite3.Database(path.join(this.base, fileID), sqlite3.OPEN_READONLY, (err) => {
  93. if (err) {
  94. return reject(err)
  95. }
  96. resolve(db)
  97. })
  98. } else {
  99. // v3 has folders
  100. let db = new sqlite3.Database(path.join(this.base, fileID.substr(0, 2), fileID), sqlite3.OPEN_READONLY, (err) => {
  101. if (err) {
  102. return reject(err)
  103. }
  104. resolve(db)
  105. })
  106. }
  107. })
  108. }
  109. /// This is deprecated. Use openDatabase Instead.
  110. getDatabase (fileID, isAbsoulte) {
  111. isAbsoulte = isAbsoulte || false
  112. // Get the backup folder
  113. // Return v2 filename
  114. if (this.status.Version < 3 || isAbsoulte) {
  115. return new sqlite3.Database(path.join(this.base, fileID), sqlite3.OPEN_READONLY, (err) => {
  116. if (err) {
  117. log.error('PANIC::', err)
  118. process.exit(1)
  119. }
  120. })
  121. } else {
  122. // v3 has folders
  123. return new sqlite3.Database(path.join(this.base, fileID.substr(0, 2), fileID), sqlite3.OPEN_READONLY, (err) => {
  124. if (err) {
  125. log.error('PANIC::', err)
  126. process.exit(1)
  127. }
  128. })
  129. }
  130. }
  131. queryDatabase (databaseID, sql) {
  132. return new Promise((resolve, reject) => {
  133. var messagedb = this.getDatabase(databaseID)
  134. messagedb.all(sql, async function (err, rows) {
  135. if (err) reject(err)
  136. resolve(rows)
  137. })
  138. })
  139. }
  140. getName (messageDest) {
  141. return new Promise((resolve, reject) => {
  142. if (messageDest.indexOf('@') === -1) {
  143. messageDest = messageDest.replace(/[\s+\-()]*/g, '')
  144. if (messageDest.length === 11 && messageDest[0] === '1') {
  145. messageDest = messageDest.substring(1)
  146. }
  147. }
  148. if (cache[messageDest] !== undefined) {
  149. return resolve(cache[messageDest])
  150. }
  151. var contactdb = this.getDatabase(databases.Contacts)
  152. contactdb.get(`SELECT
  153. c0First as first,
  154. c1Last as last,
  155. c2Middle as middle,
  156. c15Phone as phones
  157. from ABPersonFullTextSearch_content WHERE c15Phone like '%${messageDest}%'`,
  158. (err, row) => {
  159. if (err) return resolve()
  160. if (!row) return resolve()
  161. var result = {
  162. name: [row.first, row.middle, row.last].filter(el => el != null).join(' '),
  163. phones: row.phones.split(' '),
  164. query: messageDest
  165. }
  166. if (row) cache[messageDest] = result
  167. resolve(result)
  168. })
  169. })
  170. }
  171. getMessagesiOS9 (chatId) {
  172. var backup = this
  173. return new Promise((resolve, reject) => {
  174. var messagedb = this.getDatabase(databases.SMS)
  175. messagedb.all(`
  176. SELECT
  177. message.*,
  178. handle.id as sender_name,
  179. datetime(date + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING
  180. FROM chat_message_join
  181. INNER JOIN message
  182. ON message.rowid = chat_message_join.message_id
  183. INNER JOIN handle
  184. ON handle.rowid = message.handle_id
  185. WHERE chat_message_join.chat_id = ?`, [parseInt(chatId)],
  186. async function (err, chats) {
  187. if (err) return reject(err)
  188. chats = chats || []
  189. // Compute the user's name
  190. for (var i in chats) {
  191. var el = chats[i]
  192. el.x_sender = el.is_from_me ? 'Me' : el.sender_name
  193. if (!el.is_from_me) {
  194. var contact = await backup.getName(el.sender_name)
  195. if (contact) {
  196. el.x_sender = `${contact.name} <${contact.query}>`
  197. }
  198. }
  199. }
  200. resolve(chats)
  201. })
  202. })
  203. }
  204. getMessagesiOS10iOS11 (chatId) {
  205. var backup = this
  206. return new Promise((resolve, reject) => {
  207. var messagedb = this.getDatabase(databases.SMS)
  208. messagedb.all(`
  209. SELECT
  210. message.*,
  211. handle.id as sender_name,
  212. datetime(date / 1000000000 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING
  213. FROM chat_message_join
  214. INNER JOIN message
  215. ON message.rowid = chat_message_join.message_id
  216. INNER JOIN handle
  217. ON handle.rowid = message.handle_id
  218. WHERE chat_message_join.chat_id = ?`, [parseInt(chatId)],
  219. async function (err, chats) {
  220. if (err) return reject(err)
  221. chats = chats || []
  222. // Compute the user's name
  223. for (var i in chats) {
  224. var el = chats[i]
  225. el.x_sender = el.is_from_me ? 'Me' : el.sender_name
  226. if (!el.is_from_me) {
  227. var contact = await backup.getName(el.sender_name)
  228. if (contact) {
  229. el.x_sender = `${contact.name} <${contact.query}>`
  230. }
  231. }
  232. }
  233. resolve(chats)
  234. })
  235. })
  236. }
  237. getMessages (chatId) {
  238. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
  239. return this.getMessagesiOS9(chatId)
  240. } else {
  241. return this.getMessagesiOS10iOS11(chatId)
  242. }
  243. }
  244. getConversationsiOS9 () {
  245. var backup = this
  246. return new Promise((resolve, reject) => {
  247. var messagedb = this.getDatabase(databases.SMS)
  248. messagedb.all(`SELECT * FROM chat ORDER BY ROWID ASC`, async function (err, rows) {
  249. if (err) return reject(err)
  250. rows = rows || []
  251. // We need to do some manual parsing of these records.
  252. // The timestamp information is stored in a binary blob named `properties`
  253. // Which is formatted as a binary PLIST.
  254. for (var el of rows) {
  255. if (el.properties) el.properties = bplist.parseBuffer(el.properties)[0]
  256. // Interestingly, some of these do not have dates attached.
  257. if (el.properties) {
  258. el.date = new Date(el.properties.CKChatWatermarkTime * 1000)
  259. } else {
  260. el.date = new Date(0)
  261. }
  262. var contact = await backup.getName(el.chat_identifier)
  263. if (contact) {
  264. el.display_name = `${contact.name} <${contact.query}>`
  265. }
  266. // Format as YY-MM-DD HH:MM:SS
  267. try {
  268. el.XFORMATTEDDATESTRING = el.date.toISOString()
  269. .split('T')
  270. .join(' ')
  271. .split('Z')
  272. .join(' ')
  273. .split('.')[0]
  274. .trim()
  275. } catch (e) {
  276. el.XFORMATTEDDATESTRING = ''
  277. }
  278. }
  279. // Sort by the date.
  280. rows = rows.sort(function (a, b) {
  281. return (a.date.getTime() || 0) - (b.date.getTime() || 0)
  282. })
  283. resolve(rows)
  284. })
  285. })
  286. }
  287. getConversationsiOS10iOS11 () {
  288. return new Promise((resolve, reject) => {
  289. var messagedb = this.getDatabase(databases.SMS)
  290. messagedb.all(`SELECT *, datetime(last_read_message_timestamp / 1000000000 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM chat ORDER BY last_read_message_timestamp ASC`, async function (err, rows) {
  291. if (err) return reject(err)
  292. rows = rows || []
  293. resolve(rows)
  294. })
  295. })
  296. }
  297. getConversations () {
  298. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 14) {
  299. return this.getConversationsiOS9()
  300. } else {
  301. return this.getConversationsiOS10iOS11()
  302. }
  303. }
  304. getFileManifest () {
  305. return new Promise((resolve, reject) => {
  306. var messagedb = this.getDatabase('Manifest.db', true)
  307. messagedb.all('SELECT * from FILES', async function (err, rows) {
  308. if (err) reject(err)
  309. resolve(rows)
  310. })
  311. })
  312. }
  313. getOldFileManifest () {
  314. return new Promise((resolve, reject) => {
  315. let mbdbPath = this.getFileName('Manifest.mbdb', true)
  316. manifestMBDBParse.process(mbdbPath, resolve, reject)
  317. })
  318. }
  319. getOldNotes () {
  320. return new Promise((resolve, reject) => {
  321. var messagedb = this.getDatabase(databases.Notes)
  322. messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from ZNOTE LEFT JOIN ZNOTEBODY ON ZBODY = ZNOTEBODY.Z_PK`, async function (err, rows) {
  323. if (err) reject(err)
  324. resolve(rows)
  325. })
  326. })
  327. }
  328. getNewNotesiOS9 () {
  329. return new Promise((resolve, reject) => {
  330. var messagedb = this.getDatabase(databases.Notes2)
  331. messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM ZICCLOUDSYNCINGOBJECT`, async function (err, rows) {
  332. if (err) reject(err)
  333. resolve(rows)
  334. })
  335. })
  336. }
  337. getNewNotesiOS10iOS11 () {
  338. return new Promise((resolve, reject) => {
  339. var messagedb = this.getDatabase(databases.Notes2)
  340. messagedb.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING, datetime(ZCREATIONDATE1 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING1 FROM ZICCLOUDSYNCINGOBJECT`, async function (err, rows) {
  341. if (err) reject(err)
  342. resolve(rows)
  343. })
  344. })
  345. }
  346. getNotes () {
  347. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
  348. // Legacy iOS 9 support
  349. // May work for earlier but I haven't tested it
  350. return this.getNewNotesiOS9()
  351. } else {
  352. return this.getNewNotesiOS10iOS11()
  353. }
  354. }
  355. getWebHistory () {
  356. return new Promise((resolve, reject) => {
  357. var messagedb = this.getDatabase(databases.WebHistory)
  358. messagedb.all(`SELECT *, datetime(visit_time + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from history_visits LEFT JOIN history_items ON history_items.ROWID = history_visits.history_item`, async function (err, rows) {
  359. if (err) reject(err)
  360. resolve(rows)
  361. })
  362. })
  363. }
  364. getPhotoLocationHistory () {
  365. return new Promise((resolve, reject) => {
  366. var messagedb = this.getDatabase(databases.Photos)
  367. messagedb.all(`SELECT ZDATECREATED, ZLATITUDE, ZLONGITUDE, ZFILENAME, datetime(ZDATECREATED + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM ZGENERICASSET ORDER BY ZDATECREATED ASC`, async function (err, rows) {
  368. if (err) reject(err)
  369. resolve(rows)
  370. })
  371. })
  372. }
  373. getGeofencesList () {
  374. return new Promise((resolve, reject) => {
  375. var messagedb = this.getDatabase(databases.Locations)
  376. messagedb.all(`SELECT datetime(Timestamp + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING, Latitude, Longitude, Distance FROM Fences ORDER BY Timestamp ASC`, async function (err, rows) {
  377. if (err) reject(err)
  378. resolve(rows)
  379. })
  380. })
  381. }
  382. getCallsStatisticsiOS7 () {
  383. return new Promise((resolve, reject) => {
  384. var messagedb = this.getDatabase(databases.Calls)
  385. messagedb.all(`SELECT * from _SqliteDatabaseProperties`, async function (err, rows) {
  386. if (err) reject(err)
  387. resolve(rows)
  388. })
  389. })
  390. }
  391. getCallsStatistics () {
  392. return new Promise((resolve, reject) => {
  393. var messagedb = this.getDatabase(databases.Calls2)
  394. messagedb.all(`SELECT * from ZCALLDBPROPERTIES`, async function (err, rows) {
  395. if (err) reject(err)
  396. resolve(rows)
  397. })
  398. })
  399. }
  400. getCallsList () {
  401. if (parseInt(this.manifest.Lockdown.BuildVersion) <= 13) {
  402. // Legacy iOS 9 support
  403. // May work for earlier but I haven't tested it
  404. return this.getCallsListiOS7()
  405. } else {
  406. return this.getCallsListLater()
  407. }
  408. }
  409. getCallsListiOS7 () {
  410. return new Promise((resolve, reject) => {
  411. var messagedb = this.getDatabase(databases.Calls)
  412. messagedb.all(`SELECT
  413. ROWID as Z_PK,
  414. datetime(date, 'unixepoch') AS XFORMATTEDDATESTRING,
  415. answered as ZANSWERED,
  416. duration as ZDURATION,
  417. address as ZADDRESS,
  418. country_code as ZISO_COUNTRY_CODE,
  419. country_code as ZISO_COUNTRY_CODE,
  420. * from call ORDER BY date ASC`, async function (err, rows) {
  421. if (err) reject(err)
  422. resolve(rows)
  423. })
  424. })
  425. }
  426. getCallsListLater () {
  427. return new Promise((resolve, reject) => {
  428. var messagedb = this.getDatabase(databases.Calls2)
  429. messagedb.all(`SELECT *, datetime(ZDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from ZCALLRECORD ORDER BY ZDATE ASC`, async function (err, rows) {
  430. if (err) reject(err)
  431. resolve(rows)
  432. })
  433. })
  434. }
  435. getVoicemailsList () {
  436. return new Promise((resolve, reject) => {
  437. var messagedb = this.getDatabase(databases.Voicemail)
  438. messagedb.all(`SELECT *, datetime(date, 'unixepoch') AS XFORMATTEDDATESTRING from voicemail ORDER BY date ASC`, async function (err, rows) {
  439. if (err) reject(err)
  440. resolve(rows)
  441. })
  442. })
  443. }
  444. getVoicemailFileList () {
  445. return new Promise((resolve, reject) => {
  446. this.openDatabase('Manifest.db', true)
  447. .then(manifestdb => {
  448. manifestdb.all(`SELECT * from FILES where relativePath like 'Library/Voicemail/%.amr'`, async (err, rows) => {
  449. if (err) reject(err)
  450. resolve(rows)
  451. })
  452. })
  453. .catch(reject)
  454. })
  455. }
  456. getWifiList () {
  457. return new Promise((resolve, reject) => {
  458. var filename = this.getFileName(databases.WiFi)
  459. try {
  460. let wifiList = bplist.parseBuffer(fs.readFileSync(filename))[0]
  461. wifiList['List of known networks'] = wifiList['List of known networks']
  462. .map(el => {
  463. if (el.BSSID) {
  464. el.BSSID = macParse.pad_zeros(el.BSSID) + ''
  465. }
  466. return el
  467. })
  468. resolve(wifiList)
  469. } catch (e) {
  470. reject(e)
  471. }
  472. })
  473. }
  474. getCookies () {
  475. return new Promise(async (resolve, reject) => {
  476. this.openDatabase('Manifest.db', true)
  477. .then(manifestdb => {
  478. manifestdb.all(`SELECT fileID,domain,relativePath from FILES where relativePath like 'Library/Cookies/Cookies.binarycookies'`, async (err, rows) => {
  479. if (err) return reject(err)
  480. let cookiesResult = []
  481. const iterateElements = (elements, index, callback) => {
  482. if (index === elements.length) { return callback() }
  483. // do parse call with element
  484. var ele = elements[index]
  485. cookieParser.parse(this.getFileName(ele.fileID))
  486. .then(cookies => {
  487. // Map to include domain
  488. let formatted = cookies.map(el => { return { domain: ele.domain, cookie: el } })
  489. // Append result
  490. cookiesResult = [...cookiesResult, ...formatted]
  491. // Next file.
  492. iterateElements(elements, index + 1, callback)
  493. })
  494. }
  495. iterateElements(rows, 0, () => {
  496. resolve(cookiesResult)
  497. })
  498. })
  499. })
  500. .catch(reject)
  501. })
  502. }
  503. getAddressBook () {
  504. return new Promise((resolve, reject) => {
  505. var addressbookdb = this.getDatabase(databases.AddressBook)
  506. try {
  507. const query = `
  508. select ABPerson.ROWID
  509. , ABPerson.first
  510. , ABPerson.middle
  511. , ABPerson.last
  512. , ABPerson.Organization as organization
  513. , ABPerson.Department as department
  514. , ABPerson.Birthday as birthday
  515. , ABPerson.JobTitle as jobtitle
  516. , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = '_$!<Work>!$_')) as phone_work
  517. , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = '_$!<Mobile>!$_')) as phone_mobile
  518. , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = '_$!<Home>!$_')) as phone_home
  519. , (select value from ABMultiValue where property = 4 and record_id = ABPerson.ROWID) as email
  520. , (select value from ABMultiValueEntry where parent_id in (select ROWID from ABMultiValue where record_id = ABPerson.ROWID) and key = (select ROWID from ABMultiValueEntryKey where lower(value) = 'street')) as address
  521. , (select value from ABMultiValueEntry where parent_id in (select ROWID from ABMultiValue where record_id = ABPerson.ROWID) and key = (select ROWID from ABMultiValueEntryKey where lower(value) = 'city')) as city
  522. from ABPerson
  523. order by ABPerson.ROWID
  524. `
  525. addressbookdb.all(query, async function (err, rows) {
  526. if (err) reject(err)
  527. resolve(rows)
  528. })
  529. } catch (e) {
  530. reject(e)
  531. }
  532. })
  533. }
  534. getSafariBookmarks () {
  535. return new Promise((resolve, reject) => {
  536. var bookmarksdb = this.getDatabase(databases.SafariBookmarks)
  537. try {
  538. const query = `
  539. select bookmarks.id
  540. , bookmarks.title
  541. , bookmarks.url
  542. , bookmarks.parent as parent_id
  543. , bookmarks.special_id
  544. , bookmarks.type
  545. , bookmarks.num_children
  546. , bookmarks.editable
  547. , bookmarks.deletable
  548. , bookmarks.hidden
  549. , bookmarks.hidden_ancestor_count
  550. , bookmarks.order_index
  551. , bookmarks.external_uuid
  552. , bookmarks.read
  553. , bookmarks.last_modified
  554. , bookmarks.server_id
  555. , bookmarks.sync_key
  556. , bookmarks.added
  557. , bookmarks.deleted
  558. , bookmarks.fetched_icon
  559. , bookmarks.dav_generation
  560. , bookmarks.locally_added
  561. , bookmarks.archive_status
  562. , bookmarks.syncable
  563. , bookmarks.web_filter_status
  564. , bookmarks.modified_attributes
  565. , parent_bookmarks.title as parent_title
  566. from bookmarks
  567. left join bookmarks as parent_bookmarks on parent_bookmarks.id = bookmarks.parent
  568. where bookmarks.type = 0
  569. order by bookmarks.id
  570. `
  571. bookmarksdb.all(query, async function (err, rows) {
  572. if (err) reject(err)
  573. resolve(rows)
  574. })
  575. } catch (e) {
  576. reject(e)
  577. }
  578. })
  579. }
  580. getSafariBookmarksiOS7 () {
  581. return new Promise((resolve, reject) => {
  582. var bookmarksdb = this.getDatabase(databases.SafariBookmarks)
  583. try {
  584. const query = `
  585. select bookmarks.id
  586. , bookmarks.special_id
  587. , bookmarks.parent as parent_id
  588. , bookmarks.type
  589. , bookmarks.title
  590. , bookmarks.url
  591. , bookmarks.num_children
  592. , bookmarks.editable
  593. , bookmarks.deletable
  594. , bookmarks.hidden
  595. , bookmarks.hidden_ancestor_count
  596. , bookmarks.order_index
  597. , bookmarks.external_uuid
  598. , bookmarks.read
  599. , bookmarks.last_modified
  600. , bookmarks.server_id
  601. , bookmarks.sync_key
  602. , bookmarks.sync_data
  603. , bookmarks.added
  604. , bookmarks.deleted
  605. , bookmarks.extra_attributes
  606. , bookmarks.local_attributes
  607. , bookmarks.fetched_icon
  608. , bookmarks.icon
  609. , bookmarks.dav_generation
  610. , bookmarks.locally_added
  611. , bookmarks.archive_status
  612. , bookmarks.syncable
  613. , bookmarks.web_filter_status
  614. , parent_bookmarks.title as parent_title
  615. from bookmarks
  616. left join bookmarks as parent_bookmarks on parent_bookmarks.id = bookmarks.parent
  617. where bookmarks.type = 0
  618. order by bookmarks.id
  619. `
  620. bookmarksdb.all(query, async function (err, rows) {
  621. if (err) reject(err)
  622. resolve(rows)
  623. })
  624. } catch (e) {
  625. reject(e)
  626. }
  627. })
  628. }
  629. }
  630. module.exports.availableBackups = function () {
  631. const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/')
  632. return new Promise((resolve, reject) => {
  633. resolve(fs.readdirSync(base, {
  634. encoding: 'utf8'
  635. })
  636. .map(file => IPhoneBackup.fromID(file)))
  637. })
  638. }
  639. module.exports.iPhoneBackup = IPhoneBackup
  640. module.exports.IPhoneBackup = IPhoneBackup