summaryrefslogtreecommitdiff
path: root/Source/WebCore/Modules/plugins/YouTubePluginReplacement.cpp
diff options
context:
space:
mode:
authorLorry Tar Creator <lorry-tar-importer@lorry>2017-06-27 06:07:23 +0000
committerLorry Tar Creator <lorry-tar-importer@lorry>2017-06-27 06:07:23 +0000
commit1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch)
tree46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebCore/Modules/plugins/YouTubePluginReplacement.cpp
parent32761a6cee1d0dee366b885b7b9c777e67885688 (diff)
downloadWebKitGtk-tarball-master.tar.gz
Diffstat (limited to 'Source/WebCore/Modules/plugins/YouTubePluginReplacement.cpp')
-rw-r--r--Source/WebCore/Modules/plugins/YouTubePluginReplacement.cpp354
1 files changed, 354 insertions, 0 deletions
diff --git a/Source/WebCore/Modules/plugins/YouTubePluginReplacement.cpp b/Source/WebCore/Modules/plugins/YouTubePluginReplacement.cpp
new file mode 100644
index 000000000..b497adc77
--- /dev/null
+++ b/Source/WebCore/Modules/plugins/YouTubePluginReplacement.cpp
@@ -0,0 +1,354 @@
+/*
+ * 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, AtomicString("100%", AtomicString::ConstructFromLiteral));
+
+ const auto& heightValue = m_attributes.find("height");
+ if (heightValue != m_attributes.end()) {
+ iframeElement->setAttribute(HTMLNames::styleAttr, AtomicString("max-height: 100%", AtomicString::ConstructFromLiteral));
+ iframeElement->setAttributeWithoutSynchronization(HTMLNames::heightAttr, heightValue->value);
+ }
+
+ iframeElement->setAttributeWithoutSynchronization(HTMLNames::srcAttr, youTubeURL(m_attributes.get("src")));
+ iframeElement->setAttributeWithoutSynchronization(HTMLNames::frameborderAttr, AtomicString("0", AtomicString::ConstructFromLiteral));
+
+ // Disable frame flattening for this iframe.
+ iframeElement->setAttributeWithoutSynchronization(HTMLNames::scrollingAttr, AtomicString("no", AtomicString::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 hasCaseInsensitivePrefix(const String& input, const String& prefix)
+{
+ return input.startsWith(prefix, false);
+}
+
+static bool isYouTubeURL(const URL& url)
+{
+ String 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();
+
+ String 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());
+ }
+
+ String 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 = 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 (hasCaseInsensitivePrefix(path, "/v/") || hasCaseInsensitivePrefix(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
+ const String& srcPath = srcURL.path();
+ 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.appendLiteral("/embed/");
+ finalURL.append(videoID);
+ if (!query.isEmpty()) {
+ finalURL.append('?');
+ finalURL.append(query);
+ }
+ return finalURL.toString();
+}
+
+bool YouTubePluginReplacement::supportsURL(const URL& url)
+{
+ return isYouTubeURL(url);
+}
+
+bool YouTubePluginReplacement::isEnabledBySettings(const Settings& settings)
+{
+ return settings.youTubeFlashPluginReplacementEnabled();
+}
+
+}