YouTubePluginReplacement.cpp [plain text]
#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));
iframeElement->setAttributeWithoutSynchronization(HTMLNames::scrollingAttr, AtomString("no", AtomString::ConstructFromLiteral));
m_embedShadowElement->appendChild(iframeElement);
return true;
}
static URL createYouTubeURL(StringView videoID, StringView timeID)
{
ASSERT(!videoID.isEmpty());
ASSERT(videoID != "/");
return URL(URL(), makeString("youtube:", videoID, timeID.isEmpty() ? "" : "t=", timeID));
}
static YouTubePluginReplacement::KeyValueMap queryKeysAndValues(StringView 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) {
size_t equalLocation = queryString.find('=', equalSearchLocation);
if (equalLocation == notFound)
break;
size_t indexAfterEqual = equalLocation + 1;
if (indexAfterEqual > queryLength - 1)
break;
size_t keyLocation = equalSearchLocation;
size_t keyLength = equalLocation - equalSearchLocation;
size_t ampersandLocation = queryString.find('&', indexAfterEqual);
size_t valueLocation = indexAfterEqual;
size_t valueLength;
if (ampersandLocation != notFound)
valueLength = ampersandLocation - indexAfterEqual;
else
valueLength = queryLength - indexAfterEqual;
if (keyLength && valueLength) {
String key = queryString.substring(keyLocation, keyLength).convertToASCIILowercase();
String value = queryString.substring(valueLocation, valueLength).toString();
value.replace('+', ' ');
if (!key.isEmpty() && !value.isEmpty())
queryDictionary.add(key, value);
}
if (ampersandLocation == notFound)
break;
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();
if (!isYouTubeURL(url))
return URL();
auto hostName = url.host();
bool isYouTubeMobileWebAppURL = equalLettersIgnoringASCIICase(hostName, "m.youtube.com");
isYouTubeShortenedURL = equalLettersIgnoringASCIICase(hostName, "youtu.be");
if (isYouTubeShortenedURL) {
auto videoID = url.lastPathComponent();
if (videoID.isEmpty() || videoID == "/")
return URL();
return createYouTubeURL(videoID, { });
}
auto path = url.path();
auto query = url.query();
auto fragment = url.fragmentIdentifier();
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);
}
}
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/")) {
StringView videoID;
StringView pathAfterFirstAmpersand;
auto lastPathComponent = url.lastPathComponent();
size_t ampersandLocation = lastPathComponent.find('&');
if (ampersandLocation != notFound) {
videoID = lastPathComponent.substring(0, ampersandLocation);
pathAfterFirstAmpersand = lastPathComponent.substring(ampersandLocation + 1, lastPathComponent.length() - ampersandLocation);
} else
videoID = lastPathComponent;
if (!videoID.isEmpty()) {
outPathAfterFirstAmpersand = pathAfterFirstAmpersand.toString();
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 possiblyMalformedQuery;
URL youTubeURL = processAndCreateYouTubeURL(srcURL, isYouTubeShortenedURL, possiblyMalformedQuery);
if (srcURL.isEmpty() || youTubeURL.isEmpty())
return srcString;
auto 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);
locationOfPathBeforeVideoID = StringView(srcString).find(srcPath.substring(0, locationOfVideoIDInPath));
} else if (equalLettersIgnoringASCIICase(srcPath, "/watch")) {
locationOfPathBeforeVideoID = srcString.find("/watch");
} else
return srcString;
ASSERT(locationOfPathBeforeVideoID != notFound);
auto srcURLPrefix = StringView(srcString).substring(0, locationOfPathBeforeVideoID);
auto query = srcURL.query();
if (query.isEmpty())
query = possiblyMalformedQuery;
return makeString(
isYouTubeShortenedURL ? "http://www.youtube.com" : srcURLPrefix,
"/embed/",
videoID,
query.isEmpty() ? "" : "?",
query
);
}
bool YouTubePluginReplacement::supportsURL(const URL& url)
{
return isYouTubeURL(url);
}
bool YouTubePluginReplacement::isEnabledBySettings(const Settings& settings)
{
return settings.youTubeFlashPluginReplacementEnabled();
}
}