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
38namespace WebCore {
39
40void YouTubePluginReplacement::registerPluginReplacement(PluginReplacementRegistrar registrar)
41{
42    registrar(ReplacementPlugin(create, supportsMimeType, supportsFileExtension, supportsURL));
43}
44
45PassRefPtr<PluginReplacement> YouTubePluginReplacement::create(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues)
46{
47    return adoptRef(new YouTubePluginReplacement(plugin, paramNames, paramValues));
48}
49
50bool YouTubePluginReplacement::supportsMimeType(const String& mimeType)
51{
52    return equalIgnoringCase(mimeType, "application/x-shockwave-flash")
53        || equalIgnoringCase(mimeType, "application/futuresplash");
54}
55
56bool YouTubePluginReplacement::supportsFileExtension(const String& extension)
57{
58    return equalIgnoringCase(extension, "spl") || equalIgnoringCase(extension, "swf");
59}
60
61YouTubePluginReplacement::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
69RenderPtr<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(WTF::move(style));
77}
78
79bool 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
105static 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
117static 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
176static bool hasCaseInsensitivePrefix(const String& input, const String& prefix)
177{
178    return input.startsWith(prefix, false);
179}
180
181static 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
191static 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
200static 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
283String 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
338bool YouTubePluginReplacement::supportsURL(const URL& url)
339{
340    return isYouTubeURL(url);
341}
342
343}
344