Difference between revisions of "Module:Webarchive"

From Eat Every Plant
Jump to navigation Jump to search
m (1 revision: first quarter #4)
 
m (1 revision: Cola + 2 species)
 
(11 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 
--[[ ----------------------------------
 
--[[ ----------------------------------
  
    Lua module implementing the {{webarchive}} template.  
+
Lua module implementing the {{webarchive}} template.  
  
      A merger of the functionality of three templates: {{wayback}}, {{webcite}} and {{cite archives}}
+
A merger of the functionality of three templates: {{wayback}}, {{webcite}} and {{cite archives}}
 
+
  ]]
+
]]
  
local p = {}
 
  
--[[--------------------------< inlineError >-----------------------
+
--[[--------------------------< D E P E N D E N C I E S >------------------------------------------------------
 +
]]
 +
 
 +
require('Module:No globals');
 +
local getArgs = require ('Module:Arguments').getArgs;
 +
 
 +
 
 +
--[[--------------------------< F O R W A R D  D E C L A R A T I O N S >--------------------------------------
 +
]]
 +
 
 +
local categories = {}; -- category names
 +
local config = {}; -- global configuration settings
 +
local digits = {}; -- for i18n; table that translates local-wiki digits to western digits
 +
local err_warn_msgs = {}; -- error and warning messages
 +
local excepted_pages = {};
 +
local month_num = {}; -- for i18n; table that translates local-wiki month names to western digits
 +
local prefixes = {}; -- service provider tail string prefixes
 +
local services = {}; -- archive service provider data from
 +
local s_text = {}; -- table of static text strings used to build final rendering
 +
local uncategorized_namespaces = {}; -- list of namespaces that we should not categorize
 +
local uncategorized_subpages = {}; -- list of subpages that should not be categorized
 +
 
 +
 
 +
--[[--------------------------< P A G E  S C O P E  I D E N T I F I E R S >----------------------------------
 +
]]
 +
 
 +
local non_western_digits; -- boolean flag set true when data.digits.enable is true
 +
local this_page = mw.title.getCurrentTitle();
 +
 
 +
local track = {}; -- Associative array to hold tracking categories
 +
local ulx = {}; -- Associative array to hold template data
 +
 
 +
 
 +
--[[--------------------------< S U B S T I T U T E >----------------------------------------------------------
 +
 
 +
Populates numbered arguments in a message string using an argument table.
 +
 
 +
]]
 +
 
 +
local function substitute (msg, args)
 +
return args and mw.message.newRawMessage (msg, args):plain() or msg;
 +
end
 +
 
 +
 
 +
--[[--------------------------< tableLength >-----------------------
 +
 
 +
Given a 1-D table, return number of elements
 +
 
 +
]]
 +
 
 +
local function tableLength(T)
 +
local count = 0
 +
for _ in pairs(T) do count = count + 1 end
 +
return count
 +
end
 +
 
  
    Critical error. Render output completely in red. Add to tracking category.
+
--[=[-------------------------< M A K E _ W I K I L I N K >----------------------------------------------------
  
]]
+
Makes a wikilink; when both link and display text is provided, returns a wikilink in the form [[L|D]]; if only
 +
link is provided, returns a wikilink in the form [[L]]; if neither are provided or link is omitted, returns an
 +
empty string.
  
local function inlineError(arg, msg)
+
]=]
  
  track["Category:Webarchive template errors"] = 1
+
local function make_wikilink (link, display, no_link)
  return '<span style="font-size:100%" class="error citation-comment">Error in webarchive template: Check <code style="color:inherit; border:inherit; padding:inherit;">&#124;' .. arg .. '=</code> value. ' .. msg .. '</span>'
+
if nil == no_link then
 +
if link and ('' ~= link) then
 +
if display and ('' ~= display) then
 +
return table.concat ({'[[', link, '|', display, ']]'});
 +
else
 +
return table.concat ({'[[', link, ']]'});
 +
end
 +
end
 +
return display or ''; -- link not set so return the display text
  
 +
else -- no_link
 +
if display and ('' ~= display) then -- if there is display text
 +
return display; -- return that
 +
else
 +
return link or ''; -- return the target article name or empty string
 +
end
 +
end
 
end
 
end
  
--[[--------------------------< inlineRed >-----------------------
 
  
      Render a text fragment in red, such as a warning as part of the final output.
+
--[[--------------------------< createTracking >-----------------------
      Add tracking category.
+
 
 +
Return data in track[] ie. tracking categories
 +
 
 +
]]
 +
 
 +
local function createTracking()
 +
if not excepted_pages[this_page.fullText] then -- namespace:title/fragment is allowed to be categorized (typically this module's / template's testcases page(s))
 +
if uncategorized_namespaces[this_page.nsText] then
 +
return ''; -- this page not to be categorized so return empty string
 +
end
 +
for _,v in ipairs (uncategorized_subpages) do -- cycle through page name patterns
 +
if this_page.text:match (v) then -- test page name against each pattern
 +
return ''; -- this subpage type not to be categorized so return empty string
 +
end
 +
end
 +
end
 +
 
 +
local out = {};
 +
if tableLength(track) > 0 then
 +
for key, _ in pairs(track) do -- loop through table
 +
table.insert (out, make_wikilink (key)); -- and convert category names to links
 +
end
 +
end
 +
return table.concat (out); -- concat into one big string; empty string if table is empty
 +
 
 +
end
 +
 
  
]]
+
--[[--------------------------< inlineError >-----------------------
  
local function inlineRed(msg, trackmsg)
+
Critical error. Render output completely in red. Add to tracking category.
  
  if trackmsg == "warning" then
+
This function called as the last thing before abandoning this module
    track["Category:Webarchive template warnings"] = 1
 
  elseif trackmsg == "error" then
 
    track["Category:Webarchive template errors"] = 1
 
  end
 
  
  return '<span style="font-size:100%" class="error citation-comment">' .. msg .. '</span>'
+
]]
  
 +
local function inlineError (msg, args)
 +
track[categories.error] = 1
 +
return table.concat ({
 +
'<span style="font-size:100%" class="error citation-comment">Error in ', -- open the error message span
 +
config.tname, -- insert the local language template name
 +
' template: ',
 +
substitute (msg, args), -- insert the formatted error message
 +
'.</span>', -- close the span
 +
createTracking() -- add the category
 +
})
 
end
 
end
  
--[[--------------------------< trimArg >-----------------------
 
  
    trimArg returns nil if arg is "" while trimArg2 returns 'true' if arg is ""
+
--[[--------------------------< inlineRed >-----------------------
    trimArg2 is for args that might accept an empty value, as an on/off switch like nolink=
+
 
 +
Render a text fragment in red, such as a warning as part of the final output.
 +
Add tracking category.
  
 
  ]]
 
  ]]
  
local function trimArg(arg)
+
local function inlineRed(msg, trackmsg)
  if arg == "" or arg == nil then
+
if trackmsg == "warning" then
    return nil
+
track[categories.warning] = 1;
  else
+
elseif trackmsg == "error" then
    return mw.text.trim(arg)
+
track[categories.error] = 1;
  end
+
end
end
+
 
local function trimArg2(arg)
+
return '<span style="font-size:100%" class="error citation-comment">' .. msg .. '</span>'
  if arg == nil then
 
    return nil
 
  else
 
    return mw.text.trim(arg)
 
  end
 
 
end
 
end
 +
  
 
--[[--------------------------< base62 >-----------------------
 
--[[--------------------------< base62 >-----------------------
  
    Convert base-62 to base-10
+
Convert base-62 to base-10
    Credit: https://de.wikipedia.org/wiki/Modul:Expr  
+
Credit: https://de.wikipedia.org/wiki/Modul:Expr  
  
  ]]
+
]]
  
 
local function base62( value )
 
local function base62( value )
 +
local r = 1 -- default return value is input value is malformed
  
    local r = 1
+
if value:match ('%W') then -- value must only be in the set [0-9a-zA-Z]
 +
return; -- nil return when value contains extraneous characters
 +
end
  
    if value:match( "^%w+$" ) then
+
local n = #value -- number of characters in value
        local n = #value
+
local k = 1
        local k = 1
+
local c
        local c
+
r = 0
        r = 0
+
for i = n, 1, -1 do -- loop through all characters in value from ls digit to ms digit
        for i = n, 1, -1 do
+
c = value:byte( i, i )
            c = value:byte( i, i )
+
if c >= 48 and c <= 57 then -- character is digit 0-9
            if c >= 48 and c <= 57 then
+
c = c - 48
                c = c - 48
+
elseif c >= 65 and c <= 90 then -- character is ascii a-z
            elseif c >= 65 and c <= 90 then
+
c = c - 55
                c = c - 55
+
else -- must be ascii A-Z
            elseif c >= 97  and  c <= 122 then
+
c = c - 61
                c = c - 61
+
end
            else    -- How comes?
+
r = r + c * k -- accumulate this base62 character's value
                r = 1
+
k = k * 62 -- bump for next
                break    -- for i
+
end -- for i
            end
+
 
            r = r + c * k
+
return r
            k = k * 62
 
        end -- for i
 
    end
 
    return r
 
 
end  
 
end  
  
--[[--------------------------< tableLength >-----------------------
 
  
      Given a 1-D table, return number of elements
+
--[[--------------------------< D E C O D E _ D A T E >--------------------------------------------------------
 +
 
 +
Given a date string, return it in iso format along with an indicator of the date's format.  Except that month names
 +
must be recognizable as legitimate month names with proper capitalization, and that the date string must match one
 +
of the recognized date formats, no error checking is done here; return nil else
 +
 
 +
]]
 +
 
 +
local function decode_date (date_str)
 +
local patterns = {
 +
['dmy'] = {'^(%d%d?) +([^%s%d]+) +(%d%d%d%d)$', 'd', 'm', 'y'}, -- %a does not recognize unicode combining characters used by some languages
 +
['mdy'] = {'^([^%s%d]+) (%d%d?), +(%d%d%d%d)$', 'm', 'd', 'y'},
 +
['ymd'] = {'^(%d%d%d%d) +([^%s%d]+) (%d%d?)$', 'y', 'm', 'd'}, -- not mos compliant at en.wiki but may be acceptible at other wikis
 +
};
 +
 +
local t = {};
 +
 
 +
if non_western_digits then -- this wiki uses non-western digits?
 +
date_str = mw.ustring.gsub (date_str, '%d', digits); -- convert this wiki's non-western digits to western digits
 +
end
  
  ]]
+
if date_str:match ('^%d%d%d%d%-%d%d%-%d%d$') then -- already an iso format date, return western digits form
 +
return date_str, 'iso';
 +
end
 +
 +
for k, v in pairs (patterns) do
 +
local c1, c2, c3 = mw.ustring.match (date_str, patterns[k][1]); -- c1 .. c3 are captured but we don't know what they hold
 +
 +
if c1 then -- set on match
 +
t = { -- translate unspecified captures to y, m, and d
 +
[patterns[k][2]] = c1, -- fill the table of captures with the captures
 +
[patterns[k][3]] = c2, -- take index names from src_pattern table and assign sequential captures
 +
[patterns[k][4]] = c3,
 +
};
 +
if month_num[t.m] then -- when month not already a number
 +
t.m = month_num[t.m]; -- replace valid month name with a number
 +
else
 +
return nil, 'iso'; -- not a valid date form because month not valid
 +
end
  
local function tableLength(T)
+
return mw.ustring.format ('%.4d-%.2d-%.2d', t.y, t.m, t.d), k; -- return date in iso format
  local count = 0
+
end
  for _ in pairs(T) do count = count + 1 end
+
end
  return count
+
return nil, 'iso'; -- date could not be decoded; return nil and default iso date
 
end
 
end
  
 +
 +
--[[--------------------------< makeDate >-----------------------
  
--[[--------------------------< dateFormat >-----------------------
+
Given year, month, day numbers, (zero-padded or not) return a full date in df format
 +
where df may be one of:
 +
mdy, dmy, iso, ymd
  
    Given a date string, return its format: dmy, mdy, iso, ymd
+
on entry, year, month, day are presumed to be correct for the date that they represent; all are required
      If unable to determine return nil
 
  
  ]]
+
in this module, makeDate() is sometimes given an iso-format date in year:
 +
makeDate (2018-09-20, nil, nil, df)
 +
this works because table.concat() sees only one table member
  
local function dateFormat(date)
+
]]
  
  local dt = {}
+
local function makeDate (year, month, day, df)
  dt.split = {}
+
local format = {
 +
['dmy'] = 'j F Y',
 +
['mdy'] = 'F j, Y',
 +
['ymd'] = 'Y F j',
 +
['iso'] = 'Y-m-d',
 +
};
  
  dt.split = mw.text.split(date, "-")
+
local date = table.concat ({year, month, day}, '-'); -- assemble year-initial numeric-format date (zero padding not required here)
  if tableLength(dt.split) == 3 then
 
    if tonumber(dt.split[1]) > 1900 and tonumber(dt.split[1]) < 2200 and tonumber(dt.split[2]) and tonumber(dt.split[3]) then
 
      return "iso"
 
    else
 
      return nil
 
    end
 
  end 
 
  
  dt.split = mw.text.split(date, " ")
+
if non_western_digits then --this wiki uses non-western digits?
  if tableLength(dt.split) == 3 then
+
date = mw.ustring.gsub (date, '%d', digits); -- convert this wiki's non-western digits to western digits
    if tonumber(dt.split[3]) then
+
end
      if tonumber(dt.split[3]) > 1900 and tonumber(dt.split[3]) < 2200 then
 
        if tonumber(dt.split[1]) then
 
          return "dmy"
 
        else
 
          return "mdy"
 
        end  
 
      else
 
        if tonumber(dt.split[1]) then
 
          if tonumber(dt.split[1]) > 1900 and tonumber(dt.split[1]) < 2200 then
 
            return "ymd"
 
          end
 
        end
 
      end
 
    end
 
  end
 
  return nil
 
  
 +
return mw.getContentLanguage():formatDate (format[df], date);
 
end
 
end
  
--[[--------------------------< makeDate >-----------------------
 
  
    Given a zero-padded 4-digit year, 2-digit month and 2-digit day, return a full date in df format
+
--[[--------------------------< I S _ V A L I D _ D A T E >----------------------------------------------------
    df = mdy, dmy, iso, ymd
+
 
 +
Returns true if date is after 31 December 1899 (why is 1900 the min year? shouldn't the internet's date-of-birth
 +
be min year?), not after today's date, and represents a valid date (29 February 2017 is not a valid date).  Applies
 +
Gregorian leapyear rules.
  
]]
+
all arguments are required
  
local function makeDate(year, month, day, df)
+
]]
  
  if not year or year == "" or not month or month == "" or not day or day == "" then
+
local function is_valid_date (year, month, day)
    return nil
+
local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  end
+
local month_length;
 +
local y, m, d;
 +
local today = os.date ('*t'); -- fetch a table of current date parts
  
  local zmonth = month                                                     -- month with leading 0
+
if not year or '' == year or not month or '' == month or not day or '' == day then
  month = month:match("0*(%d+)")                                            -- month without leading 0
+
return false; -- something missing
  if tonumber(month) < 1 or tonumber(month) > 12 then
+
end
    return year
+
  end
+
y = tonumber (year);
  local nmonth = os.date("%B", os.time{year=2000, month=month, day=1} )     -- month in name form     
+
m = tonumber (month);
  if not nmonth then
+
d = tonumber (day);
    return year
 
  end
 
  
  local zday = day
+
if 1900 > y or today.year < y or 1 > m or 12 < m then -- year and month are within bounds TODO: 1900?
  day = zday:match("0*(%d+)")
+
return false;
  if tonumber(day) < 1 or tonumber(day) > 31 then
+
end
    if df == "mdy" or df == "dmy" then
 
      return nmonth .. " " .. year
 
    elseif df == "iso" then
 
      return year .. "-" .. zmonth
 
    elseif df == "ymd" then
 
      return year .. " " .. nmonth
 
    else
 
      return nmonth .. " " .. year
 
    end
 
  end                                      
 
  
  if df == "mdy" then
+
if (2==m) then -- if February
    return nmonth .. " " .. day .. ", " .. year        -- September 1, 2016
+
month_length = 28; -- then 28 days unless
  elseif df == "dmy" then
+
if (0==(y%4) and (0~=(y%100) or 0==(y%400))) then -- is a leap year?
    return day .. " " .. nmonth .. " " .. year          -- 1 September 2016
+
month_length = 29; -- if leap year then 29 days in February
  elseif df == "iso" then
+
end
    return year .. "-" .. zmonth .. "-" .. zday        -- 2016-09-01
+
else
  elseif df == "ymd" then
+
month_length=days_in_month[m];
    return year .. " " .. nmonth .. " " .. cday          -- 2016 September 1
+
end
  else
 
    return nmonth .. " " .. day .. ", " .. year        -- September 1, 2016
 
  end
 
  
 +
if 1 > d or month_length < d then -- day is within bounds
 +
return false;
 +
end
 +
-- here when date parts represent a valid date
 +
return os.time({['year']=y, ['month']=m, ['day']=d, ['hour']=0}) <= os.time(); -- date at midnight must be less than or equal to current date/time
 
end
 
end
  
Line 208: Line 323:
 
--[[--------------------------< decodeWebciteDate >-----------------------
 
--[[--------------------------< decodeWebciteDate >-----------------------
  
      Given a URI-path to Webcite (eg. /67xHmVFWP) return the encoded date in df format
+
Given a URI-path to Webcite (eg. /67xHmVFWP) return the encoded date in df format
  
  ]]
+
returns date string in df format - webcite date is a unix timestamp encoded as bae62
local function decodeWebciteDate(path, df)
+
or the string 'query'
  
    local dt = {}
+
]]
    dt.split = {}
 
  
    dt.split = mw.text.split(path, "/")
+
local function decodeWebciteDate(path, df)
  
    -- valid URL formats that are not base62
+
local dt = {};
 +
local decode;
  
    -- http://www.webcitation.org/query?id=1138911916587475
+
dt = mw.text.split(path, "/")
    -- http://www.webcitation.org/query?url=http..&date=2012-06-01+21:40:03
 
    -- http://www.webcitation.org/1138911916587475
 
    -- http://www.webcitation.org/cache/73e53dd1f16cf8c5da298418d2a6e452870cf50e
 
    -- http://www.webcitation.org/getfile.php?fileid=1c46e791d68e89e12d0c2532cc3cf629b8bc8c8e
 
  
    if mw.ustring.find( dt.split[2], "query", 1, plain) or
+
-- valid URL formats that are not base62
      mw.ustring.find( dt.split[2], "cache", 1, plain) or
 
      mw.ustring.find( dt.split[2], "getfile", 1, plain) or
 
      tonumber(dt.split[2]) then
 
      return "query"
 
    end
 
  
    dt.full = os.date("%Y %m %d", string.sub(string.format("%d", base62(dt.split[2])),1,10) )
+
-- http://www.webcitation.org/query?id=1138911916587475
    dt.split = mw.text.split(dt.full, " ")
+
-- http://www.webcitation.org/query?url=http..&date=2012-06-01+21:40:03
    dt.year = dt.split[1]
+
-- http://www.webcitation.org/1138911916587475
    dt.month = dt.split[2]
+
-- http://www.webcitation.org/cache/73e53dd1f16cf8c5da298418d2a6e452870cf50e
    dt.day = dt.split[3]
+
-- http://www.webcitation.org/getfile.php?fileid=1c46e791d68e89e12d0c2532cc3cf629b8bc8c8e
  
    if not tonumber(dt.year) or not tonumber(dt.month) or not tonumber(dt.day) then
+
if dt[2]:find ('query', 1, true) or
      return inlineRed("[Date error] (1)", "error")
+
dt[2]:find ('cache', 1, true) or
    end
+
dt[2]:find ('getfile', 1, true) or
 +
tonumber(dt[2]) then
 +
return 'query';
 +
end
  
    if tonumber(dt.month) > 12 or tonumber(dt.day) > 31 or tonumber(dt.month) < 1 then
+
decode = base62(dt[2]); -- base62 string -> exponential number
      return inlineRed("[Date error] (2)", "error")
+
if not decode then
    end
+
return nil; -- nil return when dt[2] contains characters not in %w
    if tonumber(dt.year) > tonumber(os.date("%Y")) or tonumber(dt.year) < 1900 then
+
end
      return inlineRed("[Date error] (3)", "error")
+
dt = os.date('*t', string.format("%d", decode):sub(1,10)) -- exponential number -> text -> first 10 characters (a unix timestamp) -> a table of date parts
    end
 
  
    fulldate = makeDate(dt.year, dt.month, dt.day, df)
+
decode = makeDate (dt.year, dt.month, dt.day, 'iso'); -- date comparisons are all done in iso format with western digits
    if not fulldate then
+
if non_western_digits then --this wiki uses non-western digits?
      return inlineRed("[Date error] (4)", "error")
+
decode = mw.ustring.gsub (decode, '%d', digits); -- convert this wiki's non-western digits to western digits
    else
+
end
      return fulldate
 
    end
 
  
 +
return decode;
 
end
 
end
  
--[[--------------------------< snapDateToString >-----------------------
+
 
 +
--[[--------------------------< decodeWaybackDate >-----------------------
  
 
Given a URI-path to Wayback (eg. /web/20160901010101/http://example.com )
 
Given a URI-path to Wayback (eg. /web/20160901010101/http://example.com )
  return the formatted date eg. "September 1, 2016" in df format  
+
or Library of Congress Web Archives (/all/20160901010101/http://example.com)
  Handle non-digits in snapshot ID such as "re_" and "-" and "*"
+
return the formatted date eg. "September 1, 2016" in df format  
 +
Handle non-digits in snapshot ID such as "re_" and "-" and "*"
  
]]
+
returns two values:
 +
first value is one of these:
 +
valid date string in df format - wayback date is valid (including the text string 'index' when date is '/*/')
 +
empty string - wayback date is malformed (less than 8 digits, not a valid date)
 +
nil - wayback date is '/save/' or otherwise not a number
 +
 +
second return value is an appropriate 'message' may or may not be formatted
 +
 
 +
]]
  
 
local function decodeWaybackDate(path, df)
 
local function decodeWaybackDate(path, df)
  
    local snapdate, snapdatelong, currdate, fulldate
+
local msg, snapdate;
 +
 
 +
snapdate = path:gsub ('^/all/', ''):gsub ('^/web/', ''):gsub ('^/', ''); -- remove leading '/all/', leading '/web/' or leading '/'
 +
snapdate = snapdate:match ('^[^/]+'); -- get timestamp
 +
if snapdate == "*" then -- eg. /web/*/http.. or /all/*/http..
 +
return 'index'; -- return indicator that this url has an index date
 +
end
 +
 
 +
snapdate = snapdate:gsub ('%a%a_%d?$', ''):gsub ('%-', ''); -- from date, remove any trailing "re_", dashes
 +
 
 +
msg = '';
 +
if snapdate:match ('%*$') then -- a trailing '*' causes calendar display at archive .org
 +
snapdate = snapdate:gsub ('%*$', ''); -- remove so not part of length calc later
 +
msg = inlineRed (err_warn_msgs.ts_cal, 'warning'); -- make a message
 +
end
 +
 
 +
if not tonumber(snapdate) then
 +
return nil, 'ts_nan'; -- return nil (fatal error flag) and message selector
 +
end
  
    local safe = path
+
local dlen = snapdate:len();
    snapdate = string.gsub(safe, "^/w?e?b?/?", "")                     -- Remove leading "/web/" or "/"
+
if dlen < 8 then -- we need 8 digits TODO: but shouldn't this be testing for 14 digits?
    safe = snapdate
+
return '', inlineRed (err_warn_msgs.ts_short, 'error'); -- return empty string and error message
    local N = mw.text.split(safe, "/")
+
end
    snapdate = N[1]
 
    if snapdate == "*" then                                             -- eg. /web/*/http..
 
      return "index"
 
    end
 
    safe = snapdate
 
    snapdate = string.gsub(safe, "[a-z][a-z]_[0-9]?$", "")             -- Remove any trailing "re_" from date
 
    safe = snapdate
 
    snapdate = string.gsub(safe, "[-]", "")                            -- Remove dashes from date eg. 2015-01-01
 
    safe = snapdate
 
    snapdate = string.gsub(safe, "[*]$", "")                            -- Remove trailing "*"
 
  
    if not tonumber(snapdate) then
+
local year, month, day = snapdate:match ('(%d%d%d%d)(%d%d)(%d%d)'); -- no need for snapdatelong here
      return inlineRed("[Date error] (2)", "error")
 
    end
 
    local dlen = string.len(snapdate)
 
    if dlen < 4 then
 
      return inlineRed("[Date error] (3)", "error")
 
    end
 
    if dlen < 14 then
 
      snapdatelong = snapdate .. string.rep("0", 14 - dlen)
 
    else
 
      snapdatelong = snapdate
 
    end
 
    local year = string.sub(snapdatelong, 1, 4)
 
    local month = string.sub(snapdatelong, 5, 6)
 
    local day = string.sub(snapdatelong, 7, 8)
 
    if not tonumber(year) or not tonumber(month) or not tonumber(day) then
 
      return inlineRed("[Date error] (4)", "error")
 
    end
 
    if tonumber(month) > 12 or tonumber(day) > 31 or tonumber(month) < 1 then
 
      return inlineRed("[Date error] (5)", "error")
 
    end
 
    currdate = os.date("%Y")
 
    if tonumber(year) > tonumber(currdate) or tonumber(year) < 1900 then
 
      return inlineRed("[Date error] (6)", "error")
 
    end
 
  
    fulldate = makeDate(year, month, day, df)
+
if not is_valid_date (year, month, day) then
    if not fulldate then
+
return '', inlineRed (err_warn_msgs.ts_date, 'error'); -- return empty string and error message
      return inlineRed("[Date error] (7)", "error")
+
end
    else
 
      return fulldate
 
    end
 
  
 +
snapdate = table.concat ({year, month, day}, '-'); -- date comparisons are all done in iso format
 +
if 14 == dlen then
 +
return snapdate, msg; -- return date with message if any
 +
else
 +
return snapdate, msg .. inlineRed (err_warn_msgs.ts_len, 'warning'); -- return date with warning message(s)
 +
end
 
end
 
end
  
  
--[[--------------------------< serviceName >-----------------------
+
--[[--------------------------< decodeArchiveisDate >-----------------------
 +
 
 +
Given an Archive.is "long link" URI-path (e.g. /2016.08.28-144552/http://example.com)
 +
return the date in df format (e.g. if df = dmy, return 28 August 2016)
 +
Handles "." and "-" in snapshot date, so 2016.08.28-144552 is same as 20160828144552
 +
 
 +
returns two values:
 +
first value is one of these:
 +
valid date string in df format - archive.is date is valid (including the text string 'short link' when url is the short form)
 +
empty string - wayback date is malformed (not a number, less than 8 digits, not a valid date)
 +
nil - wayback date is '/save/'
 +
 +
second return value is an appropriate 'message' may or may not be formatted
 +
 
 +
]]
 +
 
 +
local function decodeArchiveisDate(path, df)
 +
local snapdate
 +
 
 +
if path:match ('^/%w+$') then -- short form url path is '/' followed by some number of base 62 digits and nothing else
 +
return "short link" -- e.g. http://archive.is/hD1qz
 +
end
 +
 
 +
snapdate = mw.text.split (path, '/')[2]:gsub('[%.%-]', ''); -- get snapshot date, e.g. 2016.08.28-144552; remove periods and hyphens
 +
 
 +
local dlen = string.len(snapdate)
 +
if dlen < 8 then -- we need 8 digits TODO: but shouldn't this be testing for 14 digits?
 +
return '', inlineRed (err_warn_msgs.ts_short, 'error'); -- return empty string and error message
 +
end
 +
 
 +
local year, month, day = snapdate:match ('(%d%d%d%d)(%d%d)(%d%d)'); -- no need for snapdatelong here
  
    Given a domain extracted by mw.uri.new() (eg. web.archive.org) set tail string and service ID
+
if not is_valid_date (year, month, day) then
 +
return '', inlineRed (err_warn_msgs.ts_date, 'error'); -- return empty string and error message
 +
end
  
  ]]
+
snapdate = table.concat ({year, month, day}, '-'); -- date comparisons are all done in iso format
 +
if 14 == dlen then
 +
return snapdate; -- return date
 +
else
 +
return snapdate, inlineRed (err_warn_msgs.ts_len, 'warning'); -- return date with warning message
 +
end
 +
end
  
local function serviceName(host, nolink)
 
  
  local tracking = "Category:Webarchive template other archives"
+
--[[--------------------------< serviceName >-----------------------
  
  local bracketopen = "[["
+
Given a domain extracted by mw.uri.new() (eg. web.archive.org) set tail string and service ID
  local bracketclose = "]]"
 
  if nolink then
 
    bracketopen = ""
 
    bracketclose = ""
 
  end
 
  
  ulx.url1.service = "other"
+
]]
  ulx.url1.tail = " at " .. ulx.url1.host .. " " .. inlineRed("Error: unknown archive URL")
 
  
  if mw.ustring.find( host, "archive.org", 1, plain ) then
+
local function serviceName(host, no_link)
    ulx.url1.service = "wayback"
+
local tracking;
    ulx.url1.tail = " at the " .. bracketopen .. "Wayback Machine" .. bracketclose
+
local index;
    tracking = "Category:Webarchive template wayback links"
+
  elseif mw.ustring.find( host, "webcitation.org", 1, plain ) then
+
host = host:lower():gsub ('^web%.(.+)', '%1'):gsub ('^www%.(.+)', '%1'); -- lowercase, remove web. and www. subdomains
    ulx.url1.service = "webcite"
 
    ulx.url1.tail = " at " .. bracketopen .. "WebCite" .. bracketclose
 
    tracking = "Category:Webarchive template webcite links"
 
  elseif mw.ustring.find( host, "archive.is", 1, plain ) then
 
    ulx.url1.service = "archiveis"
 
    ulx.url1.tail = " at " .. bracketopen .. "Archive.is" .. bracketclose
 
    tracking = "Category:Webarchive template archiveis links"
 
  elseif mw.ustring.find( host, "archive.fo", 1, plain ) then
 
    ulx.url1.service = "archiveis"
 
    ulx.url1.tail = " at " .. bracketopen .. "Archive.is" .. bracketclose
 
    tracking = "Category:Webarchive template archiveis links"
 
  elseif mw.ustring.find( host, "archive.today", 1, plain ) then
 
    ulx.url1.service = "archiveis"
 
    ulx.url1.tail = " at " .. bracketopen .. "Archive.is" .. bracketclose
 
    tracking = "Category:Webarchive template archiveis links"
 
  elseif mw.ustring.find( host, "archive.il", 1, plain ) then
 
    ulx.url1.service = "archiveis"
 
    ulx.url1.tail = " at " .. bracketopen .. "Archive.is" .. bracketclose
 
    tracking = "Category:Webarchive template archiveis links"
 
  elseif mw.ustring.find( host, "archive.ec", 1, plain ) then
 
    ulx.url1.service = "archiveis"
 
    ulx.url1.tail = " at " .. bracketopen .. "Archive.is" .. bracketclose
 
    tracking = "Category:Webarchive template archiveis links"
 
  elseif mw.ustring.find( host, "archive[-]it.org", 1, plain ) then
 
    ulx.url1.service = "archiveit"
 
    ulx.url1.tail = " at " .. bracketopen .. "Archive-It" .. bracketclose
 
  elseif mw.ustring.find( host, "arquivo.pt", 1, plain) then
 
    ulx.url1.tail = " at the " .. "Portuguese Web Archive"
 
  elseif mw.ustring.find( host, "loc.gov", 1, plain ) then
 
    ulx.url1.tail = " at the " .. bracketopen .. "Library of Congress" .. bracketclose
 
  elseif mw.ustring.find( host, "webharvest.gov", 1, plain ) then
 
    ulx.url1.tail = " at the " .. bracketopen .. "National Archives and Records Administration" .. bracketclose
 
  elseif mw.ustring.find( host, "bibalex.org", 1, plain ) then
 
    ulx.url1.tail = " at " .. "[[Bibliotheca_Alexandrina#Internet_Archive_partnership|Bibliotheca Alexandrina]]"
 
  elseif mw.ustring.find( host, "collectionscanada", 1, plain ) then
 
    ulx.url1.tail = " at the " .. "Canadian Government Web Archive"
 
  elseif mw.ustring.find( host, "haw.nsk", 1, plain ) then
 
    ulx.url1.tail = " at the " .. "Croatian Web Archive (HAW)"
 
  elseif mw.ustring.find( host, "veebiarhiiv.digar.ee", 1, plain ) then
 
    ulx.url1.tail = " at the " .. "Estonian Web Archive"
 
  elseif mw.ustring.find( host, "vefsafn.is", 1, plain ) then
 
    ulx.url1.tail = " at the " .. "[[National and University Library of Iceland]]"
 
  elseif mw.ustring.find( host, "proni.gov", 1, plain ) then
 
    ulx.url1.tail = " at the " .. bracketopen .. "Public Record Office of Northern Ireland" .. bracketclose
 
  elseif mw.ustring.find( host, "uni[-]lj.si", 1, plain ) then
 
    ulx.url1.tail = " at the " .. "Slovenian Web Archive"
 
  elseif mw.ustring.find( host, "stanford.edu", 1, plain ) then
 
    ulx.url1.tail = " at the " .. "[[Stanford University Libraries|Stanford Web Archive]]"
 
  elseif mw.ustring.find( host, "nationalarchives.gov.uk", 1, plain ) then
 
    ulx.url1.tail = " at the " .. bracketopen .. "UK Government Web Archive" .. bracketclose
 
  elseif mw.ustring.find( host, "parliament.uk", 1, plain ) then
 
    ulx.url1.tail = " at the " .. bracketopen .. "UK Parliament's Web Archive" .. bracketclose
 
  elseif mw.ustring.find( host, "webarchive.org.uk", 1, plain ) then
 
    ulx.url1.tail = " at the " .. bracketopen .. "UK Web Archive" .. bracketclose
 
  elseif mw.ustring.find( host, "nlb.gov.sg", 1, plain ) then
 
    ulx.url1.tail = " at " .. "Web Archive Singapore"
 
  elseif mw.ustring.find( host, "pandora.nla.gov.au", 1, plain ) then
 
    ulx.url1.tail = " at " .. bracketopen .. "Pandora Archive" .. bracketclose
 
  elseif mw.ustring.find( host, "perma.cc", 1, plain ) then
 
    ulx.url1.tail = " at " .. bracketopen .. "Perma.cc" .. bracketclose
 
  elseif mw.ustring.find( host, "perma-archives.cc", 1, plain ) then
 
    ulx.url1.tail = " at " .. bracketopen .. "Perma.cc" .. bracketclose
 
  elseif mw.ustring.find( host, "screenshots.com", 1, plain ) then
 
    ulx.url1.tail = " at Screenshots"
 
  elseif mw.ustring.find( host, "wikiwix.com", 1, plain ) then
 
    ulx.url1.tail = " at Wikiwix"
 
  elseif mw.ustring.find( host, "freezepage.com", 1, plain ) then
 
    ulx.url1.tail = " at Freezepage"
 
  elseif mw.ustring.find( host, "webcache.googleusercontent.com", 1, plain ) then
 
    ulx.url1.tail = " at Google Cache"
 
  else
 
    tracking = "Category:Webarchive template unknown archives"
 
  end
 
  
  track[tracking] = 1
+
if services[host] then
 +
index = host;
 +
else
 +
for k, _ in pairs (services) do
 +
if host:find ('%f[%a]'..k:gsub ('([%.%-])', '%%%1')) then
 +
index = k;
 +
break;
 +
end
 +
end
 +
end
 +
 +
if index then
 +
local out = {''}; -- empty string in [1] so that concatenated result has leading single space
 +
ulx.url1.service = services[index][4] or 'other';
 +
tracking = services[index][5] or categories.other;
 +
-- build tail string
 +
if false == services[index][1] then -- select prefix
 +
table.insert (out, prefixes.at);
 +
elseif true == services[index][1] then
 +
table.insert (out, prefixes.atthe);
 +
else
 +
table.insert (out, services[index][1]);
 +
end
 +
 +
table.insert (out, make_wikilink (services[index][2], services[index][3], no_link)); -- add article wikilink
 +
if services[index][6] then -- add tail postfix if it exists
 +
table.insert (out, services[index][6]);
 +
end
 +
 +
ulx.url1.tail = table.concat (out, ' '); -- put it all together; result has leading space character
  
 +
else -- here when unknown archive
 +
ulx.url1.service = 'other';
 +
tracking = categories.unknown;
 +
ulx.url1.tail = table.concat ({'', prefixes.at, host, inlineRed (err_warn_msgs.unknown_url, error)}, ' ');
 +
end
 +
 +
track[tracking] = 1
 
end
 
end
 +
  
 
--[[--------------------------< parseExtraArgs >-----------------------
 
--[[--------------------------< parseExtraArgs >-----------------------
  
    Parse numbered arguments starting at 2, such as url2..url10, date2..date10, title2..title10
+
Parse numbered arguments starting at 2, such as url2..url10, date2..date10, title2..title10
      For example: {{webarchive |url=.. |url4=.. |url7=..}}
+
For example: {{webarchive |url=.. |url4=.. |url7=..}}
        Three url arguments not in numeric sequence (1..4..7).  
+
Three url arguments not in numeric sequence (1..4..7).  
        Function only processes arguments numbered 2 or greater (in this case 4 and 7)
+
Function only processes arguments numbered 2 or greater (in this case 4 and 7)
        It creates numeric sequenced table entries like:
+
It creates numeric sequenced table entries like:
          urlx.url2.url = <argument value for url4>
+
urlx.url2.url = <argument value for url4>
          urlx.url3.url = <argument value for url7>
+
urlx.url3.url = <argument value for url7>
      Returns the number of URL arguments found numbered 2 or greater (in this case returns "2")
+
Returns the number of URL arguments found numbered 2 or greater (in this case returns "2")
  
 
  ]]
 
  ]]
  
local function parseExtraArgs()
+
local function parseExtraArgs(args)
  
  local i, j, argurl, argurl2, argdate, argtitle
+
local i, j, argurl, argurl2, argdate, argtitle
  
  j = 2
+
j = 2
  for i = 2, maxurls do
+
for i = 2, config.maxurls do
    argurl = "url" .. i
+
argurl = "url" .. i
    if trimArg(args[argurl]) then
+
if args[argurl] then
      argurl2 = "url" .. j
+
argurl2 = "url" .. j
      ulx[argurl2] = {}
+
ulx[argurl2] = {}
      ulx[argurl2]["url"] = args[argurl]
+
ulx[argurl2]["url"] = args[argurl]
      argdate = "date" .. j
+
argdate = "date" .. j
      if trimArg(args[argdate]) then
+
if args[argdate] then
        ulx[argurl2]["date"] = args[argdate]
+
ulx[argurl2]["date"] = args[argdate]
      else
+
else
        ulx[argurl2]["date"] = inlineRed("[Date missing]", "warning")
+
ulx[argurl2]["date"] = inlineRed (err_warn_msgs.date_miss, 'warning');
      end
+
end
      argtitle = "title" .. j
+
      if trimArg(args[argtitle]) then
+
argtitle = "title" .. j
        ulx[argurl2]["title"] = args[argtitle]
+
if args[argtitle] then
      else
+
ulx[argurl2]["title"] = args[argtitle]
        ulx[argurl2]["title"] = nil
+
else
      end
+
ulx[argurl2]["title"] = nil
      j = j + 1
+
end
    end
+
j = j + 1
  end
+
end
 +
end
  
  if j == 2 then
+
if j == 2 then
    return 0
+
return 0
  else
+
else
    return j - 2
+
return j - 2
  end
+
end
 +
end
  
end
 
  
 
--[[--------------------------< comma >-----------------------
 
--[[--------------------------< comma >-----------------------
  
    Given a date string, return "," if it's MDY  
+
Given a date string, return "," if it's MDY  
  
  ]]
+
]]
  
 
local function comma(date)
 
local function comma(date)
  local N = mw.text.split(date, " ")
+
return (date and date:match ('%a+ +%d%d?(,) +%d%d%d%d')) or '';
  local O = mw.text.split(N[1], "-") -- for ISO
 
  if O[1] == "index" then return "" end
 
  if not tonumber(O[1]) then
 
    return ","
 
  else
 
    return ""
 
  end
 
 
end
 
end
  
--[[--------------------------< createTracking >-----------------------
 
  
    Return data in track[] ie. tracking categories
+
--[[--------------------------< createRendering >-----------------------
 +
 
 +
Return a rendering of the data in ulx[][]
 +
 
 +
]]
 +
 
 +
local function createRendering()
 +
 
 +
local displayfield
 +
local out = {};
 +
 +
local period1 = ''; -- For backwards compat with {{wayback}}
 +
local period2 = '.';
 +
 
 +
local index_date, msg = ulx.url1.date:match ('(index)(.*)'); -- when ulx.url1.date extract 'index' text and message text (if there is a message)
 +
ulx.url1.date = ulx.url1.date:gsub ('index.*', 'index'); -- remove message
 +
 
 +
if 'none' == ulx.url1.format then -- For {{wayback}}, {{webcite}}
 +
table.insert (out, '['); -- open extlink markup
 +
table.insert (out, ulx.url1.url); -- add url
 +
 
 +
if ulx.url1.title then
 +
table.insert (out, ' ') -- the required space
 +
table.insert (out, ulx.url1.title) -- the title
 +
table.insert (out, ']'); -- close extlink markup
 +
table.insert (out, ulx.url1.tail); -- tail text
 +
if ulx.url1.date then
 +
table.insert (out, '&#32;('); -- open date text; TODO: why the html entity? replace with regular space?
 +
table.insert (out, 'index' == ulx.url1.date and s_text.archive or s_text.archived); -- add text
 +
table.insert (out, ' '); -- insert a space
 +
table.insert (out, ulx.url1.date); -- add date
 +
table.insert (out, ')'); -- close date text
 +
end
 +
else -- no title
 +
if index_date then -- when url date is 'index'
 +
table.insert (out, table.concat ({' ', s_text.Archive_index, ']'})); -- add the index link label
 +
table.insert (out, msg or ''); -- add date mismatch message when url date is /*/ and |date= has valid date
 +
else
 +
table.insert (out, table.concat ({' ', s_text.Archived, '] '})); -- add link label for url has timestamp date (will include mismatch message if there is one)
 +
end
 +
if ulx.url1.date then
 +
if 'wayback' == ulx.url1.service then
 +
period1 = '.';
 +
period2 = '';
 +
end
 +
if 'index' ~= ulx.url1.date then
 +
table.insert (out, ulx.url1.date); -- add date when data is not 'index'
 +
end
 +
table.insert (out, comma(ulx.url1.date)); -- add ',' if date format is mdy
 +
table.insert (out, ulx.url1.tail); -- add tail text
 +
table.insert (out, period1); -- terminate
 +
else -- no date
 +
table.insert (out, ulx.url1.tail); -- add tail text
 +
end
 +
end
 +
 
 +
if 0 < ulx.url1.extraurls then -- For multiple archive URLs
 +
local tot = ulx.url1.extraurls + 1
 +
table.insert (out, period2); -- terminate first url
 +
table.insert (out, table.concat ({' ', s_text.addlarchives, ': '})); -- add header text
  
  ]]
+
for i=2, tot do -- loop through the additionals
 +
local index = table.concat ({'url', i}); -- make an index
 +
displayfield = ulx[index]['title'] and 'title' or 'date'; -- choose display text
 +
table.insert (out, '['); -- open extlink markup
 +
table.insert (out, ulx[index]['url']); -- add the url
 +
table.insert (out, ' '); -- the required space
 +
table.insert (out, ulx[index][displayfield]); -- add the label
 +
table.insert (out, ']'); -- close extlink markup
 +
table.insert (out, i==tot and '.' or ', '); -- add terminator
 +
end
 +
end
 +
return table.concat (out); -- make a big string and done
  
local function createTracking()
+
else -- For {{cite archives}}
 +
if 'addlarchives' == ulx.url1.format then -- Multiple archive services
 +
table.insert (out, table.concat ({s_text.addlarchives, ': '})); -- add header text
 +
else -- Multiple pages from the same archive
 +
table.insert (out, table.concat ({s_text.addlpages, ' '})); -- add header text
 +
table.insert (out, ulx.url1.date); -- add date to header text
 +
table.insert (out, ': '); -- close header text
 +
end
  
  local sand = ""
+
local tot = ulx.url1.extraurls + 1;
  if tableLength(track) > 0 then                       
+
for i=1, tot do -- loop through the additionals
    for key,_ in pairs(track) do
+
local index = table.concat ({'url', i}); -- make an index
      sand = sand .. "[[" .. key .. "]]"
+
table.insert (out, '['); -- open extlink markup
    end
+
table.insert (out, ulx[index]['url']); -- add url
  end
+
table.insert (out, ' '); -- add required space
  return sand
 
  
 +
displayfield = ulx[index]['title'];
 +
if 'addlarchives' == ulx.url1.format then
 +
if not displayfield then
 +
displayfield = ulx[index]['date']
 +
end
 +
else -- must be addlpages
 +
if not displayfield then
 +
displayfield = table.concat ({s_text.Page, ' ', i});
 +
end
 +
end
 +
table.insert (out, displayfield); -- add title, date, page label text
 +
table.insert (out, ']'); -- close extlink markup
 +
table.insert (out, (i==tot and '.' or ', ')); -- add terminator
 +
end
 +
return table.concat (out); -- make a big string and done
 +
end
 
end
 
end
  
--[[--------------------------< createRendering >-----------------------
 
  
    Return a rendering of the data in ulx[][]
+
--[[--------------------------< P A R A M E T E R _ N A M E _ X L A T E >--------------------------------------
 +
 
 +
for internaltionalization, translate local-language parameter names to their English equivalents
  
  ]]
+
TODO: return error message if multiple aliases of the same canonical parameter name are found?
  
local function createRendering()
+
returns two tables:
 +
new_args - holds canonical form parameters and their values either from translation or because the parameter was already in canonical form
 +
origin - maps canonical-form parameter names to their untranslated (local language) form for error messaging in the local language
  
    local sand, displayheader, displayfield
+
unrecognized parameters are ignored
  
    local period1 = ""  -- For backwards compat with {{wayback}}
+
]]
    local period2 = "."                                                           
 
 
 
    local indexstr = "archived"
 
    if ulx.url1.date == "index" then
 
      indexstr = "archive"
 
    end 
 
                                                                                          -- For {{wayback}}, {{webcite}}
 
  
    if ulx.url1.format == "none" then                                                   
+
local function parameter_name_xlate (args, params, enum_params)
      if not ulx.url1.title and not ulx.url1.date then                                    -- No title. No date
+
local name; -- holds modifiable name of the parameter name during evaluation
        sand = "[" .. ulx.url1.url .. " Archived]" .. ulx.url1.tail
+
local enum; -- for enumerated parameters, holds the enumerator during evaluation
      elseif not ulx.url1.title and ulx.url1.date then                                    -- No title. Date.
+
local found = false; -- flag used to break out of nested for loops
        if ulx.url1.service == "wayback" then
+
local new_args = {}; -- a table that holds canonical and translated parameter k/v pairs
          period1 = "."
+
local origin = {}; -- a table that maps original (local language) parameter names to their canonical name for local language error messaging
          period2 = ""
+
local unnamed_params; -- set true when unsupported positional parameters are detected
        end
+
        sand = "[" .. ulx.url1.url .. " Archived] " .. ulx.url1.date .. comma(ulx.url1.date) .. ulx.url1.tail .. period1
+
for k, v in pairs (args) do -- loop through all of the arguments in the args table
      elseif ulx.url1.title and not ulx.url1.date then                                    -- Title. No date.
+
name = k; -- copy of original parameter name
        sand = "[" .. ulx.url1.url .. " " .. ulx.url1.title .. "]" .. ulx.url1.tail
 
      elseif ulx.url1.title and ulx.url1.date then                                        -- Title. Date.
 
        sand = "[" .. ulx.url1.url .. " " .. ulx.url1.title .. "]" .. ulx.url1.tail .. "&#32;(" .. indexstr .. " " .. ulx.url1.date .. ")"
 
      else
 
        return nil
 
      end
 
      if ulx.url1.extraurls > 0 then                                                      -- For multiple archive URLs
 
        local tot = ulx.url1.extraurls + 1
 
        sand = sand .. period2 .. " Additional archives: "
 
        for i=2,tot do
 
          local indx = "url" .. i
 
          if ulx[indx]["title"] then
 
            displayfield = "title"
 
          else
 
            displayfield = "date"
 
          end
 
          sand = sand .. "[" .. ulx[indx]["url"] .. " " .. ulx[indx][displayfield] .. "]"
 
          if i == tot then
 
            sand = sand .. "."
 
          else
 
            sand = sand .. ", "
 
          end
 
        end
 
      else
 
        return sand 
 
      end
 
      return sand
 
                                                                                          -- For {{cite archives}}
 
  
    else                                                                 
+
if 'string' == type (k) then
      if ulx.url1.format == "addlarchives" then                           -- Multiple archive services
+
if non_western_digits then -- true when non-western digits supported at this wiki
        displayheader = "Additional archives: "
+
name = mw.ustring.gsub (name, '%d', digits); -- convert this wiki's non-western digits to western digits
      else                                                                -- Multiple pages from the same archive
+
end
        displayheader = "Additional pages archived&nbsp;on " .. ulx.url1.date .. ": "
+
      end
+
enum = name:match ('%d+$'); -- get parameter enumerator if it exists; nil else
      local tot = 1 + ulx.url1.extraurls
+
      local sand = displayheader
+
if not enum then -- no enumerator so looking for non-enumnerated parameters
      for i=1,tot do
+
-- TODO: insert shortcut here? if params[name] then name holds the canonical parameter name; no need to search further
        local indx = "url" .. i
+
for pname, aliases in pairs (params) do -- loop through each parameter the params table
        displayfield = ulx[indx]["title"]
+
for _, alias in ipairs (aliases) do -- loop through each alias in the parameter's aliases table
        if ulx.url1.format == "addlarchives" then
+
if name == alias then
          if not displayfield then  
+
new_args[pname] = v; -- create a new entry in the new_args table
            displayfield = ulx[indx]["date"]
+
origin [pname] = k; -- create an entry to make canonical parameter name to original local language parameter name
          end
+
found = true; -- flag so that we can break out of these nested for loops
        else
+
break; -- no need to search the rest of the aliases table for name so go on to the next k, v pair
          if not displayfield then  
+
end
            displayfield = "Page " .. i
+
end
          end
+
        end
+
if found then -- true when we found an alias that matched name
        sand = sand .. "[" .. ulx[indx]["url"] .. " " .. displayfield .. "]"
+
found = false; -- reset the flag
        if i == tot then
+
break; -- go do next args k/v pair
          sand = sand .. "."
+
end
        else
+
end
          sand = sand .. ", "
+
else -- enumerated parameters
        end
+
name = name:gsub ('%d$', '#'); -- replace enumeration digits with place holder for table search
      end
+
-- TODO: insert shortcut here? if num_params[name] then name holds the canonical parameter name; no need to search further
      return sand
+
for pname, aliases in pairs (enum_params) do -- loop through each parameter the num_params table
    end
+
for _, alias in ipairs (aliases) do -- loop through each alias in the parameter's aliases table
 +
if name == alias then
 +
pname = pname:gsub ('#$', enum); -- replace the '#' place holder with the actual enumerator
 +
new_args[pname] = v; -- create a new entry in the new_args table
 +
origin [pname] = k; -- create an entry to make canonical parameter name to original local language parameter name
 +
found = true; -- flag so that we can break out of these nested for loops
 +
break; -- no need to search the rest of the aliases table for name so go on to the next k, v pair
 +
end
 +
end
 +
 +
if found then -- true when we found an alias that matched name
 +
found = false; -- reset the flag
 +
break; -- go do next args k/v pair
 +
end
 +
end
 +
end
 +
else
 +
unnamed_params = true; -- flag for unsupported positional parameters
 +
end
 +
end -- for k, v
 +
return new_args, origin, unnamed_params;
 
end
 
end
  
function p.webarchive(frame)
 
  args = frame.args
 
  if (args[1]==nil) and (args["url"]==nil) then          -- if no argument provided than check parent template/module args
 
    args = frame:getParent().args
 
  end
 
 
  local tname = "Webarchive"                              -- name of calling template. Change if template rename.
 
  ulx = {}                                                -- Associative array to hold template data
 
  track = {}                                              -- Associative array to hold tracking categories
 
  maxurls = 10                                            -- Max number of URLs allowed.
 
  local verifydates = "yes"                              -- See documentation. Set "no" to disable.
 
  
                                                          -- URL argument (first)
+
--[[--------------------------< W E B A R C H I V E >----------------------------------------------------------
 +
 
 +
template entry point
 +
 
 +
]]
 +
 
 +
local function webarchive(frame)
 +
local args = getArgs (frame);
 +
 
 +
local data = mw.loadData (table.concat ({ -- make a data module name; sandbox or live
 +
'Module:Webarchive/data',
 +
frame:getTitle():find('sandbox', 1, true) and '/sandbox' or '' -- this instance is ./sandbox then append /sandbox
 +
}));
 +
categories = data.categories; -- fill in the forward declarations
 +
config = data.config;
 +
if data.digits.enable then
 +
digits = data.digits; -- for i18n; table of digits in the local wiki's language
 +
non_western_digits = true; -- use_non_western_digits
 +
end
 +
err_warn_msgs = data.err_warn_msgs;
 +
excepted_pages = data.excepted_pages;
 +
month_num = data.month_num; -- for i18n; table of month names in the local wiki's language
 +
prefixes = data.prefixes;
 +
services = data.services;
 +
s_text = data.s_text;
 +
uncategorized_namespaces = data.uncategorized_namespaces;
 +
uncategorized_subpages = data.uncategorized_subpages;
 +
 
 +
local origin = {}; -- holds a map of English to local language parameter names used in the current template; not currently used
 +
local unnamed_params; -- boolean set to true when template call has unnamed parameters
 +
args, origin, unnamed_params = parameter_name_xlate (args, data.params, data.enum_params); -- translate parameter names in args to English
  
  local url1 = trimArg(args.url) or trimArg(args.url1)         
+
local date, format, msg, udate, uri, url;
  if not url1 then
+
local ldf = 'iso'; -- when there is no |date= parameter, render url dates in iso format
    return inlineError("url", "Empty.") .. createTracking()
+
  end
+
if args.url and args.url1 then -- URL argument (first)
  if mw.ustring.find( url1, "https://web.http", 1, plain ) then   -- track bug  
+
return inlineError (data.crit_err_msgs.conflicting, {origin.url, origin.url1});
    track["Category:Webarchive template errors"] = 1  
+
end
    return inlineError("url", "https://web.http") .. createTracking()
+
  end  
+
url = args.url or args.url1;
  if url1 == "https://web.archive.org/http:/" then                 -- track bug
+
    track["Category:Webarchive template errors"] = 1  
+
if not url then
    return inlineError("url", "Invalid URL") .. createTracking()
+
return inlineError (data.crit_err_msgs.empty);
  end
+
end
 +
-- these iabot bugs perportedly fixed; removing these causes lua script error
 +
--[[ -- at Template:Webarchive/testcases/Production; resolve that before deleting these tests
 +
if mw.ustring.find( url, "https://web.http", 1, true ) then -- track bug - TODO: IAbot bug; not known if the bug has been fixed; deferred
 +
track[categories.error] = 1;
 +
return inlineError (data.crit_err_msgs.iabot1);
 +
end  
 +
if url == "https://web.archive.org/http:/" then -- track bug - TODO: IAbot bug; not known if the bug has been fixed; deferred
 +
track[categories.error] = 1;
 +
return inlineError (data.crit_err_msgs.iabot2);
 +
end
 +
]]
  
  ulx.url1 = {}
+
if not (url:lower():find ('^http') or url:find ('^//')) then
  ulx.url1.url = url1
+
return inlineError (data.crit_err_msgs.invalid_url );
  local uri1 = mw.uri.new(ulx.url1.url)
+
end
  ulx.url1.host = uri1.host
 
  ulx.url1.extraurls = parseExtraArgs()
 
  
                                                          -- Nolink argument
+
ulx.url1 = {}
 +
ulx.url1.url = url
  
  local nolink = trimArg2(args.nolink)
+
ulx.url1.extraurls = parseExtraArgs(args)
  
  serviceName(uri1.host, nolink)
+
local good = false;
 +
good, uri = pcall (mw.uri.new, ulx.url1.url); -- get a table of uri parts from this url; protected mode to prevent lua error when ulx.url1.url is malformed
 +
 +
if not good or nil == uri.host then -- abandon when ulx.url1.url is malformed
 +
return inlineError (data.crit_err_msgs.invalid_url);
 +
end
 +
 +
serviceName(uri.host, args.nolink)
  
                                                          -- Date argument
+
if args.date and args.date1 then -- Date argument
 +
return inlineError (data.crit_err_msgs.conflicting, {origin.date, origin.date1});
 +
end
 +
 +
date = args.date or args.date1;
 +
date = date and date:gsub (' +', ' '); -- replace multiple spaces with a single space
  
  local date = trimArg(args.date) or trimArg(args.date1)
+
if date and config.verifydates then
  if date == "*" and ulx.url1.service == "wayback" then
+
if '*' == date then
    date = "index"
+
date = 'index';
  elseif date and ulx.url1.service == "wayback" and verifydates == "yes" then  
+
ldf = 'iso'; -- set to default format
    local ldf = dateFormat(date)
+
else
    if ldf then
+
date, ldf = decode_date (date); -- get an iso format date from date and get date's original format
      local udate = decodeWaybackDate( uri1.path, ldf )
+
end
      if udate ~= date then
+
end
        date = udate .. inlineRed("<sup>[Date mismatch]</sup>", "warning")     
 
      end
 
    end
 
  elseif date and ulx.url1.service == "webcite" and verifydates == "yes" then
 
    local ldf = dateFormat(date)
 
    if ldf then
 
      local udate = decodeWebciteDate( uri1.path, ldf )
 
      if udate == "query" then -- skip
 
      elseif udate ~= date then
 
        date = udate .. inlineRed("<sup>[Date mismatch]</sup>", "warning")     
 
      end
 
    end
 
  elseif not date and ulx.url1.service == "wayback" then
 
    date = decodeWaybackDate( uri1.path, "iso" )
 
    if not date then
 
      date = inlineRed("[Date error] (1)", "error")
 
    end
 
  elseif not date and ulx.url1.service == "webcite" then
 
    date = decodeWebciteDate( uri1.path, "iso" )
 
    if date == "query" then
 
      date = inlineRed("[Date missing]", "warning")
 
    elseif not date then
 
      date = inlineRed("[Date error] (1)", "error")
 
    end
 
  elseif not date then
 
    date = inlineRed("[Date missing]", "warning")
 
  end
 
  ulx.url1.date = date
 
  
                                                          -- Format argument
+
if 'wayback' == ulx.url1.service or 'locwebarchives' == ulx.url1.service then
 +
if date then
 +
if config.verifydates then
 +
if ldf then
 +
udate, msg = decodeWaybackDate (uri.path); -- get the url date in iso format and format of date in |date=; 'index' when wayback url date is *
 +
if not udate then -- this is the only 'fatal' error return
 +
return inlineError (data.crit_err_msgs[msg]);
 +
end
  
  local format = trimArg(args.format)
+
if udate ~= date then -- date comparison using iso format dates
  if not format then
+
date = udate;
    format = "none"
+
msg = table.concat ({
  else
+
inlineRed (err_warn_msgs.mismatch, 'warning'), -- add warning message
    if format == "addlpages" then
+
msg, -- add message if there is one
      if not ulx.url1.date then
+
});
        format = "none"
+
end
      end
+
end
    elseif format == "addlarchives" then
+
end
      format = "addlarchives"
+
else -- no |date=
    else
+
udate, msg = decodeWaybackDate (uri.path);
      format = "none"
 
    end
 
  end
 
  ulx.url1.format = format
 
  
                                                          -- Title argument
+
if not udate then -- this is the only 'fatal' error return
 +
return inlineError (data.crit_err_msgs[msg]);
 +
end
  
  local title = trimArg(args.title) or trimArg(args.title1)
+
if '' == udate then
  ulx.url1.title = title
+
date = nil; -- unset
 
+
else
 +
date = udate;
 +
end
 +
end
  
  local rend = createRendering()
+
elseif 'webcite' == ulx.url1.service then
  if not rend then
+
if date then
    rend = '<span style="font-size:100%" class="error citation-comment">Error in [[:Template:' .. tname .. ']]: Unknown problem. Please report on template talk page.</span>'
+
if config.verifydates then
    track["Category:Webarchive template errors"] = 1
+
if ldf then
  end
+
udate = decodeWebciteDate (uri.path); -- get the url date in iso format
 +
if 'query' ~= udate then -- skip if query
 +
if udate ~= date then -- date comparison using iso format dates
 +
date = udate;
 +
msg = table.concat ({
 +
inlineRed (err_warn_msgs.mismatch, 'warning'),
 +
});
 +
end
 +
end
 +
end
 +
end
 +
else
 +
date = decodeWebciteDate( uri.path, "iso" )
 +
if date == "query" then
 +
date = nil; -- unset
 +
msg = inlineRed (err_warn_msgs.date_miss, 'warning');
 +
elseif not date then -- invalid base62 string
 +
date = inlineRed (err_warn_msgs.date1, 'error');
 +
end
 +
end
  
  return rend .. createTracking()
+
elseif 'archiveis' == ulx.url1.service then
 +
if date then
 +
if config.verifydates then
 +
if ldf then
 +
udate, msg = decodeArchiveisDate (uri.path) -- get the url date in iso format
 +
if 'short link' ~= udate then -- skip if short link
 +
if udate ~= date then -- date comparison using iso format dates
 +
date = udate;
 +
msg = table.concat ({
 +
inlineRed (err_warn_msgs.mismatch, 'warning'), -- add warning message
 +
msg, -- add message if there is one
 +
});
 +
end
 +
end
 +
end
 +
end
 +
else -- no |date=
 +
udate, msg = decodeArchiveisDate( uri.path, "iso" )
 +
if udate == "short link" then
 +
date = nil; -- unset
 +
msg = inlineRed (err_warn_msgs.date_miss, 'warning');
 +
elseif '' == udate then
 +
date = nil; -- unset
 +
else
 +
date = udate;
 +
end
 +
end
 +
 +
else -- some other service
 +
if not date then
 +
msg = inlineRed (err_warn_msgs.date_miss, 'warning');
 +
end
 +
end
 +
 
 +
if 'index' == date then
 +
ulx.url1.date = date .. (msg or ''); -- create index + message (if there is one)
 +
elseif date then
 +
ulx.url1.date = makeDate (date, nil, nil, ldf) .. (msg or ''); -- create a date in the wiki's local language + message (if there is one)
 +
else
 +
ulx.url1.date = msg;
 +
end
 +
 
 +
format = args.format; -- Format argument
 +
 
 +
if not format then
 +
format = "none"
 +
else
 +
for k, v in pairs (data.format_vals) do -- |format= accepts two specific values loop through a table of those values
 +
local found; -- declare a nil flag
 +
for _, p in ipairs (v) do -- loop through local language variants
 +
if format == p then -- when |format= value matches
 +
format = k; -- use name from table key
 +
found = true; -- declare found so that we can break out of outer for loop
 +
break; -- break out of inner for loop
 +
end
 +
end
 +
 +
if found then
 +
break;
 +
end
 +
end
 +
 
 +
if format == "addlpages" then
 +
if not ulx.url1.date then
 +
format = "none"
 +
end
 +
elseif format == "addlarchives" then
 +
format = "addlarchives"
 +
else
 +
format = "none"
 +
end
 +
end
 +
ulx.url1.format = format
 +
 
 +
if args.title and args.title1 then -- Title argument
 +
return inlineError (data.crit_err_msgs.conflicting, {origin.title, origin.title1});
 +
end
 +
 
 +
ulx.url1.title = args.title or args.title1;
 +
 
 +
local rend = createRendering()
 +
if not rend then
 +
return inlineError (data.crit_err_msgs.unknown);
 +
end
 +
 
 +
return rend .. ((unnamed_params and inlineRed (err_warn_msgs.unnamed_params, 'warning')) or '') .. createTracking();
  
 
end
 
end
  
return p
+
 
 +
--[[--------------------------< E X P O R T E D F U N C T I O N S >------------------------------------------
 +
]]
 +
 
 +
return {webarchive = webarchive};

Latest revision as of 14:49, 12 November 2018

--[[ ----------------------------------

Lua module implementing the Error in Webarchive template: Empty url. template.

A merger of the functionality of three templates: Template:Wayback, Template:Webcite and Template:Cite archives

]]


--[[--------------------------< D E P E N D E N C I E S >------------------------------------------------------ ]]

require('Module:No globals'); local getArgs = require ('Module:Arguments').getArgs;


--[[--------------------------< F O R W A R D D E C L A R A T I O N S >-------------------------------------- ]]

local categories = {}; -- category names local config = {}; -- global configuration settings local digits = {}; -- for i18n; table that translates local-wiki digits to western digits local err_warn_msgs = {}; -- error and warning messages local excepted_pages = {}; local month_num = {}; -- for i18n; table that translates local-wiki month names to western digits local prefixes = {}; -- service provider tail string prefixes local services = {}; -- archive service provider data from local s_text = {}; -- table of static text strings used to build final rendering local uncategorized_namespaces = {}; -- list of namespaces that we should not categorize local uncategorized_subpages = {}; -- list of subpages that should not be categorized


--[[--------------------------< P A G E S C O P E I D E N T I F I E R S >---------------------------------- ]]

local non_western_digits; -- boolean flag set true when data.digits.enable is true local this_page = mw.title.getCurrentTitle();

local track = {}; -- Associative array to hold tracking categories local ulx = {}; -- Associative array to hold template data


--[[--------------------------< S U B S T I T U T E >----------------------------------------------------------

Populates numbered arguments in a message string using an argument table.

]]

local function substitute (msg, args) return args and mw.message.newRawMessage (msg, args):plain() or msg; end


--[[--------------------------< tableLength >-----------------------

Given a 1-D table, return number of elements

]]

local function tableLength(T) local count = 0 for _ in pairs(T) do count = count + 1 end return count end


--[=[-------------------------< M A K E _ W I K I L I N K >----------------------------------------------------

Makes a wikilink; when both link and display text is provided, returns a wikilink in the form D; if only link is provided, returns a wikilink in the form L; if neither are provided or link is omitted, returns an empty string.

]=]

local function make_wikilink (link, display, no_link) if nil == no_link then if link and ( ~= link) then if display and ( ~= display) then return table.concat ({'', display, ''}); else return table.concat ({'', link, ''}); end end return display or ; -- link not set so return the display text

else -- no_link if display and ( ~= display) then -- if there is display text return display; -- return that else return link or ; -- return the target article name or empty string end end end


--[[--------------------------< createTracking >-----------------------

Return data in track[] ie. tracking categories

]]

local function createTracking() if not excepted_pages[this_page.fullText] then -- namespace:title/fragment is allowed to be categorized (typically this module's / template's testcases page(s)) if uncategorized_namespaces[this_page.nsText] then return ; -- this page not to be categorized so return empty string end for _,v in ipairs (uncategorized_subpages) do -- cycle through page name patterns if this_page.text:match (v) then -- test page name against each pattern return ; -- this subpage type not to be categorized so return empty string end end end

local out = {}; if tableLength(track) > 0 then for key, _ in pairs(track) do -- loop through table table.insert (out, make_wikilink (key)); -- and convert category names to links end end return table.concat (out); -- concat into one big string; empty string if table is empty

end


--[[--------------------------< inlineError >-----------------------

Critical error. Render output completely in red. Add to tracking category.

This function called as the last thing before abandoning this module

]]

local function inlineError (msg, args) track[categories.error] = 1 return table.concat ({ 'Error in ', -- open the error message span config.tname, -- insert the local language template name ' template: ', substitute (msg, args), -- insert the formatted error message '.', -- close the span createTracking() -- add the category }) end


--[[--------------------------< inlineRed >-----------------------

Render a text fragment in red, such as a warning as part of the final output. Add tracking category.

]]

local function inlineRed(msg, trackmsg) if trackmsg == "warning" then track[categories.warning] = 1; elseif trackmsg == "error" then track[categories.error] = 1; end

return '' .. msg .. '' end


--[[--------------------------< base62 >-----------------------

Convert base-62 to base-10 Credit: https://de.wikipedia.org/wiki/Modul:Expr

]]

local function base62( value ) local r = 1 -- default return value is input value is malformed

if value:match ('%W') then -- value must only be in the set [0-9a-zA-Z] return; -- nil return when value contains extraneous characters end

local n = #value -- number of characters in value local k = 1 local c r = 0 for i = n, 1, -1 do -- loop through all characters in value from ls digit to ms digit c = value:byte( i, i ) if c >= 48 and c <= 57 then -- character is digit 0-9 c = c - 48 elseif c >= 65 and c <= 90 then -- character is ascii a-z c = c - 55 else -- must be ascii A-Z c = c - 61 end r = r + c * k -- accumulate this base62 character's value k = k * 62 -- bump for next end -- for i

return r end


--[[--------------------------< D E C O D E _ D A T E >--------------------------------------------------------

Given a date string, return it in iso format along with an indicator of the date's format. Except that month names must be recognizable as legitimate month names with proper capitalization, and that the date string must match one of the recognized date formats, no error checking is done here; return nil else

]]

local function decode_date (date_str) local patterns = { ['dmy'] = {'^(%d%d?) +([^%s%d]+) +(%d%d%d%d)$', 'd', 'm', 'y'}, -- %a does not recognize unicode combining characters used by some languages ['mdy'] = {'^([^%s%d]+) (%d%d?), +(%d%d%d%d)$', 'm', 'd', 'y'}, ['ymd'] = {'^(%d%d%d%d) +([^%s%d]+) (%d%d?)$', 'y', 'm', 'd'}, -- not mos compliant at en.wiki but may be acceptible at other wikis };

local t = {};

if non_western_digits then -- this wiki uses non-western digits? date_str = mw.ustring.gsub (date_str, '%d', digits); -- convert this wiki's non-western digits to western digits end

if date_str:match ('^%d%d%d%d%-%d%d%-%d%d$') then -- already an iso format date, return western digits form return date_str, 'iso'; end

for k, v in pairs (patterns) do local c1, c2, c3 = mw.ustring.match (date_str, patterns[k][1]); -- c1 .. c3 are captured but we don't know what they hold

if c1 then -- set on match t = { -- translate unspecified captures to y, m, and d [patterns[k][2]] = c1, -- fill the table of captures with the captures [patterns[k][3]] = c2, -- take index names from src_pattern table and assign sequential captures [patterns[k][4]] = c3, }; if month_num[t.m] then -- when month not already a number t.m = month_num[t.m]; -- replace valid month name with a number else return nil, 'iso'; -- not a valid date form because month not valid end

return mw.ustring.format ('%.4d-%.2d-%.2d', t.y, t.m, t.d), k; -- return date in iso format end end return nil, 'iso'; -- date could not be decoded; return nil and default iso date end


--[[--------------------------< makeDate >-----------------------

Given year, month, day numbers, (zero-padded or not) return a full date in df format where df may be one of: mdy, dmy, iso, ymd

on entry, year, month, day are presumed to be correct for the date that they represent; all are required

in this module, makeDate() is sometimes given an iso-format date in year: makeDate (2018-09-20, nil, nil, df) this works because table.concat() sees only one table member

]]

local function makeDate (year, month, day, df) local format = { ['dmy'] = 'j F Y', ['mdy'] = 'F j, Y', ['ymd'] = 'Y F j', ['iso'] = 'Y-m-d', };

local date = table.concat ({year, month, day}, '-'); -- assemble year-initial numeric-format date (zero padding not required here)

if non_western_digits then --this wiki uses non-western digits? date = mw.ustring.gsub (date, '%d', digits); -- convert this wiki's non-western digits to western digits end

return mw.getContentLanguage():formatDate (format[df], date); end


--[[--------------------------< I S _ V A L I D _ D A T E >----------------------------------------------------

Returns true if date is after 31 December 1899 (why is 1900 the min year? shouldn't the internet's date-of-birth be min year?), not after today's date, and represents a valid date (29 February 2017 is not a valid date). Applies Gregorian leapyear rules.

all arguments are required

]]

local function is_valid_date (year, month, day) local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; local month_length; local y, m, d; local today = os.date ('*t'); -- fetch a table of current date parts

if not year or == year or not month or == month or not day or == day then return false; -- something missing end

y = tonumber (year); m = tonumber (month); d = tonumber (day);

if 1900 > y or today.year < y or 1 > m or 12 < m then -- year and month are within bounds TODO: 1900? return false; end

if (2==m) then -- if February month_length = 28; -- then 28 days unless if (0==(y%4) and (0~=(y%100) or 0==(y%400))) then -- is a leap year? month_length = 29; -- if leap year then 29 days in February end else month_length=days_in_month[m]; end

if 1 > d or month_length < d then -- day is within bounds return false; end -- here when date parts represent a valid date return os.time({['year']=y, ['month']=m, ['day']=d, ['hour']=0}) <= os.time(); -- date at midnight must be less than or equal to current date/time end


--[[--------------------------< decodeWebciteDate >-----------------------

Given a URI-path to Webcite (eg. /67xHmVFWP) return the encoded date in df format

returns date string in df format - webcite date is a unix timestamp encoded as bae62 or the string 'query'

]]

local function decodeWebciteDate(path, df)

local dt = {}; local decode;

dt = mw.text.split(path, "/")

-- valid URL formats that are not base62

-- http://www.webcitation.org/query?id=1138911916587475 -- http://www.webcitation.org/query?url=http..&date=2012-06-01+21:40:03 -- http://www.webcitation.org/1138911916587475 -- http://www.webcitation.org/cache/73e53dd1f16cf8c5da298418d2a6e452870cf50e -- http://www.webcitation.org/getfile.php?fileid=1c46e791d68e89e12d0c2532cc3cf629b8bc8c8e

if dt[2]:find ('query', 1, true) or dt[2]:find ('cache', 1, true) or dt[2]:find ('getfile', 1, true) or tonumber(dt[2]) then return 'query'; end

decode = base62(dt[2]); -- base62 string -> exponential number if not decode then return nil; -- nil return when dt[2] contains characters not in %w end dt = os.date('*t', string.format("%d", decode):sub(1,10)) -- exponential number -> text -> first 10 characters (a unix timestamp) -> a table of date parts

decode = makeDate (dt.year, dt.month, dt.day, 'iso'); -- date comparisons are all done in iso format with western digits if non_western_digits then --this wiki uses non-western digits? decode = mw.ustring.gsub (decode, '%d', digits); -- convert this wiki's non-western digits to western digits end

return decode; end


--[[--------------------------< decodeWaybackDate >-----------------------

Given a URI-path to Wayback (eg. /web/20160901010101/http://example.com ) or Library of Congress Web Archives (/all/20160901010101/http://example.com) return the formatted date eg. "September 1, 2016" in df format Handle non-digits in snapshot ID such as "re_" and "-" and "*"

returns two values: first value is one of these: valid date string in df format - wayback date is valid (including the text string 'index' when date is '/*/') empty string - wayback date is malformed (less than 8 digits, not a valid date) nil - wayback date is '/save/' or otherwise not a number

second return value is an appropriate 'message' may or may not be formatted

]]

local function decodeWaybackDate(path, df)

local msg, snapdate;

snapdate = path:gsub ('^/all/', ):gsub ('^/web/', ):gsub ('^/', ); -- remove leading '/all/', leading '/web/' or leading '/' snapdate = snapdate:match ('^[^/]+'); -- get timestamp if snapdate == "*" then -- eg. /web/*/http.. or /all/*/http.. return 'index'; -- return indicator that this url has an index date end

snapdate = snapdate:gsub ('%a%a_%d?$', ):gsub ('%-', ); -- from date, remove any trailing "re_", dashes

msg = ; if snapdate:match ('%*$') then -- a trailing '*' causes calendar display at archive .org snapdate = snapdate:gsub ('%*$', ); -- remove so not part of length calc later msg = inlineRed (err_warn_msgs.ts_cal, 'warning'); -- make a message end

if not tonumber(snapdate) then return nil, 'ts_nan'; -- return nil (fatal error flag) and message selector end

local dlen = snapdate:len(); if dlen < 8 then -- we need 8 digits TODO: but shouldn't this be testing for 14 digits? return , inlineRed (err_warn_msgs.ts_short, 'error'); -- return empty string and error message end

local year, month, day = snapdate:match ('(%d%d%d%d)(%d%d)(%d%d)'); -- no need for snapdatelong here

if not is_valid_date (year, month, day) then return , inlineRed (err_warn_msgs.ts_date, 'error'); -- return empty string and error message end

snapdate = table.concat ({year, month, day}, '-'); -- date comparisons are all done in iso format if 14 == dlen then return snapdate, msg; -- return date with message if any else return snapdate, msg .. inlineRed (err_warn_msgs.ts_len, 'warning'); -- return date with warning message(s) end end


--[[--------------------------< decodeArchiveisDate >-----------------------

Given an Archive.is "long link" URI-path (e.g. /2016.08.28-144552/http://example.com) return the date in df format (e.g. if df = dmy, return 28 August 2016) Handles "." and "-" in snapshot date, so 2016.08.28-144552 is same as 20160828144552

returns two values: first value is one of these: valid date string in df format - archive.is date is valid (including the text string 'short link' when url is the short form) empty string - wayback date is malformed (not a number, less than 8 digits, not a valid date) nil - wayback date is '/save/'

second return value is an appropriate 'message' may or may not be formatted

]]

local function decodeArchiveisDate(path, df) local snapdate

if path:match ('^/%w+$') then -- short form url path is '/' followed by some number of base 62 digits and nothing else return "short link" -- e.g. http://archive.is/hD1qz end

snapdate = mw.text.split (path, '/')[2]:gsub('[%.%-]', ); -- get snapshot date, e.g. 2016.08.28-144552; remove periods and hyphens

local dlen = string.len(snapdate) if dlen < 8 then -- we need 8 digits TODO: but shouldn't this be testing for 14 digits? return , inlineRed (err_warn_msgs.ts_short, 'error'); -- return empty string and error message end

local year, month, day = snapdate:match ('(%d%d%d%d)(%d%d)(%d%d)'); -- no need for snapdatelong here

if not is_valid_date (year, month, day) then return , inlineRed (err_warn_msgs.ts_date, 'error'); -- return empty string and error message end

snapdate = table.concat ({year, month, day}, '-'); -- date comparisons are all done in iso format if 14 == dlen then return snapdate; -- return date else return snapdate, inlineRed (err_warn_msgs.ts_len, 'warning'); -- return date with warning message end

end


--[[--------------------------< serviceName >-----------------------

Given a domain extracted by mw.uri.new() (eg. web.archive.org) set tail string and service ID

]]

local function serviceName(host, no_link) local tracking; local index;

host = host:lower():gsub ('^web%.(.+)', '%1'):gsub ('^www%.(.+)', '%1'); -- lowercase, remove web. and www. subdomains

if services[host] then index = host; else for k, _ in pairs (services) do if host:find ('%f[%a]'..k:gsub ('([%.%-])', '%%%1')) then index = k; break; end end end

if index then local out = {}; -- empty string in [1] so that concatenated result has leading single space ulx.url1.service = services[index][4] or 'other'; tracking = services[index][5] or categories.other; -- build tail string if false == services[index][1] then -- select prefix table.insert (out, prefixes.at); elseif true == services[index][1] then table.insert (out, prefixes.atthe); else table.insert (out, services[index][1]); end

table.insert (out, make_wikilink (services[index][2], services[index][3], no_link)); -- add article wikilink if services[index][6] then -- add tail postfix if it exists table.insert (out, services[index][6]); end

ulx.url1.tail = table.concat (out, ' '); -- put it all together; result has leading space character

else -- here when unknown archive ulx.url1.service = 'other'; tracking = categories.unknown; ulx.url1.tail = table.concat ({, prefixes.at, host, inlineRed (err_warn_msgs.unknown_url, error)}, ' '); end

track[tracking] = 1 end


--[[--------------------------< parseExtraArgs >-----------------------

Parse numbered arguments starting at 2, such as url2..url10, date2..date10, title2..title10 For example: Error in Webarchive template: Invalid URL. Three url arguments not in numeric sequence (1..4..7). Function only processes arguments numbered 2 or greater (in this case 4 and 7) It creates numeric sequenced table entries like: urlx.url2.url = <argument value for url4> urlx.url3.url = <argument value for url7> Returns the number of URL arguments found numbered 2 or greater (in this case returns "2")

]]

local function parseExtraArgs(args)

local i, j, argurl, argurl2, argdate, argtitle

j = 2 for i = 2, config.maxurls do argurl = "url" .. i if args[argurl] then argurl2 = "url" .. j ulx[argurl2] = {} ulx[argurl2]["url"] = args[argurl] argdate = "date" .. j if args[argdate] then ulx[argurl2]["date"] = args[argdate] else ulx[argurl2]["date"] = inlineRed (err_warn_msgs.date_miss, 'warning'); end

argtitle = "title" .. j if args[argtitle] then ulx[argurl2]["title"] = args[argtitle] else ulx[argurl2]["title"] = nil end j = j + 1 end end

if j == 2 then return 0 else return j - 2 end end


--[[--------------------------< comma >-----------------------

Given a date string, return "," if it's MDY

]]

local function comma(date) return (date and date:match ('%a+ +%d%d?(,) +%d%d%d%d')) or ; end


--[[--------------------------< createRendering >-----------------------

Return a rendering of the data in ulx[][]

]]

local function createRendering()

local displayfield local out = {};

local period1 = ; -- For backwards compat with Template:Wayback local period2 = '.';

local index_date, msg = ulx.url1.date:match ('(index)(.*)'); -- when ulx.url1.date extract 'index' text and message text (if there is a message) ulx.url1.date = ulx.url1.date:gsub ('index.*', 'index'); -- remove message

if 'none' == ulx.url1.format then -- For Template:Wayback, Template:Webcite table.insert (out, '['); -- open extlink markup table.insert (out, ulx.url1.url); -- add url

if ulx.url1.title then table.insert (out, ' ') -- the required space table.insert (out, ulx.url1.title) -- the title table.insert (out, ']'); -- close extlink markup table.insert (out, ulx.url1.tail); -- tail text if ulx.url1.date then table.insert (out, ' ('); -- open date text; TODO: why the html entity? replace with regular space? table.insert (out, 'index' == ulx.url1.date and s_text.archive or s_text.archived); -- add text table.insert (out, ' '); -- insert a space table.insert (out, ulx.url1.date); -- add date table.insert (out, ')'); -- close date text end else -- no title if index_date then -- when url date is 'index' table.insert (out, table.concat ({' ', s_text.Archive_index, ']'})); -- add the index link label table.insert (out, msg or ); -- add date mismatch message when url date is /*/ and |date= has valid date else table.insert (out, table.concat ({' ', s_text.Archived, '] '})); -- add link label for url has timestamp date (will include mismatch message if there is one) end if ulx.url1.date then if 'wayback' == ulx.url1.service then period1 = '.'; period2 = ; end if 'index' ~= ulx.url1.date then table.insert (out, ulx.url1.date); -- add date when data is not 'index' end table.insert (out, comma(ulx.url1.date)); -- add ',' if date format is mdy table.insert (out, ulx.url1.tail); -- add tail text table.insert (out, period1); -- terminate else -- no date table.insert (out, ulx.url1.tail); -- add tail text end end

if 0 < ulx.url1.extraurls then -- For multiple archive URLs local tot = ulx.url1.extraurls + 1 table.insert (out, period2); -- terminate first url table.insert (out, table.concat ({' ', s_text.addlarchives, ': '})); -- add header text

for i=2, tot do -- loop through the additionals local index = table.concat ({'url', i}); -- make an index displayfield = ulx[index]['title'] and 'title' or 'date'; -- choose display text table.insert (out, '['); -- open extlink markup table.insert (out, ulx[index]['url']); -- add the url table.insert (out, ' '); -- the required space table.insert (out, ulx[index][displayfield]); -- add the label table.insert (out, ']'); -- close extlink markup table.insert (out, i==tot and '.' or ', '); -- add terminator end end return table.concat (out); -- make a big string and done

else -- For Template:Cite archives if 'addlarchives' == ulx.url1.format then -- Multiple archive services table.insert (out, table.concat ({s_text.addlarchives, ': '})); -- add header text else -- Multiple pages from the same archive table.insert (out, table.concat ({s_text.addlpages, ' '})); -- add header text table.insert (out, ulx.url1.date); -- add date to header text table.insert (out, ': '); -- close header text end

local tot = ulx.url1.extraurls + 1; for i=1, tot do -- loop through the additionals local index = table.concat ({'url', i}); -- make an index table.insert (out, '['); -- open extlink markup table.insert (out, ulx[index]['url']); -- add url table.insert (out, ' '); -- add required space

displayfield = ulx[index]['title']; if 'addlarchives' == ulx.url1.format then if not displayfield then displayfield = ulx[index]['date'] end else -- must be addlpages if not displayfield then displayfield = table.concat ({s_text.Page, ' ', i}); end end table.insert (out, displayfield); -- add title, date, page label text table.insert (out, ']'); -- close extlink markup table.insert (out, (i==tot and '.' or ', ')); -- add terminator end return table.concat (out); -- make a big string and done end end


--[[--------------------------< P A R A M E T E R _ N A M E _ X L A T E >--------------------------------------

for internaltionalization, translate local-language parameter names to their English equivalents

TODO: return error message if multiple aliases of the same canonical parameter name are found?

returns two tables: new_args - holds canonical form parameters and their values either from translation or because the parameter was already in canonical form origin - maps canonical-form parameter names to their untranslated (local language) form for error messaging in the local language

unrecognized parameters are ignored

]]

local function parameter_name_xlate (args, params, enum_params) local name; -- holds modifiable name of the parameter name during evaluation local enum; -- for enumerated parameters, holds the enumerator during evaluation local found = false; -- flag used to break out of nested for loops local new_args = {}; -- a table that holds canonical and translated parameter k/v pairs local origin = {}; -- a table that maps original (local language) parameter names to their canonical name for local language error messaging local unnamed_params; -- set true when unsupported positional parameters are detected

for k, v in pairs (args) do -- loop through all of the arguments in the args table name = k; -- copy of original parameter name

if 'string' == type (k) then if non_western_digits then -- true when non-western digits supported at this wiki name = mw.ustring.gsub (name, '%d', digits); -- convert this wiki's non-western digits to western digits end

enum = name:match ('%d+$'); -- get parameter enumerator if it exists; nil else

if not enum then -- no enumerator so looking for non-enumnerated parameters -- TODO: insert shortcut here? if params[name] then name holds the canonical parameter name; no need to search further for pname, aliases in pairs (params) do -- loop through each parameter the params table for _, alias in ipairs (aliases) do -- loop through each alias in the parameter's aliases table if name == alias then new_args[pname] = v; -- create a new entry in the new_args table origin [pname] = k; -- create an entry to make canonical parameter name to original local language parameter name found = true; -- flag so that we can break out of these nested for loops break; -- no need to search the rest of the aliases table for name so go on to the next k, v pair end end

if found then -- true when we found an alias that matched name found = false; -- reset the flag break; -- go do next args k/v pair end end else -- enumerated parameters name = name:gsub ('%d$', '#'); -- replace enumeration digits with place holder for table search -- TODO: insert shortcut here? if num_params[name] then name holds the canonical parameter name; no need to search further for pname, aliases in pairs (enum_params) do -- loop through each parameter the num_params table for _, alias in ipairs (aliases) do -- loop through each alias in the parameter's aliases table if name == alias then pname = pname:gsub ('#$', enum); -- replace the '#' place holder with the actual enumerator new_args[pname] = v; -- create a new entry in the new_args table origin [pname] = k; -- create an entry to make canonical parameter name to original local language parameter name found = true; -- flag so that we can break out of these nested for loops break; -- no need to search the rest of the aliases table for name so go on to the next k, v pair end end

if found then -- true when we found an alias that matched name found = false; -- reset the flag break; -- go do next args k/v pair end end end else unnamed_params = true; -- flag for unsupported positional parameters end end -- for k, v return new_args, origin, unnamed_params; end


--[[--------------------------< W E B A R C H I V E >----------------------------------------------------------

template entry point

]]

local function webarchive(frame) local args = getArgs (frame);

local data = mw.loadData (table.concat ({ -- make a data module name; sandbox or live 'Module:Webarchive/data', frame:getTitle():find('sandbox', 1, true) and '/sandbox' or -- this instance is ./sandbox then append /sandbox })); categories = data.categories; -- fill in the forward declarations config = data.config; if data.digits.enable then digits = data.digits; -- for i18n; table of digits in the local wiki's language non_western_digits = true; -- use_non_western_digits end err_warn_msgs = data.err_warn_msgs; excepted_pages = data.excepted_pages; month_num = data.month_num; -- for i18n; table of month names in the local wiki's language prefixes = data.prefixes; services = data.services; s_text = data.s_text; uncategorized_namespaces = data.uncategorized_namespaces; uncategorized_subpages = data.uncategorized_subpages;

local origin = {}; -- holds a map of English to local language parameter names used in the current template; not currently used local unnamed_params; -- boolean set to true when template call has unnamed parameters args, origin, unnamed_params = parameter_name_xlate (args, data.params, data.enum_params); -- translate parameter names in args to English

local date, format, msg, udate, uri, url; local ldf = 'iso'; -- when there is no |date= parameter, render url dates in iso format

if args.url and args.url1 then -- URL argument (first) return inlineError (data.crit_err_msgs.conflicting, {origin.url, origin.url1}); end

url = args.url or args.url1;

if not url then return inlineError (data.crit_err_msgs.empty); end -- these iabot bugs perportedly fixed; removing these causes lua script error --[[ -- at Template:Webarchive/testcases/Production; resolve that before deleting these tests if mw.ustring.find( url, "https://web.http", 1, true ) then -- track bug - TODO: IAbot bug; not known if the bug has been fixed; deferred track[categories.error] = 1; return inlineError (data.crit_err_msgs.iabot1); end if url == "https://web.archive.org/http:/" then -- track bug - TODO: IAbot bug; not known if the bug has been fixed; deferred track[categories.error] = 1; return inlineError (data.crit_err_msgs.iabot2); end ]]

if not (url:lower():find ('^http') or url:find ('^//')) then return inlineError (data.crit_err_msgs.invalid_url ); end

ulx.url1 = {} ulx.url1.url = url

ulx.url1.extraurls = parseExtraArgs(args)

local good = false; good, uri = pcall (mw.uri.new, ulx.url1.url); -- get a table of uri parts from this url; protected mode to prevent lua error when ulx.url1.url is malformed

if not good or nil == uri.host then -- abandon when ulx.url1.url is malformed return inlineError (data.crit_err_msgs.invalid_url); end

serviceName(uri.host, args.nolink)

if args.date and args.date1 then -- Date argument return inlineError (data.crit_err_msgs.conflicting, {origin.date, origin.date1}); end

date = args.date or args.date1; date = date and date:gsub (' +', ' '); -- replace multiple spaces with a single space

if date and config.verifydates then if '*' == date then date = 'index'; ldf = 'iso'; -- set to default format else date, ldf = decode_date (date); -- get an iso format date from date and get date's original format end end

if 'wayback' == ulx.url1.service or 'locwebarchives' == ulx.url1.service then if date then if config.verifydates then if ldf then udate, msg = decodeWaybackDate (uri.path); -- get the url date in iso format and format of date in |date=; 'index' when wayback url date is * if not udate then -- this is the only 'fatal' error return return inlineError (data.crit_err_msgs[msg]); end

if udate ~= date then -- date comparison using iso format dates date = udate; msg = table.concat ({ inlineRed (err_warn_msgs.mismatch, 'warning'), -- add warning message msg, -- add message if there is one }); end end end else -- no |date= udate, msg = decodeWaybackDate (uri.path);

if not udate then -- this is the only 'fatal' error return return inlineError (data.crit_err_msgs[msg]); end

if == udate then date = nil; -- unset else date = udate; end end

elseif 'webcite' == ulx.url1.service then if date then if config.verifydates then if ldf then udate = decodeWebciteDate (uri.path); -- get the url date in iso format if 'query' ~= udate then -- skip if query if udate ~= date then -- date comparison using iso format dates date = udate; msg = table.concat ({ inlineRed (err_warn_msgs.mismatch, 'warning'), }); end end end end else date = decodeWebciteDate( uri.path, "iso" ) if date == "query" then date = nil; -- unset msg = inlineRed (err_warn_msgs.date_miss, 'warning'); elseif not date then -- invalid base62 string date = inlineRed (err_warn_msgs.date1, 'error'); end end

elseif 'archiveis' == ulx.url1.service then if date then if config.verifydates then if ldf then udate, msg = decodeArchiveisDate (uri.path) -- get the url date in iso format if 'short link' ~= udate then -- skip if short link if udate ~= date then -- date comparison using iso format dates date = udate; msg = table.concat ({ inlineRed (err_warn_msgs.mismatch, 'warning'), -- add warning message msg, -- add message if there is one }); end end end end else -- no |date= udate, msg = decodeArchiveisDate( uri.path, "iso" ) if udate == "short link" then date = nil; -- unset msg = inlineRed (err_warn_msgs.date_miss, 'warning'); elseif == udate then date = nil; -- unset else date = udate; end end

else -- some other service if not date then msg = inlineRed (err_warn_msgs.date_miss, 'warning'); end end

if 'index' == date then ulx.url1.date = date .. (msg or ); -- create index + message (if there is one) elseif date then ulx.url1.date = makeDate (date, nil, nil, ldf) .. (msg or ); -- create a date in the wiki's local language + message (if there is one) else ulx.url1.date = msg; end

format = args.format; -- Format argument

if not format then format = "none" else for k, v in pairs (data.format_vals) do -- |format= accepts two specific values loop through a table of those values local found; -- declare a nil flag for _, p in ipairs (v) do -- loop through local language variants if format == p then -- when |format= value matches format = k; -- use name from table key found = true; -- declare found so that we can break out of outer for loop break; -- break out of inner for loop end end

if found then break; end end

if format == "addlpages" then if not ulx.url1.date then format = "none" end elseif format == "addlarchives" then format = "addlarchives" else format = "none" end end ulx.url1.format = format

if args.title and args.title1 then -- Title argument return inlineError (data.crit_err_msgs.conflicting, {origin.title, origin.title1}); end

ulx.url1.title = args.title or args.title1;

local rend = createRendering() if not rend then return inlineError (data.crit_err_msgs.unknown); end

return rend .. ((unnamed_params and inlineRed (err_warn_msgs.unnamed_params, 'warning')) or ) .. createTracking();

end


--[[--------------------------< E X P O R T E D F U N C T I O N S >------------------------------------------ ]]

return {webarchive = webarchive};