standalone mintegral checker
This commit is contained in:
1
standalone/assets/mintegral/preview-util.js
Normal file
1
standalone/assets/mintegral/preview-util.js
Normal file
File diff suppressed because one or more lines are too long
@@ -328,7 +328,8 @@ var navItems = []navItem{
|
||||
{Path: "/applovin", Label: "AppLovin Playable Preview", Description: "Upload to p.applov.in (QR preview)"},
|
||||
{Path: "/base64", Label: "Base64 Scanner", Description: "Find non-base64 assets in HTML"},
|
||||
{Path: "/mobile", Label: "Send To Mobile", Description: "Share HTML to a phone via LAN + QR"},
|
||||
{Path: "/mraid", Label: "MRAID Checker", Description: "Check MRAID requirements and best practices"},
|
||||
{Path: "/mraid", Label: "MRAID Checker", Description: "Check MRAID requirements and best practices"},
|
||||
{Path: "/mintegral", Label: "Mintegral Checker", Description: "Validate Mintegral playable ZIPs against PlayTurbo checks"},
|
||||
{Path: "/playworks", Label: "Playworks Converter", Description: "Convert Playworks HTML to per-network variants", Beta: true},
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ func buildMux() *http.ServeMux {
|
||||
mux.HandleFunc("GET /applovin", ApplovinPage)
|
||||
mux.HandleFunc("GET /base64", Base64Page)
|
||||
mux.HandleFunc("GET /mraid", MraidPage)
|
||||
mux.HandleFunc("GET /mintegral", MintegralPage)
|
||||
mux.HandleFunc("GET /mobile", MobilePage)
|
||||
mux.HandleFunc("GET /playworks", PlayworksPage)
|
||||
mux.HandleFunc("GET /changelog", ChangelogPage)
|
||||
@@ -145,6 +146,14 @@ func buildMux() *http.ServeMux {
|
||||
mux.HandleFunc("POST /api/mraid/pickFiles", MraidPickFiles)
|
||||
mux.HandleFunc("POST /api/mraid/scan", MraidScan)
|
||||
|
||||
// Mintegral Checker
|
||||
mux.HandleFunc("POST /api/mintegral/pick", MintegralPick)
|
||||
mux.HandleFunc("POST /api/mintegral/load", MintegralLoad)
|
||||
mux.HandleFunc("GET /mintegral/runtime", MintegralRuntimePage)
|
||||
mux.HandleFunc("GET /mintegral/ad", MintegralAd)
|
||||
mux.HandleFunc("GET /mintegral/monitor.js", MintegralMonitor)
|
||||
mux.HandleFunc("GET /mintegral/preview-util.js", MintegralPreviewUtil)
|
||||
|
||||
// Send To Mobile
|
||||
mux.HandleFunc("POST /api/mobile/pick", MobilePick)
|
||||
mux.HandleFunc("POST /api/mobile/pickFolder", MobilePickFolder)
|
||||
|
||||
545
standalone/mintegral.go
Normal file
545
standalone/mintegral.go
Normal file
@@ -0,0 +1,545 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed assets/mintegral/preview-util.js
|
||||
var mintegralPreviewUtil string
|
||||
|
||||
const (
|
||||
mintegralZipLimit = 5 * 1024 * 1024
|
||||
mintegralHTMLLimit = 5 * 1024 * 1024
|
||||
)
|
||||
|
||||
type mintegralCheck struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
|
||||
type mintegralZipEntry struct {
|
||||
Name string
|
||||
CompressedSize uint64
|
||||
UncompressedSize uint64
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type mintegralRuntimePayload struct {
|
||||
FileName string `json:"fileName"`
|
||||
ZipSize int64 `json:"zipSize"`
|
||||
HTML string `json:"html"`
|
||||
Checks []mintegralCheck `json:"checks"`
|
||||
}
|
||||
|
||||
var mintegralRuntime = struct {
|
||||
sync.RWMutex
|
||||
payload *mintegralRuntimePayload
|
||||
}{}
|
||||
|
||||
func MintegralPage(w http.ResponseWriter, r *http.Request) {
|
||||
body := `
|
||||
<header class="tool-header">
|
||||
<h2 class="tool-title">Mintegral Checker</h2>
|
||||
<p class="tool-description">Validates a Mintegral playable ad ZIP against PlayTurbo requirements. Runtime checks update live as you interact with the playable.</p>
|
||||
</header>
|
||||
|
||||
<section class="tool-panel input-panel">
|
||||
<div class="panel-header"><h3 class="panel-title">Input</h3></div>
|
||||
<div class="panel-body">
|
||||
<div class="control-label">Playable ZIP</div>
|
||||
<div id="dropZone" class="drop-zone">
|
||||
<div class="file-row">
|
||||
<button id="pickBtn" class="secondary">Select File</button>
|
||||
<button id="reloadBtn" class="secondary" style="display:none">Reload</button>
|
||||
</div>
|
||||
<div class="drop-zone-text">or drop a Mintegral ZIP here</div>
|
||||
</div>
|
||||
<div class="file-name" id="fileName" style="margin-top:8px;">(no file selected)</div>
|
||||
<div id="status" class="status-panel"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tool-panel embedded-runtime" id="runtimePanel" style="display:none">
|
||||
<div class="panel-header"><h3 class="panel-title">Runtime Checker</h3></div>
|
||||
<iframe id="runtimeFrame"></iframe>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.embedded-runtime { height: calc(100vh - 260px); min-height: 360px; }
|
||||
.embedded-runtime iframe { width:100%; height:calc(100% - 39px); min-height:0; border:0; background:#1e1e1e; display:block; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const statusEl = document.getElementById('status');
|
||||
const fileNameEl = document.getElementById('fileName');
|
||||
const runtimePanel = document.getElementById('runtimePanel');
|
||||
const runtimeFrame = document.getElementById('runtimeFrame');
|
||||
const reloadBtn = document.getElementById('reloadBtn');
|
||||
let currentPath = '';
|
||||
function basename(p){ const i=Math.max(p.lastIndexOf('/'),p.lastIndexOf('\\')); return i>=0?p.slice(i+1):p; }
|
||||
async function loadZip(path){
|
||||
if(!path){ statusEl.textContent='Pick a ZIP first.'; return; }
|
||||
currentPath = path;
|
||||
statusEl.textContent = 'Reading ' + basename(path) + '...';
|
||||
statusEl.classList.add('is-busy');
|
||||
runtimeFrame.src = 'about:blank';
|
||||
runtimePanel.style.display = 'none';
|
||||
const r = await fetch('/api/mintegral/load', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({path})});
|
||||
const j = await r.json();
|
||||
statusEl.classList.remove('is-busy');
|
||||
if(j.error){ statusEl.textContent = 'Error: ' + j.error; return; }
|
||||
fileNameEl.textContent = j.fileName + ' (' + Math.round(j.zipSize / 1024) + ' KB)';
|
||||
statusEl.textContent = '';
|
||||
reloadBtn.style.display = '';
|
||||
runtimePanel.style.display = 'block';
|
||||
runtimeFrame.src = j.runtimeUrl;
|
||||
}
|
||||
document.getElementById('pickBtn').addEventListener('click', async () => {
|
||||
const r = await fetch('/api/mintegral/pick', {method:'POST'});
|
||||
const j = await r.json();
|
||||
if(j.path) loadZip(j.path);
|
||||
});
|
||||
reloadBtn.addEventListener('click', () => loadZip(currentPath));
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('is-dragover'); });
|
||||
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('is-dragover'));
|
||||
dropZone.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('is-dragover');
|
||||
const paths = extractDroppedPaths(e.dataTransfer).filter(p => /\.zip$/i.test(p));
|
||||
if(!paths.length){ statusEl.textContent = 'Drop a ZIP file.'; return; }
|
||||
loadZip(paths[0]);
|
||||
});
|
||||
</script>`
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write([]byte(Page("/mintegral", "Mintegral Checker", body)))
|
||||
}
|
||||
|
||||
func MintegralPick(w http.ResponseWriter, r *http.Request) {
|
||||
paths := PickFiles("Select Mintegral ZIP", []FileFilter{{Name: "ZIP", Extensions: []string{"zip"}}}, false, "")
|
||||
path := ""
|
||||
if len(paths) > 0 {
|
||||
path = paths[0]
|
||||
}
|
||||
writeJSON(w, map[string]any{"path": path})
|
||||
}
|
||||
|
||||
func MintegralLoad(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
payload, err := loadMintegralZip(req.Path)
|
||||
if err != nil {
|
||||
writeJSON(w, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
mintegralRuntime.Lock()
|
||||
mintegralRuntime.payload = payload
|
||||
mintegralRuntime.Unlock()
|
||||
writeJSON(w, map[string]any{
|
||||
"fileName": payload.FileName,
|
||||
"zipSize": payload.ZipSize,
|
||||
"runtimeUrl": "/mintegral/runtime",
|
||||
})
|
||||
}
|
||||
|
||||
func MintegralRuntimePage(w http.ResponseWriter, r *http.Request) {
|
||||
mintegralRuntime.RLock()
|
||||
payload := mintegralRuntime.payload
|
||||
mintegralRuntime.RUnlock()
|
||||
if payload == nil {
|
||||
http.Error(w, "No Mintegral ZIP loaded.", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_, _ = w.Write([]byte(mintegralRuntimeHTML(payload)))
|
||||
}
|
||||
|
||||
func MintegralAd(w http.ResponseWriter, r *http.Request) {
|
||||
mintegralRuntime.RLock()
|
||||
payload := mintegralRuntime.payload
|
||||
mintegralRuntime.RUnlock()
|
||||
if payload == nil {
|
||||
http.Error(w, "No Mintegral ZIP loaded.", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_, _ = w.Write([]byte(payload.HTML))
|
||||
}
|
||||
|
||||
func MintegralPreviewUtil(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_, _ = w.Write([]byte(mintegralPreviewUtil))
|
||||
}
|
||||
|
||||
func MintegralMonitor(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_, _ = w.Write([]byte(mintegralMonitorScript))
|
||||
}
|
||||
|
||||
func loadMintegralZip(zipPath string) (*mintegralRuntimePayload, error) {
|
||||
if zipPath == "" {
|
||||
return nil, fmt.Errorf("Pick a ZIP first.")
|
||||
}
|
||||
info, err := os.Stat(zipPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil, fmt.Errorf("Expected a ZIP file, got a folder.")
|
||||
}
|
||||
entries, err := readMintegralZip(zipPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var index *mintegralZipEntry
|
||||
for i := range entries {
|
||||
if entries[i].Name == "index.html" {
|
||||
index = &entries[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
htmlText := ""
|
||||
if index != nil {
|
||||
htmlText = string(index.Data)
|
||||
}
|
||||
checks := runMintegralStaticChecks(htmlText, entries, info.Size())
|
||||
return &mintegralRuntimePayload{
|
||||
FileName: filepath.Base(zipPath),
|
||||
ZipSize: info.Size(),
|
||||
HTML: injectMintegralMonitoring(htmlText),
|
||||
Checks: checks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readMintegralZip(zipPath string) ([]mintegralZipEntry, error) {
|
||||
zr, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer zr.Close()
|
||||
entries := make([]mintegralZipEntry, 0, len(zr.File))
|
||||
for _, f := range zr.File {
|
||||
if strings.HasSuffix(f.Name, "/") {
|
||||
continue
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
data, err := io.ReadAll(rc)
|
||||
_ = rc.Close()
|
||||
if err != nil {
|
||||
data = nil
|
||||
}
|
||||
entries = append(entries, mintegralZipEntry{
|
||||
Name: filepath.ToSlash(f.Name),
|
||||
CompressedSize: f.CompressedSize64,
|
||||
UncompressedSize: f.UncompressedSize64,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func injectMintegralMonitoring(htmlText string) string {
|
||||
stub := `<script src="/mintegral/preview-util.js"></script>
|
||||
<script src="/mintegral/monitor.js"></script>`
|
||||
if regexp.MustCompile(`(?i)<head\b[^>]*>`).MatchString(htmlText) {
|
||||
return regexp.MustCompile(`(?i)<head\b[^>]*>`).ReplaceAllStringFunc(htmlText, func(m string) string {
|
||||
return m + "\n" + stub
|
||||
})
|
||||
}
|
||||
return stub + "\n" + htmlText
|
||||
}
|
||||
|
||||
func runMintegralStaticChecks(htmlText string, entries []mintegralZipEntry, zipSize int64) []mintegralCheck {
|
||||
scripts := mintegralExtractScripts(htmlText)
|
||||
return []mintegralCheck{
|
||||
mintegralCheckHTML(htmlText),
|
||||
mintegralCheckCTA(scripts),
|
||||
mintegralCheckGameEnd(scripts),
|
||||
mintegralCheckGameReady(htmlText, scripts),
|
||||
mintegralCheckGameStart(scripts),
|
||||
mintegralCheckGameClose(scripts),
|
||||
mintegralCheckFileHandling(htmlText),
|
||||
mintegralCheckFileSpec(entries, zipSize),
|
||||
mintegralCheckStorage(scripts),
|
||||
}
|
||||
}
|
||||
|
||||
func mintegralExtractScripts(htmlText string) string {
|
||||
re := regexp.MustCompile(`(?is)<script\b[^>]*>(.*?)</script>`)
|
||||
matches := re.FindAllStringSubmatch(htmlText, -1)
|
||||
parts := make([]string, 0, len(matches))
|
||||
for _, m := range matches {
|
||||
parts = append(parts, m[1])
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
func mintegralCheckHTML(htmlText string) mintegralCheck {
|
||||
issues := []string{}
|
||||
if !regexp.MustCompile(`(?i)^\s*<!DOCTYPE\s+html\s*>`).MatchString(htmlText) {
|
||||
issues = append(issues, "Missing <!DOCTYPE html>")
|
||||
}
|
||||
if !regexp.MustCompile(`(?i)<meta\b[^>]*\bcharset\s*=`).MatchString(htmlText) {
|
||||
issues = append(issues, "Missing charset meta tag")
|
||||
}
|
||||
if !regexp.MustCompile(`(?i)<meta\b[^>]*\bname\s*=\s*["']viewport["']`).MatchString(htmlText) {
|
||||
issues = append(issues, "Missing viewport meta tag")
|
||||
}
|
||||
if regexp.MustCompile(`(?i)<script\b[^>]*\btype\s*=\s*["']module["']`).MatchString(htmlText) {
|
||||
issues = append(issues, `type="module" on script tag`)
|
||||
}
|
||||
if regexp.MustCompile(`(?i)<script\b[^>]*\bcrossorigin\b`).MatchString(htmlText) {
|
||||
issues = append(issues, "crossorigin attribute on script tag")
|
||||
}
|
||||
if !regexp.MustCompile(`(?i)</html\s*>`).MatchString(htmlText) {
|
||||
issues = append(issues, "Missing </html> closing tag")
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
return mintegralCheck{ID: "html-requirements", Status: "pass", Detail: "Valid HTML structure"}
|
||||
}
|
||||
return mintegralCheck{ID: "html-requirements", Status: "fail", Detail: strings.Join(issues, "; ")}
|
||||
}
|
||||
|
||||
func mintegralCheckCTA(scripts string) mintegralCheck {
|
||||
if regexp.MustCompile(`\bwindow\.install\b|\btypeof\s+window\.install`).MatchString(scripts) {
|
||||
return mintegralCheck{ID: "cta-method", Status: "pass", Detail: "window.install() referenced in scripts"}
|
||||
}
|
||||
return mintegralCheck{ID: "cta-method", Status: "fail", Detail: "window.install() not found - Mintegral CTA requires window.install()"}
|
||||
}
|
||||
|
||||
func mintegralCheckGameEnd(scripts string) mintegralCheck {
|
||||
if regexp.MustCompile(`\bgameEnd\b`).MatchString(scripts) {
|
||||
return mintegralCheck{ID: "game-end", Status: "pass", Detail: "gameEnd() found in scripts"}
|
||||
}
|
||||
return mintegralCheck{ID: "game-end", Status: "fail", Detail: "gameEnd() not found"}
|
||||
}
|
||||
|
||||
func mintegralCheckGameReady(htmlText, scripts string) mintegralCheck {
|
||||
inScript := regexp.MustCompile(`\bgameReady\b`).MatchString(scripts)
|
||||
inOnload := regexp.MustCompile(`(?i)\bonload\s*=\s*["'][^"']*\bgameReady\b`).MatchString(htmlText)
|
||||
if !inScript && !inOnload {
|
||||
return mintegralCheck{ID: "game-ready", Status: "fail", Detail: "gameReady() not found"}
|
||||
}
|
||||
if !inOnload {
|
||||
return mintegralCheck{ID: "game-ready", Status: "warn", Detail: `gameReady() found but not wired to body onload - Mintegral requires onload="gameReady()"`}
|
||||
}
|
||||
return mintegralCheck{ID: "game-ready", Status: "pass", Detail: "gameReady() wired to body onload"}
|
||||
}
|
||||
|
||||
func mintegralCheckGameStart(scripts string) mintegralCheck {
|
||||
if regexp.MustCompile(`\bgameStart\b`).MatchString(scripts) {
|
||||
return mintegralCheck{ID: "game-start", Status: "pass", Detail: "gameStart() found in scripts"}
|
||||
}
|
||||
return mintegralCheck{ID: "game-start", Status: "fail", Detail: "gameStart() not found"}
|
||||
}
|
||||
|
||||
func mintegralCheckGameClose(scripts string) mintegralCheck {
|
||||
if regexp.MustCompile(`\bgameClose\b`).MatchString(scripts) {
|
||||
return mintegralCheck{ID: "game-close", Status: "pass", Detail: "gameClose() found in scripts"}
|
||||
}
|
||||
return mintegralCheck{ID: "game-close", Status: "fail", Detail: "gameClose() not found"}
|
||||
}
|
||||
|
||||
func mintegralCheckFileHandling(htmlText string) mintegralCheck {
|
||||
re := regexp.MustCompile(`(?i)(?:src|href|action)\s*=\s*["'](https?://[^"']+)["']|url\s*\(\s*["']?(https?://[^)"'\s]+)`)
|
||||
matches := re.FindAllStringSubmatch(htmlText, -1)
|
||||
seen := map[string]bool{}
|
||||
found := []string{}
|
||||
for _, m := range matches {
|
||||
u := m[1]
|
||||
if u == "" {
|
||||
u = m[2]
|
||||
}
|
||||
if u != "" && !seen[u] {
|
||||
seen[u] = true
|
||||
found = append(found, u)
|
||||
}
|
||||
}
|
||||
if len(found) == 0 {
|
||||
return mintegralCheck{ID: "file-handling", Status: "pass", Detail: "All assets are inline - no external URLs detected"}
|
||||
}
|
||||
detail := fmt.Sprintf("%d external URL(s): %s", len(found), strings.Join(found[:minInt(2, len(found))], ", "))
|
||||
if len(found) > 2 {
|
||||
detail += fmt.Sprintf(" (+%d more)", len(found)-2)
|
||||
}
|
||||
return mintegralCheck{ID: "file-handling", Status: "fail", Detail: detail}
|
||||
}
|
||||
|
||||
func mintegralCheckFileSpec(entries []mintegralZipEntry, zipSize int64) mintegralCheck {
|
||||
issues := []string{}
|
||||
var index *mintegralZipEntry
|
||||
for i := range entries {
|
||||
if entries[i].Name == "index.html" {
|
||||
index = &entries[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == nil {
|
||||
nested := ""
|
||||
for _, e := range entries {
|
||||
if regexp.MustCompile(`(?i)/index\.html$`).MatchString(e.Name) {
|
||||
nested = e.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
if nested != "" {
|
||||
issues = append(issues, fmt.Sprintf("index.html found at '%s' - must be at ZIP root", nested))
|
||||
} else {
|
||||
issues = append(issues, "index.html not found in ZIP")
|
||||
}
|
||||
}
|
||||
if zipSize > mintegralZipLimit {
|
||||
issues = append(issues, fmt.Sprintf("ZIP size %.2f MB exceeds 5 MB limit", float64(zipSize)/1024/1024))
|
||||
}
|
||||
if index != nil && index.UncompressedSize > mintegralHTMLLimit {
|
||||
issues = append(issues, fmt.Sprintf("index.html %.2f MB uncompressed exceeds 5 MB", float64(index.UncompressedSize)/1024/1024))
|
||||
}
|
||||
if len(issues) > 0 {
|
||||
return mintegralCheck{ID: "file-spec", Status: "fail", Detail: strings.Join(issues, "; ")}
|
||||
}
|
||||
extras := []string{}
|
||||
for _, e := range entries {
|
||||
if e.Name != "index.html" {
|
||||
extras = append(extras, e.Name)
|
||||
}
|
||||
}
|
||||
sizeStr := fmt.Sprintf("index.html %.0f KB, ZIP %.0f KB", float64(index.UncompressedSize)/1024, float64(zipSize)/1024)
|
||||
if len(extras) > 0 {
|
||||
return mintegralCheck{ID: "file-spec", Status: "warn", Detail: fmt.Sprintf("%s; %d extra file(s) in ZIP (%s)", sizeStr, len(extras), strings.Join(extras, ", "))}
|
||||
}
|
||||
return mintegralCheck{ID: "file-spec", Status: "pass", Detail: sizeStr}
|
||||
}
|
||||
|
||||
func mintegralCheckStorage(scripts string) mintegralCheck {
|
||||
issues := []string{}
|
||||
if regexp.MustCompile(`\blocalStorage\s*\.\s*setItem\s*\(`).MatchString(scripts) {
|
||||
issues = append(issues, "localStorage.setItem")
|
||||
}
|
||||
if regexp.MustCompile(`\bsessionStorage\s*\.\s*setItem\s*\(`).MatchString(scripts) {
|
||||
issues = append(issues, "sessionStorage.setItem")
|
||||
}
|
||||
if regexp.MustCompile(`\bdocument\.cookie\s*=`).MatchString(scripts) {
|
||||
issues = append(issues, "document.cookie write")
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
return mintegralCheck{ID: "storage", Status: "pass", Detail: "No persistent storage writes detected"}
|
||||
}
|
||||
return mintegralCheck{ID: "storage", Status: "fail", Detail: "Forbidden storage write(s): " + strings.Join(issues, ", ")}
|
||||
}
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func mintegralRuntimeHTML(payload *mintegralRuntimePayload) string {
|
||||
boot, _ := json.Marshal(map[string]any{
|
||||
"fileName": payload.FileName,
|
||||
"zipSize": payload.ZipSize,
|
||||
"checks": payload.Checks,
|
||||
})
|
||||
return `<!DOCTYPE html>
|
||||
<html><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Mintegral Runtime Checker</title>
|
||||
<style>
|
||||
:root{color-scheme:dark;--gap-xs:4px;--gap-sm:8px;--gap-md:12px;--radius:4px;--border:#333;--panel:#252526;--panel-soft:rgba(128,128,128,.08);--fg:#ddd;--muted:#a7a7a7;--ok:#89d185;--err:#f48771;--warn:#ffd580}
|
||||
*{box-sizing:border-box}html,body{width:100%;height:100%;overflow:hidden}body{margin:0;background:#1e1e1e;color:var(--fg);font-family:Segoe UI,Arial,sans-serif;font-size:13px}
|
||||
main{height:100vh;display:grid;grid-template-columns:minmax(360px,1fr) minmax(320px,420px);gap:var(--gap-md);padding:var(--gap-md);overflow:hidden}
|
||||
.panel{border:1px solid var(--border);background:var(--panel-soft);border-radius:var(--radius);overflow:hidden}.head{display:flex;align-items:center;justify-content:space-between;gap:var(--gap-md);padding:10px 12px;border-bottom:1px solid var(--border);background:var(--panel)}
|
||||
h1{margin:0;font-size:12px;font-weight:700;letter-spacing:.45px;text-transform:uppercase}.sub{color:var(--muted);font-size:12px;overflow-wrap:anywhere}.head-meta{display:flex;align-items:center;justify-content:flex-end;gap:6px;min-width:0;white-space:nowrap}.head-meta .sub{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.head-meta .sub+.sub::before{content:"|";margin-right:6px;color:var(--border)}
|
||||
.title-actions{display:flex;gap:var(--gap-xs)}button{min-height:28px;padding:5px 12px;border:1px solid transparent;border-radius:var(--radius);background:#0e639c;color:#fff;font:inherit;cursor:pointer}.icon-btn{width:28px;height:28px;min-height:28px;padding:0;display:grid;place-items:center;font-size:15px;line-height:1;background:#3a3d41;color:#ddd;border-color:#444}.icon-btn[aria-pressed=true]{color:var(--ok)}
|
||||
.runtime-side{display:grid;grid-template-rows:minmax(0,1fr);min-height:0}.playable-panel{min-height:0;display:flex;flex-direction:column}.playable-body{min-height:0;flex:1;display:flex;flex-direction:column;padding:10px}.preview-stage{min-height:0;flex:1;display:grid;place-items:center;overflow:hidden;container-type:size}.device{width:min(100cqw,calc(100cqh*.5625));max-width:100%;aspect-ratio:9/16;margin:auto}.screen{position:relative;width:100%;height:100%;background:#000;border-radius:4px;overflow:hidden}iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:#000}
|
||||
.tab-panel{min-height:0;display:flex;flex-direction:column}.tabs{display:flex;gap:var(--gap-xs);padding:8px 8px 0;background:var(--panel);border-bottom:1px solid var(--border)}.tab{min-height:28px;padding:4px 10px;background:#3a3d41;color:#ddd;border-color:#444;border-bottom:0;border-radius:var(--radius) var(--radius) 0 0}.tab.active{background:var(--panel-soft)}.tab-content{min-height:0;flex:1;display:none;overflow:auto}.tab-content.active{display:block}
|
||||
ol{list-style:none;margin:0;padding:0}.check{display:flex;gap:10px;padding:10px 12px;border-bottom:1px solid var(--border)}.num{flex:0 0 22px;height:22px;border:1px solid var(--border);border-radius:50%;display:grid;place-items:center;color:var(--muted);background:var(--panel);font-size:10px;font-weight:700}.label{font-size:12px;font-weight:600}.detail{margin-top:3px;color:var(--muted);font-size:11px;line-height:1.35;overflow-wrap:anywhere}.badge{display:inline-flex;min-height:18px;margin-left:6px;padding:1px 6px;border:1px solid var(--border);border-radius:999px;font-size:11px}.badge.pass{color:var(--ok);background:rgba(137,209,133,.12)}.badge.fail{color:var(--err);background:rgba(244,135,113,.14)}.badge.warn{color:var(--warn);background:rgba(210,153,34,.16)}.badge.waiting{color:var(--muted)}
|
||||
ul.log{list-style:none;margin:0;padding:0;overflow:auto;font-family:Consolas,monospace;font-size:12px;height:100%}ul.log li{display:flex;gap:8px;padding:4px 8px;border-bottom:1px solid var(--border)}.time{color:var(--muted);flex:0 0 auto}.msg{white-space:pre-wrap;overflow-wrap:anywhere;min-width:0}.level{font-weight:700}.pass{color:var(--ok)}.fail{color:var(--err)}.warn{color:var(--warn)}
|
||||
@media(max-width:900px){html,body{overflow:auto}main{height:auto;min-height:100vh;grid-template-columns:1fr;overflow:visible}.playable-panel{height:min(72vh,640px);min-height:360px}.tab-panel{min-height:360px}}@media(max-height:700px) and (max-width:900px){.playable-panel{height:calc(100vh - 24px);min-height:300px}}
|
||||
</style></head><body>
|
||||
<main>
|
||||
<section class="runtime-side"><section class="panel playable-panel"><div class="head"><h1>Playable</h1><div class="title-actions"><button id="muteBtn" class="icon-btn" title="Mute" aria-label="Mute" aria-pressed="false">🔇</button><button id="reloadBtn" class="icon-btn" title="Reload" aria-label="Reload">↻</button></div></div><div class="playable-body"><div class="preview-stage"><div class="device"><div class="screen"><iframe id="adFrame" src="/mintegral/ad?preview=true&loading=0&review=1"></iframe></div></div></div></div></section></section>
|
||||
<section class="panel tab-panel"><div class="head"><h1>Checks</h1><div class="head-meta"><div class="sub" id="bridge">Starting...</div><div class="sub" id="timer">60s</div></div></div><div class="tabs"><button class="tab active" data-tab="checksTab">Checklist</button><button class="tab" data-tab="eventsTab">Events</button></div><ol class="tab-content active" id="checksTab"></ol><ul class="log tab-content" id="eventsTab"></ul></section>
|
||||
</main>
|
||||
<script>const BOOT=` + string(boot) + `;</script>
|
||||
<script>` + mintegralRuntimeJS + `</script>
|
||||
</body></html>`
|
||||
}
|
||||
|
||||
const mintegralRuntimeJS = `
|
||||
const CHECKS=[
|
||||
{id:'html-requirements',num:1,label:'HTML Requirements',runtimeEvent:null},{id:'cta-method',num:2,label:'CTA Call Method',runtimeEvent:'install'},{id:'game-end',num:3,label:'Game End Call Method',runtimeEvent:'gameEnd'},{id:'game-ready',num:4,label:'Game Ready Call Method',runtimeEvent:'gameReady'},{id:'game-start',num:5,label:'Game Start Call Method',runtimeEvent:'gameStart'},{id:'game-close',num:6,label:'Game Close Call Method',runtimeEvent:'gameClose'},{id:'file-handling',num:7,label:'File Handling Method',runtimeEvent:null},{id:'file-spec',num:8,label:'File Spec',runtimeEvent:null},{id:'storage',num:9,label:'Storage Requirements',runtimeEvent:null},{id:'code-exception',num:10,label:'Code Exception',runtimeEvent:'exception'}];
|
||||
const EVENT_TO_CHECK={};CHECKS.forEach(c=>{if(c.runtimeEvent)EVENT_TO_CHECK[c.runtimeEvent]=c.id;});
|
||||
const SDK_TYPE_TO_CHECK={game_ready:'game-ready',game_end:'game-end',game_start:'game-start',game_close:'game-close',install:'cta-method',outer_chain:'file-handling',base64:'file-handling',code:'code-exception'};
|
||||
const state={checks:{},seconds:0,muted:false,connected:false,timer:null,timeout:60,codeCleanAfter:5,hasSentNoRetry:false};
|
||||
CHECKS.forEach(c=>state.checks[c.id]={status:'pending',detail:'',runtimeStatus:c.runtimeEvent?'waiting':null,runtimeDetail:''});
|
||||
BOOT.checks.forEach(r=>{const ch=state.checks[r.id];if(ch){ch.status=r.status;ch.detail=r.detail;}});
|
||||
state.checks['code-exception'].status='pass';state.checks['code-exception'].runtimeStatus='monitoring';
|
||||
['cta-method','game-end','game-ready','game-start','game-close'].forEach(id=>{if(state.checks[id].status==='fail')state.checks[id].runtimeStatus=null;});
|
||||
const checksEl=document.getElementById('checksTab'),eventLog=document.getElementById('eventsTab'),adFrame=document.getElementById('adFrame'),muteBtn=document.getElementById('muteBtn');
|
||||
document.querySelectorAll('.tab').forEach(btn=>btn.addEventListener('click',()=>{document.querySelectorAll('.tab').forEach(b=>b.classList.remove('active'));document.querySelectorAll('.tab-content').forEach(p=>p.classList.remove('active'));btn.classList.add('active');document.getElementById(btn.dataset.tab).classList.add('active');}));
|
||||
function resetChecks(){if(state.timer)clearInterval(state.timer);state.seconds=0;state.connected=false;state.hasSentNoRetry=false;CHECKS.forEach(c=>state.checks[c.id]={status:'pending',detail:'',runtimeStatus:c.runtimeEvent?'waiting':null,runtimeDetail:''});BOOT.checks.forEach(r=>{const ch=state.checks[r.id];if(ch){ch.status=r.status;ch.detail=r.detail;}});state.checks['code-exception'].status='pass';state.checks['code-exception'].runtimeStatus='monitoring';['cta-method','game-end','game-ready','game-start','game-close'].forEach(id=>{if(state.checks[id].status==='fail')state.checks[id].runtimeStatus=null;});eventLog.innerHTML='';document.getElementById('bridge').textContent='Starting...';document.getElementById('timer').textContent=state.timeout+'s';render();state.timer=startRuntimeTimer();}
|
||||
function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
|
||||
function addLog(list,name,cls,msg){const d=new Date(),li=document.createElement('li');li.innerHTML='<span class="time">'+String(d.getMinutes()).padStart(2,'0')+':'+String(d.getSeconds()).padStart(2,'0')+'</span><span class="level '+(cls||'')+'">'+esc(name)+'</span><span class="msg">'+esc(msg||'')+'</span>';list.appendChild(li);list.scrollTop=list.scrollHeight;}
|
||||
function render(){checksEl.innerHTML=CHECKS.map(c=>{const ch=state.checks[c.id];let s=ch.status,d=ch.detail;if(ch.runtimeStatus==='waiting'&&ch.status==='pass'){s='waiting';d=ch.detail+' - awaiting runtime confirmation';}else if(ch.runtimeStatus==='monitoring'){s='waiting';d='Monitoring for exceptions...';}else if(ch.runtimeStatus==='pass'){s='pass';d=(ch.detail||'')+(ch.runtimeDetail?' - '+ch.runtimeDetail:'');}else if(ch.runtimeStatus==='fail'){s='fail';d=ch.runtimeDetail||ch.detail;}else if(ch.runtimeStatus==='timeout'&&ch.status==='pass'){s='warn';d=ch.detail+' - not fired within '+state.timeout+'s';}else if(ch.runtimeStatus==='clean'){s='pass';d='No exceptions detected';}return '<li class="check"><span class="num">'+c.num+'</span><span><div class="label">'+esc(c.label)+' <span class="badge '+s+'">'+s.toUpperCase()+'</span></div><div class="detail">'+esc(d||'')+'</div></span></li>';}).join('');}
|
||||
function sdkCheck(type,value,msg){const id=SDK_TYPE_TO_CHECK[type],ch=state.checks[id];if(!ch)return;const pass=value===1||value===true;if(type==='code'){ch.status='fail';ch.runtimeStatus='fail';ch.runtimeDetail=msg||'JS error detected';addLog(eventLog,'code error','fail',ch.runtimeDetail);}else if(type==='outer_chain'){ch.runtimeStatus=pass?'pass':'fail';ch.runtimeDetail=pass?'No external resources at runtime':'External resources detected at runtime';addLog(eventLog,type,pass?'pass':'fail',ch.runtimeDetail);}else if(type==='base64'){if(!pass){ch.status='fail';ch.runtimeStatus='fail';ch.runtimeDetail='Non-JS/HTML assets must be base64';addLog(eventLog,type,'warn',ch.runtimeDetail);}}else{if(pass&&(ch.runtimeStatus==='waiting'||ch.runtimeStatus===null)){ch.runtimeStatus='pass';ch.runtimeDetail='Confirmed by SDK at '+state.seconds+'s';addLog(eventLog,type,'pass','');}else if(!pass&&(ch.runtimeStatus==='waiting'||ch.runtimeStatus===null)){ch.runtimeStatus='fail';ch.runtimeDetail=type+' not defined';addLog(eventLog,type,'warn',ch.runtimeDetail);}}render();done();}
|
||||
function inferAll(text){const s=String(text||''),out=[];if(/(^|[^a-z])gameReady([^a-z]|$)|previewer:gameReady|game_ready|\bDISPLAYED\b|\bLOADED\b/i.test(s))out.push('gameReady');if(/(^|[^a-z])gameStart([^a-z]|$)|previewer:gameStart|game_start|\bCHALLENGE_STARTED\b/i.test(s))out.push('gameStart');if(/(^|[^a-z])gameEnd([^a-z]|$)|previewer:gameEnd|game_end|\bCHALLENGE_SOLVED\b|\bENDCARD_SHOWN\b/i.test(s))out.push('gameEnd');if(/(^|[^a-z])gameClose([^a-z]|$)|previewer:gameClose|game_close/i.test(s))out.push('gameClose');if(/(^|[^a-z])install([^a-z]|$)|previewer:install|\bCTA_CLICKED\b/i.test(s))out.push('install');return out;}
|
||||
function handle(msg){const name=msg.name,extra=msg.extra||{};if(name==='__ping__'){state.connected=true;document.getElementById('bridge').textContent='Monitor connected';addLog(eventLog,'Monitor connected','pass','');return;}if(name==='sdkCheck'){sdkCheck(extra.type,extra.value,extra.msg||'');return;}if(name==='console'){inferAll(extra.text||'').forEach(ev=>handle({name:ev,extra:{source:'console'}}));return;}if(name==='diagnostic'){addLog(eventLog,extra.label||'diagnostic','warn',extra.detail||'');return;}if(name==='audioContext'){addLog(eventLog,'Audio captured','pass',extra.routed?'master gain ready':'context only');return;}if(name==='exception'){const ch=state.checks['code-exception'];ch.status='fail';ch.runtimeStatus='fail';ch.runtimeDetail=extra.message||'Unhandled exception';addLog(eventLog,'exception','fail',ch.runtimeDetail);render();done();return;}const id=EVENT_TO_CHECK[name];if(id){const ch=state.checks[id];if(ch.runtimeStatus==='waiting'||ch.runtimeStatus===null){ch.runtimeStatus='pass';ch.runtimeDetail=(extra.source==='console'?'Seen in console at ':'Fired at ')+state.seconds+'s';addLog(eventLog,name,'pass',extra.source==='console'?'from console':'');render();done();}}}
|
||||
window.__hplMtgNotify=(name,extra)=>handle({name,extra:extra||{}});
|
||||
window.addEventListener('message',e=>{const m=e.data;if(!m)return;if(m.type==='lifecycle')handle(m);else if(typeof m.type==='string'&&m.type.startsWith('previewer:')){const map={'previewer:gameReady':'gameReady','previewer:gameStart':'gameStart','previewer:install':'install','previewer:gameEnd':'gameEnd','previewer:gameClose':'gameClose'};if(map[m.type])handle({name:map[m.type],extra:{}});if(m.type==='previewer:review'&&m.data&&Array.isArray(m.data.reviewResult)){if(!state.hasSentNoRetry){state.hasSentNoRetry=true;try{adFrame.contentWindow.postMessage({type:'previewer:review',data:{reviewResult:{no_game_retry:true}}},'*');}catch(e){}}m.data.reviewResult.forEach(r=>sdkCheck(r.type,r.value,r.msg||''));}}});
|
||||
function applyMute(){try{const cw=adFrame.contentWindow;if(!cw)return;cw.__hplMuted=state.muted;if(typeof cw.__hplApplyMutedState==='function')cw.__hplApplyMutedState();(cw.__hplMasterGains||[]).forEach(g=>{try{if(g&&g.gain)g.gain.value=state.muted?0:1;}catch(e){}});(cw.__hplMediaElements||[]).forEach(el=>{try{el.muted=state.muted;el.volume=state.muted?0:1;}catch(e){}});}catch(e){}}
|
||||
function renderMuteButton(){muteBtn.innerHTML=state.muted?'🔈':'🔇';muteBtn.title=state.muted?'Unmute':'Mute';muteBtn.setAttribute('aria-label',state.muted?'Unmute':'Mute');muteBtn.setAttribute('aria-pressed',state.muted?'true':'false');}
|
||||
muteBtn.onclick=()=>{state.muted=!state.muted;renderMuteButton();applyMute();addLog(eventLog,state.muted?'Muted':'Unmuted','pass','');};
|
||||
document.getElementById('reloadBtn').onclick=()=>{resetChecks();document.getElementById('bridge').textContent='Reloading...';adFrame.src='/mintegral/ad?preview=true&loading=0&review=1&t='+Date.now();};
|
||||
['pointerdown','click','touchstart','keydown'].forEach(evt=>window.addEventListener(evt,()=>setTimeout(applyMute,0),true));
|
||||
function markCodeClean(){const ch=state.checks['code-exception'];if(ch&&ch.runtimeStatus==='monitoring'){ch.runtimeStatus='clean';ch.runtimeDetail='No exceptions detected';addLog(eventLog,'code clean','pass',ch.runtimeDetail);render();}}
|
||||
function done(){const othersDone=CHECKS.every(c=>c.id==='code-exception'||!c.runtimeEvent||state.checks[c.id].runtimeStatus!=='waiting');if(othersDone)markCodeClean();if(CHECKS.every(c=>!c.runtimeEvent||!['waiting','monitoring'].includes(state.checks[c.id].runtimeStatus))){clearInterval(state.timer);document.getElementById('timer').textContent='Runtime complete';}}
|
||||
function startRuntimeTimer(){return setInterval(()=>{state.seconds++;document.getElementById('timer').textContent=Math.max(0,state.timeout-state.seconds)+'s';if(state.seconds===2&&!state.connected){document.getElementById('bridge').textContent='Monitor not connected';addLog(eventLog,'Monitor not connected','fail','Check browser DevTools console for blocked inline script/CSP errors.');}if(state.seconds>=state.codeCleanAfter)markCodeClean();done();if(state.seconds>=state.timeout){clearInterval(state.timer);CHECKS.forEach(c=>{const ch=state.checks[c.id];if(ch.runtimeStatus==='waiting')ch.runtimeStatus='timeout';if(ch.runtimeStatus==='monitoring'){ch.runtimeStatus='clean';ch.runtimeDetail='No exceptions detected';}});render();document.getElementById('timer').textContent='Runtime complete';}},1000);}
|
||||
state.timer=startRuntimeTimer();renderMuteButton();render();
|
||||
`
|
||||
|
||||
const mintegralMonitorScript = `
|
||||
(function(){
|
||||
window.__hplMtgEvents=window.__hplMtgEvents||[];
|
||||
function notify(n,extra){window.__hplMtgEvents.push({name:n,extra:extra||{}});try{if(typeof parent.__hplMtgNotify==='function')parent.__hplMtgNotify(n,extra||{});}catch(e){}try{parent.postMessage({type:'lifecycle',name:n,extra:extra||{}},'*');}catch(e){}}
|
||||
notify('__ping__');
|
||||
function applyMutedState(){var muted=!!window.__hplMuted;try{(window.__hplMasterGains||[]).forEach(function(g){try{if(g&&g.gain)g.gain.value=muted?0:1;}catch(e){}});(window.__hplMediaElements||[]).forEach(function(el){try{el.muted=muted;el.volume=muted?0:1;}catch(e){}});}catch(e){}}
|
||||
window.__hplApplyMutedState=applyMutedState;
|
||||
try{(function(){var OrigAC=window.AudioContext||window.webkitAudioContext;if(!OrigAC||OrigAC.__hplProxy)return;window.__hplAudioContexts=[];window.__hplMasterGains=[];window.__hplAudioDestinations=[];function ACProxy(opts){var ctx=arguments.length?new OrigAC(opts):new OrigAC();var dest=ctx.destination,gain=null;try{gain=ctx.createGain();gain.gain.value=window.__hplMuted?0:1;gain.connect(dest);if(window.AudioNode&&window.AudioNode.prototype&&!window.AudioNode.prototype.connect.__hpl){var origConnect=window.AudioNode.prototype.connect;var connectProxy=function(target){var args=Array.prototype.slice.call(arguments);try{var idx=window.__hplAudioDestinations.indexOf(args[0]);if(idx>=0&&window.__hplMasterGains[idx])args[0]=window.__hplMasterGains[idx];}catch(e){}return origConnect.apply(this,args);};connectProxy.__hpl=true;window.AudioNode.prototype.connect=connectProxy;}}catch(e){}window.__hplAudioContexts.push(ctx);if(gain)window.__hplMasterGains.push(gain);if(gain)window.__hplAudioDestinations.push(dest);notify('audioContext',{state:ctx.state||'',routed:!!gain});return ctx;}ACProxy.prototype=OrigAC.prototype;Object.setPrototypeOf&&Object.setPrototypeOf(ACProxy,OrigAC);ACProxy.__hplProxy=true;window.AudioContext=ACProxy;window.webkitAudioContext=ACProxy;})();}catch(e){}
|
||||
try{window.__hplMediaElements=[];var mediaPlay=window.HTMLMediaElement&&window.HTMLMediaElement.prototype&&window.HTMLMediaElement.prototype.play;if(mediaPlay&&!mediaPlay.__hpl){var playProxy=function(){window.__hplMediaElements.push(this);applyMutedState();notify('audioContext',{state:'media-play',routed:true});return mediaPlay.apply(this,arguments);};playProxy.__hpl=true;window.HTMLMediaElement.prototype.play=playProxy;}}catch(e){}
|
||||
function patchMwInit(obj){if(!obj||obj.__hplMwPatched)return;obj.__hplMwPatched=true;obj._hasReview=function(){return true;};obj._isPreview=function(){return true;};var _oe=obj.emitCheckData;if(typeof _oe==='function'){obj.emitCheckData=function(d){_oe.call(obj,d);if(d&&d.type)notify('sdkCheck',{type:d.type,value:d.value,msg:d.msg||''});};}}
|
||||
var _mwInit=window.MW_INIT||null;patchMwInit(_mwInit);try{Object.defineProperty(window,'MW_INIT',{configurable:true,enumerable:true,get:function(){return _mwInit;},set:function(obj){_mwInit=obj;patchMwInit(obj);}});}catch(e){}
|
||||
var _owp=window.postMessage;window.postMessage=function(data,origin,transfer){if(data&&typeof data.type==='string'){var pm={'previewer:gameReady':'gameReady','previewer:gameStart':'gameStart','previewer:gameEnd':'gameEnd','previewer:gameClose':'gameClose','previewer:install':'install','previewer:gameRetry':'gameRetry'};if(pm[data.type])notify(pm[data.type]);}return _owp.apply(this,arguments);};
|
||||
function mkAccess(name,onCall){var existing,_v;try{existing=window[name];Object.defineProperty(window,name,{configurable:true,enumerable:true,get:function(){return _v;},set:function(fn){var orig=fn;if(typeof orig==='function'){var w=function(){onCall();return orig.apply(this,arguments);};w.__hpl=true;_v=w;}else{_v=orig;}}});if(typeof existing==='function')window[name]=existing;}catch(e){}}
|
||||
var _lh={gameReady:function(){notify('gameReady');setTimeout(function(){notify('sdkCheck',{type:'game_start',value:typeof window.gameStart==='function'?1:0});notify('sdkCheck',{type:'game_close',value:typeof window.gameClose==='function'?1:0});},100);},gameEnd:function(){notify('sdkCheck',{type:'game_end',value:1});},gameStart:function(){notify('sdkCheck',{type:'game_start',value:1});},gameClose:function(){notify('sdkCheck',{type:'game_close',value:1});},install:function(){notify('sdkCheck',{type:'install',value:1});}};
|
||||
mkAccess('gameReady',_lh.gameReady);mkAccess('gameEnd',_lh.gameEnd);mkAccess('gameStart',_lh.gameStart);mkAccess('gameClose',_lh.gameClose);if(typeof window.install!=='function'){var _installW=function(){_lh.install();};_installW.__hpl=true;window.install=_installW;}
|
||||
window.addEventListener('load',function(){Object.keys(_lh).forEach(function(name){var fn=window[name];if(typeof fn==='function'&&!fn.__hpl){var orig=fn,h=_lh[name];var w=function(){h();return orig.apply(this,arguments);};w.__hpl=true;try{window[name]=w;}catch(e){}}else if(typeof fn!=='function'){var ww=function(){_lh[name]();};ww.__hpl=true;try{window[name]=ww;}catch(e){}}});},true);
|
||||
window.onerror=function(msg,src,line){notify('exception',{message:String(msg),line:line||0,source:src||''});return false;};
|
||||
window.addEventListener('unhandledrejection',function(e){notify('exception',{message:String(e&&e.reason||'Unhandled promise rejection')});});
|
||||
})();
|
||||
`
|
||||
|
||||
var _ = html.EscapeString
|
||||
Reference in New Issue
Block a user