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