8f96c9f028c728fda03bb50a7ee96bafc3da905b
[WebKit-https.git] / Source / WebCore / Modules / plugins / YouTubePluginReplacement.cpp
1 /*
2  * Copyright (C) 2014 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  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #include "config.h"
27 #include "YouTubePluginReplacement.h"
28
29 #include "HTMLIFrameElement.h"
30 #include "HTMLNames.h"
31 #include "HTMLParserIdioms.h"
32 #include "HTMLPlugInElement.h"
33 #include "Page.h"
34 #include "RenderElement.h"
35 #include "ShadowRoot.h"
36 #include "YouTubeEmbedShadowElement.h"
37 #include <wtf/text/StringBuilder.h>
38
39 namespace WebCore {
40
41 void YouTubePluginReplacement::registerPluginReplacement(PluginReplacementRegistrar registrar)
42 {
43     registrar(ReplacementPlugin(create, supportsMimeType, supportsFileExtension, supportsURL));
44 }
45
46 PassRefPtr<PluginReplacement> YouTubePluginReplacement::create(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues)
47 {
48     return adoptRef(new YouTubePluginReplacement(plugin, paramNames, paramValues));
49 }
50
51 bool YouTubePluginReplacement::supportsMimeType(const String& mimeType)
52 {
53     return equalIgnoringCase(mimeType, "application/x-shockwave-flash")
54         || equalIgnoringCase(mimeType, "application/futuresplash");
55 }
56
57 bool YouTubePluginReplacement::supportsFileExtension(const String& extension)
58 {
59     return equalIgnoringCase(extension, "spl") || equalIgnoringCase(extension, "swf");
60 }
61
62 YouTubePluginReplacement::YouTubePluginReplacement(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues)
63     : m_parentElement(&plugin)
64 {
65     ASSERT(paramNames.size() == paramValues.size());
66     for (size_t i = 0; i < paramNames.size(); ++i)
67         m_attributes.add(paramNames[i], paramValues[i]);
68 }
69
70 RenderPtr<RenderElement> YouTubePluginReplacement::createElementRenderer(HTMLPlugInElement& plugin, Ref<RenderStyle>&& style, const RenderTreePosition& insertionPosition)
71 {
72     ASSERT_UNUSED(plugin, m_parentElement == &plugin);
73
74     if (!m_embedShadowElement)
75         return nullptr;
76     
77     return m_embedShadowElement->createElementRenderer(WTF::move(style), insertionPosition);
78 }
79
80 bool YouTubePluginReplacement::installReplacement(ShadowRoot* root)
81 {
82     m_embedShadowElement = YouTubeEmbedShadowElement::create(m_parentElement->document());
83
84     root->appendChild(m_embedShadowElement.get());
85
86     RefPtr<HTMLIFrameElement> iframeElement = HTMLIFrameElement::create(HTMLNames::iframeTag, m_parentElement->document());
87     if (m_attributes.contains("width"))
88         iframeElement->setAttribute(HTMLNames::widthAttr, AtomicString("100%", AtomicString::ConstructFromLiteral));
89     
90     const auto& heightValue = m_attributes.find("height");
91     if (heightValue != m_attributes.end()) {
92         iframeElement->setAttribute(HTMLNames::styleAttr, AtomicString("max-height: 100%", AtomicString::ConstructFromLiteral));
93         iframeElement->setAttribute(HTMLNames::heightAttr, heightValue->value);
94     }
95
96     iframeElement->setAttribute(HTMLNames::srcAttr, youTubeURL(m_attributes.get("src")));
97     iframeElement->setAttribute(HTMLNames::frameborderAttr, AtomicString("0", AtomicString::ConstructFromLiteral));
98     
99     // Disable frame flattening for this iframe.
100     iframeElement->setAttribute(HTMLNames::scrollingAttr, AtomicString("no", AtomicString::ConstructFromLiteral));
101     m_embedShadowElement->appendChild(iframeElement);
102
103     return true;
104 }
105     
106 static inline URL createYouTubeURL(const String& videoID, const String& timeID)
107 {
108     ASSERT(!videoID.isEmpty());
109     ASSERT(videoID != "/");
110     
111     URL result(URL(), "youtube:" + videoID);
112     if (!timeID.isEmpty())
113         result.setQuery("t=" + timeID);
114     
115     return result;
116 }
117     
118 static YouTubePluginReplacement::KeyValueMap queryKeysAndValues(const String& queryString)
119 {
120     YouTubePluginReplacement::KeyValueMap queryDictionary;
121     
122     size_t queryLength = queryString.length();
123     if (!queryLength)
124         return queryDictionary;
125     
126     size_t equalSearchLocation = 0;
127     size_t equalSearchLength = queryLength;
128     
129     while (equalSearchLocation < queryLength - 1 && equalSearchLength) {
130         
131         // Search for "=".
132         size_t equalLocation = queryString.find('=', equalSearchLocation);
133         if (equalLocation == notFound)
134             break;
135         
136         size_t indexAfterEqual = equalLocation + 1;
137         if (indexAfterEqual > queryLength - 1)
138             break;
139         
140         // Get the key before the "=".
141         size_t keyLocation = equalSearchLocation;
142         size_t keyLength = equalLocation - equalSearchLocation;
143         
144         // Seach for the ampersand.
145         size_t ampersandLocation = queryString.find('&', indexAfterEqual);
146         
147         // Get the value after the "=", before the ampersand.
148         size_t valueLocation = indexAfterEqual;
149         size_t valueLength;
150         if (ampersandLocation != notFound)
151             valueLength = ampersandLocation - indexAfterEqual;
152         else
153             valueLength = queryLength - indexAfterEqual;
154         
155         // Save the key and the value.
156         if (keyLength && valueLength) {
157             const String& key = queryString.substring(keyLocation, keyLength).lower();
158             String value = queryString.substring(valueLocation, valueLength);
159             value.replace('+', ' ');
160
161             if (!key.isEmpty() && !value.isEmpty())
162                 queryDictionary.add(key, value);
163         }
164         
165         if (ampersandLocation == notFound)
166             break;
167         
168         // Continue searching after the ampersand.
169         size_t indexAfterAmpersand = ampersandLocation + 1;
170         equalSearchLocation = indexAfterAmpersand;
171         equalSearchLength = queryLength - indexAfterAmpersand;
172     }
173     
174     return queryDictionary;
175 }
176     
177 static bool hasCaseInsensitivePrefix(const String& input, const String& prefix)
178 {
179     return input.startsWith(prefix, false);
180 }
181     
182 static bool isYouTubeURL(const URL& url)
183 {
184     const String& hostName = url.host().lower();
185     
186     return hostName == "m.youtube.com"
187         || hostName == "youtu.be"
188         || hostName == "www.youtube.com"
189         || hostName == "youtube.com"
190         || hostName == "www.youtube-nocookie.com"
191         || hostName == "youtube-nocookie.com";
192 }
193
194 static const String& valueForKey(const YouTubePluginReplacement::KeyValueMap& dictionary, const String& key)
195 {
196     const auto& value = dictionary.find(key);
197     if (value == dictionary.end())
198         return emptyString();
199
200     return value->value;
201 }
202
203 static URL processAndCreateYouTubeURL(const URL& url, bool& isYouTubeShortenedURL)
204 {
205     if (!url.protocolIs("http") && !url.protocolIs("https"))
206         return URL();
207     
208     // Bail out early if we aren't even on www.youtube.com or youtube.com.
209     if (!isYouTubeURL(url))
210         return URL();
211     
212     const String& hostName = url.host().lower();
213     
214     bool isYouTubeMobileWebAppURL = hostName == "m.youtube.com";
215     isYouTubeShortenedURL = hostName == "youtu.be";
216     
217     // Short URL of the form: http://youtu.be/v1d301D
218     if (isYouTubeShortenedURL) {
219         const String& videoID = url.lastPathComponent();
220         if (videoID.isEmpty() || videoID == "/")
221             return URL();
222         
223         return createYouTubeURL(videoID, emptyString());
224     }
225     
226     String path = url.path();
227     String query = url.query();
228     String fragment = url.fragmentIdentifier();
229     
230     // On the YouTube mobile web app, the path and query string are put into the
231     // fragment so that one web page is only ever loaded (see <rdar://problem/9550639>).
232     if (isYouTubeMobileWebAppURL) {
233         size_t location = fragment.find('?');
234         if (location == notFound) {
235             path = fragment;
236             query = emptyString();
237         } else {
238             path = fragment.substring(0, location);
239             query = fragment.substring(location + 1);
240         }
241         fragment = emptyString();
242     }
243     
244     if (path.lower() == "/watch") {
245         if (!query.isEmpty()) {
246             const auto& queryDictionary = queryKeysAndValues(query);
247             String videoID = valueForKey(queryDictionary, "v");
248             
249             if (!videoID.isEmpty()) {
250                 const auto& fragmentDictionary = queryKeysAndValues(url.fragmentIdentifier());
251                 String timeID = valueForKey(fragmentDictionary, "t");
252                 return createYouTubeURL(videoID, timeID);
253             }
254         }
255         
256         // May be a new-style link (see <rdar://problem/7733692>).
257         if (fragment.startsWith('!')) {
258             query = fragment.substring(1);
259             
260             if (!query.isEmpty()) {
261                 const auto& queryDictionary = queryKeysAndValues(query);
262                 String videoID = valueForKey(queryDictionary, "v");
263                 
264                 if (!videoID.isEmpty()) {
265                     String timeID = valueForKey(queryDictionary, "t");
266                     return createYouTubeURL(videoID, timeID);
267                 }
268             }
269         }
270     } else if (hasCaseInsensitivePrefix(path, "/v/") || hasCaseInsensitivePrefix(path, "/e/")) {
271         String videoID = url.lastPathComponent();
272         
273         // These URLs are funny - they don't have a ? for the first query parameter.
274         // Strip all characters after and including '&' to remove extraneous parameters after the video ID.
275         size_t ampersand = videoID.find('&');
276         if (ampersand != notFound)
277             videoID = videoID.substring(0, ampersand);
278         
279         if (!videoID.isEmpty())
280             return createYouTubeURL(videoID, emptyString());
281     }
282     
283     return URL();
284 }
285
286 String YouTubePluginReplacement::youTubeURL(const String& srcString)
287 {
288     URL srcURL = m_parentElement->document().completeURL(stripLeadingAndTrailingHTMLSpaces(srcString));
289
290     bool isYouTubeShortenedURL = false;
291     URL youTubeURL = processAndCreateYouTubeURL(srcURL, isYouTubeShortenedURL);
292     if (srcURL.isEmpty() || youTubeURL.isEmpty())
293         return srcString;
294
295     // Transform the youtubeURL (youtube:VideoID) to iframe embed url which has the format: http://www.youtube.com/embed/VideoID
296     const String& srcPath = srcURL.path();
297     const String& videoID = youTubeURL.string().substring(youTubeURL.protocol().length() + 1);
298     size_t locationOfVideoIDInPath = srcPath.find(videoID);
299
300     size_t locationOfPathBeforeVideoID = notFound;
301     if (locationOfVideoIDInPath != notFound) {
302         ASSERT(locationOfVideoIDInPath);
303     
304         // From the original URL, we need to get the part before /path/VideoId.
305         locationOfPathBeforeVideoID = srcString.find(srcPath.substring(0, locationOfVideoIDInPath));
306     } else if (srcPath.lower() == "/watch") {
307         // From the original URL, we need to get the part before /watch/#!v=VideoID
308         locationOfPathBeforeVideoID = srcString.find("/watch");
309     } else
310         return srcString;
311
312     ASSERT(locationOfPathBeforeVideoID != notFound);
313
314     const String& srcURLPrefix = srcString.substring(0, locationOfPathBeforeVideoID);
315     String query = srcURL.query();
316
317     // By default, the iframe will display information like the video title and uploader on top of the video. Don't display
318     // them if the embeding html doesn't specify it.
319     if (!query.isEmpty() && !query.contains("showinfo"))
320         query.append("&showinfo=0");
321     else
322         query = "showinfo=0";
323     
324     // Append the query string if it is valid. Some sites apparently forget to add "?" for the query string, in that case,
325     // we will discard the parameters in the url.
326     // See: <rdar://problem/11535155>
327     StringBuilder finalURL;
328     if (isYouTubeShortenedURL)
329         finalURL.appendLiteral("http://www.youtube.com");
330     else
331         finalURL.append(srcURLPrefix);
332     finalURL.appendLiteral("/embed/");
333     finalURL.append(videoID);
334     if (!query.isEmpty()) {
335         finalURL.append('?');
336         finalURL.append(query);
337     }
338     return finalURL.toString();
339 }
340     
341 bool YouTubePluginReplacement::supportsURL(const URL& url)
342 {
343     return isYouTubeURL(url);
344 }
345     
346 }