cgi-bin/tek/class/loona.lua
author Timm S. Mueller <tmueller@neoscientists.org>
Sun, 20 May 2007 18:28:42 +0200
changeset 188 77f536d09f08
parent 183 cgi-bin/loona.lua@8fd80d31c396
child 189 efb1bd425c56
permissions -rw-r--r--
tek namespace and application restructured
     1 
     2 --
     3 --	loona - tiny CMS
     4 --	Written by Timm S. Mueller <tmueller at neoscientists.org>
     5 --	See copyright notice in COPYRIGHT
     6 --
     7 
     8 local lib = require "tek.lib"
     9 local luahtml = require "tek.lib.luahtml"
    10 local posix = require "tek.os.posix"
    11 local http = require "tek.class.http"
    12 local Request = require "tek.class.http.request"
    13 local util = require "tek.class.loona.util"
    14 local markup = require "tek.class.loona.markup"
    15 
    16 local boxed_G = { 
    17 	string = string, table = table,
    18 	assert = assert, collectgarbage = collectgarbage, dofile = dofile,
    19 	error = error, getfenv = getfenv, getmetatable = getmetatable,
    20 	ipairs = ipairs, load = load, loadfile = loadfile, loadstring = loadstring,
    21 	next = next, pairs = pairs, pcall = pcall, print = print,
    22 	rawequal = rawequal, rawget = rawget, rawset = rawset, require = require,
    23 	select = select, setfenv = setfenv, setmetatable = setmetatable,
    24 	tonumber = tonumber, tostring = tostring, type = type, unpack = unpack,
    25 	xpcall = xpcall
    26 }
    27 
    28 local table, string, assert, unpack, ipairs, pairs, type, require =
    29 	table, string, assert, unpack, ipairs, pairs, type, require
    30 local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv
    31 local open, remove, rename, getenv, time, date =
    32 	io.open, os.remove, os.rename, os.getenv, os.time, os.date
    33 
    34 
    35 module "tek.class.loona"
    36 
    37 
    38 _VERSION = 4
    39 _REVISION = 1
    40 
    41 
    42 -- Session
    43 
    44 
    45 local Session = { }
    46 
    47 
    48 function Session:new(o)
    49 
    50 	o = o or { }
    51  	setmetatable(o, self)
    52  	self.__index = self
    53 	
    54 	assert(o.id, "No session Id")
    55  	assert(o.sessiondir, "No session directory")
    56 	
    57 	o.name = o.id:gsub("(.)", function(a)
    58 		return ("%02x"):format(a:byte())
    59 	end)
    60 	o.filename = o.sessiondir .. "/" .. o.name
    61 	-- remove non-dotted files (expired sessions) from sessions dir:
    62 	util.expire(o.sessiondir, "[^.]%S+", o.maxage or 600)
    63 	-- load session state:
    64 	o.data = lib.source(o.filename) or { }
    65 	
    66 	return o
    67 end
    68 
    69 
    70 function Session:save()
    71 	local f = open(self.filename, "wb")
    72 	assert(f, "Failed to open session file for writing")
    73 	lib.dump(self.data, function(...) 
    74 		f:write(unpack(arg)) 
    75 	end)
    76 	f:close()
    77 end
    78 
    79 
    80 function Session:delete()
    81 	remove(self.filename)
    82 end
    83 
    84 
    85 -- LOona
    86 
    87 
    88 local Loona = getfenv()
    89 
    90 
    91 function Loona:dbmsg(msg, detail)
    92  	return (msg and detail and self.authuser) and
    93  		("%s : %s"):format(msg, detail) or msg
    94 end
    95 
    96 
    97 function Loona:checkprofilename(n)
    98 	assert(n:match("^%w+$") and n ~= "current",
    99 		self:dbmsg("Invalid profile name", n))
   100 	return n
   101 end
   102 
   103 
   104 function Loona:checklanguage(n)
   105 	assert(n:match("^%l%l$"), self:dbmsg("Invalid language code", n))
   106 	return n
   107 end
   108 
   109 
   110 function Loona:checkbodyname(s)
   111 	s = s or "main"
   112 	assert(s:match("^[%w_]*%w+[%w_]*$"), self:dbmsg("Invalid body name", s))
   113 	return s
   114 end
   115 
   116 
   117 function Loona:deleteprofile(p, lang)
   118 	p = self.config.contentdir .. "/" .. p .. "_" .. (lang or self.lang)
   119 	for e in util.readdir(p) do
   120  		local success, msg = remove(p .. "/" .. e)
   121 		assert(success, self:dbmsg("Error removing entry in profile", msg))
   122 	end
   123 	return remove(p)
   124 end
   125 
   126 
   127 function Loona:copyprofile(dstprof, srcprof, dstlang, srclang)
   128 	local contentdir = self.config.contentdir
   129 	local src = ("%s/%s_%s"):format(contentdir,
   130 		srcprof or self.profile, srclang or self.lang)
   131 	local dst = ("%s/%s_%s"):format(contentdir,
   132 		dstprof or self.profile, dstlang or self.lang)
   133 	assert(src ~= dst, self:dbmsg("Attempt to copy profile over itself"))
   134 	assert(posix.stat(src, "mode") == "directory",
   135 		self:dbmsg("Source profile not a directory", src))
   136 	local success, msg = posix.mkdir(dst)
   137 	assert(success, self:dbmsg("Error creating profile directory " .. dst, msg))
   138 	for e in util.readdir(src) do
   139 		local ext = e:match("^[^.].*%.([^.]*)$")
   140 		if ext ~= "LOCK" then
   141 			local f = src .. "/" .. e
   142 			if posix.stat(f, "mode") == "file" then
   143 				success, msg = lib.copyfile(f, dst .. "/" .. e)
   144 				assert(success, self:dbmsg("Error copying file", msg))
   145 			end
   146 		end
   147 	end
   148 	-- create "current" symlink if none exists for new profile/language
   149 	if not posix.readlink(contentdir .. "/current_" .. dstlang) then
   150 		self:makecurrent(dstprof, dstlang)
   151 	end
   152 end
   153 
   154 
   155 function Loona:makecurrent(prof, lang)
   156 	prof = prof or self.profile
   157 	lang = lang or self.lang
   158 	local contentdir = self.config.contentdir
   159 	local newpath = ("%s/current_%s"):format(contentdir, lang)
   160 	local tmppath = newpath .. "." .. self.session.name
   161 	local success, msg = posix.symlink(prof .. "_" .. lang, tmppath)
   162 	assert(success, self:dbmsg("Cannot create symlink", msg))
   163 	success, msg = rename(tmppath, newpath)
   164 	assert(success, self:dbmsg("Cannot put symlink in place", msg))
   165 	return true
   166 end
   167 
   168 
   169 function Loona:publishprofile(profile, lang)
   170 	lang = lang or self.lang
   171 	local contentdir = self.config.contentdir
   172 	
   173 	-- Get languages for the current profile
   174 	
   175 	local plangs = { }
   176 	local lmatch = "^" .. self.profile .. "_(%w+)$"
   177 	for e in util.readdir(self.config.contentdir) do
   178 		local l = e:match(lmatch)
   179 		if l then
   180 			table.insert(plangs, l)
   181 		end
   182 	end
   183 	
   184 	-- For all languages, update "current" symlink
   185 	
   186 	for _, lang in ipairs(plangs) do
   187 		self:makecurrent(profile, lang)
   188 	end
   189 	
   190 	-- These arguments are overwritten globally and need to get restored
   191 	
   192 	local save_args = { self.args.lang, self.args.profile, self.args.session }
   193 	
   194 	-- For all languages, unroll site to static HTML
   195 	
   196 	for _, lang in ipairs(plangs) do
   197 		local ext = (#plangs == 1 and ".html") or (".html." .. lang)
   198 		self:recursesections(self.sections, function(self, s, e, path)
   199 			path = path and path .. "/" .. e.name or e.name
   200 			if not e.notvisible then
   201 				Loona:dumphtml { requestpath = path, requestlang = lang,
   202 					htmlext = ext, insecure = true }
   203 			end
   204 			return path
   205 		end)
   206 	end
   207 	
   208 	-- Restore arguments
   209 	
   210 	self.args.lang, self.args.profile, self.args.session = unpack(save_args)
   211 	
   212 	-- Update file cache
   213 
   214 	local htdocs = self.config.htdocsdir
   215 	local cache = self.config.htmlcachedir
   216 
   217 	for e in util.readdir(cache) do
   218 		local f = e:match("^.*%.html%.?(%w*)$")
   219 		if f and f ~= "tmp" then
   220 			local success, msg = remove(htdocs .. "/" .. e)
   221 			success, msg = remove(cache .. "/" .. e)
   222  			assert(success,
   223  				self:dbmsg("Could not purge cached HTML file", msg))
   224 		end
   225 	end
   226 	
   227 	for e in util.readdir(cache) do
   228 		local f = e:match("^(.*%.html%.?%w*)%.tmp$")
   229 		if f then
   230 			local success, msg = rename(cache .. "/" .. e, cache .. "/" .. f)
   231 			assert(success,
   232 				self:dbmsg("Could not update cached HTML file", msg))
   233 			success, msg = rename(htdocs .. "/" .. e, htdocs .. "/" .. f)
   234 			assert(success,
   235 				self:dbmsg("Could not update cached HTML file", msg))
   236 		end
   237 	end
   238 end
   239 
   240 
   241 function Loona:recursesections(s, func, ...)
   242 	for _, e in ipairs(s) do
   243 		local udata = { func(self, s, e, unpack(arg)) }
   244 		if e.subs then
   245 			self:recursesections(e.subs, func, unpack(udata))
   246 		end
   247 	end
   248 end
   249 
   250 
   251 function Loona:indexsections()
   252 	self:recursesections(self.sections, function(self, s, e)
   253 		e.notvalid = (not self.secure and e.secure) or 
   254 			(not self.authuser and e.secret) or nil
   255 		e.notvisible = e.notvalid or not self.authuser and e.hidden or nil
   256 		s[e.name] = e
   257 	end)
   258 end
   259 
   260 
   261 --	Decompose section path into a stack of sections, returning only up to
   262 --	the last valid element in the path. additionally returns the table of
   263 --	the last section path element (or the default section)
   264 
   265 function Loona:getsection(path)
   266 	local default = not self.authuser and self.config.defname
   267 	local tab = { { entries = self.sections, name = default } }
   268 	local ss = self.sections
   269 	local sectionpath
   270 	(path or ""):gsub("(%w+)/?", function(a)
   271 		if ss then
   272 			local s = ss[a]
   273 			if s and not s.notvalid then
   274 				sectionpath = s
   275 				tab[#tab].name = a
   276 				ss = s.subs
   277 				if ss then
   278 					table.insert(tab, { entries = ss })
   279 				end
   280 			else
   281 				ss = nil -- stop.
   282 			end
   283 		end
   284 	end)
   285 	if not self.section and not sectionpath then
   286 		sectionpath = self.sections[default]
   287 		if sectionpath then
   288 			table.insert(tab, { entries = sectionpath.subs })
   289 		end
   290 	end
   291 	return tab, sectionpath
   292 end
   293 
   294 
   295 function Loona:getpath(delimiter, maxdepth)
   296 	local t = { }
   297 	local d = 0
   298 	maxdepth = maxdepth or #self.submenus
   299 	for _, menu in ipairs(self.submenus) do
   300 		if menu.name then
   301 			table.insert(t, menu.name)
   302 		end
   303 		d = d + 1
   304 		if d == maxdepth then
   305 			break
   306 		end
   307 	end
   308 	return table.concat(t, delimiter or "/")
   309 end
   310 
   311 
   312 function Loona:deletesection(fname, all_bodies)
   313 	local fullname = self.contentdir .. "/" .. fname
   314 	local success, msg = remove(fullname)
   315 	if all_bodies then
   316 		local pat = "^" .. 
   317 			fname:gsub("%^%$%(%)%%%.%[%]%*%+%-%?", "%%%1") .. "%..*$"
   318 		for e in util.readdir(self.contentdir) do
   319 			if e:match(pat) then
   320 				remove(self.contentdir .. "/" .. e)
   321 			end
   322 		end
   323 	end
   324 	return success, msg
   325 end
   326 
   327 
   328 function Loona:addpath(path, e)
   329 	local tab = self.sections
   330 	path:gsub("(%w+)/?", function(a)
   331 		if tab then
   332 			local s = tab[a]
   333 			if s then
   334 				if not s.subs then
   335 					s.subs = { }
   336 				end
   337 				tab = s.subs
   338 			else
   339 				table.insert(tab, e)
   340 				tab[a] = e
   341  				tab = nil -- stop
   342 			end
   343 		end
   344 	end)
   345 	return e
   346 end
   347 
   348 
   349 local function lookupname(tab, val)
   350 	for i, v in ipairs(tab) do
   351 		if v.name == val then
   352 			return i
   353 		end
   354 	end
   355 end
   356 
   357 
   358 function Loona:rmpath(path)
   359 	local parent
   360 	local tab = self.sections
   361 	path:gsub("(%w+)/?", function(a)
   362 		if tab then
   363 			local idx = lookupname(tab, a)
   364 			if idx then
   365 				if tab[idx].subs then
   366 					parent = tab[idx]
   367 					tab = tab[idx].subs
   368 				else
   369 					table.remove(tab, idx)
   370 					tab[a] = nil
   371 					if #tab == 0 and parent then
   372 						parent.subs = nil
   373 					end
   374 					tab = nil
   375 				end
   376 			end
   377 		end
   378 	end)
   379 end
   380 
   381 
   382 function Loona:checkpath(path)
   383 	if path ~= "index" then -- "index" is reserved
   384 		local res, idx
   385 		local tab = self.sections
   386 		path:gsub("(%w+)/?", function(a)
   387 			if tab then
   388 				local i = lookupname(tab, a)
   389 				if i then
   390 					res, idx = tab, i
   391 					tab = tab[i].subs
   392 				else
   393 					res, idx = nil, nil
   394 				end
   395 			end
   396 		end)
   397 		return res, idx
   398 	end
   399 end
   400 
   401 
   402 function Loona:title()
   403 	return self.section and (self.section.title or self.section.label or
   404 		self.section.name) or ""
   405 end
   406 
   407 
   408 --	Run a site function snippet, with full error recovery
   409 --	(also recovers from errors in error handling function)
   410 
   411 function Loona:dosnippet(func, errfunc)
   412 	local ret = { lib.catch(func) }
   413 	if ret[1] == 0 or (errfunc and lib.catch(errfunc) == 0) then
   414 		return unpack(ret)
   415 	end
   416 	self:out("<h2>Error</h2>")
   417 	self:out("<h3>" .. self:encodeform(ret[2] or "") .. "</h3>")
   418 	if self.authuser then
   419 		if type(ret[3]) == "string" then
   420 			self:out("<p>" .. self:encodeform(ret[3]) .. "</p>")
   421 		end
   422 		if ret[4] then
   423 			self:out("<pre>" .. self:encodeform(ret[4]) .. "</pre>")
   424 		end
   425 	end
   426 end	
   427 
   428 
   429 function Loona:lockfile(file)
   430 	return not self.session and true or 
   431 		posix.symlink(self.session.filename, file .. ".LOCK")
   432 end
   433 
   434 
   435 function Loona:unlockfile(file)
   436 	return not self.session and true or remove(file .. ".LOCK")
   437 end
   438 
   439 
   440 function Loona:saveindex()
   441 	local tempname = self.indexfname .. "." .. self.session.name
   442 	local f, msg = open(tempname, "wb")
   443 	assert(f, self:dbmsg("Error opening section file for writing", msg))
   444 	lib.dump(self.sections, function(...)
   445 		f:write(unpack(arg))
   446 	end)
   447 	f:close()
   448 	local success, msg = rename(tempname, self.indexfname)
   449 	assert(success, self:dbmsg("Error renaming section file", msg))
   450 end
   451 
   452 
   453 function Loona:savebody(fname, content)
   454 	fname = self.contentdir .. "/" .. fname
   455 	local f, msg = open(fname, "wb")
   456 	assert(f, self:dbmsg("Could not open file for writing", msg))
   457 	f:write(content or "")
   458 	f:close()
   459 end
   460 
   461 
   462 function Loona:runboxed(func, envitems, ...)
   463 	local fenv = {
   464  		arg = arg,
   465  		loona = self
   466  	}
   467  	if envitems then
   468 	 	for k, v in pairs(envitems) do
   469  			fenv[k] = v
   470  		end
   471  	end
   472  	setmetatable(fenv, { __index = boxed_G }) -- boxed global environment
   473 	setfenv(func, fenv)
   474 	return func()
   475 end
   476 
   477 
   478 function Loona:include(fname, ...)
   479 	assert(not fname:match("%W"), self:dbmsg("Invalid include name", fname))
   480 	local fname2 = ("%s/%s.lua"):format(self.config.extdir, fname)
   481 	local f, msg = open(fname2)
   482 	assert(f, self:dbmsg("Cannot open file", msg))
   483 	local parsed, msg = self:loadhtml(f, "loona:out", fname2)
   484 	assert(parsed, self:dbmsg("Syntax error", msg))
   485 	return self:runboxed(parsed, nil, unpack(arg))
   486 end
   487 
   488 
   489 --	produce link target (simple)
   490 
   491 function Loona:shref(section, arg)
   492 	local args2 = { } -- propagated or new arguments
   493 	for _, a in ipairs(arg) do
   494 		local key, val = a:match("^(%w+)=(.*)$")
   495 		if key and val then -- "arg=val" sets/overrides argument
   496 			table.insert(args2, { name = key, value = val })
   497 		elseif self.args[a] then -- just "arg" propagates argument
   498 			table.insert(args2, { name = a, value = self.args[a] })
   499 		end
   500 	end
   501 	local doc = self:getdocname(section, #args2 > 0)
   502 	local url, anch = doc:match("^(.+)(#.+)$")
   503 	local notfirst = doc:match("%?")
   504 	local href = { anch and url or doc }
   505 	for i, arg in ipairs(args2) do
   506 		if i > 1 or notfirst then
   507 			table.insert(href, "&amp;")
   508 		else
   509 			table.insert(href, "?")
   510 		end
   511 		table.insert(href, arg.name .. "=" .. http.encodeurl(arg.value))
   512 	end
   513 	if anch then
   514 		insert(href, anch)
   515 	end
   516 	return table.concat(href)
   517 end
   518 
   519 
   520 --	produce link target, implicit propagation of lang, profile, session
   521 
   522 function Loona:href(section, ...)
   523 	if self.session then
   524 		table.insert(arg, 1, "profile")
   525 		table.insert(arg, 1, "session")
   526 	end
   527 	if self.explicitlang then
   528 		table.insert(arg, 1, "lang")
   529 	end
   530 	return self:shref(section, arg)
   531 end
   532 
   533 
   534 function Loona:ilink(target, text, extra)
   535 	return ('<a href="%s"%s>%s</a>'):format(target, extra or "", text)
   536 end
   537 
   538 
   539 --	internal link, implicit propagation of lang, profile, session
   540 
   541 function Loona:link(section, text, ...)
   542 	return self:ilink(self:href(section, unpack(arg)), text or section,
   543 	' class="intlink"')
   544 end
   545 
   546 
   547 --	external link (opens in a new window), no argument propagation
   548 
   549 function Loona:elink(target, text)
   550 	return self:ilink(target, text or target, self.config.extlinkextra)
   551 end
   552 
   553 
   554 --	plain link, no implicit argument propagation
   555 
   556 function Loona:plink(section, text, ...)
   557 	return self:ilink(self:shref(section, arg), text or section)
   558 end
   559 
   560 
   561 --	user interface link, implicit propagation of lang, profile, session
   562 
   563 function Loona:uilink(section, text, ...)
   564 	return self:ilink(self:href(section, unpack(arg)), text or section,
   565 	' class="uilink"')
   566 end
   567 
   568 
   569 --	produce a hidden input value in forms
   570 
   571 function Loona:hidden(name, value)
   572 	return not value and "" or
   573 		('<input type="hidden" name="%s" value="%s" />'):format(name, value)
   574 end
   575 
   576 
   577 function Loona:scanprofiles(func)
   578 	local tab = { }
   579 	local dir = self.config.contentdir
   580 	for f in util.readdir(dir) do
   581 		if posix.lstat(dir .. "/" .. f, "mode") == "directory" then
   582 			f = func(f)
   583 			if f then
   584 				table.insert(tab, f)
   585 			end
   586 		end
   587 	end
   588 	table.sort(tab)
   589 	for _, v in ipairs(tab) do
   590 		tab[v] = v	
   591 	end
   592 	return tab
   593 end
   594 
   595 
   596 function Loona:getprofiles(lang)
   597 	lang = lang or self.lang
   598 	return self:scanprofiles(function(f)
   599 		return f:match("^(%w+)_" .. lang .. "$")
   600 	end)
   601 end
   602 
   603 
   604 function Loona:getlanguages(prof)
   605 	prof = prof or self.profile
   606 	return self:scanprofiles(function(f)
   607 		return f:match("^" .. prof .. "_(%l%l)$")
   608 	end)
   609 end
   610 
   611 
   612 --	Functions to produce a navigation menu
   613 
   614 local newent = { name = "new", label = "[+]", action="actionnew=true" }
   615 
   616 function Loona:rmenu(level, render, path, addnew, recurse)
   617 	local sub = (addnew and level == #self.submenus + 1) and
   618 		{ name = "new", entries = { }} or self.submenus[level]
   619  	if sub and sub.entries then
   620 		local visible = { }
   621 		for _, e in ipairs(sub.entries) do
   622 			if not e.notvisible then
   623 				table.insert(visible, e)
   624 			end
   625 		end
   626 		if addnew then
   627 			table.insert(visible, newent)
   628 		end
   629 		local numvis = #visible
   630 		if numvis > 0 then
   631 			render.listbegin(self, level, numvis, path)
   632 			for idx, e in ipairs(visible) do
   633 				local label = self:encodeform(e.label or e.name)
   634 				local newpath = path and path .. "/" .. e.name or e.name
   635 				local active = (e.name == sub.name)
   636 				render.itembegin(self, level, idx)
   637 				render.link(self, level, newpath, label, active, e.action)
   638 				if recurse and active then
   639 					self:rmenu(level + 1, render, newpath, addnew, recurse)
   640 				end
   641 				render.itemend(self)
   642 			end
   643 			render.listend(self)
   644 		end
   645 	end
   646 end
   647 
   648 
   649 function Loona:menu(level, recurse, render)
   650 	level = level or 1
   651 	render = render or { }
   652 	render.link = render.link or 
   653 		function(self, level, path, label, active, ...)
   654 			self:out(('<a %shref="%s">%s</a>\n'):format(active and 
   655 				'class="active" ' or "", self:href(path, unpack(arg)), label))
   656 		end
   657 	render.listbegin = render.listbegin or
   658 		function(self, level) -- , numvis, path
   659 			self:out('<ul id="menulevel' .. level .. '">\n')
   660 		end
   661 	render.listend = render.listend or
   662 		function(self)
   663 			self:out('</ul>\n')
   664 		end
   665 	render.itembegin = render.itembegin or
   666 		function(self) -- , level, idx
   667 			self:out('<li>\n')
   668 		end
   669 	render.itemend = render.itemend or
   670 		function(self)
   671 			self:out('</li>\n')
   672 		end
   673 	recurse = recurse == nil and true or recurse
   674 	local path = level > 1 and self:getpath("/", level - 1) or nil
   675 	local addnew = self.authuser and not self.ispubprofile
   676 	self:rmenu(level, render, path, addnew, recurse)
   677 end
   678 
   679 
   680 function Loona:loadcontent(fname)
   681 	if fname then
   682 		local f = open(self.contentdir .. "/" .. fname)
   683 		local c = f:read("*a")
   684 		f:close()
   685 		return c
   686 	end
   687 	return ""
   688 end
   689 
   690 
   691 function Loona:loadmarkup(fname)
   692 	return (fname and fname ~= "") and
   693 		self:domarkup(self:loadcontent(fname)) or ""
   694 end
   695 
   696 
   697 function Loona:editable(editkey, fname, savename)
   698 	
   699 	local contentdir = self.contentdir
   700 	local edit, show, hidden, extramsg, changed
   701 	
   702 	if self.authuser then
   703 		
   704 		local hiddenvars = table.concat( {
   705 			self:hidden("lang", self.args.lang),
   706 			self:hidden("profile", self.profile),
   707 			self:hidden("session", self.session.id),
   708 			self:hidden("editkey", editkey) }, " ")
   709 	
   710 		local lockfname = fname and (contentdir .. "/" .. fname)
   711 		
   712 		if self.useralert and editkey == self.args.editkey then
   713 			
   714 			--	display user alert/request/confirmation
   715 			
   716 			hidden = true
   717 			self:out([[
   718 			<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   719 				<fieldset>
   720 					<legend>]] .. self.useralert.text ..[[</legend>
   721 					]] .. (self.useralert.confirm or "") .. [[
   722 					]] .. (self.useralert.returnto or "") .. [[
   723 					<input type="submit" name="actioncancel" value="]] .. self.locale.CANCEL  ..[[" />
   724 					]] .. hiddenvars .. [[
   725 				</fieldset>
   726 			</form>
   727 			]])
   728 			
   729 		elseif self.args.actionnew and editkey == "main" then
   730 			
   731 			--	form for creating a new section
   732 			
   733 			hidden = true
   734 			if self.ispubprofile then
   735 				self:out([[<h2><span class="warn">]] .. self.locale.WARNING_YOU_ARE_IN_PUBLISHED_PROFILE ..[[</span></h2>]])
   736 			end
   737 			self:out([[
   738 			<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   739 				<fieldset>
   740 					<legend>
   741 						]] .. self.locale.CREATE_NEW_SECTION_UNDER .. " " .. self.sectionpath .. [[
   742 					</legend>
   743 					<table>
   744 						<tr>
   745 							<td align="right">
   746 								]] .. self.locale.PATHNAME .. [[
   747 							</td>
   748 							<td>
   749 								<input size="30" maxlength="30" name="editname" />
   750 							</td>
   751 						</tr>
   752 						<tr>
   753 							<td align="right">
   754 								]] .. self.locale.MENULABEL .. [[
   755 							</td>
   756 							<td>
   757 								<input size="30" maxlength="50" name="editlabel" />
   758 							</td>
   759 						</tr>
   760 						<tr>
   761 							<td align="right">
   762 								]] .. self.locale.WINDOWTITLE .. [[
   763 							</td>
   764 							<td>
   765 								<input size="30" maxlength="50" name="edittitle" />
   766 							</td>
   767 						</tr>
   768 						<tr>
   769 							<td align="right">
   770 								]] .. self.locale.INVISIBLE .. [[
   771 							</td>
   772 							<td>
   773 								<input type="checkbox" name="editvisibility" />
   774 							</td>
   775 						</tr>
   776 						<tr>
   777 							<td align="right">
   778 								]] .. self.locale.SECRET .. [[
   779 							</td>
   780 							<td>
   781 								<input type="checkbox" name="editsecrecy" />
   782 							</td>
   783 						</tr>
   784 						<tr>
   785 							<td align="right">
   786 								]] .. self.locale.SECURE_CONNECTION .. [[
   787 							</td>
   788 							<td>
   789 								<input type="checkbox" name="editsecure" />
   790 							</td>
   791 						</tr>
   792 						<tr>
   793 							<td align="right">
   794 								]] .. self.locale.REDIRECT .. [[
   795 							</td>
   796 							<td>
   797 								<input size="30" maxlength="50" name="editredirect" />
   798 							</td>
   799 						</tr>
   800 					</table>					
   801 					<input type="submit" name="actioncreate" value="]] .. self.locale.CREATE .. [[" />
   802 					]] .. hiddenvars .. [[
   803 				</fieldset>
   804 			</form>
   805 			<hr />
   806 			]])
   807 		
   808 		elseif self.args.actioneditprops and editkey == "main" then
   809 			hidden = true
   810 			if self.ispubprofile then
   811 				self:out([[<h2><span class="warn">]] .. self.locale.WARNING_YOU_ARE_IN_PUBLISHED_PROFILE ..[[</span></h2>]])
   812 			end
   813 			self:out([[
   814 			<form action="]] ..self.document .. [[" method="post" accept-charset="utf-8">
   815 				<fieldset>
   816 					<legend>
   817 						]] .. self.locale.MODIFY_PROPERTIES_OF_SECTION .. " " .. self.sectionpath .. [[
   818 					</legend>
   819 					<table>
   820 						<tr>
   821 							<td align="right">
   822 								]] .. self.locale.MENULABEL .. [[
   823 							</td>
   824 							<td>
   825 								<input size="30" maxlength="50" name="editlabel" value="]] .. (self.section.label or "") .. [[" />
   826 							</td>
   827 						</tr>
   828 						<tr>
   829 							<td align="right">
   830 								]] .. self.locale.WINDOWTITLE .. [[
   831 							</td>
   832 							<td>
   833 								<input size="30" maxlength="50" name="edittitle" value="]] .. (self.section.title or "") .. [[" />
   834 							</td>
   835 						</tr>
   836 						<tr>
   837 							<td align="right">
   838 								]] .. self.locale.INVISIBLE .. [[
   839 							</td>
   840 							<td>
   841 								<input type="checkbox" name="editvisibility" ]] .. (self.section.hidden and 'checked="checked"' or "") .. [[/>
   842 							</td>
   843 						</tr>
   844 						<tr>
   845 							<td align="right">
   846 								]] .. self.locale.SECRET .. [[
   847 							</td>
   848 							<td>
   849 								<input type="checkbox" name="editsecrecy" ]] .. (self.section.secret and 'checked="checked"' or "") .. [[/>
   850 							</td>
   851 						</tr>
   852 						<tr>
   853 							<td align="right">
   854 								]] .. self.locale.SECURE_CONNECTION .. [[
   855 							</td>
   856 							<td>
   857 								<input type="checkbox" name="editsecure" ]] .. (self.section.secure and 'checked="checked"' or "") .. [[/>
   858 							</td>
   859 						</tr>
   860 						<tr>
   861 							<td align="right">
   862 								]] .. self.locale.REDIRECT .. [[
   863 							</td>
   864 							<td>
   865 								<input size="30" maxlength="50" name="editredirect" value="]] .. (self.section.redirect or "") .. [[" />
   866 							</td>
   867 						</tr>
   868 					</table>
   869 					<input type="submit" name="actionsaveprops" value="]] .. self.locale.SAVE .. [[" />
   870 					<input type="submit" name="actioncancel" value="]] .. self.locale.CANCEL .. [[" />
   871 					]] .. hiddenvars .. [[
   872 				</fieldset>
   873 			</form>
   874 			]])
   875 		
   876 		elseif (self.args.actioneditprofiles or
   877 			self.args.actioncreateprofile or 
   878 			self.args.actionchangeprofile or 
   879 			self.args.actionchangelanguage or
   880 			self.args.actionpublishprofile) and editkey == "main" then
   881 			hidden = true
   882 			self:out([[
   883 			<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   884 				<fieldset>
   885 					<legend>
   886 						]] .. self.locale.CHANGEPROFILE .. [[
   887 					</legend>
   888 					<select name="changeprofile" size="1">]])
   889 						for _, val in ipairs(self:getprofiles()) do
   890 							self:out('<option' .. (val == self.profile and ' selected="selected"' or '') .. '>')
   891 							self:out(val)
   892 							self:out('</option>')
   893 						end
   894 					self:out([[
   895 					</select>							
   896 					<input type="submit" name="actionchangeprofile" value="]] .. self.locale.CHANGE ..[[" />
   897 					]] .. hiddenvars .. [[
   898 				</fieldset>
   899 			</form>
   900 			<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   901 				<fieldset>
   902 					<legend>
   903 						]] .. self.locale.CHANGELANGUAGE .. [[
   904 					</legend>
   905 					<select name="changelanguage" size="1">]])
   906 						for _, val in ipairs(self:getlanguages()) do
   907 							self:out('<option' .. (val == self.lang and ' selected="selected"' or '') .. '>')
   908 							self:out(val)
   909 							self:out('</option>')
   910 						end
   911 					self:out([[
   912 					</select>							
   913 					<input type="submit" name="actionchangelanguage" value="]] .. self.locale.CHANGE ..[[" />
   914 					]] .. hiddenvars .. [[
   915 				</fieldset>
   916 			</form>
   917 			<form action="]] .. self.document ..[[" method="post" accept-charset="utf-8">
   918 				<fieldset>
   919 					<legend>
   920 						]] .. self.locale.CREATEPROFILE .. [[
   921 					</legend>
   922 					<input size="20" maxlength="20" name="createprofile" />
   923 					]] .. self.locale.LANGUAGE ..[[
   924 					<input size="2" maxlength="2" name="createlanguage" value="]] .. self.lang ..[[" />
   925 					<input type="submit" name="actioncreateprofile" value="]] .. self.locale.CREATE .. [[" />
   926 					]] .. hiddenvars .. [[
   927 				</fieldset>
   928 			</form>
   929 			]])
   930 			if not self.ispubprofile then
   931 				self:out([[
   932 				<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   933 					<fieldset>
   934 						<legend>
   935 							]] .. self.locale.PUBLISHPROFILE .. [[
   936 						</legend>
   937 						]] .. self:hidden("publishprofile", self.profile) .. [[
   938 						<input type="submit" name="actionpublishprofile" value="]] .. self.locale.PUBLISH .. [[" />
   939 						]] .. hiddenvars .. [[
   940 					</fieldset>
   941 				</form>
   942 				<form action="]] .. self.document .. [[" method="post" accept-charset="utf-8">
   943 					<fieldset>
   944 						<legend>
   945 							]] .. self.locale.DELETEPROFILE .. [[
   946 						</legend>
   947 						]] .. self:hidden("deleteprofile", self.profile) .. [[
   948 						<input type="submit" name="actiondeleteprofile" value="]] .. self.locale.DELETE .. [[" />
   949 						]] .. hiddenvars .. [[
   950 					</fieldset>
   951 				</form>
   952 				]])
   953 			end
   954 			
   955 		elseif self.args.actionedit and editkey == self.args.editkey then
   956 			if not self.section.redirect then
   957 				extramsg = self.ispubprofile and
   958 					self.locale.WARNING_YOU_ARE_IN_PUBLISHED_PROFILE
   959 				edit = self:loadcontent(fname):gsub("\194\160", "&nbsp;") -- TODO
   960 				changed = self.section and (self.section.revisiondate or self.section.creationdate)
   961 			end
   962 		
   963 		elseif self.args.actionpreview and editkey == self.args.editkey then
   964 			edit = self.args.editform
   965 			show = self:domarkup(edit:gsub("&nbsp;", "\194\160")) -- TODO
   966 		
   967 		elseif self.args.actionsave and editkey == self.args.editkey then
   968 			local c = self.args.editform
   969 			local dynamic
   970 			
   971 			if lockfname then
   972 				self:expire(contentdir, "[^.]%S+.LOCK")
   973 				if self:lockfile(lockfname) then
   974 					-- lock was expired, aquired a new one
   975 					extramsg = self.locale.SECTION_COULD_HAVE_CHANGED
   976 					edit = c
   977 				else
   978 					local tab = lib.source(lockfname .. ".LOCK")
   979 					if tab and tab.id == self.session.id then
   980 						-- lock already held and is mine - try to save:
   981 						local savec = c:gsub("&nbsp;", "\194\160") -- TODO
   982 						remove(contentdir .. "/" .. savename .. ".html")
   983 						self:savebody(savename, savec)
   984 						-- TODO: error handling
   985 						self:unlockfile(lockfname)
   986 						show, dynamic = self:domarkup(savec)
   987 						changed = time()
   988 					else
   989 						-- lock was expired and someone else has it now
   990 						extramsg = self.locale.SECTION_IN_USE
   991 						edit = c
   992 					end
   993 				end
   994 			else
   995 				-- new sidefile
   996 				local savec = c:gsub("&nbsp;", "\194\160") -- TODO
   997 				self:savebody(savename, savec)
   998 				-- TODO: error handling
   999 				show, dynamic = self:domarkup(savec)
  1000 			end
  1001 			
  1002 			-- mark dynamic text bodies
  1003 			if not self.section.dynamic then
  1004 				self.section.dynamic = { }
  1005 			end
  1006 			self.section.dynamic[editkey] = dynamic
  1007 			local n = 0
  1008 			for _ in pairs(self.section.dynamic) do
  1009 				n = n + 1
  1010 			end
  1011 			if n == 0 then
  1012 				self.section.dynamic = nil
  1013 			end
  1014 			
  1015 			self:saveindex()
  1016 			
  1017 		elseif self.args.actioncancel and editkey == self.args.editkey then
  1018 			if lockfname then
  1019 				self:unlockfile(lockfname) -- remove lock
  1020 			end
  1021 		end
  1022 		
  1023 		if editkey == "main" and self.section and self.section.redirect then
  1024 			self:out('<h2>' .. self.locale.SECTION_IS_REDIRECT ..'</h2>')
  1025 			self:out(self:link(self.section.redirect))
  1026 			self:out('<hr />')
  1027 		end
  1028 	
  1029 		if edit then
  1030 			self:expire(contentdir, "[^.]%S+.LOCK")
  1031 			if fname and not self:lockfile(contentdir .. "/" .. fname) then
  1032 				local tab = lib.source(contentdir .. "/" .. fname .. ".LOCK")
  1033 				if tab and tab.id ~= self.session.id then
  1034 					extramsg = self.locale.SECTION_IN_USE
  1035 				end
  1036 				-- else already owner
  1037 			end
  1038 			if extramsg then
  1039 				self:out('<h2><span class="warn">' .. extramsg .. '</span></h2>')
  1040 			end
  1041 			self:out([[
  1042 			<form action="]] .. self.document .. [[#preview" method="post" accept-charset="utf-8">
  1043 				<fieldset>
  1044 					<legend>
  1045 						]] .. self.locale.EDIT_SECTION .. [[
  1046 					</legend>
  1047 					<textarea cols="80" rows="25" name="editform">
  1048 ]] .. self:encodeform(edit) .. [[</textarea>
  1049 					<br />
  1050 					<input type="submit" name="actionsave" value="]] .. self.locale.SAVE .. [[" />
  1051 					<input type="submit" name="actionpreview" value="]] .. self.locale.PREVIEW .. [[" />
  1052 					<input type="submit" name="actioncancel" value="]] .. self.locale.CANCEL .. [[" />
  1053 					]] .. hiddenvars .. [[
  1054 				</fieldset>
  1055 			</form>
  1056 			]])
  1057 		end
  1058 	end	
  1059 	
  1060 	if not hidden then
  1061 		self:dosnippet(function()
  1062 			if not show then
  1063 				show = self:loadmarkup(fname)
  1064 				changed = self.section and (self.section.revisiondate or self.section.creationdate)
  1065 			end
  1066 			local parsed, msg = self:loadhtml(show, "loona:out", "<parsed html>")
  1067 			assert(parsed, msg and "Syntax error : " .. msg)
  1068 			self:runboxed(parsed)
  1069 		end)
  1070 	end
  1071 	
  1072 	if self.authuser then
  1073 		self:out([[
  1074 		<hr />
  1075 		<div class="edit">]])
  1076 			if editkey == "main" then
  1077 				self:out([[
  1078 				<a name="preview"></a>
  1079 				]] .. self.authuser .. [[ : 
  1080 				]] .. self:uilink(self.sectionpath, "[" .. self.locale.PROFILE .. "]", "actioneditprofiles=true", "editkey=" .. editkey) .. [[ :
  1081 				]] .. self.profile .. "_" .. self.lang)
  1082 				if self.ispubprofile then
  1083 					self:out([[
  1084 						<span class="warn">[]] .. self.locale.PUBLIC .. [[]</span>]])
  1085 				end
  1086 				self:out(' : ' .. self.sectionpath .. ' ')
  1087 			end
  1088 			if self.section and not self.ispubprofile then
  1089 				self:out(self:uilink(self.sectionpath, "[" .. self.locale.EDIT .. "]", "actionedit=true", "editkey=" .. editkey) .. " ")
  1090 				if editkey == "main" then
  1091 					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.PROPERTIES .. "]", "actioneditprops=true", "editkey=" .. editkey) .. " ")
  1092 				end
  1093 				if fname == savename or not self.section.subs then
  1094 					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.DELETE .. "]", "actiondelete=true", "editkey=" .. editkey) .. " ")
  1095 				end
  1096 				if editkey == "main" then
  1097 					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.MOVEUP .. "]", "actionup=true", "editkey=" .. editkey) .. " ")
  1098 					self:out('- ' .. self:uilink(self.sectionpath, "[" .. self.locale.MOVEDOWN .. "]", "actiondown=true", "editkey=" .. editkey) .. " ")
  1099 				end
  1100 				if changed and editkey == "main" then
  1101 					self:out('- ' .. self.locale.CHANGED .. ': ' .. date("%d-%b-%Y %T", changed))
  1102 				end
  1103 			end
  1104 		self:out('</div>')
  1105 	end
  1106 
  1107 end
  1108 
  1109 
  1110 --	Get pathname of an existing content file that
  1111 --	the current path is determined by (or defaults to)
  1112 
  1113 function Loona:getsectionpath(bodyname, requestpath)
  1114 	local ext = (not bodyname or bodyname == "main") and "" or "." .. bodyname
  1115 	local t, path, section = { }
  1116 	for _, menu in ipairs(self.submenus) do
  1117 		if menu.entries and menu.entries[menu.name] then
  1118 			table.insert(t, menu.name)
  1119 			local fn = table.concat(t, "_")
  1120 			if posix.stat(self.contentdir .. "/" .. fn .. ext, 
  1121 				"mode") == "file" then
  1122 				path, section = fn, menu
  1123 			end
  1124 		end
  1125 	end
  1126 	return path, ext, section
  1127 end
  1128 
  1129 
  1130 function Loona:body(name)
  1131 	name = self:checkbodyname(name)
  1132 	local path, ext = self:getsectionpath(name)
  1133 	self:dosnippet(function()
  1134 		self:editable(name, path and path .. ext, self.sectionname .. ext)
  1135 	end)
  1136 end
  1137 
  1138 
  1139 function Loona:init()
  1140 	
  1141 	-- get list of languages, in order of preference
  1142 	-- TODO: respect quality parameter, not just order
  1143 	
  1144 	local l = self.requestlang or self.args.lang
  1145 	self.langs = { l and l:match("^%w+$") }
  1146 	local s = getenv("HTTP_ACCEPT_LANGUAGE")
  1147 	while s do
  1148 		local l, r = s:match("^([%w.=]+)[,;](.*)$")
  1149 		l = l or s
  1150 		s = r
  1151 		if l:match("^%w+$") then
  1152 			table.insert(self.langs, l)
  1153 		end
  1154 	end
  1155 	table.insert(self.langs, self.config.deflang)
  1156 	
  1157 	-- get list of possible profiles
  1158 	
  1159 	local profiles = { }
  1160 	for e in util.readdir(self.config.contentdir) do
  1161 		profiles[e] = e
  1162 	end
  1163 	
  1164 	-- get pubprofile
  1165 	
  1166 	for _, lang in ipairs(self.langs) do
  1167 		local p = posix.readlink(self.config.contentdir .. "/current_" .. lang)
  1168 		p = p and p:match("^(%w+)_" .. lang .. "$")
  1169 		if p then
  1170 			self.pubprofile = p
  1171 			break
  1172 		end
  1173 	end
  1174 	
  1175 	-- get profile
  1176 	
  1177 	local checkprofile =
  1178 		self.authuser and self.args.profile or self.pubprofile or "work"
  1179 	for _, lang in ipairs(self.langs) do
  1180 		if profiles[checkprofile .. "_" .. lang] then
  1181 			self.profile = checkprofile
  1182 			self.lang = lang
  1183 			break
  1184 		end
  1185 	end
  1186 	
  1187 	assert(self.profile and self.lang, "Invalid profile or language")
  1188 	
  1189 	
  1190 	self.ispubprofile = self.profile == self.pubprofile
  1191 	
  1192 	-- write back language and profile
  1193 	
  1194 	self.args.lang = (self.explicitlang or self.lang ~= self.config.deflang)
  1195 		and self.lang or nil
  1196 	self.args.profile = self.profile
  1197 	
  1198 	-- determine content directory pathname and section filename
  1199 	
  1200 	self.contentdir =
  1201 		("%s/%s_%s"):format(self.config.contentdir, self.profile, self.lang)
  1202  	self.indexfname = self.contentdir .. "/.sections"
  1203 	
  1204 	-- load sections
  1205 	
  1206  	self.sections = lib.source(self.indexfname)
  1207 	
  1208 	-- index sections, determine visibility in menu
  1209 	
  1210 	self:indexsections()
  1211 	
  1212 	-- decompose request path, produce a stack of sections
  1213 	
  1214 	self.submenus, self.section = self:getsection(self.requestpath)
  1215 
  1216 	-- handle redirects if not logged on
  1217 	
  1218 	if not self.authuser and self.section and self.section.redirect then
  1219 		self.submenus, self.section = self:getsection(self.section.redirect)
  1220 	end
  1221 			
  1222 	-- section path and document name (refined)
  1223 	
  1224 	self.sectionpath = self:getpath()
  1225 	self.sectionname = self:getpath("_")
  1226 
  1227 end
  1228 
  1229 
  1230 function Loona:handlechanges()
  1231 	
  1232 	local save
  1233 
  1234 	if self.args.editkey == "main" then
  1235 		
  1236 		-- In main editable section:
  1237 		
  1238 		if self.args.actioncreate then
  1239 			
  1240 			-- Create new section
  1241 			
  1242 			local editname = self.args.editname:lower()
  1243 			assert(not editname:match("%W"),
  1244 				self:dbmsg("Invalid section name", editname))
  1245 			if not (section and (section.subs or section)[editname]) then
  1246 				local newpath = (self.sectionpath and 
  1247 					(self.sectionpath .. "/")) .. editname
  1248 				local s = self:addpath(newpath, { name = editname,
  1249 					label = self.args.editlabel ~= "" and
  1250 						self.args.editlabel or nil,
  1251 					title = self.args.edittitle ~= "" and
  1252 						self.args.edittitle or nil,
  1253 					redirect = self.args.editredirect ~= "" and
  1254 						self.args.editredirect or nil,
  1255 					hidden = self.args.editvisibility and true,
  1256 					secret = self.args.editsecrecy and true,
  1257 					secure = self.args.editsecure and true,
  1258 					creator = self.authuser,
  1259 					creationdate = time() })
  1260 				save = true
  1261 			end
  1262 		
  1263 		elseif self.args.actionsave then
  1264 			
  1265 			-- Save section
  1266 			
  1267 			self.section.revisiondate = time()
  1268 			self.section.revisioner = self.authuser
  1269 			save = true
  1270  		
  1271 		elseif self.args.actionsaveprops then
  1272 			
  1273 			-- Save properties
  1274 			
  1275 			self.section.hidden = self.args.editvisibility and true
  1276 			self.section.secret = self.args.editsecrecy and true
  1277 			self.section.secure = self.args.editsecure and true
  1278 			self.section.label = self.args.editlabel ~= "" and
  1279 				self.args.editlabel or nil
  1280 			self.section.title = self.args.edittitle ~= "" and
  1281 				self.args.edittitle or nil
  1282 			self.section.redirect =
  1283 				self.args.editredirect ~= "" and self.args.editredirect or nil
  1284 			save = true
  1285 		
  1286 		elseif self.args.actionup then
  1287 			
  1288 			-- Move section up
  1289 			
  1290 			local t, i = self:checkpath(self.sectionpath)
  1291 			if t and i > 1 then
  1292 				if self.ispubprofile and not self.args.actionconfirm then
  1293 					useralert = {
  1294 						text = self.locale.ALERT_MOVE_IN_PUBLISHED_PROFILE,
  1295 						confirm =
  1296 							'<input type="submit" name="actionup" value="' ..
  1297 							self.locale.MOVE .. '" /> ' ..
  1298 							self:hidden("actionconfirm", "true")
  1299 					}
  1300 				else
  1301 					local item = table.remove(t, i)
  1302 					table.insert(t, i - 1, item)
  1303 					save = true
  1304 				end
  1305 			end
  1306 		
  1307 		elseif self.args.actiondown then
  1308 			
  1309 			-- Move section down
  1310 			
  1311 			local t, i = self:checkpath(self.sectionpath)
  1312 			if t and i < #t then
  1313 				if self.ispubprofile and not self.args.actionconfirm then
  1314 					useralert = {
  1315 						text = self.locale.ALERT_MOVE_IN_PUBLISHED_PROFILE,
  1316 						confirm =
  1317 							'<input type="submit" name="actiondown" value="' ..
  1318 							self.locale.MOVE .. '" /> ' ..
  1319 							self:hidden("actionconfirm", "true")
  1320 					}
  1321 				else
  1322 					local item = table.remove(t, i)
  1323 					table.insert(t, i + 1, item)
  1324 					save = true
  1325 				end
  1326 			end
  1327 		
  1328 		elseif self.args.actioncreateprofile and self.args.createprofile then
  1329 			
  1330 			-- Create profile
  1331 			
  1332 			local c = self.args.createprofile
  1333 			if c == "" then
  1334 				c = self.profile
  1335 			end
  1336 			c = self:checkprofilename(c:lower())
  1337 			local l = self:checklanguage((self.args.createlanguage or self.lang):lower())
  1338 			if c == self.profile and l == self.lang then
  1339 				useralert = { 
  1340 					text = self.locale.ALERT_CANNOT_COPY_PROFILE_TO_SELF,
  1341 					returnto = self:hidden("actioneditprofiles", "true")
  1342 				}
  1343 			else
  1344 				local profiles = self:getprofiles(l)
  1345 				local text
  1346 				if c == self.pubprofile then
  1347 					text = self.locale.ALERT_OVERWRITE_PUBLISHED_PROFILE
  1348 				elseif profiles[c] and l == self.lang then
  1349 					text = self.locale.ALERT_OVERWRITE_EXISTING_PROFILE
  1350 				end
  1351 				if text and not self.args.actionconfirm then
  1352 					useralert = {
  1353 						text = text,
  1354 						returnto = self:hidden("actioneditprofiles", "true"),
  1355 						confirm = '<input type="submit" ' ..
  1356 							'name="actioncreateprofile" value="' ..
  1357 							self.locale.OVERWRITE .. '" /> ' ..
  1358 							self:hidden("actionconfirm", "true") ..
  1359 							self:hidden("createlanguage", l) ..
  1360 							self:hidden("createprofile", c)
  1361 					}
  1362 				else
  1363 					if profiles[c] then
  1364 						self:deleteprofile(c, l)
  1365 					end
  1366 					self:copyprofile(c, self.profile, l, self.lang)
  1367 				end
  1368 			end
  1369 		
  1370 		elseif self.args.actiondeleteprofile and self.args.deleteprofile then
  1371 			
  1372 			-- Delete profile
  1373 			
  1374 			local c = self:checkprofilename(self.args.deleteprofile:lower())
  1375 			assert(c ~= self.pubprofile,
  1376 				self:dbmsg("Cannot delete published profile", c))
  1377 			if self.args.actionconfirm then
  1378 				self:deleteprofile(c)
  1379 				self.profile = nil
  1380 				self.args.profile = nil
  1381 				self:init()
  1382 				save = true
  1383 			else
  1384 				useralert = { 
  1385 					text = self.locale.ALERT_DELETE_PROFILE,
  1386 					returnto = self:hidden("actioneditprofiles", "true"),
  1387 					confirm = '<input type="submit" ' ..
  1388 						'name="actiondeleteprofile" value="' .. 
  1389 						self.locale.DELETE .. '" /> ' ..
  1390 						self:hidden("actionconfirm", "true") ..
  1391 						self:hidden("deleteprofile", c)
  1392 				}
  1393 			end
  1394 		
  1395 		elseif self.args.actionchangeprofile and self.args.changeprofile then
  1396 			
  1397 			-- Change profile
  1398 			
  1399 			local c = self:checkprofilename(self.args.changeprofile:lower())
  1400 			self.profile = c
  1401 			self.args.profile = c
  1402 			save = true
  1403 		
  1404 		elseif self.args.actionchangelanguage and self.args.changelanguage then
  1405 			
  1406 			-- Change language
  1407 			
  1408 			local l = self:checklanguage(self.args.changelanguage:lower())
  1409 			self.lang = l
  1410 			self.args.lang = l
  1411  			self.explicitlang = l
  1412 			save = true
  1413 		
  1414 		elseif self.args.actionpublishprofile and self.args.publishprofile then
  1415 			
  1416 			-- Publish profile
  1417 			
  1418 			local c = self:checkprofilename(self.args.publishprofile:lower())
  1419 			if c ~= self.publicprofile then
  1420 				if self.args.actionconfirm then
  1421 					self:publishprofile(c)
  1422 					save = true
  1423 				else
  1424 					useralert = {
  1425 						text = self.locale.ALERT_PUBLISH_PROFILE,
  1426 						returnto = self:hidden("actioneditprofiles", "true"),
  1427 						confirm = '<input type="submit" ' ..
  1428 							'name="actionpublishprofile" value="' ..
  1429 							self.locale.PUBLISH .. '" /> ' ..
  1430 							self:hidden("actionconfirm", "true") ..
  1431 							self:hidden("publishprofile", c)
  1432 					}
  1433 				end
  1434 			end
  1435 		end
  1436 		
  1437 	end
  1438 	
  1439 	if self.args.actiondelete then
  1440 		
  1441 		-- Delete section
  1442 		
  1443 		if not self.args.actionconfirm then
  1444 			useralert = {
  1445 				text = self.ispubprofile and
  1446 					self.locale.ALERT_DELETE_IN_PUBLISHED_PROFILE or
  1447 					self.locale.ALERT_DELETE_SECTION,
  1448 				confirm =
  1449 					'<input type="submit" name="actiondelete" value="' .. 
  1450 					self.locale.DELETE .. '" /> ' ..
  1451 					self:hidden("actionconfirm", "true")
  1452 			}
  1453 		else
  1454 			local key = self.args.editkey
  1455 			if key == "main" and not self.section.subs then
  1456 				self:deletesection(self.sectionname, true) -- all bodies
  1457 				self:rmpath(self.sectionpath) -- and node
  1458 			else
  1459 				local ext = (key == "main" and "") or "." .. key
  1460 				self:deletesection(self.sectionname .. ext) -- only text
  1461 				if self.section.dynamic then
  1462 					self.section.dynamic[key] = nil
  1463 					local n = 0
  1464 					for _ in pairs(self.section.dynamic) do
  1465 						n = n + 1
  1466 					end
  1467 					if n == 0 then
  1468 						self.section.dynamic = nil
  1469 					end
  1470 				end
  1471 			end
  1472 			save = true
  1473 		end
  1474 	end
  1475 		
  1476 	if save then
  1477 		self:saveindex()
  1478 		self:init()
  1479 	end
  1480 	
  1481 end
  1482 
  1483 
  1484 function Loona:encodeform(s)
  1485 	return http.encodeform(s)
  1486 end
  1487 
  1488 
  1489 function Loona:loadhtml(src, outfunc, chunkname)
  1490  	return luahtml.load(src, outfunc, chunkname)
  1491 end
  1492 
  1493 
  1494 function Loona:domarkup(s)
  1495 	return markup.load(s)
  1496 end
  1497 
  1498 
  1499 function Loona:expire(dir, pat, maxage)
  1500 	return util.expire(dir, pat, maxage or self.config.sessionmaxage)
  1501 end
  1502 
  1503 
  1504 function Loona:new(o)
  1505 
  1506 	local parsed, msg
  1507 	
  1508 	o = o or { }
  1509 	setmetatable(o, self)
  1510 	self.__index = self
  1511 -- 	o = atom.new(self, o)
  1512 	
  1513 	-- Buffer
  1514 	
  1515 	o.out = o.out or function(self, s)
  1516 		self.buf:out(s)
  1517 	end
  1518 	o.setheader = o.setheader or function(self, s)
  1519 		self.buf:setheader(s)
  1520 	end
  1521 	
  1522 	-- Get configuration
  1523 	
  1524 	o.config = o.config or lib.source(o.conffile or "../etc/config.lua") or { }
  1525 	o.config.defname = o.config.defname or "home"
  1526 	o.config.deflang = o.config.deflang or "en"
  1527 	o.config.sessionmaxage = o.config.sessionmaxage or 6000
  1528 	o.config.secureport = o.config.secureport or 443
  1529 	o.config.passwdfile =
  1530 		posix.abspath(o.config.passwdfile or "../etc/passwd.lua")
  1531 	o.config.sessiondir =
  1532 		posix.abspath(o.config.sessiondir or "../var/sessions")
  1533 	o.config.extdir = posix.abspath(o.config.extdir or "../extensions")
  1534 	o.config.contentdir = posix.abspath(o.config.contentdir or "../content")
  1535 	o.config.localedir = posix.abspath(o.config.localedir or "../locale")
  1536 	o.config.htdocsdir = posix.abspath(o.config.htdocsdir or "../htdocs")
  1537 	o.config.htmlcachedir = 
  1538 		posix.abspath(o.config.htmlcachedir or "../var/htmlcache")
  1539 	o.config.extlinkextra = o.config.extlinksamewindow and ' class="extlink"'
  1540 		or ' class="extlink" onclick="void(window.open(this.href, \'\', \'\')); return false;"'
  1541 	
  1542 	-- Create proxy for on-demand loading of locales
  1543 	
  1544 	o.locale = { }
  1545 	local locmt = { }
  1546 	locmt.__index = function(_, key)
  1547 		for _, l in ipairs(o.langs) do
  1548 			locmt.__locale = lib.source(o.config.localedir .. "/" .. l)
  1549 			if locmt.__locale then
  1550 				break
  1551 			end
  1552 		end
  1553 		locmt.__index = function(tab, key)
  1554 			return locmt.__locale[key] or key
  1555 		end
  1556 		return locmt.__locale[key] or key
  1557 	end
  1558 	setmetatable(o.locale, locmt)
  1559 	
  1560 	-- Get request, args, document, script name, request path
  1561 	
  1562 	o.request = o.request or Request:new()
  1563 	o.args = o.request:getargs()
  1564 	o.cgi_document = o.request:getdocument()
  1565  	
  1566  	o.scriptpath = o.scriptpath or o.cgi_document.Path
  1567 	o.requesthandler = o.requesthandler or o.cgi_document.Handler
  1568  	o.requestdocument = o.requestdocument or o.cgi_document.Name
  1569 	o.requestpath = o.requestpath or o.cgi_document.VirtualPath
  1570 	o.explicitlang = not o.requestlang and o.args.lang
  1571 	o.secure = not o.insecure and (o.request.Port == o.config.secureport)
  1572 
  1573 	-- Manage login and establish session
  1574 	
  1575 	if not o.nologin then
  1576 		local sid = o.args.session or o.request.UniqueID
  1577 		o.session = o.session or Session:new {
  1578 			id = sid,
  1579 			sessiondir = o.config.sessiondir,
  1580 			maxage = o.config.sessionmaxage
  1581 		}
  1582 		if o.args.login then
  1583 			-- write back session ID into request args:
  1584 			o.args.session = sid -- !
  1585 			if o.args.login == "false" then
  1586 				o.session:delete()
  1587 				o.session = nil
  1588 			elseif o.args.password then
  1589 				o.loginfailed = true
  1590 				local pwddb = lib.source(o.config.passwdfile)
  1591 				local pwdentry = pwddb[o.args.login]
  1592 				if pwdentry and pwdentry.password == o.args.password then
  1593 					o.session.data.authuser = pwdentry.username
  1594 					o.session.data.id = o.session.id
  1595 					o.loginfailed = nil
  1596 				end
  1597 			end
  1598 		end
  1599 		o.authuser = o.session and o.session.data.authuser
  1600 	end
  1601 
  1602 	if o.nologin or not o.authuser then
  1603 		o.authuser = nil
  1604 		o.session = nil
  1605 		o.args.session = nil
  1606 	end
  1607 
  1608 	-- Get lang, locale, profile, section
  1609 
  1610 	o:init()
  1611 	if o.authuser then
  1612 		o:handlechanges()
  1613 	else
  1614 		o.args.profile = nil
  1615 	end
  1616 	
  1617 	-- Current document
  1618 	
  1619 	o.document = o.requestdocument .. "/" .. o.sectionpath
  1620 	if o.authuser then
  1621 		o.getdocname = function(self, path)
  1622 			return self.requestdocument .. "/" .. (path or self.sectionpath)
  1623 		end
  1624 	else
  1625 		o.getdocname = function(self, path, haveargs)
  1626 			local dyn
  1627 			dyn, path = self:isdynamic(path or self.sectionpath)
  1628 			if dyn or haveargs then
  1629 				return self.requestdocument .. "/" .. path
  1630 			end
  1631 			path = path == self.config.defname and "index" or path
  1632 			return "/" .. path:gsub("/", "_") .. ".html"
  1633 		end
  1634 	end
  1635 	
  1636 	-- Save session state
  1637 	
  1638 	if o.session then
  1639 		o.session:save()
  1640 	end
  1641 	
  1642 	return o
  1643 end
  1644 
  1645 
  1646 function Loona:run(fname)
  1647 	self:indexdynamic()
  1648 	fname = fname or self.requesthandler
  1649 	local parsed, msg = self:loadhtml(open(fname), "loona:out", fname)
  1650 	assert(parsed, self:dbmsg("HTML/Lua parsing failed", msg))
  1651 	self:runboxed(parsed)
  1652 	return self
  1653 end
  1654 
  1655 
  1656 function Loona:indexdynamic()
  1657 	self:recursesections(self.sections, function(self, s, e, path, dynamic)
  1658 		path = path and path .. "_" .. e.name or e.name
  1659 		dynamic = dynamic or { }
  1660 		for k in pairs(e.dynamic or { }) do
  1661 			dynamic[k] = true
  1662 		end
  1663 		for k in pairs(dynamic) do
  1664 			local ext = (k == "main" and "") or "." .. k
  1665 			if posix.stat(self.contentdir .. "/" .. path .. ext,
  1666 				"mode") == "file" then
  1667 				dynamic[k] = e.dynamic and e.dynamic[k]
  1668 			end
  1669 		end
  1670 		local n = 0
  1671 		for k in pairs(dynamic) do
  1672 			n = n + 1
  1673 		end
  1674 		if n > 0 then
  1675 			e.dynamic = { }
  1676 			for k in pairs(dynamic) do
  1677 				e.dynamic[k] = true
  1678 			end
  1679 		else
  1680 			e.dynamic = nil
  1681 		end
  1682 		return path, dynamic
  1683 	end)
  1684 end
  1685 
  1686 
  1687 function Loona:isdynamic(path)
  1688 	path = path or self.sectionpath
  1689 	local t, i = self:checkpath(path)
  1690 	if t and t[i].redirect then
  1691 		path = t[i].redirect
  1692 		t, i = self:isdynamic(path) -- TODO: prohibit endless recursion
  1693 	end
  1694 	return t and t[i].dynamic, path
  1695 end
  1696 
  1697 
  1698 function Loona:dumphtml(o)
  1699 	local outbuf = { }
  1700 	o = o or { }
  1701 	o.nologin = true
  1702 	o.out = function(self, s) table.insert(outbuf, s) end
  1703 	o.setheader = function(self, s) end
  1704 	o = self:new(o):run()
  1705 	if not o:isdynamic() then
  1706 		local path = o.sectionname
  1707 		path = path == o.config.defname and "index" or path
  1708 		local srcname = o.config.htdocsdir .. "/" .. path .. o.htmlext
  1709 		local fh, msg = open(srcname .. ".tmp", "wb")
  1710 		assert(fh, self:dbmsg("Could not write cached HTML", msg))
  1711 		fh:write(unpack(outbuf))
  1712 		fh:close()
  1713 		local dstname = o.config.htmlcachedir .. "/" .. path .. o.htmlext
  1714 		local success, msg = posix.symlink(srcname, dstname .. ".tmp")
  1715 -- 		assert(success, self:dbmsg("Could not link to cached HTML", msg))
  1716 	end
  1717 end