Protect frames during style and layout changes
[WebKit-https.git] / Source / WebCore / html / ImageDocument.cpp
1 /*
2  * Copyright (C) 2006-2019 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
20  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
23  */
24
25 #include "config.h"
26 #include "ImageDocument.h"
27
28 #include "CachedImage.h"
29 #include "Chrome.h"
30 #include "ChromeClient.h"
31 #include "CustomHeaderFields.h"
32 #include "DOMWindow.h"
33 #include "DocumentLoader.h"
34 #include "EventListener.h"
35 #include "EventNames.h"
36 #include "Frame.h"
37 #include "FrameLoader.h"
38 #include "FrameLoaderClient.h"
39 #include "FrameView.h"
40 #include "HTMLBodyElement.h"
41 #include "HTMLHeadElement.h"
42 #include "HTMLHtmlElement.h"
43 #include "HTMLImageElement.h"
44 #include "HTMLNames.h"
45 #include "LocalizedStrings.h"
46 #include "MIMETypeRegistry.h"
47 #include "MouseEvent.h"
48 #include "Page.h"
49 #include "RawDataDocumentParser.h"
50 #include "RenderElement.h"
51 #include "Settings.h"
52 #include <wtf/IsoMallocInlines.h>
53 #include <wtf/text/StringConcatenateNumbers.h>
54
55 namespace WebCore {
56
57 WTF_MAKE_ISO_ALLOCATED_IMPL(ImageDocument);
58
59 using namespace HTMLNames;
60
61 #if !PLATFORM(IOS_FAMILY)
62 class ImageEventListener final : public EventListener {
63 public:
64     static Ref<ImageEventListener> create(ImageDocument& document) { return adoptRef(*new ImageEventListener(document)); }
65
66 private:
67     ImageEventListener(ImageDocument& document)
68         : EventListener(ImageEventListenerType)
69         , m_document(document)
70     {
71     }
72
73     bool operator==(const EventListener&) const override;
74     void handleEvent(ScriptExecutionContext&, Event&) override;
75
76     ImageDocument& m_document;
77 };
78 #endif
79
80 class ImageDocumentParser final : public RawDataDocumentParser {
81 public:
82     static Ref<ImageDocumentParser> create(ImageDocument& document)
83     {
84         return adoptRef(*new ImageDocumentParser(document));
85     }
86
87 private:
88     ImageDocumentParser(ImageDocument& document)
89         : RawDataDocumentParser(document)
90     {
91     }
92
93     ImageDocument& document() const;
94
95     void appendBytes(DocumentWriter&, const char*, size_t) override;
96     void finish() override;
97 };
98
99 class ImageDocumentElement final : public HTMLImageElement {
100     WTF_MAKE_ISO_ALLOCATED_INLINE(ImageDocumentElement);
101 public:
102     static Ref<ImageDocumentElement> create(ImageDocument&);
103
104 private:
105     ImageDocumentElement(ImageDocument& document)
106         : HTMLImageElement(imgTag, document)
107         , m_imageDocument(&document)
108     {
109     }
110
111     virtual ~ImageDocumentElement();
112     void didMoveToNewDocument(Document& oldDocument, Document& newDocument) override;
113
114     ImageDocument* m_imageDocument;
115 };
116
117 inline Ref<ImageDocumentElement> ImageDocumentElement::create(ImageDocument& document)
118 {
119     return adoptRef(*new ImageDocumentElement(document));
120 }
121
122 // --------
123
124 HTMLImageElement* ImageDocument::imageElement() const
125 {
126     return m_imageElement;
127 }
128
129 LayoutSize ImageDocument::imageSize()
130 {
131     ASSERT(m_imageElement);
132     updateStyleIfNeeded();
133     return m_imageElement->cachedImage()->imageSizeForRenderer(m_imageElement->renderer(), frame() ? frame()->pageZoomFactor() : 1);
134 }
135
136 void ImageDocument::updateDuringParsing()
137 {
138     if (!settings().areImagesEnabled())
139         return;
140
141     if (!m_imageElement)
142         createDocumentStructure();
143
144     if (RefPtr<SharedBuffer> buffer = loader()->mainResourceData())
145         m_imageElement->cachedImage()->updateBuffer(*buffer);
146
147     imageUpdated();
148 }
149
150 void ImageDocument::finishedParsing()
151 {
152     if (!parser()->isStopped() && m_imageElement) {
153         CachedImage& cachedImage = *m_imageElement->cachedImage();
154         RefPtr<SharedBuffer> data = loader()->mainResourceData();
155
156         // If this is a multipart image, make a copy of the current part, since the resource data
157         // will be overwritten by the next part.
158         if (data && loader()->isLoadingMultipartContent())
159             data = data->copy();
160
161         cachedImage.finishLoading(data.get());
162         cachedImage.finish();
163
164         // Report the natural image size in the page title, regardless of zoom level.
165         // At a zoom level of 1 the image is guaranteed to have an integer size.
166         updateStyleIfNeeded();
167         IntSize size = flooredIntSize(cachedImage.imageSizeForRenderer(m_imageElement->renderer(), 1));
168         if (size.width()) {
169             // Compute the title. We use the decoded filename of the resource, falling
170             // back on the hostname if there is no path.
171             String name = decodeURLEscapeSequences(url().lastPathComponent());
172             if (name.isEmpty())
173                 name = url().host().toString();
174             setTitle(imageTitle(name, size));
175         }
176
177         imageUpdated();
178     }
179
180     HTMLDocument::finishedParsing();
181 }
182     
183 inline ImageDocument& ImageDocumentParser::document() const
184 {
185     // Only used during parsing, so document is guaranteed to be non-null.
186     ASSERT(RawDataDocumentParser::document());
187     return downcast<ImageDocument>(*RawDataDocumentParser::document());
188 }
189
190 void ImageDocumentParser::appendBytes(DocumentWriter&, const char*, size_t)
191 {
192     document().updateDuringParsing();
193 }
194
195 void ImageDocumentParser::finish()
196 {
197     document().finishedParsing();
198 }
199
200 ImageDocument::ImageDocument(Frame& frame, const URL& url)
201     : HTMLDocument(&frame, url, ImageDocumentClass)
202     , m_imageElement(nullptr)
203     , m_imageSizeIsKnown(false)
204 #if !PLATFORM(IOS_FAMILY)
205     , m_didShrinkImage(false)
206 #endif
207     , m_shouldShrinkImage(frame.settings().shrinksStandaloneImagesToFit() && frame.isMainFrame())
208 {
209     setCompatibilityMode(DocumentCompatibilityMode::QuirksMode);
210     lockCompatibilityMode();
211 }
212     
213 Ref<DocumentParser> ImageDocument::createParser()
214 {
215     return ImageDocumentParser::create(*this);
216 }
217
218 void ImageDocument::createDocumentStructure()
219 {
220     auto rootElement = HTMLHtmlElement::create(*this);
221     appendChild(rootElement);
222     rootElement->insertedByParser();
223
224     frame()->injectUserScripts(InjectAtDocumentStart);
225
226     // We need a <head> so that the call to setTitle() later on actually has an <head> to append to <title> to.
227     auto head = HTMLHeadElement::create(*this);
228     rootElement->appendChild(head);
229
230     auto body = HTMLBodyElement::create(*this);
231     body->setAttribute(styleAttr, "margin: 0px");
232     if (MIMETypeRegistry::isPDFMIMEType(document().loader()->responseMIMEType()))
233         body->setInlineStyleProperty(CSSPropertyBackgroundColor, "white");
234     rootElement->appendChild(body);
235     
236     auto imageElement = ImageDocumentElement::create(*this);
237     if (m_shouldShrinkImage)
238         imageElement->setAttribute(styleAttr, "-webkit-user-select:none; display:block; margin:auto; padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);");
239     else
240         imageElement->setAttribute(styleAttr, "-webkit-user-select:none; padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);");
241     imageElement->setLoadManually(true);
242     imageElement->setSrc(url().string());
243     imageElement->cachedImage()->setResponse(loader()->response());
244     body->appendChild(imageElement);
245     
246     if (m_shouldShrinkImage) {
247 #if PLATFORM(IOS_FAMILY)
248         // Set the viewport to be in device pixels (rather than the default of 980).
249         processViewport("width=device-width,viewport-fit=cover"_s, ViewportArguments::ImageDocument);
250 #else
251         auto listener = ImageEventListener::create(*this);
252         if (RefPtr<DOMWindow> window = this->domWindow())
253             window->addEventListener("resize", listener.copyRef(), false);
254         imageElement->addEventListener("click", WTFMove(listener), false);
255 #endif
256     }
257
258     m_imageElement = imageElement.ptr();
259 }
260
261 void ImageDocument::imageUpdated()
262 {
263     ASSERT(m_imageElement);
264
265     if (m_imageSizeIsKnown)
266         return;
267
268     LayoutSize imageSize = this->imageSize();
269     if (imageSize.isEmpty())
270         return;
271
272     m_imageSizeIsKnown = true;
273
274     if (m_shouldShrinkImage) {
275 #if PLATFORM(IOS_FAMILY)
276         FloatSize screenSize = page()->chrome().screenSize();
277         if (imageSize.width() > screenSize.width())
278             processViewport(makeString("width=", imageSize.width().toInt(), ",viewport-fit=cover"), ViewportArguments::ImageDocument);
279
280         if (page())
281             page()->chrome().client().imageOrMediaDocumentSizeChanged(IntSize(imageSize.width(), imageSize.height()));
282 #else
283         // Call windowSizeChanged for its side effect of sizing the image.
284         windowSizeChanged();
285 #endif
286     }
287 }
288
289 #if !PLATFORM(IOS_FAMILY)
290 float ImageDocument::scale()
291 {
292     if (!m_imageElement)
293         return 1;
294
295     RefPtr<FrameView> view = this->view();
296     if (!view)
297         return 1;
298
299     LayoutSize imageSize = this->imageSize();
300
301     IntSize viewportSize = view->visibleSize();
302     float widthScale = viewportSize.width() / imageSize.width().toFloat();
303     float heightScale = viewportSize.height() / imageSize.height().toFloat();
304
305     return std::min(widthScale, heightScale);
306 }
307
308 void ImageDocument::resizeImageToFit()
309 {
310     if (!m_imageElement)
311         return;
312
313     LayoutSize imageSize = this->imageSize();
314
315     float scale = this->scale();
316     m_imageElement->setWidth(static_cast<int>(imageSize.width() * scale));
317     m_imageElement->setHeight(static_cast<int>(imageSize.height() * scale));
318
319     m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomIn);
320 }
321
322 void ImageDocument::restoreImageSize()
323 {
324     if (!m_imageElement || !m_imageSizeIsKnown)
325         return;
326
327     LayoutSize imageSize = this->imageSize();
328     m_imageElement->setWidth(imageSize.width().toUnsigned());
329     m_imageElement->setHeight(imageSize.height().toUnsigned());
330
331     if (imageFitsInWindow())
332         m_imageElement->removeInlineStyleProperty(CSSPropertyCursor);
333     else
334         m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomOut);
335
336     m_didShrinkImage = false;
337 }
338
339 bool ImageDocument::imageFitsInWindow()
340 {
341     if (!m_imageElement)
342         return true;
343
344     RefPtr<FrameView> view = this->view();
345     if (!view)
346         return true;
347
348     LayoutSize imageSize = this->imageSize();
349     IntSize viewportSize = view->visibleSize();
350     return imageSize.width() <= viewportSize.width() && imageSize.height() <= viewportSize.height();
351 }
352
353
354 void ImageDocument::windowSizeChanged()
355 {
356     if (!m_imageElement || !m_imageSizeIsKnown)
357         return;
358
359     bool fitsInWindow = imageFitsInWindow();
360
361     // If the image has been explicitly zoomed in, restore the cursor if the image fits
362     // and set it to a zoom out cursor if the image doesn't fit
363     if (!m_shouldShrinkImage) {
364         if (fitsInWindow)
365             m_imageElement->removeInlineStyleProperty(CSSPropertyCursor);
366         else
367             m_imageElement->setInlineStyleProperty(CSSPropertyCursor, CSSValueZoomOut);
368         return;
369     }
370
371     if (m_didShrinkImage) {
372         // If the window has been resized so that the image fits, restore the image size,
373         // otherwise update the restored image size.
374         if (fitsInWindow)
375             restoreImageSize();
376         else
377             resizeImageToFit();
378     } else {
379         // If the image isn't resized but needs to be, then resize it.
380         if (!fitsInWindow) {
381             resizeImageToFit();
382             m_didShrinkImage = true;
383         }
384     }
385 }
386
387 void ImageDocument::imageClicked(int x, int y)
388 {
389     if (!m_imageSizeIsKnown || imageFitsInWindow())
390         return;
391
392     m_shouldShrinkImage = !m_shouldShrinkImage;
393
394     if (m_shouldShrinkImage) {
395         // Call windowSizeChanged for its side effect of sizing the image.
396         windowSizeChanged();
397     } else {
398         restoreImageSize();
399
400         updateLayout();
401
402         if (!view())
403             return;
404
405         float scale = this->scale();
406
407         IntSize viewportSize = view()->visibleSize();
408         int scrollX = static_cast<int>(x / scale - viewportSize.width() / 2.0f);
409         int scrollY = static_cast<int>(y / scale - viewportSize.height() / 2.0f);
410
411         view()->setScrollPosition(IntPoint(scrollX, scrollY));
412     }
413 }
414
415 void ImageEventListener::handleEvent(ScriptExecutionContext&, Event& event)
416 {
417     if (event.type() == eventNames().resizeEvent)
418         m_document.windowSizeChanged();
419     else if (event.type() == eventNames().clickEvent && is<MouseEvent>(event)) {
420         MouseEvent& mouseEvent = downcast<MouseEvent>(event);
421         m_document.imageClicked(mouseEvent.offsetX(), mouseEvent.offsetY());
422     }
423 }
424
425 bool ImageEventListener::operator==(const EventListener& other) const
426 {
427     // All ImageEventListener objects compare as equal; OK since there is only one per document.
428     return other.type() == ImageEventListenerType;
429 }
430 #endif
431
432 // --------
433
434 ImageDocumentElement::~ImageDocumentElement()
435 {
436     if (m_imageDocument)
437         m_imageDocument->disconnectImageElement();
438 }
439
440 void ImageDocumentElement::didMoveToNewDocument(Document& oldDocument, Document& newDocument)
441 {
442     if (m_imageDocument) {
443         m_imageDocument->disconnectImageElement();
444         m_imageDocument = nullptr;
445     }
446     HTMLImageElement::didMoveToNewDocument(oldDocument, newDocument);
447 }
448
449 }