diff --git a/contrib/dtMediaWiki/README.md b/contrib/dtMediaWiki/README.md new file mode 100755 index 00000000..fef021e6 --- /dev/null +++ b/contrib/dtMediaWiki/README.md @@ -0,0 +1,52 @@ +# dtMediaWiki + +Wikimedia Commons export plugin for [darktable](https://www.darktable.org/) + +See also: [Commons:DtMediaWiki](https://commons.wikimedia.org/wiki/Commons:DtMediaWiki) + +## Dependencies + +- [lua-sec](https://luarocks.org/modules/brunoos/luasec) + - Lua bindings for OpenSSL library to provide TLS/SSL communication +- [lua-luajson](https://luarocks.org/modules/harningt/luajson) + - JSON parser/encoder for Lua +- [lua-multipart-post](https://luarocks.org/modules/catwell/multipart-post) + - HTTP Multipart Post helper + +Note that `mediawikiapi.lua` is independent of darktable. + +## Installation + +- Download the plugin from [https://github.com/trougnouf/dtMediaWiki/archive/master.zip](https://github.com/trougnouf/dtMediaWiki/archive/master.zip) +- Create the [darktable plugin directory](https://www.darktable.org/usermanual/en/lua_chapter.html#lua_usage) if it doesn't exist + - `# mkdir /usr/share/darktable/lua/contrib` +- Copy (or link) the dtMediaWiki directory over there + - `# cp -r /path/to/dtMediaWiki /usr/share/darktable/lua/contrib` +- Activate the plugin in your darktable luarc config file by adding `require "contrib/dtMediaWiki/dtMediaWiki"` + - `$ echo 'require "contrib/dtMediaWiki/dtMediaWiki"' >> ~/.config/darktable/luarc` + +… or simply use the [Arch Linux package](https://aur.archlinux.org/packages/darktable-plugin-dtmediawiki-git/) and activate the plugin. + +## Usage + +- Login to Wikimedia Commons by setting your "Wikimedia username" and "Wikimedia password" in _[darktable preferences](https://www.darktable.org/usermanual/en/preferences_chapter.html) > lua options_ then restarting darktable. + - This will add the "Wikimedia Commons" entry into target storage. +- Ensure your image contains the following [metadata](https://www.darktable.org/usermanual/en/metadata_editor.html) and [tags](https://www.darktable.org/usermanual/en/tagging.html): + - **title** and/or **description** – The default output filename is `title (filename) description.ext` or `title (filename).ext` depending on what is available + - **rights** – Use something compatible with the [`{{self}}`](https://commons.wikimedia.org/wiki/Template:Self) template, some options are [`cc-by-sa-4.0`](https://commons.wikimedia.org/wiki/Template:Cc-by-sa-4.0), [`cc-by-4.0`](https://commons.wikimedia.org/wiki/Template:Cc-by-4.0), [`GFDL`](https://commons.wikimedia.org/wiki/Template:GFDL), see [Commons:Copyright tags](https://commons.wikimedia.org/wiki/Commons:Copyright_tags) + - **tags** – Categories and templates. Any tag that matches `Category:something` will be added as `[[Category:something]]` (no need to include the brackets), likewise any template matching `{{something}}` will be added as-is. + +The image coordinates will be added if they exist, and the creator metadata will be added as `[[User:Wikimedia username|creator]]` if it has been set. + +## Thanks + +- Iulia and Leslie for excellent coworking companionship and love +- darktable developers for an excellent open-source imaging software with a well documented [Lua API](https://www.darktable.org/lua-api/) +- [LrMediaWiki](https://github.com/Hasenlaeufer/LrMediaWiki) developers [robinkrahl](https://github.com/robinkrahl) and [Hasenlaeufer](https://github.com/Hasenlaeufer) for what inspired this and some base code +- MediaWiki [User:Platonides](https://www.mediawiki.org/wiki/User:Platonides) for helping me figure out the cookie issue +- [catwell](https://github.com/catwell): author of lua-multipart-post and a responsive fellow +- [simon04](https://github.com/simon04): second user and first contributor + +![:)](https://upload.wikimedia.org/wikipedia/commons/3/30/Binette-typo.png) + +--[Trougnouf](https://commons.wikimedia.org/wiki/User:Trougnouf) diff --git a/contrib/dtMediaWiki/dtMediaWiki.lua b/contrib/dtMediaWiki/dtMediaWiki.lua new file mode 100644 index 00000000..80d0789f --- /dev/null +++ b/contrib/dtMediaWiki/dtMediaWiki.lua @@ -0,0 +1,447 @@ +--[[dtMediaWiki is a darktable plugin which exports images to Wikimedia Commons + Author: Trougnouf (Benoit Brummer) + Contributor: Simon Legner (simon04) + +Dependencies: +* lua-sec: Lua bindings for OpenSSL library to provide TLS/SSL communication +* lua-luajson: JSON parser/encoder for Lua +* lua-multipart-post: HTTP Multipart Post helper +]] +local dt = require "darktable" +local MediaWikiApi = require "contrib/dtMediaWiki/lib/mediawikiapi" +local version = 61 + +--[[The version number is generated by .git/hooks/pre-commit (+x) + with the following content: + #!/bin/sh + NEWVERSION="$(expr "$(git log master --pretty=oneline | wc -l)" + 1)" + sed -i -r "s/local version = [0-9]+/local version = ${NEWVERSION}/1" dtMediaWiki.lua + git add dtMediaWiki.lua + ]] + +-- Use this _ function for translatable strings +local gettext = dt.gettext +gettext.bindtextdomain("dtMediaWiki",dt.configuration.config_dir.."/lua/locale/") +local function _(msgid) + return gettext.dgettext("dtMediaWiki", msgid) +end + +local function msgout(str) + -- Use msgout(gettext.dgettext("dtMediaWiki", "STRING")) to output STRING in both the terminal and GUI + print(str) + dt.print(str) +end + +local function dbgout(str) + -- Use dbgout(gettext.dgettext("dtMediaWiki", "STRING")) to output AMESSAGE in the debug terminal (darktable -d lua) + dt.print_log(str) +end +-- Preference entries +local preferences_prefix = "mediawiki" +dt.preferences.register( + preferences_prefix, + "username", + "string", + _('Wikimedia username'), + _("Wikimedia Commons username"), + "" +) +dt.preferences.register( + preferences_prefix, + "password", + "string", -- TODO Use Lua password storage once in release. See https://github.com/darktable-org/darktable/pull/7508 + _("Wikimedia password"), + _("Wikimedia Commons password (to be stored in plain-text!)"), + "" +) +dt.preferences.register( + preferences_prefix, + "overwrite", + "bool", + _("Commons: Overwrite existing images?"), + _("Existing images will be overwritten without confirmation, otherwise the upload will fail."), + false +) +dt.preferences.register( + preferences_prefix, + "cat_cam", + "bool", + _("Commons: Categorize camera?"), + _("A category will be added with the camera information " .. + "(eg: [[Category:Taken with Fujifilm X-E2 and XF18-55mmF2.8-4 R LM OIS]])"), + false +) + +dt.preferences.register( + preferences_prefix, + "desc_templates", + "string", + _("Commons: Templates to be placed in {{Information |description= ...}}"), + _('These templates are placed in the {{Information |description= ...}} field. (comma-separated)'), + "Description,Depicted person,en,de,fr,es,ja,ru,zh,it,pt,ar" +) + +local namepattern_default = "$TITLE ($FILE_NAME)" + +local namepattern_widget = + dt.new_widget("entry") { + tooltip = table.concat( + { + _("Determines the `File:` page name"), + _("recognized variables:"), + _("$FILE_NAME - basename of the input image"), + _("$TITLE - title from metadata"), + _("$DESCRIPTION - description from metadata"), + _("Note that $TITLE or $DESCRIPTION is required, and if both are chosen but only one is available " .. + "then the fallback name will be `$TITLE$DESCRIPTION ($FILE_NAME)`") + }, + "\n" + ), + text = dt.preferences.read(preferences_prefix, "namepattern", "string"), + reset_callback = function(self) + self.text = namepattern_default + dt.preferences.write(preferences_prefix, "namepattern", "string", self.text) + end +} + +-- language widget shown in lighttable export +local language_widget = + dt.new_widget("entry") { + text = "en", + tooltip = _("Description language code. Additional descriptions may be added with tag " + .. "{{Description|language_code|description_text}} or any of the templates listed in " + .. "the desc_template setting."), + reset_callback = function(self) + self.text = "en" + end +} + +dt.preferences.register( + preferences_prefix, + "authorpattern", + "string", + _("Commons: Preferred author pattern"), + _("Determines the author value; variables are username, $CREATOR"), + "[[User:$USERNAME|$CREATOR]]" +) +dt.preferences.register( + preferences_prefix, + "titleindesc", + "bool", + _("Commons: Use title in description"), + _("Use the title in description if both are available: description={{en|1=$TITLE: $DESCRIPTION}}"), + false +) + +-- Generate image name +local function make_image_name(image, tmp_exp_path) + local basename = image.filename:match "[^.]+" + local outname = namepattern_widget.text ~= '' and namepattern_widget.text or namepattern_default + dt.preferences.write(preferences_prefix, "namepattern", "string", outname) + local presdata = image.title .. image.description + if image.title ~= "" and image.description ~= "" then --2 items available + outname = outname:gsub("$TITLE", image.title) + outname = outname:gsub("$FILE_NAME", basename) + outname = outname:gsub("$DESCRIPTION", image.description) + elseif outname:find("$TITLE") and outname:find("$DESCRIPTION") then + outname = presdata .. " (" .. basename .. ")" + else + outname = outname:gsub("$TITLE", presdata) + outname = outname:gsub("$FILE_NAME", basename) + outname = outname:gsub("$DESCRIPTION", presdata) + end + local ext = tmp_exp_path:match "[^.]+$" + return outname .. "." .. ext +end + +-- Round to 1 decimal, remove useless .0's and convert number to string. Ensure that "." decimal separator is used +local function fmt_flt(num) + return string.format("%.1f", num):gsub(",", "."):gsub("%.0", "") +end + +-- Get description field from the description (and optionally title) metadata +local function get_description(image) + local titleindesc = dt.preferences.read(preferences_prefix, "titleindesc", "bool") + if titleindesc and image.description ~= "" and image.title ~= "" then + return image.title .. ": " .. image.description + elseif image.description ~= "" then + return image.description + else + return image.title + end +end + +local function split(astring) -- helper from http://lua-users.org/wiki/SplitJoin, doesn't work with local (?) + local sep, fields = ",", {} + local pattern = string.format("([^%s]+)", sep) + astring:gsub(pattern, function(c) fields[#fields+1] = c end) + return fields +end + +-- get description templates which need to be added to {{Information| description=...}} +local function get_intl_descriptions(image, discarded_tags) + local desc_templates = split(dt.preferences.read(preferences_prefix, "desc_templates", "string")) + local intl_descriptions = "" + for _, tag in pairs(dt.tags.get_tags(image)) do + if tag ~= 0 then -- workaround for dt bug #9715 + local tagstr = tag.name + for _, dtemplate in pairs(desc_templates) do + if tagstr:sub(1,#dtemplate+3) == '{{' .. dtemplate .. '|' then + intl_descriptions = intl_descriptions .. tagstr + discarded_tags[tagstr] = true + end + end + end + end + return intl_descriptions +end + + +-- Get other fields that are then added to the Information template (TODO document this feature) +local function get_other_fields(image, discarded_tags) + local other_fields = '' + for _, tag in pairs(dt.tags.get_tags(image)) do + if tag ~= 0 then -- workaround for dt bug #9715 + local tagstr = tag.name + if string.sub(tagstr, 0, 20) == '{{Information field|' + or string.sub(tagstr, 0, 7) == '{{InFi|' then + other_fields = other_fields .. tagstr + discarded_tags[tagstr] = true + end + end + end + return other_fields +end + +local function substitute_keywords(string, image) + + local username = dt.preferences.read(preferences_prefix, "username", "string") + string = string:gsub("$USERNAME", username) + string = string:gsub("$CREATOR", image.creator or username) + string = string:gsub("$FILE_NAME", image.filename) + string = string:gsub("$DATETIME", image.exif_datetime_taken) + string = string:gsub("$YEAR", image.exif_datetime_taken:sub(0, 4)) + string = string:gsub("\\comma", ",") + return string +end + +-- get "Other versions" of this image, ie different views of the scene or duplicate +-- of this image (the latter needs to have a different title). +-- Used with the "alt:" tag; +-- matches contained in other images' filename in the current library. +-- input: +---- image: working image (dt_lua_image_t) +---- tmp_img_fn: working image temporary export filename (string) +-- output: +---- gallery following "Other versions = " on the image page, or an empty string (string) +local function get_alt_images(image, tmp_img_fn) + local alts = {} + for _, tag in pairs(dt.tags.get_tags(image)) do + if tag ~= 0 then -- workaround for dt bug #9715 + if string.sub(tag.name, 1, 4) == "alt:" then + table.insert(alts, string.sub(tag.name, 5)) + end + end + end + if next(alts) == nil then + return "" + end + local altcode = {""} + for _, img in pairs(dt.collection) do + dbgout(gettext.dgettext("dtMediaWiki", ("dtMediaWiki.get_alt_images: looking at ")) .. img.filename) + for _, altimg in pairs(alts) do + dbgout(gettext.dgettext("dtMediaWiki", ("dtMediaWiki.get_alt_images: looking for ")) .. altimg) + if string.find(img.filename, altimg) then + local alt_fn = make_image_name(img, tmp_img_fn) + table.insert(altcode, alt_fn) + dbgout(gettext.dgettext("dtMediaWiki", ("dtMediaWiki.get_alt_images: found ")) .. alt_fn) + end + end + end + table.insert(altcode, '') + return table.concat(altcode, "\n") +end + + +-- Generate an image page with all required info from tags, metadata, and such. +local function make_image_page(image, tmp_img_fn) + local discarded_tags = {} + local imgpg = {"=={{int:filedesc}}==\n{{Information"} + table.insert(imgpg, "|description={{" .. language_widget.text .. "|1=" + .. substitute_keywords(get_description(image), image) .. "}}" + .. get_intl_descriptions(image, discarded_tags)) + local date = image.exif_datetime_taken + date = date:gsub("(%d%d%d%d):(%d%d):(%d%d)", "%1-%2-%3") -- format date in ISO 8601 / RFC 3339 + table.insert(imgpg, "|date=" .. date) + table.insert(imgpg, "|source={{own}}") + local author = dt.preferences.read(preferences_prefix, "authorpattern", "string") + author = substitute_keywords(author, image) + table.insert(imgpg, "|author=" .. author) + table.insert(imgpg, '|other fields = ' .. get_other_fields(image, discarded_tags)) + table.insert(imgpg, '|other versions = ' .. get_alt_images(image, tmp_img_fn)) + table.insert(imgpg, "}}") + if image.latitude ~= nil and image.longitude ~= nil then + table.insert(imgpg, "{{Location |1=" .. string.gsub(image.latitude,",",".") + .. " |2=" .. string.gsub(image.longitude,",",".") .. " }}") + end + table.insert(imgpg, "=={{int:license-header}}==") + table.insert(imgpg, "{{self|" .. image.rights .. "}}") + for _, tag in pairs(dt.tags.get_tags(image)) do + if tag ~= 0 then -- workaround for dt bug #9715 + local subbed_tag = substitute_keywords(tag.name, image) + if string.sub(subbed_tag, 1, 9) == "Category:" then + table.insert(imgpg, "[[" .. subbed_tag .. "]]") + elseif subbed_tag:sub(1, 2) == "{{" and not discarded_tags[subbed_tag] then + table.insert(imgpg, subbed_tag) + end + end + end + if dt.preferences.read(preferences_prefix, "cat_cam", "bool") then + if image.exif_model ~= "" then + local model = image.exif_maker:sub(1, 1) .. image.exif_maker:sub(2):lower() + local catcam = "[[Category:Taken with " .. model .. " " .. image.exif_model + if image.exif_lens ~= "" then + catcam = catcam .. " and " .. image.exif_lens .. "]]" + else + catcam = catcam .. "]]" + end + table.insert(imgpg, catcam) + end + if image.exif_aperture then + -- convert aperture to US_en locale and remove trailing .0 if there is any + table.insert(imgpg, "[[Category:F-number f/" .. fmt_flt(image.exif_aperture) .. "]]") + end + if image.exif_focal_length ~= "" then + table.insert(imgpg, "[[Category:Lens focal length " .. fmt_flt(image.exif_focal_length) .. " mm]]") + end + if image.exif_iso ~= "" then + table.insert(imgpg, "[[Category:ISO speed rating " .. fmt_flt(image.exif_iso) .. "]]") + end + -- if image.exif_exposure ~= "" then + -- table.insert(imgpg, "[[Category:Exposure time "..image.exif_exposure.." sec]]") + -- end -- decimal instead of fraction (TODO) + end + table.insert(imgpg, "[[Category:Uploaded with dtMediaWiki]]") + imgpg = table.concat(imgpg, "\n") + return imgpg +end + +-- comment widget shown in lighttable export +local comment_widget = + dt.new_widget("entry") { + text = _("Uploaded with dtMediaWiki ") .. version, + reset_callback = function(self) + self.text = _("Uploaded with dtMediaWiki ") .. version + end +} + +--This function is called once for each exported image +local function register_storage_store(_, image, _, tmp_exp_path, _, _, _, _) + msgout(gettext.dgettext("dtMediaWiki", ("register_storage_store: exporting the following image:"))) + msgout(tmp_exp_path .. '(' .. image.title .. ')') + for _, tag in pairs(dt.tags.get_tags(image)) do + if tag == 0 then + msgout(gettext.dgettext("dtMediaWiki", ("BUG: invalid tag 0 for image ")) .. image.title) + end + end + local imagepage = make_image_page(image, tmp_exp_path) + local imagename = make_image_name(image, tmp_exp_path) + --print(imagepage) + local success = MediaWikiApi.uploadfile( + tmp_exp_path, + imagepage, + imagename, + dt.preferences.read(preferences_prefix, "overwrite", "bool"), + comment_widget.text + ) + if success then + msgout(gettext.dgettext("dtMediaWiki", ("exported ")) .. imagename) -- that is the path also + else + msgout(gettext.dgettext("dtMediaWiki", ("Failed to export ")) .. imagename) + end +end + +--This function is called once all images are processed and all store calls are finished. +local function register_storage_finalize(_, image_table, extra_data) + local fcnt = 0 + for _ in pairs(image_table) do + fcnt = fcnt + 1 + end + msgout(gettext.dgettext("dtMediaWiki", ("exported ")) .. fcnt .. "/" .. extra_data["init_img_cnt"] .. + gettext.dgettext("dtMediaWiki", (" images to Wikimedia Commons"))) +end + +--A function called to check if a given image format is supported by the Lua storage; +--This is used to build the dropdown format list for the GUI. +local function register_storage_supported(_, format) + local ext = format.extension + return ext == "jpg" or ext == "png" or ext == "tif" or ext == "webp" +end + +--A function called before storage happens +--This function can change the list of exported functions +local function register_storage_initialize(_, _, images, _, extra_data) + local out_images = {} + for _, img in pairs(images) do + -- BUG? images contain "0" value (with key 3 in tested instance) instead of dt_lua_image_t + if type(img) == 'number' then + msgout(gettext.dgettext("dtMediaWiki", + ("BUG: dt.register_storage.initialize sent a number instead of dt_lua_image_t in images table: ")) .. img) + elseif img.rights == "" then + msgout(gettext.dgettext("dtMediaWiki", ("Error: ")) .. img.path .. + gettext.dgettext("dtMediaWiki", (" has no rights, cannot be exported to Wikimedia Commons"))) + elseif img.title == "" and img.description == "" then + msgout( + gettext.dgettext("dtMediaWiki", ("Error: ")) .. + img.path .. gettext.dgettext("dtMediaWiki", (" is missing a meaningful title and/or description, ")) .. + gettext.dgettext("dtMediaWiki", ("won't be exported to Wikimedia Commons")) + ) + else + table.insert(out_images, img) + end + end + extra_data["init_img_cnt"] = #images + return out_images +end + +-- widgets shown in lighttable +local export_widgets = + dt.new_widget("box") { + dt.new_widget("box") { + orientation = "horizontal", + dt.new_widget("label") {label = _("Naming pattern:")}, + namepattern_widget + }, + dt.new_widget("box") { + orientation = "horizontal", + dt.new_widget("label") {label = _("Comment:")}, + comment_widget + }, + dt.new_widget("box") { + orientation = "horizontal", + dt.new_widget("label") {label = _("Language code:"), + tooltip = _("Description language code (eg: en, fr, ...).") + .. _("More descriptions can be entered in the tags (eg: {{fr|Une description}}")}, + language_widget + } +} + +-- Darktable target storage entry +if + MediaWikiApi.login( + dt.preferences.read(preferences_prefix, "username", "string"), + dt.preferences.read(preferences_prefix, "password", "string") + ) + then + dt.register_storage( + "mediawiki", + "Wikimedia Commons", + register_storage_store, + register_storage_finalize, + register_storage_supported, + register_storage_initialize, + export_widgets + ) +else + msgout(_("Unable to log into Wikimedia Commons, export disabled.")) +end diff --git a/contrib/dtMediaWiki/lib/mediawikiapi.lua b/contrib/dtMediaWiki/lib/mediawikiapi.lua new file mode 100755 index 00000000..d2c02781 --- /dev/null +++ b/contrib/dtMediaWiki/lib/mediawikiapi.lua @@ -0,0 +1,318 @@ +--[[ +Author: Trougnouf (Benoit Brummer) +Contributor: Simon Legner (simon04) + +mediawikiapi.lua uses some code adapted from LrMediaWiki +LrMediaWiki authors: +Robin Krahl +Eckhard Henkel + +Dependencies: +* lua-sec: Lua bindings for OpenSSL library to provide TLS/SSL communication +* lua-luajson: JSON parser/encoder for Lua +* lua-multipart-post: HTTP Multipart Post helper + (darktable is not a dependency) +]] +package.path = package.path .. ";/dtMediaWiki/?.lua" +package.path = package.path .. ";/usr/share/darktable/lua/contrib/dtMediaWiki/?.lua" +local https = require "ssl.https" +local json = require "json" +local ltn12 = require "ltn12" +local mpost = require "multipart-post" + +local MediaWikiApi = { + userAgent = string.format("mediawikilua %d.%d", 0, 1), + apiPath = "https://commons.wikimedia.org/w/api.php", + cookie = {}, + edit_token = nil +} + +local function httpsget(url, reqheaders) + local res, code, resheaders, _ = + https.request { + url = url, + headers = reqheaders + } + resheaders.status = code + + return res, resheaders +end + +local function httpspost(url, postBody, reqheaders) + local res = {} + local _, code, resheaders, _ = + https.request { + url = url, + method = "POST", + headers = reqheaders, + source = ltn12.source.string(postBody), + sink = ltn12.sink.table(res) + } + resheaders.status = code + + return table.concat(res), resheaders +end + +local function throwUserError(text) + print(text) +end + +-- parse a received cookie and update MediaWikiApi.cookie +function MediaWikiApi.parseCookie(unparsedcookie) + while unparsedcookie and string.len(unparsedcookie) > 0 do + local i = string.find(unparsedcookie, ";") + local crumb = string.sub(unparsedcookie, 1, i - 1) + local isep = string.find(crumb, "=") + if isep then + local cvar = string.sub(crumb, 1, isep - 1) + local icvarcomma = string.find(cvar, ",") + while icvarcomma do + cvar = string.sub(cvar, icvarcomma + 2) + icvarcomma = string.find(cvar, ",") + end + MediaWikiApi.cookie[cvar] = string.sub(crumb, isep + 1) + end + local nexti = string.find(unparsedcookie, ",") + if not nexti then + return + end + unparsedcookie = string.sub(unparsedcookie, nexti + 2) + end +end + +-- generate a cookie string from MediaWikiApi.cookie to send to server +function MediaWikiApi.cookie2string() + local prestr = {} + for cvar, cval in pairs(MediaWikiApi.cookie) do + table.insert(prestr, cvar .. "=" .. cval .. ";") + end + return table.concat(prestr) +end + +-- Demand an edit token. probably can change this to request only one per session +function MediaWikiApi.getEditToken() + --if MediaWikiApi.edit_token == nil then + local arguments = { + action = "query", + meta = "tokens", + type = "csrf", + format = "json" + } + local jsonres = MediaWikiApi.performRequest(arguments) + MediaWikiApi.edit_token = jsonres.query.tokens.csrftoken + --end + return MediaWikiApi.edit_token +end + +function MediaWikiApi.uploadfile(filepath, pagetext, filename, overwrite, comment) + local file_handler = io.open(filepath) + local content = { + action = "upload", + format = "json", + filename = filename, + text = pagetext, + comment = comment, + token = MediaWikiApi.getEditToken(), + file = { + filename = filename, + data = file_handler:read("*all") + } + } + if overwrite then + content["ignorewarnings"] = "true" + end + local res = {} + local req = mpost.gen_request(content) + req.headers["cookie"] = MediaWikiApi.cookie2string() + req.url = MediaWikiApi.apiPath + req.sink = ltn12.sink.table(res) + local _, _, resheaders = https.request(req) + local jsonres = json.decode(table.concat(res)) + local success = jsonres.upload.result == 'Success' + MediaWikiApi.parseCookie(resheaders["set-cookie"]) + return success +end + +-- Code adapted from LrMediaWiki: +MediaWikiApi.trace = function(...) + print(...) +end + +--- URL-encode a string according to RFC 3986. +-- Based on http://lua-users.org/wiki/StringRecipes +-- @param str the string to encode +-- @return the URL-encoded string +function MediaWikiApi.urlEncode(str) + if str then + str = string.gsub(str, "\n", "\r\n") + str = + string.gsub( + str, + "([^%w %-%_%.%~])", + function(c) + return string.format("%%%02X", string.byte(c)) + end + ) + str = string.gsub(str, " ", "+") + end + return str +end + +--- Convert HTTP arguments to a URL-encoded request body. +-- @param arguments (table) the arguments to convert +-- @return (string) a request body created from the URL-encoded arguments +function MediaWikiApi.createRequestBody(arguments) + local body = nil + for key, value in pairs(arguments) do + if body then + body = body .. "&" + else + body = "" + end + body = body .. MediaWikiApi.urlEncode(key) .. "=" .. MediaWikiApi.urlEncode(value) + end + return body or "" +end + +function MediaWikiApi.performHttpRequest(path, arguments, post) -- changed signature! + local requestBody = MediaWikiApi.createRequestBody(arguments) + local requestHeaders = { + ["Content-Type"] = "application/x-www-form-urlencoded", + ["User-Agent"] = MediaWikiApi.userAgent + } + if post then + requestHeaders["Content-Length"] = #requestBody + end + requestHeaders["Cookie"] = MediaWikiApi.cookie2string() + MediaWikiApi.trace("Performing HTTP request") + MediaWikiApi.trace(" Path:", path) + MediaWikiApi.trace(" Request body:", requestBody) + + local resultBody, resultHeaders + if post then + resultBody, resultHeaders = httpspost(path, requestBody, requestHeaders) + else + resultBody, resultHeaders = httpsget(path .. "?" .. requestBody, requestHeaders) + end + + MediaWikiApi.trace(" Result status:", resultHeaders.status) + + if not resultHeaders.status then + throwUserError("No network connection") + elseif resultHeaders.status ~= 200 then + MediaWikiApi.httpError(resultHeaders.status) + end + MediaWikiApi.parseCookie(resultHeaders["set-cookie"]) + --MediaWikiApi.trace("new cookie: "..resultHeaders["set-cookie"]) + MediaWikiApi.trace(" Result body:", resultBody) + return resultBody +end + +function MediaWikiApi.performRequest(arguments) + local resultBody = MediaWikiApi.performHttpRequest(MediaWikiApi.apiPath, arguments, true) + local jsonres = json.decode(resultBody) + return jsonres +end + +function MediaWikiApi.logout() + -- See https://www.mediawiki.org/wiki/API:Logout + local arguments = { + action = "logout" + } + MediaWikiApi.performRequest(arguments) +end + +function MediaWikiApi.login(username, password) + -- See https://www.mediawiki.org/wiki/API:Login + -- Check if the credentials are a main-account or a bot-account. + -- The different credentials need different login arguments. + -- The existance of the character "@" inside of an username is an + -- identicator if the credentials are a bot-account or a main-account. + local credentials + if string.find(username, "@") then + credentials = "bot-account" + else + credentials = "main-account" + end + MediaWikiApi.trace("Credentials: " .. credentials) + + -- Check if a user is logged in: + local arguments = { + action = "query", + meta = "userinfo", + format = "json" + } + local jsonres = MediaWikiApi.performRequest(arguments) + local id = jsonres.query.userinfo.id + local name = jsonres.query.userinfo.name + if id == "0" or id == 0 then -- not logged in, name is the IP address + MediaWikiApi.trace("Not logged in, need to login") + else -- id ~= '0' – logged in + MediaWikiApi.trace('Logged in as user "' .. name .. '" (ID: ' .. id .. ")") + if name == username then -- user is already logged in + MediaWikiApi.trace("No new login needed (1)") + return true + else -- name ~= username + -- Check if name is main-account name of bot-username + if credentials == "bot-account" then + local pattern = "(.*)@" -- all characters up to "@" + if name == string.match(username, pattern) then + MediaWikiApi.trace("No new login needed (2)") + return true + end + end + MediaWikiApi.trace('Logout and new login needed with username "' .. username .. '".') + MediaWikiApi.logout() -- without this logout a new login MIGHT fail + end + end + + -- A login token needs to be retrieved prior of a login action: + arguments = { + action = "query", + meta = "tokens", + type = "login", + format = "json" + } + jsonres = MediaWikiApi.performRequest(arguments) + local logintoken = jsonres.query.tokens.logintoken + + -- Perform login: + if credentials == "main-account" then + arguments = { + format = "json", + action = "clientlogin", + loginreturnurl = "https://www.mediawiki.org", -- dummy; required parameter + username = username, + password = password, + logintoken = logintoken + } + jsonres = MediaWikiApi.performRequest(arguments) + local loginResult = jsonres.clientlogin.status + if loginResult == "PASS" then + return true + else + MediaWikiApi.track('Login failed: ' .. jsonres.clientlogin.message) + return false + end + else -- credentials == bot-account + assert(credentials == "bot-account") + arguments = { + format = "json", + action = "login", + lgname = username, + lgpassword = password, + lgtoken = logintoken + } + jsonres = MediaWikiApi.performRequest(arguments) + local loginResult = jsonres.login.result + if loginResult == "Success" then + return true + else + MediaWikiApi.track('Login failed: ' .. jsonres.login.reason) + return false + end + end +end +-- end of LrMediaWiki code + +return MediaWikiApi