| /* |
| * Copyright (C) 2014 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "YouTubePluginReplacement.h" |
| |
| #include "HTMLIFrameElement.h" |
| #include "HTMLNames.h" |
| #include "HTMLParserIdioms.h" |
| #include "HTMLPlugInElement.h" |
| #include "RenderElement.h" |
| #include "Settings.h" |
| #include "ShadowRoot.h" |
| #include "YouTubeEmbedShadowElement.h" |
| #include <wtf/text/StringBuilder.h> |
| |
| namespace WebCore { |
| |
| void YouTubePluginReplacement::registerPluginReplacement(PluginReplacementRegistrar registrar) |
| { |
| registrar(ReplacementPlugin(create, supportsMimeType, supportsFileExtension, supportsURL, isEnabledBySettings)); |
| } |
| |
| Ref<PluginReplacement> YouTubePluginReplacement::create(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues) |
| { |
| return adoptRef(*new YouTubePluginReplacement(plugin, paramNames, paramValues)); |
| } |
| |
| bool YouTubePluginReplacement::supportsMimeType(const String& mimeType) |
| { |
| return equalLettersIgnoringASCIICase(mimeType, "application/x-shockwave-flash") |
| || equalLettersIgnoringASCIICase(mimeType, "application/futuresplash"); |
| } |
| |
| bool YouTubePluginReplacement::supportsFileExtension(const String& extension) |
| { |
| return equalLettersIgnoringASCIICase(extension, "spl") || equalLettersIgnoringASCIICase(extension, "swf"); |
| } |
| |
| YouTubePluginReplacement::YouTubePluginReplacement(HTMLPlugInElement& plugin, const Vector<String>& paramNames, const Vector<String>& paramValues) |
| : m_parentElement(&plugin) |
| { |
| ASSERT(paramNames.size() == paramValues.size()); |
| for (size_t i = 0; i < paramNames.size(); ++i) |
| m_attributes.add(paramNames[i], paramValues[i]); |
| } |
| |
| RenderPtr<RenderElement> YouTubePluginReplacement::createElementRenderer(HTMLPlugInElement& plugin, RenderStyle&& style, const RenderTreePosition& insertionPosition) |
| { |
| ASSERT_UNUSED(plugin, m_parentElement == &plugin); |
| |
| if (!m_embedShadowElement) |
| return nullptr; |
| |
| return m_embedShadowElement->createElementRenderer(WTFMove(style), insertionPosition); |
| } |
| |
| bool YouTubePluginReplacement::installReplacement(ShadowRoot& root) |
| { |
| m_embedShadowElement = YouTubeEmbedShadowElement::create(m_parentElement->document()); |
| |
| root.appendChild(*m_embedShadowElement); |
| |
| auto iframeElement = HTMLIFrameElement::create(HTMLNames::iframeTag, m_parentElement->document()); |
| if (m_attributes.contains("width")) |
| iframeElement->setAttributeWithoutSynchronization(HTMLNames::widthAttr, AtomString("100%", AtomString::ConstructFromLiteral)); |
| |
| const auto& heightValue = m_attributes.find("height"); |
| if (heightValue != m_attributes.end()) { |
| iframeElement->setAttribute(HTMLNames::styleAttr, AtomString("max-height: 100%", AtomString::ConstructFromLiteral)); |
| iframeElement->setAttributeWithoutSynchronization(HTMLNames::heightAttr, heightValue->value); |
| } |
| |
| iframeElement->setAttributeWithoutSynchronization(HTMLNames::srcAttr, youTubeURL(m_attributes.get("src"))); |
| iframeElement->setAttributeWithoutSynchronization(HTMLNames::frameborderAttr, AtomString("0", AtomString::ConstructFromLiteral)); |
| |
| // Disable frame flattening for this iframe. |
| iframeElement->setAttributeWithoutSynchronization(HTMLNames::scrollingAttr, AtomString("no", AtomString::ConstructFromLiteral)); |
| m_embedShadowElement->appendChild(iframeElement); |
| |
| return true; |
| } |
| |
| static inline URL createYouTubeURL(const String& videoID, const String& timeID) |
| { |
| ASSERT(!videoID.isEmpty()); |
| ASSERT(videoID != "/"); |
| |
| URL result(URL(), "youtube:" + videoID); |
| if (!timeID.isEmpty()) |
| result.setQuery("t=" + timeID); |
| |
| return result; |
| } |
| |
| static YouTubePluginReplacement::KeyValueMap queryKeysAndValues(const String& queryString) |
| { |
| YouTubePluginReplacement::KeyValueMap queryDictionary; |
| |
| size_t queryLength = queryString.length(); |
| if (!queryLength) |
| return queryDictionary; |
| |
| size_t equalSearchLocation = 0; |
| size_t equalSearchLength = queryLength; |
| |
| while (equalSearchLocation < queryLength - 1 && equalSearchLength) { |
| |
| // Search for "=". |
| size_t equalLocation = queryString.find('=', equalSearchLocation); |
| if (equalLocation == notFound) |
| break; |
| |
| size_t indexAfterEqual = equalLocation + 1; |
| if (indexAfterEqual > queryLength - 1) |
| break; |
| |
| // Get the key before the "=". |
| size_t keyLocation = equalSearchLocation; |
| size_t keyLength = equalLocation - equalSearchLocation; |
| |
| // Seach for the ampersand. |
| size_t ampersandLocation = queryString.find('&', indexAfterEqual); |
| |
| // Get the value after the "=", before the ampersand. |
| size_t valueLocation = indexAfterEqual; |
| size_t valueLength; |
| if (ampersandLocation != notFound) |
| valueLength = ampersandLocation - indexAfterEqual; |
| else |
| valueLength = queryLength - indexAfterEqual; |
| |
| // Save the key and the value. |
| if (keyLength && valueLength) { |
| String key = queryString.substring(keyLocation, keyLength).convertToASCIILowercase(); |
| String value = queryString.substring(valueLocation, valueLength); |
| value.replace('+', ' '); |
| |
| if (!key.isEmpty() && !value.isEmpty()) |
| queryDictionary.add(key, value); |
| } |
| |
| if (ampersandLocation == notFound) |
| break; |
| |
| // Continue searching after the ampersand. |
| size_t indexAfterAmpersand = ampersandLocation + 1; |
| equalSearchLocation = indexAfterAmpersand; |
| equalSearchLength = queryLength - indexAfterAmpersand; |
| } |
| |
| return queryDictionary; |
| } |
| |
| static bool isYouTubeURL(const URL& url) |
| { |
| auto hostName = url.host(); |
| return equalLettersIgnoringASCIICase(hostName, "m.youtube.com") |
| || equalLettersIgnoringASCIICase(hostName, "youtu.be") |
| || equalLettersIgnoringASCIICase(hostName, "www.youtube.com") |
| || equalLettersIgnoringASCIICase(hostName, "youtube.com") |
| || equalLettersIgnoringASCIICase(hostName, "www.youtube-nocookie.com") |
| || equalLettersIgnoringASCIICase(hostName, "youtube-nocookie.com"); |
| } |
| |
| static const String& valueForKey(const YouTubePluginReplacement::KeyValueMap& dictionary, const String& key) |
| { |
| const auto& value = dictionary.find(key); |
| if (value == dictionary.end()) |
| return emptyString(); |
| |
| return value->value; |
| } |
| |
| static URL processAndCreateYouTubeURL(const URL& url, bool& isYouTubeShortenedURL, String& outPathAfterFirstAmpersand) |
| { |
| if (!url.protocolIsInHTTPFamily()) |
| return URL(); |
| |
| // Bail out early if we aren't even on www.youtube.com or youtube.com. |
| if (!isYouTubeURL(url)) |
| return URL(); |
| |
| auto hostName = url.host(); |
| bool isYouTubeMobileWebAppURL = equalLettersIgnoringASCIICase(hostName, "m.youtube.com"); |
| isYouTubeShortenedURL = equalLettersIgnoringASCIICase(hostName, "youtu.be"); |
| |
| // Short URL of the form: http://youtu.be/v1d301D |
| if (isYouTubeShortenedURL) { |
| String videoID = url.lastPathComponent(); |
| if (videoID.isEmpty() || videoID == "/") |
| return URL(); |
| return createYouTubeURL(videoID, emptyString()); |
| } |
| |
| auto path = url.path(); |
| String query = url.query(); |
| String fragment = url.fragmentIdentifier(); |
| |
| // On the YouTube mobile web app, the path and query string are put into the |
| // fragment so that one web page is only ever loaded (see <rdar://problem/9550639>). |
| if (isYouTubeMobileWebAppURL) { |
| size_t location = fragment.find('?'); |
| if (location == notFound) { |
| path = fragment; |
| query = emptyString(); |
| } else { |
| path = StringView(fragment).substring(0, location); |
| query = fragment.substring(location + 1); |
| } |
| fragment = emptyString(); |
| } |
| |
| if (equalLettersIgnoringASCIICase(path, "/watch")) { |
| if (!query.isEmpty()) { |
| const auto& queryDictionary = queryKeysAndValues(query); |
| String videoID = valueForKey(queryDictionary, "v"); |
| |
| if (!videoID.isEmpty()) { |
| const auto& fragmentDictionary = queryKeysAndValues(url.fragmentIdentifier()); |
| String timeID = valueForKey(fragmentDictionary, "t"); |
| return createYouTubeURL(videoID, timeID); |
| } |
| } |
| |
| // May be a new-style link (see <rdar://problem/7733692>). |
| if (fragment.startsWith('!')) { |
| query = fragment.substring(1); |
| |
| if (!query.isEmpty()) { |
| const auto& queryDictionary = queryKeysAndValues(query); |
| String videoID = valueForKey(queryDictionary, "v"); |
| |
| if (!videoID.isEmpty()) { |
| String timeID = valueForKey(queryDictionary, "t"); |
| return createYouTubeURL(videoID, timeID); |
| } |
| } |
| } |
| } else if (startsWithLettersIgnoringASCIICase(path, "/v/") || startsWithLettersIgnoringASCIICase(path, "/e/")) { |
| String lastPathComponent = url.lastPathComponent(); |
| String videoID; |
| String pathAfterFirstAmpersand; |
| |
| size_t ampersandLocation = lastPathComponent.find('&'); |
| if (ampersandLocation != notFound) { |
| // Some URLs we care about use & in place of ? for the first query parameter. |
| videoID = lastPathComponent.substring(0, ampersandLocation); |
| pathAfterFirstAmpersand = lastPathComponent.substring(ampersandLocation + 1, lastPathComponent.length() - ampersandLocation); |
| } else |
| videoID = lastPathComponent; |
| |
| if (!videoID.isEmpty()) { |
| outPathAfterFirstAmpersand = pathAfterFirstAmpersand; |
| return createYouTubeURL(videoID, emptyString()); |
| } |
| } |
| |
| return URL(); |
| } |
| |
| String YouTubePluginReplacement::youTubeURL(const String& srcString) |
| { |
| URL srcURL = m_parentElement->document().completeURL(stripLeadingAndTrailingHTMLSpaces(srcString)); |
| return youTubeURLFromAbsoluteURL(srcURL, srcString); |
| } |
| |
| String YouTubePluginReplacement::youTubeURLFromAbsoluteURL(const URL& srcURL, const String& srcString) |
| { |
| bool isYouTubeShortenedURL = false; |
| String possibleMalformedQuery; |
| URL youTubeURL = processAndCreateYouTubeURL(srcURL, isYouTubeShortenedURL, possibleMalformedQuery); |
| if (srcURL.isEmpty() || youTubeURL.isEmpty()) |
| return srcString; |
| |
| // Transform the youtubeURL (youtube:VideoID) to iframe embed url which has the format: http://www.youtube.com/embed/VideoID |
| auto srcPath = srcURL.path().toString(); |
| const String& videoID = youTubeURL.string().substring(youTubeURL.protocol().length() + 1); |
| size_t locationOfVideoIDInPath = srcPath.find(videoID); |
| |
| size_t locationOfPathBeforeVideoID = notFound; |
| if (locationOfVideoIDInPath != notFound) { |
| ASSERT(locationOfVideoIDInPath); |
| |
| // From the original URL, we need to get the part before /path/VideoId. |
| locationOfPathBeforeVideoID = srcString.find(srcPath.substring(0, locationOfVideoIDInPath)); |
| } else if (equalLettersIgnoringASCIICase(srcPath, "/watch")) { |
| // From the original URL, we need to get the part before /watch/#!v=VideoID |
| // FIXME: Shouldn't this be ASCII case-insensitive? |
| locationOfPathBeforeVideoID = srcString.find("/watch"); |
| } else |
| return srcString; |
| |
| ASSERT(locationOfPathBeforeVideoID != notFound); |
| |
| const String& srcURLPrefix = srcString.substring(0, locationOfPathBeforeVideoID); |
| String query = srcURL.query(); |
| // If the URL has no query, use the possibly malformed query we found. |
| if (query.isEmpty()) |
| query = possibleMalformedQuery; |
| |
| // Append the query string if it is valid. |
| StringBuilder finalURL; |
| if (isYouTubeShortenedURL) |
| finalURL.appendLiteral("http://www.youtube.com"); |
| else |
| finalURL.append(srcURLPrefix); |
| finalURL.append("/embed/", videoID); |
| if (!query.isEmpty()) |
| finalURL.append('?', query); |
| return finalURL.toString(); |
| } |
| |
| bool YouTubePluginReplacement::supportsURL(const URL& url) |
| { |
| return isYouTubeURL(url); |
| } |
| |
| bool YouTubePluginReplacement::isEnabledBySettings(const Settings& settings) |
| { |
| return settings.youTubeFlashPluginReplacementEnabled(); |
| } |
| |
| } |