Replace PassRef with Ref/Ref&& across the board.
[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)
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));
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 }
191
192 static const String& valueForKey(const YouTubePluginReplacement::KeyValueMap& dictionary, const String& key)
193 {
194     const auto& value = dictionary.find(key);
195     if (value == dictionary.end())
196         return emptyString();
197
198     return value->value;
199 }
200
201 static URL processAndCreateYouTubeURL(const URL& url, bool& isYouTubeShortenedURL)
202 {
203     if (!url.protocolIs("http") && !url.protocolIs("https"))
204         return URL();
205     
206     // Bail out early if we aren't even on www.youtube.com or youtube.com.
207     if (!isYouTubeURL(url))
208         return URL();
209     
210     const String& hostName = url.host().lower();
211     
212     bool isYouTubeMobileWebAppURL = hostName == "m.youtube.com";
213     isYouTubeShortenedURL = hostName == "youtu.be";
214     
215     // Short URL of the form: http://youtu.be/v1d301D
216     if (isYouTubeShortenedURL) {
217         const String& videoID = url.lastPathComponent();
218         if (videoID.isEmpty() || videoID == "/")
219             return URL();
220         
221         return createYouTubeURL(videoID, emptyString());
222     }
223     
224     String path = url.path();
225     String query = url.query();
226     String fragment = url.fragmentIdentifier();
227     
228     // On the YouTube mobile web app, the path and query string are put into the
229     // fragment so that one web page is only ever loaded (see <rdar://problem/9550639>).
230     if (isYouTubeMobileWebAppURL) {
231         size_t location = fragment.find('?');
232         if (location == notFound) {
233             path = fragment;
234             query = emptyString();
235         } else {
236             path = fragment.substring(0, location);
237             query = fragment.substring(location + 1);
238         }
239         fragment = emptyString();
240     }
241     
242     if (path.lower() == "/watch") {
243         if (!query.isEmpty()) {
244             const auto& queryDictionary = queryKeysAndValues(query);
245             String videoID = valueForKey(queryDictionary, "v");
246             
247             if (!videoID.isEmpty()) {
248                 const auto& fragmentDictionary = queryKeysAndValues(url.fragmentIdentifier());
249                 String timeID = valueForKey(fragmentDictionary, "t");
250                 return createYouTubeURL(videoID, timeID);
251             }
252         }
253         
254         // May be a new-style link (see <rdar://problem/7733692>).
255         if (fragment.startsWith('!')) {
256             query = fragment.substring(1);
257             
258             if (!query.isEmpty()) {
259                 const auto& queryDictionary = queryKeysAndValues(query);
260                 String videoID = valueForKey(queryDictionary, "v");
261                 
262                 if (!videoID.isEmpty()) {
263                     String timeID = valueForKey(queryDictionary, "t");
264                     return createYouTubeURL(videoID, timeID);
265                 }
266             }
267         }
268     } else if (hasCaseInsensitivePrefix(path, "/v/") || hasCaseInsensitivePrefix(path, "/e/")) {
269         String videoID = url.lastPathComponent();
270         
271         // These URLs are funny - they don't have a ? for the first query parameter.
272         // Strip all characters after and including '&' to remove extraneous parameters after the video ID.
273         size_t ampersand = videoID.find('&');
274         if (ampersand != notFound)
275             videoID = videoID.substring(0, ampersand);
276         
277         if (!videoID.isEmpty())
278             return createYouTubeURL(videoID, emptyString());
279     }
280     
281     return URL();
282 }
283
284 String YouTubePluginReplacement::youTubeURL(const String& srcString)
285 {
286     URL srcURL = m_parentElement->document().completeURL(stripLeadingAndTrailingHTMLSpaces(srcString));
287
288     bool isYouTubeShortenedURL = false;
289     URL youTubeURL = processAndCreateYouTubeURL(srcURL, isYouTubeShortenedURL);
290     if (srcURL.isEmpty() || youTubeURL.isEmpty())
291         return srcString;
292
293     // Transform the youtubeURL (youtube:VideoID) to iframe embed url which has the format: http://www.youtube.com/embed/VideoID
294     const String& srcPath = srcURL.path();
295     const String& videoID = youTubeURL.string().substring(youTubeURL.protocol().length() + 1);
296     size_t locationOfVideoIDInPath = srcPath.find(videoID);
297
298     size_t locationOfPathBeforeVideoID = notFound;
299     if (locationOfVideoIDInPath != notFound) {
300         ASSERT(locationOfVideoIDInPath);
301     
302         // From the original URL, we need to get the part before /path/VideoId.
303         locationOfPathBeforeVideoID = srcString.find(srcPath.substring(0, locationOfVideoIDInPath));
304     } else if (srcPath.lower() == "/watch") {
305         // From the original URL, we need to get the part before /watch/#!v=VideoID
306         locationOfPathBeforeVideoID = srcString.find("/watch");
307     } else
308         return srcString;
309
310     ASSERT(locationOfPathBeforeVideoID != notFound);
311
312     const String& srcURLPrefix = srcString.substring(0, locationOfPathBeforeVideoID);
313     String query = srcURL.query();
314
315     // By default, the iframe will display information like the video title and uploader on top of the video. Don't display
316     // them if the embeding html doesn't specify it.
317     if (!query.isEmpty() && !query.contains("showinfo"))
318         query.append("&showinfo=0");
319     else
320         query = "showinfo=0";
321     
322     // Append the query string if it is valid. Some sites apparently forget to add "?" for the query string, in that case,
323     // we will discard the parameters in the url.
324     // See: <rdar://problem/11535155>
325     StringBuilder finalURL;
326     if (isYouTubeShortenedURL)
327         finalURL.append("http://www.youtube.com");
328     else
329         finalURL.append(srcURLPrefix);
330     finalURL.appendLiteral("/embed/");
331     finalURL.append(videoID);
332     if (!query.isEmpty()) {
333         finalURL.appendLiteral("?");
334         finalURL.append(query);
335     }
336     return finalURL.toString();
337 }
338     
339 bool YouTubePluginReplacement::supportsURL(const URL& url)
340 {
341     return isYouTubeURL(url);
342 }
343     
344 }