├── .gitignore ├── README.md ├── data ├── audio-iframe.html ├── audio-iframe.js ├── channels.js ├── chat-rtc.js ├── chat.html ├── domutils.js ├── freeze.js ├── logging.js ├── login-iframe.html ├── login-iframe.js ├── master.js ├── md5.js ├── mirror.js ├── rtc.js ├── share-worker.js ├── sharing.html ├── sharing.js ├── startup-help.html ├── user.js └── wsecho.js ├── examples └── index.html ├── lib ├── channels.js ├── chrome-md5.js ├── main.js ├── rtc.js ├── sharer.js ├── sidebar.js ├── startup-panel.js └── user.js ├── package.json ├── server.js ├── site ├── blank.html ├── bootstrap │ ├── css │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ └── js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js ├── client.js ├── homepage.css ├── homepage.html ├── share-button-screenshot.png ├── share-html-inner.js ├── share-html.js ├── share-link-2-screenshot.png ├── share-link-screenshot.png ├── view-inner.html └── view.html ├── test ├── test_diff.html └── test_jsmirror.html └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | Profile 2 | data/browsermirror.xpi 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Browser Mirror 2 | 3 | This is an experiment in session sharing through the browser. 4 | 5 | The goal is primarily to explore the feasibility of a particular technique; it is well possible that this will not be a feasible manner of session sharing, but exactly *why* should be interesting. 6 | 7 | ## Implementation 8 | 9 | This is not cooperative sharing, as in the case of Etherpad or Google Wave, but rather a master/client relationship. The master browser is the active browser, and runs all the Javascript. The mirror receives all the HTML (i.e., DOM elements) but no Javascript. Anything that *happens* happens on the master browser and is relayed to the mirror. 10 | 11 | The mirror in turn will capture events and relay them back to the master. (This is where feasibility becomes questionable.) So if for instance the mirror (client) user clicks on something, the specific element and the click event goes back to the master, where it is presented as though the master user clicked on the element. If this in turn has visible side-effects, these are then transmitted back to the mirror. 12 | 13 | Form inputs however are transmitted differently, in part because form status is not directly visible in the DOM, but also because the two browsers do not share a mouse cursor, place of focus, or typing cursor. Instead these edits are allowed in the mirror and the resulting values transmitted back to the master. There are problems however with things like an input that itself has a dynamic event handler, consider for instance `` which on focus removes the `Search...` text. This only happens in response to a focus event, and the focus event does not happen on the master. (Though perhaps it could be triggered, without actually triggering the master's true focus?) 14 | 15 | Editing conflicts are most likely to occur within these input fields as well. It is possible that a form of [Operational Transformation](http://en.wikipedia.org/wiki/Operational_transformation) could be used to specifically manage concurrent changes to form fields ([mobwrite](http://code.google.com/p/google-mobwrite/) could be used for this). It is unclear to me how WYSIWYG editors will work; though since `contentEditable` is a fairly explicit feature we could special case this as well. 16 | 17 | Notable iframes require additional work, but only insofar as compound pages must be handled. 18 | 19 | Flash is a complete no-go. 20 | 21 | ## Communication 22 | 23 | In addition to just sharing the same page, direct communication will be appropriate. Simple chat is implemented, as is the concept of highlighting an element. Because the two browsers are not necessarily rendering the same way (e.g., zoom levels, browser screen size, etc) all these operations have to happen on a DOM level. I.e., you can't point at a *place*, you can only point at an *element* (though we find the element closest to your place, and possibly we could use an offset to get finer resolution). 24 | 25 | There is a rough implementation of screen position, but I haven't figured out how to present that. Also noting changes would probably be useful, so you can see that the other user is changing something (potentially off-screen for you) 26 | . 27 | ## Bookmarklet or Plugin 28 | 29 | Right now this is implemented as a bookmarklet. This is relatively easy to work with and largely works. It cannot feasibly handle iframes, nor can it handle the transition to another page (i.e., if you click a link you will lose your sharing). 30 | 31 | Ultimately the master would probably be best as a plugin. This could manage the transitions, and it's possible some things will be revealed which can be solved in a plugin but could not with in-page Javascript. 32 | 33 | The mirror should probably not require any special browser functionality. This would also make it easier to share a session with any user, regardless of what they are using. 34 | 35 | ## Initiating the sharing 36 | 37 | Right now you are given a URL which the mirror user should go to, starting the sharing session. Later it's possible F1 or an Open Web App service could facilitate the starting of a shared session, utilizing things like your social contacts. This work seems to be progressing reasonably elsewhere, so this project won't attempt anything fancy. 38 | 39 | ## Native sharing 40 | 41 | A web application *written to be shared* can in many ways be more elegant than what is implemented here. For instance, it would be superior to share a Google Doc using their built-in sharing facilities than to use this technique. The reliability of course will be much higher right now, but even if this project worked exactly as intended it would still be better to use the first-class editing concepts built into the web application, which understand intention far better than this project can. 42 | 43 | One could imagine a formal way for a web page to indicate that it is shareable (and how), and for this to be a fallback when the page doesn't have a native sense of session sharing. 44 | 45 | ## Permissions 46 | 47 | Right now the mirror client basically has permission to do anything (though certain things like file selection are not possible). But it would be easy to give more limited permissions, for instance only permission to view a session, or to require confirmation before some actions take place (like browsing to another URL). With a plugin it would also be possible to allow the remote client to select a file (possibly using for tech support). 48 | -------------------------------------------------------------------------------- /data/audio-iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /data/audio-iframe.js: -------------------------------------------------------------------------------- 1 | var expectedOrigin = null; 2 | var source; 3 | 4 | var pc; 5 | 6 | // console messages don't show up in this context: 7 | console = { 8 | log: function () { 9 | var s = ""; 10 | for (var i=0; i this._pingPollMax ? this._pingPollMax : time; 271 | this._pingTimeout = setTimeout(this._setupConnection.bind(this), time); 272 | }, 273 | 274 | _receiveMessage: function (event) { 275 | if (event.source !== this.window) { 276 | return; 277 | } 278 | if (this.expectedOrigin && event.origin != this.expectedOrigin) { 279 | console.info("Expected message from", this.expectedOrigin, 280 | "but got message from", event.origin); 281 | return; 282 | } 283 | if (! this.expectedOrigin) { 284 | this.expectedOrigin = event.origin; 285 | } 286 | if (event.data == "hello") { 287 | this._pingReceived = true; 288 | if (this._pingTimeout) { 289 | clearTimeout(this._pingTimeout); 290 | this._pingTimeout = null; 291 | } 292 | if (this.onopen) { 293 | this.onopen(); 294 | } 295 | this._flush(); 296 | return; 297 | } 298 | this._incoming(event.data); 299 | }, 300 | 301 | close: function () { 302 | this.closed = true; 303 | this._pingReceived = false; 304 | if (this._pingTimeout) { 305 | clearTimeout(this._pingTimeout); 306 | } 307 | window.removeEventListener("message", this._receiveMessage, false); 308 | if (this.onclose) { 309 | this.onclose(); 310 | } 311 | } 312 | 313 | }); 314 | 315 | 316 | /* Handles message FROM an exterior window/parent */ 317 | var PostMessageIncomingChannel = AbstractChannel.subclass({ 318 | 319 | constructor: function (expectedOrigin) { 320 | this.source = null; 321 | this.expectedOrigin = expectedOrigin; 322 | this._receiveMessage = this._receiveMessage.bind(this); 323 | window.addEventListener("message", this._receiveMessage, false); 324 | }, 325 | 326 | toString: function () { 327 | var s = '[PostMessageIncomingChannel'; 328 | if (this.source) { 329 | s += ' bound to source ' + s; 330 | } else { 331 | s += ' awaiting source'; 332 | } 333 | return s + ']'; 334 | }, 335 | 336 | _send: function (data) { 337 | this.source.postMessage(data, this.expectedOrigin); 338 | }, 339 | 340 | _ready: function () { 341 | return !!this.source; 342 | }, 343 | 344 | _setupConnection: function () { 345 | }, 346 | 347 | _receiveMessage: function (event) { 348 | if (this.expectedOrigin && this.expectedOrigin != "*" && 349 | event.origin != this.expectedOrigin) { 350 | // FIXME: Maybe not worth mentioning? 351 | console.info("Expected message from", this.expectedOrigin, 352 | "but got message from", event.origin); 353 | return; 354 | } 355 | if (! this.expectedOrigin) { 356 | this.expectedOrigin = event.origin; 357 | } 358 | if (! this.source) { 359 | this.source = event.source; 360 | } 361 | if (event.data == "hello") { 362 | // Just a ping 363 | this.source.postMessage("hello", this.expectedOrigin); 364 | return; 365 | } 366 | this._incoming(event.data); 367 | }, 368 | 369 | close: function () { 370 | this.closed = true; 371 | window.removeEventListener("message", this._receiveMessage, false); 372 | if (this._pingTimeout) { 373 | clearTimeout(this._pingTimeout); 374 | } 375 | if (this.onclose) { 376 | this.onclose(); 377 | } 378 | } 379 | 380 | }); 381 | 382 | 383 | var CHROME_CHANNEL_EVENT = "BrowserMirrorChromeChannel"; 384 | 385 | /* Sends TO a window or iframe, from a CHROME window. Windows and 386 | iframes can't talk back to a chrome window, so we use another 387 | approach */ 388 | var ChromePostMessageChannel = AbstractChannel.subclass({ 389 | _pingPollPeriod: 100, // milliseconds 390 | 391 | constructor: function (win, doc, expectedOrigin) { 392 | this.expectedOrigin = expectedOrigin; 393 | this._pingReceived = false; 394 | this.doc = doc; 395 | if (win) { 396 | this.bindWindow(win, true); 397 | } 398 | this._receiveMessage = this._receiveMessage.bind(this); 399 | this.doc.addEventListener(CHROME_CHANNEL_EVENT, this._receiveMessage, false, true); 400 | }, 401 | 402 | toString: function () { 403 | var s = '[ChromePostMessageChannel'; 404 | if (this.window) { 405 | s += ' to window ' + this.window; 406 | } else { 407 | s += ' not bound to a window'; 408 | } 409 | if (this.window && ! this._pingReceived) { 410 | s += ' still establishing'; 411 | } 412 | return s + ']'; 413 | }, 414 | 415 | bindWindow: function (win, noSetup) { 416 | if (this.window) { 417 | this.close(); 418 | } 419 | if (win && win.contentWindow) { 420 | win = win.contentWindow; 421 | } 422 | this.window = win; 423 | if (! noSetup) { 424 | this._setupConnection(); 425 | } 426 | }, 427 | 428 | _send: function (data) { 429 | this.window.postMessage(data, this.expectedOrigin || "*"); 430 | }, 431 | 432 | _ready: function () { 433 | return this.window && this.pingReceived; 434 | }, 435 | 436 | _setupConnection: function () { 437 | if (this.closed || this._pingReceived || (! this.window)) { 438 | return; 439 | } 440 | this._send("hello"); 441 | this._pingTimeout = setTimeout(this._setupConnection.bind(this), this._pingPollPeriod); 442 | }, 443 | 444 | _receiveMessage: function (event) { 445 | var el = event.target; 446 | var data = el.getAttribute('data-payload'); 447 | el.parentNode.removeChild(el); 448 | el = null; 449 | if (data == "hello") { 450 | this._pingReceived = true; 451 | if (this._pingTimeout) { 452 | clearTimeout(this._pingTimeout); 453 | this._pingTimeout = null; 454 | } 455 | if (this.onopen) { 456 | this.onopen(); 457 | } 458 | this._flush(); 459 | return; 460 | } 461 | self._incoming(data); 462 | }, 463 | 464 | close: function () { 465 | this.closed = true; 466 | this.doc.remoteEventListener(CHROME_CHANNEL_EVENT, this._receiveMessage, false, true); 467 | if (this._pingTimeout) { 468 | clearTimeout(this._pingTimeout); 469 | } 470 | if (this.onclose) { 471 | this.onclose(); 472 | } 473 | } 474 | 475 | }); 476 | 477 | 478 | /* Handles message FROM an exterior CHROME window/parent, with events 479 | for handling the inability of these windows to talk to their chrome 480 | parents */ 481 | var ChromePostMessageIncomingChannel = AbstractChannel.subclass({ 482 | 483 | constructor: function () { 484 | this._receiveMessage = this._receiveMessage.bind(this); 485 | window.addEventListener("message", this._receiveMessage, false); 486 | }, 487 | 488 | toString: function () { 489 | var s = '[ChromePostMessageIncomingChannel]'; 490 | return s; 491 | }, 492 | 493 | _ready: function () { 494 | return !! document.head; 495 | }, 496 | 497 | _send: function (data) { 498 | var event = document.createEvent("Events"); 499 | event.initEvent(CHROME_CHANNEL_EVENT, true, false); 500 | var el = document.createElement("BrowserMirrorPayloadElement"); 501 | el.setAttribute("data-payload", data); 502 | document.head.appendChild(el); 503 | el.dispatchEvent(event); 504 | }, 505 | 506 | _setupConnection: function () { 507 | }, 508 | 509 | _receiveMessage: function (event) { 510 | if (event.source || event.origin) { 511 | // If either of these are set, it's not a postMessage from a 512 | // Chrome window 513 | // FIXME: warn? Or an option to warn in this case? 514 | return; 515 | } 516 | if (event.data == "hello") { 517 | if (this._ready()) { 518 | this._send("hello"); 519 | } else { 520 | // Wait a moment to respond 521 | setTimeout((function () { 522 | this._receiveMessage(event); 523 | }).bind(this), 100); 524 | } 525 | return; 526 | } 527 | this._incoming(event.data); 528 | }, 529 | 530 | close: function () { 531 | this.closed = true; 532 | window.removeEventListener("message", this._receiveMessage, false); 533 | if (this.onclose) { 534 | this.onclose(); 535 | } 536 | } 537 | 538 | }); 539 | 540 | /* Sends messages over a port. The "port" is the port object, such as self.port 541 | or worker.port; it must implement emit, on, and removeListener. 542 | 543 | Closed and Opened events are sent across the port to indicate when 544 | the remote channel is setup or closed. 545 | */ 546 | var PortChannel = AbstractChannel.subclass({ 547 | 548 | constructor: function (port, prefix) { 549 | this.prefix = prefix || ''; 550 | this.port = port; 551 | this._incoming = this._incoming.bind(this); 552 | this._remoteOpened = this._remoteOpened.bind(this); 553 | this._remoteClosed = this._remoteClosed.bind(this); 554 | this._gotHello = false; 555 | }, 556 | 557 | toString: function () { 558 | var s = '[PortChannel'; 559 | s += ' port: ' + this.port.toString(); 560 | if (this.prefix) { 561 | s += ' prefix: "' + this.prefix + '"'; 562 | } 563 | s += ']'; 564 | return s; 565 | }, 566 | 567 | _setupConnection: function () { 568 | this.port.on(this.prefix + 'Send', this._incoming); 569 | this.port.on(this.prefix + 'Opened', this._remoteOpened); 570 | this.port.on(this.prefix + 'Closed', this._remoteClosed); 571 | this.port.emit("Opened"); 572 | }, 573 | 574 | destroy: function () { 575 | this.port.removeListener(this.prefix + 'Send', this._incoming); 576 | this.port.removeListener(this.prefix + 'Opened', this._remoteOpened); 577 | this.port.removeListener(this.prefix + 'Closed', this._remoteClosed); 578 | }, 579 | 580 | _ready: function () { 581 | // FIXME: is any kind of ping necessary? 582 | return this._gotHello; 583 | }, 584 | 585 | _remoteOpened: function (helloBack) { 586 | this._gotHello = true; 587 | if (! helloBack) { 588 | // We'll say hello back, in case our original hello was lost 589 | this.port.emit("Opened", true); 590 | } 591 | if (this.onopen) { 592 | this.onopen(); 593 | } 594 | this._flush(); 595 | }, 596 | 597 | _remoteClosed: function () { 598 | this._gotHello = false; 599 | if (this.onclose) { 600 | this.onclose(); 601 | } 602 | this.destroy(); 603 | }, 604 | 605 | close: function () { 606 | this.port.emit("Closed"); 607 | this._remoteClosed(); 608 | }, 609 | 610 | _send: function (s) { 611 | this.port.emit("Send", s); 612 | } 613 | 614 | }); 615 | 616 | 617 | /* This proxies to another channel located in another process, via port.emit/port.on */ 618 | var PortProxyChannel = AbstractChannel.subclass({ 619 | 620 | constructor: function (prefix, self_) { 621 | this.prefix = prefix || ''; 622 | this.self = self_ || self; 623 | this._incoming = this._incoming.bind(this); 624 | this._remoteOpened = this._remoteOpened.bind(this); 625 | }, 626 | 627 | toString: function () { 628 | var s = '[PortProxyChannel'; 629 | if (typeof self == "undefined" || this.self !== self) { 630 | s += ' bound to self ' + this.self; 631 | } 632 | if (this.prefix) { 633 | s += ' with prefix "' + this.prefix + '"'; 634 | } 635 | return s + ']'; 636 | }, 637 | 638 | _setupConnection: function () { 639 | this.self.port.on(this.prefix + "IncomingData", this._incoming); 640 | this.self.port.on(this.prefix + "Opened", this._remoteOpened); 641 | }, 642 | 643 | _ready: function () { 644 | // FIXME: is any kind of ping necessary? 645 | return true; 646 | }, 647 | 648 | _send: function (data) { 649 | this.self.port.emit(this.prefix + "SendData", data); 650 | }, 651 | 652 | close: function () { 653 | try { 654 | this.self.port.emit(this.prefix + "Close"); 655 | } catch (e) { 656 | console.log('Error on close', e, e.name); 657 | } 658 | this.self.port.removeListener(this.prefix + "IncomingData", this._incoming); 659 | this.self.port.removeListener(this.prefix + "Opened", this._remoteOpened); 660 | this.closed = true; 661 | if (this.onclose) { 662 | this.onclose(); 663 | } 664 | }, 665 | 666 | _remoteOpened: function () { 667 | // Note this isn't the same as _ready, because we rely on the 668 | // remote connection to do caching/buffering 669 | if (this.onopen) { 670 | this.onopen(); 671 | } 672 | } 673 | 674 | }); 675 | 676 | /* Will handle incoming requests for the given channel over a port. 677 | Returns a function that tears down this connection. The teardown 678 | happens automatically on close. */ 679 | function PortIncomingChannel(channel, prefix, self_) { 680 | prefix = prefix || ''; 681 | self_ = self_ || self; 682 | function remoteSendData(data) { 683 | channel.send(data); 684 | } 685 | function remoteClose() { 686 | self_.port.removeListener(prefix + "SendData", remoteSendData); 687 | self_.port.removeListener(prefix + "Close", remoteClose); 688 | channel.close(); 689 | } 690 | self_.port.on(prefix + "SendData", remoteSendData); 691 | self_.port.on(prefix + "Close", remoteClose); 692 | channel.rawdata = true; 693 | channel.onmessage = function (data) { 694 | self_.port.emit(prefix + "IncomingData", data); 695 | }; 696 | channel.onopen = function () { 697 | self_.port.emit(prefix + "Opened"); 698 | }; 699 | channel.onclose = function () { 700 | // FIXME: call remoteClose? 701 | self_.port.emit(prefix + "Closed"); 702 | }; 703 | return { 704 | close: remoteClose 705 | }; 706 | } 707 | 708 | /* Echos all the connection proxying from from_ (the worker that wants 709 | a connection) to to_ (the worker that actually implements the 710 | connection). Returns a function that will tear down the connection. */ 711 | function EchoProxy(from_, to_, prefix) { 712 | prefix = prefix || ''; 713 | var bindings = []; 714 | function echo(name, source, dest) { 715 | function echoer() { 716 | var args = [name]; 717 | for (var i=0; i 2 | 3 | 20 | 21 | 22 | 23 | 38 | 39 |
40 | 41 |
42 | 43 | 46 | 47 | 50 | 51 |
52 |
53 |
54 | 55 |
56 |
57 |
58 | 59 | 62 | 63 | 64 | 69 | 70 | share 74 | 75 |
76 | 77 |
78 | 79 | Chat: 80 |
81 | 84 |
85 | 86 |
87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /data/domutils.js: -------------------------------------------------------------------------------- 1 | function getElementPosition(el) { 2 | var top = 0; 3 | var left = 0; 4 | while (el) { 5 | if (el.offsetTop) { 6 | top += el.offsetTop; 7 | } 8 | if (el.offsetLeft) { 9 | left += el.offsetLeft; 10 | } 11 | el = el.offsetParent; 12 | } 13 | return {top: top, left: left}; 14 | } 15 | 16 | function doOnLoad(func) { 17 | /* Runs a function on window load, or immediately if the window has 18 | already loaded */ 19 | if (this.document.readyState == 'complete') { 20 | func(); 21 | } else { 22 | window.addEventListener('load', func, false); 23 | } 24 | } 25 | 26 | 27 | function getScreenRange(doc) { 28 | /* Returns {start, end} where these elements are the closest ones 29 | to the top and bottom of the currently-visible screen. */ 30 | doc = doc || document; 31 | var win = doc.defaultView; 32 | var start = win.pageYOffset; 33 | var end = start + win.innerHeight; 34 | var nodes = iterNodes(document.body); 35 | var atStart = true; 36 | var startEl = null; 37 | var endEl = null; 38 | var startOffsetTop = 0; 39 | var endOffsetTop = 0; 40 | while (true) { 41 | var next = nodes(); 42 | if (! next) { 43 | break; 44 | } 45 | if (next.jsmirrorHide || (! next.jsmirrorId)) { 46 | continue; 47 | } 48 | if (next.nodeType != document.ELEMENT_NODE) { 49 | continue; 50 | } 51 | var offsetTop = 0; 52 | var el = next; 53 | while (el) { 54 | if (el.offsetTop) { 55 | offsetTop += el.offsetTop; 56 | } 57 | el = el.offsetParent; 58 | } 59 | if (atStart) { 60 | if (offsetTop > start) { 61 | startEl = endEl = next; 62 | atStart = false; 63 | startOffsetTop = start - offsetTop; 64 | continue; 65 | } 66 | } else { 67 | if (offsetTop + next.clientHeight > end) { 68 | break; 69 | } else { 70 | endOffsetTop = end - (offsetTop + next.clientHeight); 71 | endEl = next; 72 | } 73 | } 74 | } 75 | return {start: startEl, end: endEl, 76 | startOffsetTop: startOffsetTop, endOffsetTop: endOffsetTop}; 77 | } 78 | 79 | function iterNodes(start) { 80 | /* Iterates, in order (depth-first) all elements in the document. 81 | Returns a callback that yields these elements. */ 82 | var stack = [[start, null]]; 83 | var cursor = 0; 84 | return function () { 85 | if (! stack[cursor]) { 86 | // Finished 87 | return null; 88 | } 89 | var item = stack[cursor]; 90 | if (item[1] === null) { 91 | // We should return the item directly 92 | var result = item[0]; 93 | if (! result) { 94 | throw 'weird item in stack'; 95 | } 96 | if (result.nodeType == this.document.ELEMENT_NODE && result.childNodes.length) { 97 | // Put the children into the item's place 98 | stack[cursor] = [result.childNodes, 0]; 99 | } else { 100 | // Otherwise just pop this item from the stack 101 | stack[cursor] = null; 102 | cursor--; 103 | } 104 | } else { 105 | var result = item[0][item[1]]; 106 | if (! result) { 107 | throw 'weird item in stack'; 108 | } 109 | if (result.nodeType == this.document.ELEMENT_NODE && result.childNodes.length) { 110 | // Add this new element's child to the stack 111 | if (item[1] >= (item[0].length - 1)) { 112 | stack[cursor] = [result.childNodes, 0]; 113 | } else { 114 | item[1]++; 115 | cursor++; 116 | stack[cursor] = [result.childNodes, 0]; 117 | } 118 | } else { 119 | if (item[1] >= (item[0].length - 1)) { 120 | // We've finished with this element's children entirely 121 | stack[cursor] = null; 122 | cursor--; 123 | } else { 124 | // We have more to do, so just go to the next child 125 | item[1]++; 126 | } 127 | } 128 | } 129 | return result; 130 | }; 131 | } 132 | 133 | function expandRange(range) { 134 | /* Given a range object, return 135 | {start: el, startOffset: int, startSibling: bool, 136 | end: el, endOffset: int, endSibling: bool} 137 | The standard range object (https://developer.mozilla.org/en/DOM/range) tends to 138 | point to text nodes which are not referencable for us. If *Sibling is true, then the 139 | offset is after/before the element; if false then it is *interior to* the element. 140 | */ 141 | var result = {start: range.startContainer, end: range.endContainer, 142 | startOffset: range.startOffset, endOffset: range.endOffset, 143 | startText: false, endText: false}; 144 | function doit(name) { 145 | if (result[name].nodeType == this.document.TEXT_NODE) { 146 | while (true) { 147 | var prev = result[name].previousSibling; 148 | if (prev === null) { 149 | result[name] = result[name].parentNode; 150 | result[name+'Text'] = 'inner'; 151 | break; 152 | } else if (prev.nodeType == this.document.ELEMENT_NODE) { 153 | result[name] = prev; 154 | result[name+'Text'] = 'after'; 155 | break; 156 | } else if (prev.nodeType == this.document.TEXT_NODE) { 157 | result[name] = prev; 158 | result[name+'Offset'] += prev.nodeValue.length; 159 | } 160 | } 161 | } 162 | } 163 | doit('start'); doit('end'); 164 | return result; 165 | } 166 | 167 | function showRange(range, elCallback) { 168 | var inner; 169 | if (range.start == range.end && range.startText == 'inner' && range.endText == 'inner') { 170 | // A special case, when the range is entirely within one element 171 | var el = splitTextBetween(range.start, range.startOffset, range.endOffset); 172 | elCallback(el); 173 | return; 174 | } 175 | if (range.startText == 'inner') { 176 | range.start = splitTextAfter(range.start.childNodes[0], range.startOffset); 177 | } else if (range.startText == 'after') { 178 | range.start = splitTextAfter(range.start.nextSibling, range.startOffset); 179 | } else if (range.startOffset) { 180 | inner = range.start.childNodes[range.startOffset]; 181 | // While the spec says these offsets specify children, they don't always, and sometimes 182 | // the "container" is the element selected. 183 | if (inner) { 184 | range.start = inner; 185 | } 186 | } 187 | if (range.endText == 'inner') { 188 | range.end = splitTextBefore(range.end.childNodes[0], range.endOffset); 189 | } else if (range.endText == 'after') { 190 | range.end = splitTextBefore(range.end.nextSibling, range.endOffset); 191 | } else if (range.endOffset) { 192 | inner = range.end.childNodes[range.endOffset]; 193 | if (inner) { 194 | range.end = inner; 195 | } 196 | } 197 | // Now we strictly need to go from the start element to the end element (inclusive!) 198 | var pos = range.start; 199 | while (true) { 200 | elCallback(pos); 201 | pos = getNextElement(pos); 202 | if (pos === null) { 203 | log(WARN, 'pos fell out to null', range.start); 204 | break; 205 | } 206 | while (containsElement(pos, range.end)) { 207 | // FIXME: at some point pos might be a TextNode that needs to be wrapped in 208 | // a span. 209 | pos = pos.childNodes[0]; 210 | } 211 | if (pos == range.end) { 212 | elCallback(pos); 213 | break; 214 | } 215 | } 216 | } 217 | 218 | function containsElement(container, subelement) { 219 | /* Returns true if subelement is inside container 220 | Does not return true if subelement == container */ 221 | if (container == subelement) { 222 | return false; 223 | } 224 | if (container.nodeType != this.document.ELEMENT_NODE) { 225 | return false; 226 | } 227 | for (var i=0; i end) { 326 | throw 'Unexpected range: '+start+' to '+end; 327 | } 328 | var innerLength = end-start; 329 | var startText = ''; 330 | var endText = ''; 331 | var innerText = ''; 332 | var inStart = true; 333 | var inEnd = false; 334 | var textNodes = []; 335 | for (var i=0; i'; 295 | } 296 | var replSrc = null; 297 | if (el.tagName == 'IFRAME') { 298 | // FIXME: need to add element 299 | try { 300 | var html = this.staticHTML(el.contentWindow.document.documentElement); 301 | replSrc = this.encodeData('text/html', html); 302 | } catch (e) { 303 | console.warn('Had to skip iframe for permission reasons:', e+''); 304 | } 305 | } 306 | var s = '<' + el.tagName; 307 | var attrs = el.attributes; 308 | var l; 309 | if (attrs && (l = attrs.length)) { 310 | for (var i=0; i'; 330 | } 331 | return s; 332 | }; 333 | 334 | Freeze.getAttributes = function (el) { 335 | var result = []; 336 | var attrs = el.attributes; 337 | if (attrs && attrs.length) { 338 | var l = attrs.length; 339 | for (var i=0; i WARN && console.trace) { 6 | console.trace(); 7 | } 8 | if (typeof console == 'undefined') { 9 | return; 10 | } 11 | if (level < LOG_LEVEL) { 12 | return; 13 | } 14 | var args = []; 15 | for (var i=1; i= ERROR && console.error) { 20 | method = 'error'; 21 | } else if (level >= INFO && console.info) { 22 | method = 'info'; 23 | } else if (console.debug) { 24 | method = 'debug'; 25 | } 26 | if (! console[method]) { 27 | method = 'log'; 28 | } 29 | if (! console[method].apply) { 30 | // On Fennec I'm getting problems with console[method].apply 31 | console.log(args); 32 | } else { 33 | try { 34 | console[method].apply(console, args); 35 | } catch (e) { 36 | console[method].apply(console, ["Could not log: " + e]); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /data/login-iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | login 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /data/login-iframe.js: -------------------------------------------------------------------------------- 1 | // console messages don't show up in this context: 2 | console = { 3 | log: function () { 4 | var s = ""; 5 | for (var i=0; i= origLength) { 157 | commands.push(["delete_last_text", current.jsmirrorId]); 158 | } else { 159 | commands.push(["deletetext-", origChildren[origPos+1][1]]); 160 | } 161 | } else { 162 | // Some elements have to be deleted 163 | var startText = typeof origChildren[origPos] == "string"; 164 | for (var i=origPos; i= origChildren.length || curStart >= curChildren.length) { 210 | return null; 211 | } 212 | while (typeof curChildren[curStart] == "string" || 213 | (! curChildren[curStart].jsmirrorId)) { 214 | curStart++; 215 | if (curStart >= curChildren.length) { 216 | // There's nothing with an id 217 | return null; 218 | } 219 | } 220 | // First we see if we can find a match for curStart in origChildren 221 | var check = origStart; 222 | var checkId = curChildren[curStart].jsmirrorId; 223 | if (! checkId) { // FIXME: why is this if statement here 224 | while (check < origChildren.length) { 225 | if (typeof origChildren[check] != "string" && checkId == origChildren[check][1]) { 226 | return [check, curStart]; 227 | } 228 | check++; 229 | } 230 | } 231 | // We didn't find a match, so we'll try to find a match for the origStart in curChildren 232 | // This should never really go more than one loop 233 | while (typeof origChildren[origStart] == "string") { 234 | origStart++; 235 | if (origStart >= origChildren.length) { 236 | // There's no more elements 237 | return null; 238 | } 239 | } 240 | checkId = origChildren[origStart][1]; 241 | check = curStart; 242 | while (check < curChildren.length) { 243 | if (typeof curChildren[check] != "string" && 244 | checkId == curChildren[check].jsmirrorId) { 245 | return [origStart, check]; 246 | } 247 | check++; 248 | } 249 | // Fell out of the loop - nothing matched, so we'll try later elements all around 250 | return this.findNextMatch(origChildren, curChildren, origStart+1, curStart+1); 251 | }, 252 | 253 | processCommand: function (command) { 254 | if (command.event) { 255 | this.processEvent(command.event); 256 | } 257 | if (command.change) { 258 | this.processChange(command.change); 259 | } 260 | if (command.highlight) { 261 | this.processHighlight(command.highlight); 262 | } 263 | // Skipping screen 264 | if (command.hello) { 265 | // Have to send the doc again 266 | if (command.isMaster) { 267 | alert('Two computers are sending updates, everything will break!\n' + 268 | 'The other computer is at: ' + (command.href || 'unknown')); 269 | } 270 | this.lastSentDoc = this.lastSentDocData = this.sentRange = null; 271 | this.sendDoc(); 272 | } 273 | }, 274 | 275 | processEvent: function (event) { 276 | var realEvent = this.deserializeEvent(event); 277 | if (realEvent.type == 'keypress') { 278 | event.type = 'keydown'; 279 | var downEvent = this.deserializeEvent(event); 280 | this.dispatchEvent(downEvent, event.target); 281 | } 282 | this.dispatchEvent(realEvent, event.target); 283 | if (realEvent.type == 'keypress') { 284 | event.type = 'keyup'; 285 | var upEvent = this.deserializeEvent(event); 286 | this.dispatchEvent(upEvent, event.target); 287 | } 288 | }, 289 | 290 | deserializeEvent: function (event) { 291 | var value; 292 | var newEvent = this.document.createEvent(event.module); 293 | for (var i in event) { 294 | if (! event.hasOwnProperty(i)) { 295 | continue; 296 | } 297 | value = event[i]; 298 | if (value && typeof value == "object" && value.jsmirrorId) { 299 | var el = this.getElement(value.jsmirrorId) || null; 300 | console.log('derefing object', i, value.jsmirrorId, el); 301 | if (! el) { 302 | log(WARN, "Could not find element", value.jsmirrorId); 303 | } 304 | value = el; 305 | } 306 | event[i] = value; 307 | } 308 | this.initEvent(newEvent, event, event.module); 309 | // This might be redundant: 310 | for (i in event) { 311 | if (! event.hasOwnProperty(i)) { 312 | continue; 313 | } 314 | newEvent[i] = event[i]; 315 | } 316 | return newEvent; 317 | }, 318 | 319 | 320 | initEvent: function (event, data, module) { 321 | /* Instantiates an actual native event object */ 322 | if (module in this.eventAliases) { 323 | module = this.eventAliases[module]; 324 | } 325 | if (module === 'UIEvents') { 326 | event.initUIEvent( 327 | data.type, 328 | data.canBubble || true, 329 | data.cancelable || true, 330 | data.view || window, 331 | data.detail); 332 | } else if (module == 'MouseEvents') { 333 | log(INFO, { 334 | type:data.type, 335 | canBubble:data.canBubble || true, 336 | cancelable:data.cancelable || true, 337 | view:this.document.defaultView, 338 | detail:data.detail === undefined ? 1 : this.detail, 339 | screenX:data.screenX, 340 | screenY:data.screenY, 341 | clientX:data.clientX, 342 | clientY:data.clientY, 343 | ctrlKey:data.ctrlKey, 344 | altKey:data.altKey, 345 | shiftKey:data.shiftKey, 346 | metaKey:data.metaKey, 347 | button:data.button, 348 | relatedTraget:data.relatedTarget || null}); 349 | event.initMouseEvent( 350 | data.type, 351 | data.canBubble || true, 352 | data.cancelable || true, 353 | this.document.defaultView, 354 | 1,//data.detail, 355 | data.screenX, 356 | data.screenY, 357 | data.clientX, 358 | data.clientY, 359 | data.ctrlKey, 360 | data.altKey, 361 | data.shiftKey, 362 | data.metaKey, 363 | data.button, 364 | data.relatedTarget || null); 365 | } else if (module == 'HTMLEvents') { 366 | event.initEvent( 367 | data.type, 368 | data.canBubble || true, 369 | data.cancelable || true); 370 | } else if (module == 'KeyboardEvent') { 371 | var method = event.initKeyboardEvent ? 'initKeyboardEvent' : 'initKeyEvent'; 372 | event[method]( 373 | data.type, 374 | data.canBubble || true, 375 | data.cancelable || true, 376 | data.view || window, 377 | data.ctrlKey, 378 | data.altKey, 379 | data.shiftKey, 380 | data.metaKey, 381 | data.keyCode, 382 | data.charCode); 383 | } 384 | }, 385 | 386 | eventAliases: { 387 | UIEvent: 'UIEvents', 388 | KeyEvents: 'KeyboardEvent', 389 | Event: 'HTMLEvents', 390 | Events: 'HTMLEvents', 391 | MouseScrollEvents: 'MouseEvents', 392 | MouseEvent: 'MouseEvents', 393 | HTMLEvent: 'HTMLEvents', 394 | PopupEvents: 'MouseEvents' 395 | }, 396 | 397 | dispatchEvent: function (event, target) { 398 | log(INFO, 'Dispatching internal event', event.type, event, target); 399 | if (target && ! target.dispatchEvent) { 400 | log(WARN, 'huh', event, target, target===window); 401 | target = window; 402 | } 403 | if (target) { 404 | var doDefault = target.dispatchEvent(event); 405 | log(DEBUG, 'should do default', doDefault, event.type, target.tagName, target.href); 406 | if (doDefault && target['on'+event.type]) { 407 | // FIXME: how do you cancel this? 408 | target['on'+event.type](event); 409 | } 410 | if (doDefault) { 411 | if (event.type == 'click') { 412 | this.doDefaultAction(event, target); 413 | } 414 | } 415 | } else { 416 | // FIXME: do other default actions 417 | this.document.dispatchEvent(event); 418 | } 419 | }, 420 | 421 | doDefaultAction: function (event, target) { 422 | if (target.tagName === 'A') { 423 | if (target.href) { 424 | var base = target.href; 425 | var hash = ''; 426 | if (base.indexOf('#') != -1) { 427 | hash = base.substr(base.indexOf('#'), base.length); 428 | base = base.substr(0, base.indexOf('#')); 429 | } 430 | var hereBase = location.href; 431 | if (hereBase.indexOf('#') != -1) { 432 | hereBase = hereBase.substr(0, hereBase.indexOf('#')); 433 | } 434 | if (base === hereBase) { 435 | // Not a remote link, so it's okay 436 | location.hash = hash; 437 | this.channel.send({doc: {hash: hash}}); 438 | return; 439 | } 440 | // FIXME: make this a query: 441 | //this.queryHref(target.href); 442 | location.href = target.href; 443 | } 444 | return; 445 | } 446 | target = target.parentNode; 447 | if (target) { 448 | this.doDefaultAction(event, target); 449 | } 450 | }, 451 | 452 | processChange: function (change) { 453 | var target = this.getElement(change.target); 454 | target.value = change.value; 455 | var realEvent = this.document.createEvent("UIEvent"); 456 | realEvent.initUIEvent( 457 | "change", 458 | true, // canBubble 459 | true, // cancelable 460 | window, // view 461 | {} // detail 462 | ); 463 | var doDefault = target.dispatchEvent(realEvent); 464 | if (doDefault && target.onchange) { 465 | target.onchange(realEvent); 466 | } 467 | // FIXME: if not doDefault, should I set the target.value? 468 | // FIXME: and should I set value after or before firing events? 469 | // A normal change event will also fire lots of keydown and keyup events 470 | // which sometimes are caught instead of a change event. We'll trigger 471 | // a keyup event just to make sure... 472 | realEvent = this.document.createEvent('KeyboardEvent'); 473 | // FIXME: is it okay to leave both keyCode and charCode as 0? 474 | // FIXME: probably should only fire on text fields 475 | // FIXME: should test this with some field that does something special with Return 476 | var method = realEvent.initKeyboardEvent ? 'initKeyboardEvent' : 'initKeyEvent'; 477 | realEvent[method]( 478 | 'keyup', 479 | true, // canBubble 480 | true, // cancelable 481 | window, // view 482 | false, // ctrlKey 483 | false, // altKey 484 | false, // shiftKey 485 | false, // metaKey 486 | 0, // keyCode 487 | 0 // charCode 488 | ); 489 | doDefault = target.dispatchEvent(realEvent); 490 | if (doDefault && target.onkeyup) { 491 | target.onkeyup(realEvent); 492 | } 493 | }, 494 | 495 | processHighlight: function (highlight) { 496 | var el = this.getElement(highlight.target); 497 | if (el) { 498 | this.temporaryHighlight(el, highlight.offsetTop, highlight.offsetLeft, 499 | highlight.mode || 'remote'); 500 | } 501 | }, 502 | 503 | refreshElements: function () { 504 | return; 505 | this.elements = {}; 506 | function recur(elements, el) { 507 | elements[el.jsmirrorId] = el; 508 | var l = el.childNodes.length; 509 | for (var i=0; i 16) bkey = binl_md5(bkey, key.length * 8); 58 | 59 | var ipad = Array(16), opad = Array(16); 60 | for(var i = 0; i < 16; i++) 61 | { 62 | ipad[i] = bkey[i] ^ 0x36363636; 63 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 64 | } 65 | 66 | var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); 67 | return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)); 68 | } 69 | 70 | /* 71 | * Convert a raw string to a hex string 72 | */ 73 | function rstr2hex(input) 74 | { 75 | try { hexcase } catch(e) { hexcase=0; } 76 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; 77 | var output = ""; 78 | var x; 79 | for(var i = 0; i < input.length; i++) 80 | { 81 | x = input.charCodeAt(i); 82 | output += hex_tab.charAt((x >>> 4) & 0x0F) 83 | + hex_tab.charAt( x & 0x0F); 84 | } 85 | return output; 86 | } 87 | 88 | /* 89 | * Convert a raw string to a base-64 string 90 | */ 91 | function rstr2b64(input) 92 | { 93 | try { b64pad } catch(e) { b64pad=''; } 94 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 95 | var output = ""; 96 | var len = input.length; 97 | for(var i = 0; i < len; i += 3) 98 | { 99 | var triplet = (input.charCodeAt(i) << 16) 100 | | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0) 101 | | (i + 2 < len ? input.charCodeAt(i+2) : 0); 102 | for(var j = 0; j < 4; j++) 103 | { 104 | if(i * 8 + j * 6 > input.length * 8) output += b64pad; 105 | else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F); 106 | } 107 | } 108 | return output; 109 | } 110 | 111 | /* 112 | * Convert a raw string to an arbitrary string encoding 113 | */ 114 | function rstr2any(input, encoding) 115 | { 116 | var divisor = encoding.length; 117 | var i, j, q, x, quotient; 118 | 119 | /* Convert to an array of 16-bit big-endian values, forming the dividend */ 120 | var dividend = Array(Math.ceil(input.length / 2)); 121 | for(i = 0; i < dividend.length; i++) 122 | { 123 | dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); 124 | } 125 | 126 | /* 127 | * Repeatedly perform a long division. The binary array forms the dividend, 128 | * the length of the encoding is the divisor. Once computed, the quotient 129 | * forms the dividend for the next step. All remainders are stored for later 130 | * use. 131 | */ 132 | var full_length = Math.ceil(input.length * 8 / 133 | (Math.log(encoding.length) / Math.log(2))); 134 | var remainders = Array(full_length); 135 | for(j = 0; j < full_length; j++) 136 | { 137 | quotient = Array(); 138 | x = 0; 139 | for(i = 0; i < dividend.length; i++) 140 | { 141 | x = (x << 16) + dividend[i]; 142 | q = Math.floor(x / divisor); 143 | x -= q * divisor; 144 | if(quotient.length > 0 || q > 0) 145 | quotient[quotient.length] = q; 146 | } 147 | remainders[j] = x; 148 | dividend = quotient; 149 | } 150 | 151 | /* Convert the remainders to the output string */ 152 | var output = ""; 153 | for(i = remainders.length - 1; i >= 0; i--) 154 | output += encoding.charAt(remainders[i]); 155 | 156 | return output; 157 | } 158 | 159 | /* 160 | * Encode a string as utf-8. 161 | * For efficiency, this assumes the input is valid utf-16. 162 | */ 163 | function str2rstr_utf8(input) 164 | { 165 | var output = ""; 166 | var i = -1; 167 | var x, y; 168 | 169 | while(++i < input.length) 170 | { 171 | /* Decode utf-16 surrogate pairs */ 172 | x = input.charCodeAt(i); 173 | y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0; 174 | if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF) 175 | { 176 | x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF); 177 | i++; 178 | } 179 | 180 | /* Encode output as utf-8 */ 181 | if(x <= 0x7F) 182 | output += String.fromCharCode(x); 183 | else if(x <= 0x7FF) 184 | output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F), 185 | 0x80 | ( x & 0x3F)); 186 | else if(x <= 0xFFFF) 187 | output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F), 188 | 0x80 | ((x >>> 6 ) & 0x3F), 189 | 0x80 | ( x & 0x3F)); 190 | else if(x <= 0x1FFFFF) 191 | output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07), 192 | 0x80 | ((x >>> 12) & 0x3F), 193 | 0x80 | ((x >>> 6 ) & 0x3F), 194 | 0x80 | ( x & 0x3F)); 195 | } 196 | return output; 197 | } 198 | 199 | /* 200 | * Encode a string as utf-16 201 | */ 202 | function str2rstr_utf16le(input) 203 | { 204 | var output = ""; 205 | for(var i = 0; i < input.length; i++) 206 | output += String.fromCharCode( input.charCodeAt(i) & 0xFF, 207 | (input.charCodeAt(i) >>> 8) & 0xFF); 208 | return output; 209 | } 210 | 211 | function str2rstr_utf16be(input) 212 | { 213 | var output = ""; 214 | for(var i = 0; i < input.length; i++) 215 | output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF, 216 | input.charCodeAt(i) & 0xFF); 217 | return output; 218 | } 219 | 220 | /* 221 | * Convert a raw string to an array of little-endian words 222 | * Characters >255 have their high-byte silently ignored. 223 | */ 224 | function rstr2binl(input) 225 | { 226 | var output = Array(input.length >> 2); 227 | for(var i = 0; i < output.length; i++) 228 | output[i] = 0; 229 | for(var i = 0; i < input.length * 8; i += 8) 230 | output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32); 231 | return output; 232 | } 233 | 234 | /* 235 | * Convert an array of little-endian words to a string 236 | */ 237 | function binl2rstr(input) 238 | { 239 | var output = ""; 240 | for(var i = 0; i < input.length * 32; i += 8) 241 | output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF); 242 | return output; 243 | } 244 | 245 | /* 246 | * Calculate the MD5 of an array of little-endian words, and a bit length. 247 | */ 248 | function binl_md5(x, len) 249 | { 250 | /* append padding */ 251 | x[len >> 5] |= 0x80 << ((len) % 32); 252 | x[(((len + 64) >>> 9) << 4) + 14] = len; 253 | 254 | var a = 1732584193; 255 | var b = -271733879; 256 | var c = -1732584194; 257 | var d = 271733878; 258 | 259 | for(var i = 0; i < x.length; i += 16) 260 | { 261 | var olda = a; 262 | var oldb = b; 263 | var oldc = c; 264 | var oldd = d; 265 | 266 | a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); 267 | d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); 268 | c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); 269 | b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); 270 | a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); 271 | d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); 272 | c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); 273 | b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); 274 | a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); 275 | d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); 276 | c = md5_ff(c, d, a, b, x[i+10], 17, -42063); 277 | b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); 278 | a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); 279 | d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); 280 | c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); 281 | b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); 282 | 283 | a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); 284 | d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); 285 | c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); 286 | b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); 287 | a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); 288 | d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); 289 | c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); 290 | b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); 291 | a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); 292 | d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); 293 | c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); 294 | b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); 295 | a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); 296 | d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); 297 | c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); 298 | b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); 299 | 300 | a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); 301 | d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); 302 | c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); 303 | b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); 304 | a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); 305 | d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); 306 | c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); 307 | b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); 308 | a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); 309 | d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); 310 | c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); 311 | b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); 312 | a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); 313 | d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); 314 | c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); 315 | b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); 316 | 317 | a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); 318 | d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); 319 | c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); 320 | b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); 321 | a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); 322 | d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); 323 | c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); 324 | b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); 325 | a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); 326 | d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); 327 | c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); 328 | b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); 329 | a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); 330 | d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); 331 | c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); 332 | b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); 333 | 334 | a = safe_add(a, olda); 335 | b = safe_add(b, oldb); 336 | c = safe_add(c, oldc); 337 | d = safe_add(d, oldd); 338 | } 339 | return Array(a, b, c, d); 340 | } 341 | 342 | /* 343 | * These functions implement the four basic operations the algorithm uses. 344 | */ 345 | function md5_cmn(q, a, b, x, s, t) 346 | { 347 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); 348 | } 349 | function md5_ff(a, b, c, d, x, s, t) 350 | { 351 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); 352 | } 353 | function md5_gg(a, b, c, d, x, s, t) 354 | { 355 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); 356 | } 357 | function md5_hh(a, b, c, d, x, s, t) 358 | { 359 | return md5_cmn(b ^ c ^ d, a, b, x, s, t); 360 | } 361 | function md5_ii(a, b, c, d, x, s, t) 362 | { 363 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); 364 | } 365 | 366 | /* 367 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 368 | * to work around bugs in some JS interpreters. 369 | */ 370 | function safe_add(x, y) 371 | { 372 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 373 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 374 | return (msw << 16) | (lsw & 0xFFFF); 375 | } 376 | 377 | /* 378 | * Bitwise rotate a 32-bit number to the left. 379 | */ 380 | function bit_rol(num, cnt) 381 | { 382 | return (num << cnt) | (num >>> (32 - cnt)); 383 | } 384 | -------------------------------------------------------------------------------- /data/rtc.js: -------------------------------------------------------------------------------- 1 | function supportsWebRTC() { 2 | // FIXME: this is to disable the currently-nonfunctioning RTC support 3 | return false; 4 | return !!( 5 | (window.mozRTCPeerConnection || window.RTCPeerConnection) && 6 | (navigator.mozGetUserMedia || navigator.getUserMedia) 7 | ); 8 | } 9 | 10 | if (window.mozRTCPeerConnection) { 11 | RTCPeerConnection = mozRTCPeerConnection; 12 | } 13 | 14 | if (navigator.mozGetUserMedia) { 15 | navigator.getUserMedia = navigator.mozGetUserMedia; 16 | } 17 | 18 | function setupAudio(callback, audioEl, offer) { 19 | if (offer) { 20 | console.log('Got offer, creating answer'); 21 | } else { 22 | console.log('Creating offer'); 23 | } 24 | var pc = new RTCPeerConnection(); 25 | if (typeof audioEl == "string") { 26 | audioEl = document.getElementById(audioEl); 27 | } 28 | /*if (audioEl.tagName != "AUDIO") { 29 | // FIXME: add controls? 30 | var el = document.createElement("audio"); 31 | audioEl.appendChild(el); 32 | audioEl = el; 33 | }*/ 34 | if (audioEl.tagName != "VIDEO") { 35 | // FIXME: add controls? 36 | var el = document.createElement("video"); 37 | el.style.width = '100%'; 38 | audioEl.appendChild(el); 39 | audioEl = el; 40 | } 41 | console.log('creating media'); 42 | // FIXME: change to audio 43 | console.log(navigator.mozGetUserMedia({video: true}, function (stream) { 44 | console.log('media created', stream); 45 | pc.addStream(stream); 46 | audioEl.mozSrcObject = stream; 47 | audioEl.play(); 48 | if (offer) { 49 | console.log('setting remotedescription from offer'); 50 | pc.setRemoteDescription(offer, function () { 51 | console.log('remotedescription set / making answer'); 52 | pc.createAnswer(offer, function (answer) { 53 | console.log('createAnswer returned / setting localdescription'); 54 | pc.setLocalDescription(answer, function () { 55 | console.log('setLocalDescription done'); 56 | callback(null, {pc: pc, answer: answer}); 57 | }, function (code) { 58 | callback({stage: 'setLocalDescription', code: code}); 59 | }); 60 | }, function (code) { 61 | callback({stage: 'createAnswer', code: code}); 62 | }); 63 | }, function (code) { 64 | callback({stage: 'setRemoteDescription', code: code}); 65 | }); 66 | } else { 67 | // We need to generate an offer 68 | console.log('creating offer'); 69 | pc.createOffer(function (offer) { 70 | console.log('offer created / setting localDescription'); 71 | pc.setLocalDescription(offer, function () { 72 | console.log('Finished description ready for callback'); 73 | callback(null, {pc: pc, offer: offer}); 74 | }, function (code) { 75 | callback({stage: 'setLocalDescription', code: code}); 76 | }); 77 | }, function (code) { 78 | callback({stage: 'createOffer', code: code}); 79 | }); 80 | } 81 | }, function (code) { 82 | console.error("No stream available"); 83 | callback({stage: "getUserMedia", code: code}); 84 | })); 85 | console.log('thing finished'); 86 | } 87 | 88 | function respondToAnswer(callback, pc, answer) { 89 | pc.setRemoteDescription(function () { 90 | callback(); 91 | }, function (code) { 92 | callback({stage: "setRemoteDescription", code: code}); 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /data/share-worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | In-content share implementation for the addon 3 | */ 4 | 5 | var channel = null; 6 | var master = null; 7 | 8 | var myId = parseInt(Math.random()*1000, 10); 9 | 10 | console.log('Starting up worker', myId); 11 | 12 | self.port.on("StartShare", function () { 13 | channel = new PortChannel(self.port); 14 | console.log('Starting channel:', channel.toString()); 15 | channel.send({hello: true, isMaster: true, supportsWebRTC: supportsWebRTC()}); 16 | master = new Master(channel, unsafeWindow.document); 17 | channel.onmessage = function (data) { 18 | /* 19 | if (data.chatMessage) { 20 | self.port.emit("LocalMessage", data); 21 | } 22 | if (data.bye) { 23 | // FIXME: should sanitize ID 24 | self.port.emit("LocalMessage", {chatMessage: "Bye!", messageId: "browsermirror-bye-" + data.clientId}); 25 | } 26 | if (data.hello) { 27 | self.port.emit("LocalMessage", {connected: data.clientId}); 28 | } 29 | */ 30 | if (data.rtcOffer || data.rtcAnswer) { 31 | console.log('Got remote offer'); 32 | self.port.emit("RTC", data); 33 | return; 34 | } 35 | if (data.supportsWebRTC && supportsWebRTC()) { 36 | self.port.emit("SupportsWebRTC"); 37 | } 38 | if (data.hello) { 39 | channel.send({helloBack: true, isMaster: true, supportsWebRTC: supportsWebRTC()}); 40 | } 41 | master.processCommand(data); 42 | }; 43 | }); 44 | -------------------------------------------------------------------------------- /data/sharing.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /data/sharing.js: -------------------------------------------------------------------------------- 1 | self.port.on("ShareOn", function () { 2 | document.getElementById('share').innerHTML = 'sharing...'; 3 | }); 4 | 5 | self.port.on("ShareOff", function () { 6 | document.getElementById('share').innerHTML = 'share'; 7 | }); 8 | -------------------------------------------------------------------------------- /data/startup-help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Welcome to SeeItSaveIt! 5 | 6 | 7 | 8 |

Thanks for checking out BrowserMirror!

10 | 11 |

You should see a link/button at the bottom right of the 12 | browser: share — if you don't then you need to show the 14 | Add-on Bar:

15 | 16 |

Go to View > Toolbars > Add-on bar to 17 | display the bar 18 | (or read 19 | more here).

20 | 21 |

Once you see the button, click it to start sharing your session. 22 | Once you are sharing you have to tell someone else; on the sidebar on 23 | the left click "share" and it will give you a link 24 | that you can give to someone else to start sharing your session.

25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /data/user.js: -------------------------------------------------------------------------------- 1 | function Class(proto) { 2 | var makeInstance = function () { 3 | var instance = Object.create(proto); 4 | instance.constructor.apply(instance, arguments); 5 | return instance; 6 | }; 7 | makeInstance.prototype = proto; 8 | return makeInstance; 9 | } 10 | 11 | var Users = Class({ 12 | constructor: function (containerEl) { 13 | this.data = {}; 14 | this.containerEl = containerEl; 15 | }, 16 | 17 | get: function (clientId) { 18 | if (! this.data.hasOwnProperty(clientId)) { 19 | var user = new User(clientId, this); 20 | user.addToUserList(this.containerEl); 21 | this.data[clientId] = user; 22 | return user; 23 | } else { 24 | return this.data[clientId]; 25 | } 26 | }, 27 | 28 | checkAll: function () { 29 | for (var i in this.data) { 30 | if (! this.data.hasOwnProperty(i)) { 31 | continue; 32 | } 33 | var user = this.data[i]; 34 | user.checkTimeout(); 35 | } 36 | }, 37 | 38 | removeUser: function (user) { 39 | delete this.data[user.clientId]; 40 | }, 41 | 42 | setContainer: function (el) { 43 | this.containerEl = el; 44 | for (var i in this.data) { 45 | if (! this.data.hasOwnProperty(i)) { 46 | continue; 47 | } 48 | this.data[i].addToUserList(el); 49 | } 50 | }, 51 | 52 | destroy: function () { 53 | for (var i in this.data) { 54 | if (! this.data.hasOwnProperty(i)) { 55 | continue; 56 | } 57 | this.data[i].removeUser(); 58 | } 59 | this.data = {}; 60 | } 61 | }); 62 | 63 | function User(clientId, users) { 64 | this.clientId = clientId; 65 | this.users = users; 66 | this.lastPing = Date.now(); 67 | } 68 | 69 | User.prototype = { 70 | 71 | MAX_PING_TIME: 1000 * 60 * 30, // 30 minutes 72 | BYE_TIMEOUT: 1000, // 1 second until a "bye" is final 73 | defunct: false, 74 | timingOut: false, 75 | 76 | processCommand: function (command) { 77 | this.lastPing = Date.now(); 78 | if (this.timingOut) { 79 | this.setTimingOut(); 80 | } 81 | this.timingOut = false; 82 | if (command.email) { 83 | // FIXME: this just trusts the user 84 | this.setEmail(command.email); 85 | } 86 | if (command.bye) { 87 | this.defunct = true; 88 | // FIXME: show that the user is going? 89 | this.timingOut = true; 90 | this.setTimingOut(); 91 | setTimeout(this.checkTimeout.bind(this), this.BYE_TIMEOUT); 92 | } 93 | }, 94 | 95 | checkTimeout: function () { 96 | if (this.timingOut) { 97 | this.removeUser(); 98 | } 99 | if (this.isExpired()) { 100 | this.timingOut = true; 101 | this.setTimingOut(); 102 | setTimeout(this.checkTimeout.bind(this), this.BYE_TIMEOUT); 103 | } 104 | }, 105 | 106 | setTimingOut: function () { 107 | if (! this.userElement) { 108 | return; 109 | } 110 | if (this.timingOut) { 111 | if (this.userElement.className.search(/timing-out/) == -1) { 112 | this.userElement.className += ' timing-out'; 113 | } 114 | } else if (this.userElement.className.search(/timing-out/) != -1) { 115 | this.userElement.className = this.userElement.className.replace(/\s*timing-out/, ''); 116 | } 117 | }, 118 | 119 | setEmail: function (email) { 120 | if (this.email) { 121 | this.email = null; 122 | this.showIcon(); 123 | this.showName(); 124 | } 125 | this.email = email; 126 | this.addIcon(); 127 | this.showName(); 128 | }, 129 | 130 | showIcon: function () { 131 | if (! this.userElement) { 132 | return; 133 | } 134 | if (this.email) { 135 | var img = this.userElement.ownerDocument.createElememnt('img'); 136 | img.src = secureGravatar(this.email, 32, 'retro'); 137 | img.className = 'profile'; 138 | this.userElement.insertBefore(img, this.userElement.childNodes[0]); 139 | } else { 140 | var el = this.userElement.querySelector('img.profile'); 141 | if (el) { 142 | el.parentNode.removeChild(el); 143 | } 144 | } 145 | }, 146 | 147 | showName: function () { 148 | if (! this.userElement) { 149 | return; 150 | } 151 | var el = this.userElement.querySelector('span.username'); 152 | el.innerHTML = ''; 153 | el.appendChild(el.ownerDocument.createTextNode(this.email || 'unknown person')); 154 | el.className = 'username'; 155 | if (! this.email) { 156 | el.className += ' unknown'; 157 | } 158 | }, 159 | 160 | isExpired: function () { 161 | if (this.defunct) { 162 | return true; 163 | } 164 | return Date.now() - this.lastPing > this.MAX_PING_TIME; 165 | }, 166 | 167 | removeUser: function () { 168 | if (this.userElement) { 169 | this.userElement.parentNode.removeChild(this.userElement); 170 | } 171 | this.userElement = null; 172 | this.users.removeUser(this); 173 | }, 174 | 175 | addToUserList: function (userListEl) { 176 | if (this.userElement) { 177 | this.removeUser(); 178 | } 179 | if (! userListEl) { 180 | return; 181 | } 182 | var doc = userListEl.ownerDocument; 183 | this.userElement = doc.createElement('div'); 184 | this.userElement.className = 'user'; 185 | var name = doc.createElement('span'); 186 | name.className = 'username'; 187 | this.userElement.appendChild(name); 188 | userListEl.appendChild(this.userElement); 189 | } 190 | 191 | }; 192 | 193 | function secureGravatar(email, size, fallback) { 194 | email = email.replace(/^\s*/, ''); 195 | email = email.replace(/\s*$/, ''); 196 | email = email.toLowerCase(); 197 | return ('https://secure.gravatar.com/avatar/' + 198 | hex_md5(email) + 199 | '?size=' + encodeURIComponent(size) + 200 | 'd=' + encodeURIComponent(fallback)); 201 | } 202 | 203 | 204 | if (typeof require != "undefined") { 205 | hex_md5 = require("./chrome-md5.js").hex_md5; 206 | setTimeout = require("timers").setTimeout; 207 | } 208 | 209 | if (typeof exports != "undefined") { 210 | exports.Users = Users; 211 | } 212 | -------------------------------------------------------------------------------- /data/wsecho.js: -------------------------------------------------------------------------------- 1 | /* Acts as a kind of echoing websocket */ 2 | 3 | function startProxier(address) { 4 | var channel = null; 5 | var channelAddress = null; 6 | var closing = false; 7 | var closer = null; 8 | channel = new WebSocketChannel(address); 9 | closer = PortIncomingChannel(channel); 10 | } 11 | 12 | self.port.on("StartProxier", startProxier); 13 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Example document 4 | 5 | 16 | 17 | 18 | 19 |

Example

20 | 21 |

This is an example document. 22 | A field: 23 |

24 | 25 |

A link: testy

26 | 27 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 28 | 29 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 30 | 31 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 32 | 33 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 34 | 35 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 36 | 37 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 38 | 39 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 40 | 41 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 42 | 43 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 44 | 45 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 46 | 47 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /lib/channels.js: -------------------------------------------------------------------------------- 1 | ../data/channels.js -------------------------------------------------------------------------------- /lib/chrome-md5.js: -------------------------------------------------------------------------------- 1 | const { CC, Ci } = require("chrome"); 2 | 3 | exports.hex_md5 = function (str) { 4 | var converter = 5 | Cc["@mozilla.org/intl/scriptableunicodeconverter"]. 6 | createInstance(Ci.nsIScriptableUnicodeConverter); 7 | 8 | converter.charset = "UTF-8"; 9 | // result is an out parameter, 10 | // result.value will contain the array length 11 | var result = {}; 12 | // data is an array of bytes 13 | var data = converter.convertToByteArray(str, result); 14 | var ch = Cc["@mozilla.org/security/hash;1"] 15 | .createInstance(Ci.nsICryptoHash); 16 | ch.init(ch.MD5); 17 | ch.update(data, data.length); 18 | var hash = ch.finish(false); 19 | 20 | // return the two-digit hexadecimal code for a byte 21 | function toHexString(charCode) 22 | { 23 | return ("0" + charCode.toString(16)).slice(-2); 24 | } 25 | 26 | // convert the binary hash data to a hex string. 27 | var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join(""); 28 | return s; 29 | }; 30 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | The main.js module handles the widget that activates sharing, and the 3 | configuration for URLs 4 | */ 5 | 6 | const widgets = require("widget"); 7 | const data = require("self").data; 8 | const tabs = require("tabs"); 9 | const { Sharer } = require("./sharer.js"); 10 | const { btoa } = require("chrome").Cu.import("resource://gre/modules/Services.jsm"); 11 | const simplePrefs = require('simple-prefs'); 12 | const { StartupPanel } = require("./startup-panel"); 13 | 14 | function getServer() { 15 | return simplePrefs.prefs.server.replace(/\/*$/, ''); 16 | } 17 | 18 | function httpServer(server) { 19 | // Fixes problem where insecure content (like CSS from a site) isn't 20 | // served up because the viewer is over https. Not sure what we 21 | // should really do. 22 | var livePrefix = 'https://browsermirror.ianbicking.org'; 23 | if (server.indexOf(livePrefix) == 0) { 24 | return 'http://browsermirror.ianbicking.org:8080' + server.substr(livePrefix.length, server.length); 25 | } 26 | return server; 27 | } 28 | 29 | function makeId() { 30 | if (simplePrefs.prefs.stickyShareId) { 31 | return simplePrefs.prefs.stickyShareId; 32 | } 33 | return btoa(Math.random()).replace(/=/g, '').substr(0, 10); 34 | } 35 | 36 | StartupPanel({ 37 | name: "SeeItSaveIt", 38 | contentURL: data.url("startup-help.html") 39 | }); 40 | 41 | var sharer = widgets.Widget({ 42 | id: "browsermirror-sharer", 43 | label: "Share This Session", 44 | contentURL: data.url("sharing.html"), 45 | contentScriptFile: data.url("sharing.js"), 46 | onClick: startShare, 47 | width: 46 48 | }); 49 | 50 | var sharingWorker = null; 51 | 52 | function startShare() { 53 | if (sharingWorker) { 54 | sharingWorker.destroy(); 55 | sharingWorker = null; 56 | sharer.port.emit("ShareOff"); 57 | return; 58 | } 59 | var id = makeId(); 60 | var server = getServer(); 61 | var urls = { 62 | hub: server + '/hub/' + id, 63 | share: httpServer(server) + '/' + id, 64 | audioIframe: server + '/data/audio-iframe.html', 65 | loginIframe: server + '/data/login-iframe.html', 66 | blank: server + '/static/blank.html' 67 | }; 68 | sharingWorker = new Sharer(tabs.activeTab, urls, function () { 69 | sharingWorker.destroy(); 70 | sharingWorker = null; 71 | sharer.port.emit("ShareOff"); 72 | }); 73 | sharer.port.emit("ShareOn"); 74 | } 75 | -------------------------------------------------------------------------------- /lib/rtc.js: -------------------------------------------------------------------------------- 1 | const { Cu, Ci, Cc } = require("chrome"); 2 | Cu.import("resource://gre/modules/Services.jsm"); 3 | 4 | 5 | const windowMediator = Cc['@mozilla.org/appshell/window-mediator;1']. 6 | getService(Ci.nsIWindowMediator); 7 | 8 | function getBrowser() { 9 | /* 10 | FIXME: this is how it's done in webrtcUI.jsm: 11 | 12 | let someWindow = Services.wm.getMostRecentWindow(null); 13 | let contentWindow = someWindow.QueryInterface(Ci.nsIInterfaceRequestor) 14 | .getInterface(Ci.nsIDOMWindowUtils) 15 | .getOuterWindowWithId(windowID); 16 | */ 17 | let contentWindow = windowMediator.getMostRecentWindow("navigator:browser"); 18 | let browser = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) 19 | .getInterface(Ci.nsIWebNavigation) 20 | .QueryInterface(Ci.nsIDocShell) 21 | .chromeEventHandler; 22 | return browser; 23 | } 24 | 25 | function getUserMedia(options, onsuccess, onerror) { 26 | let browser = getBrowser(); 27 | let navigator = browser.ownerDocument.defaultView.navigator; 28 | if (! onerror) { 29 | onerror = function (error) { 30 | Cu.reportError(error); 31 | }; 32 | } 33 | navigator.mozGetUserMedia(options, onsuccess, onerror); 34 | } 35 | 36 | function getPeerConnection() { 37 | let browser = getBrowser(); 38 | let win = browser.ownerDocument.defaultView; 39 | return new win.MozRTCPeerConnection(); 40 | } 41 | 42 | exports.getUserMedia = getUserMedia; 43 | -------------------------------------------------------------------------------- /lib/sharer.js: -------------------------------------------------------------------------------- 1 | const { EchoProxy, ChromePostMessageChannel, PortProxyChannel, PortChannel } = require("./channels.js"); 2 | const { data } = require("self"); 3 | const clipboard = require("clipboard"); 4 | const { Sidebar } = require("./sidebar"); 5 | const tabs = require("tabs"); 6 | const { Page } = require("page-worker"); 7 | const { setInterval } = require("timers"); 8 | const { Users } = require("./user.js"); 9 | const windows = require("windows"); 10 | const { getUserMedia } = require("./rtc"); 11 | 12 | function Sharer(tab, urls, onclose) { 13 | this.tab = tab; 14 | this.urls = urls; 15 | this._onclose = onclose; 16 | this.sidebar = new Sidebar({ 17 | title: 'Sharing', 18 | url: data.url('chat.html'), 19 | onReady: (function () { 20 | this.bindChatEvents(); 21 | }).bind(this), 22 | showForTab: (function (tab) { 23 | if (tab == this.tab) { 24 | return true; 25 | } 26 | // We have to check if we changed tabs in *this* window, or changed windows 27 | // or tabs in another window 28 | var tabs = windows.browserWindows.activeWindow.tabs; 29 | for (var i=0; i", 5 | "version": "0.1", 6 | "fullName": "Browser Mirror", 7 | "id": "jid1-TppibpBYlnPBrQ", 8 | "description": "Browser session sharing", 9 | "preferences": [{ 10 | "name": "server", 11 | "title": "Echo/Server URL", 12 | "description": "This is where to find the server for echoing messages", 13 | "type": "string", 14 | "value": "https://browsermirror.ianbicking.org" 15 | }, 16 | { 17 | "name": "stickyShareId", 18 | "title": "Sticky share ID", 19 | "description": "For testing, you can force your shared session to always be at the same URL. Enter a value like email@example.com", 20 | "type": "string", 21 | "value": "" 22 | }] 23 | } 24 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var WebSocketServer = require('websocket').server; 2 | var WebSocketRouter = require('websocket').router; 3 | var http = require('http'); 4 | var https = require('https'); 5 | var static = require('node-static'); 6 | var parseUrl = require('url').parse; 7 | var fs = require('fs'); 8 | 9 | // FIXME: this is a terrible way to override the mimetype: 10 | var mime = require('./node_modules/node-static/lib/node-static/mime.js'); 11 | mime.contentTypes['xpi'] = 'application/x-xpinstall'; 12 | 13 | var dataRoot = new static.Server(__dirname + '/data', {cache: 7200}); 14 | var siteRoot = new static.Server(__dirname + '/site', {cache: 7200}); 15 | 16 | var server = http.createServer(function(request, response) { 17 | var url = parseUrl(request.url); 18 | var filename = null; 19 | var app = null; 20 | if (url.pathname == '/') { 21 | filename = 'homepage.html'; 22 | app = siteRoot; 23 | } else if (url.pathname.indexOf('/data/') == 0) { 24 | filename = url.pathname.substr('/data/'.length); 25 | app = dataRoot; 26 | } else if (url.pathname.indexOf('/static/') == 0) { 27 | filename = url.pathname.substr('/static/'.length); 28 | app = siteRoot; 29 | } else if (url.pathname.search(/^\/[^\/]+\/?$/) == 0) { 30 | filename = '/view.html'; 31 | app = siteRoot; 32 | } 33 | if (filename) { 34 | // FIXME: doesn't handle 404s (stops entire server): 35 | var fullPath = app.resolve(filename); 36 | fs.exists(fullPath, function (exists) { 37 | if (! exists) { 38 | write404(response); 39 | } else { 40 | app.serveFile(filename, 200, {}, request, response); 41 | } 42 | }); 43 | return; 44 | } 45 | console.log((new Date()) + ' Received request for ' + request.url); 46 | write404(response); 47 | }); 48 | 49 | function write404(response) { 50 | response.writeHead(404); 51 | response.end(); 52 | } 53 | 54 | function startServer(port) { 55 | server.listen(port, '0.0.0.0', function() { 56 | console.log((new Date()) + ' Server is listening on port ' + port); 57 | }); 58 | } 59 | 60 | var wsServer = new WebSocketServer({ 61 | httpServer: server, 62 | // 10Mb max size (1Mb is default, maybe unnecessary) 63 | maxReceivedMessageSize: 0x1000000, 64 | // The browser doesn't seem to break things up into frames (not sure what this means) 65 | // and the default of 64Kb was exceeded; raised to 1Mb 66 | maxReceivedFrameSize: 0x100000, 67 | // Using autoaccept because the origin is somewhat dynamic 68 | // (but maybe is not anymore) 69 | // FIXME: make this fixed 70 | autoAcceptConnections: false 71 | }); 72 | 73 | function originIsAllowed(origin) { 74 | // Unfortunately the origin will be whatever page you are sharing, which is 75 | // any possible origin. 76 | console.log('got origin', origin); 77 | return true; 78 | } 79 | 80 | var allConnections = {}; 81 | var verifiedClientIds = {}; 82 | 83 | var ID = 0; 84 | 85 | wsServer.on('request', function(request) { 86 | if (!originIsAllowed(request.origin)) { 87 | // Make sure we only accept requests from an allowed origin 88 | request.reject(); 89 | console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.'); 90 | return; 91 | } 92 | 93 | var id = request.httpRequest.url.replace(/^\/hub\/+/, '').replace(/\/.*/, ''); 94 | 95 | // FIXME: we should use a protocol here instead of null, but I can't get it to work 96 | var connection = request.accept(null, request.origin); 97 | connection.ID = ID++; 98 | if (! allConnections[id]) { 99 | allConnections[id] = []; 100 | } 101 | allConnections[id].push(connection); 102 | console.log((new Date()) + ' Connection accepted to ' + JSON.stringify(id) + ' ID:' + connection.ID); 103 | connection.on('message', function(message) { 104 | var parsed = JSON.parse(message.utf8Data); 105 | if (parsed.verifyEmail) { 106 | // FIXME: move this into a function 107 | var result = ''; 108 | console.log('Sending verification'); 109 | var body = "assertion=" + encodeURIComponent(parsed.verifyEmail.assertion) + 110 | "&audience=" + encodeURIComponent(parsed.verifyEmail.audience); 111 | var httpReq = https.request({ 112 | hostname: "verifier.login.persona.org", 113 | path: "/verify", 114 | method: "POST", 115 | headers: { 116 | "Content-Length": body.length, 117 | "Content-Type": "application/x-www-form-urlencoded" 118 | } 119 | }, function (resp) { 120 | resp.setEncoding("utf8"); 121 | resp.on("data", function (chunk) { 122 | result += chunk; 123 | }); 124 | resp.on("end", function () { 125 | console.log('end on', result); 126 | try { 127 | result = JSON.parse(result); 128 | } catch (e) { 129 | result = {status: "error", message: "Internal: " + e}; 130 | } 131 | var verified = {clientVerified: result, clientId: parsed.clientId}; 132 | verified = JSON.stringify(verified); 133 | if (result.status == "okay") { 134 | verifiedClientIds[parsed.verifyEmail.clientId] = result; 135 | for (var i=0; i 2 | -------------------------------------------------------------------------------- /site/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/browsermirror/f26a645a48975382ea8bfcda7cf010f920032f05/site/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /site/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/browsermirror/f26a645a48975382ea8bfcda7cf010f920032f05/site/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /site/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap.js by @fat & @mdo 3 | * plugins: bootstrap-transition.js, bootstrap-modal.js, bootstrap-dropdown.js, bootstrap-scrollspy.js, bootstrap-tab.js, bootstrap-tooltip.js, bootstrap-popover.js, bootstrap-alert.js, bootstrap-button.js, bootstrap-collapse.js, bootstrap-carousel.js, bootstrap-typeahead.js 4 | * Copyright 2012 Twitter, Inc. 5 | * http://www.apache.org/licenses/LICENSE-2.0.txt 6 | */ 7 | !function(a){a(function(){a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",msTransition:"MSTransitionEnd",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){function c(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),d.call(b)},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),d.call(b)})}function d(a){this.$element.hide().trigger("hidden"),e.call(this)}function e(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('