Add data abstraction and validation for Ad Click Attribution
authorwilander@apple.com <wilander@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 29 Jan 2019 19:54:32 +0000 (19:54 +0000)
committerwilander@apple.com <wilander@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 29 Jan 2019 19:54:32 +0000 (19:54 +0000)
https://bugs.webkit.org/show_bug.cgi?id=193916
<rdar://problem/47603481>

Reviewed by Daniel Bates, Brent Fulgham, and Alex Christensen.

Source/WebCore:

New API tests added.

Ad click attribution has two steps. First, the storage of an ad
campaign ID for a click that takes the user to a destination
site. Second, a conversion on the destination site that can be
attributed to the ad click.

This patch adds a class that represents a request for ad click
attribution. Validation makes sure that the bits of entropy
reported through this mechanism is limited.

This feature is experimental and off by default.

* Sources.txt:
    Added loader/AdClickAttribution.cpp.
* WebCore.xcodeproj/project.pbxproj:
* loader/AdClickAttribution.cpp: Added.
(WebCore::AdClickAttribution::isValid const):
(WebCore::AdClickAttribution::setConversion):
(WebCore::AdClickAttribution::url const):
(WebCore::AdClickAttribution::referrer const):
* loader/AdClickAttribution.h: Added.
(WebCore::AdClickAttribution::Campaign::Campaign):
(WebCore::AdClickAttribution::Campaign::isValid const):
(WebCore::AdClickAttribution::Source::Source):
(WebCore::AdClickAttribution::Destination::Destination):
(WebCore::AdClickAttribution::Priority::Priority):
(WebCore::AdClickAttribution::Conversion::Conversion):
(WebCore::AdClickAttribution::Conversion::isValid const):
(WebCore::AdClickAttribution::AdClickAttribution):
(WebCore::AdClickAttribution::earliestTimeToSend const):
* loader/DocumentLoader.cpp:
    Added missing #include "RuntimeEnabledFeatures.h".

Tools:

Ad click attribution has two steps. First, the storage of an ad
campaign ID for a click that takes the user to a destination
site. Second, a conversion on the destination site that can be
attributed to the ad click.

This patch adds a class that represents a request for ad click
attribution. Validation makes sure that the bits of entropy
reported through this mechanism is limited.

* TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
* TestWebKitAPI/Tests/WebCore/AdClickAttribution.cpp: Added.
(TestWebKitAPI::TEST):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@240667 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebCore/ChangeLog
Source/WebCore/Sources.txt
Source/WebCore/WebCore.xcodeproj/project.pbxproj
Source/WebCore/loader/AdClickAttribution.cpp [new file with mode: 0644]
Source/WebCore/loader/AdClickAttribution.h [new file with mode: 0644]
Source/WebCore/loader/DocumentLoader.cpp
Tools/ChangeLog
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
Tools/TestWebKitAPI/Tests/WebCore/AdClickAttribution.cpp [new file with mode: 0644]

index 07eafdd..8680f06 100644 (file)
@@ -1,3 +1,45 @@
+2019-01-29  John Wilander  <wilander@apple.com>
+
+        Add data abstraction and validation for Ad Click Attribution
+        https://bugs.webkit.org/show_bug.cgi?id=193916
+        <rdar://problem/47603481>
+
+        Reviewed by Daniel Bates, Brent Fulgham, and Alex Christensen.
+
+        New API tests added.
+
+        Ad click attribution has two steps. First, the storage of an ad
+        campaign ID for a click that takes the user to a destination
+        site. Second, a conversion on the destination site that can be
+        attributed to the ad click.
+
+        This patch adds a class that represents a request for ad click
+        attribution. Validation makes sure that the bits of entropy
+        reported through this mechanism is limited.
+
+        This feature is experimental and off by default.
+
+        * Sources.txt:
+            Added loader/AdClickAttribution.cpp.
+        * WebCore.xcodeproj/project.pbxproj:
+        * loader/AdClickAttribution.cpp: Added.
+        (WebCore::AdClickAttribution::isValid const):
+        (WebCore::AdClickAttribution::setConversion):
+        (WebCore::AdClickAttribution::url const):
+        (WebCore::AdClickAttribution::referrer const):
+        * loader/AdClickAttribution.h: Added.
+        (WebCore::AdClickAttribution::Campaign::Campaign):
+        (WebCore::AdClickAttribution::Campaign::isValid const):
+        (WebCore::AdClickAttribution::Source::Source):
+        (WebCore::AdClickAttribution::Destination::Destination):
+        (WebCore::AdClickAttribution::Priority::Priority):
+        (WebCore::AdClickAttribution::Conversion::Conversion):
+        (WebCore::AdClickAttribution::Conversion::isValid const):
+        (WebCore::AdClickAttribution::AdClickAttribution):
+        (WebCore::AdClickAttribution::earliestTimeToSend const):
+        * loader/DocumentLoader.cpp:
+            Added missing #include "RuntimeEnabledFeatures.h".
+
 2019-01-29  Zalan Bujtas  <zalan@apple.com>
 
         [MathML] Move enum class ScriptType to MathMLScriptsElement.
index fd0568f..e073841 100644 (file)
@@ -1354,6 +1354,7 @@ layout/layouttree/LayoutLineBreakBox.cpp
 layout/layouttree/LayoutReplaced.cpp
 layout/layouttree/LayoutTreeBuilder.cpp
 
+loader/AdClickAttribution.cpp
 loader/CanvasActivityRecord.cpp
 loader/ContentFilter.cpp
 loader/CookieJar.cpp
index cd7294d..d9eac25 100644 (file)
                69A6CBAD1C6BE42C00B836E9 /* AccessibilitySVGElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 697101081C6BE1550018C7F1 /* AccessibilitySVGElement.h */; };
                6A22E8701F10418600F546C3 /* InspectorCanvas.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A22E86F1F10418600F546C3 /* InspectorCanvas.h */; };
                6A72798B1F16C29C003F39B8 /* InspectorShaderProgram.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A7279881F16C29B003F39B8 /* InspectorShaderProgram.h */; };
+               6B0A07F221FA4B5C00D57391 /* AdClickAttribution.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B0A07F021FA4B5C00D57391 /* AdClickAttribution.h */; settings = {ATTRIBUTES = (Private, ); }; };
                6B3480940EEF50D400AC1B41 /* NativeImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B3480920EEF50D400AC1B41 /* NativeImage.h */; settings = {ATTRIBUTES = (Private, ); }; };
                6B693A2E1C51A82E00B03BEF /* ResourceLoadObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B693A2D1C51A82E00B03BEF /* ResourceLoadObserver.h */; settings = {ATTRIBUTES = (Private, ); }; };
                6C4C96DF1AD4483500363F64 /* JSReadableByteStreamController.h in Headers */ = {isa = PBXBuildFile; fileRef = 6C4C96DB1AD4483500363F64 /* JSReadableByteStreamController.h */; };
                6A22E8721F1042C400F546C3 /* InspectorCanvas.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = InspectorCanvas.cpp; sourceTree = "<group>"; };
                6A7279881F16C29B003F39B8 /* InspectorShaderProgram.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InspectorShaderProgram.h; sourceTree = "<group>"; };
                6A7279891F16C29B003F39B8 /* InspectorShaderProgram.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = InspectorShaderProgram.cpp; sourceTree = "<group>"; };
+               6B0A07F021FA4B5C00D57391 /* AdClickAttribution.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AdClickAttribution.h; sourceTree = "<group>"; };
+               6B0A07F121FA4B5C00D57391 /* AdClickAttribution.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AdClickAttribution.cpp; sourceTree = "<group>"; };
                6B3480920EEF50D400AC1B41 /* NativeImage.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = NativeImage.h; sourceTree = "<group>"; };
                6B693A2D1C51A82E00B03BEF /* ResourceLoadObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResourceLoadObserver.h; sourceTree = "<group>"; };
                6B693A331C51A95D00B03BEF /* ResourceLoadObserver.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ResourceLoadObserver.cpp; sourceTree = "<group>"; };
                                5126E6B60A2E3AEF005C29FA /* icon */,
                                A15E31F01E0CB075004B371C /* ios */,
                                93A1EAA20A5634D8006960A0 /* mac */,
+                               6B0A07F121FA4B5C00D57391 /* AdClickAttribution.cpp */,
+                               6B0A07F021FA4B5C00D57391 /* AdClickAttribution.h */,
                                63152D181F9531EE007A5E4B /* ApplicationManifestLoader.cpp */,
                                63152D171F9531EE007A5E4B /* ApplicationManifestLoader.h */,
                                EFB7287B2124C73D005C2558 /* CanvasActivityRecord.cpp */,
                                E1C4DE690EA75C1E0023CCD6 /* ActiveDOMObject.h in Headers */,
                                724EE5501DC80D7F00A91FFB /* ActivityState.h in Headers */,
                                724EE5511DC80D8400A91FFB /* ActivityStateChangeObserver.h in Headers */,
+                               6B0A07F221FA4B5C00D57391 /* AdClickAttribution.h in Headers */,
                                A1677E0E213E02A000A08C34 /* AddressErrors.h in Headers */,
                                BCF7E491137CD7C7001DDAE7 /* AdjustViewSizeOrNot.h in Headers */,
                                84D0C4061115F1EA0018AA34 /* AffineTransform.h in Headers */,
                        buildActionMask = 2147483647;
                        files = (
                                A9787CB41F5F5C6600C551C6 /* AccessibilityMediaObject.cpp in Sources */,
+                               6B0A07F321FA4B5C00D57391 /* AdClickAttribution.cpp in Sources */,
                                31A795C81888BCB500382F90 /* ANGLEInstancedArrays.cpp in Sources */,
                                490707E61219C04300D90E51 /* ANGLEWebKitBridge.cpp in Sources */,
                                CD0EEE0E14743F39003EAFA2 /* AudioDestinationIOS.cpp in Sources */,
diff --git a/Source/WebCore/loader/AdClickAttribution.cpp b/Source/WebCore/loader/AdClickAttribution.cpp
new file mode 100644 (file)
index 0000000..bdc0639
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2019 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. AND ITS CONTRIBUTORS ``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 ITS 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 "AdClickAttribution.h"
+
+#include <wtf/RandomNumber.h>
+#include <wtf/URL.h>
+#include <wtf/text/StringBuilder.h>
+
+namespace WebCore {
+
+bool AdClickAttribution::isValid() const
+{
+    return m_conversion
+        && m_conversion.value().isValid()
+        && m_campaign.isValid()
+        && !m_source.registrableDomain.isEmpty()
+        && !m_destination.registrableDomain.isEmpty()
+        && m_earliestTimeToSend;
+}
+
+void AdClickAttribution::setConversion(Conversion&& conversion)
+{
+    if (!conversion.isValid() || (m_conversion && m_conversion->priority > conversion.priority))
+        return;
+
+    m_conversion = WTFMove(conversion);
+    // 24-48 hour delay before sending. This helps privacy since the conversion and the attribution
+    // requests are detached and the time of the attribution does not reveal the time of the conversion.
+    m_earliestTimeToSend = m_timeOfAdClick + 24_h + Seconds(randomNumber() * (24_h).value());
+}
+
+URL AdClickAttribution::url() const
+{
+    if (!isValid())
+        return URL();
+
+    StringBuilder builder;
+    builder.appendLiteral("https://");
+    builder.append(m_source.registrableDomain);
+    builder.appendLiteral("/.well-known/ad-click-attribution/");
+    builder.appendNumber(m_conversion.value().data);
+    builder.append('/');
+    builder.appendNumber(m_campaign.id);
+
+    URL url { URL(), builder.toString() };
+    if (url.isValid())
+        return url;
+
+    return URL();
+}
+
+URL AdClickAttribution::referrer() const
+{
+    if (!isValid())
+        return URL();
+
+    StringBuilder builder;
+    builder.appendLiteral("https://");
+    builder.append(m_destination.registrableDomain);
+    builder.append('/');
+
+    URL url { URL(), builder.toString() };
+    if (url.isValid())
+        return url;
+    
+    return URL();
+}
+
+}
diff --git a/Source/WebCore/loader/AdClickAttribution.h b/Source/WebCore/loader/AdClickAttribution.h
new file mode 100644 (file)
index 0000000..7df05ee
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2019 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. AND ITS CONTRIBUTORS ``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 ITS 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.
+ */
+
+#pragma once
+
+#include "PublicSuffix.h"
+#include <wtf/Noncopyable.h>
+#include <wtf/Optional.h>
+#include <wtf/WallTime.h>
+#include <wtf/text/WTFString.h>
+
+namespace WTF {
+class URL;
+}
+
+namespace WebCore {
+
+constexpr unsigned short maxEntropy = 64;
+
+class AdClickAttribution {
+    WTF_MAKE_NONCOPYABLE(AdClickAttribution);
+public:
+    using CampaignId = unsigned short;
+    using ConversionData = unsigned short;
+    using PriorityValue = unsigned short;
+
+    struct Campaign {
+        explicit Campaign(CampaignId id)
+            : id { id }
+        {
+        }
+        
+        bool isValid() const
+        {
+            return id < maxEntropy;
+        }
+        
+        CampaignId id;
+    };
+
+    struct Source {
+        explicit Source(const String& host)
+#if ENABLE(PUBLIC_SUFFIX_LIST)
+            : registrableDomain { WebCore::topPrivatelyControlledDomain(host) }
+#else
+            : registrableDomain { emptyString() }
+#endif
+        {
+        }
+
+        String registrableDomain;
+    };
+
+    struct Destination {
+        explicit Destination(const String& host)
+#if ENABLE(PUBLIC_SUFFIX_LIST)
+            : registrableDomain { WebCore::topPrivatelyControlledDomain(host) }
+#else
+            : registrableDomain { emptyString() }
+#endif
+        {
+        }
+
+        String registrableDomain;
+    };
+
+    struct Priority {
+        explicit Priority(PriorityValue value)
+        : value { value }
+        {
+        }
+        
+        PriorityValue value;
+    };
+    
+    struct Conversion {
+        explicit Conversion(ConversionData data, Priority priority)
+            : data { data }
+            , priority { priority.value }
+        {
+        }
+
+        bool isValid() const
+        {
+            return data < maxEntropy && priority < maxEntropy;
+        }
+        
+        ConversionData data;
+        PriorityValue priority;
+    };
+
+    AdClickAttribution(Campaign campaign, const Source& source, const Destination& destination)
+        : m_campaign { campaign }
+        , m_source { source }
+        , m_destination { destination }
+        , m_timeOfAdClick { WallTime::now() }
+    {
+    }
+
+    WEBCORE_EXPORT void setConversion(Conversion&&);
+    WEBCORE_EXPORT URL url() const;
+    WEBCORE_EXPORT URL referrer() const;
+    Optional<WallTime> earliestTimeToSend() const { return m_earliestTimeToSend; };
+
+private:
+    bool isValid() const;
+
+    Campaign m_campaign;
+    Source m_source;
+    Destination m_destination;
+    WallTime m_timeOfAdClick;
+
+    Optional<Conversion> m_conversion;
+    Optional<WallTime> m_earliestTimeToSend;
+};
+    
+} // namespace WebCore
index 6ac2131..135d29e 100644 (file)
@@ -71,6 +71,7 @@
 #include "ProgressTracker.h"
 #include "ResourceHandle.h"
 #include "ResourceLoadObserver.h"
+#include "RuntimeEnabledFeatures.h"
 #include "SWClientConnection.h"
 #include "SchemeRegistry.h"
 #include "ScriptableDocumentParser.h"
index a9a8c05..804864b 100644 (file)
@@ -1,3 +1,24 @@
+2019-01-29  John Wilander  <wilander@apple.com>
+
+        Add data abstraction and validation for Ad Click Attribution
+        https://bugs.webkit.org/show_bug.cgi?id=193916
+        <rdar://problem/47603481>
+
+        Reviewed by Daniel Bates, Brent Fulgham, and Alex Christensen.
+
+        Ad click attribution has two steps. First, the storage of an ad
+        campaign ID for a click that takes the user to a destination
+        site. Second, a conversion on the destination site that can be
+        attributed to the ad click.
+
+        This patch adds a class that represents a request for ad click
+        attribution. Validation makes sure that the bits of entropy
+        reported through this mechanism is limited.
+
+        * TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj:
+        * TestWebKitAPI/Tests/WebCore/AdClickAttribution.cpp: Added.
+        (TestWebKitAPI::TEST):
+
 2019-01-29  Chris Dumez  <cdumez@apple.com>
 
         REGRESSION (PSON): Twitter link gets stuck at t.co after navigating back in tab
index d4a28c1..98b0744 100644 (file)
                637281A721AE1386009E0DE6 /* DownloadProgress.mm in Sources */ = {isa = PBXBuildFile; fileRef = 637281A621AE1386009E0DE6 /* DownloadProgress.mm */; };
                63A61B8B1FAD251100F06885 /* display-mode.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 63A61B8A1FAD204D00F06885 /* display-mode.html */; };
                63F668221F97F7F90032EE51 /* ApplicationManifest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 63F668201F97C3AA0032EE51 /* ApplicationManifest.mm */; };
+               6B0A07F721FA9C2B00D57391 /* AdClickAttribution.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6B0A07F621FA9C2B00D57391 /* AdClickAttribution.cpp */; };
                6B306106218A372900F5A802 /* ClosingWebView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6B306105218A372900F5A802 /* ClosingWebView.mm */; };
                6B9ABE122086952F00D75DE6 /* HTTPParsers.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6B9ABE112086952F00D75DE6 /* HTTPParsers.cpp */; };
                6BFD294C1D5E6C1D008EC968 /* HashCountedSet.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 7A38D7E51C752D5F004F157D /* HashCountedSet.cpp */; };
                637281A621AE1386009E0DE6 /* DownloadProgress.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = DownloadProgress.mm; sourceTree = "<group>"; };
                63A61B8A1FAD204D00F06885 /* display-mode.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "display-mode.html"; sourceTree = "<group>"; };
                63F668201F97C3AA0032EE51 /* ApplicationManifest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ApplicationManifest.mm; sourceTree = "<group>"; };
+               6B0A07F621FA9C2B00D57391 /* AdClickAttribution.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AdClickAttribution.cpp; sourceTree = "<group>"; };
                6B306105218A372900F5A802 /* ClosingWebView.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ClosingWebView.mm; sourceTree = "<group>"; };
                6B9ABE112086952F00D75DE6 /* HTTPParsers.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = HTTPParsers.cpp; sourceTree = "<group>"; };
                751B05D51F8EAC1A0028A09E /* DatabaseTrackerTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = DatabaseTrackerTest.mm; sourceTree = "<group>"; };
                                A1EC11851F4253D900D0146E /* ios */,
                                3162AE9A1E6F2F8F000E4DBC /* mac */,
                                ABF510632A19B8AC7EC40E17 /* AbortableTaskQueue.cpp */,
+                               6B0A07F621FA9C2B00D57391 /* AdClickAttribution.cpp */,
                                7A909A6F1D877475007E10F8 /* AffineTransform.cpp */,
                                57152B5D21CC2045000C37CA /* ApduTest.cpp */,
                                6354F4D01F7C3AB500D89DF3 /* ApplicationManifestParser.cpp */,
                                2E205BA41F527746005952DD /* AccessibilityTestsIOS.mm in Sources */,
                                9BD5111C1FE8E11600D2B630 /* AccessingPastedImage.mm in Sources */,
                                F45B63FE1F19D410009D38B9 /* ActionSheetTests.mm in Sources */,
+                               6B0A07F721FA9C2B00D57391 /* AdClickAttribution.cpp in Sources */,
                                37E7DD641EA06FF2009B396D /* AdditionalReadAccessAllowedURLs.mm in Sources */,
                                55A817FC218100E00004A39A /* AdditionalSupportedImageTypes.mm in Sources */,
                                7A909A7D1D877480007E10F8 /* AffineTransform.cpp in Sources */,
diff --git a/Tools/TestWebKitAPI/Tests/WebCore/AdClickAttribution.cpp b/Tools/TestWebKitAPI/Tests/WebCore/AdClickAttribution.cpp
new file mode 100644 (file)
index 0000000..57ce281
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2019 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. AND ITS CONTRIBUTORS ``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 ITS 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 <WebCore/AdClickAttribution.h>
+#include <wtf/URL.h>
+#include <wtf/WallTime.h>
+
+using namespace WebCore;
+
+namespace TestWebKitAPI {
+
+constexpr unsigned short min6BitValue { 0 };
+constexpr unsigned short max6BitValue { 63 };
+
+// Positive test cases.
+
+TEST(AdClickAttribution, ValidMinValues)
+{
+    AdClickAttribution attribution(AdClickAttribution::Campaign(min6BitValue), AdClickAttribution::Source("webkit.org"), AdClickAttribution::Destination("example.com"));
+    attribution.setConversion(AdClickAttribution::Conversion(min6BitValue, AdClickAttribution::Priority(min6BitValue)));
+
+    auto attributionURL = attribution.url();
+    auto referrerURL = attribution.referrer();
+    
+    ASSERT_EQ(attributionURL.string(), "https://webkit.org/.well-known/ad-click-attribution/0/0");
+    ASSERT_EQ(referrerURL.string(), "https://example.com/");
+}
+
+TEST(AdClickAttribution, ValidMidValues)
+{
+    AdClickAttribution attribution(AdClickAttribution::Campaign((unsigned short)12), AdClickAttribution::Source("webkit.org"), AdClickAttribution::Destination("example.com"));
+    attribution.setConversion(AdClickAttribution::Conversion((unsigned short)44, AdClickAttribution::Priority((unsigned short)22)));
+
+    auto attributionURL = attribution.url();
+    auto referrerURL = attribution.referrer();
+
+    ASSERT_EQ(attributionURL.string(), "https://webkit.org/.well-known/ad-click-attribution/44/12");
+    ASSERT_EQ(referrerURL.string(), "https://example.com/");
+}
+
+TEST(AdClickAttribution, ValidMaxValues)
+{
+    AdClickAttribution attribution(AdClickAttribution::Campaign(max6BitValue), AdClickAttribution::Source("webkit.org"), AdClickAttribution::Destination("example.com"));
+    attribution.setConversion(AdClickAttribution::Conversion(max6BitValue, AdClickAttribution::Priority(max6BitValue)));
+
+    auto attributionURL = attribution.url();
+    auto referrerURL = attribution.referrer();
+
+    ASSERT_EQ(attributionURL.string(), "https://webkit.org/.well-known/ad-click-attribution/63/63");
+    ASSERT_EQ(referrerURL.string(), "https://example.com/");
+}
+
+TEST(AdClickAttribution, EarliestTimeToSendAttributionMinimumDelay)
+{
+    AdClickAttribution attribution(AdClickAttribution::Campaign(max6BitValue), AdClickAttribution::Source("webkit.org"), AdClickAttribution::Destination("example.com"));
+    auto now = WallTime::now();
+    attribution.setConversion(AdClickAttribution::Conversion(max6BitValue, AdClickAttribution::Priority(max6BitValue)));
+    auto earliestTimeToSend = attribution.earliestTimeToSend();
+    ASSERT_TRUE(earliestTimeToSend);
+    ASSERT_TRUE(earliestTimeToSend.value().secondsSinceEpoch() - 24_h >= now.secondsSinceEpoch());
+}
+
+// Negative test cases.
+
+TEST(AdClickAttribution, InvalidCampaignId)
+{
+    AdClickAttribution attribution(AdClickAttribution::Campaign(max6BitValue + 1), AdClickAttribution::Source("webkit.org"), AdClickAttribution::Destination("example.com"));
+    attribution.setConversion(AdClickAttribution::Conversion(max6BitValue, AdClickAttribution::Priority(max6BitValue)));
+
+    auto attributionURL = attribution.url();
+    auto referrerURL = attribution.referrer();
+
+    ASSERT_TRUE(attributionURL.string().isEmpty());
+    ASSERT_TRUE(referrerURL.string().isEmpty());
+}
+
+TEST(AdClickAttribution, InvalidSourceHost)
+{
+    AdClickAttribution attribution(AdClickAttribution::Campaign(max6BitValue), AdClickAttribution::Source("webkitorg"), AdClickAttribution::Destination("example.com"));
+    attribution.setConversion(AdClickAttribution::Conversion(max6BitValue, AdClickAttribution::Priority(max6BitValue)));
+
+    auto attributionURL = attribution.url();
+    auto referrerURL = attribution.referrer();
+
+    ASSERT_TRUE(attributionURL.string().isEmpty());
+    ASSERT_TRUE(referrerURL.string().isEmpty());
+}
+
+TEST(AdClickAttribution, InvalidDestinationHost)
+{
+    AdClickAttribution attribution(AdClickAttribution::Campaign(max6BitValue + 1), AdClickAttribution::Source("webkit.org"), AdClickAttribution::Destination("examplecom"));
+    attribution.setConversion(AdClickAttribution::Conversion(max6BitValue, AdClickAttribution::Priority(max6BitValue)));
+
+    auto attributionURL = attribution.url();
+    auto referrerURL = attribution.referrer();
+
+    ASSERT_TRUE(attributionURL.string().isEmpty());
+    ASSERT_TRUE(referrerURL.string().isEmpty());
+}
+
+TEST(AdClickAttribution, InvalidConversionData)
+{
+    AdClickAttribution attribution(AdClickAttribution::Campaign(max6BitValue), AdClickAttribution::Source("webkit.org"), AdClickAttribution::Destination("example.com"));
+    attribution.setConversion(AdClickAttribution::Conversion((max6BitValue + 1), AdClickAttribution::Priority(max6BitValue)));
+
+    auto attributionURL = attribution.url();
+    auto referrerURL = attribution.referrer();
+
+    ASSERT_TRUE(attributionURL.string().isEmpty());
+    ASSERT_TRUE(referrerURL.string().isEmpty());
+}
+
+TEST(AdClickAttribution, InvalidPriority)
+{
+    AdClickAttribution attribution(AdClickAttribution::Campaign(max6BitValue), AdClickAttribution::Source("webkit.org"), AdClickAttribution::Destination("example.com"));
+    attribution.setConversion(AdClickAttribution::Conversion(max6BitValue, AdClickAttribution::Priority(max6BitValue + 1)));
+
+    auto attributionURL = attribution.url();
+    auto referrerURL = attribution.referrer();
+
+    ASSERT_TRUE(attributionURL.string().isEmpty());
+    ASSERT_TRUE(referrerURL.string().isEmpty());
+}
+
+TEST(AdClickAttribution, InvalidMissingConversion)
+{
+    AdClickAttribution attribution(AdClickAttribution::Campaign(max6BitValue), AdClickAttribution::Source("webkit.org"), AdClickAttribution::Destination("example.com"));
+
+    auto attributionURL = attribution.url();
+    auto referrerURL = attribution.referrer();
+
+    ASSERT_TRUE(attributionURL.string().isEmpty());
+    ASSERT_TRUE(referrerURL.string().isEmpty());
+    ASSERT_FALSE(attribution.earliestTimeToSend());
+}
+
+} // namespace TestWebKitAPI