diff options
| author | Robin Krahl <me@robin-krahl.de> | 2015-06-27 23:05:52 +0200 | 
|---|---|---|
| committer | Robin Krahl <me@robin-krahl.de> | 2015-06-27 23:05:52 +0200 | 
| commit | 24875af9d53e3f1a90fa5e6bd4aaef36bb0c3591 (patch) | |
| tree | 907a6204f1dbfd5ff4e76870bf944f6bf352031b | |
| parent | 717b11ecdf663dbc9e43cbdd6b5aab3a2dbcdafe (diff) | |
| download | LrMediaWiki-24875af9d53e3f1a90fa5e6bd4aaef36bb0c3591.tar.gz LrMediaWiki-24875af9d53e3f1a90fa5e6bd4aaef36bb0c3591.tar.bz2 | |
Check for new versions after start.
After LrMediaWiki is loaded, GitHub is checked for new versions if the
user enabled that setting. To parse the response of the GitHub API,
a third-party library is used for JSON parsing (JSON.lua). As the GitHub
API query for the latest release only returns non-pre-release versions,
all releases are queried and the first one is considered the latest.
 - create MediaWikiInit that queries the new version and shows a
   message (if necessary)
 - add MediaWikiInit to Info so that it is run after LrMediaWiki is
   loaded
 - add ‘Check version’ setting to the configuration
 - refactor MediaWikiApi and create performHttpRequest as a generic
   HTTP request method
 - add MediaWikiApi.getCurrentPluginVersion to query the GitHub API
Fix #44.
| -rwxr-xr-x | LICENSE.txt | 9 | ||||
| -rwxr-xr-x | README.md | 7 | ||||
| -rwxr-xr-x | mediawiki.lrdevplugin/Info.lua | 2 | ||||
| -rw-r--r-- | mediawiki.lrdevplugin/JSON.lua | 1053 | ||||
| -rwxr-xr-x | mediawiki.lrdevplugin/MediaWikiApi.lua | 64 | ||||
| -rw-r--r-- | mediawiki.lrdevplugin/MediaWikiInit.lua | 33 | ||||
| -rw-r--r-- | mediawiki.lrdevplugin/MediaWikiPluginInfoProvider.lua | 16 | ||||
| -rw-r--r-- | mediawiki.lrdevplugin/MediaWikiUtils.lua | 15 | ||||
| -rwxr-xr-x | mediawiki.lrdevplugin/TranslatedStrings_de.txt | 5 | 
9 files changed, 1185 insertions, 19 deletions
| diff --git a/LICENSE.txt b/LICENSE.txt index 955762a..f8b0764 100755 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,7 +1,12 @@  Copyright (c) 2014, 2015 by the LrMediaWiki team (see CREDITS.txt file in the
 -project root directory or [0])
 +project root directory or [0]), X11 License
 -[0]  <https://raw.githubusercontent.com/ireas/LrMediaWiki/master/CREDITS.txt>
 +Except:
 + - JSON.lua: Copyright 2010-2014 Jeffrey Friedl [1], CC-by 3.0 [2]
 +
 +[0] <https://raw.githubusercontent.com/ireas/LrMediaWiki/master/CREDITS.txt>
 +[1] <http://regex.info/blog/lua/json>
 +[2] <http://creativecommons.org/licenses/by/3.0/deed.en_US>
  The X11 License
 @@ -27,5 +27,12 @@ MediaWiki upload.  The upload is done in `MediaWikiApi.lua`.  `MediaWikiMetadataProvider.lua` defines the custom metadata containing e. g.  the file description and additional categories and templates. +### Libraries + +LrMediaWiki contains a copy of [`JSON.lua`][jsonlua] written by Jeffrey Friedl +and released under [CC-by 3.0][ccby3]. +  [comlrmw]: https://commons.wikimedia.org/wiki/Commons:LrMediaWiki  [lrsdk]: http://www.adobe.com/devnet/photoshoplightroom.html +[jsonlua]: http://regex.info/blog/lua/json +[ccby3]: http://creativecommons.org/licenses/by/3.0/deed.en_US diff --git a/mediawiki.lrdevplugin/Info.lua b/mediawiki.lrdevplugin/Info.lua index cbfa8c6..479385e 100755 --- a/mediawiki.lrdevplugin/Info.lua +++ b/mediawiki.lrdevplugin/Info.lua @@ -14,6 +14,8 @@ return {  	LrToolkitIdentifier = 'org.ireas.lightroom.mediawiki',
  	LrPluginName = LOC '$$$/LrMediaWiki/PluginName=MediaWiki for Lightroom',
 +	LrInitPlugin = 'MediaWikiInit.lua',
 +
  	LrExportServiceProvider = {
  		title = LOC '$$$/LrMediaWiki/MediaWiki=MediaWiki',
  		file = 'MediaWikiExportServiceProvider.lua',
 diff --git a/mediawiki.lrdevplugin/JSON.lua b/mediawiki.lrdevplugin/JSON.lua new file mode 100644 index 0000000..5f11425 --- /dev/null +++ b/mediawiki.lrdevplugin/JSON.lua @@ -0,0 +1,1053 @@ +-- -*- coding: utf-8 -*- +-- +-- Simple JSON encoding and decoding in pure Lua. +-- +-- Copyright 2010-2014 Jeffrey Friedl +-- http://regex.info/blog/ +-- +-- Latest version: http://regex.info/blog/lua/json +-- +-- This code is released under a Creative Commons CC-BY "Attribution" License: +-- http://creativecommons.org/licenses/by/3.0/deed.en_US +-- +-- It can be used for any purpose so long as the copyright notice above, +-- the web-page links above, and the 'AUTHOR_NOTE' string below are +-- maintained. Enjoy. +-- +local VERSION = 20141223.14 -- version history at end of file +local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20141223.14 ]-" + +-- +-- The 'AUTHOR_NOTE' variable exists so that information about the source +-- of the package is maintained even in compiled versions. It's also +-- included in OBJDEF below mostly to quiet warnings about unused variables. +-- +local OBJDEF = { +   VERSION      = VERSION, +   AUTHOR_NOTE  = AUTHOR_NOTE, +} + + +-- +-- Simple JSON encoding and decoding in pure Lua. +-- http://www.json.org/ +-- +-- +--   JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines +-- +--   local lua_value = JSON:decode(raw_json_text) +-- +--   local raw_json_text    = JSON:encode(lua_table_or_value) +--   local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability +-- +-- +-- +-- DECODING (from a JSON string to a Lua table) +-- +-- +--   JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines +-- +--   local lua_value = JSON:decode(raw_json_text) +-- +--   If the JSON text is for an object or an array, e.g. +--     { "what": "books", "count": 3 } +--   or +--     [ "Larry", "Curly", "Moe" ] +-- +--   the result is a Lua table, e.g. +--     { what = "books", count = 3 } +--   or +--     { "Larry", "Curly", "Moe" } +-- +-- +--   The encode and decode routines accept an optional second argument, +--   "etc", which is not used during encoding or decoding, but upon error +--   is passed along to error handlers. It can be of any type (including nil). +-- +-- +-- +-- ERROR HANDLING +-- +--   With most errors during decoding, this code calls +-- +--      JSON:onDecodeError(message, text, location, etc) +-- +--   with a message about the error, and if known, the JSON text being +--   parsed and the byte count where the problem was discovered. You can +--   replace the default JSON:onDecodeError() with your own function. +-- +--   The default onDecodeError() merely augments the message with data +--   about the text and the location if known (and if a second 'etc' +--   argument had been provided to decode(), its value is tacked onto the +--   message as well), and then calls JSON.assert(), which itself defaults +--   to Lua's built-in assert(), and can also be overridden. +-- +--   For example, in an Adobe Lightroom plugin, you might use something like +-- +--          function JSON:onDecodeError(message, text, location, etc) +--             LrErrors.throwUserError("Internal Error: invalid JSON data") +--          end +-- +--   or even just +-- +--          function JSON.assert(message) +--             LrErrors.throwUserError("Internal Error: " .. message) +--          end +-- +--   If JSON:decode() is passed a nil, this is called instead: +-- +--      JSON:onDecodeOfNilError(message, nil, nil, etc) +-- +--   and if JSON:decode() is passed HTML instead of JSON, this is called: +-- +--      JSON:onDecodeOfHTMLError(message, text, nil, etc) +-- +--   The use of the fourth 'etc' argument allows stronger coordination +--   between decoding and error reporting, especially when you provide your +--   own error-handling routines. Continuing with the the Adobe Lightroom +--   plugin example: +-- +--          function JSON:onDecodeError(message, text, location, etc) +--             local note = "Internal Error: invalid JSON data" +--             if type(etc) = 'table' and etc.photo then +--                note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName') +--             end +--             LrErrors.throwUserError(note) +--          end +-- +--            : +--            : +-- +--          for i, photo in ipairs(photosToProcess) do +--               :              +--               :              +--               local data = JSON:decode(someJsonText, { photo = photo }) +--               :              +--               :              +--          end +-- +-- +-- +-- +-- +-- DECODING AND STRICT TYPES +-- +--   Because both JSON objects and JSON arrays are converted to Lua tables, +--   it's not normally possible to tell which original JSON type a +--   particular Lua table was derived from, or guarantee decode-encode +--   round-trip equivalency. +-- +--   However, if you enable strictTypes, e.g. +-- +--      JSON = assert(loadfile "JSON.lua")() --load the routines +--      JSON.strictTypes = true +-- +--   then the Lua table resulting from the decoding of a JSON object or +--   JSON array is marked via Lua metatable, so that when re-encoded with +--   JSON:encode() it ends up as the appropriate JSON type. +-- +--   (This is not the default because other routines may not work well with +--   tables that have a metatable set, for example, Lightroom API calls.) +-- +-- +-- ENCODING (from a lua table to a JSON string) +-- +--   JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines +-- +--   local raw_json_text    = JSON:encode(lua_table_or_value) +--   local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability +--   local custom_pretty    = JSON:encode(lua_table_or_value, etc, { pretty = true, indent = "|  ", align_keys = false }) +-- +--   On error during encoding, this code calls: +-- +--     JSON:onEncodeError(message, etc) +-- +--   which you can override in your local JSON object. +-- +--   The 'etc' in the error call is the second argument to encode() +--   and encode_pretty(), or nil if it wasn't provided. +-- +-- +-- PRETTY-PRINTING +-- +--   An optional third argument, a table of options, allows a bit of +--   configuration about how the encoding takes place: +-- +--     pretty = JSON:encode(val, etc, { +--                                       pretty = true,      -- if false, no other options matter +--                                       indent = "   ",     -- this provides for a three-space indent per nesting level +--                                       align_keys = false, -- see below +--                                     }) +-- +--   encode() and encode_pretty() are identical except that encode_pretty() +--   provides a default options table if none given in the call: +-- +--       { pretty = true, align_keys = false, indent = "  " } +-- +--   For example, if +-- +--      JSON:encode(data) +-- +--   produces: +-- +--      {"city":"Kyoto","climate":{"avg_temp":16,"humidity":"high","snowfall":"minimal"},"country":"Japan","wards":11} +-- +--   then +-- +--      JSON:encode_pretty(data) +-- +--   produces: +-- +--      { +--        "city": "Kyoto", +--        "climate": { +--          "avg_temp": 16, +--          "humidity": "high", +--          "snowfall": "minimal" +--        }, +--        "country": "Japan", +--        "wards": 11 +--      } +-- +--   The following three lines return identical results: +--       JSON:encode_pretty(data) +--       JSON:encode_pretty(data, nil, { pretty = true, align_keys = false, indent = "  " }) +--       JSON:encode       (data, nil, { pretty = true, align_keys = false, indent = "  " }) +-- +--   An example of setting your own indent string: +-- +--     JSON:encode_pretty(data, nil, { pretty = true, indent = "|    " }) +-- +--   produces: +-- +--      { +--      |    "city": "Kyoto", +--      |    "climate": { +--      |    |    "avg_temp": 16, +--      |    |    "humidity": "high", +--      |    |    "snowfall": "minimal" +--      |    }, +--      |    "country": "Japan", +--      |    "wards": 11 +--      } +-- +--   An example of setting align_keys to true: +-- +--     JSON:encode_pretty(data, nil, { pretty = true, indent = "  ", align_keys = true }) +--   +--   produces: +--    +--      { +--           "city": "Kyoto", +--        "climate": { +--                     "avg_temp": 16, +--                     "humidity": "high", +--                     "snowfall": "minimal" +--                   }, +--        "country": "Japan", +--          "wards": 11 +--      } +-- +--   which I must admit is kinda ugly, sorry. This was the default for +--   encode_pretty() prior to version 20141223.14. +-- +-- +--  AMBIGUOUS SITUATIONS DURING THE ENCODING +-- +--   During the encode, if a Lua table being encoded contains both string +--   and numeric keys, it fits neither JSON's idea of an object, nor its +--   idea of an array. To get around this, when any string key exists (or +--   when non-positive numeric keys exist), numeric keys are converted to +--   strings. +-- +--   For example,  +--     JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) +--   produces the JSON object +--     {"1":"one","2":"two","3":"three","SOMESTRING":"some string"} +-- +--   To prohibit this conversion and instead make it an error condition, set +--      JSON.noKeyConversion = true +-- + + + + +-- +-- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT +-- +--    assert +--    onDecodeError +--    onDecodeOfNilError +--    onDecodeOfHTMLError +--    onEncodeError +-- +--  If you want to create a separate Lua JSON object with its own error handlers, +--  you can reload JSON.lua or use the :new() method. +-- +--------------------------------------------------------------------------- + +local default_pretty_indent  = "  " +local default_pretty_options = { pretty = true, align_keys = false, indent = default_pretty_indent } + +local isArray  = { __tostring = function() return "JSON array"  end }    isArray.__index  = isArray +local isObject = { __tostring = function() return "JSON object" end }    isObject.__index = isObject + + +function OBJDEF:newArray(tbl) +   return setmetatable(tbl or {}, isArray) +end + +function OBJDEF:newObject(tbl) +   return setmetatable(tbl or {}, isObject) +end + +local function unicode_codepoint_as_utf8(codepoint) +   -- +   -- codepoint is a number +   -- +   if codepoint <= 127 then +      return string.char(codepoint) + +   elseif codepoint <= 2047 then +      -- +      -- 110yyyxx 10xxxxxx         <-- useful notation from http://en.wikipedia.org/wiki/Utf8 +      -- +      local highpart = math.floor(codepoint / 0x40) +      local lowpart  = codepoint - (0x40 * highpart) +      return string.char(0xC0 + highpart, +                         0x80 + lowpart) + +   elseif codepoint <= 65535 then +      -- +      -- 1110yyyy 10yyyyxx 10xxxxxx +      -- +      local highpart  = math.floor(codepoint / 0x1000) +      local remainder = codepoint - 0x1000 * highpart +      local midpart   = math.floor(remainder / 0x40) +      local lowpart   = remainder - 0x40 * midpart + +      highpart = 0xE0 + highpart +      midpart  = 0x80 + midpart +      lowpart  = 0x80 + lowpart + +      -- +      -- Check for an invalid character (thanks Andy R. at Adobe). +      -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070 +      -- +      if ( highpart == 0xE0 and midpart < 0xA0 ) or +         ( highpart == 0xED and midpart > 0x9F ) or +         ( highpart == 0xF0 and midpart < 0x90 ) or +         ( highpart == 0xF4 and midpart > 0x8F ) +      then +         return "?" +      else +         return string.char(highpart, +                            midpart, +                            lowpart) +      end + +   else +      -- +      -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx +      -- +      local highpart  = math.floor(codepoint / 0x40000) +      local remainder = codepoint - 0x40000 * highpart +      local midA      = math.floor(remainder / 0x1000) +      remainder       = remainder - 0x1000 * midA +      local midB      = math.floor(remainder / 0x40) +      local lowpart   = remainder - 0x40 * midB + +      return string.char(0xF0 + highpart, +                         0x80 + midA, +                         0x80 + midB, +                         0x80 + lowpart) +   end +end + +function OBJDEF:onDecodeError(message, text, location, etc) +   if text then +      if location then +         message = string.format("%s at char %d of: %s", message, location, text) +      else +         message = string.format("%s: %s", message, text) +      end +   end + +   if etc ~= nil then +      message = message .. " (" .. OBJDEF:encode(etc) .. ")" +   end + +   if self.assert then +      self.assert(false, message) +   else +      assert(false, message) +   end +end + +OBJDEF.onDecodeOfNilError  = OBJDEF.onDecodeError +OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError + +function OBJDEF:onEncodeError(message, etc) +   if etc ~= nil then +      message = message .. " (" .. OBJDEF:encode(etc) .. ")" +   end + +   if self.assert then +      self.assert(false, message) +   else +      assert(false, message) +   end +end + +local function grok_number(self, text, start, etc) +   -- +   -- Grab the integer part +   -- +   local integer_part = text:match('^-?[1-9]%d*', start) +                     or text:match("^-?0",        start) + +   if not integer_part then +      self:onDecodeError("expected number", text, start, etc) +   end + +   local i = start + integer_part:len() + +   -- +   -- Grab an optional decimal part +   -- +   local decimal_part = text:match('^%.%d+', i) or "" + +   i = i + decimal_part:len() + +   -- +   -- Grab an optional exponential part +   -- +   local exponent_part = text:match('^[eE][-+]?%d+', i) or "" + +   i = i + exponent_part:len() + +   local full_number_text = integer_part .. decimal_part .. exponent_part +   local as_number = tonumber(full_number_text) + +   if not as_number then +      self:onDecodeError("bad number", text, start, etc) +   end + +   return as_number, i +end + + +local function grok_string(self, text, start, etc) + +   if text:sub(start,start) ~= '"' then +      self:onDecodeError("expected string's opening quote", text, start, etc) +   end + +   local i = start + 1 -- +1 to bypass the initial quote +   local text_len = text:len() +   local VALUE = "" +   while i <= text_len do +      local c = text:sub(i,i) +      if c == '"' then +         return VALUE, i + 1 +      end +      if c ~= '\\' then +         VALUE = VALUE .. c +         i = i + 1 +      elseif text:match('^\\b', i) then +         VALUE = VALUE .. "\b" +         i = i + 2 +      elseif text:match('^\\f', i) then +         VALUE = VALUE .. "\f" +         i = i + 2 +      elseif text:match('^\\n', i) then +         VALUE = VALUE .. "\n" +         i = i + 2 +      elseif text:match('^\\r', i) then +         VALUE = VALUE .. "\r" +         i = i + 2 +      elseif text:match('^\\t', i) then +         VALUE = VALUE .. "\t" +         i = i + 2 +      else +         local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) +         if hex then +            i = i + 6 -- bypass what we just read + +            -- We have a Unicode codepoint. It could be standalone, or if in the proper range and +            -- followed by another in a specific range, it'll be a two-code surrogate pair. +            local codepoint = tonumber(hex, 16) +            if codepoint >= 0xD800 and codepoint <= 0xDBFF then +               -- it's a hi surrogate... see whether we have a following low +               local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) +               if lo_surrogate then +                  i = i + 6 -- bypass the low surrogate we just read +                  codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16) +               else +                  -- not a proper low, so we'll just leave the first codepoint as is and spit it out. +               end +            end +            VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint) + +         else + +            -- just pass through what's escaped +            VALUE = VALUE .. text:match('^\\(.)', i) +            i = i + 2 +         end +      end +   end + +   self:onDecodeError("unclosed string", text, start, etc) +end + +local function skip_whitespace(text, start) + +   local _, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2 +   if match_end then +      return match_end + 1 +   else +      return start +   end +end + +local grok_one -- assigned later + +local function grok_object(self, text, start, etc) +   if text:sub(start,start) ~= '{' then +      self:onDecodeError("expected '{'", text, start, etc) +   end + +   local i = skip_whitespace(text, start + 1) -- +1 to skip the '{' + +   local VALUE = self.strictTypes and self:newObject { } or { } + +   if text:sub(i,i) == '}' then +      return VALUE, i + 1 +   end +   local text_len = text:len() +   while i <= text_len do +      local key, new_i = grok_string(self, text, i, etc) + +      i = skip_whitespace(text, new_i) + +      if text:sub(i, i) ~= ':' then +         self:onDecodeError("expected colon", text, i, etc) +      end + +      i = skip_whitespace(text, i + 1) + +      local new_val, new_i = grok_one(self, text, i) + +      VALUE[key] = new_val + +      -- +      -- Expect now either '}' to end things, or a ',' to allow us to continue. +      -- +      i = skip_whitespace(text, new_i) + +      local c = text:sub(i,i) + +      if c == '}' then +         return VALUE, i + 1 +      end + +      if text:sub(i, i) ~= ',' then +         self:onDecodeError("expected comma or '}'", text, i, etc) +      end + +      i = skip_whitespace(text, i + 1) +   end + +   self:onDecodeError("unclosed '{'", text, start, etc) +end + +local function grok_array(self, text, start, etc) +   if text:sub(start,start) ~= '[' then +      self:onDecodeError("expected '['", text, start, etc) +   end + +   local i = skip_whitespace(text, start + 1) -- +1 to skip the '[' +   local VALUE = self.strictTypes and self:newArray { } or { } +   if text:sub(i,i) == ']' then +      return VALUE, i + 1 +   end + +   local VALUE_INDEX = 1 + +   local text_len = text:len() +   while i <= text_len do +      local val, new_i = grok_one(self, text, i) + +      -- can't table.insert(VALUE, val) here because it's a no-op if val is nil +      VALUE[VALUE_INDEX] = val +      VALUE_INDEX = VALUE_INDEX + 1 + +      i = skip_whitespace(text, new_i) + +      -- +      -- Expect now either ']' to end things, or a ',' to allow us to continue. +      -- +      local c = text:sub(i,i) +      if c == ']' then +         return VALUE, i + 1 +      end +      if text:sub(i, i) ~= ',' then +         self:onDecodeError("expected comma or '['", text, i, etc) +      end +      i = skip_whitespace(text, i + 1) +   end +   self:onDecodeError("unclosed '['", text, start, etc) +end + + +grok_one = function(self, text, start, etc) +   -- Skip any whitespace +   start = skip_whitespace(text, start) + +   if start > text:len() then +      self:onDecodeError("unexpected end of string", text, nil, etc) +   end + +   if text:find('^"', start) then +      return grok_string(self, text, start, etc) + +   elseif text:find('^[-0123456789 ]', start) then +      return grok_number(self, text, start, etc) + +   elseif text:find('^%{', start) then +      return grok_object(self, text, start, etc) + +   elseif text:find('^%[', start) then +      return grok_array(self, text, start, etc) + +   elseif text:find('^true', start) then +      return true, start + 4 + +   elseif text:find('^false', start) then +      return false, start + 5 + +   elseif text:find('^null', start) then +      return nil, start + 4 + +   else +      self:onDecodeError("can't parse JSON", text, start, etc) +   end +end + +function OBJDEF:decode(text, etc) +   if type(self) ~= 'table' or self.__index ~= OBJDEF then +      OBJDEF:onDecodeError("JSON:decode must be called in method format", nil, nil, etc) +   end + +   if text == nil then +      self:onDecodeOfNilError(string.format("nil passed to JSON:decode()"), nil, nil, etc) +   elseif type(text) ~= 'string' then +      self:onDecodeError(string.format("expected string argument to JSON:decode(), got %s", type(text)), nil, nil, etc) +   end + +   if text:match('^%s*$') then +      return nil +   end + +   if text:match('^%s*<') then +      -- Can't be JSON... we'll assume it's HTML +      self:onDecodeOfHTMLError(string.format("html passed to JSON:decode()"), text, nil, etc) +   end + +   -- +   -- Ensure that it's not UTF-32 or UTF-16. +   -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3), +   -- but this package can't handle them. +   -- +   if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then +      self:onDecodeError("JSON package groks only UTF-8, sorry", text, nil, etc) +   end + +   local success, value = pcall(grok_one, self, text, 1, etc) + +   if success then +      return value +   else +      -- if JSON:onDecodeError() didn't abort out of the pcall, we'll have received the error message here as "value", so pass it along as an assert. +      if self.assert then +         self.assert(false, value) +      else +         assert(false, value) +      end +      -- and if we're still here, return a nil and throw the error message on as a second arg +      return nil, value +   end +end + +local function backslash_replacement_function(c) +   if c == "\n" then +      return "\\n" +   elseif c == "\r" then +      return "\\r" +   elseif c == "\t" then +      return "\\t" +   elseif c == "\b" then +      return "\\b" +   elseif c == "\f" then +      return "\\f" +   elseif c == '"' then +      return '\\"' +   elseif c == '\\' then +      return '\\\\' +   else +      return string.format("\\u%04x", c:byte()) +   end +end + +local chars_to_be_escaped_in_JSON_string +   = '[' +   ..    '"'    -- class sub-pattern to match a double quote +   ..    '%\\'  -- class sub-pattern to match a backslash +   ..    '%z'   -- class sub-pattern to match a null +   ..    '\001' .. '-' .. '\031' -- class sub-pattern to match control characters +   .. ']' + +local function json_string_literal(value) +   local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function) +   return '"' .. newval .. '"' +end + +local function object_or_array(self, T, etc) +   -- +   -- We need to inspect all the keys... if there are any strings, we'll convert to a JSON +   -- object. If there are only numbers, it's a JSON array. +   -- +   -- If we'll be converting to a JSON object, we'll want to sort the keys so that the +   -- end result is deterministic. +   -- +   local string_keys = { } +   local number_keys = { } +   local number_keys_must_be_strings = false +   local maximum_number_key + +   for key in pairs(T) do +      if type(key) == 'string' then +         table.insert(string_keys, key) +      elseif type(key) == 'number' then +         table.insert(number_keys, key) +         if key <= 0 or key >= math.huge then +            number_keys_must_be_strings = true +         elseif not maximum_number_key or key > maximum_number_key then +            maximum_number_key = key +         end +      else +         self:onEncodeError("can't encode table with a key of type " .. type(key), etc) +      end +   end + +   if #string_keys == 0 and not number_keys_must_be_strings then +      -- +      -- An empty table, or a numeric-only array +      -- +      if #number_keys > 0 then +         return nil, maximum_number_key -- an array +      elseif tostring(T) == "JSON array" then +         return nil +      elseif tostring(T) == "JSON object" then +         return { } +      else +         -- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects +         return nil +      end +   end + +   table.sort(string_keys) + +   local map +   if #number_keys > 0 then +      -- +      -- If we're here then we have either mixed string/number keys, or numbers inappropriate for a JSON array +      -- It's not ideal, but we'll turn the numbers into strings so that we can at least create a JSON object. +      -- + +      if self.noKeyConversion then +         self:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc) +      end + +      -- +      -- Have to make a shallow copy of the source table so we can remap the numeric keys to be strings +      -- +      map = { } +      for key, val in pairs(T) do +         map[key] = val +      end + +      table.sort(number_keys) + +      -- +      -- Throw numeric keys in there as strings +      -- +      for _, number_key in ipairs(number_keys) do +         local string_key = tostring(number_key) +         if map[string_key] == nil then +            table.insert(string_keys , string_key) +            map[string_key] = T[number_key] +         else +            self:onEncodeError("conflict converting table with mixed-type keys into a JSON object: key " .. number_key .. " exists both as a string and a number.", etc) +         end +      end +   end + +   return string_keys, nil, map +end + +-- +-- Encode +-- +-- 'options' is nil, or a table with possible keys: +--    pretty            -- if true, return a pretty-printed version +--    indent            -- a string (usually of spaces) used to indent each nested level +--    align_keys        -- if true, align all the keys when formatting a table +-- +local encode_value -- must predeclare because it calls itself +function encode_value(self, value, parents, etc, options, indent) + +   if value == nil then +      return 'null' + +   elseif type(value) == 'string' then +      return json_string_literal(value) + +   elseif type(value) == 'number' then +      if value ~= value then +         -- +         -- NaN (Not a Number). +         -- JSON has no NaN, so we have to fudge the best we can. This should really be a package option. +         -- +         return "null" +      elseif value >= math.huge then +         -- +         -- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should +         -- really be a package option. Note: at least with some implementations, positive infinity +         -- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is. +         -- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">=" +         -- case first. +         -- +         return "1e+9999" +      elseif value <= -math.huge then +         -- +         -- Negative infinity. +         -- JSON has no INF, so we have to fudge the best we can. This should really be a package option. +         -- +         return "-1e+9999" +      else +         return tostring(value) +      end + +   elseif type(value) == 'boolean' then +      return tostring(value) + +   elseif type(value) ~= 'table' then +      self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc) + +   else +      -- +      -- A table to be converted to either a JSON object or array. +      -- +      local T = value + +      if type(options) ~= 'table' then +         options = {} +      end +      if type(indent) ~= 'string' then +         indent = "" +      end + +      if parents[T] then +         self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc) +      else +         parents[T] = true +      end + +      local result_value + +      local object_keys, maximum_number_key, map = object_or_array(self, T, etc) +      if maximum_number_key then +         -- +         -- An array... +         -- +         local ITEMS = { } +         for i = 1, maximum_number_key do +            table.insert(ITEMS, encode_value(self, T[i], parents, etc, options, indent)) +         end + +         if options.pretty then +            result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]" +         else +            result_value = "["  .. table.concat(ITEMS, ",")  .. "]" +         end + +      elseif object_keys then +         -- +         -- An object +         -- +         local TT = map or T + +         if options.pretty then + +            local KEYS = { } +            local max_key_length = 0 +            for _, key in ipairs(object_keys) do +               local encoded = encode_value(self, tostring(key), parents, etc, options, indent) +               if options.align_keys then +                  max_key_length = math.max(max_key_length, #encoded) +               end +               table.insert(KEYS, encoded) +            end +            local key_indent = indent .. tostring(options.indent or "") +            local subtable_indent = key_indent .. string.rep(" ", max_key_length) .. (options.align_keys and "  " or "") +            local FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s" + +            local COMBINED_PARTS = { } +            for i, key in ipairs(object_keys) do +               local encoded_val = encode_value(self, TT[key], parents, etc, options, subtable_indent) +               table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val)) +            end +            result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}" + +         else + +            local PARTS = { } +            for _, key in ipairs(object_keys) do +               local encoded_val = encode_value(self, TT[key],       parents, etc, options, indent) +               local encoded_key = encode_value(self, tostring(key), parents, etc, options, indent) +               table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val)) +            end +            result_value = "{" .. table.concat(PARTS, ",") .. "}" + +         end +      else +         -- +         -- An empty array/object... we'll treat it as an array, though it should really be an option +         -- +         result_value = "[]" +      end + +      parents[T] = false +      return result_value +   end +end + + +function OBJDEF:encode(value, etc, options) +   if type(self) ~= 'table' or self.__index ~= OBJDEF then +      OBJDEF:onEncodeError("JSON:encode must be called in method format", etc) +   end +   return encode_value(self, value, {}, etc, options or nil) +end + +function OBJDEF:encode_pretty(value, etc, options) +   if type(self) ~= 'table' or self.__index ~= OBJDEF then +      OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc) +   end +   return encode_value(self, value, {}, etc, options or default_pretty_options) +end + +function OBJDEF.__tostring() +   return "JSON encode/decode package" +end + +OBJDEF.__index = OBJDEF + +function OBJDEF:new(args) +   local new = { } + +   if args then +      for key, val in pairs(args) do +         new[key] = val +      end +   end + +   return setmetatable(new, OBJDEF) +end + +return OBJDEF:new() + +-- +-- Version history: +-- +--   20141223.14   The encode_pretty() routine produced fine results for small datasets, but isn't really +--                 appropriate for anything large, so with help from Alex Aulbach I've made the encode routines +--                 more flexible, and changed the default encode_pretty() to be more generally useful. +-- +--                 Added a third 'options' argument to the encode() and encode_pretty() routines, to control +--                 how the encoding takes place. +-- +--                 Updated docs to add assert() call to the loadfile() line, just as good practice so that +--                 if there is a problem loading JSON.lua, the appropriate error message will percolate up. +-- +--   20140920.13   Put back (in a way that doesn't cause warnings about unused variables) the author string, +--                 so that the source of the package, and its version number, are visible in compiled copies. +-- +--   20140911.12   Minor lua cleanup. +--                 Fixed internal reference to 'JSON.noKeyConversion' to reference 'self' instead of 'JSON'. +--                 (Thanks to SmugMug's David Parry for these.) +-- +--   20140418.11   JSON nulls embedded within an array were being ignored, such that +--                     ["1",null,null,null,null,null,"seven"], +--                 would return +--                     {1,"seven"} +--                 It's now fixed to properly return +--                     {1, nil, nil, nil, nil, nil, "seven"} +--                 Thanks to "haddock" for catching the error. +-- +--   20140116.10   The user's JSON.assert() wasn't always being used. Thanks to "blue" for the heads up. +-- +--   20131118.9    Update for Lua 5.3... it seems that tostring(2/1) produces "2.0" instead of "2", +--                 and this caused some problems. +-- +--   20131031.8    Unified the code for encode() and encode_pretty(); they had been stupidly separate, +--                 and had of course diverged (encode_pretty didn't get the fixes that encode got, so +--                 sometimes produced incorrect results; thanks to Mattie for the heads up). +-- +--                 Handle encoding tables with non-positive numeric keys (unlikely, but possible). +-- +--                 If a table has both numeric and string keys, or its numeric keys are inappropriate +--                 (such as being non-positive or infinite), the numeric keys are turned into +--                 string keys appropriate for a JSON object. So, as before, +--                         JSON:encode({ "one", "two", "three" }) +--                 produces the array +--                         ["one","two","three"] +--                 but now something with mixed key types like +--                         JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) +--                 instead of throwing an error produces an object: +--                         {"1":"one","2":"two","3":"three","SOMESTRING":"some string"} +-- +--                 To maintain the prior throw-an-error semantics, set +--                      JSON.noKeyConversion = true +--                  +--   20131004.7    Release under a Creative Commons CC-BY license, which I should have done from day one, sorry. +-- +--   20130120.6    Comment update: added a link to the specific page on my blog where this code can +--                 be found, so that folks who come across the code outside of my blog can find updates +--                 more easily. +-- +--   20111207.5    Added support for the 'etc' arguments, for better error reporting. +-- +--   20110731.4    More feedback from David Kolf on how to make the tests for Nan/Infinity system independent. +-- +--   20110730.3    Incorporated feedback from David Kolf at http://lua-users.org/wiki/JsonModules: +-- +--                   * When encoding lua for JSON, Sparse numeric arrays are now handled by +--                     spitting out full arrays, such that +--                        JSON:encode({"one", "two", [10] = "ten"}) +--                     returns +--                        ["one","two",null,null,null,null,null,null,null,"ten"] +-- +--                     In 20100810.2 and earlier, only up to the first non-null value would have been retained. +-- +--                   * When encoding lua for JSON, numeric value NaN gets spit out as null, and infinity as "1+e9999". +--                     Version 20100810.2 and earlier created invalid JSON in both cases. +-- +--                   * Unicode surrogate pairs are now detected when decoding JSON. +-- +--   20100810.2    added some checking to ensure that an invalid Unicode character couldn't leak in to the UTF-8 encoding +-- +--   20100731.1    initial public release +-- diff --git a/mediawiki.lrdevplugin/MediaWikiApi.lua b/mediawiki.lrdevplugin/MediaWikiApi.lua index de432a7..e54e973 100755 --- a/mediawiki.lrdevplugin/MediaWikiApi.lua +++ b/mediawiki.lrdevplugin/MediaWikiApi.lua @@ -18,12 +18,14 @@ local LrHttp = import 'LrHttp'  local LrPathUtils = import 'LrPathUtils'  local LrXml = import 'LrXml' +local JSON = require 'JSON'  local Info = require 'Info'  local MediaWikiUtils = require 'MediaWikiUtils'  local MediaWikiApi = {  	userAgent = string.format('LrMediaWiki %d.%d', Info.VERSION.major, Info.VERSION.minor),  	apiPath = nil, +	githubApiVersion = 'https://api.github.com/repos/robinkrahl/LrMediaWiki/releases',  }  --- URL-encode a string according to RFC 3986. @@ -53,7 +55,7 @@ function MediaWikiApi.createRequestBody(arguments)  		end  		body = body .. MediaWikiApi.urlEncode(key) .. '=' .. MediaWikiApi.urlEncode(value)  	end -	return body +	return body or ''  end  function MediaWikiApi.parseXmlDom(xmlDomInstance) @@ -76,30 +78,27 @@ function MediaWikiApi.parseXmlDom(xmlDomInstance)  	return value  end -function MediaWikiApi.performRequest(arguments) -	arguments.format = 'xml' +function MediaWikiApi.performHttpRequest(path, arguments, requestHeaders, post)  	local requestBody = MediaWikiApi.createRequestBody(arguments) -	local requestHeaders = { -		{ -			field = 'User-Agent', -			value = MediaWikiApi.userAgent, -		}, -		{ -			field = 'Content-Type', -			value = 'application/x-www-form-urlencoded', -		}, -	} -	MediaWikiUtils.trace('Performing API request'); + +	MediaWikiUtils.trace('Performing HTTP request'); +	MediaWikiUtils.trace('Path:') +	MediaWikiUtils.trace(path)  	MediaWikiUtils.trace('Request body:');  	MediaWikiUtils.trace(requestBody); -	local resultBody, resultHeaders = LrHttp.post(MediaWikiApi.apiPath, requestBody, requestHeaders) +	local resultBody, resultHeaders +	if post then +		resultBody, resultHeaders = LrHttp.post(path, requestBody, requestHeaders) +	else +		resultBody, resultHeaders = LrHttp.get(path .. '?' .. requestBody, requestHeaders) +	end  	MediaWikiUtils.trace('Result status:');  	MediaWikiUtils.trace(resultHeaders.status);  	if not resultHeaders.status then -		LrErrors.throwUserError(LOC('$$$/LrMediaWiki/Api/NoConnection=Cannot connect to the MediaWiki API.')) +		LrErrors.throwUserError(LOC('$$$/LrMediaWiki/Api/NoConnection=No network connection.'))  	elseif resultHeaders.status ~= 200 then  		LrErrors.throwUserError(LOC('$$$/LrMediaWiki/Api/HttpError=Received HTTP status ^1.', resultHeaders.status))  	end @@ -107,6 +106,23 @@ function MediaWikiApi.performRequest(arguments)  	MediaWikiUtils.trace('Result body:');  	MediaWikiUtils.trace(resultBody); +	return resultBody +end + +function MediaWikiApi.performRequest(arguments) +	arguments.format = 'xml' +	local requestHeaders = { +		{ +			field = 'User-Agent', +			value = MediaWikiApi.userAgent, +		}, +		{ +			field = 'Content-Type', +			value = 'application/x-www-form-urlencoded', +		}, +	} + +	local resultBody = MediaWikiApi.performHttpRequest(MediaWikiApi.apiPath, arguments, requestHeaders, true)  	local resultXml = MediaWikiApi.parseXmlDom(LrXml.parseXml(resultBody))  	if resultXml.error then  		LrErrors.throwUserError(LOC('$$$/LrMediaWiki/Api/MediaWikiError=The MediaWiki error ^1 occured: ^2', resultXml.error.code, resultXml.error.info)) @@ -114,6 +130,22 @@ function MediaWikiApi.performRequest(arguments)  	return resultXml  end +function MediaWikiApi.getCurrentPluginVersion() +	local requestHeaders = { +		{ +			field = 'User-Agent', +			value = MediaWikiApi.userAgent, +		}, +	} +	local resultBody = MediaWikiApi.performHttpRequest(MediaWikiApi.githubApiVersion, {}, requestHeaders, false) +	local resultJson = JSON:decode(resultBody) +	local firstKey = MediaWikiUtils.getFirstKey(resultJson) +	if firstKey ~= nil then +		return resultJson[firstKey].tag_name +	end +	return nil +end +  function MediaWikiApi.login(username, password, token)  	local arguments = {  		action = 'login', diff --git a/mediawiki.lrdevplugin/MediaWikiInit.lua b/mediawiki.lrdevplugin/MediaWikiInit.lua new file mode 100644 index 0000000..d775930 --- /dev/null +++ b/mediawiki.lrdevplugin/MediaWikiInit.lua @@ -0,0 +1,33 @@ +-- This file is part of the LrMediaWiki project and distributed under the terms
 +-- of the MIT license (see LICENSE.txt file in the project root directory or
 +-- [0]).  See [1] for more information about LrMediaWiki.
 +--
 +-- Copyright (C) 2015 by the LrMediaWiki team (see CREDITS.txt file in the
 +-- project root directory or [2])
 +--
 +-- [0]  <https://raw.githubusercontent.com/robinkrahl/LrMediaWiki/master/LICENSE.txt>
 +-- [1]  <https://commons.wikimedia.org/wiki/Commons:LrMediaWiki>
 +-- [2]  <https://raw.githubusercontent.com/robinkrahl/LrMediaWiki/master/CREDITS.txt>
 +
 +-- Code status:
 +-- doc:   missing
 +-- i18n:  complete
 +
 +local LrDialogs = import 'LrDialogs'
 +local LrTasks = import 'LrTasks'
 +
 +local MediaWikiApi = require 'MediaWikiApi'
 +local MediaWikiUtils = require 'MediaWikiUtils'
 +
 +if MediaWikiUtils.getCheckVersion() then
 +  LrTasks.startAsyncTask(function()
 +    local currentVersion = MediaWikiApi.getCurrentPluginVersion()
 +    if currentVersion ~= nil then
 +      MediaWikiUtils.trace('Current version of LrMediaWiki is: ' .. currentVersion)
 +      if currentVersion ~= 'v' .. MediaWikiUtils.getVersionString() then
 +        -- new version available!
 +        LrDialogs.message(LOC '$$$/LrMediaWiki/Init/Version/Message=New version available', LOC('$$$/LrMediaWiki/Init/Version/Info=Please update to LrMediaWiki ^1.', currentVersion), 'info')
 +      end
 +    end
 +  end)
 +end
 diff --git a/mediawiki.lrdevplugin/MediaWikiPluginInfoProvider.lua b/mediawiki.lrdevplugin/MediaWikiPluginInfoProvider.lua index ca87904..1f2f66a 100644 --- a/mediawiki.lrdevplugin/MediaWikiPluginInfoProvider.lua +++ b/mediawiki.lrdevplugin/MediaWikiPluginInfoProvider.lua @@ -24,11 +24,13 @@ local MediaWikiPluginInfoProvider = {}  MediaWikiPluginInfoProvider.startDialog = function(propertyTable)
    propertyTable.logging = MediaWikiUtils.getLogging()
    propertyTable.create_snapshots = MediaWikiUtils.getCreateSnapshots()
 +  propertyTable.check_version = MediaWikiUtils.getCheckVersion()
  end
  MediaWikiPluginInfoProvider.endDialog = function(propertyTable)
    MediaWikiUtils.setLogging(propertyTable.logging)
    MediaWikiUtils.setCreateSnapshots(propertyTable.create_snapshots)
 +  MediaWikiUtils.setCheckVersion(propertyTable.check_version)
  end
  MediaWikiPluginInfoProvider.sectionsForBottomOfDialog = function(viewFactory, propertyTable)
 @@ -65,6 +67,20 @@ MediaWikiPluginInfoProvider.sectionsForBottomOfDialog = function(viewFactory, pr  					},
  					viewFactory:checkbox {
 +						value = bind 'check_version',
 +            title = LOC '$$$/LrMediaWiki/Section/Config/Version=Check for new plugin versions after Lightroom starts',
 +					},
 +				},
 +
 +				viewFactory:row {
 +					spacing = viewFactory:label_spacing(),
 +
 +					viewFactory:static_text {
 +						alignment = labelAlignment,
 +						width = LrView.share "label_width",
 +					},
 +
 +					viewFactory:checkbox {
  						value = bind 'logging',
              title = LOC '$$$/LrMediaWiki/Section/Config/Logging=Enable logging',
  					},
 diff --git a/mediawiki.lrdevplugin/MediaWikiUtils.lua b/mediawiki.lrdevplugin/MediaWikiUtils.lua index abde828..588121d 100644 --- a/mediawiki.lrdevplugin/MediaWikiUtils.lua +++ b/mediawiki.lrdevplugin/MediaWikiUtils.lua @@ -34,6 +34,13 @@ MediaWikiUtils.isStringEmpty = function(str)  	return str == nil or string.match(str, '^%s*$') ~= nil
  end
 +MediaWikiUtils.getFirstKey = function(table)
 +  for key, value in pairs(table) do
 +		return key
 +	end
 +  return nil
 +end
 +
  MediaWikiUtils.getVersionString = function()
      local str = Info.VERSION.major .. '.' .. Info.VERSION.minor
      if Info.VERSION.revision > 0 then
 @@ -52,6 +59,14 @@ MediaWikiUtils.setCreateSnapshots = function(create_snapshot)  	prefs.create_snapshot = create_snapshot
  end
 +MediaWikiUtils.getCheckVersion = function()
 +	return prefs.check_version or false
 +end
 +
 +MediaWikiUtils.setCheckVersion = function(check_version)
 +	prefs.check_version = check_version
 +end
 +
  MediaWikiUtils.getLogging = function()
  	return prefs.logging or false
  end
 diff --git a/mediawiki.lrdevplugin/TranslatedStrings_de.txt b/mediawiki.lrdevplugin/TranslatedStrings_de.txt index eee73b8..f4b8c33 100755 --- a/mediawiki.lrdevplugin/TranslatedStrings_de.txt +++ b/mediawiki.lrdevplugin/TranslatedStrings_de.txt @@ -1,6 +1,6 @@  "$$$/LrMediaWiki/Api/HttpError=HTTP-Status ^1 erhalten."
  "$$$/LrMediaWiki/Api/MediaWikiError=Der MediaWiki-Fehler ^1 ist aufgetreten: ^2"
 -"$$$/LrMediaWiki/Api/NoConnection=Kann keine Verbindung zur MediaWiki-API aufbauen."
 +"$$$/LrMediaWiki/Api/NoConnection=Kann keine Netzwerkverbindung aufbauen."
  "$$$/LrMediaWiki/Export/DescriptionError=Fehler beim Lesen der Dateibeschreibung"
  "$$$/LrMediaWiki/Export/NoApiPath=Kein API-Pfad angegeben!"
  "$$$/LrMediaWiki/Export/NoAuthor=Kein Urheber angegeben!"
 @@ -12,6 +12,8 @@  "$$$/LrMediaWiki/Export/Progress/One=Exportiere ein Bild in ein MediaWiki"
  "$$$/LrMediaWiki/Export/Progress=Exportiere ^1 Bilder in ein MediaWiki"
  "$$$/LrMediaWiki/Export/Snapshot=MediaWiki-Export, ^1 ^2, ^3"
 +"$$$/LrMediaWiki/Init/Version/Info=Bitte aktualisiere LrMediaWiki auf die neue Version ^1."
 +"$$$/LrMediaWiki/Init/Version/Message=Neue Version verfügbar"
  "$$$/LrMediaWiki/Interface/Internal/NotLoggedIn=Interner Fehler: vor dem Upload nicht angemeldet."
  "$$$/LrMediaWiki/Interface/InUse/Cancel=Abbrechen"
  "$$$/LrMediaWiki/Interface/InUse/Details=Es gibt bereits eine Datei mit dem Namen ^1. Überschreiben? (Die Dateibeschreibung wird nicht verändert.)"
 @@ -39,6 +41,7 @@  "$$$/LrMediaWiki/Section/Config/Logging/Warning=Achtung:"
  "$$$/LrMediaWiki/Section/Config/Snapshots=Beim Export einen Schnappschuss anlegen"
  "$$$/LrMediaWiki/Section/Config/Title=Einstellungen"
 +"$$$/LrMediaWiki/Section/Config/Version=Suche nach neuen Pluginversionen, wenn Lightroom startet"
  "$$$/LrMediaWiki/Section/Licensing/Author=Urheber"
  "$$$/LrMediaWiki/Section/Licensing/Categories/Details=mit ; trennen"
  "$$$/LrMediaWiki/Section/Licensing/Categories=Kategorien"
 | 
