applebot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,656 @@
1
+ /* applebot.js */
2
+
3
+ var require = patchRequire(require);
4
+ var fs = require('fs');
5
+ var Casper = require('casper');
6
+
7
+ var iterate = function(object, callback) {
8
+ var keys = Object.keys(object);
9
+ for (var i = keys.length - 1; i >= 0; i--) {
10
+ var key = keys[i];
11
+ var value = object[key];
12
+ callback(key, value);
13
+ }
14
+ };
15
+
16
+ var merge = function(obj1,obj2) {
17
+ var obj3 = {};
18
+ for (var attrname in obj1) { obj3[attrname] = obj1[attrname]; }
19
+ for (var attrname in obj2) { obj3[attrname] = obj2[attrname]; }
20
+ return obj3;
21
+ };
22
+
23
+ var isFunction = function(obj) {
24
+ return typeof(obj) === 'function';
25
+ };
26
+
27
+ var isUndefined = function(obj) {
28
+ return obj === void(0);
29
+ };
30
+
31
+ var $outputFormat;
32
+ var jsonLog = function(obj) {
33
+ console.log(JSON.stringify(obj));
34
+ };
35
+ console.formatLog = function(string, json) {
36
+ if ($outputFormat === 'json') {
37
+ jsonLog(merge({normal_output: string}, json));
38
+ }
39
+ else if (string.length > 0) {
40
+ console.log(string);
41
+ }
42
+ };
43
+
44
+ var die = function(page, message, metadata) {
45
+ if (isUndefined(metadata)) {
46
+ metadata = {};
47
+ }
48
+ console.formatLog("", {event: "debug_html", html: page.getHTML()});
49
+ //var error = merge({event: "error", message: message}, metadata);
50
+ //console.formatLog("☠ " + message, error);
51
+ page.die("☠ " + message, 1);
52
+ };
53
+
54
+ var dieFromException = function(page, ex, metadata) {
55
+ if (isUndefined(metadata)) {
56
+ metadata = {};
57
+ }
58
+ metadata.stack = ex.stack;
59
+ die(page, ex.message, metadata);
60
+ };
61
+
62
+ var Shortcuts = {
63
+ findHrefSelector: function(innerHTML, page, userQueryScope, options) {
64
+ var queryScope = 'a'
65
+ if (userQueryScope) {
66
+ queryScope = userQueryScope + ' ' + queryScope;
67
+ }
68
+ if (!options) {
69
+ options = {};
70
+ }
71
+ var href = page.evaluate(function(innerHTML, queryScope, options) {
72
+ var links = document.querySelectorAll(queryScope);
73
+ var href = false;
74
+ for(var i = 0; i < links.length; i++) {
75
+ var link = links[i];
76
+ if (options.fuzzy) {
77
+ if (link.innerHTML.toLowerCase().indexOf(innerHTML.toLowerCase()) !== -1) {
78
+ href = link.href;
79
+ }
80
+ }
81
+ else if (link.innerHTML === innerHTML) {
82
+ href = link.href;
83
+ }
84
+ }
85
+
86
+ return href;
87
+ }, innerHTML, queryScope, options);
88
+
89
+ var hrefSelector = false;
90
+ if (href) {
91
+ hrefSelector = queryScope +'[href="' + href.replace("https://itunesconnect.apple.com", "") +'"]';
92
+ }
93
+
94
+ if (href === false || !page.exists(hrefSelector)) {
95
+ page.debugHTML();
96
+ throw ("Error: could not find " + innerHTML + " link: " + hrefSelector);
97
+ }
98
+
99
+ return hrefSelector;
100
+ },
101
+ findFieldName: function(page, fieldLabel, baseSelector) {
102
+ var fieldName = page.evaluate(function(fieldLabel, baseSelector) {
103
+ var fieldContainers = document.querySelectorAll(baseSelector);
104
+ var fieldName = false;
105
+ for (var i = 0; i < fieldContainers.length; i++) {
106
+ var parent = fieldContainers[i];
107
+ var children = parent.children;
108
+ for (var j = 0; j < children.length; j++) {
109
+ var child = children[j];
110
+ if (child.innerHTML === fieldLabel) {
111
+ fieldName = parent.querySelector('input').name;
112
+ break;
113
+ }
114
+ }
115
+ if (fieldName !== false) {
116
+ break;
117
+ }
118
+ }
119
+
120
+ return fieldName;
121
+ }, fieldLabel, baseSelector);
122
+ if (fieldName === false) {
123
+ die(page, "Error: could not find " + fieldLabel + " field");
124
+ }
125
+ return fieldName;
126
+ },
127
+ openManageApps: function(page) {
128
+ var hrefSelector = Shortcuts.findHrefSelector("Manage Your Apps", page);
129
+ page.click(hrefSelector);
130
+ },
131
+ getScreenshotPaths: function(userOptions, optionsKey) {
132
+ var screenshotsOption = userOptions[optionsKey];
133
+ var screenshotPaths = [];
134
+
135
+ if (screenshotsOption.charAt(0) === "[") {
136
+ // using JSON
137
+ screenshotPaths = JSON.parse(screenshotsOption);
138
+ }
139
+ else {
140
+ screenshotPaths = [screenshotsOption];
141
+ }
142
+
143
+ return screenshotPaths;
144
+ },
145
+ popScreenshotPaths: function(userOptions, optionsKey) {
146
+ var screenshotPaths = Shortcuts.getScreenshotPaths(userOptions, optionsKey);
147
+
148
+ var value = screenshotPaths.pop();
149
+ userOptions[optionsKey] = JSON.stringify(screenshotPaths);
150
+ return value;
151
+ },
152
+ addStepsForScreenshotGroup: function(applebot, page, userOptions, screenshotStepOption) {
153
+ var key_path = screenshotStepOption.key_path;
154
+ var selector = screenshotStepOption.selector;
155
+ var description = screenshotStepOption.description;
156
+ var next_step = screenshotStepOption.next_step;
157
+ var while_step = screenshotStepOption.while_step;
158
+
159
+ var numScreenshots = Shortcuts.getScreenshotPaths(userOptions, key_path).length;
160
+ var waitForScreenshotCount = function(screenshotSelector, count) {
161
+ var wait = function() {
162
+ var numberOfScreenshots = page.evaluate(function(screenshotSelector) {
163
+ return document.querySelectorAll(screenshotSelector).length;
164
+ }, screenshotSelector);
165
+ return numberOfScreenshots === count;
166
+ }
167
+ wait.timeout = 8000 * numScreenshots;
168
+ return wait;
169
+ };
170
+
171
+ for (var i = 1; i <= numScreenshots; i++) {
172
+ var numUploaded = 0;
173
+ applebot.step("Wait for " + description + " screenshot #" + i + " to upload", 'waitFor', waitForScreenshotCount(selector, i), function() {
174
+ numUploaded += 1;
175
+ if (numUploaded >= numScreenshots) {
176
+ applebot.action(next_step);
177
+ }
178
+ else {
179
+ applebot.action(while_step);
180
+ }
181
+ });
182
+ }
183
+ },
184
+ waitToParseProfiles: function(applebot, page, profileType, callback) {
185
+ page.evaluate(function(profileType) {
186
+ window.profileType = profileType;
187
+ }, profileType);
188
+
189
+ if (!page.evaluate(function() { return window.profileDataURL; })) {
190
+ throw "Invalid profile list page, missing window.profileDataURL";
191
+ }
192
+
193
+ applebot.action("Grab profile data", function() {
194
+ var parsedProfiles = page.evaluate(function() {
195
+ return window.parsedProfiles;
196
+ });
197
+ callback(parsedProfiles);
198
+ });
199
+
200
+ var waitFor = function() {
201
+ var result = page.evaluate(function() {
202
+ if (window.parseFinished) {
203
+ return true;
204
+ }
205
+ if (window.parseRequest) {
206
+ return false;
207
+ }
208
+ window.parseStarted = true;
209
+ var url = window.profileDataURL + "&type=" + window.profileType;
210
+ window.parseRequest = $.post(url, function(remoteData, success) {
211
+ var profiles = {};
212
+ remoteData.provisioningProfiles.forEach(function(profile) {
213
+ var appId = profile.appId.identifier;
214
+ profiles[appId] = {
215
+ id: profile.provisioningProfileId,
216
+ download_url: "https://developer.apple.com/account/ios/profile/profileContentDownload.action?displayId=" + profile.provisioningProfileId
217
+ };
218
+ });
219
+ window.parsedProfiles = profiles;
220
+ window.parseFinished = true;
221
+ });
222
+ return false;
223
+ });
224
+ return result;
225
+ };
226
+ applebot.step("Wait for profiles to parse", 'waitFor', waitFor, function() {
227
+ applebot.action("Grab profile data");
228
+ }, {timeout: 15 * 1000});
229
+ },
230
+ safeFillSelectors: function(page, formSelector, fillOptions, doSubmission) {
231
+ if (doSubmission === undefined) {
232
+ doSubmission = true;
233
+ }
234
+ var safeOptions = {};
235
+
236
+ iterate(fillOptions, function(selector, value) {
237
+ if (page.exists(selector)) {
238
+ safeOptions[selector] = value;
239
+ }
240
+ });
241
+
242
+ page.fillSelectors(formSelector, safeOptions, doSubmission);
243
+ },
244
+ safeFill: function(page, formSelector, fillOptions, doSubmission) {
245
+ var transformedFillOptions = {};
246
+
247
+
248
+ iterate(fillOptions, function(fieldName, value) {
249
+ transformedFillOptions['[name="' + fieldName + '"]'] = value;
250
+ });
251
+
252
+
253
+ Shortcuts.safeFillSelectors(page, formSelector, transformedFillOptions, doSubmission);
254
+ },
255
+ filterBundleIdsFromItunesConnect: function(page, toTitle) {
256
+ var bundleIdMap = {};
257
+ var bundleIdFieldInfo = page.evaluate(function() {
258
+ var bundleIdField = document.querySelector('.bundleIdentifierBox select');
259
+ if (!bundleIdField) {
260
+ return false;
261
+ }
262
+ var bundleIdFieldName = bundleIdField.name;
263
+ var bundleIdFieldOptions = [];
264
+ for (var i = 0; i < bundleIdField.options.length; i++) {
265
+ var option = bundleIdField.options[i];
266
+ if (option.innerHTML === 'Select') {
267
+ continue;
268
+ }
269
+ bundleIdFieldOptions.push([option.innerHTML, option.value]);
270
+ }
271
+
272
+ return {
273
+ name: bundleIdFieldName,
274
+ options: bundleIdFieldOptions
275
+ };
276
+ });
277
+ if (bundleIdFieldInfo === false) {
278
+ die(page, "Error: could not find Bundle ID field");
279
+ }
280
+ var bundleIdOptions = bundleIdFieldInfo.options;
281
+ var i = bundleIdOptions.length; //or 10
282
+ while (i--) {
283
+ var option = bundleIdFieldInfo.options[i];
284
+ var nameString = option[0];
285
+ var formValue = option[1];
286
+ var nameAndBundle = nameString.split("-").map(function(s) { return s.trim() });
287
+ if (toTitle) {
288
+ bundleIdMap[nameAndBundle[1]] = nameAndBundle[0];
289
+ }
290
+ else {
291
+ bundleIdMap[nameAndBundle[1]] = formValue;
292
+ }
293
+ }
294
+
295
+ return bundleIdMap;
296
+ },
297
+ searchITCApps: function(page, searchParams) {
298
+ var formOptions = {};
299
+ if (searchParams.title) {
300
+ formOptions['.search-param-value-name input'] = searchParams.title;
301
+ }
302
+ if (searchParams.apple_id) {
303
+ formOptions['.search-param-value-appleId input'] = searchParams.apple_id;
304
+ }
305
+ if (searchParams.sku) {
306
+ formOptions['.search-param-value-sku input'] = searchParams.sku;
307
+ }
308
+ Shortcuts.safeFillSelectors(page, 'form[name="mainForm"]', formOptions, false);
309
+ page.click('.searchfield input');
310
+ },
311
+ addStepToWaitForListOfRecentApps: function(applebot, callback) {
312
+ applebot.step("Wait for the list of recent apps", 'waitForResource', 'btn-blue-add-new-app.png', function() {
313
+ callback();
314
+ });
315
+ },
316
+ addStepsToSearchAndClickAppTitle: function(applebot, page, title, callback) {
317
+ applebot.action("Fill the apps search box", function() {
318
+ applebot.shortcuts.searchITCApps(page, {title: title});
319
+ });
320
+
321
+ applebot.action('Find the app link and click it', function() {
322
+ var hrefSelector = applebot.shortcuts.findHrefSelector(title, page, undefined, { fuzzy: true});
323
+ page.click(hrefSelector);
324
+ });
325
+
326
+ applebot.shortcuts.addStepToWaitForListOfRecentApps(applebot, function() {
327
+ applebot.action("Fill the apps search box");
328
+ });
329
+
330
+ applebot.step("Wait for the search results", 'waitForText', 'Search Results', function() {
331
+ applebot.action('Find the app link and click it');
332
+ });
333
+
334
+ applebot.step("Wait for the app info screen", 'waitForText', 'App Information', function() {
335
+ callback();
336
+ });
337
+ }
338
+ }
339
+
340
+ function AppleBot(options) {
341
+ var _username, _password;
342
+ var applebot = this;
343
+ var _currentPage;
344
+ var _actions = {};
345
+ var _steps = [];
346
+
347
+ if (!isUndefined(options.username)) {
348
+ _username = options.username;
349
+ }
350
+ if (!isUndefined(options.password)) {
351
+ _password = options.password;
352
+ }
353
+ if (!isUndefined(options.output_format)) {
354
+ $outputFormat = options.output_format;
355
+ }
356
+
357
+ var logStepStart = function(stepName) {
358
+ console.formatLog("- " + stepName, {event: "step_start", name: stepName});
359
+ };
360
+ var logStepComplete = function(stepName) {
361
+ console.formatLog("✔ " + stepName, {event: "step_complete", name: stepName});
362
+ };
363
+ var logActionStart = function(actionName) {
364
+ console.formatLog("- " + actionName, {event: "action_start", name: actionName});
365
+ };
366
+ var logActionComplete = function(actionName) {
367
+ console.formatLog("✔ " + actionName, {event: "action_complete", name: actionName});
368
+ };
369
+ var logStepFail = function(stepName) {
370
+ console.formatLog("! " + actionName, {event: "step_fail", name: stepName});
371
+ };
372
+ var logPageError = function(error) {
373
+ console.formatLog("X - " + errors[i], {event: "page_error", error: error});
374
+ };
375
+
376
+ this.result = function(result) {
377
+ jsonLog({'result': result});
378
+ };
379
+
380
+ var createPage = function() {
381
+ var page = Casper.create({
382
+ verbose: true,
383
+ logLevel: 'warning',
384
+ //logLevel: 'info',
385
+ waitTimeout: 10000
386
+ });
387
+ page.echo = function(string, style) {
388
+ console.formatLog(string, {message: string, event: "echo"});
389
+ return page;
390
+ };
391
+
392
+ phantom.cookiesEnabled = true;
393
+ page.userAgent('Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.41 Safari/534.7');
394
+ return page;
395
+ };
396
+
397
+ // Works for both ADC and ITC
398
+ this.openPage = function(url, callback) {
399
+ var page = createPage();
400
+
401
+ logStepStart("Wait for the login URL");
402
+ page.start(url, function() {
403
+ logStepComplete("Wait for the login URL");
404
+ logActionStart("Fill the login form");
405
+ try {
406
+ Shortcuts.safeFill(page, 'form', {
407
+ 'theAccountName': _username,
408
+ 'theAccountPW': _password,
409
+ 'appleId': _username,
410
+ 'accountPassword': _password
411
+ }, true);
412
+ }
413
+ catch (ex) {
414
+ dieFromException(page, ex);
415
+ }
416
+ });
417
+ page.then(function(response) {
418
+ logActionComplete("Fill the login form");
419
+
420
+ logStepStart("Wait for the login to process");
421
+ if (page.exists('.dserror')) {
422
+ die(page, "Login error: " + page.getHTML('.dserror'));
423
+ }
424
+ logStepComplete("Wait for the login to process");
425
+ logActionStart("Open the action URL (" + url + ")");
426
+ });
427
+ page.thenOpen(url, function() {
428
+ logActionComplete("Wait for the action URL to load");
429
+ callback(page);
430
+ });
431
+
432
+ page.run();
433
+
434
+ this.setPage(page);
435
+ };
436
+
437
+ this.shortcuts = Shortcuts;
438
+
439
+ var addAction = function(actionName, actionImpl) {
440
+ _actions[actionName] = actionImpl;
441
+ };
442
+
443
+ var runAction = function(actionName) {
444
+ var action = _actions[actionName];
445
+ if (action) {
446
+ logActionStart(actionName);
447
+ try {
448
+ action();
449
+ logActionComplete(actionName);
450
+ } catch (ex) {
451
+ dieFromException(_currentPage, ex, {action: actionName});
452
+ }
453
+ }
454
+ else {
455
+ die(_currentPage, "Could not find registered action for '" + actionName + "'");
456
+ }
457
+ };
458
+
459
+ this.action = function(actionName, actionImpl) {
460
+ if (isUndefined(actionImpl)) {
461
+ runAction(actionName);
462
+ }
463
+ else {
464
+ addAction(actionName, actionImpl);
465
+ }
466
+ };
467
+
468
+ var logPageErrors = function() {
469
+ _currentPage.debugHTML();
470
+
471
+ var errors = _currentPage.evaluate(function() {
472
+ var errors = [];
473
+ var errorEls = document.querySelectorAll(".global-message.error li span");
474
+ for (var i = 0; i < errorEls.length; i++) {
475
+ errors.push(errorEls[i].innerHTML);
476
+ }
477
+ return errors;
478
+ });
479
+ for (var i = 0; i < errors.length; i++) {
480
+ logPageError(errors[i]);
481
+ }
482
+ };
483
+
484
+ this.step = function(stepName, methodName, methodArg, callback, options) {
485
+ var _step = {
486
+ name: stepName,
487
+ methodName: methodName,
488
+ methodArg: methodArg,
489
+ methodDescription: function() {
490
+ var argDescription = "'" + _step.methodArg + "'";
491
+ if (isFunction(_step.methodArg)) {
492
+ argDescription = "[function]";
493
+ }
494
+ return _step.name + " ( " + _step.methodName + "(" + argDescription + ")" + " )";
495
+ },
496
+ callback: callback,
497
+ options: options || {}
498
+ }
499
+ var method = _currentPage[_step.methodName];
500
+ var timeout = undefined;
501
+ if (_step.options.timeout) {
502
+ timeout = _step.options.timeout;
503
+ }
504
+ method.bind(_currentPage)(_step.methodArg, function() {
505
+ logStepComplete(_step.name);
506
+ if (isFunction(_step.callback)) {
507
+ _step.callback();
508
+ }
509
+ }, function() {
510
+ logStepFail(_step.name);
511
+ var userDieMethod = _step.options.onFail;
512
+ if (isFunction(_step.options)) {
513
+ userDieMethod = _step.options;
514
+ }
515
+ var shouldDie = true;
516
+ if (userDieMethod) {
517
+ shouldDie = (userDieMethod() !== false);
518
+ }
519
+ if (shouldDie === true) {
520
+ logPageErrors();
521
+ die(_currentPage, "Failed @ " + _step.methodDescription(), {step: _step.name});
522
+ }
523
+ }, timeout);
524
+ }
525
+
526
+ this.setPage = function(page) {
527
+ _currentPage = page;
528
+ }
529
+ };
530
+
531
+ var getOptionsWithManifest = function() {
532
+ var casper = require("casper").create();
533
+ var options = casper.cli.options;
534
+
535
+ var manifest = options.manifest;
536
+ if (manifest) {
537
+ var f = fs.open(manifest, "r");
538
+ options = JSON.parse(f.read());
539
+ }
540
+
541
+ return options;
542
+ }
543
+
544
+ var _commandConfigs = null;
545
+ var CommandConfigs = function() {
546
+ if (_commandConfigs === null) {
547
+ var options = getOptionsWithManifest();
548
+ // no __FILE__ equivalent in phantomjs, unfortunately
549
+ var applebotRootPath = options.applebot_root_path;
550
+ var _commandsFilePath = applebotRootPath + fs.separator + 'phantom' + fs.separator + '_commands.json';
551
+ _commandConfigs = JSON.parse(fs.open(_commandsFilePath, "r").read());
552
+ }
553
+ return _commandConfigs;
554
+ }
555
+
556
+ function CommandHandler(fileName) {
557
+
558
+ var _fullOptions = CommandConfigs()[fileName];
559
+ var _options = {};
560
+ _options.options = {
561
+ required: [],
562
+ optional: []
563
+ };
564
+
565
+ var optionToString = function(optionData) {
566
+ return optionData.key;
567
+ };
568
+ var parseOption = function(optionData) {
569
+ if (optionData.batch) {
570
+ return optionData.keys;
571
+ }
572
+ else {
573
+ return optionToString(optionData);
574
+ }
575
+ };
576
+ var parseOptions = function(optionList) {
577
+ var _optionList = [];
578
+ for (var i = optionList.length - 1; i >= 0; i--) {
579
+ var parsed = parseOption(optionList[i]);
580
+ if (parsed instanceof Array) {
581
+ _optionList = _optionList.concat(parsed);
582
+ }
583
+ else {
584
+ _optionList.push(parsed);
585
+ }
586
+ };
587
+ return _optionList;
588
+ };
589
+
590
+ if (_fullOptions.options.required) {
591
+ _options.options.required = parseOptions(_fullOptions.options.required);
592
+ }
593
+ if (_fullOptions.options.optional) {
594
+ _options.options.optional = parseOptions(_fullOptions.options.optional);
595
+ }
596
+
597
+ var _description = _options.description;
598
+ var _requiredCommands = _options.options.required;
599
+ var _optionalCommands = _options.options.optional;
600
+ var ARG_OFFSET = 1;
601
+
602
+ this.description = function() {
603
+ var requiredString = _requiredCommands.map(function(c) {
604
+ return "<" + c + ">";
605
+ }).join(" ");
606
+ var optionalString = _optionalCommands.map(function(c) {
607
+ return "--" + c + "=<" + c + ">";
608
+ }).join(" ");
609
+ return _description + ": " + requiredString + " " + optionalString;
610
+ }
611
+
612
+ this.parseArgs = function() {
613
+ var casper = require("casper").create();
614
+
615
+ var givenOptions = {};
616
+ var requiredValues = casper.cli.args || [];
617
+ var allButRequired = getOptionsWithManifest();
618
+
619
+ for (var i = 0; i < _requiredCommands.length; i++) {
620
+ var key = _requiredCommands[i];
621
+ if (allButRequired[key]) {
622
+ requiredValues.push(allButRequired[key]);
623
+ }
624
+ }
625
+
626
+ if (requiredValues.length != _requiredCommands.length) {
627
+ var message = "Incomplete arguments - need to include " + _requiredCommands;
628
+ throw message;
629
+ }
630
+
631
+ for (var i = 0; i < _requiredCommands.length; i++) {
632
+ var key = _requiredCommands[i];
633
+ givenOptions[key] = requiredValues[i];
634
+ }
635
+
636
+ for (var i = 0; i < _optionalCommands.length; i++) {
637
+ var key = _optionalCommands[i];
638
+ givenOptions[key] = allButRequired[key];
639
+ }
640
+
641
+ return givenOptions;
642
+ };
643
+
644
+ this.getAuthFromArgs = function() {
645
+ var options = getOptionsWithManifest();
646
+
647
+ return {
648
+ username: options.username,
649
+ password: options.password,
650
+ output_format: options.output_format
651
+ };
652
+ }
653
+ }
654
+
655
+ exports.AppleBot = AppleBot;
656
+ exports.CommandHandler = CommandHandler;