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