package main import ( "encoding/json" "net/http" "os" "path/filepath" "regexp" "strings" ) type mraidIssue struct { ID string `json:"id"` Severity string `json:"severity"` Title string `json:"title"` Detail string `json:"detail"` Line int `json:"line,omitempty"` Reference string `json:"reference"` } type mraidResult struct { File string `json:"file"` OK bool `json:"ok"` Issues []mraidIssue `json:"issues"` } type mraidRule struct { ID string Severity string Title string Detail string Reference string Test func(mraidContext, mraidRule) *mraidIssue } type mraidContext struct { HTML string ScriptText string MraidCallLines map[string]int } func MraidPage(w http.ResponseWriter, r *http.Request) { body := `

MRAID Checker

Static checks for AppLovin, ironSource, and Unity folder HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.

Inputs

` w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(Page("/mraid", "MRAID Checker", body))) } func MraidPickFolder(w http.ResponseWriter, r *http.Request) { p := PickFolder("Select folder to scan", "") if p == "" { writeJSON(w, map[string]any{"paths": []string{}}) return } files := []string{} for _, f := range collectHTMLFiles(p) { if isInSupportedMraidFolderPath(f) { files = append(files, f) } } writeJSON(w, map[string]any{"paths": files}) } func MraidPickFiles(w http.ResponseWriter, r *http.Request) { paths := PickFiles("Select HTML files", []FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}}, true, "") writeJSON(w, map[string]any{"paths": paths}) } func MraidScan(w http.ResponseWriter, r *http.Request) { var req struct { Folder string `json:"folder"` Files []string `json:"files"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]any{"results": []mraidResult{}, "error": err.Error()}) return } var targets []string var baseDir string skipped := 0 skippedReason := "" noTargetsError := "No HTML files were found." if len(req.Files) > 0 { var existing []string for _, f := range req.Files { if fileExists(f) { existing = append(existing, f) } } targets = existing skipped = len(req.Files) - len(existing) skippedReason = "missing file(s)." noTargetsError = "No selected HTML files were found." if len(targets) > 0 { baseDir = commonBaseDir(targets) } } else if req.Folder != "" && fileExists(req.Folder) { all := collectHTMLFiles(req.Folder) for _, f := range all { if isInSupportedMraidFolderPath(f) { targets = append(targets, f) } } skipped = len(all) - len(targets) skippedReason = "HTML file(s) outside AppLovin/ironSource/Unity folders." noTargetsError = "No AppLovin, ironSource, or Unity folder HTML files were found." baseDir = req.Folder } else { writeJSON(w, map[string]any{"results": []mraidResult{}, "error": "Pick a folder or files first"}) return } if len(targets) == 0 { writeJSON(w, map[string]any{"results": []mraidResult{}, "skipped": skipped, "skippedReason": skippedReason, "error": noTargetsError}) return } results := []mraidResult{} for _, file := range targets { data, err := os.ReadFile(file) if err != nil { continue } issues := checkMraid(string(data)) display := file if baseDir != "" { if rel, err := filepath.Rel(baseDir, file); err == nil && rel != "" { display = rel } else { display = filepath.Base(file) } } results = append(results, mraidResult{File: display, OK: len(issues) == 0, Issues: issues}) } writeJSON(w, map[string]any{"results": results, "skipped": skipped, "skippedReason": skippedReason}) } var ( mraidScriptRx = regexp.MustCompile(`(?is)]*>(.*?)`) mraidCallRx = regexp.MustCompile(`(?i)\bmraid\s*\.\s*([a-zA-Z_$][\w$]*)\s*\(`) mraidReferenceRx = regexp.MustCompile(`(?i)\bmraid\b`) mraidWindowOpenRx = regexp.MustCompile(`(?i)\bwindow\s*\.\s*open\s*\(`) mraidLocationNavRx = regexp.MustCompile(`(?i)\blocation\s*\.\s*(href|assign|replace)\b`) mraidCloseUiRx = regexp.MustCompile(`(?i)\b(close|dismiss|skip)\b`) mraidLoadingLeftRx = regexp.MustCompile(`(?i)mraid\s*\.\s*getState\s*\(\s*\)\s*={0,2}\s*['"]loading['"]`) mraidLoadingRightRx = regexp.MustCompile(`(?i)['"]loading['"]\s*={0,2}\s*mraid\s*\.\s*getState\s*\(\s*\)`) ) const mraidReference = "MRAID 3.0 specification and MRAID 3.0 Best Practices Guide in mraidDocuments/" func isInSupportedMraidFolderPath(filePath string) bool { supported := map[string]bool{ "al": true, "applovin": true, "app-lovin": true, "is": true, "ironsource": true, "iron-source": true, "iron_source": true, "un": true, "unity": true, "unityads": true, "unity-ads": true, "unity_ads": true, } parts := strings.FieldsFunc(strings.ToLower(filepath.Dir(filePath)), func(r rune) bool { return r == '/' || r == '\\' }) for _, part := range parts { if supported[part] { return true } } return false } func checkMraid(htmlSrc string) []mraidIssue { ctx := mraidContext{ HTML: htmlSrc, ScriptText: extractMraidScriptText(htmlSrc), MraidCallLines: collectMraidCallLines(htmlSrc), } var out []mraidIssue for _, rule := range mraidRules { if found := rule.Test(ctx, rule); found != nil { out = append(out, *found) } } if out == nil { out = []mraidIssue{} } return out } func extractMraidScriptText(htmlSrc string) string { var parts []string for _, m := range mraidScriptRx.FindAllStringSubmatch(htmlSrc, -1) { parts = append(parts, m[1]) } return strings.Join(parts, "\n") } func collectMraidCallLines(htmlSrc string) map[string]int { out := map[string]int{} for idx, line := range strings.Split(htmlSrc, "\n") { for _, m := range mraidCallRx.FindAllStringSubmatch(line, -1) { if _, ok := out[m[1]]; !ok { out[m[1]] = idx + 1 } } } return out } func mraidHasReference(ctx mraidContext) bool { return mraidReferenceRx.MatchString(ctx.HTML) } func mraidHasCall(ctx mraidContext, method string) bool { _, ok := ctx.MraidCallLines[method] return ok } func mraidLine(ctx mraidContext, method string) int { return ctx.MraidCallLines[method] } func mraidHasEventListener(ctx mraidContext, eventName string) bool { rx := regexp.MustCompile(`(?i)mraid\s*\.\s*addEventListener\s*\(\s*['"]` + regexp.QuoteMeta(eventName) + `['"]`) return rx.MatchString(ctx.ScriptText) } func mraidHasSupportCheck(ctx mraidContext, feature string) bool { rx := regexp.MustCompile(`(?i)mraid\s*\.\s*supports\s*\(\s*['"]` + regexp.QuoteMeta(feature) + `['"]`) return rx.MatchString(ctx.ScriptText) } func mraidHasReadyGate(ctx mraidContext) bool { return mraidHasEventListener(ctx, "ready") || mraidLoadingLeftRx.MatchString(ctx.ScriptText) || mraidLoadingRightRx.MatchString(ctx.ScriptText) } func mraidIssueFrom(rule mraidRule, line int) *mraidIssue { found := mraidIssue{ID: rule.ID, Severity: rule.Severity, Title: rule.Title, Detail: rule.Detail, Reference: rule.Reference} if line > 0 { found.Line = line } return &found } var mraidRules = []mraidRule{ {ID: "mraid-reference", Severity: "requirement", Title: "MRAID API reference not found", Detail: "Playable HTML should use the injected mraid object when it depends on an MRAID container.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasReference(ctx) { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "ready-gate", Severity: "requirement", Title: "MRAID ready state is not guarded", Detail: "Gate ad startup behind mraid.ready, or call startup immediately only when mraid.getState() is not loading.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if !mraidHasReference(ctx) || mraidHasReadyGate(ctx) { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "error-listener", Severity: "best-practice", Title: "No MRAID error listener found", Detail: "Listen for mraid error events so unsupported or failed MRAID calls can be logged or handled gracefully.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if !mraidHasReference(ctx) || mraidHasEventListener(ctx, "error") { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "viewability", Severity: "best-practice", Title: "Viewability handling not found", Detail: "Use mraid.isViewable() and/or viewableChange before starting animation, audio, video, or timers.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if !mraidHasReference(ctx) || mraidHasCall(ctx, "isViewable") || mraidHasEventListener(ctx, "viewableChange") || mraidHasEventListener(ctx, "exposureChange") { return nil } return mraidIssueFrom(rule, 0) }}, {ID: "open-api", Severity: "requirement", Title: "Browser navigation used instead of mraid.open", Detail: "Use mraid.open(url) for click-through navigation inside an MRAID ad container.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if (mraidWindowOpenRx.MatchString(ctx.ScriptText) || mraidLocationNavRx.MatchString(ctx.ScriptText)) && !mraidHasCall(ctx, "open") { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "close-api", Severity: "best-practice", Title: "No mraid.close call found", Detail: "If the creative renders its own close control, wire that control to mraid.close().", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidCloseUiRx.MatchString(ctx.HTML) && !mraidHasCall(ctx, "close") { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "expand-support", Severity: "requirement", Title: "mraid.expand used without supports check", Detail: "Check mraid.supports(\"expand\") before using expand behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "expand") && !mraidHasSupportCheck(ctx, "expand") { return mraidIssueFrom(rule, mraidLine(ctx, "expand")) } return nil }}, {ID: "resize-support", Severity: "requirement", Title: "mraid.resize used without supports check", Detail: "Check mraid.supports(\"resize\") before using resize behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "resize") && !mraidHasSupportCheck(ctx, "resize") { return mraidIssueFrom(rule, mraidLine(ctx, "resize")) } return nil }}, {ID: "orientation-support", Severity: "best-practice", Title: "Orientation properties set without supports check", Detail: "Check mraid.supports(\"orientation\") before requiring orientation behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { if mraidHasCall(ctx, "setOrientationProperties") && !mraidHasSupportCheck(ctx, "orientation") { return mraidIssueFrom(rule, mraidLine(ctx, "setOrientationProperties")) } return nil }}, {ID: "size-change", Severity: "best-practice", Title: "No sizeChange listener found", Detail: "Listen for sizeChange when layout depends on container size, resize, expand, or orientation behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { depends := mraidHasCall(ctx, "getMaxSize") || mraidHasCall(ctx, "getScreenSize") || mraidHasCall(ctx, "resize") || mraidHasCall(ctx, "expand") || mraidHasCall(ctx, "setOrientationProperties") if depends && !mraidHasEventListener(ctx, "sizeChange") { return mraidIssueFrom(rule, 0) } return nil }}, {ID: "state-change", Severity: "best-practice", Title: "No stateChange listener found", Detail: "Listen for stateChange when using expand, resize, close, or other state-changing container behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue { changes := mraidHasCall(ctx, "expand") || mraidHasCall(ctx, "resize") || mraidHasCall(ctx, "close") if changes && !mraidHasEventListener(ctx, "stateChange") { return mraidIssueFrom(rule, 0) } return nil }}, }