update playworks checker
This commit is contained in:
90
standalone/mraid_dogcat3_test.go
Normal file
90
standalone/mraid_dogcat3_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDogCat3MraidChecker(t *testing.T) {
|
||||
root := filepath.Join("..", "aiAssets", "DogCat3")
|
||||
files, err := collectDogCat3MraidTargets(root)
|
||||
if err != nil {
|
||||
t.Fatalf("collect targets: %v", err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
t.Fatalf("no DogCat3 MRAID targets found")
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
t.Run(filepath.Base(file), func(t *testing.T) {
|
||||
html, displayPath, err := readDogCat3HTML(file)
|
||||
if err != nil {
|
||||
t.Fatalf("read target: %v", err)
|
||||
}
|
||||
issues := checkMraid(html, displayPath)
|
||||
if len(issues) == 0 {
|
||||
return
|
||||
}
|
||||
var parts []string
|
||||
for _, issue := range issues {
|
||||
parts = append(parts, issue.ID+": "+issue.Title+" ("+issue.Severity+")")
|
||||
}
|
||||
t.Fatalf("MRAID checker issues:\n%s", strings.Join(parts, "\n"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func collectDogCat3MraidTargets(root string) ([]string, error) {
|
||||
var out []string
|
||||
for _, folder := range []string{"Applovin", "Ironsource", "Unity"} {
|
||||
dir := filepath.Join(root, folder)
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := strings.ToLower(entry.Name())
|
||||
if strings.HasSuffix(name, ".html") || strings.HasSuffix(name, ".htm") || strings.HasSuffix(name, ".zip") {
|
||||
out = append(out, filepath.Join(dir, entry.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func readDogCat3HTML(file string) (html string, displayPath string, err error) {
|
||||
if strings.HasSuffix(strings.ToLower(file), ".zip") {
|
||||
zr, err := zip.OpenReader(file)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer zr.Close()
|
||||
for _, entry := range zr.File {
|
||||
if strings.EqualFold(entry.Name, "index.html") {
|
||||
rc, err := entry.Open()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer rc.Close()
|
||||
data, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return string(data), file + "#index.html", nil
|
||||
}
|
||||
}
|
||||
return "", "", os.ErrNotExist
|
||||
}
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return string(data), file, nil
|
||||
}
|
||||
@@ -82,6 +82,7 @@ func PlayworksPage(w http.ResponseWriter, r *http.Request) {
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="mo" checked /><span class="net-label">Moloco <span class="net-tag">mo</span></span><span class="net-note">HTML + FbPlayableAd CTA</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="vu" checked /><span class="net-label">Vungle <span class="net-tag">vu</span></span><span class="net-note">ZIP + __VUNGLE__ flag</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="mtg" checked /><span class="net-label">Mintegral <span class="net-tag">mtg</span></span><span class="net-note">ZIP + onload gameReady</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="un" checked /><span class="net-label">Unity <span class="net-tag">un</span></span><span class="net-note">HTML + mraid.js in head</span></label>
|
||||
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="tt" /><span class="net-label">TikTok <span class="net-tag">tt</span></span><span class="net-note">HTML + __TIKTOK__ flag</span></label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,6 +235,12 @@ var playworksExitapiScriptRx = regexp.MustCompile(`(?i)<script\b[^>]*\bsrc\s*=\s
|
||||
var playworksHeadCloseRx = regexp.MustCompile(`(?i)</head>`)
|
||||
var playworksBodyOpenRx = regexp.MustCompile(`(?i)<body\b[^>]*>`)
|
||||
var playworksBodyTagRx = regexp.MustCompile(`(?i)<body\b([^>]*)>`)
|
||||
var playworksHtmlCloseRx = regexp.MustCompile(`(?i)</html\s*>`)
|
||||
var playworksRemoteDebugScriptRx = regexp.MustCompile(`(?is)<script\b[^>]*>.*?insertYourRemoteDebuggingTokenHere.*?</script>\s*`)
|
||||
var playworksStatsURLRx = regexp.MustCompile(`(?i)https://mrdoob\.github\.io/stats\.js/build/stats\.min\.js`)
|
||||
var playworksCrossoriginRx = regexp.MustCompile(`(?i)\s+crossorigin(?:\s*=\s*("[^"]*"|'[^']*'|[^\s>]+))?`)
|
||||
var playworksTypeModuleRx = regexp.MustCompile(`(?i)\s+type\s*=\s*["']module["']`)
|
||||
var playworksConsoleErrorRx = regexp.MustCompile(`\bconsole\.error\s*\(`)
|
||||
|
||||
func playworksSourceBaseName(sourcePath string) string {
|
||||
base := strings.TrimSuffix(filepath.Base(sourcePath), filepath.Ext(sourcePath))
|
||||
@@ -305,6 +312,8 @@ func playworksNetworkOutputFolder(network string) string {
|
||||
return "Vungle"
|
||||
case "mtg":
|
||||
return "Mintegral"
|
||||
case "un":
|
||||
return "Unity"
|
||||
case "tt":
|
||||
return "TikTok"
|
||||
default:
|
||||
@@ -317,12 +326,12 @@ func playworksNeedsZip(network string) bool {
|
||||
}
|
||||
|
||||
func playworksIsMraidNetwork(network string) bool {
|
||||
return network == "al" || network == "is"
|
||||
return network == "al" || network == "is" || network == "un"
|
||||
}
|
||||
|
||||
func playworksIsSupportedNetwork(network string) bool {
|
||||
switch network {
|
||||
case "al", "is", "fb", "gg", "mo", "vu", "mtg", "tt":
|
||||
case "al", "is", "fb", "gg", "mo", "vu", "mtg", "un", "tt":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -330,11 +339,10 @@ func playworksIsSupportedNetwork(network string) bool {
|
||||
}
|
||||
|
||||
func transformPlayworksHTML(htmlSrc, network string) string {
|
||||
result := htmlSrc
|
||||
result = removePlayworksNetworkScripts(result, network)
|
||||
result := sanitizePlayworksHTML(htmlSrc)
|
||||
headInject := buildPlayworksLifecycleScript()
|
||||
if playworksIsMraidNetwork(network) {
|
||||
headInject += `<script src="mraid.js"></script>`
|
||||
headInject += `<script src="mraid.js"></script>` + buildPlayworksMraidComplianceScript()
|
||||
} else if network == "gg" {
|
||||
headInject += `<script src="exitapi.js"></script>`
|
||||
}
|
||||
@@ -349,23 +357,64 @@ func transformPlayworksHTML(htmlSrc, network string) string {
|
||||
}
|
||||
|
||||
result = replacePlayworksCTAScript(result, network)
|
||||
return finalizePlayworksNetworkHTML(result, network)
|
||||
}
|
||||
|
||||
func sanitizePlayworksHTML(htmlSrc string) string {
|
||||
return removePlayworksNetworkScripts(cleanupPlayworksHTML(htmlSrc))
|
||||
}
|
||||
|
||||
func cleanupPlayworksHTML(htmlSrc string) string {
|
||||
result := trimPlayworksAfterFirstHTMLClose(htmlSrc)
|
||||
result = playworksRemoteDebugScriptRx.ReplaceAllString(result, "")
|
||||
result = playworksStatsURLRx.ReplaceAllString(result, "data:text/javascript,")
|
||||
result = playworksCrossoriginRx.ReplaceAllString(result, "")
|
||||
result = playworksTypeModuleRx.ReplaceAllString(result, "")
|
||||
result = playworksConsoleErrorRx.ReplaceAllString(result, "console.warn(")
|
||||
return result
|
||||
}
|
||||
|
||||
func removePlayworksNetworkScripts(htmlSrc, network string) string {
|
||||
result := htmlSrc
|
||||
if playworksIsMraidNetwork(network) {
|
||||
result = playworksMraidScriptRx.ReplaceAllString(result, "")
|
||||
func trimPlayworksAfterFirstHTMLClose(htmlSrc string) string {
|
||||
loc := playworksHtmlCloseRx.FindStringIndex(htmlSrc)
|
||||
if loc == nil {
|
||||
return htmlSrc
|
||||
}
|
||||
if network == "gg" {
|
||||
result = playworksExitapiScriptRx.ReplaceAllString(result, "")
|
||||
return htmlSrc[:loc[1]]
|
||||
}
|
||||
|
||||
func finalizePlayworksNetworkHTML(htmlSrc, network string) string {
|
||||
result := cleanupPlayworksHTML(htmlSrc)
|
||||
result = dedupePlayworksScriptSrc(result, "mraid.js", playworksIsMraidNetwork(network))
|
||||
result = dedupePlayworksScriptSrc(result, "exitapi.js", network == "gg")
|
||||
return result
|
||||
}
|
||||
|
||||
func removePlayworksNetworkScripts(htmlSrc string) string {
|
||||
result := playworksMraidScriptRx.ReplaceAllString(htmlSrc, "")
|
||||
result = playworksExitapiScriptRx.ReplaceAllString(result, "")
|
||||
return result
|
||||
}
|
||||
|
||||
func dedupePlayworksScriptSrc(htmlSrc, src string, shouldExist bool) string {
|
||||
rx := regexp.MustCompile(`(?i)<script\b[^>]*\bsrc\s*=\s*["']` + regexp.QuoteMeta(src) + `["'][^>]*>\s*</script>\s*`)
|
||||
seen := false
|
||||
result := rx.ReplaceAllStringFunc(htmlSrc, func(match string) string {
|
||||
if shouldExist && !seen {
|
||||
seen = true
|
||||
return `<script src="` + src + `"></script>`
|
||||
}
|
||||
return ""
|
||||
})
|
||||
if shouldExist && !seen {
|
||||
result = injectPlayworksBeforeHeadClose(result, `<script src="`+src+`"></script>`)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func injectPlayworksBeforeHeadClose(htmlSrc, injection string) string {
|
||||
if playworksHeadCloseRx.MatchString(htmlSrc) {
|
||||
return playworksHeadCloseRx.ReplaceAllString(htmlSrc, injection+"\n</head>")
|
||||
loc := playworksHeadCloseRx.FindStringIndex(htmlSrc)
|
||||
if loc != nil {
|
||||
return htmlSrc[:loc[0]] + injection + "\n" + htmlSrc[loc[0]:]
|
||||
}
|
||||
return injection + "\n" + htmlSrc
|
||||
}
|
||||
@@ -489,12 +538,34 @@ func buildPlayworksLifecycleScript() string {
|
||||
}, "")
|
||||
}
|
||||
|
||||
func buildPlayworksMraidComplianceScript() string {
|
||||
return strings.Join([]string{
|
||||
`<script>(function(){`,
|
||||
`var m=null,scene=null,viewable=true,exposed=true,volume=null;`,
|
||||
`function get(){return window.mraid||m;}`,
|
||||
`function emit(name,detail){try{window.dispatchEvent(new CustomEvent(name,{detail:detail}));}catch(e){}}`,
|
||||
`function apply(){emit("hpl:mraid:visibility",{viewable:viewable,exposed:exposed,hidden:!viewable||!exposed});if(scene&&scene.sound&&typeof scene.sound.setMute==="function")scene.sound.setMute(!viewable||!exposed);}`,
|
||||
`function onViewable(v){viewable=!!v;apply();}`,
|
||||
`function onExposure(p){if(typeof p==="number")exposed=p>0;apply();}`,
|
||||
`function onVolume(v){if(typeof v==="number"){volume=v/100;emit("hpl:mraid:volume",volume);if(scene&&scene.sound&&typeof scene.sound.setVolume==="function")scene.sound.setVolume(volume);}}`,
|
||||
`function add(name,fn){var x=get();try{if(x&&typeof x.addEventListener==="function")x.addEventListener(name,fn);}catch(e){}}`,
|
||||
`function unload(){try{if(typeof mraid!=="undefined"&&typeof mraid.unload==="function")mraid.unload();}catch(e){}}`,
|
||||
`window.__hplMraidUnload=unload;`,
|
||||
`function setup(){var x=get();if(!x||setup.done)return;setup.done=true;m=x;try{if(typeof mraid!=="undefined"&&typeof mraid.isViewable==="function")viewable=!!mraid.isViewable();else if(typeof x.isViewable==="function")viewable=!!x.isViewable();}catch(e){}try{if(typeof mraid!=="undefined"&&typeof mraid.addEventListener==="function"){mraid.addEventListener("error",function(message,action){console.warn("[MRAID error]",{message:message,action:action});});mraid.addEventListener("stateChange",function(state){console.log("[MRAID stateChange]",state);});mraid.addEventListener("exposureChange",onExposure);mraid.addEventListener("viewableChange",onViewable);mraid.addEventListener("audioVolumeChange",onVolume);apply();return;}}catch(e){}add("error",function(message,action){console.warn("[MRAID error]",{message:message,action:action});});add("stateChange",function(state){console.log("[MRAID stateChange]",state);});add("exposureChange",onExposure);add("viewableChange",onViewable);add("audioVolumeChange",onVolume);apply();}`,
|
||||
`function ready(){var x=get();if(!x)return false;try{if(typeof mraid!=="undefined"&&typeof mraid.getState==="function"&&mraid.getState()==="loading"){mraid.addEventListener("ready",setup);return true;}if(typeof x.getState==="function"&&x.getState()==="loading"){add("ready",setup);return true;}}catch(e){}setup();return true;}`,
|
||||
`window.__hplMraidBindScene=function(s){scene=s;apply();if(typeof volume==="number"&&scene&&scene.sound&&typeof scene.sound.setVolume==="function")scene.sound.setVolume(volume);};`,
|
||||
`if(!ready()){var end=Date.now()+500;(function poll(){if(ready()||Date.now()>end)return;setTimeout(poll,50);})();}`,
|
||||
`setTimeout(setup,2000);`,
|
||||
`})();</script>`,
|
||||
}, "")
|
||||
}
|
||||
|
||||
func buildPlayworksCTAScript(network string) string {
|
||||
urlSetup := `n=n||window.$environment.packageConfig.iosLink,i=i||window.$environment.packageConfig.androidLink;var o=/iphone|ipad|ipod|macintosh/i.test(window.navigator.userAgent.toLowerCase())?n:i;`
|
||||
closeCall := `try{window.gameClose();}catch(e){}`
|
||||
ctaLogic := closeCall + `window.open(o,"_blank");`
|
||||
switch network {
|
||||
case "al", "is":
|
||||
case "al", "is", "un":
|
||||
ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){var s=typeof mraid.getState==="function"?mraid.getState():"default";if(s!=="loading"){mraid.open(o);return;}}window.open(o,"_blank");`
|
||||
case "fb":
|
||||
ctaLogic = closeCall + `if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}window.open(o,"_blank");`
|
||||
|
||||
32
standalone/playworks_export_test.go
Normal file
32
standalone/playworks_export_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExportPlayworksNetworksForRuntimeCheck(t *testing.T) {
|
||||
outDir := os.Getenv("PLAYWORKS_EXPORT_DIR")
|
||||
if outDir == "" {
|
||||
t.Skip("PLAYWORKS_EXPORT_DIR not set")
|
||||
}
|
||||
|
||||
sourcePath := filepath.Join("..", "aiAssets", "Playworks_UnityNetwork.html")
|
||||
data, err := os.ReadFile(sourcePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read source: %v", err)
|
||||
}
|
||||
|
||||
opts := playworksConvertOptions{
|
||||
SourcePath: sourcePath,
|
||||
OutputDir: outDir,
|
||||
BaseName: "wat_mip_grhpl_dogcat3_05_real_na_noseason_en_full_na",
|
||||
}
|
||||
|
||||
for _, network := range []string{"al", "fb", "gg", "is", "mo", "mtg", "un", "vu"} {
|
||||
if _, err := convertPlayworksNetwork(string(data), opts, network); err != nil {
|
||||
t.Fatalf("convert %s: %v", network, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
75
standalone/playworks_test.go
Normal file
75
standalone/playworks_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTransformPlayworksHTMLNetworkRequirements(t *testing.T) {
|
||||
sourcePath := filepath.Join("..", "aiAssets", "Playworks_UnityNetwork.html")
|
||||
data, err := os.ReadFile(sourcePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read source: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
network string
|
||||
wantMraid int
|
||||
wantExitapi int
|
||||
}{
|
||||
{network: "al", wantMraid: 1},
|
||||
{network: "is", wantMraid: 1},
|
||||
{network: "un", wantMraid: 1},
|
||||
{network: "gg", wantExitapi: 1},
|
||||
{network: "fb"},
|
||||
{network: "mo"},
|
||||
{network: "vu"},
|
||||
{network: "mtg"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.network, func(t *testing.T) {
|
||||
html := transformPlayworksHTML(string(data), tt.network)
|
||||
assertCount(t, html, `src="mraid.js"`, tt.wantMraid)
|
||||
assertCount(t, html, `src="exitapi.js"`, tt.wantExitapi)
|
||||
assertCount(t, strings.ToLower(html), "</html>", 1)
|
||||
assertCount(t, strings.ToLower(html), "</head>", 1)
|
||||
|
||||
for _, forbidden := range []string{
|
||||
`type="module"`,
|
||||
`crossorigin`,
|
||||
`console.error(`,
|
||||
`stats.min.js`,
|
||||
`insertYourRemoteDebuggingTokenHere`,
|
||||
`console.re/connector.js`,
|
||||
} {
|
||||
if strings.Contains(html, forbidden) {
|
||||
t.Fatalf("forbidden content remains: %s", forbidden)
|
||||
}
|
||||
}
|
||||
|
||||
for _, required := range []string{"gameReady", "gameStart", "gameEnd", "gameClose"} {
|
||||
if !strings.Contains(html, required) {
|
||||
t.Fatalf("missing lifecycle symbol: %s", required)
|
||||
}
|
||||
}
|
||||
|
||||
if tt.wantMraid == 1 {
|
||||
for _, required := range []string{"exposureChange", "viewableChange", "audioVolumeChange", "[MRAID error]", "stateChange"} {
|
||||
if !strings.Contains(html, required) {
|
||||
t.Fatalf("missing MRAID requirement marker: %s", required)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertCount(t *testing.T, haystack, needle string, want int) {
|
||||
t.Helper()
|
||||
if got := strings.Count(haystack, needle); got != want {
|
||||
t.Fatalf("%q count = %d, want %d", needle, got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user