aboutsummaryrefslogtreecommitdiff
-- mod-version:3 priority:99
local core = require "core"
local common = require "core.common"
local command = require "core.command"
local config = require "core.config"
local Doc = require "core.doc"


-- When pkexec version >= 121 is more widespread,
-- change to "pkexec --keep-cwd cp '%s' '%s'"
local default_command = "pkexec sh -c \"cd $PWD; cp '%s' '%s'\""
config.plugins.su_save = common.merge({
  enabled = true,
  save_command = default_command,
  config_spec = {
    name = "Super User Save",
    {
      label = "Enabled",
      description = "Disable or enable the automatic save as super user.",
      path = "enabled",
      type = "toggle",
      default = true
    },
    {
      label = "Save command",
      description = "Command used to save the temporary file (first '%s') over the original file (second '%s').",
      path = "save_command",
      type = "string",
      default = default_command
    },
  }
}, config.plugins.su_save)


local doc_save = Doc.save
local function su_save(doc, filename, abs_filename, ...)
  if not config.plugins.su_save then
    return error("Bad su_save plugin configuration")
  end

  local old_clean_change_id = doc.clean_change_id

  -- Override io.open to check for permission errors
  local io_open = io.open
  local temp_filename, save_location
  local io_open_valid = true
  io.open = function(...)
    -- Only override the first io.open call. Hopefully this works well enough.
    io.open = io_open

    -- In case Doc.save crashes before even getting to the first io.open,
    -- we need to use the original one.
    if not io_open_valid then return io_open(...) end

    local fp, error_msg, error_code = io_open(...)
    -- If there was an access issue with open, save to a temporary file
    if error_code == 13 then -- 13 seems to be EACCES, to verify use `errno -l`
      save_location = select(1, ...)
      temp_filename = core.temp_filename()
      core.log_quiet('Trying to save "%s" as super user using "%s" as temporary file', save_location, temp_filename)
      return io_open(temp_filename, select(2, ...))
    end

    return fp, error_msg, error_code
  end

  -- Call original Doc:save, now with custom io.open
  local ok, result = pcall(doc_save, doc, filename, abs_filename, ...)
  io_open_valid = false

  if temp_filename then
    if ok then
      -- This is using the blocking os.execute to simplify error management
      local success, exit_type, exit_code = os.execute(string.format(config.plugins.su_save.save_command, temp_filename, save_location))
      if not success then
        -- Restore change_id because save failed
        doc.clean_change_id = old_clean_change_id
        -- 126 means "dismissed" for pkexec. TODO: Should this be configurable?
        if exit_type == "exit" and exit_code == 126 then
          return error(string.format('Unable to save "%s" with super user permissions (dismissed)', save_location))
        end
        return error(string.format('Unable to save "%s" with super user permissions (%s code %d)', save_location, exit_type, exit_code))
      end
    end
    os.remove(temp_filename)
  end

  if not ok then
    -- Propagate error
    return error(result)
  end

  return result
end

function Doc:save(...)
  if not (config.plugins.su_save and config.plugins.su_save.enabled) then
    return doc_save(self, ...)
  end
  return su_save(self, ...)
end

command.add("core.docview!", {
  ["su-save:save-as-super-user"] = function(dv)
    su_save(dv.doc)
    core.log('Saved "%s"', dv.doc.filename)
  end
})