mraid checker
This commit is contained in:
@@ -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
|
||||
}},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user