Skip to main content
MPStorysMAPLESTORY SERIES HUB

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

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 &#34; 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 &#34; 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 &#34; / " 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('&#34;', '%%') :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('&#34;', 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