luci-base: Make default for FileUpload 'safe'
[project/luci.git] / modules / luci-base / luasrc / cbi.lua
index 34de44a..9b88e9f 100644 (file)
@@ -12,6 +12,7 @@ require("luci.http")
 local fs         = require("nixio.fs")
 local uci        = require("luci.model.uci")
 local datatypes  = require("luci.cbi.datatypes")
+local dispatcher = require("luci.dispatcher")
 local class      = util.class
 local instanceof = util.instanceof
 
@@ -307,8 +308,30 @@ function Map.__init__(self, config, ...)
 
        self.changed = false
 
-       if not self.uci:load(self.config) then
-               error("Unable to read UCI data: " .. self.config)
+       local path = "%s/%s" %{ self.uci:get_confdir(), self.config }
+       if fs.stat(path, "type") ~= "reg" then
+               fs.writefile(path, "")
+       end
+
+       local ok, err = self.uci:load(self.config)
+       if not ok then
+               local url = dispatcher.build_url(unpack(dispatcher.context.request))
+               local source = self:formvalue("cbi.source")
+               if type(source) == "string" then
+                       fs.writefile(path, source:gsub("\r\n", "\n"))
+                       ok, err = self.uci:load(self.config)
+                       if ok then
+                               luci.http.redirect(url)
+                       end
+               end
+               self.save = false
+       end
+
+       if not ok then
+               self.template   = "cbi/error"
+               self.error      = err
+               self.source     = fs.readfile(path) or ""
+               self.pageaction = false
        end
 end
 
@@ -344,63 +367,64 @@ end
 
 -- Use optimized UCI writing
 function Map.parse(self, readinput, ...)
-       self.readinput = (readinput ~= false)
-       self:_run_hooks("on_parse")
-
        if self:formvalue("cbi.skip") then
                self.state = FORM_SKIP
+       elseif not self.save then
+               self.state = FORM_INVALID
+       elseif not self:submitstate() then
+               self.state = FORM_NODATA
+       end
+
+       -- Back out early to prevent unauthorized changes on the subsequent parse
+       if self.state ~= nil then
                return self:state_handler(self.state)
        end
 
+       self.readinput = (readinput ~= false)
+       self:_run_hooks("on_parse")
+
        Node.parse(self, ...)
 
-       if self.save then
-               self:_run_hooks("on_save", "on_before_save")
+       self:_run_hooks("on_save", "on_before_save")
+       for i, config in ipairs(self.parsechain) do
+               self.uci:save(config)
+       end
+       self:_run_hooks("on_after_save")
+       if (not self.proceed and self.flow.autoapply) or luci.http.formvalue("cbi.apply") then
+               self:_run_hooks("on_before_commit")
                for i, config in ipairs(self.parsechain) do
-                       self.uci:save(config)
-               end
-               self:_run_hooks("on_after_save")
-               if self:submitstate() and ((not self.proceed and self.flow.autoapply) or luci.http.formvalue("cbi.apply")) then
-                       self:_run_hooks("on_before_commit")
-                       for i, config in ipairs(self.parsechain) do
-                               self.uci:commit(config)
-
-                               -- Refresh data because commit changes section names
-                               self.uci:load(config)
-                       end
-                       self:_run_hooks("on_commit", "on_after_commit", "on_before_apply")
-                       if self.apply_on_parse then
-                               self.uci:apply(self.parsechain)
-                               self:_run_hooks("on_apply", "on_after_apply")
-                       else
-                               -- This is evaluated by the dispatcher and delegated to the
-                               -- template which in turn fires XHR to perform the actual
-                               -- apply actions.
-                               self.apply_needed = true
-                       end
-
-                       -- Reparse sections
-                       Node.parse(self, true)
+                       self.uci:commit(config)
 
+                       -- Refresh data because commit changes section names
+                       self.uci:load(config)
                end
-               for i, config in ipairs(self.parsechain) do
-                       self.uci:unload(config)
-               end
-               if type(self.commit_handler) == "function" then
-                       self:commit_handler(self:submitstate())
+               self:_run_hooks("on_commit", "on_after_commit", "on_before_apply")
+               if self.apply_on_parse then
+                       self.uci:apply(self.parsechain)
+                       self:_run_hooks("on_apply", "on_after_apply")
+               else
+                       -- This is evaluated by the dispatcher and delegated to the
+                       -- template which in turn fires XHR to perform the actual
+                       -- apply actions.
+                       self.apply_needed = true
                end
+
+               -- Reparse sections
+               Node.parse(self, true)
+       end
+       for i, config in ipairs(self.parsechain) do
+               self.uci:unload(config)
+       end
+       if type(self.commit_handler) == "function" then
+               self:commit_handler(self:submitstate())
        end
 
-       if self:submitstate() then
-               if not self.save then
-                       self.state = FORM_INVALID
-               elseif self.proceed then
-                       self.state = FORM_PROCEED
-               else
-                       self.state = self.changed and FORM_CHANGED or FORM_VALID
-               end
+       if self.proceed then
+               self.state = FORM_PROCEED
+       elseif self.changed then
+               self.state = FORM_CHANGED
        else
-               self.state = FORM_NODATA
+               self.state = FORM_VALID
        end
 
        return self:state_handler(self.state)
@@ -1447,6 +1471,7 @@ function Value.__init__(self, ...)
        self.template  = "cbi/value"
        self.keylist = {}
        self.vallist = {}
+       self.readonly = nil
 end
 
 function Value.reset_values(self)
@@ -1460,6 +1485,10 @@ function Value.value(self, key, val)
        table.insert(self.vallist, tostring(val))
 end
 
+function Value.parse(self, section, novld)
+       if self.readonly then return end
+       AbstractValue.parse(self, section, novld)
+end
 
 -- DummyValue - This does nothing except being there
 DummyValue = class(AbstractValue)
@@ -1504,26 +1533,39 @@ function Flag.__init__(self, ...)
 end
 
 -- A flag can only have two states: set or unset
-function Flag.parse(self, section)
+function Flag.parse(self, section, novld)
        local fexists = self.map:formvalue(
                FEXIST_PREFIX .. self.config .. "." .. section .. "." .. self.option)
 
        if fexists then
                local fvalue = self:formvalue(section) and self.enabled or self.disabled
-               if fvalue ~= self.default or (not self.optional and not self.rmempty) then
-                       self:write(section, fvalue)
-               else
+               local cvalue = self:cfgvalue(section)
+               local val_err
+               fvalue, val_err = self:validate(fvalue, section)
+               if not fvalue then
+                       if not novld then
+                               self:add_error(section, "invalid", val_err)
+                       end
+                       return
+               end
+               if fvalue == self.default and (self.optional or self.rmempty) then
                        self:remove(section)
+               else
+                       self:write(section, fvalue)
                end
+               if (fvalue ~= cvalue) then self.section.changed = true end
        else
                self:remove(section)
+               self.section.changed = true
        end
 end
 
 function Flag.cfgvalue(self, section)
        return AbstractValue.cfgvalue(self, section) or self.default
 end
-
+function Flag.validate(self, value)
+       return value
+end
 
 --[[
 ListValue - A one-line value predefined in a list
@@ -1769,6 +1811,7 @@ function Button.__init__(self, ...)
        self.template  = "cbi/button"
        self.inputstyle = nil
        self.rmempty = true
+        self.unsafeupload = false
 end
 
 
@@ -1785,9 +1828,15 @@ function FileUpload.__init__(self, ...)
 end
 
 function FileUpload.formcreated(self, section)
-       return AbstractValue.formcreated(self, section) or
-               self.map:formvalue("cbi.rlf."..section.."."..self.option) or
-               self.map:formvalue("cbi.rlf."..section.."."..self.option..".x")
+       if self.unsafeupload then
+               return AbstractValue.formcreated(self, section) or
+                       self.map:formvalue("cbi.rlf."..section.."."..self.option) or
+                       self.map:formvalue("cbi.rlf."..section.."."..self.option..".x") or
+                       self.map:formvalue("cbid."..self.map.config.."."..section.."."..self.option..".textbox")
+       else
+               return AbstractValue.formcreated(self, section) or
+                       self.map:formvalue("cbid."..self.map.config.."."..section.."."..self.option..".textbox")
+       end
 end
 
 function FileUpload.cfgvalue(self, section)
@@ -1798,27 +1847,50 @@ function FileUpload.cfgvalue(self, section)
        return nil
 end
 
+-- If we have a new value, use it
+-- otherwise use old value
+-- deletion should be managed by a separate button object
+-- unless self.unsafeupload is set in which case if the user
+-- choose to remove the old file we do so.
+-- Also, allow to specify (via textbox) a file already on router
 function FileUpload.formvalue(self, section)
        local val = AbstractValue.formvalue(self, section)
        if val then
-               if not self.map:formvalue("cbi.rlf."..section.."."..self.option) and
-                  not self.map:formvalue("cbi.rlf."..section.."."..self.option..".x")
-               then
+               if self.unsafeupload then
+                       if not self.map:formvalue("cbi.rlf."..section.."."..self.option) and
+                           not self.map:formvalue("cbi.rlf."..section.."."..self.option..".x")
+                       then
+                               return val
+                       end
+                       fs.unlink(val)
+                       self.value = nil
+                       return nil
+                elseif val ~= "" then
                        return val
-               end
-               fs.unlink(val)
-               self.value = nil
+                end
        end
-       return nil
+       val = luci.http.formvalue("cbid."..self.map.config.."."..section.."."..self.option..".textbox")
+       if val == "" then
+               val = nil
+       end
+        if not self.unsafeupload then
+               if not val then
+                       val = self.map:formvalue("cbi.rlf."..section.."."..self.option)
+               end
+        end
+       return val
 end
 
 function FileUpload.remove(self, section)
-       local val = AbstractValue.formvalue(self, section)
-       if val and fs.access(val) then fs.unlink(val) end
-       return AbstractValue.remove(self, section)
+       if self.unsafeupload then
+               local val = AbstractValue.formvalue(self, section)
+               if val and fs.access(val) then fs.unlink(val) end
+               return AbstractValue.remove(self, section)
+       else
+               return nil
+       end
 end
 
-
 FileBrowser = class(AbstractValue)
 
 function FileBrowser.__init__(self, ...)