cgi-bin/loona.lua
author Timm S. Mueller <tmueller@neoscientists.org>
Sat, 03 Mar 2007 03:29:10 +0100
changeset 115 6a530f325004
parent 104 a35dcfbe5f4d
child 119 bf7dddc5253e
permissions -rw-r--r--
Moving nodes up/down now requires confirmation in the published profile
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@68
     8
local tek = require "tek"
tmueller@68
     9
local cgi = require "tek.cgi"
tmueller@68
    10
local posix = require "tek.posix"
tmueller@0
    11
require "tek.cgi.request"
tmueller@0
    12
require "tek.cgi.request.args"
tmueller@0
    13
require "tek.cgi.session"
tmueller@0
    14
require "tek.web"
tmueller@0
    15
require "tek.web.markup"
tmueller@0
    16
require "tek.util"
tmueller@0
    17
tmueller@23
    18
tmueller@0
    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@68
    31
local table, string, assert, unpack, ipairs, pairs, type, require =
tmueller@68
    32
	table, string, assert, unpack, ipairs, pairs, type, require
tmueller@0
    33
local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv
tmueller@0
    34
local open, remove, rename, getenv, time =
tmueller@0
    35
	io.open, os.remove, os.rename, os.getenv, os.time
tmueller@0
    36
tmueller@68
    37
local sectionfname, langs, docname
tmueller@0
    38
tmueller@0
    39
tmueller@0
    40
module "loona"
tmueller@0
    41
tmueller@0
    42
tmueller@0
    43
_VERSION = 2
tmueller@80
    44
_REVISION = 2
tmueller@0
    45
tmueller@0
    46
tmueller@0
    47
out = tek.web.out
tmueller@0
    48
setheader = tek.web.setheader
tmueller@0
    49
session = cgi.session
tmueller@0
    50
request = cgi.request
tmueller@0
    51
args = request.args
tmueller@0
    52
encodeform = cgi.encodeform
tmueller@0
    53
loadhtml = tek.web.include.load
tmueller@0
    54
source = tek.source
tmueller@0
    55
domarkup = tek.web.markup.main
tmueller@0
    56
expire = tek.util.expire
tmueller@0
    57
tmueller@0
    58
tmueller@20
    59
local function dbmsg(msg, detail)
tmueller@80
    60
	return (msg and detail and config.debug) and
tmueller@47
    61
		("%s : %s"):format(msg, detail) or msg
tmueller@20
    62
end
tmueller@20
    63
tmueller@20
    64
tmueller@0
    65
local function checkprofilename(c)
tmueller@47
    66
	assert(c:match("^%w+$") and c ~= "current",
tmueller@47
    67
		dbmsg("Invalid profile name", c))
tmueller@0
    68
	return c
tmueller@0
    69
end
tmueller@0
    70
tmueller@0
    71
tmueller@47
    72
local function checkbodyname(s)
tmueller@0
    73
	s = s or "main"
tmueller@47
    74
	assert(s:match("^[%w_]*%w+[%w_]*$"), dbmsg("Invalid body name", s))
tmueller@0
    75
	return s
tmueller@0
    76
end
tmueller@0
    77
tmueller@0
    78
tmueller@0
    79
local function deletedir(dst)
tmueller@0
    80
	for e in tek.util.readdir(dst) do
tmueller@47
    81
 		local success, msg = remove(dst .. "/" .. e)
tmueller@20
    82
		assert(success, dbmsg("Error removing entry in profile", msg))
tmueller@0
    83
	end
tmueller@0
    84
	return remove(dst)
tmueller@0
    85
end
tmueller@0
    86
tmueller@0
    87
tmueller@0
    88
local function copyprofile(contentdir, lang, srcprofile, newprofile)
tmueller@68
    89
	local src = ("%s/%s_%s"):format(contentdir, srcprofile, lang)
tmueller@20
    90
	assert(posix.stat(src, "mode") == "directory",
tmueller@20
    91
		dbmsg("Not a directory", src))
tmueller@68
    92
	local dst = ("%s/%s_%s"):format(contentdir, newprofile, lang)
tmueller@0
    93
	local success, msg = posix.mkdir(dst)
tmueller@20
    94
	assert(success, dbmsg("Error creating profile directory", msg))
tmueller@0
    95
	for e in tek.util.readdir(src) do
tmueller@0
    96
		local ext = e:match("^[^.].*%.([^.]*)$")
tmueller@0
    97
		if ext ~= "LOCK" then
tmueller@0
    98
			local f = src .. "/" .. e
tmueller@0
    99
			if posix.stat(f, "mode") == "file" then
tmueller@0
   100
				success, msg = tek.copyfile(f, dst .. "/" .. e)
tmueller@20
   101
				assert(success, dbmsg("Error copying file", msg))
tmueller@0
   102
			end
tmueller@0
   103
		end
tmueller@0
   104
	end
tmueller@0
   105
end
tmueller@0
   106
tmueller@0
   107
tmueller@0
   108
local function publishprofile(contentdir, lang, profile)
tmueller@68
   109
	local newpath = ("%s/current_%s"):format(contentdir, lang)
tmueller@68
   110
	local tmppath = newpath .. "." .. session.name
tmueller@23
   111
	local success, msg = posix.symlink(profile .. "_" .. lang, tmppath)
tmueller@20
   112
	assert(success, dbmsg("Cannot create symlink", msg))
tmueller@23
   113
	success, msg = rename(tmppath, newpath)
tmueller@23
   114
	assert(success, dbmsg("Cannot put symlink in place", msg))
tmueller@0
   115
end
tmueller@0
   116
tmueller@0
   117
tmueller@20
   118
local function lookupname(tab, val)
tmueller@0
   119
	if tab then
tmueller@0
   120
		for i, v in ipairs(tab) do
tmueller@20
   121
			if v.name == val then
tmueller@0
   122
				return i
tmueller@0
   123
			end
tmueller@0
   124
		end
tmueller@0
   125
	end
tmueller@0
   126
end
tmueller@0
   127
tmueller@0
   128
tmueller@20
   129
--	Index sections, determine accessibility and visibility in menu
tmueller@20
   130
tmueller@20
   131
local function indexsections(s)
tmueller@0
   132
	for _, e in ipairs(s) do
tmueller@0
   133
		if e.subs then
tmueller@20
   134
			indexsections(e.subs)
tmueller@0
   135
		end
tmueller@0
   136
		e.notvalid = (not secure and e.secure) or 
tmueller@0
   137
			(not authuser and e.secret) or nil
tmueller@0
   138
		e.notvisible = e.notvalid or not authuser and e.hidden or nil
tmueller@16
   139
		s[e.name] = e
tmueller@0
   140
	end
tmueller@0
   141
end
tmueller@0
   142
tmueller@0
   143
tmueller@20
   144
--	Decompose section path into a stack of sections, returning only up to
tmueller@0
   145
--	the last valid element in the path. additionally returns the table of
tmueller@0
   146
--	the last section path element (or the default section)
tmueller@0
   147
tmueller@27
   148
local function getsection(section, authuser, path, default)
tmueller@27
   149
	local tab = { { entries = sections, name = default } }
tmueller@27
   150
	local ss = sections
tmueller@0
   151
	local sectionpath
tmueller@0
   152
	(path or default):gsub("(%w+)/?", function(a)
tmueller@27
   153
		if ss then
tmueller@27
   154
			local s = ss[a]
tmueller@20
   155
			if s and not s.notvalid then
tmueller@20
   156
				sectionpath = s
tmueller@0
   157
				tab[#tab].name = a
tmueller@27
   158
				ss = s.subs
tmueller@27
   159
				if ss then
tmueller@27
   160
					table.insert(tab, { entries = ss })
tmueller@0
   161
				end
tmueller@0
   162
			else
tmueller@27
   163
				ss = nil -- stop.
tmueller@0
   164
			end
tmueller@0
   165
		end
tmueller@0
   166
	end)
tmueller@0
   167
	if not section and not sectionpath then
tmueller@27
   168
		sectionpath = sections[default]
tmueller@0
   169
		if sectionpath then
tmueller@0
   170
			table.insert(tab, { entries = sectionpath.subs })
tmueller@0
   171
		end
tmueller@0
   172
	end
tmueller@0
   173
	return tab, sectionpath
tmueller@0
   174
end
tmueller@0
   175
tmueller@0
   176
tmueller@27
   177
local function getpath(menus, delimiter)
tmueller@0
   178
	local t = { }
tmueller@27
   179
	for _, menu in ipairs(menus) do
tmueller@0
   180
		if menu.name then
tmueller@0
   181
			table.insert(t, menu.name)
tmueller@0
   182
		end
tmueller@0
   183
	end
tmueller@0
   184
	return table.concat(t, delimiter or "/")
tmueller@0
   185
end
tmueller@0
   186
tmueller@0
   187
tmueller@20
   188
--	Descending into the sections table alongside the current path,
tmueller@20
   189
--	return the filename to include, defaulting to its parent value
tmueller@20
   190
--	(or the default specified)
tmueller@0
   191
tmueller@27
   192
local function getsectionfile(menus, path, ext, default)
tmueller@20
   193
	local t, val = { }
tmueller@27
   194
	for _, menu in ipairs(menus) do
tmueller@20
   195
		if menu.entries and menu.entries[menu.name] then
tmueller@20
   196
			table.insert(t, menu.name)
tmueller@20
   197
			local fn = table.concat(t, "_") .. ext
tmueller@0
   198
			if posix.stat(path .. "/" .. fn, "mode") == "file" then
tmueller@0
   199
				val = fn
tmueller@0
   200
			end
tmueller@0
   201
		end
tmueller@0
   202
	end
tmueller@0
   203
	return val or default
tmueller@0
   204
end
tmueller@0
   205
tmueller@0
   206
tmueller@45
   207
local function deletesection(dir, fname)
tmueller@0
   208
	local fullname = dir .. "/" .. fname
tmueller@0
   209
	local success, msg = remove(fullname)
tmueller@0
   210
	if success then
tmueller@0
   211
		local pat = "^" .. 
tmueller@0
   212
			fname:gsub("%^%$%(%)%%%.%[%]%*%+%-%?", "%%%1") .. "%..*$"
tmueller@0
   213
		for e in tek.util.readdir(dir) do
tmueller@0
   214
			if e:match(pat) then
tmueller@0
   215
				remove(dir .. "/" .. e)
tmueller@0
   216
			end
tmueller@0
   217
		end
tmueller@0
   218
	end
tmueller@0
   219
	return success, msg
tmueller@0
   220
end
tmueller@0
   221
tmueller@0
   222
tmueller@20
   223
--	Add element to path
tmueller@0
   224
tmueller@20
   225
local function addtopath(tab, path, e)
tmueller@0
   226
	path:gsub("(%w+)/?", function(a)
tmueller@20
   227
		if tab then
tmueller@20
   228
			local s = tab[a]
tmueller@20
   229
			if s then
tmueller@20
   230
				if not s.subs then
tmueller@20
   231
					s.subs = { }
tmueller@0
   232
				end
tmueller@20
   233
				tab = s.subs
tmueller@0
   234
			else
tmueller@20
   235
				table.insert(tab, e)
tmueller@20
   236
				tab[a] = e
tmueller@20
   237
 				tab = nil -- stop
tmueller@0
   238
			end
tmueller@0
   239
		end
tmueller@0
   240
	end)
tmueller@0
   241
end
tmueller@0
   242
tmueller@0
   243
tmueller@20
   244
--	Remove element from path
tmueller@0
   245
tmueller@20
   246
local function rmpath(tab, path)
tmueller@20
   247
	local parent
tmueller@0
   248
	path:gsub("(%w+)/?", function(a)
tmueller@20
   249
		if tab then
tmueller@20
   250
			local idx = lookupname(tab, a)
tmueller@20
   251
			if idx then
tmueller@20
   252
				if tab[idx].subs then
tmueller@20
   253
					parent = tab[idx]
tmueller@20
   254
					tab = tab[idx].subs
tmueller@0
   255
				else
tmueller@20
   256
					table.remove(tab, idx)
tmueller@43
   257
					tab[a] = nil
tmueller@20
   258
					if #tab == 0 and parent then
tmueller@20
   259
						parent.subs = nil
tmueller@0
   260
					end
tmueller@20
   261
					tab = nil
tmueller@0
   262
				end
tmueller@0
   263
			end
tmueller@0
   264
		end
tmueller@0
   265
	end)
tmueller@0
   266
end
tmueller@0
   267
tmueller@0
   268
tmueller@0
   269
-------------------------------------------------------------------------------
tmueller@0
   270
tmueller@0
   271
tmueller@8
   272
--	Produce page title
tmueller@8
   273
tmueller@8
   274
function title()
tmueller@8
   275
	return section and (section.title or section.label or section.name) or ""
tmueller@8
   276
end
tmueller@8
   277
tmueller@8
   278
tmueller@47
   279
--	Create proxy for on-demand loading of locale strings
tmueller@0
   280
tmueller@47
   281
locale = { }
tmueller@47
   282
local locmt = { }
tmueller@47
   283
locmt.__index = function(_, key)
tmueller@47
   284
	for _, l in ipairs(langs) do
tmueller@47
   285
		locmt.__locale = source(config.localedir .. "/" .. l)
tmueller@47
   286
		if locmt.__locale then
tmueller@47
   287
			break
tmueller@0
   288
		end
tmueller@0
   289
	end
tmueller@47
   290
	locmt.__index = locmt.__locale
tmueller@47
   291
	return locmt.__locale[key]
tmueller@0
   292
end
tmueller@47
   293
setmetatable(locale, locmt)
tmueller@0
   294
tmueller@0
   295
tmueller@0
   296
--	Find element in path
tmueller@0
   297
tmueller@0
   298
function checkpath(tab, path)
tmueller@0
   299
	local res, idx
tmueller@0
   300
	path:gsub("(%w+)/?", function(a)
tmueller@0
   301
		if tab then
tmueller@20
   302
			local i = lookupname(tab, a)
tmueller@0
   303
			if i then
tmueller@0
   304
				res, idx = tab, i
tmueller@0
   305
				tab = tab[i].subs
tmueller@0
   306
			else
tmueller@0
   307
				res, idx = nil, nil
tmueller@0
   308
			end
tmueller@0
   309
		end
tmueller@0
   310
	end)
tmueller@0
   311
	return res, idx
tmueller@0
   312
end
tmueller@0
   313
tmueller@0
   314
tmueller@0
   315
--	Run a site function snippet, with full error recovery
tmueller@0
   316
--	(also recovers from errors in error handling function)
tmueller@0
   317
tmueller@23
   318
function dosnippet(config, func, errfunc)
tmueller@0
   319
	local ret = { tek.catch(func) }
tmueller@0
   320
	if ret[1] == 0 or (errfunc and tek.catch(errfunc) == 0) then
tmueller@0
   321
		return unpack(ret)
tmueller@0
   322
	end
tmueller@0
   323
	out("<h2>Error</h2>")
tmueller@68
   324
	out("<h3>" .. cgi.encodeform(ret[2] or "") .. "</h3>")
tmueller@80
   325
	if config.debug then
tmueller@0
   326
		if type(ret[3]) == "string" then
tmueller@68
   327
			out("<p>" .. cgi.encodeform(ret[3]) .. "</p>")
tmueller@0
   328
		end
tmueller@80
   329
		if ret[4] and config.debug then
tmueller@68
   330
			out("<pre>" .. cgi.encodeform(ret[4]) .. "</pre>")
tmueller@0
   331
		end
tmueller@0
   332
	end
tmueller@0
   333
end	
tmueller@0
   334
tmueller@0
   335
tmueller@0
   336
function lockfile(newfile)
tmueller@0
   337
	return not session and true or 
tmueller@0
   338
		posix.symlink(session.filename, newfile .. ".LOCK")
tmueller@0
   339
end
tmueller@0
   340
tmueller@0
   341
tmueller@0
   342
function unlockfile(dstfile)
tmueller@0
   343
	return not session and true or remove(dstfile .. ".LOCK")
tmueller@0
   344
end
tmueller@0
   345
tmueller@0
   346
tmueller@45
   347
function savesection(dir, fname, content)
tmueller@0
   348
	fname = dir .. "/" .. fname
tmueller@0
   349
	local f, msg = open(fname, "wb")
tmueller@20
   350
	assert(f, dbmsg("Could not open file for writing", msg))
tmueller@0
   351
	f:write(content or "")
tmueller@0
   352
	f:close()
tmueller@0
   353
end
tmueller@0
   354
tmueller@23
   355
tmueller@20
   356
--	Run extension in a controlled environment
tmueller@0
   357
tmueller@0
   358
function include(fname, ...)
tmueller@47
   359
	assert(not fname:match("%W"), dbmsg("Invalid include name", fname))
tmueller@47
   360
	local fname2 = ("%s/%s.lua"):format(config.extdir, fname)
tmueller@0
   361
	local f, msg = open(fname2)
tmueller@20
   362
	assert(f, dbmsg("Cannot open file", msg))
tmueller@0
   363
	local parsed, msg = loadhtml(f, "loona.out", fname2)
tmueller@20
   364
	assert(parsed, dbmsg("Syntax error", msg))
tmueller@0
   365
 	local fenv = {
tmueller@0
   366
 		arg = arg,
tmueller@0
   367
 		loona = {
tmueller@0
   368
 			out = out,
tmueller@0
   369
 			setheader = setheader,
tmueller@0
   370
			hidden = hidden,
tmueller@15
   371
			link = link,
tmueller@0
   372
			elink = elink,
tmueller@68
   373
			plink = plink,
tmueller@0
   374
			href = href,
tmueller@0
   375
			checkpath = checkpath,
tmueller@0
   376
			authuser = authuser,
tmueller@0
   377
			document = document,
tmueller@0
   378
			contentdir = contentdir,
tmueller@0
   379
			profile = profile,
tmueller@0
   380
			pubprofile = pubprofile,
tmueller@0
   381
			lang = lang,
tmueller@47
   382
			locale = locale,
tmueller@0
   383
			secure = secure,
tmueller@0
   384
			config = config,
tmueller@27
   385
			sectionpath = sectionpath,
tmueller@27
   386
			sections = sections,
tmueller@0
   387
			session = session,
tmueller@104
   388
			loginfailed = loginfailed
tmueller@0
   389
		}
tmueller@0
   390
	}
tmueller@0
   391
 	setmetatable(fenv, { __index = boxed_G }) -- boxed global environment
tmueller@0
   392
	setfenv(parsed, fenv)
tmueller@0
   393
	return parsed()
tmueller@0
   394
end
tmueller@0
   395
tmueller@0
   396
tmueller@68
   397
--	produce link target, propagate lang, profile, session
tmueller@68
   398
tmueller@0
   399
function href(section, ...)
tmueller@68
   400
	local target = section and docname .. "/" .. section or docname
tmueller@68
   401
	if session or profile ~= pubprofile then
tmueller@68
   402
		return tek.web.gethref(target, { "profile", "session", "lang", 
tmueller@68
   403
			unpack(arg) })
tmueller@0
   404
	end
tmueller@68
   405
	return tek.web.gethref(target, { "lang", unpack(arg) })
tmueller@0
   406
end
tmueller@0
   407
tmueller@68
   408
local function ilink(target, text, extra)
tmueller@68
   409
	return ('<a href="%s"%s>%s</a>'):format(target, extra or "", text)
tmueller@0
   410
end
tmueller@0
   411
tmueller@68
   412
--	internal link, propagation of lang, profile, session
tmueller@0
   413
tmueller@68
   414
function link(section, text, ...)
tmueller@83
   415
	return ilink(href(section, unpack(arg)), text or section)
tmueller@0
   416
end
tmueller@0
   417
tmueller@68
   418
--	external link (opens in a new window), no argument propagation
tmueller@0
   419
tmueller@68
   420
function elink(target, text)
tmueller@68
   421
	return ilink(target, text or target,
tmueller@83
   422
		' class="extlink" onclick="void(window.open(this.href, \'\', \'\')); return false;"')
tmueller@0
   423
end
tmueller@0
   424
tmueller@68
   425
--	plain link, no argument propagation
tmueller@68
   426
tmueller@68
   427
function plink(section, text, ...)
tmueller@68
   428
	return ilink(tek.web.gethref(section, arg), text or section)
tmueller@68
   429
end
tmueller@68
   430
tmueller@68
   431
--	user interface link, propagation of lang, profile, session
tmueller@68
   432
tmueller@68
   433
function uilink(section, text, ...)
tmueller@83
   434
	return ilink(href(section, unpack(arg)), text or section)
tmueller@68
   435
end
tmueller@68
   436
tmueller@68
   437
--	produce a hidden input value in forms
tmueller@0
   438
tmueller@0
   439
function hidden(name, value)
tmueller@68
   440
	return not value and "" or
tmueller@68
   441
		('<input type="hidden" name="%s" value="%s" />'):format(name, value)
tmueller@0
   442
end
tmueller@0
   443
tmueller@0
   444
tmueller@0
   445
function getprofiles(contentdir, lang)
tmueller@0
   446
	local t = { }
tmueller@0
   447
	for f in tek.util.readdir(contentdir) do
tmueller@0
   448
		if posix.lstat(contentdir .. "/" .. f, "mode") == "directory" then
tmueller@0
   449
			local e = f:match("^(%w+)_" .. lang .. "$")
tmueller@0
   450
 			if e then
tmueller@0
   451
	 			t[e] = e
tmueller@0
   452
 	 		end
tmueller@0
   453
		end
tmueller@0
   454
	end
tmueller@0
   455
	return t
tmueller@0
   456
end
tmueller@0
   457
tmueller@0
   458
tmueller@80
   459
--	Functions to produce a hierarchical navigation menu
tmueller@80
   460
tmueller@80
   461
local function rmenu(level, linkf, path)
tmueller@80
   462
	local sub = submenus[level]
tmueller@80
   463
	if sub and sub.entries then
tmueller@80
   464
		local visible = { }
tmueller@80
   465
		for _, e in ipairs(sub.entries) do
tmueller@80
   466
			if not e.notvisible then
tmueller@80
   467
				table.insert(visible, e)
tmueller@80
   468
			end
tmueller@80
   469
		end
tmueller@80
   470
		if #visible > 0 then
tmueller@87
   471
			out('<ul id="menulevel' .. level .. '">\n')
tmueller@80
   472
			for _, e in ipairs(visible) do
tmueller@80
   473
				local label = encodeform(e.label or e.name)
tmueller@80
   474
				local newpath = path and path .. "/" .. e.name or e.name
tmueller@80
   475
				local active = (e.name == sub.name)
tmueller@87
   476
				out('<li>\n')
tmueller@80
   477
				linkf(newpath, label, active, e.action)
tmueller@80
   478
				if active then
tmueller@80
   479
					rmenu(level + 1, linkf, newpath)
tmueller@80
   480
				end
tmueller@87
   481
				out('</li>\n')
tmueller@80
   482
			end
tmueller@87
   483
			out('</ul>\n')
tmueller@80
   484
		end
tmueller@80
   485
	end
tmueller@80
   486
end
tmueller@80
   487
tmueller@87
   488
local function menulink(path, label, active, ...)
tmueller@87
   489
	out(('<a %shref="%s">%s</a>\n'):format(active and 'class="active" ' or "",
tmueller@87
   490
		href(path, unpack(arg)), label))
tmueller@80
   491
end
tmueller@80
   492
tmueller@80
   493
function menu(level, linkf)
tmueller@87
   494
	rmenu(level or 1, linkf or menulink)
tmueller@80
   495
end
tmueller@80
   496
tmueller@80
   497
tmueller@0
   498
-- Init
tmueller@0
   499
tmueller@0
   500
local function init()
tmueller@0
   501
	
tmueller@0
   502
	-- get list of languages, in order of preference
tmueller@0
   503
	
tmueller@0
   504
	langs = { args.lang and args.lang:match("^%w+$") }
tmueller@80
   505
	if config.browserlang then
tmueller@0
   506
		local s = getenv("HTTP_ACCEPT_LANGUAGE")
tmueller@0
   507
		while s do
tmueller@0
   508
			local l, r = s:match("^([%w.=]+)[,;](.*)$")
tmueller@0
   509
			l = l or s
tmueller@0
   510
			s = r
tmueller@0
   511
			if l:match("^%w+$") then
tmueller@0
   512
				table.insert(langs, l)
tmueller@0
   513
			end
tmueller@0
   514
		end
tmueller@0
   515
	end
tmueller@0
   516
	table.insert(langs, config.deflang)
tmueller@0
   517
	
tmueller@0
   518
	-- get list of possible profiles
tmueller@0
   519
	
tmueller@0
   520
	local profiles = { }
tmueller@0
   521
	for e in tek.util.readdir(config.contentdir) do
tmueller@0
   522
		profiles[e] = e
tmueller@0
   523
	end
tmueller@0
   524
	
tmueller@0
   525
	-- get pubprofile
tmueller@0
   526
	
tmueller@0
   527
	for _, lang in ipairs(langs) do
tmueller@0
   528
		local p = posix.readlink(config.contentdir .. "/current_" .. lang)
tmueller@0
   529
		p = p and p:match("^(%w+)_" .. lang .. "$")
tmueller@0
   530
		if p then
tmueller@0
   531
			pubprofile = p
tmueller@0
   532
			break
tmueller@0
   533
		end
tmueller@0
   534
	end
tmueller@0
   535
	
tmueller@0
   536
	-- get profile
tmueller@0
   537
	
tmueller@0
   538
	local checkprofile = authuser and args.profile or pubprofile or "default"
tmueller@0
   539
	for _, l in ipairs(langs) do
tmueller@0
   540
		if profiles[checkprofile .. "_" .. l] then
tmueller@0
   541
			profile = checkprofile
tmueller@0
   542
			lang = l
tmueller@0
   543
			break
tmueller@0
   544
		end
tmueller@0
   545
	end
tmueller@0
   546
	
tmueller@20
   547
	assert(profile and lang, "Invalid profile or language")
tmueller@0
   548
	
tmueller@47
   549
	
tmueller@47
   550
	-- Write back language and profile
tmueller@0
   551
tmueller@0
   552
	args.lang = lang ~= config.deflang and lang or nil
tmueller@0
   553
	args.profile = profile
tmueller@0
   554
	
tmueller@47
   555
	
tmueller@0
   556
	-- determine content directory pathname and section filename
tmueller@0
   557
	
tmueller@68
   558
	contentdir = ("%s/%s_%s"):format(config.contentdir, profile, lang)
tmueller@68
   559
 	sectionfname = contentdir .. "/.sections"
tmueller@0
   560
	
tmueller@0
   561
	-- load sections
tmueller@0
   562
	
tmueller@68
   563
 	sections = source(sectionfname)
tmueller@0
   564
	
tmueller@20
   565
	-- index sections, determine visibility in menu
tmueller@0
   566
	
tmueller@27
   567
	indexsections(sections)
tmueller@0
   568
	
tmueller@0
   569
	-- decompose section path, produce a stack of sections
tmueller@0
   570
	
tmueller@27
   571
	submenus, section = getsection(section, authuser, 
tmueller@0
   572
		cgi.document.VirtualPath or "", not authuser and config.defname)
tmueller@0
   573
tmueller@0
   574
	-- handle redirects if not logged on
tmueller@0
   575
	
tmueller@0
   576
	if not authuser and section and section.redirect then
tmueller@27
   577
		submenus, section = getsection(section, authuser, 
tmueller@0
   578
			section.redirect, not authuser and config.defname)
tmueller@0
   579
	end
tmueller@0
   580
			
tmueller@0
   581
	-- section path and document name (refined)
tmueller@0
   582
	
tmueller@0
   583
	sectionpath = getpath(submenus)
tmueller@77
   584
	sectionname = getpath(submenus, "_")
tmueller@0
   585
tmueller@0
   586
end
tmueller@0
   587
tmueller@0
   588
tmueller@20
   589
--	Handle state modifications (create/save/delete, profile management)
tmueller@0
   590
tmueller@0
   591
local function handlestate()
tmueller@23
   592
tmueller@0
   593
	if args.editkey == "main" then
tmueller@23
   594
		
tmueller@23
   595
		-- In main editable section:
tmueller@23
   596
		
tmueller@23
   597
		local save
tmueller@0
   598
		
tmueller@0
   599
		if args.actioncreate then
tmueller@45
   600
			-- Create new section
tmueller@0
   601
			local editname = args.editname:lower()
tmueller@20
   602
			assert(not editname:match("%W"),
tmueller@47
   603
				dbmsg("Invalid section name", editname))
tmueller@20
   604
			if not (section and (section.subs or section)[editname]) then
tmueller@20
   605
				local newpath = 
tmueller@20
   606
					(sectionpath and (sectionpath .. "/")) .. editname
tmueller@27
   607
				addtopath(sections, newpath, { name = editname,
tmueller@0
   608
					label = args.editlabel ~= "" and args.editlabel or nil,
tmueller@0
   609
					title = args.edittitle ~= "" and args.edittitle or nil,
tmueller@43
   610
					redirect = args.editredirect ~= "" and args.editredirect or nil,
tmueller@43
   611
					hidden = args.editvisibility and true,
tmueller@43
   612
					secret = args.editsecrecy and true,
tmueller@43
   613
					secure = args.editsecure and true,
tmueller@0
   614
					creator = authuser,
tmueller@20
   615
					creationdate = time() })
tmueller@23
   616
				save = true
tmueller@0
   617
			end
tmueller@20
   618
		
tmueller@0
   619
		elseif args.actionsave then
tmueller@45
   620
			-- Save section
tmueller@0
   621
			section.revisiondate = time()
tmueller@0
   622
			section.revisioner = authuser
tmueller@23
   623
			save = true
tmueller@20
   624
		
tmueller@0
   625
		elseif args.actiondelete then
tmueller@45
   626
			-- Delete section
tmueller@0
   627
			if not args.actionconfirm then
tmueller@20
   628
				useralert = {
tmueller@47
   629
					text = locale.ALERT_DELETE_SECTION,
tmueller@20
   630
					confirm =
tmueller@20
   631
						'<input type="submit" name="actiondelete" value="' .. 
tmueller@47
   632
						locale.DELETE .. '" /> ' ..
tmueller@20
   633
						hidden("actionconfirm", "true")
tmueller@20
   634
				}
tmueller@0
   635
			else
tmueller@77
   636
				deletesection(contentdir, sectionname)
tmueller@27
   637
				rmpath(sections, sectionpath)
tmueller@23
   638
				save = true
tmueller@0
   639
			end
tmueller@20
   640
		
tmueller@0
   641
		elseif args.actionsaveprops then
tmueller@23
   642
			-- Save properties
tmueller@0
   643
			section.hidden = args.editvisibility and true
tmueller@0
   644
			section.secret = args.editsecrecy and true
tmueller@0
   645
			section.secure = args.editsecure and true
tmueller@0
   646
			section.label = args.editlabel ~= "" and args.editlabel or nil
tmueller@0
   647
			section.title = args.edittitle ~= "" and args.edittitle or nil
tmueller@20
   648
			section.redirect =
tmueller@20
   649
				args.editredirect ~= "" and args.editredirect or nil
tmueller@23
   650
			save = true
tmueller@20
   651
		
tmueller@0
   652
		elseif args.actionup then
tmueller@45
   653
			-- Move section up
tmueller@27
   654
			local t, i = checkpath(sections, sectionpath)
tmueller@0
   655
			if t and i > 1 then
tmueller@115
   656
				if profile == pubprofile and not args.actionconfirm then
tmueller@115
   657
					useralert = {
tmueller@115
   658
						text = locale.ALERT_MOVE_SECTION_IN_PUBLISHED_PROFILE,
tmueller@115
   659
						confirm =
tmueller@115
   660
							'<input type="submit" name="actionup" value="' .. 
tmueller@115
   661
							locale.MOVE .. '" /> ' ..
tmueller@115
   662
							hidden("actionconfirm", "true")
tmueller@115
   663
					}
tmueller@115
   664
				else
tmueller@115
   665
					local item = table.remove(t, i)
tmueller@115
   666
					table.insert(t, i - 1, item)
tmueller@115
   667
					save = true
tmueller@115
   668
				end
tmueller@0
   669
			end
tmueller@20
   670
		
tmueller@0
   671
		elseif args.actiondown then
tmueller@45
   672
			-- Move section down
tmueller@27
   673
			local t, i = checkpath(sections, sectionpath)
tmueller@0
   674
			if t and i < #t then
tmueller@115
   675
				if profile == pubprofile and not args.actionconfirm then
tmueller@115
   676
					useralert = {
tmueller@115
   677
						text = locale.ALERT_MOVE_SECTION_IN_PUBLISHED_PROFILE,
tmueller@115
   678
						confirm =
tmueller@115
   679
							'<input type="submit" name="actiondown" value="' .. 
tmueller@115
   680
							locale.MOVE .. '" /> ' ..
tmueller@115
   681
							hidden("actionconfirm", "true")
tmueller@115
   682
					}
tmueller@115
   683
				else
tmueller@115
   684
					local item = table.remove(t, i)
tmueller@115
   685
					table.insert(t, i + 1, item)
tmueller@115
   686
					save = true
tmueller@115
   687
				end
tmueller@0
   688
			end
tmueller@20
   689
		
tmueller@0
   690
		elseif args.actioncreateprofile and args.createprofile then
tmueller@23
   691
			-- Create profile
tmueller@0
   692
			local c = checkprofilename(args.createprofile:lower())
tmueller@0
   693
			if c == profile then
tmueller@47
   694
				useralert = { text = locale.ALERT_CANNOT_COPY_PROFILE_TO_SELF }
tmueller@0
   695
			else
tmueller@0
   696
				local profiles = getprofiles(config.contentdir, lang)
tmueller@0
   697
				if profiles[c] and not args.actionconfirm then
tmueller@20
   698
					useralert = {
tmueller@20
   699
						text = c == pubprofile and 
tmueller@47
   700
							locale.ALERT_OVERWRITE_PUBLISHED_PROFILE or
tmueller@47
   701
							locale.ALERT_OVERWRITE_EXISTING_PROFILE,
tmueller@0
   702
						confirm =
tmueller@20
   703
							'<input type="submit" name="actioncreateprofile" value="' .. 
tmueller@47
   704
							locale.OVERWRITE .. '" /> ' ..
tmueller@20
   705
							hidden("actionconfirm", "true") .. 
tmueller@20
   706
							hidden("createprofile", c)
tmueller@20
   707
					}
tmueller@0
   708
				else
tmueller@0
   709
					if profiles[c] then
tmueller@0
   710
						deletedir(config.contentdir .. "/" .. c .. "_" .. lang)
tmueller@0
   711
					end
tmueller@0
   712
					copyprofile(config.contentdir, lang, profile, c)
tmueller@0
   713
				end
tmueller@0
   714
			end
tmueller@20
   715
		
tmueller@0
   716
		elseif args.actiondeleteprofile and args.deleteprofile then
tmueller@23
   717
			-- Delete profile
tmueller@0
   718
			local c = checkprofilename(args.deleteprofile:lower())
tmueller@47
   719
			assert(c ~= pubprofile, dbmsg("Cannot delete published profile", c))
tmueller@0
   720
			if args.actionconfirm then
tmueller@0
   721
				deletedir(config.contentdir .. "/" .. c .. "_" .. lang)
tmueller@0
   722
				profile = nil
tmueller@0
   723
				args.profile = nil
tmueller@68
   724
				init()
tmueller@23
   725
				save = true
tmueller@0
   726
			else
tmueller@20
   727
				useralert = { 
tmueller@47
   728
					text = locale.ALERT_DELETE_PROFILE,
tmueller@20
   729
					confirm = 
tmueller@20
   730
						'<input type="submit" name="actiondeleteprofile" value="' .. 
tmueller@47
   731
						locale.DELETE .. '" /> ' ..
tmueller@20
   732
						hidden("actionconfirm", "true") ..
tmueller@20
   733
						hidden("deleteprofile", c)
tmueller@20
   734
				}
tmueller@0
   735
			end
tmueller@20
   736
		
tmueller@0
   737
		elseif args.actionchangeprofile and args.changeprofile then
tmueller@23
   738
			-- Change profile
tmueller@0
   739
			local c = checkprofilename(args.changeprofile:lower())
tmueller@0
   740
			profile = c
tmueller@0
   741
			args.profile = c
tmueller@23
   742
			save = true
tmueller@20
   743
		
tmueller@0
   744
		elseif args.actionpublishprofile and args.publishprofile then
tmueller@23
   745
			-- Publish profile
tmueller@0
   746
			local c = checkprofilename(args.publishprofile:lower())
tmueller@0
   747
			if c ~= _publicprofile then
tmueller@0
   748
				if args.actionconfirm then
tmueller@0
   749
					publishprofile(config.contentdir, lang, c)
tmueller@23
   750
					save = true
tmueller@0
   751
				else
tmueller@20
   752
					useralert = {
tmueller@47
   753
						text = locale.ALERT_PUBLISH_PROFILE,
tmueller@20
   754
						confirm =
tmueller@20
   755
							'<input type="submit" name="actionpublishprofile" value="' ..
tmueller@47
   756
							locale.PUBLISH .. '" /> ' ..
tmueller@20
   757
							hidden("actionconfirm", "true") ..
tmueller@20
   758
							hidden("publishprofile", c)
tmueller@20
   759
					}
tmueller@20
   760
				end
tmueller@20
   761
			end
tmueller@0
   762
		end
tmueller@0
   763
		
tmueller@23
   764
		if save then
tmueller@23
   765
			-- Write sections atomically, reload
tmueller@68
   766
			local tempname = sectionfname .. "." .. session.name
tmueller@0
   767
			local f, msg = open(tempname, "wb")
tmueller@20
   768
			assert(f, dbmsg("Error opening section file for writing", msg))
tmueller@27
   769
			tek.dump(sections, function(...)
tmueller@0
   770
				f:write(unpack(arg))
tmueller@0
   771
			end)
tmueller@0
   772
			f:close()
tmueller@0
   773
			local success, msg = rename(tempname, sectionfname)
tmueller@20
   774
			assert(success, dbmsg("Error renaming section file", msg))
tmueller@0
   775
			init()
tmueller@0
   776
		end
tmueller@0
   777
	
tmueller@47
   778
	elseif args.editkey and checkbodyname(args.editkey) then
tmueller@0
   779
		if args.actiondelete then
tmueller@45
   780
			-- Delete section in secondary editable body:
tmueller@77
   781
			deletesection(contentdir, sectionname .. "." .. args.editkey)
tmueller@0
   782
		end
tmueller@0
   783
	end
tmueller@0
   784
end
tmueller@0
   785
tmueller@0
   786
tmueller@80
   787
--	Load/create configuration
tmueller@0
   788
tmueller@0
   789
config = source("../etc/config.lua") or { }
tmueller@80
   790
tmueller@80
   791
config.defname = config.defname or "home"
tmueller@80
   792
config.deflang = config.deflang or "en"
tmueller@80
   793
config.sessionmaxage = config.sessionmaxage or 600
tmueller@80
   794
config.secureport = config.secureport or 443
tmueller@80
   795
tmueller@80
   796
--	Check paths and make them absolute
tmueller@80
   797
tmueller@20
   798
config.passwdfile = posix.abspath(config.passwdfile or "../etc/passwd.lua")
tmueller@0
   799
config.sessiondir = posix.abspath(config.sessiondir or "../var/sessions")
tmueller@0
   800
config.extdir = posix.abspath(config.extdir or "../extensions")
tmueller@20
   801
config.contentdir = posix.abspath(config.contentdir or "../content")
tmueller@20
   802
config.localedir = posix.abspath(config.localedir or "../locale")
tmueller@0
   803
tmueller@80
   804
--	Manage login and establish session
tmueller@0
   805
tmueller@0
   806
session.init(config.sessiondir, args.session, config.sessionmaxage)
tmueller@0
   807
if args.login then
tmueller@0
   808
	if args.login == "false" then
tmueller@0
   809
		session.delete()
tmueller@0
   810
		session = nil
tmueller@0
   811
	elseif args.password then
tmueller@104
   812
		loginfailed = true
tmueller@0
   813
		local pwddb = source(config.passwdfile)
tmueller@0
   814
		local pwdentry = pwddb[args.login]
tmueller@0
   815
		if pwdentry and pwdentry.password == args.password then
tmueller@0
   816
			session.data.authuser = pwdentry.username
tmueller@0
   817
			session.data.id = session.id
tmueller@104
   818
			loginfailed = nil
tmueller@0
   819
		end
tmueller@0
   820
	end
tmueller@0
   821
end
tmueller@0
   822
tmueller@68
   823
secure = request.Port == config.secureport
tmueller@20
   824
authuser = session and session.data.authuser
tmueller@20
   825
tmueller@20
   826
if not authuser then
tmueller@0
   827
	session = nil
tmueller@0
   828
	args.session = nil
tmueller@0
   829
end
tmueller@0
   830
tmueller@23
   831
tmueller@80
   832
--	get lang, locale, profile, section
tmueller@0
   833
tmueller@0
   834
init()
tmueller@0
   835
if authuser then
tmueller@80
   836
	-- handle state modifications
tmueller@0
   837
	handlestate()
tmueller@0
   838
end
tmueller@0
   839
tmueller@23
   840
tmueller@80
   841
--	add links for creating new sections
tmueller@20
   842
tmueller@0
   843
if authuser then
tmueller@20
   844
	local newent = { name = "new", 
tmueller@47
   845
		label = "[" .. locale.NEW .. "]",
tmueller@20
   846
		action = "actionnew=true" }
tmueller@0
   847
	for _, s in ipairs(submenus) do
tmueller@20
   848
		table.insert(s.entries, newent)
tmueller@0
   849
	end
tmueller@0
   850
	if submenus[#submenus].name then
tmueller@20
   851
		table.insert(submenus, {
tmueller@20
   852
			name = "new", 
tmueller@20
   853
			entries = { [1] = newent }
tmueller@20
   854
		})
tmueller@0
   855
	end
tmueller@0
   856
end
tmueller@0
   857
tmueller@23
   858
tmueller@80
   859
--	current document
tmueller@80
   860
tmueller@80
   861
docname = cgi.document.Name
tmueller@80
   862
document = docname .. "/" .. sectionpath
tmueller@80
   863
tmueller@80
   864
tmueller@0
   865
--	create section function
tmueller@0
   866
tmueller@0
   867
local func, msg = loadhtml(open("loona/editable.lua"),
tmueller@0
   868
	"tek.web.out", "loona/editable.lua")
tmueller@0
   869
tmueller@20
   870
assert(func, dbmsg("Syntax error", msg))
tmueller@0
   871
tmueller@0
   872
local editable = func()
tmueller@0
   873
tmueller@0
   874
function body(name)
tmueller@47
   875
	name = checkbodyname(name)
tmueller@0
   876
	if name == "main" then
tmueller@80
   877
		dosnippet(config, editable("main", contentdir, sectionname, sectionname))
tmueller@0
   878
	else
tmueller@0
   879
		local ext = "." .. name
tmueller@0
   880
		dosnippet(config, editable(name, contentdir,
tmueller@77
   881
			getsectionfile(submenus, contentdir, ext), sectionname .. ext))
tmueller@0
   882
	end
tmueller@0
   883
end
tmueller@0
   884
tmueller@23
   885
tmueller@0
   886
--	write back session state
tmueller@0
   887
tmueller@0
   888
if session then
tmueller@0
   889
	session.save()
tmueller@0
   890
end