Documentation for this module may be created at Module:Cargo/doc -- Module:Cargo -- Shared helpers for building Cargo WHERE terms that are safe against -- double-quotes (and apostrophes) in values derived from page titles. -- -- The problem this solves -- ----------------------- -- Page titles reach templates and modules HTML-entity-encoded. PAGENAME-family -- magic words ({{PAGENAME}}, {{ROOTPAGENAME}}, ...) and {{#invoke}} arguments -- emit a double-quote in a title as the NUMERIC entity " and an apostrophe -- as ' (values chained from a prior query, or a {{{param}}} carrying a page -- name, may be encoded too). Cargo HTML-DECODES the WHERE string at SQL build -- time, so an un-neutralized " turns back into a raw " that closes the SQL -- string literal early and throws MariaDB "Error 1064". -- -- The fix -- ------- -- whereEq(field, value) returns an ordinary field="value" for normal values, -- but field LIKE "pattern" when the value contains a double-quote in ANY form -- (entity " / " or literal "). In the LIKE pattern every quote and -- apostrophe is replaced by the % wildcard, so the term matches the stored row -- under any encoding and no raw quote ever reaches the SQL string. LIKE -- metacharacters (% and _) that can legally appear in a title are escaped -- first so they match literally. -- -- Apostrophes alone are left as an EXACT match on purpose: a ' inside a -- double-quoted SQL string literal is syntactically harmless, and exact match -- keeps the query precise (a LIKE wildcard for "Alex's Request" could also -- match "Alexandros Request"). So normal titles and the ~316 apostrophe titles -- on this wiki produce byte-identical WHERE terms to the old inline form; only -- a genuine double-quote title changes behaviour. See the quote-injection -- audit (2026-06) for the full vulnerable-consumer list. -- -- Usage -- ----- -- Templates (wikitext): -- {{#cargo_query: ... |where={{#invoke:Cargo|where|monster_id|{{ROOTPAGENAME}}}} AND category="equip"}} -- Modules (Lua): -- local cargo = require('Module:Cargo') -- local q = mw.ext.cargo.query('t', 'f', { where = cargo.whereEq('monster_id', name) }) local p = {} -- Replace LIKE metacharacters and every quote/apostrophe form with a wildcard. local function toLikePattern(value) value = tostring(value) -- Escape backslash and the LIKE metacharacters % and _ first so a literal -- one in the title matches itself (MariaDB's default LIKE escape is \). value = value:gsub('\\', '\\\\'):gsub('%%', '\\%%'):gsub('_', '\\_') -- Swap every quote/apostrophe (entity OR literal) for the % "any run" -- wildcard. Entities first (including the double-encoded apostrophe). value = value:gsub(''', '%%') :gsub(''', '%%') :gsub('"', '%%') :gsub('"', '%%') :gsub("'", '%%') :gsub('"', '%%') return value end -- True when the value carries a double-quote that would break the SQL literal. local function hasDoubleQuote(value) if value:find('"', 1, true) then return true end if value:find('"', 1, true) then return true end if value:find('"', 1, true) then return true end return false end --- Build one safe WHERE term comparing `field` to a page-derived `value`. -- Returns field="value" normally, or field LIKE "pattern" when value contains -- a double-quote in any form. function p.whereEq(field, value) field = mw.text.trim(tostring(field or '')) value = mw.text.trim(tostring(value or '')) if hasDoubleQuote(value) then return field .. ' LIKE "' .. toLikePattern(value) .. '"' end return field .. '="' .. value .. '"' end --- Template entry point: {{#invoke:Cargo|where|field|value}} function p.where(frame) return p.whereEq(frame.args , frame.args ) end --- One exact member of a slash-delimited field, such as items.job. -- {{#invoke:Cargo|whereSlashList|job|Warrior}} function p.whereSlashList(frame) local field = mw.text.trim(tostring(frame.args or '')) -- Page names chained through Cargo can carry two layers of entities. local value = mw.text.decode(tostring(frame.args or ''), true) value = mw.text.trim(mw.text.decode(value, true)) local function quote(s) return "'" .. s:gsub('\\', '\\\\'):gsub("'", "''") .. "'" end local pattern = value:gsub('\\', '\\\\'):gsub('%%', '\\%%'):gsub('_', '\\_') local terms = { field .. '=' .. quote(value) } for _, member in ipairs({ pattern .. '/%', '%/' .. pattern, '%/' .. pattern .. '/%' }) do terms = field .. ' LIKE ' .. quote(member) end -- Cargo decodes HTML after this helper runs. Protect literal entity text -- so that decoding cannot introduce a quote after SQL escaping. return mw.text.encode('(' .. table.concat(terms, ' OR ') .. ')', '&<>') end --- The current page's full literal title (with a literal apostrophe, unlike -- {{PAGENAME}}/{{ROOTPAGENAME}} which emit the entity ' that Cargo then -- stores as text and double-encodes). Use as a #cargo_store value for a -- self-referential page-name key, e.g. quest_id, so the stored value stays -- clean and matches the row's _pageName. function p.pagetitle(frame) return mw.title.getCurrentTitle().text end --- The current page's title as MediaWiki stores it inside a file name. -- Upload normalisation replaces every character in $wgIllegalFileChars -- (":", "/" and "\") with "-", so "Final Attack: Bow" can only ever be -- uploaded as "Final Attack- Bow ...". A template looking for a render named -- after its own page has to apply the same rule or it will never find it. -- {{#invoke:Cargo|filetitle}} function p.filetitle(frame) return (mw.title.getCurrentTitle().text:gsub(' ', '-')) end --- One field from one Cargo row, as plain text, for use inside another -- query's WHERE clause. -- -- The reason this exists rather than a nested {{#cargo_query:}}: a parser -- function's output is a MediaWiki STRIP MARKER, not its text, until the -- parser reassembles the page. A strip marker looks like -- '"`UNIQ--item-1--QINU`"', and Cargo rejects any WHERE containing -- "--" as SQL comment injection. So a nested query inside a WHERE fails with -- 'the string "--" cannot be used within #cargo_query' even though the value -- it resolves to is perfectly ordinary. Lua has no such problem: mw.ext.cargo -- returns the real value, and a module's return text is substituted directly. -- -- {{#invoke:Cargo|lookup|jobs|branch|_pageName|{{FULLPAGENAME}}| }} -- -- Args: table, field, whereField, whereValue, and an optional default used -- when the row does not exist or holds an empty value. function p.lookup(frame) local a = frame.args local tbl = mw.text.trim(a or '') local field = mw.text.trim(a or '') local whereField = mw.text.trim(a or '') local whereValue = mw.text.trim(a or '') local fallback = mw.text.trim(a or '') if tbl == '' or field == '' or whereField == '' or whereValue == '' then return fallback end local ok, rows = pcall(function() return mw.ext.cargo.query(tbl, field, { where = p.whereEq(whereField, whereValue), limit = '1', }) end) if ok and rows and rows then local v = rows if v then v = mw.text.trim(mw.text.decode(v, true)) if v ~= '' then return v end end end return fallback end --- One stored value of a page's own row, for article prose ({{Stat}}). -- -- {{#invoke:Cargo|stat|monsters|level}} this page's row -- {{#invoke:Cargo|stat|items|required_level|Sword}} another page's row -- -- A monster page also stores one row per {{Monster variant}} under its own -- name; the page's own row is the one without a variant, which is the row -- Module:MonsterOverview reads too. Prints nothing when the row or the value -- is missing. function p.stat(frame) local a = frame.args local tbl = mw.text.trim(a or '') local field = mw.text.trim(a or '') local page = mw.text.trim(a or '') if page == '' then page = mw.title.getCurrentTitle().prefixedText end if tbl == '' or field == '' then return '' end local where = p.whereEq('_pageName', page) if tbl == 'monsters' then where = where .. ' AND (variant IS NULL OR variant = "")' end local ok, rows = pcall(function() return mw.ext.cargo.query(tbl, field, { where = where, limit = '1' }) end) if ok and rows and rows and rows then return mw.text.trim(mw.text.decode(rows , true)) end return '' end --- A value read back out of Cargo, with its HTML entities turned back into -- the characters they stand for. -- -- Cargo hands a stored value to a format=template row still encoded, so a -- title holding an apostrophe arrives as "Rest 'N Peace", not -- "Rest 'N Peace". Printed into running text that difference is invisible, -- because the browser decodes it again on the way to the screen. Passed as a -- parser-tag attribute it is not: the tag receives the literal entity, and a -- file lookup on that name finds nothing and renders nothing. -- -- {{#tag:bgmplayer||src={{#invoke:Cargo|decode|{{{file}}}}}}} function p.decode(frame) return mw.text.decode(frame.args or '', true) end return p
Module:Cargo
From the MapleStory Classic Wiki, mirrored inside MPStorys
This article could not be fully rendered. Showing plain text from the mirror.View on MapleStory Classic Wiki