html icon indicating copy to clipboard operation
html copied to clipboard

Modernized version of window.open() API

Open domenic opened this issue 3 years ago • 10 comments

window.open() is full of legacy design mistakes. Here is a proposal for what, IMO, it should look like:

window.openWindow(url, { allowOpenerAccess, referrerPolicy });
window.openPopup(url, { left, top, width, height, allowOpenerAccess, referrerPolicy });

In particular such a clean slate would:

  • Not encode left/top/width/height/noopener/noreferrer/popup-ness into strings
  • Not also have a second use where you pass a window name and it navigates that window
  • Allow any referrer policy, not just no-referrer (the latter is possible via today's noreferrer string option)
  • Flip the default so usually you don't get opener access, and you have to opt in to it
  • Not parse the URL relative to the entry settings object (https://github.com/whatwg/html/issues/1431) but instead use the relevant settings object like other modern APIs.
  • Not special case the empty string or about:blank URLs
  • Be extensible to future additional options without adding more terrible string parsing; see e.g. https://github.com/WICG/conversion-measurement-api/issues/130 and https://github.com/w3ctag/design-reviews/issues/691#issuecomment-993155427
  • Avoid the issue window.open() has where it's impossible to add a fourth argument dictionary since there is legacy code that does window.open(url, name, features, "replace") and "replace" throws an error when converting to a dictionary.

domenic avatar Jan 11 '22 19:01 domenic

We could also make it throw an exception if the popup is blocked, instead of returning null.

domenic avatar Jan 11 '22 19:01 domenic

FWIW, we had discussed making Clients.openWindow() (and the rest of clients API) available in documents.

https://w3c.github.io/ServiceWorker/#clients-openwindow

It seemed it might have avoided some of the mistakes of window.open(), although its likely missing features. I don't know if leaning into that API would make sense here.

wanderview avatar Jan 11 '22 20:01 wanderview

Could you please add the ability to pass in header values to the GET request that occurs when a new tab is open in the browser using window.open?

I ran across this restricted behavior when attempting to pass in an "Accept" header to tell the server to return CSV instead of JSON on that first GET request which happens when windows.open(url, '_blank') is called.

It seems that the only way I can get this to happen is by altering the url or the query string to define what mime type I am expecting in the response, which the Accept header is really for. Or by making a POST request to generate the CSV file and then making another GET request to download it in the new tab. Which is 2 requests when it could be done in one if passing in headers to that first new tab GET request was possible.

If the window.open would allow headers to be passed in that would make it so I could do all this in one call to window.open like so:

options.url = '/reports/q12w3e4tynbvcx4567'; options.headers = {'Accept':'text/csv'}; options.mode = '_blank'; window.open(options);

There are many other folks which have a similar interest. https://stackoverflow.com/questions/4325968/window-open-with-headers

Thanks

codecando-x avatar Mar 31 '22 21:03 codecando-x

No, allowing arbitrary headers on navigations is not something the web platform will support, for security reasons.

domenic avatar Mar 31 '22 21:03 domenic

Thanks @domenic

Could you explain what specific security reasons you are concerned about?

"Arbitrary" is an all encompassing word here, since not all headers are created equal. The request is for a HTTP standard header to be accepted not just any custom header.

The "Accept' header https://datatracker.ietf.org/doc/html/rfc7231#page-38

On top of that, web-servers hopefully validate the values that are passed into the Accept header most likely using a list like this: https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types

I am not familiar with a specific browser's codebases but they probably also validate that the value that is passed into the Accept header is on a list of supported mime types.

The Accept header is passed in already by the browser. The request is to be able to choose this value from an already defined list which the browsers and webserver use.

Just trying to understand the security concern here.

codecando-x avatar Mar 31 '22 22:03 codecando-x

While we could probably allow some headers to be set for navigations (essentially in line with what "no-cors" allows), it's not clear that the complexity it warrants in terms of validation and ensuring correctness of implementation is worth the effort. It's probably best to open a separate issue for that specific request if you want to make a case for it.

annevk avatar Apr 04 '22 06:04 annevk

Thanks @annevk

Opened a new issue with more detail: https://github.com/whatwg/html/issues/7810

codecando-x avatar Apr 11 '22 18:04 codecando-x

tip for controlling referrerPolicy, headers, method, body cache and all the rest of the things you could be able to do with a Request object

const request = new Request(url, { method: 'POST', body: new FormData() })

window.openWindow(request, ...rest)
window.openPopup(request, { left, top, width, height, allowOpenerAccess })

jimmywarting avatar May 09 '22 08:05 jimmywarting

@domenic Are there words speaking that window.openWindow and window.openTab are Transient activation-consuming APIs? Because in the current spec I found nothing that express that window.open is Transient activation-consuming API (its behaviour directly says that it is).

dSalieri avatar Jul 14 '22 01:07 dSalieri

@dSalieri that is not related to the new feature proposal in this issue, and is off-topic. If you'd like help reading the spec, I suggest https://stackoverflow.com/ or https://whatwg.org/chat.

domenic avatar Jul 14 '22 01:07 domenic

The current window.open() does indeed have a quite a few odd quirks, so perhaps a new API would be reasonable. But before adding anything it would be good to figure out if similar features should be added also to anchor elements (with target attribute) or form elements (with target attribute). And what other ways there are to open new windows, at least Clients.openWindow() (which is rather weird itself, since it has so limited features).

smaug---- avatar Apr 06 '23 23:04 smaug----

I shared related explorations in 2020. I wonder if returning a promise would help avoid sync access to pre-initialized window properties (e.g screenX|Y, outerWidth|Height rely on async user agent, operating system, and window manager functionality crbug.com/1434097) It might also help avoid accessing the initial about:blank document (e.g. crbug.com/1434136) These loose ideas may be infeasible or increase friction, I'm just brainstorming.

michaelwasserman avatar Apr 18 '23 17:04 michaelwasserman

In the triage calls we've briefly discussed @smaug----'s suggestion of additionally augmenting <a> elements to allow them access to the same feature set. I took the action item to write up what that would look like.

<a> elements already have rel="noopener" and referrerpolicy="". Combined with target="_blank", they have all of the functionality of the OP's proposed openWindow().

What remains is the functionality of the OP's openPopup(): specifically, saying that you want a popup instead of a new window/tab, and saying the left/top/width/height of the popup. If we wanted to add the ability to do that declaratively, I think it'd be something like a popup="" attribute, which has a small microsyntax for specifying the dimensions: e.g. popup="left top width height" or popup="left top widthxheight", with clear rules for parsing that so that if you omit some values you get sensible defaults. (E.g. you should be able to just specify width + height, or maybe just width or just top.)

How this popup="" combines with target="" is a bit tricky. Ideas:

  • It must be used with target="_blank", and is ignored otherwise. (Probably simplest.)
  • It can be used with target="_blank". If the target="" is a preexisting popup window (including the default target of _self), then it moves-and-resizes the window (subject to the same security restrictions as usual on which windows are allowed to move-and-resize other windows).

I think this is fairly separable from the JavaScript API proposal, so I don't think it's a blocker for moving forward with that. But I'm glad we checked to make sure, before forging ahead.


On the question of the return value: we discussed this also a bit in the triage call. There was indeed interest in having it return a promise, but no clarity about when the promise would resolve. Ideas included:

  • As soon as possible, i.e. as soon as the popup's position and size can be determined from the window manager.
  • Something involving the initial navigation away from the "about:blank" document. After the new document's load event is quite late, but maybe after navigation commits, i.e. once location.href has been updated?
    • This feels a bit like a potential cross-origin information leak, but it's not really any more than we have today, since you can just poll w.location for w returned from window.open().

On the connection to Service Worker's openWindow(): that one takes a URL, with no options, and returns a promise for a WindowClient, or null if the resulting window is cross-storage-partition. I personally think that existing API fits well with the newly-proposed ones, especially if we make them promise-returning. They're different, but not in confusing ways:

  • window.openWindow() takes extra options. clients.openWindow() doesn't. Maybe the latter could be expanded to take a referrerPolicy option. (But the allowOpenerAccess option doesn't make sense there.)
  • window.openWindow() returns a promise for a Window. clients.openWindow() returns a promise for a WindowClient. These seem like appropriate in-Window vs. in-ServiceWorkerGlobalScope representations of a window.

So in conclusion, I think this proposal could move forward.

domenic avatar Apr 27 '23 08:04 domenic

This came up in our TAG review of Document Picture-in-Picture, as the reasons for this whole new API (instead of just an alwaysOnTop option for window.open()) were these issues with window.open():

  • Options cannot be feature detected
  • Windows can outlive their opener, which is often not desirable

It may be good to take these into account when designing a window.open() replacement, as these are more broadly useful, and not specific to PiP use cases.

It had come up a while before, when a popup option to distinguish popups vs tabs was proposed. It would be unfortunate if ad hoc features keep getting proposed to work around the issues in window.open(), so I hope we can get stakeholder interest to fix the root problem.

I think we're largely aligned on the goals:

  • Improved ergonomics
  • Feature detection
  • Improved coverage of use cases
  • Improved security by default

Now, on to the specifics:

@domenic

  • Not encode left/top/width/height/noopener/noreferrer/popup-ness into strings

Yes! Dictionary instead of weird encoded strings is a low hanging fruit here.

  • Not also have a second use where you pass a window name and it navigates that window

Makes sense, it's a bit weird, and there are other ways to address these use cases.

  • Allow any referrer policy, not just no-referrer (the latter is possible via today's noreferrer string option)

Agreed.

  • Flip the default so usually you don't get opener access, and you have to opt in to it

I suspect the vast majority of use cases do need opener access, but making it explicit does improve security. I wonder if we want to generalize it into an allow option, so that permissons can be more fine grained in the future (akin to iframe sandbox)

From a quick read that seems reasonable, but could you elaborate on what this would mean for this API?

  • Not special case the empty string or about:blank URLs

Would it instead make sense to make the url optional, part of the settings dictionary? It’s a very common use case to open a blank window and generate content for it via JS.

Yes!

  • Avoid the issue window.open() has where it's impossible to add a fourth argument dictionary since there is legacy code that does window.open(url, name, features, "replace") and "replace" throws an error when converting to a dictionary.

Also low-hanging fruit.

window.openWindow(url, { allowOpenerAccess, referrerPolicy });
window.openPopup(url, { left, top, width, height, allowOpenerAccess, referrerPolicy });

It does seem reasonable to have separate methods for opening a new tab or creating a popup. I wonder if making Window constructible would make sense? Though that would afford less flexibility than a factory method (e.g. can’t return a promise).

We could also make it throw an exception if the popup is blocked, instead of returning null.

Makes sense.


I don't see a plan for supporting feature detection. Simply throwing when these methods are called is not sufficient, as authors need to be able to know what options are supported before opening any windows. Making Window constructible and having a separate method for the actual navigation (either popup or tab) could address this, as it would be the constructor that would throw, and the constructor by itself would not produce any navigation. If the method returns the window object produced (or a promise that resolves to it), it can still be a one liner.

LeaVerou avatar Jun 12 '23 18:06 LeaVerou

just tough it could be nice if you could apply the same sandboxing possibilities that you can do to iframes to control weather or not it should allow javascript, download files allowing same origin (guess this is allowOpenerAccess?)

jimmywarting avatar Jan 05 '24 18:01 jimmywarting