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