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