mraid checker

This commit is contained in:
2026-05-28 11:38:41 +08:00
parent afbf504957
commit 274bafe1cb
7 changed files with 618 additions and 52 deletions

View File

@@ -34,6 +34,7 @@ type mraidRule struct {
}
type mraidContext struct {
FilePath string
HTML string
ScriptText string
MraidCallLines map[string]int
@@ -43,7 +44,7 @@ func MraidPage(w http.ResponseWriter, r *http.Request) {
body := `
<header class="tool-header">
<h2 class="tool-title">MRAID Checker</h2>
<p class="tool-description">Static checks for AppLovin, ironSource, and Unity folder HTML builds based on MRAID 3.0 requirements and best practices from mraidDocuments/.</p>
<p class="tool-description">Static checks for AppLovin, ironSource, and Unity HTML builds based on GUIDE.md plus the MRAID 3.0 specification and Best Practices Guide.</p>
</header>
<section class="tool-panel">
<div class="panel-header"><h3 class="panel-title">Inputs</h3></div>
@@ -271,7 +272,7 @@ func MraidScan(w http.ResponseWriter, r *http.Request) {
if err != nil {
continue
}
issues := checkMraid(string(data))
issues := checkMraid(string(data), file)
display := file
if baseDir != "" {
if rel, err := filepath.Rel(baseDir, file); err == nil && rel != "" {
@@ -286,17 +287,33 @@ func MraidScan(w http.ResponseWriter, r *http.Request) {
}
var (
mraidScriptRx = regexp.MustCompile(`(?is)<script\b[^>]*>(.*?)</script>`)
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*\)`)
mraidScriptRx = regexp.MustCompile(`(?is)<script\b[^>]*>(.*?)</script>`)
mraidCallRx = regexp.MustCompile(`(?i)\bmraid\s*\.\s*([a-zA-Z_$][\w$]*)\s*\(`)
mraidReferenceRx = regexp.MustCompile(`(?i)\bmraid\b`)
mraidJsRx = regexp.MustCompile(`(?i)\bmraid\.js\b`)
mraidViewportRx = regexp.MustCompile(`(?i)<meta\b[^>]*\bname\s*=\s*['"]viewport['"][^>]*>`)
mraidWindowOpenRx = regexp.MustCompile(`(?i)\bwindow\s*\.\s*open\s*\(`)
mraidLocationNavRx = regexp.MustCompile(`(?i)\blocation\s*\.\s*(href|assign|replace)\b`)
mraidHyperlinkRx = regexp.MustCompile(`(?i)<a\b[^>]*\bhref\s*=`)
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*\)`)
mraidDomReadyRx = regexp.MustCompile(`(?i)document\s*\.\s*readyState|DOMContentLoaded|\bwindow\s*\.\s*(addEventListener\s*\(\s*['"]load['"]|onload\b)`)
mraidTypeofGuardRx = regexp.MustCompile(`(?i)typeof\s+(window\s*\.\s*)?mraid\s*!={1,2}\s*['"]undefined['"]`)
mraidReadyTimeoutRx = regexp.MustCompile(`(?is)setTimeout\s*\(.*mraid|mraid.*setTimeout\s*\(`)
mraidLateDetectionRx = regexp.MustCompile(`(?i)(setInterval\s*\(|setTimeout\s*\(|requestAnimationFrame\s*\().*(window\s*\.\s*mraid|\bmraid\b)|(window\s*\.\s*mraid|\bmraid\b).*(setInterval\s*\(|setTimeout\s*\(|requestAnimationFrame\s*\()`)
mraidOpenGuardRx = regexp.MustCompile(`(?i)typeof\s+(window\s*\.\s*)?mraid\s*\.\s*open\s*={2,3}\s*['"]function['"]|typeof\s+(window\s*\.\s*)?mraid\s*\.\s*open\s*!={1,2}\s*['"]undefined['"]`)
mraidOpenLoadingGuardRx = regexp.MustCompile(`(?i)mraid\s*\.\s*getState\s*\(\s*\)\s*!={1,2}\s*['"]loading['"]|['"]loading['"]\s*!={1,2}\s*mraid\s*\.\s*getState\s*\(\s*\)|typeof\s+(window\s*\.\s*)?mraid\s*\.\s*getState\s*!={1,2}\s*['"]function['"]`)
mraidVariableLoadingGuardRx = regexp.MustCompile(`(?i)\b\w+\s*!={1,2}\s*['"]loading['"]|['"]loading['"]\s*!={1,2}\s*\w+\b`)
mraidLoadingFallbackRx = regexp.MustCompile(`(?is)\b\w+\s*={2,3}\s*['"]loading['"].{0,500}\bwindow\s*\.\s*open\s*\(`)
mraidAudioSafeRx = regexp.MustCompile(`(?i)typeof\s+\w+\s*={2,3}\s*['"]number['"]|!=\s*null|!==\s*null`)
mraidTwoPartExpandRx = regexp.MustCompile(`(?i)mraid\s*\.\s*expand\s*\(\s*[^)\s]`)
mraidVideoOpenRx = regexp.MustCompile(`(?i)mraid\s*\.\s*open\s*\(\s*[^)]*\.(mp4|m4v|mov|webm|avi|m3u8)(['"` + "`" + `?#)]|$)`)
mraidRejectedScriptAttrsRx = regexp.MustCompile(`(?i)<script\b[^>]*(\btype\s*=\s*['"]module['"]|\bcrossorigin\b)`)
mraidConsoleErrorRx = regexp.MustCompile(`(?i)\bconsole\s*\.\s*error\s*\(`)
)
const mraidReference = "MRAID 3.0 specification and MRAID 3.0 Best Practices Guide in mraidDocuments/"
const mraidReference = "GUIDE.md, MRAID 3.0 specification, and MRAID 3.0 Best Practices Guide."
func isInSupportedMraidFolderPath(filePath string) bool {
supported := map[string]bool{
@@ -324,8 +341,13 @@ func isInSupportedMraidFolderPath(filePath string) bool {
return false
}
func checkMraid(htmlSrc string) []mraidIssue {
func checkMraid(htmlSrc string, filePath ...string) []mraidIssue {
sourcePath := ""
if len(filePath) > 0 {
sourcePath = filePath[0]
}
ctx := mraidContext{
FilePath: sourcePath,
HTML: htmlSrc,
ScriptText: extractMraidScriptText(htmlSrc),
MraidCallLines: collectMraidCallLines(htmlSrc),
@@ -363,12 +385,31 @@ func collectMraidCallLines(htmlSrc string) map[string]int {
}
func mraidHasReference(ctx mraidContext) bool { return mraidReferenceRx.MatchString(ctx.HTML) }
func mraidCountJSReferences(ctx mraidContext) int {
return len(mraidJsRx.FindAllStringIndex(ctx.HTML, -1))
}
func mraidHasViewport(ctx mraidContext) bool { return mraidViewportRx.MatchString(ctx.HTML) }
func mraidIsNetworkTarget(ctx mraidContext) bool {
if ctx.FilePath == "" {
return true
}
name := strings.ToLower(filepath.Base(ctx.FilePath))
return isInSupportedMraidFolderPath(ctx.FilePath) ||
strings.HasSuffix(name, "_al.html") || strings.HasSuffix(name, "_is.html") || strings.HasSuffix(name, "_un.html") ||
strings.HasSuffix(name, "_al.htm") || strings.HasSuffix(name, "_is.htm") || strings.HasSuffix(name, "_un.htm")
}
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 mraidHasFunctionGuard(ctx mraidContext, method string) bool {
escaped := regexp.QuoteMeta(method)
rx := regexp.MustCompile(`(?i)typeof\s+(window\s*\.\s*)?mraid\s*\.\s*` + escaped + `\s*={2,3}\s*['"]function['"]|typeof\s+(window\s*\.\s*)?mraid\s*\.\s*` + escaped + `\s*!={1,2}\s*['"]undefined['"]`)
return rx.MatchString(ctx.ScriptText)
}
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)
@@ -383,6 +424,38 @@ func mraidHasReadyGate(ctx mraidContext) bool {
return mraidHasEventListener(ctx, "ready") || mraidLoadingLeftRx.MatchString(ctx.ScriptText) || mraidLoadingRightRx.MatchString(ctx.ScriptText)
}
func mraidHasDomReadyGate(ctx mraidContext) bool { return mraidDomReadyRx.MatchString(ctx.ScriptText) }
func mraidHasReadyStateFallback(ctx mraidContext) bool {
return mraidHasEventListener(ctx, "ready") && mraidHasCall(ctx, "getState")
}
func mraidHasTypeofGuard(ctx mraidContext) bool {
return mraidTypeofGuardRx.MatchString(ctx.ScriptText)
}
func mraidHasReadyTimeout(ctx mraidContext) bool {
return mraidReadyTimeoutRx.MatchString(ctx.ScriptText)
}
func mraidHasLateDetection(ctx mraidContext) bool {
return mraidLateDetectionRx.MatchString(ctx.ScriptText)
}
func mraidHasOpenGuard(ctx mraidContext) bool { return mraidOpenGuardRx.MatchString(ctx.ScriptText) }
func mraidHasOpenLoadingGuard(ctx mraidContext) bool {
return mraidOpenLoadingGuardRx.MatchString(ctx.ScriptText) ||
(mraidHasCall(ctx, "getState") && mraidVariableLoadingGuardRx.MatchString(ctx.ScriptText)) ||
(mraidHasCall(ctx, "getState") && mraidLoadingFallbackRx.MatchString(ctx.ScriptText))
}
func mraidUsesBrowserNavigation(ctx mraidContext) bool {
return mraidWindowOpenRx.MatchString(ctx.ScriptText)
}
func mraidUsesLocationNavigation(ctx mraidContext) bool {
return mraidLocationNavRx.MatchString(ctx.ScriptText)
}
func mraidHasAudioVolumeSafeHandling(ctx mraidContext) bool {
return mraidHasEventListener(ctx, "audioVolumeChange") && mraidAudioSafeRx.MatchString(ctx.ScriptText)
}
func mraidHasLifecycleSignal(ctx mraidContext, name string) bool {
return regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(name) + `\b`).MatchString(ctx.HTML)
}
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 {
@@ -392,56 +465,171 @@ func mraidIssueFrom(rule mraidRule, line int) *mraidIssue {
}
var mraidRules = []mraidRule{
{ID: "mraid-js-missing", Severity: "requirement", Title: "mraid.js reference not found", Detail: "MRAID creatives for AppLovin, ironSource, and Unity must request mraid.js early in the generated HTML.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidIsNetworkTarget(ctx) && mraidCountJSReferences(ctx) == 0 {
return mraidIssueFrom(rule, 0)
}
return nil
}},
{ID: "mraid-js-duplicate", Severity: "requirement", Title: "Multiple mraid.js references found", Detail: "Request mraid.js exactly once; duplicate references can inject multiple MRAID libraries and slow or destabilize the ad.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidCountJSReferences(ctx) > 1 {
return mraidIssueFrom(rule, 0)
}
return nil
}},
{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: "viewport-meta", Severity: "best-practice", Title: "Viewport meta tag not found", Detail: "MRAID creatives should include a mobile viewport meta tag so the ad scales predictably on mobile WebViews.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasViewport(ctx) {
return nil
}
return mraidIssueFrom(rule, 0)
}},
{ID: "mraid-typeof-guard", Severity: "requirement", Title: "MRAID object is not guarded", Detail: "Guard MRAID access with typeof mraid !== \"undefined\" or an equivalent window.mraid guard before making MRAID calls.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasReference(ctx) && !mraidHasTypeofGuard(ctx) {
return mraidIssueFrom(rule, 0)
}
return nil
}},
{ID: "dom-ready-gate", Severity: "requirement", Title: "DOM readiness is not guarded", Detail: "MRAID startup should wait for DOM readiness as well as MRAID readiness before building or binding rich media content.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if !mraidHasReference(ctx) || mraidHasDomReadyGate(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: "ready-state-fallback", Severity: "requirement", Title: "MRAID ready listener/state fallback pair not found", Detail: "Use both mraid.addEventListener(\"ready\", ...) and mraid.getState() so startup works when ready fires before the listener attaches.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if !mraidHasReference(ctx) || mraidHasReadyStateFallback(ctx) {
return nil
}
return mraidIssueFrom(rule, 0)
}},
{ID: "ready-timeout-fallback", Severity: "best-practice", Title: "MRAID ready timeout fallback not found", Detail: "A timeout fallback prevents startup hangs when a container never fires ready.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if !mraidHasReference(ctx) || mraidHasReadyTimeout(ctx) {
return nil
}
return mraidIssueFrom(rule, 0)
}},
{ID: "late-mraid-detection", Severity: "best-practice", Title: "Late MRAID injection detection not found", Detail: "Poll or retry briefly for late window.mraid injection before deciding the creative is running without MRAID.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if !mraidHasReference(ctx) || mraidHasLateDetection(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") {
{ID: "viewability", Severity: "best-practice", Title: "MRAID viewability fallback chain not found", Detail: "Use exposureChange for MRAID 3.0 and keep viewableChange or isViewable() as MRAID 2.0 fallback before starting animation, audio, video, or timers.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
hasVisibility := mraidHasEventListener(ctx, "exposureChange") && (mraidHasCall(ctx, "isViewable") || mraidHasEventListener(ctx, "viewableChange"))
if !mraidHasReference(ctx) || hasVisibility {
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") {
{ID: "audio-volume-change", Severity: "best-practice", Title: "audioVolumeChange handling not found", Detail: "Listen for audioVolumeChange and ignore null values; only apply volume math when the value is numeric.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if !mraidHasReference(ctx) || mraidHasAudioVolumeSafeHandling(ctx) {
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; avoid hyperlinks, location redirects, and unguarded browser navigation.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHyperlinkRx.MatchString(ctx.HTML) || mraidUsesLocationNavigation(ctx) || (mraidUsesBrowserNavigation(ctx) && !mraidHasCall(ctx, "open")) {
return mraidIssueFrom(rule, 0)
}
return nil
}},
{ID: "open-guarded-fallback", Severity: "requirement", Title: "mraid.open guarded fallback not found", Detail: "Guard mraid.open with function/state checks and provide a window.open fallback when MRAID is unavailable or still loading.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasCall(ctx, "open") && (!mraidHasOpenGuard(ctx) || !mraidHasOpenLoadingGuard(ctx) || !mraidUsesBrowserNavigation(ctx)) {
return mraidIssueFrom(rule, mraidLine(ctx, "open"))
}
return nil
}},
{ID: "open-video-url", Severity: "best-practice", Title: "mraid.open appears to target video media", Detail: "Use mraid.playVideo(url) for native video playback cases instead of mraid.open(videoUrl).", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidVideoOpenRx.MatchString(ctx.ScriptText) && !mraidHasCall(ctx, "playVideo") {
return mraidIssueFrom(rule, mraidLine(ctx, "open"))
}
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"))
{ID: "unload-fallback", Severity: "best-practice", Title: "No mraid.unload graceful failure path found", Detail: "Use mraid.unload() when the ad cannot run or refuses to show, instead of leaving a broken or blank creative.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasReference(ctx) && !mraidHasCall(ctx, "unload") {
return mraidIssueFrom(rule, 0)
}
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") {
{ID: "resize-properties", Severity: "requirement", Title: "mraid.resize used without setResizeProperties", Detail: "Call mraid.setResizeProperties(...) before mraid.resize(); otherwise the container should emit an error.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasCall(ctx, "resize") && !mraidHasCall(ctx, "setResizeProperties") {
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") {
{ID: "use-custom-close", Severity: "best-practice", Title: "Deprecated useCustomClose found", Detail: "MRAID 3.0 ignores useCustomClose; the host provides the mandatory close control.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasCall(ctx, "useCustomClose") || regexp.MustCompile(`(?i)useCustomClose`).MatchString(ctx.ScriptText) {
return mraidIssueFrom(rule, mraidLine(ctx, "useCustomClose"))
}
return nil
}},
{ID: "two-part-expand", Severity: "best-practice", Title: "Two-part expand appears to be used", Detail: "Two-part expandable ads are deprecated in MRAID 3.0; use self-contained one-part expandables.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidTwoPartExpandRx.MatchString(ctx.ScriptText) {
return mraidIssueFrom(rule, mraidLine(ctx, "expand"))
}
return nil
}},
{ID: "expand-support", Severity: "requirement", Title: "mraid.expand used without function guard", Detail: "Guard mraid.expand with a function check before using expand behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasCall(ctx, "expand") && !mraidHasFunctionGuard(ctx, "expand") {
return mraidIssueFrom(rule, mraidLine(ctx, "expand"))
}
return nil
}},
{ID: "resize-support", Severity: "requirement", Title: "mraid.resize used without function guard", Detail: "Guard mraid.resize with a function check before using resize behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasCall(ctx, "resize") && !mraidHasFunctionGuard(ctx, "resize") {
return mraidIssueFrom(rule, mraidLine(ctx, "resize"))
}
return nil
}},
{ID: "store-picture-support", Severity: "requirement", Title: "storePicture used without supports check", Detail: "Call mraid.supports(\"storePicture\") before mraid.storePicture(...).", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasCall(ctx, "storePicture") && !mraidHasSupportCheck(ctx, "storePicture") {
return mraidIssueFrom(rule, mraidLine(ctx, "storePicture"))
}
return nil
}},
{ID: "calendar-support", Severity: "requirement", Title: "createCalendarEvent used without supports check", Detail: "Call mraid.supports(\"calendar\") before mraid.createCalendarEvent(...).", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasCall(ctx, "createCalendarEvent") && !mraidHasSupportCheck(ctx, "calendar") {
return mraidIssueFrom(rule, mraidLine(ctx, "createCalendarEvent"))
}
return nil
}},
{ID: "location-support", Severity: "requirement", Title: "getLocation used without supports check", Detail: "Call mraid.supports(\"location\") before mraid.getLocation(). Do not use HTML5 geolocation as a substitute in MRAID.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasCall(ctx, "getLocation") && !mraidHasSupportCheck(ctx, "location") {
return mraidIssueFrom(rule, mraidLine(ctx, "getLocation"))
}
return nil
}},
{ID: "vpaid-support", Severity: "requirement", Title: "VPAID integration used without supports check", Detail: "Call mraid.supports(\"vpaid\") before mraid.initVpaid(...).", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasCall(ctx, "initVpaid") && !mraidHasSupportCheck(ctx, "vpaid") {
return mraidIssueFrom(rule, mraidLine(ctx, "initVpaid"))
}
return nil
}},
{ID: "orientation-support", Severity: "best-practice", Title: "Orientation properties set without function guard", Detail: "Guard mraid.setOrientationProperties with a function check before requiring orientation behavior.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidHasCall(ctx, "setOrientationProperties") && !mraidHasFunctionGuard(ctx, "setOrientationProperties") {
return mraidIssueFrom(rule, mraidLine(ctx, "setOrientationProperties"))
}
return nil
@@ -460,4 +648,30 @@ var mraidRules = []mraidRule{
}
return nil
}},
{ID: "output-script-hygiene", Severity: "requirement", Title: "Rejected script attributes found", Detail: "Playable output should not contain type=\"module\" or crossorigin on script tags; ad networks can reject ES modules/CORS attributes.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidRejectedScriptAttrsRx.MatchString(ctx.HTML) {
return mraidIssueFrom(rule, 0)
}
return nil
}},
{ID: "console-error", Severity: "best-practice", Title: "console.error found", Detail: "The ship checklist expects no console.error calls in output; use guarded logging or remove debug error logs before submission.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
if mraidConsoleErrorRx.MatchString(ctx.ScriptText) {
return mraidIssueFrom(rule, 0)
}
return nil
}},
{ID: "lifecycle-stubs", Severity: "requirement", Title: "Playable lifecycle signals not found", Detail: "Expose or call gameReady, gameStart, gameEnd, and gameClose so network preview tools can detect the playable lifecycle.", Reference: mraidReference, Test: func(ctx mraidContext, rule mraidRule) *mraidIssue {
missing := []string{}
for _, name := range []string{"gameReady", "gameStart", "gameEnd", "gameClose"} {
if !mraidHasLifecycleSignal(ctx, name) {
missing = append(missing, name)
}
}
if len(missing) == 0 {
return nil
}
found := mraidIssueFrom(rule, 0)
found.Detail = found.Detail + " Missing: " + strings.Join(missing, ", ") + "."
return found
}},
}