This documentation is supposed to explain the architecture and the implementation detail of the seamless-posting feature of the Privly extension.
Seamless posting is a method that puts the extension's posting form element directly over the form element of the host page. It is 'Seamless' because the user does not need to leave a web app in order to post encrypted content to it.
In seamless-posting, we first create a new Privly link and place it in the host page's form element. While the user types content into the protected form element, the content associated with the link continually updates. If the user clicks the cancel button, the content associated with the Privly link will be destroyed and the link will be removed from the host page's form element.
Seamless-posting implements the following parts:
-
Seamless-posting template (called prototype view) and related JavaScript (called view adapter)
prototype view:
privly-application/templates/seamless.html.template
view adapter:
privly-application/shared/javascripts/viewAdapters/seamless.js
This is the view layer. It will be loaded into an iframe (to ensure safety) and put inside the host page. Users will write secure messages in this iframe that sits directly over the form of the host page.
The default template provides a textarea with a green background color. The view adapter calls the network interfaces to update the link as the user types into the text area.
This is a "prototype view" because it is a base template that can be extended/inherited in the specific Privly Application.
-
Seamless-posting TTLSelect view and view adapter
prototype view:
privly-application/templates/seamless_ttlselect.html.template
view adapter:privly-application/shared/javascripts/viewAdapters/seamless_ttlselect.js
This view layer provides menu-style UI for user to change the seconds_until_burn(TTL) option. This will be loaded into an iframe above the seamless-posting form if the user hovers over the Privly button.
-
Seamless-posting feature for Message and PlainPost application
Privly-Apps implement the seamless-posting and seamless-posting TTLSelect views.
Message App:
view:
privly-application/Message/seamless.html.subtemplate
controller:
privly-application/Message/js/controller/seamless.js
PlainPost App:
view:
privly-application/PlainPost/seamless.html.subtemplate
controller:
privly-application/PlainPost/js/controller/seamless.js
See
privly-application/Message/js/controllers/seamless.js
for samples of hooking an app into the seamless form.See
privly-application/Message/js/messageModel.js
for samples of manipulating Privly links.
To support seamless-posting, the extension includes the following content scripts:
-
Seamless-posting contentscript
Content script:
javascripts/content_scripts/posting.*.js
The content script is split into several files for better readability.
The content scripts:
- Create a Privly button at the top-right corner when user focus on an editable element in the host page (
posting.button.js
). - Provides tooltip (
Clicks to enable Privly posting
) when user hovers on the button (posting.tooltip.js
) - Creates the iframe of privly-application seamless-posting view (
app
) when user clicks the button (posting.app.js
) - Inserts Privly link into the original editable element (
target
) after the view in iframe creates a Privly link (posting.target.js
) - Provides a dropdown menu (
TTLSelect
) when user hovers on the button after enabling seamless-posting (posting.ttlselect.js
) - Destroys the iframe if user clicks the button again (
posting.app.js
)
Notice that most of the features above involve more than one content script file. See implementation section below for details.
- Create a Privly button at the top-right corner when user focus on an editable element in the host page (
-
Seamless-posting background script
Background script:
javascripts/background_scripts/posting_process.js
javascripts/background_scripts/context_menu.js
javascripts/background_scripts/modal_button.js
The background script:
- Creates a bridge for communicating between the content script and the Privly application (for example, which link to insert) (
posting_process.js
) - Pops up login dialog if the user is not logged in (
posting_process.js
) - Creates context menu for the user to select the desired App and enable seamless-posting (
context_menu.js
) - Update the icon of the action_button (
modal_button
) according to the scripting context that can capture keyboard inputs (modal_button.js
)
- Creates a bridge for communicating between the content script and the Privly application (for example, which link to insert) (
ECMAScript 6 Promise is heavily used to arrange the asynchronous callback order.
-
(Privly-Chrome appends the iframe into the host page.)
-
Send message to content script to switch the icon of Privly button to spinner icon (
msgStartLoading
). -
Check connection.
-
If fails: send message to content script to destroy the iframe (
msgAppClosed
) and restore the Privly button icon to lock icon and stops (msgStopLoading
) and pops up login dialog (msgPopupLoginDialog
). -
If succeeds: goto 4.
-
Send message to content script to retrive the original content of the editable element (
target
) (msgGetTargetText
). -
Check whether the original content contains a Privly link.
-
If contains: try to load it (
loadLink
).
1. If loading succeeded and the link is a valid Privly link and the user has edit permission: Use the content of the Privly link as the initial content of the posting form (`initial content = (content of target).replace(the privly link, the content of the privly link)`), Goto 9.
2. Else: Goto 6.
-
If not contains: Goto 6.
-
Create a new and empty Privly link (
createLink
) and use it as the main link. -
Send message to content script to insert the link into the editable element (
target
) (msgInsertLink
). -
Initialize using a new link completes. Goto 10.
-
Initialize using an existing link completes. Goto 10.
-
Set up targetContentMonitor (
beginContentClearObserver
), monitoring whether the content of the target contains our main link, once cleared, send message to content script to destroy this iframe (msgAppClosed
). -
Send message to content script switch the icon of Privly button from spinner icon to original icon (
msgStopLoading
). -
Send message to content script to notify the completion of initial process (
msgAppStarted
).
(Privly-Chrome tries to destroy the iframe)
-
(Privly-Chrome send message to the iframe that it is going to be destroyed)
-
Destroy the main link (
deleteLink
). -
Send message to content script to notify the closing (
msgAppClosed
). -
(Privly-Chrome removes iframe from DOM tree)
-
Call
privlyNetworkService.sameOriginPostRequest
to create a link. -
Call Application Model to post-process the link (
application.postprocessLink
). -
Emit
afterCreateLink
event for Application Models. -
Use the link as the main link.
-
Cancel last ongoing update request.
-
Call Application Model to transform textarea content into structured content (
application.getRequestContent
). -
Call
privlyNetworkService.sameOriginPutRequest
to update the main link according to the structured content. -
Emit
afterUpdateLink
event for Application Models.
-
Send message to content script to switch the icon of Privly button to spinner icon (
msgStartLoading
). -
Call
privlyNetworkService.sameOriginDeleteRequest
to delete the main link -
Emit
afterDeleteLink
event for Application Models. -
Send message to content script to switch the icon of Privly button to original icon (
msgStopLoading
).
-
Update link (
updateLink
) -
If the pressed key is
enter
: send message to content script to simulate theenter
key on the editable element (target
) (msgEmitEnterEvent
)
Sending message to content script is achieved by sending message to background script and background script forwarding message to the content script.
-
Get TTL options from Application Model (
application.getTTLOptions
) -
Calculate width and height
-
Send message to content script to notify the completion of loading, containing the width and height
-
(Privly-Chrome resize the iframe and calculates the position of the iframe, based on the width and height. The iframe may be below the button, or above the button)
-
(Privly-Chrome send message to iframe to notify whether it is below the button or above the button)
-
Generate menu DOM according to position: smaller options are always closer to the mouse-pointer.
-
(Privly-Chrome fade in the iframe)
- Send message to content script to notify that user has clicked an option (
msgTTLChange
)
Sending message to content script is achieved by sending a message to the background script and background script forwarding message to the content script.
We have to manage state for editable elements on the page (for example, whether it is in seamless-posting mode). We also need to manage state for programmatically-created elements (for example, a Privly button may be spinner icon, or lock button, or cancel button, based on state). Besides, we also need to manage some global objects (for example, a Privly button may contain a timer to postpone the hiding process). In addition, those stuff should be treated together: when editable element is removed, our state data should be cleared, our Privly button DOM related to that editable element should be removed and our Privly button timer should be canceled.
Thus we created the Resource
class (implemented in posting.resource.js
), to provide a container for those components (ResourceItem
, implemented in posting.resource.js
), for a specific editable element.
Features:
-
Different editable elements are linked to different
Resource
s. -
A
Resource
can hold many differentResourceItem
s. -
A Privly button DOM is managed by a
ResourceItem
(implemented inposting.button.js
), an editable element DOM itself is managed by aResourceItem
(implemented inposting.target.js
), a Privly button tooltip DOM is managed by aResourceItem
(implemented inposting.tooltip.js
), a Privly seamless-posting Application iframe DOM is managed by aResourceItem
(implemented inposting.app.js
), etc. -
There is a global pool of
Resource
s (managing allResource
instance). -
ResourceItem
has adestroy
method, which is a kind of destructor. -
Different
ResourceItem
can have different destructor behaviors, for example, for a Privly button Resource Item, when it is destroyed, the DOM should be removed from DOM tree. However for a Target Resource Item, when it is destroyed, the DOM (the editable element itself) should not be removed from DOM tree. Notice that, Privly button Resource Item also cancels its timers insidedestroy
. -
Some
ResourceItem
may not contain DOM nodes, for example,posting.controller.js
implements aResourceItem
which only control otherResourceItem
s. -
ResourceItem
s inside the sameResource
can communicate with others by callingbroadcastInternal
(It is the observer design pattern). -
Each
Resource
has a unique id, theoretically (even across all tabs). -
When a Privly Application want to communicate with a specific
Resource
, it need to provide the id of theResource
. -
The id of the
Resource
is passed by URI querystring to the privly-application when iframe is created, thus the application can send message back later. -
When
Resource
receives a message, it will forward it to itsResourceItem
s. -
A message received by
Resource
can be external -- Chrome message from background script, or internal -- by callingbroadcastInternal()
. -
Messages sent from
broadcastInternal
do not supportsendResponse
. -
A
ResourceItem
can subscribe different kind of message (differentiate byaction
property) by callingaddMessageListener
. -
We appoint that, messages of which
action
start withposting/internal/
are internal messages (which means, when you are usingbroadcastInternal
, theaction
property of your messages should beposting/internal/
prefixed). -
The background script will forward Chrome messages of which
action
start withposting/app
to all Privly applications (each Privly applications will filter messages). -
The background script will forward Chrome messages of which
action
start withposting/contentScript
to all content scripts (eachResource
will filter messages as mentioned above). -
The background script itself will try to handle Chrome messages of which
action
start withposting/background
.
A Resource
has a state
property that indicates whether it is in the seamless-posting mode.
state == OPEN
: In seamless-posting mode (posting form is open).
state == CLOSE
: Not in seamless-posting mode (posting form is closed).
Button has three kind of icons, lock
, spinner
and cancel
.
Button also has two properties indicates its state:
loading
: whether the button should show a spinner icon.
state
: the same to Resource State.
We separated the two properties thus each ResourceItem
can set one of them without caring about affecting real states.
-
For
loading == true
: internal state isLOADING
, The button will showspinner
icon. -
For
loading == false
andstate == CLOSE
: internal state isCLOSE
, the button will showlock
button. -
For
loading == false
andstate == OPEN
: internal state isOPEN
, the button will showcancel
button.
INTERNAL_STATE_PROPERTY
defines behaviors of the button in each internal state:
var INTERNAL_STATE_PROPERTY = {
CLOSE: {
autohide: true, // whether the button should hide after seconds
clickable: true, // whether the button is clickable
tooltip: true, // whether to show the tooltip when hovering on the button
icon: SVG_OPEN // the SVG of the icon when button is in this state
},
OPEN: {
autohide: false,
clickable: true,
tooltip: false,
icon: SVG_CLOSE
},
LOADING: {
autohide: false,
clickable: false,
tooltip: false,
icon: SVG_LOADING
}
};
Each iframe in each tab contains a unique contextid
, which is generated in context_messenger.js
, to filter contexts for messages (notice that every Chrome message is broadcasted to all tabs, so we filter the message at the context layer first).
Each Resource
contains a unique resid
, which is used to tell Privly applications that where to send Chrome message back later. It is generated by Resource
class when constructing. Each Resource
filter messages according to the resourceid
property in the message body to ensure that the message is indeed send to this Resource
and then broadcast them to its ResourceItem
s.
Each Seamless-posting App contains a appid
, which is used for Privly applications to filter messages sent from content script. It is generated by ResourceItem that creates the iframe (posting.app.js
or posting.ttlselect.js
). Again, it is used to ensure that such message is indeed send to this Privly application.
The process of showing the Privly button:
-
posting.service.js
addsclick
,focus
,blur
event listener to the document of the host page. -
(User clicks an editable element)
-
posting.service.js
detects whether it is an editable element and calculates the correct target element -
If there are no
Resource
containing the target element, create one (createResource@posting.service.js
): -
Create ControllerResourceItem implemented in
posting.controller.js
-
Create TargetResourceItem implemented in
posting.target.js
-
Create ButtonResourceItem implemented in
posting.button.js
-
Create TooltipResourceItem implemented in
posting.tooltip.js
-
Create TTLSelectResourceItem implemented in
posting.ttlselect.js
-
Create a
Resource
containingResourceItem
s above and add it to the globalResource
pool. -
Send internal message to all
ResourceItem
of theResource
that the target element is activated (posting/internal/targetActivated
)
// Nothing other than add message listeners
- Store the target node in the
ResourceItem
-
Create the DOM node for the button
-
Add event listeners
-
Set the button icon to lock (
updateInternalState
)
- Create DOM for the tooltip (see
posting.floating.js
for underlying implementation)
- Create DOM for the TTLSelect (see
posting.floating.js
for underlying implementation)
-
posting.target.js
starts the resize monitor (updateResizeMonitor
) to detect whether the position or size of the target element has changed (detectResize
). -
If position or size changed, send internal message to all
ResourceItem
of theResource
:posting/internal/targetPositionChanged
posting.target.js
stops the resize monitor (updateResizeMonitor
) if thisResource
is not inOPEN
state.
-
Update the position of the button (
updatePosition
) according to the position of the target -
Show the button
-
Set a timer to postpone hiding
-
Cancel the postpone timer.
-
Immediately hide the button if it should be hidden according to internal state and
INTERNAL_STATE_PROPERTY
.
- Updates the position of the button
The process of starting seamless-posting or stoping seamless-posting:
-
Stop if the button is not in a clickable state
-
Send internal message to all
ResourceItem
of theResource
:posting/internal/buttonClicked
-
If the
Resource
is inCLOSE
state (seamless-posting is not enabled and user clicks the Privly button thus the user is going to enable seamless-posting): CreateAppResourceItem
implemented inposting.app.js
-
If the
Resource
is inOPEN
state: Send internal message to allResourceItem
of theResource
:posting/internal/closeRequested
(user has requested to close seamless-posting form)
-
Generate a unique app id
-
Creates an iframe, which
src
is likeprivly-applications/Message/seamless.html?contextid=xxx&resid=xxx&appid=xxx
. -
Append to DOM tree
- Send message to the app:
posting/app/userClose
The process of hovering on the Privly button:
- Send internal message to all
ResourceItem
of theResource
:posting/internal/buttonMouseEntered
-
Call
tooltip.show
if theResource
is inCLOSE
state -
Call
ttltooltip.show
if theResource
is inOPEN
state
There are many other event or message handling process, please check out the code to see others. Event or message handlers above are those typical ones, which I think is enough to give you a comprehensive understanding of the content script architecture.