Extending <object> in Dart - javascript

The Dart <object> element does not support a getter to access <object>.contentDocument and thus I thought about extending the object to add the functionality.
I took a look at the implementation of the ObjectElement and I basically need to add these lines:
#DomName('HTMLObjectElement.contentDocument')
#DocsEditable()
Document get contentDocument => _blink.BlinkHTMLObjectElement.instance.contentDocument_Getter_(this);
However, I have no idea how to do this. The solution I am using at this time is with a proxy which redirects all calls to the underlying JsObject but to be honest, this is not just dirty, it impossible to maintain.
/* Updated to explain the root of all evil */
When starting the project I am working on, I wanted to display SVGs, which are uploaded by the user, on the website and let the user manipulate these SVGs by inserting additional SvgElements or removing others.
When downloading the SVGs as a String and displaying them by
container.append(new SvgElement(svgCode))
I got really strange display bugs such that embeded images in the SVGs are displaced or even removed and other bugs with masks.
The problem was solved by using an <object> tag and set setting its data attribute to the SVG's url. The SVGs are rendered correctly. That being said, another issue came up. I wasn't able to access and manipulate the SVGs DOM because it's inside an <object> tag and the tag's document cannot be accessed by using contentDocument.
When taking all this into account, there are pretty much only two options left:
I use the <object> tag with no display bugs but not being able to manipulate the SVGs or
I create new SvgElements fromt the SVG's source and append them to the DOM which let's me manipulate the SVGs but having display bugs.
Since having display bugs isn't really a solution I can only make use of the first option, using an <object> tag and working around with Javascript to access the object's contentDocument.
As you can see, accessing the contentDocument is not always a security issue and not allowing to make use of it, is just a quick and dirty solution of a problem.
When accessing the contentDocument by using a JsObject, I get a JsObject back and not an Element. Thus I do not only have to update my code pretty much everywhere, but it gets also pretty ugly since I have to use the JsObject with callMethod(blabla).

class MyObjectElement extends ObjectElement {
static bool _isRegistered = false;
static register() {
if (!_isRegistered) {
document.registerElement('my-object', MyObjectElement,
extendsTag: 'object');
_isRegistered = true;
}
}
factory MyObjectElement() {
var result = document.createElement('object', 'my-object');
return result;
}
MyObjectElement.created() : super.created();
js.JsObject get contentDocument {
// doesn't seem to work with a custom element.
return new js.JsObject.fromBrowserObject(this)['contentDocument'];
}
}
use it like
MyObjectElement.register();
var obj = new MyObjectElement()
..data =
"https://www.suntico.com/wp-content/uploads/DemoStampRotate01-e1400242575670.png";
document.body.append(obj);

Related

How to assert element property value with Espresso-web?

I'm working on a subclass of default Android WebView with some additional functionality (content-filtering) on the top. So i override shouldInterceptRequest(...) and prevent loading of some resource (let's say images with filename "image.png").
Now i want to test this functionality with the help of Espresso-Web. So i do start embedded http server (with WireMock) and load some webpage that uses that "image.png" and it's expected to be blocked.
How can i assert the state of DOM element with id "someId"?
I expected to have smth like:
onWebView().
.withElement(findElement(Locator.ID, blockImageId)) // finds the DOM node
.check(webMatches(getProperty("naturalWidth"), equalTo("0"))) // "getProperty"?
What i need is smth like "getProperty()" that just generates JS to access the property of node found by findElement(Locator.ID, blockImageId), is there anything out-of-box?
I was able to find getText() but it seems it does it in completely different way (so i can't just request another "node property"):
/** Returns the visible text beneath a given DOM element. */
public static Atom<String> getText() {
return new GetTextTransformingAtom(new GetVisibleTextSimpleAtom(), castOrDie(String.class));
}
I was able to do it in JS way:
// JS-way helpers
const val naturalWidth = "naturalWidth"
const val notBlockedImageWidth = 50
const val blockedImageWidth = 0
fun getPropertyForElementId(elementId: String, property: String): Atom<String> =
Atoms.script(
"""return String(document.getElementById("$elementId").$property);""",
Atoms.castOrDie(String::class.java))
fun imageIsBlocked(elementId: String): WebAssertion<String>? = WebViewAssertions.webMatches(
getPropertyForElementId(elementId, naturalWidth),
equalTo(blockedImageWidth.toString())) {
"""An image with id="$elementId" IS expected to be blocked, but it's NOT."""
}
// part of the test
val redImage = "image.png"
load(
listOf(blockingPathPrefix),
"""
|<html>
|<body>
| <img id="$blockImageId" src="${blockingPathPrefix}$redImage"/>
| <img id="$notBlockImageId" src="$greenImage"/>
|</body>
|</html>
|""".trimMargin()
)
onWebView()
.withTimeout(1, TimeUnit.SECONDS) // it's already loaded
.check(imageIsBlocked(blockImageId)) // red image IS blocked
.check(imageIsNotBlocked(notBlockImageId)) // green image is NOT blocked
I do understand that the way i did it is suboptimal as it joins everything: searching of the node and accessing at once and i wonder what's the right way to do it. Any suggestions, links?
PS. "naturalWidth" is just a property that helps me in this particular case. In common case i just need to access a property of found DOM node and it can be some other property next time (eg "style.display" to check element visibility).
PS. Can anybody explain how to write WebDriverAtomScripts, eg. smth similar to WebDriverAtomScripts.GET_VISIBLE_TEXT_ANDROID?
is there anything out-of-box?
The answer to your question is "no", I don't think there is anything better out-of-the-box than creating an Atom like you are doing (or similar approaches like Atoms.scriptWithArgs or subclassing SimpleAtom).
Your best bet is to file a feature request here (and then maybe propose/contribute an implementation): https://github.com/android/android-test
You can assert on the html document with xpath, but that won't work for computed DOM node attributes like you are looking for.
onWebView()
.check(webContent(hasElementWithXpath("//*[#id=\"myButton\" and #type=\"button\"]")))

SVG onload event listener doesn't seem to fire in Edge [duplicate]

I have a simple SVG loaded inside a object tag like the code below. On Safari, the load event is fired just once, when I load the first time the page after opening the browser. All the other times it doesn't. I'm using the load event to initialize some animations with GSAP, so I need to know when the SVG is fully loaded before being able to select the DOM nodes. A quick workaround that seems to work is by using setTimeout instead of attaching to the event, but it seems a bit akward as slower networks could not have load the object in the specified amount of time. I know this event is not really standardized, but I don't think I'm the first person that faced this problem. How would you solve it?
var myElement = document.getElementById('my-element').getElementsByTagName('object')[0];
myElement.addEventListener('load', function () {
var svgDocument = this.contentDocument;
var myNode = svgDocument.getElementById('my-node');
...
}
It sounds more like the problem is that, when the data is cached, the load event fires before you attached the handler.
What you can try is to reset the data attribute once you attached the event :
object.addEventListener('load', onload_handler);
// reset the data attribte so the load event fires again if it was cached
object.data = object.data;
I also ran into this problem while developing an Electron application. In my workflow I edit index.html and renderer.js in VSCode, and hit <Ctrl>+R to see the changes. I only restart the debugger to capture changes made to the main.js file.
I want to load an SVG that I can then manipulate from my application. Because the SVG is large I prefer to keep it in an external file that gets loaded from disk. To accomplish this, the HTML file index.html contains this declaration:
<object id="svgObj" type="image/svg+xml" data="images/file.svg"></object>
The application logic in renderer.js contains:
let svgDOM // global to access SVG DOM from other functions
const svgObj = document.getElementById('svgObj')
svgObj.onload = function () {
svgDOM = this.contentDocument
mySvgReady(this)
}
The problem is non-obvious because it appears intermittent: When the debugger/application first starts this works fine. But when reloading the application via <Ctrl>+R, the .contentDocument property is null.
After much investigation and hair-pulling, a few long-form notes about this include:
Using svgObj.addEventListener ('load', function() {...}) instead of
svgObj.onload makes no difference. Using addEventListener
is better because attempting to set another handler via 'onload'
will replace the current handler. Contrary to other Node.js
applications, you do not need to removeEventListener when the element
is removed from the DOM. Old versions of IE (pre-11) had problems but
this should now be considered safe (and doesn't apply to Electron anyway).
Usage of this.contentDocument is preferred. There is a nicer-looking
getSVGDocument() method that works, but this appears to be for backwards
compatibility with old Adobe tools, perhaps Flash. The DOM returned is the same.
The SVG DOM appears to be permanently cached once loaded as described by #Kaiido, except that I believe the event never fires. What's more, in Node.js, the SVG DOM remains cached in the same svgDOM variable it was loaded into. I don't understand this at all. My intuition suggests that the require('renderer.js') code in index.html has cached this in the module system somewhere, but changes to renderer.js do take effect so this can't be the whole answer.
Regardless, here is an alternate approach to capturing the SVG DOM in Electron's render process that is working for me:
let svgDOM // global to access from other functions
const svgObj = document.getElementById('svgObj')
svgObj.onload = function () {
if (svgDOM) return mySvgReady(this) // Done: it already loaded, somehow
if (!this.contentDocument) { // Event fired before DOM loaded
const oldDataUri = svgObj.data // Save the original "data" attribute
svgObj.data = '' // Force it to a different "data" value
// setImmediate() is too quick and this handler can get called many
// times as the data value bounces between '' and the actual SVG data.
// 50ms was chosen and seemed to work, and no other values were tested.
setTimeout (x => svgObj.data = oldDataUri, 50)
return;
}
svgDOM = this.contentDocument
mySvgReady(this)
}
Next, I was very disappointed to learn that the CSS rules loaded by index.html can't access the elements within the SVG DOM. There are a number of ways to inject the stylesheet into the SVG DOM programmatically, but I ended up changing my index.html to this format:
<svg id="svgObj" class="svgLoader" src="images/file.svg"></svg>
I then added this code to my DOM setup code in renderer.js to load the SVG directly into the document. If you are using a compressed SVG format I expect you will need to do the decompression yourself.
const fs = require ('fs') // This is Electron/Node. Browsers need XHR, etc.
document.addEventListener('DOMContentLoaded', function() {
...
document.querySelectorAll ('svg.svgLoader').forEach (el => {
const src = el.getAttribute ('src')
if (!src) throw "SVGLoader Element missing src"
const svgSrc = fs.readFileSync (src)
el.innerHTML = svgSrc
})
...
})
I don't necessarily love it, but this is the solution I'm going with because I can now change classes on the SVG object and my CSS rules apply to the elements within the SVG. For example, these rules from index.css can now be used to declaritively alter which parts of the SVG are displayed:
...
#svgObj.cssClassBad #groupBad,
#svgObj.cssClassGood #groupGood {
visibility: visible;
}
...

Load event not fired on Safari when reloading page

I have a simple SVG loaded inside a object tag like the code below. On Safari, the load event is fired just once, when I load the first time the page after opening the browser. All the other times it doesn't. I'm using the load event to initialize some animations with GSAP, so I need to know when the SVG is fully loaded before being able to select the DOM nodes. A quick workaround that seems to work is by using setTimeout instead of attaching to the event, but it seems a bit akward as slower networks could not have load the object in the specified amount of time. I know this event is not really standardized, but I don't think I'm the first person that faced this problem. How would you solve it?
var myElement = document.getElementById('my-element').getElementsByTagName('object')[0];
myElement.addEventListener('load', function () {
var svgDocument = this.contentDocument;
var myNode = svgDocument.getElementById('my-node');
...
}
It sounds more like the problem is that, when the data is cached, the load event fires before you attached the handler.
What you can try is to reset the data attribute once you attached the event :
object.addEventListener('load', onload_handler);
// reset the data attribte so the load event fires again if it was cached
object.data = object.data;
I also ran into this problem while developing an Electron application. In my workflow I edit index.html and renderer.js in VSCode, and hit <Ctrl>+R to see the changes. I only restart the debugger to capture changes made to the main.js file.
I want to load an SVG that I can then manipulate from my application. Because the SVG is large I prefer to keep it in an external file that gets loaded from disk. To accomplish this, the HTML file index.html contains this declaration:
<object id="svgObj" type="image/svg+xml" data="images/file.svg"></object>
The application logic in renderer.js contains:
let svgDOM // global to access SVG DOM from other functions
const svgObj = document.getElementById('svgObj')
svgObj.onload = function () {
svgDOM = this.contentDocument
mySvgReady(this)
}
The problem is non-obvious because it appears intermittent: When the debugger/application first starts this works fine. But when reloading the application via <Ctrl>+R, the .contentDocument property is null.
After much investigation and hair-pulling, a few long-form notes about this include:
Using svgObj.addEventListener ('load', function() {...}) instead of
svgObj.onload makes no difference. Using addEventListener
is better because attempting to set another handler via 'onload'
will replace the current handler. Contrary to other Node.js
applications, you do not need to removeEventListener when the element
is removed from the DOM. Old versions of IE (pre-11) had problems but
this should now be considered safe (and doesn't apply to Electron anyway).
Usage of this.contentDocument is preferred. There is a nicer-looking
getSVGDocument() method that works, but this appears to be for backwards
compatibility with old Adobe tools, perhaps Flash. The DOM returned is the same.
The SVG DOM appears to be permanently cached once loaded as described by #Kaiido, except that I believe the event never fires. What's more, in Node.js, the SVG DOM remains cached in the same svgDOM variable it was loaded into. I don't understand this at all. My intuition suggests that the require('renderer.js') code in index.html has cached this in the module system somewhere, but changes to renderer.js do take effect so this can't be the whole answer.
Regardless, here is an alternate approach to capturing the SVG DOM in Electron's render process that is working for me:
let svgDOM // global to access from other functions
const svgObj = document.getElementById('svgObj')
svgObj.onload = function () {
if (svgDOM) return mySvgReady(this) // Done: it already loaded, somehow
if (!this.contentDocument) { // Event fired before DOM loaded
const oldDataUri = svgObj.data // Save the original "data" attribute
svgObj.data = '' // Force it to a different "data" value
// setImmediate() is too quick and this handler can get called many
// times as the data value bounces between '' and the actual SVG data.
// 50ms was chosen and seemed to work, and no other values were tested.
setTimeout (x => svgObj.data = oldDataUri, 50)
return;
}
svgDOM = this.contentDocument
mySvgReady(this)
}
Next, I was very disappointed to learn that the CSS rules loaded by index.html can't access the elements within the SVG DOM. There are a number of ways to inject the stylesheet into the SVG DOM programmatically, but I ended up changing my index.html to this format:
<svg id="svgObj" class="svgLoader" src="images/file.svg"></svg>
I then added this code to my DOM setup code in renderer.js to load the SVG directly into the document. If you are using a compressed SVG format I expect you will need to do the decompression yourself.
const fs = require ('fs') // This is Electron/Node. Browsers need XHR, etc.
document.addEventListener('DOMContentLoaded', function() {
...
document.querySelectorAll ('svg.svgLoader').forEach (el => {
const src = el.getAttribute ('src')
if (!src) throw "SVGLoader Element missing src"
const svgSrc = fs.readFileSync (src)
el.innerHTML = svgSrc
})
...
})
I don't necessarily love it, but this is the solution I'm going with because I can now change classes on the SVG object and my CSS rules apply to the elements within the SVG. For example, these rules from index.css can now be used to declaritively alter which parts of the SVG are displayed:
...
#svgObj.cssClassBad #groupBad,
#svgObj.cssClassGood #groupGood {
visibility: visible;
}
...

JsApi sometimes gets broken

We're using the JsApin for two way communication between the plugin and the page js.
Sometimes this object just stops working.
Here's what we're doing:
<object type="...">
...
<param name="onload" value="pluginloaded" />
...
</object>
var myObj = {
element: ..., // reference to the dom element for the object tag
...
}
function pluginloaded(jsapi) {
myObj.jsapi = jsapi;
}
As you can see we keep a reference to both the DOM object element and the JsApi object that is being passed to the onload method.
When there's a try to execute a method on the plugin (or a property) it will first try myObj.jsapi.method() and if that failed then 'myObj.element.method()`, and if that fails then that will be reported.
Statistics show that it doesn't happen very frequently, but it does indeed happen, though I have no idea how to reproduce it, just sometimes happens.
Any idea what might cause this object to be unavailable? from the js perspective the jsapi object is undefined, and the object element just doesn't have the method/properties which are exposed from the plugin.
Thanks.
Without spending more time looking at your actual project it's hard to say for sure, but this sounds to me like you're actually unloading and/or reloading your plugin. The most common thing that causes this is when you move it around in the DOM -- for example, if you use javascript to create the object tag, and you set the .type before you put it in the DOM:
var obj = Document.create("object");
obj.type = "application/x-mytype";
someElement.appendChild(obj);
This seems like a good idea, but the browser actually will partially destroy the plugin object when you do this. Similarly, if you set css display: none or overflow: {anything here} on the plugin object or any of its parents it can cause this.
Anyway, however it is happening I bet you are unloading the plugin, thus invalidating the jsapi object you grabbed.

When to use document.implementation.createHTMLDocument?

What are some use cases and is it deprecated? As I found out at http://groups.google.com/group/envjs/browse_thread/thread/6c22d0f959666009/c389fc11537f2a97 that it's "non-standard and not supported by any modern browser".
About document.implementation at http://javascript.gakaa.com/document-implementation.aspx:
Returns a reference to the W3C DOMImplementation object, which
represents, to a limited degree, the environment that makes up the
document containerthe browser, for our purposes. Methods of the object
let you see which DOM modules the browser reports supporting. This
object is also a gateway to creating virtual W3C Document and
DocumentType objects outside of the current document tree. Thus, in
Netscape 6 you can use the document.implementation property as a start
to generating a nonrendered document for external XML documents. See
the DOMImplementation object for details about the methods and their
browser support.
Given that it provides methods (such as createHTMLDocument) for creating a non-rendered document outside of the current document tree, would it be safe to feed it untrusted third party HTML input that may contain some XSS? I ask because I would like to use createHTMLDocument for traversal purposes of third party HTML input. May that be one of the use cases?
I always use this because it doesn't make requests to images, execute scripts or affect styling:
function cleanHTML( html ) {
var root = document.implementation.createHTMLDocument().body;
root.innerHTML = html;
//Manipulate the DOM here
$(root).find("script, style, img").remove(); //jQuery is not relevant, I just didn't want to write exhausting boilerplate code just to make a point
return root.innerHTML;
}
cleanHTML( '<div>hello</div><img src="google"><script>alert("hello");</script><style type="text/css">body {display: none !important;}</style>' );
//returns "<div>hello</div>" with the page unaffected
Yes. You can use this to load untrusted third-party content and strip it of dangerous tags and attributes before including it into your own document. There is some great research incorporating this trick, described at http://blog.kotowicz.net/2011/10/sad-state-of-dom-security-or-how-we-all.html.
The technique documented by Esailija above is insufficient, however. You also need to strip out most attributes. An attacker could set an onerror or onmouseover element to malicious JS. The style attribute can be used to include CSS that runs malicious JS. Iframe and other embed tags can also be abused. View source at https://html5sec.org/xssme/xssme2 to see a version of this technique.
Just a cleaner answer besides #Esailija and #Greg answers:
This function will create another document outside the tree of current document, and clean all scripts, styles and images from the new document:
function insertDocument (myHTML) {
var newHTMLDocument = document.implementation.createHTMLDocument().body;
newHTMLDocument.innerHTML = myHTML;
[].forEach.call(newHTMLDocument.querySelectorAll("script, style, img"), function(el) {el.remove(); });
documentsList.push(newHTMLDocument);
return $(newHTMLDocument.innerHTML);
}
This one is fantastic for making ajax requests and scraping the content will be faster :)

Categories

Resources