wave-service.js (23978B)
1 function sendRequest(method, uri, headers, data, onSuccess, onError) { 2 var xhr = new XMLHttpRequest(); 3 xhr.onload = function () { 4 if (xhr.status === 200) { 5 onSuccess(xhr.response); 6 } else { 7 if (onError) onError(xhr.status, xhr.response); 8 } 9 }; 10 xhr.onerror = function () { 11 if (onError) onError(); 12 }; 13 xhr.open(method, WaveService.uriPrefix + uri, true); 14 for (var header in headers) { 15 xhr.setRequestHeader(header, headers[header]); 16 } 17 xhr.send(data); 18 return xhr; 19 } 20 21 var WEB_ROOT = "{{WEB_ROOT}}"; 22 var HTTP_PORT = "{{HTTP_PORT}}"; 23 var HTTPS_PORT = "{{HTTPS_PORT}}"; 24 var OPEN = "open"; 25 var CLOSED = "closed"; 26 27 var WaveService = { 28 uriPrefix: WEB_ROOT, 29 socket: { 30 state: CLOSED, 31 onMessage: function () {}, 32 onOpen: function () {}, 33 onClose: function () {}, 34 send: function () {}, 35 close: function () {}, 36 onStateChange: function () {}, 37 }, 38 // SESSIONS API 39 createSession: function (configuration, onSuccess, onError) { 40 var data = JSON.stringify({ 41 tests: configuration.tests, 42 types: configuration.types, 43 timeouts: configuration.timeouts, 44 reference_tokens: configuration.referenceTokens, 45 expiration_date: configuration.expirationDate, 46 labels: configuration.labels, 47 }); 48 sendRequest( 49 "POST", 50 "api/sessions", 51 { "Content-Type": "application/json" }, 52 data, 53 function (response) { 54 var jsonObject = JSON.parse(response); 55 onSuccess(jsonObject.token); 56 }, 57 onError 58 ); 59 }, 60 readSession: function (token, onSuccess, onError) { 61 sendRequest( 62 "GET", 63 "api/sessions/" + token, 64 null, 65 null, 66 function (response) { 67 var jsonObject = JSON.parse(response); 68 onSuccess({ 69 token: jsonObject.token, 70 tests: jsonObject.tests, 71 types: jsonObject.types, 72 userAgent: jsonObject.user_agent, 73 labels: jsonObject.labels, 74 timeouts: jsonObject.timeouts, 75 browser: jsonObject.browser, 76 isPublic: jsonObject.is_public, 77 referenceTokens: jsonObject.reference_tokens, 78 }); 79 }, 80 onError 81 ); 82 }, 83 readMultipleSessions: function (tokens, onSuccess, onError) { 84 var requestsLeft = tokens.length; 85 if (requestsLeft === 0) onSuccess([]); 86 var configurations = []; 87 for (var i = 0; i < tokens.length; i++) { 88 var token = tokens[i]; 89 WaveService.readSession( 90 token, 91 function (configuration) { 92 requestsLeft--; 93 configurations.push(configuration); 94 if (requestsLeft === 0) onSuccess(configurations); 95 }, 96 function (status) { 97 if (status === 404) requestsLeft--; 98 if (status !== 404 && onError) onError(); 99 if (requestsLeft === 0) onSuccess(configurations); 100 } 101 ); 102 } 103 }, 104 readSessionStatus: function (token, onSuccess, onError) { 105 sendRequest( 106 "GET", 107 "api/sessions/" + token + "/status", 108 null, 109 null, 110 function (response) { 111 var jsonObject = JSON.parse(response); 112 var dateStarted = null; 113 if (jsonObject.date_started) { 114 dateStarted = new Date(jsonObject.date_started); 115 } 116 var dateFinished = null; 117 if (jsonObject.date_finished) { 118 dateFinished = new Date(jsonObject.date_finished); 119 } 120 var expirationDate = null; 121 if (jsonObject.expiration_date) { 122 expirationDate = new Date(jsonObject.expiration_date); 123 } 124 onSuccess({ 125 token: jsonObject.token, 126 dateStarted: dateStarted, 127 dateFinished: dateFinished, 128 testFilesCount: jsonObject.test_files_count, 129 testFilesCompleted: jsonObject.test_files_completed, 130 status: jsonObject.status, 131 expirationDate: expirationDate, 132 }); 133 }, 134 function () { 135 if (onError) onError(); 136 } 137 ); 138 }, 139 readMultipleSessionStatuses: function (tokens, onSuccess, onError) { 140 var requestsLeft = tokens.length; 141 if (requestsLeft === 0) onSuccess([]); 142 var statuses = []; 143 for (var i = 0; i < tokens.length; i++) { 144 var token = tokens[i]; 145 WaveService.readSessionStatus( 146 token, 147 function (status) { 148 requestsLeft--; 149 statuses.push(status); 150 if (requestsLeft === 0) onSuccess(statuses); 151 }, 152 function () { 153 requestsLeft--; 154 if (requestsLeft === 0) onSuccess(statuses); 155 } 156 ); 157 } 158 }, 159 readPublicSessions: function (onSuccess, onError) { 160 sendRequest( 161 "GET", 162 "api/sessions/public", 163 null, 164 null, 165 function (response) { 166 var jsonObject = JSON.parse(response); 167 onSuccess(jsonObject); 168 }, 169 onError 170 ); 171 }, 172 updateSession: function (token, configuration, onSuccess, onError) { 173 var data = JSON.stringify({ 174 tests: configuration.tests, 175 types: configuration.types, 176 timeouts: configuration.timeouts, 177 reference_tokens: configuration.referenceTokens, 178 expiration_date: configuration.expirationDate, 179 type: configuration.type, 180 }); 181 sendRequest( 182 "PUT", 183 "api/sessions/" + token, 184 { "Content-Type": "application/json" }, 185 data, 186 function () { 187 onSuccess(); 188 }, 189 onError 190 ); 191 }, 192 updateLabels: function (token, labels, onSuccess, onError) { 193 var data = JSON.stringify({ labels: labels }); 194 sendRequest( 195 "PUT", 196 "api/sessions/" + token + "/labels", 197 { "Content-Type": "application/json" }, 198 data, 199 function () { 200 if (onSuccess) onSuccess(); 201 }, 202 onError 203 ); 204 }, 205 findToken: function (fragment, onSuccess, onError) { 206 sendRequest( 207 "GET", 208 "api/sessions/" + fragment, 209 null, 210 null, 211 function (response) { 212 var jsonObject = JSON.parse(response); 213 onSuccess(jsonObject.token); 214 }, 215 onError 216 ); 217 }, 218 startSession: function (token, onSuccess, onError) { 219 sendRequest( 220 "POST", 221 "api/sessions/" + token + "/start", 222 null, 223 null, 224 function () { 225 onSuccess(); 226 }, 227 onError 228 ); 229 }, 230 pauseSession: function (token, onSuccess, onError) { 231 sendRequest( 232 "POST", 233 "api/sessions/" + token + "/pause", 234 null, 235 null, 236 function () { 237 onSuccess(); 238 }, 239 onError 240 ); 241 }, 242 stopSession: function (token, onSuccess, onError) { 243 sendRequest( 244 "POST", 245 "api/sessions/" + token + "/stop", 246 null, 247 null, 248 function () { 249 onSuccess(); 250 }, 251 onError 252 ); 253 }, 254 resumeSession: function (token, resumeToken, onSuccess, onError) { 255 var data = JSON.stringify({ resume_token: resumeToken }); 256 sendRequest( 257 "POST", 258 "api/sessions/" + token + "/resume", 259 { "Content-Type": "application/json" }, 260 data, 261 function () { 262 if (onSuccess) onSuccess(); 263 }, 264 function (response) { 265 if (onError) onError(response); 266 } 267 ); 268 }, 269 deleteSession: function (token, onSuccess, onError) { 270 sendRequest( 271 "DELETE", 272 "api/sessions/" + token, 273 null, 274 null, 275 function () { 276 onSuccess(); 277 }, 278 onError 279 ); 280 }, 281 282 // TESTS API 283 readTestList: function (onSuccess, onError) { 284 sendRequest( 285 "GET", 286 "api/tests", 287 null, 288 null, 289 function (response) { 290 var jsonObject = JSON.parse(response); 291 onSuccess(jsonObject); 292 }, 293 onError 294 ); 295 }, 296 readNextTest: function (token, onSuccess, onError) { 297 sendRequest( 298 "GET", 299 "api/tests/" + token + "/next", 300 null, 301 null, 302 function (response) { 303 var jsonObject = JSON.parse(response); 304 onSuccess(jsonObject.next_test); 305 }, 306 onError 307 ); 308 }, 309 readLastCompletedTests: function (token, resultTypes, onSuccess, onError) { 310 var status = ""; 311 if (resultTypes) { 312 for (var i = 0; i < resultTypes.length; i++) { 313 var type = resultTypes[i]; 314 status += type + ","; 315 } 316 } 317 sendRequest( 318 "GET", 319 "api/tests/" + token + "/last_completed?status=" + status, 320 null, 321 null, 322 function (response) { 323 var tests = JSON.parse(response); 324 var parsedTests = []; 325 for (var status in tests) { 326 for (var i = 0; i < tests[status].length; i++) { 327 var path = tests[status][i]; 328 parsedTests.push({ path: path, status: status }); 329 } 330 } 331 onSuccess(parsedTests); 332 }, 333 onError 334 ); 335 }, 336 readMalfunctioningTests: function (token, onSuccess, onError) { 337 sendRequest( 338 "GET", 339 "api/tests/" + token + "/malfunctioning", 340 null, 341 null, 342 function (response) { 343 var tests = JSON.parse(response); 344 onSuccess(tests); 345 }, 346 function (response) { 347 var errorMessage = JSON.parse(response).error; 348 onError(errorMessage); 349 } 350 ); 351 }, 352 updateMalfunctioningTests: function ( 353 token, 354 malfunctioningTests, 355 onSuccess, 356 onError 357 ) { 358 var data = JSON.stringify(malfunctioningTests); 359 sendRequest( 360 "PUT", 361 "api/tests/" + token + "/malfunctioning", 362 { "Content-Type": "application/json" }, 363 data, 364 function () { 365 onSuccess(); 366 }, 367 function (response) { 368 var errorMessage = JSON.parse(response).error; 369 onError(errorMessage); 370 } 371 ); 372 }, 373 readAvailableApis: function (onSuccess, onError) { 374 sendRequest( 375 "GET", 376 "api/tests/apis", 377 null, 378 null, 379 function (response) { 380 var apis = JSON.parse(response); 381 onSuccess(apis); 382 }, 383 function (response) { 384 if (!onError) return; 385 var errorMessage = JSON.parse(response).error; 386 onError(errorMessage); 387 } 388 ); 389 }, 390 391 // RESULTS API 392 createResult: function (token, result, onSuccess, onError) { 393 sendRequest( 394 "POST", 395 "api/results/" + token, 396 { "Content-Type": "application/json" }, 397 JSON.stringify(result), 398 function () { 399 onSuccess(); 400 }, 401 onError 402 ); 403 }, 404 readResults: function (token, onSuccess, onError) { 405 sendRequest( 406 "GET", 407 "api/results/" + token, 408 null, 409 null, 410 function (response) { 411 onSuccess(JSON.parse(response)); 412 }, 413 onError 414 ); 415 }, 416 readResultsCompact: function (token, onSuccess, onError) { 417 sendRequest( 418 "GET", 419 "api/results/" + token + "/compact", 420 null, 421 null, 422 function (response) { 423 var jsonObject = JSON.parse(response); 424 onSuccess(jsonObject); 425 }, 426 onError 427 ); 428 }, 429 readResultComparison: function (tokens, onSuccess, onError) { 430 var comparison = {}; 431 var fetchComplete = function (results) { 432 comparison.total = {}; 433 for (var i = 0; i < results.length; i++) { 434 var result = results[i]; 435 var token = result.token; 436 comparison[token] = {}; 437 for (var api in result) { 438 if (api === "token") continue; 439 comparison[token][api] = result[api].pass; 440 if (!comparison.total[api]) { 441 var total = 0; 442 for (var status in result[api]) { 443 total = total + result[api][status]; 444 } 445 comparison.total[api] = total; 446 } 447 } 448 } 449 onSuccess(comparison); 450 }; 451 var requestsLeft = tokens.length; 452 if (requestsLeft === 0) onSuccess([]); 453 var results = []; 454 for (var i = 0; i < tokens.length; i++) { 455 var token = tokens[i]; 456 (function (token) { 457 WaveService.readResultsCompact( 458 token, 459 function (result) { 460 requestsLeft--; 461 result.token = token; 462 results.push(result); 463 if (requestsLeft === 0) fetchComplete(results); 464 }, 465 function (responseStatus) { 466 if (responseStatus === 404) requestsLeft--; 467 if (status !== 404 && onError) onError(); 468 if (requestsLeft === 0) fetchComplete(results); 469 } 470 ); 471 })(token); 472 } 473 }, 474 downloadResults: function (token) { 475 location.href = "api/results/" + token + "/export"; 476 }, 477 downloadApiResult: function (token, api) { 478 location.href = "api/results/" + token + "/" + api + "/json"; 479 }, 480 downloadAllApiResults: function (token, api) { 481 location.href = "api/results/" + token + "/json"; 482 }, 483 downloadReport: function (token, api) { 484 location.href = "api/results/" + token + "/" + api + "/report"; 485 }, 486 importResults: function (data, onSuccess, onError) { 487 sendRequest( 488 "POST", 489 "api/results/import", 490 { "Content-Type": "application/octet-stream" }, 491 data, 492 function (response) { 493 var token = JSON.parse(response).token; 494 onSuccess(token); 495 }, 496 function (status, response) { 497 var errorMessage; 498 if (status === 500) { 499 errorMessage = "Internal server error."; 500 } else { 501 errorMessage = JSON.parse(response).error; 502 } 503 onError(errorMessage); 504 } 505 ); 506 }, 507 readReportUri: function (token, api, onSuccess, onError) { 508 sendRequest( 509 "GET", 510 "api/results/" + token + "/" + api + "/reporturl", 511 null, 512 null, 513 function (response) { 514 var jsonObject = JSON.parse(response); 515 onSuccess(jsonObject.uri); 516 }, 517 onError 518 ); 519 }, 520 downloadMultiReport: function (tokens, api) { 521 location.href = "api/results/" + api + "/report?tokens=" + tokens.join(","); 522 }, 523 readMultiReportUri: function (tokens, api, onSuccess, onError) { 524 sendRequest( 525 "GET", 526 "api/results/" + api + "/reporturl?tokens=" + tokens.join(","), 527 null, 528 null, 529 function (response) { 530 var jsonObject = JSON.parse(response); 531 onSuccess(jsonObject.uri); 532 }, 533 onError 534 ); 535 }, 536 downloadResultsOverview: function (token) { 537 location.href = "api/results/" + token + "/overview"; 538 }, 539 540 // DEVICES API 541 _device_token: null, 542 _deviceEventListeners: {}, 543 _deviceEventNumbers: {}, 544 registerDevice: function (onSuccess, onError) { 545 sendRequest( 546 "POST", 547 "api/devices", 548 null, 549 null, 550 function (response) { 551 var data = JSON.parse(response); 552 WaveService._device_token = data.token; 553 onSuccess(data.token); 554 }, 555 onError 556 ); 557 }, 558 readDevice: function (token, onSuccess, onError) { 559 sendRequest( 560 "GET", 561 "api/devices/" + token, 562 null, 563 null, 564 function (response) { 565 if (!onSuccess) return; 566 var data = JSON.parse(response); 567 onSuccess(data); 568 }, 569 function (error) { 570 if (!onError) return; 571 onError(error); 572 } 573 ); 574 }, 575 DEVICE_ADDED_EVENT: "device_added", 576 DEVICE_REMOVED_EVENT: "device_removed", 577 START_SESSION: "start_session", 578 addDeviceEventListener: function (token, callback) { 579 var listeners = WaveService._deviceEventListeners; 580 if (!listeners[token]) listeners[token] = []; 581 listeners[token].push(callback); 582 WaveService._deviceEventListeners = listeners; 583 WaveService.listenDeviceEvents(token); 584 }, 585 removeDeviceEventListener: function (callback) { 586 var listeners = WaveService._deviceEventListeners; 587 for (var token of Object.keys(listeners)) { 588 var index = listeners[token].indexOf(callback); 589 if (index === -1) continue; 590 listeners[token].splice(index, 1); 591 break; 592 } 593 WaveService._deviceEventListeners = listeners; 594 }, 595 listenDeviceEvents: function (token) { 596 var listeners = WaveService._deviceEventListeners; 597 if (!listeners[token] || listeners.length === 0) return; 598 var url = "api/devices/" + token + "/events"; 599 var lastEventNumber = WaveService._deviceEventNumbers[token]; 600 if (lastEventNumber) { 601 url += "?last_active=" + lastEventNumber; 602 } 603 WaveService.listenHttpPolling( 604 url, 605 function (response) { 606 if (!response) { 607 WaveService.listenDeviceEvents(token); 608 return; 609 } 610 for (var listener of listeners[token]) { 611 listener(response); 612 } 613 WaveService._deviceEventNumbers[token] = lastEventNumber; 614 WaveService.listenDeviceEvents(token); 615 }, 616 function () { 617 setTimeout(function () { 618 WaveService.listenDeviceEvents(); 619 }, 1000); 620 } 621 ); 622 }, 623 sendDeviceEvent: function (device_token, event, onSuccess, onError) { 624 var data = JSON.stringify({ 625 type: event.type, 626 data: event.data, 627 }); 628 sendRequest( 629 "POST", 630 "api/devices/" + device_token + "/events", 631 { "Content-Type": "application/json" }, 632 data, 633 onSuccess, 634 onError 635 ); 636 }, 637 addGlobalDeviceEventListener: function (callback) { 638 WaveService._globalDeviceEventListeners.push(callback); 639 WaveService.listenGlobalDeviceEvents(); 640 }, 641 removeGlobalDeviceEventListener: function (callback) { 642 var index = WaveService._globalDeviceEventListeners.indexOf(callback); 643 WaveService._globalDeviceEventListeners.splice(index, 1); 644 }, 645 listenGlobalDeviceEvents: function () { 646 var listeners = WaveService._globalDeviceEventListeners; 647 if (listeners.length === 0) return; 648 var query = ""; 649 if (WaveService._device_token) { 650 query = "?device_token=" + WaveService._device_token; 651 } 652 WaveService.listenHttpPolling( 653 "api/devices/events" + query, 654 function (response) { 655 if (!response) { 656 WaveService.listenGlobalDeviceEvents(); 657 return; 658 } 659 for (var listener of listeners) { 660 listener(response); 661 } 662 WaveService.listenGlobalDeviceEvents(); 663 }, 664 function () { 665 setTimeout(function () { 666 WaveService.listenGlobalDeviceEvents(); 667 }, 1000); 668 } 669 ); 670 }, 671 sendGlobalDeviceEvent: function (event, onSuccess, onError) { 672 var data = JSON.stringify({ 673 type: event.type, 674 data: event.data, 675 }); 676 sendRequest( 677 "POST", 678 "api/devices/events", 679 { "Content-Type": "application/json" }, 680 data, 681 onSuccess, 682 onError 683 ); 684 }, 685 686 // GENERAL API 687 readStatus: function (onSuccess, onError) { 688 sendRequest( 689 "GET", 690 "api/status", 691 null, 692 null, 693 function (response) { 694 var data = JSON.parse(response); 695 var configuration = { 696 readSessionsEnabled: data.read_sessions_enabled, 697 importResultsEnabled: data.import_results_enabled, 698 reportsEnabled: data.reports_enabled, 699 versionString: data.version_string, 700 testTypeSelectionEnabled: data.test_type_selection_enabled, 701 testFileSelectionEnabled: data.test_file_selection_enabled 702 }; 703 onSuccess(configuration); 704 }, 705 onError 706 ); 707 }, 708 709 // UTILITY 710 addRecentSession: function (token) { 711 if (!token) return; 712 var state = WaveService.getState(); 713 if (!state.recent_sessions) state.recent_sessions = []; 714 if (state.recent_sessions.indexOf(token) !== -1) return; 715 state.recent_sessions.unshift(token); 716 WaveService.setState(state); 717 }, 718 addRecentSessions: function (tokens) { 719 for (var i = 0; i < tokens.length; i++) { 720 var token = tokens[i]; 721 WaveService.addRecentSession(token); 722 } 723 }, 724 getPinnedSessions: function () { 725 var state = WaveService.getState(); 726 if (!state || !state.pinned_sessions) return []; 727 return state.pinned_sessions; 728 }, 729 addPinnedSession: function (token) { 730 if (!token) return; 731 var state = WaveService.getState(); 732 if (!state.pinned_sessions) state.pinned_sessions = []; 733 if (state.pinned_sessions.indexOf(token) !== -1) return; 734 state.pinned_sessions.unshift(token); 735 WaveService.setState(state); 736 }, 737 getRecentSessions: function () { 738 var state = WaveService.getState(); 739 if (!state || !state.recent_sessions) return []; 740 return state.recent_sessions; 741 }, 742 setRecentSessions: function (sessionTokens) { 743 var state = WaveService.getState(); 744 state.recent_sessions = sessionTokens; 745 WaveService.setState(state); 746 }, 747 removePinnedSession: function (token) { 748 if (!token) return; 749 var state = WaveService.getState(); 750 if (!state.pinned_sessions) return; 751 var index = state.pinned_sessions.indexOf(token); 752 if (index === -1) return; 753 state.pinned_sessions.splice(index, 1); 754 WaveService.setState(state); 755 }, 756 removeRecentSession: function (token) { 757 var state = WaveService.getState(); 758 if (!state.recent_sessions) return; 759 var index = state.recent_sessions.indexOf(token); 760 if (index === -1) return; 761 state.recent_sessions.splice(index, 1); 762 WaveService.setState(state); 763 }, 764 getState: function () { 765 if (!window.localStorage) return null; 766 var storage = window.localStorage; 767 var state = JSON.parse(storage.getItem("wave")); 768 if (!state) return {}; 769 return state; 770 }, 771 setState: function (state) { 772 if (!window.localStorage) return null; 773 var storage = window.localStorage; 774 storage.setItem("wave", JSON.stringify(state)); 775 }, 776 _globalDeviceEventListeners: [], 777 _sessionEventListeners: {}, 778 _sessionEventNumbers: {}, 779 listenHttpPolling: function (url, onSuccess, onError) { 780 var uniqueId = new Date().getTime(); 781 if (url.indexOf("?") === -1) { 782 url = url + "?id=" + uniqueId; 783 } else { 784 url = url + "&id=" + uniqueId; 785 } 786 sendRequest( 787 "GET", 788 url, 789 null, 790 null, 791 function (response) { 792 if (!response) { 793 onSuccess(null); 794 return; 795 } 796 onSuccess(JSON.parse(response)); 797 }, 798 onError 799 ); 800 }, 801 addSessionEventListener: function (token, callback) { 802 var listeners = WaveService._sessionEventListeners; 803 if (!listeners[token]) listeners[token] = []; 804 if (listeners[token].indexOf(callback) >= 0) return; 805 listeners[token].push(callback); 806 WaveService._sessionEventListeners = listeners; 807 WaveService.listenSessionEvents(token); 808 }, 809 removeSessionEventListener: function (callback) { 810 var listeners = WaveService._sessionEventListeners; 811 for (var token of Object.keys(listeners)) { 812 var index = listeners[token].indexOf(callback); 813 if (index === -1) continue; 814 listeners[token].splice(index, 1); 815 break; 816 } 817 WaveService._sessionEventListeners = listeners; 818 }, 819 listenSessionEvents: function (token) { 820 var listeners = WaveService._sessionEventListeners; 821 if (!listeners[token] || listeners.length === 0) return; 822 var url = "api/sessions/" + token + "/events"; 823 var lastEventNumber = WaveService._sessionEventNumbers[token]; 824 if (lastEventNumber) { 825 url += "?last_event=" + lastEventNumber; 826 } 827 WaveService.listenHttpPolling( 828 url, 829 function (response) { 830 if (!response) { 831 WaveService.listenSessionEvents(token); 832 return; 833 } 834 var lastEventNumber = 0; 835 for (var listener of listeners[token]) { 836 for (var event of response) { 837 if (event.number > lastEventNumber) { 838 lastEventNumber = event.number; 839 } 840 listener(event); 841 } 842 } 843 WaveService._sessionEventNumbers[token] = lastEventNumber; 844 WaveService.listenSessionEvents(token); 845 }, 846 function () { 847 setTimeout(function () { 848 WaveService.listenSessionEvents(); 849 }, 1000); 850 } 851 ); 852 }, 853 openSession: function (token) { 854 location.href = "/results.html?token=" + token; 855 }, 856 }; 857 858 if (!Object.keys) 859 Object.keys = function (o) { 860 if (o !== Object(o)) 861 throw new TypeError("Object.keys called on a non-object"); 862 var k = [], 863 p; 864 for (p in o) if (Object.prototype.hasOwnProperty.call(o, p)) k.push(p); 865 return k; 866 };