|
@@ -1,301 +1,218 @@
|
|
|
-#!/usr/bin/env node
|
|
|
-
|
|
|
-const program = require('commander')
|
|
|
const path = require('path')
|
|
|
-const chalk = require('chalk')
|
|
|
-const version = require('./util/version_compare')
|
|
|
-const iPhoneBackup = require('./util/iphone_backup.js').iPhoneBackup
|
|
|
const log = require('./util/log')
|
|
|
+const report = require('./reports')
|
|
|
+const matcher = require('./util/matcher')
|
|
|
+const Group = report.Group
|
|
|
+const Backup = require('./backup')
|
|
|
+
|
|
|
+// Backup source directory
|
|
|
+var backupDirectory = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/')
|
|
|
+
|
|
|
+// Object containing all report modules
|
|
|
+var moduleCache = report.types
|
|
|
+
|
|
|
+// Array of plugin modules
|
|
|
+var plugins = []
|
|
|
+
|
|
|
+/**
|
|
|
+ * Add an array of plugins to the plugins list.
|
|
|
+ * @param {Array<Object>} array contains array of plugin objects.
|
|
|
+ */
|
|
|
+function registerModule (array) {
|
|
|
+ if (!(array instanceof Array)) {
|
|
|
+ array = [ array ]
|
|
|
+ }
|
|
|
|
|
|
-var base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/')
|
|
|
-
|
|
|
-var reportTypes = {
|
|
|
- 'apps': require('./reports/apps'),
|
|
|
- 'calls': require('./reports/calls'),
|
|
|
- 'conversations': require('./reports/conversations'),
|
|
|
- 'conversations_full': require('./reports/conversations_full'),
|
|
|
- 'cookies': require('./reports/cookies'),
|
|
|
- 'list': require('./reports/list'),
|
|
|
- 'manifest': require('./reports/manifest'),
|
|
|
- 'messages': require('./reports/messages'),
|
|
|
- 'notes': require('./reports/notes'),
|
|
|
- 'oldnotes': require('./reports/oldnotes'),
|
|
|
- 'photolocations': require('./reports/photolocations'),
|
|
|
- 'voicemail-files': require('./reports/voicemail-files'),
|
|
|
- 'voicemail': require('./reports/voicemail'),
|
|
|
- 'webhistory': require('./reports/webhistory'),
|
|
|
- 'calls_statistics': require('./reports/calls_statistics'),
|
|
|
- 'wifi': require('./reports/wifi'),
|
|
|
- 'address_book': require('./reports/address_book'),
|
|
|
- 'safari_bookmarks': require('./reports/safari_bookmarks'),
|
|
|
- 'pushstore': require('./reports/pushstore'),
|
|
|
- 'calendar': require('./reports/calendar'),
|
|
|
- 'facebook_profile': require('./reports/facebook_profile'),
|
|
|
- 'facebook_messenger_friends': require('./reports/facebook_messenger_friends'),
|
|
|
- 'spotify': require('./reports/spotify'),
|
|
|
- 'instagram_profile': require('./reports/instagram_profile'),
|
|
|
- 'instagram_recent_searches': require('./reports/instagram_recent_searches'),
|
|
|
- 'instagram_following_users_coded': require('./reports/instagram_following_users_coded'),
|
|
|
- 'instagram_fb_friends': require('./reports/instagram_fb_friends'),
|
|
|
- 'speed_dial': require('./reports/speed_dial'),
|
|
|
- 'bluetooth_devices': require('./reports/bluetooth_devices'),
|
|
|
- 'safari_open_tabs': require('./reports/safari_open_tabs'),
|
|
|
- 'safari_recent_searches': require('./reports/safari_recent_searches'),
|
|
|
- 'gmail_accounts': require('./reports/gmail_accounts'),
|
|
|
- 'gmail_shared_contacts': require('./reports/gmail_shared_contacts'),
|
|
|
- 'waze_favorites': require('./reports/waze_favorites'),
|
|
|
- 'waze_places': require('./reports/waze_places'),
|
|
|
- 'waze_recents': require('./reports/waze_recents'),
|
|
|
- 'skype_accounts': require('./reports/skype_accounts'),
|
|
|
- 'skype_calls': require('./reports/skype_calls'),
|
|
|
- 'viber_contacts': require('./reports/viber_contacts'),
|
|
|
- 'viber_calls': require('./reports/viber_calls'),
|
|
|
- 'viber_messages': require('./reports/viber_messages')
|
|
|
+ plugins.push(...array)
|
|
|
+ moduleCache = getCompleteModuleList()
|
|
|
}
|
|
|
|
|
|
-var formatters = {
|
|
|
- 'json': require('./formatters/json'),
|
|
|
- 'table': require('./formatters/table'),
|
|
|
- 'raw': require('./formatters/raw-json'),
|
|
|
- 'raw-json': require('./formatters/raw-json'),
|
|
|
- 'csv': require('./formatters/csv'),
|
|
|
- 'raw-csv': require('./formatters/raw-csv')
|
|
|
+/**
|
|
|
+ * Remove all non-default plugins.
|
|
|
+ */
|
|
|
+function clearModules () {
|
|
|
+ plugins = []
|
|
|
+ moduleCache = getCompleteModuleList()
|
|
|
}
|
|
|
|
|
|
-program
|
|
|
- .version('3.0.0')
|
|
|
- .option('-l, --list', 'List Backups')
|
|
|
- .option(`-b, --backup <backup>`, 'Backup ID')
|
|
|
- .option(`-d, --dir <directory>`, `Backup Directory (default: ${base})`)
|
|
|
- .option('-r, --report <report_type>', 'Select a report type. see below for a full list.')
|
|
|
- .option('-i, --id <id>', 'Specify an ID for filtering certain reports')
|
|
|
- .option('-f, --formatter <type>', 'Specify output format. default: table')
|
|
|
- .option(`-e, --extract <dir>`, 'Extract data for commands. supported by: voicemail-files, manifest')
|
|
|
- .option('-o, --output <path>', 'Specify an output directory for files to be written to.')
|
|
|
- .option(`-v, --verbose`, 'Verbose debugging output')
|
|
|
- .option(` --filter <filter>`, 'Filter output fo r individual reports. See the README for usage.')
|
|
|
- .option(' --join-reports', 'Join JSON reports together. (available for -f json or -f raw only!)')
|
|
|
- .option(` --no-color`, 'Disable colorized output')
|
|
|
- .option(` --dump`, 'alias for "--formatter raw"')
|
|
|
- .option(` --quiet`, 'quiet all messages, except for errors and raw output')
|
|
|
-
|
|
|
-program.on('--help', function () {
|
|
|
- console.log('')
|
|
|
- console.log('Supported Report Types:')
|
|
|
-
|
|
|
- // Generate a list of report types.
|
|
|
- for (var i in reportTypes) {
|
|
|
- var r = reportTypes[i]
|
|
|
- console.log(' ', chalk.green(r.name), (r.supportedVersions ? chalk.gray('(iOS ' + r.supportedVersions + ') ') : '') + '-', r.description)
|
|
|
+/**
|
|
|
+ * Get all modules in a top-down manner.
|
|
|
+ */
|
|
|
+function getCompleteModuleList () {
|
|
|
+ let allModules = {}
|
|
|
+
|
|
|
+ // Add all of the require()'d modules into the plugins list.
|
|
|
+ plugins.forEach(function (plugin) {
|
|
|
+ allModules = { ...allModules, ...plugin }
|
|
|
+ })
|
|
|
+
|
|
|
+ // Add all of the modules to a single object.
|
|
|
+ // JS's behavior dictates that the items are added sequentially
|
|
|
+ // So, the default reports overwrite any third party plugin.
|
|
|
+ let result = {
|
|
|
+ ...allModules,
|
|
|
+ ...report.types
|
|
|
}
|
|
|
- console.log('')
|
|
|
- console.log("If you're interested to know how this works, check out my post:")
|
|
|
- console.log('https://www.richinfante.com/2017/3/16/reverse-engineering-the-ios-backup')
|
|
|
- console.log('')
|
|
|
- console.log('Issue tracker:')
|
|
|
- console.log('https://github.com/richinfante/iphonebackuptools/issues')
|
|
|
- console.log('')
|
|
|
-})
|
|
|
-
|
|
|
-process.on('unhandledRejection', (e) => {
|
|
|
- console.log('unhandled', e)
|
|
|
- process.exit(1)
|
|
|
-})
|
|
|
-
|
|
|
-// Parse argv.
|
|
|
-program.parse(process.argv)
|
|
|
-
|
|
|
-log.setVerbose(program.quiet ? 0 : (program.verbose ? 2 : 1))
|
|
|
-
|
|
|
-// Save the formatter
|
|
|
-program.formatter = formatters[program.formatter] || formatters.table
|
|
|
-
|
|
|
-// Legacy support for `--dump` flag.
|
|
|
-if (program.dump) {
|
|
|
- program.formatter = formatters.raw
|
|
|
-}
|
|
|
-
|
|
|
-// Disable color for non-ttys.
|
|
|
-if (!process.stdout.isTTY) { program.color = false }
|
|
|
-
|
|
|
-// Find the base
|
|
|
-base = program.dir || base
|
|
|
-
|
|
|
-log.verbose('Using source:', base)
|
|
|
|
|
|
-// Run the main function
|
|
|
-main()
|
|
|
+ moduleCache = result
|
|
|
|
|
|
-async function main () {
|
|
|
- if (program.list) {
|
|
|
- // Run the list report standalone
|
|
|
- await new Promise((resolve, reject) => {
|
|
|
- reportTypes.list.func(program, base, resolve, reject)
|
|
|
- })
|
|
|
- } else if (program.report) {
|
|
|
- var reportContents = []
|
|
|
+ return result
|
|
|
+}
|
|
|
|
|
|
- // Turn the report argument into an array of report type names
|
|
|
- var selectedTypes = program.report
|
|
|
- .split(',')
|
|
|
- .map(el => el.trim())
|
|
|
- .filter(el => el !== '')
|
|
|
+/**
|
|
|
+ * Try to find a single report.
|
|
|
+ * @param {string} query name to find.
|
|
|
+ */
|
|
|
+function findReport (query) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ // Check there is no wildcard in the query.
|
|
|
+ if (query.indexOf('*') > -1) {
|
|
|
+ return reject(new Error('Cannot run a wildcard match here.'))
|
|
|
+ }
|
|
|
|
|
|
- // Add all types if type is 'all'
|
|
|
- if (program.report === 'all') {
|
|
|
- selectedTypes = []
|
|
|
+ // Run matches.
|
|
|
+ let matches = matcher(moduleCache, query, (el) => !(el instanceof Group))
|
|
|
|
|
|
- for (var key in reportTypes) {
|
|
|
- if (reportTypes[key].requiresInteractivity === true) {
|
|
|
- continue
|
|
|
- }
|
|
|
+ // If no report found, fail.
|
|
|
+ if (matches.length === 0) {
|
|
|
+ return reject(new Error(`No report found with name "${query}"`))
|
|
|
+ }
|
|
|
|
|
|
- selectedTypes.push(key)
|
|
|
- }
|
|
|
+ // If multiple matches, fail.
|
|
|
+ if (matches.length > 1) {
|
|
|
+ return reject(new Error(`Multiple report matches for name "${query}", not allowed.`))
|
|
|
}
|
|
|
|
|
|
- for (var reportName of selectedTypes) {
|
|
|
- // If the report is valid
|
|
|
- if (reportTypes[reportName]) {
|
|
|
- var report = reportTypes[reportName]
|
|
|
+ // Resolve match
|
|
|
+ resolve(matches[0])
|
|
|
+ })
|
|
|
+}
|
|
|
|
|
|
- if (selectedTypes.length > 1 && !report.usesPromises) {
|
|
|
- log.warning('the report', report.name, 'does not utilize promises.')
|
|
|
- log.warning('this may not work')
|
|
|
- }
|
|
|
+/**
|
|
|
+ * Translate the raw output of a report to the correct result, based on the "raw" parameter.
|
|
|
+ * @param {Object} report The report module
|
|
|
+ * @param {Object} result Raw data output from the aforementioned report
|
|
|
+ * @param {Object} params parameters object.
|
|
|
+ */
|
|
|
+function compileReport (report, result, { raw }) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ if (!raw && report.output) {
|
|
|
+ if (result instanceof Array) {
|
|
|
+ log.verbose('compiling report (array)...')
|
|
|
+ // if it's an array, translate each item.
|
|
|
+ result = result.map(item => {
|
|
|
+ // For each item, run the functions on the entry.
|
|
|
+ let editedResult = {}
|
|
|
+ for (let [key, value] of Object.entries(report.output)) {
|
|
|
+ editedResult[key] = value(item)
|
|
|
+ }
|
|
|
|
|
|
- log.begin('run', report.name)
|
|
|
+ return editedResult
|
|
|
+ })
|
|
|
|
|
|
- // Check if there's a backup specified and one is required.
|
|
|
- if (report.requiresBackup) {
|
|
|
- if (!program.backup) {
|
|
|
- log.error('use -b or --backup <id> to specify backup.')
|
|
|
- process.exit(1)
|
|
|
- }
|
|
|
- }
|
|
|
- try {
|
|
|
- if (report.func) {
|
|
|
- let contents = await runSingleReport(report, program)
|
|
|
- if (contents == null) { log.end(); continue }
|
|
|
-
|
|
|
- reportContents.push({
|
|
|
- name: reportName,
|
|
|
- contents: contents
|
|
|
- })
|
|
|
- } else if (report.functions) {
|
|
|
- let contents = await runSwitchedReport(report, program)
|
|
|
- if (contents == null) { log.end(); continue }
|
|
|
-
|
|
|
- reportContents.push({
|
|
|
- name: reportName,
|
|
|
- contents: contents
|
|
|
- })
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- log.error(`Couldn't run '${report.name}'.`)
|
|
|
- log.error(e)
|
|
|
+ resolve(result)
|
|
|
+ } else {
|
|
|
+ log.verbose('compiling report (single)...')
|
|
|
+ // Otherwise, translate the object returned.
|
|
|
+ let editedResult = {}
|
|
|
+ for (let [key, value] of Object.entries(report.output)) {
|
|
|
+ editedResult[key] = value(result)
|
|
|
}
|
|
|
|
|
|
- log.end()
|
|
|
- } else {
|
|
|
- log.error('Unknown report type:', reportName)
|
|
|
- log.error(`It's possible this tool is out-of date.`)
|
|
|
- log.error(`https://github.com/richinfante/iphonebackuptools/issues`)
|
|
|
- program.outputHelp()
|
|
|
+ resolve(editedResult)
|
|
|
}
|
|
|
+ } else {
|
|
|
+ resolve(result)
|
|
|
}
|
|
|
-
|
|
|
- program.formatter.finalReport(reportContents, program)
|
|
|
- } else {
|
|
|
- program.outputHelp()
|
|
|
- }
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
-async function runSwitchedReport (report, program) {
|
|
|
- async function createPromise (key, program, backup) {
|
|
|
- log.verbose('resolving using promises.')
|
|
|
-
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- report.functions[key](program, backup, resolve, reject)
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- // New type of reports
|
|
|
- var backup = iPhoneBackup.fromID(program.backup, base)
|
|
|
-
|
|
|
- var flag = false
|
|
|
- var value
|
|
|
- // Check for a compatible reporting tool.
|
|
|
- for (var key in report.functions) {
|
|
|
- if (version.versionCheck(backup.iOSVersion, key)) {
|
|
|
- if (!report.usesPromises) {
|
|
|
- log.verbose('using synchronous call.')
|
|
|
-
|
|
|
- value = report.functions[key](program, backup)
|
|
|
- } else {
|
|
|
- // Use promises to resolve synchronously
|
|
|
- value = await createPromise(key, program, backup)
|
|
|
- }
|
|
|
- flag = true
|
|
|
- break
|
|
|
+/**
|
|
|
+ * Run a named report and resolve to it's output.
|
|
|
+ * The output is formatted based on the `report.output` key, if the params.raw option is NOT set to true.
|
|
|
+ * @param {string} query report name
|
|
|
+ * @param {Object=} params parameters.
|
|
|
+ */
|
|
|
+function run (query, params) {
|
|
|
+ params = params || {}
|
|
|
+ return new Promise(async (resolve, reject) => {
|
|
|
+ try {
|
|
|
+ let report = await findReport(query)
|
|
|
+ let result = await runReport(report, params)
|
|
|
+ let compiled = await compileReport(report, result, params)
|
|
|
+
|
|
|
+ resolve(compiled)
|
|
|
+ } catch (e) {
|
|
|
+ reject(e)
|
|
|
}
|
|
|
- }
|
|
|
+ })
|
|
|
+}
|
|
|
|
|
|
- if (!flag) {
|
|
|
- log.error(`Couldn't run '${report.name}'.`)
|
|
|
- log.error(`The report generator '${report.name}' does not support iOS`, backup.iOSVersion)
|
|
|
- log.error(`If you think it should, file an issue here:`)
|
|
|
- log.error(`https://github.com/richinfante/iphonebackuptools/issues`)
|
|
|
- return null
|
|
|
- }
|
|
|
+/**
|
|
|
+ * Run a report
|
|
|
+ * @param {object} report report module
|
|
|
+ * @param {object=} params parameters
|
|
|
+ */
|
|
|
+function runReport (report, params) {
|
|
|
+ params = params || {}
|
|
|
|
|
|
- return value
|
|
|
-}
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ var backup
|
|
|
|
|
|
-async function runSingleReport (report, program) {
|
|
|
- async function createPromise (program, backup, base) {
|
|
|
- log.verbose('resolving using promises.')
|
|
|
+ // Cannot run < v3 backups in this manner.
|
|
|
+ if (!report.version || report.version < 3) {
|
|
|
+ return reject(new Error(`Cannot call ${report.name} as a module, it is not updated to the v3 api`))
|
|
|
+ }
|
|
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- if (report.requiresBackup) {
|
|
|
- report.func(program, backup, resolve, reject)
|
|
|
- } else {
|
|
|
- report.func(program, base, resolve, reject)
|
|
|
+ // If it requires a backup and none is provided, reject.
|
|
|
+ if (report.requiresBackup) {
|
|
|
+ if (!params.backup) {
|
|
|
+ return reject(new Error('Please specify the `backup` parameter to run this report.'))
|
|
|
}
|
|
|
- })
|
|
|
- }
|
|
|
|
|
|
- async function runReport (backup, base) {
|
|
|
- if (!report.usesPromises) {
|
|
|
- log.verbose('using synchronous call.')
|
|
|
+ backup = new Backup(backupDirectory, params.backup)
|
|
|
+ }
|
|
|
|
|
|
- // Old-style non-promise based report.
|
|
|
- if (report.requiresBackup) {
|
|
|
- return report.func(program, backup)
|
|
|
- } else {
|
|
|
- return report.func(program, base)
|
|
|
- }
|
|
|
- } else {
|
|
|
- // Create a promise to resolve this function
|
|
|
- // Use promises to resolve synchronously
|
|
|
- return createPromise(program, backup, base)
|
|
|
+ // Input params to func
|
|
|
+ let inputParams = {
|
|
|
+ ...params,
|
|
|
+ backup
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- // New type of reports
|
|
|
- var backup = iPhoneBackup.fromID(program.backup, base)
|
|
|
+ report.run(module.exports, inputParams)
|
|
|
+ .then(resolve)
|
|
|
+ .catch(reject)
|
|
|
+ })
|
|
|
+}
|
|
|
|
|
|
- if (report.supportedVersions !== undefined) {
|
|
|
- if (version.versionCheck(backup.iOSVersion, report.supportedVersions)) {
|
|
|
- return runReport(backup, base)
|
|
|
- } else {
|
|
|
- log.error(`Couldn't run '${report.name}'.`)
|
|
|
- log.error(`The report generator '${report.name}' does not support iOS`, backup.iOSVersion)
|
|
|
- log.error(`If you think it should, file an issue here:`)
|
|
|
- log.error(`https://github.com/richinfante/iphonebackuptools/issues`)
|
|
|
- return null
|
|
|
- }
|
|
|
- } else {
|
|
|
- return runReport(backup, base)
|
|
|
+module.exports = {
|
|
|
+ // Exported Libraries
|
|
|
+ Backup,
|
|
|
+ Group,
|
|
|
+
|
|
|
+ // Module management
|
|
|
+ registerModule,
|
|
|
+ clearModules,
|
|
|
+ get modules () {
|
|
|
+ return moduleCache
|
|
|
+ },
|
|
|
+
|
|
|
+ // Source directory
|
|
|
+ set base (value) {
|
|
|
+ backupDirectory = value
|
|
|
+ },
|
|
|
+ get base () {
|
|
|
+ return backupDirectory
|
|
|
+ },
|
|
|
+
|
|
|
+ // Runners
|
|
|
+ run,
|
|
|
+ findReport,
|
|
|
+ runReport,
|
|
|
+ compileReport,
|
|
|
+
|
|
|
+ // misc
|
|
|
+ setLogLevel (lvl) {
|
|
|
+ log.setVerbose(lvl)
|
|
|
}
|
|
|
}
|