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