commands.lua 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. --------------------------------------------------------
  2. -- Minetest :: Auth Redux Mod v2.13 (auth_rx)
  3. --
  4. -- See README.txt for licensing and release notes.
  5. -- Copyright (c) 2017-2018, Leslie E. Krause
  6. --------------------------------------------------------
  7. local auth_db, auth_filter -- imported
  8. minetest.register_chatcommand( "filter", {
  9. description = "Enable or disable ruleset-based login filtering, or reload a ruleset definition.",
  10. privs = { server = true },
  11. func = function( name, param )
  12. if param == "" then
  13. return true, "Login filtering is currently " .. ( auth_filter.is_enabled and "enabled" or "disabled" ) .. "."
  14. elseif param == "disable" then
  15. auth_filter.is_enabled = false
  16. minetest.log( "action", "Login filtering disabled by " .. name .. "." )
  17. return true, "Login filtering is disabled."
  18. elseif param == "enable" then
  19. auth_filter.is_enabled = true
  20. minetest.log( "action", "Login filtering enabled by " .. name .. "." )
  21. return true, "Login filtering is enabled."
  22. elseif param == "reload" then
  23. auth_filter.refresh( )
  24. return true, "Ruleset definition was loaded successfully."
  25. else
  26. return false, "Unknown parameter specified."
  27. end
  28. end
  29. } )
  30. minetest.register_chatcommand( "fdebug", {
  31. description = "Start an interactive debugger for testing ruleset definitions.",
  32. privs = { server = true },
  33. func = function( name, param )
  34. if not minetest.create_form then
  35. return false, "This feature is not supported."
  36. end
  37. local epoch = os.time( { year = 1970, month = 1, day = 1, hour = 0 } )
  38. local vars = {
  39. __debug = { type = FILTER_TYPE_NUMBER, value = 0 },
  40. name = { type = FILTER_TYPE_STRING, value = "singleplayer" },
  41. addr = { type = FILTER_TYPE_ADDRESS, value = convert_ipv4( "127.0.0.1" ) },
  42. is_new = { type = FILTER_TYPE_BOOLEAN, value = true },
  43. privs_list = { type = FILTER_TYPE_SERIES, value = { } },
  44. users_list = { type = FILTER_TYPE_SERIES, is_auto = true },
  45. cur_users = { type = FILTER_TYPE_NUMBER, is_auto = true },
  46. max_users = { type = FILTER_TYPE_NUMBER, value = get_minetest_config( "max_users" ) },
  47. lifetime = { type = FILTER_TYPE_PERIOD, value = 0 },
  48. sessions = { type = FILTER_TYPE_NUMBER, value = 0 },
  49. failures = { type = FILTER_TYPE_NUMBER, value = 0 },
  50. attempts = { type = FILTER_TYPE_NUMBER, value = 0 },
  51. owner = { type = FILTER_TYPE_STRING, value = get_minetest_config( "name" ) },
  52. uptime = { type = FILTER_TYPE_PERIOD, is_auto = true },
  53. oldlogin = { type = FILTER_TYPE_MOMENT, value = epoch },
  54. newlogin = { type = FILTER_TYPE_MOMENT, value = epoch },
  55. ip_names_list = { type = FILTER_TYPE_SERIES, value = { } },
  56. ip_prelogin = { type = FILTER_TYPE_MOMENT, value = epoch },
  57. ip_oldcheck = { type = FILTER_TYPE_MOMENT, value = epoch },
  58. ip_newcheck = { type = FILTER_TYPE_MOMENT, value = epoch },
  59. ip_failures = { type = FILTER_TYPE_NUMBER, value = 0 },
  60. ip_attempts = { type = FILTER_TYPE_NUMBER, value = 0 }
  61. }
  62. local vars_list = { "__debug", "clock", "name", "addr", "is_new", "privs_list", "users_list", "cur_users", "max_users", "lifetime", "sessions", "failures", "attempts", "owner", "uptime", "oldlogin", "newlogin", "ip_names_list", "ip_prelogin", "ip_oldcheck", "ip_newcheck", "ip_failures", "ip_attempts" }
  63. local datatypes = { [FILTER_TYPE_NUMBER] = "NUMBER", [FILTER_TYPE_STRING] = "STRING", [FILTER_TYPE_BOOLEAN] = "BOOLEAN", [FILTER_TYPE_ADDRESS] = "ADDRESS", [FILTER_TYPE_PERIOD] = "PERIOD", [FILTER_TYPE_MOMENT] = "MOMENT", [FILTER_TYPE_SERIES] = "SERIES" }
  64. local has_prompt = true
  65. local has_output = true
  66. local login_index = 2
  67. local var_index = 1
  68. local translate = GenericFilter( ).translate
  69. local temp_name = "~greenlist_" .. minetest.encode_base64( name ) .. ".mt"
  70. local temp_file = io.open( minetest.get_worldpath( ) .. "/" .. temp_name, "w" ):close( )
  71. local temp_filter = AuthFilter( minetest.get_worldpath( ), temp_name, function ( err, num )
  72. return "The server encountered an internal error.", num, err
  73. end )
  74. local function clear_prompts( buffer, has_single )
  75. -- clear debug prompts from source code
  76. return string.gsub( buffer, "\n# ====== .- ======\n", "\n", has_single and 1 or nil )
  77. end
  78. local function insert_prompt( buffer, num, err )
  79. -- insert debug prompts into source code
  80. local i = 0
  81. return string.gsub( buffer, "\n", function ( )
  82. i = i + 1
  83. return ( i == num and string.format( "\n# ====== ^ Line %d: %s ^ ======\n", num, err ) or "\n" )
  84. end )
  85. end
  86. local function format_value( value, type )
  87. -- convert values to a human-readable format
  88. if type == FILTER_TYPE_STRING then
  89. return "\"" .. value .. "\""
  90. elseif type == FILTER_TYPE_NUMBER then
  91. return tostring( value )
  92. elseif type == FILTER_TYPE_BOOLEAN then
  93. return "$" .. tostring( value )
  94. elseif type == FILTER_TYPE_PERIOD then
  95. return tostring( math.abs( value ) ) .. "s"
  96. elseif type == FILTER_TYPE_MOMENT then
  97. return "+" .. tostring( value - vars.epoch.value ) .. "s"
  98. elseif type == FILTER_TYPE_ADDRESS then
  99. return table.concat( unpack_address( value ), "." )
  100. elseif type == FILTER_TYPE_SERIES then
  101. return "(" .. string.gsub( table.concat( value, "," ), "[^,]+", "\"%1\"" ) .. ")"
  102. end
  103. end
  104. local function update_vars( )
  105. -- automatically update preset variables
  106. if vars.uptime.is_auto then
  107. vars.uptime.value = minetest.get_server_uptime( ) end
  108. if vars.clock.is_auto then
  109. vars.clock.value = os.time( ) end
  110. if vars.users_list.is_auto then
  111. vars.users_list.value = auth_db.search( true ) end
  112. if vars.cur_users.is_auto then
  113. vars.cur_users.value = #auth_db.search( true ) end
  114. end
  115. local function get_formspec( buffer, status, var_state )
  116. local var_name = vars_list[ var_index ]
  117. local var_type = vars[ var_name ].type
  118. local var_value = vars[ var_name ].value
  119. local var_is_auto = vars[ var_name ].is_auto
  120. local formspec = "size[13.5,8.5]"
  121. .. default.gui_bg
  122. .. default.gui_bg_img
  123. .. "label[0.1,0.0;Ruleset Definition:]"
  124. .. "checkbox[2.6,-0.2;has_output;Show Client Output;" .. tostring( has_output ) .. "]"
  125. .. "checkbox[5.6,-0.2;has_prompt;Show Debug Prompt;" .. tostring( has_prompt ) .. "]"
  126. .. "textarea[0.4,0.5;8.6," .. ( not status and "8.4" or status.user and "5.6" or "7.3" ) .. ";buffer;;" .. minetest.formspec_escape( buffer ) .. "]"
  127. .. "button[0.1,7.8;2,1;export_ruleset;Save]"
  128. .. "button[2.0,7.8;2,1;import_ruleset;Load]"
  129. .. "button[4.0,7.8;2,1;process_ruleset;Process]"
  130. .. "dropdown[6,7.9;2.6,1;login_mode;Normal,New Account,Wrong Password;" .. login_index .. "]"
  131. .. "label[9.0,0.0;Preset Variables:]"
  132. .. "textlist[9.0,0.5;4,4.7;vars_list"
  133. for i, v in pairs( vars_list ) do
  134. formspec = formspec .. ( i == 1 and ";" or "," ) .. minetest.formspec_escape( v .. " = " .. format_value( vars[ v ].value, vars[ v ].type ) )
  135. end
  136. formspec = formspec .. string.format( ";%d;false]", var_index )
  137. .. "label[9.0,5.4;Name:]"
  138. .. "label[9.0,5.9;Type:]"
  139. .. string.format( "label[10.5,5.4;%s]", minetest.colorize( "#BBFF77", "$" .. var_name ) )
  140. .. string.format( "label[10.5,5.9;%s]", datatypes[ var_type ] )
  141. .. "label[9.0,6.4;Value:]"
  142. .. "field[9.2,7.5;4.3,0.25;var_value;;" .. minetest.formspec_escape( format_value( var_value, var_type ) ) .. "]"
  143. .. "button[9.0,7.8;1,1;prev_var;<<]"
  144. .. "button[10.0,7.8;1,1;next_var;>>]"
  145. .. "button[11.8,7.8;1.5,1;set_var;Set]"
  146. if var_is_auto ~= nil then
  147. formspec = formspec .. "checkbox[10.5,6.2;var_is_auto;Auto Update;" .. tostring( var_is_auto ) .. "]"
  148. end
  149. if status then
  150. formspec = formspec .. "box[0.1,6.9;8.4,0.8;#555555]"
  151. .. "label[0.3,7.1;" .. minetest.colorize( status.type == "ERROR" and "#CCCC22" or "#22CC22", status.type .. ": " ) .. status.desc .. "]"
  152. if status.user then
  153. formspec = formspec .. "textlist[0.1,5.5;8.4,1.2;;Access denied. Reason: " .. minetest.formspec_escape( status.user ) .. ";0;false]"
  154. end
  155. end
  156. return formspec
  157. end
  158. local function on_close( meta, player, fields )
  159. login_index = ( { ["Normal"] = 1, ["New Account"] = 2, ["Wrong Password"] = 3 } )[ fields.login_mode ] or 1 -- sanity check
  160. if fields.quit then
  161. os.remove( minetest.get_worldpath( ) .. "/~greenlist.mt" )
  162. elseif fields.vars_list then
  163. local event = minetest.explode_textlist_event( fields.vars_list )
  164. if event.type == "CHG" then
  165. var_index = event.index
  166. minetest.update_form( name, get_formspec( fields.buffer ) )
  167. end
  168. elseif fields.has_prompt then
  169. has_prompt = fields.has_prompt == "true"
  170. elseif fields.has_output then
  171. has_output = fields.has_output == "true"
  172. elseif fields.export_ruleset then
  173. local buffer = clear_prompts( fields.buffer .. "\n", true )
  174. local file = io.open( minetest.get_worldpath( ) .. "/greenlist.mt", "w" )
  175. if not file then
  176. error( "Cannot write to ruleset definition file." )
  177. end
  178. file:write( buffer )
  179. file:close( )
  180. minetest.update_form( name, get_formspec( buffer, { type = "ACTION", desc = "Ruleset definition exported." } ) )
  181. elseif fields.import_ruleset then
  182. local file = io.open( minetest.get_worldpath( ) .. "/greenlist.mt", "r" )
  183. if not file then
  184. error( "Cannot read from ruleset definition file." )
  185. end
  186. minetest.update_form( name, get_formspec( file:read( "*a" ), { type = "ACTION", desc = "Ruleset definition imported." } ) )
  187. file:close( )
  188. elseif fields.process_ruleset then
  189. local status
  190. local buffer = clear_prompts( fields.buffer .. "\n", true ) -- we need a trailing newline, or things will break
  191. -- output ruleset to temp file for processing
  192. local temp_file = io.open( minetest.get_worldpath( ) .. "/" .. temp_name, "w" )
  193. temp_file:write( buffer )
  194. temp_file:close( )
  195. temp_filter.refresh( )
  196. update_vars( )
  197. if fields.login_mode == "New Account" then
  198. vars.is_new.value = true
  199. vars.privs_list.value = { }
  200. vars.lifetime.value = 0
  201. vars.sessions.value = 0
  202. vars.failures.value = 0
  203. vars.attempts.value = 0
  204. vars.newlogin.value = epoch
  205. vars.oldlogin.value = epoch
  206. else
  207. vars.is_new.value = false
  208. vars.attempts.value = vars.attempts.value + 1
  209. end
  210. -- process ruleset and benchmark performance
  211. local t = minetest.get_us_time( )
  212. local res, num, err = temp_filter.process( vars )
  213. t = ( minetest.get_us_time( ) - t ) / 1000
  214. if err then
  215. if has_prompt then buffer = insert_prompt( buffer, num, err ) end
  216. status = { type = "ERROR", desc = string.format( "%s (line %d).", err, num ), user = has_output and res }
  217. vars.ip_attempts.value = vars.ip_attempts.value + 1
  218. vars.ip_prelogin.value = vars.clock.value
  219. table.insert( vars.ip_names_list.value, vars.name.value )
  220. elseif res then
  221. if has_prompt then buffer = insert_prompt( buffer, num, "Ruleset failed" ) end
  222. status = { type = "ACTION", desc = string.format( "Ruleset failed at line %d (took %0.1f ms).", num, t ), user = has_output and res }
  223. vars.ip_attempts.value = vars.ip_attempts.value + 1
  224. vars.ip_prelogin.value = vars.clock.value
  225. table.insert( vars.ip_names_list.value, vars.name.value )
  226. elseif fields.login_mode == "Wrong Password" then
  227. if has_prompt then buffer = insert_prompt( buffer, num, "Ruleset failed" ) end
  228. status = { type = "ACTION", desc = string.format( "Ruleset passed at line %d (took %0.1f ms).", num, t ), user = has_output and "Invalid password" }
  229. vars.failures.value = vars.failures.value + 1
  230. vars.ip_attempts.value = vars.ip_attempts.value + 1
  231. vars.ip_failures.value = vars.ip_failures.value + 1
  232. vars.ip_prelogin.value = vars.clock.value
  233. vars.ip_newcheck.value = vars.clock.value
  234. if vars.ip_oldcheck.value == epoch then
  235. vars.ip_oldcheck.value = vars.clock.value
  236. end
  237. table.insert( vars.ip_names_list.value, vars.name.value )
  238. else
  239. if has_prompt then buffer = insert_prompt( buffer, num, "Ruleset passed" ) end
  240. status = { type = "ACTION", desc = string.format( "Ruleset passed at line %d (took %0.1f ms).", num, t ) }
  241. if fields.login_mode == "New Account" then
  242. vars.privs_list.value = get_default_privs( )
  243. end
  244. vars.sessions.value = vars.sessions.value + 1
  245. vars.newlogin.value = vars.clock.value
  246. if vars.oldlogin.value == epoch then
  247. vars.oldlogin.value = vars.clock.value
  248. end
  249. vars.ip_failures.value = 0
  250. vars.ip_attempts.value = 0
  251. vars.ip_prelogin.value = epoch
  252. vars.ip_oldcheck.value = epoch
  253. vars.ip_newcheck.value = epoch
  254. vars.ip_names_list.value = { }
  255. end
  256. minetest.update_form( name, get_formspec( buffer, status ) )
  257. elseif fields.next_var or fields.prev_var then
  258. local idx = var_index
  259. local off = fields.next_var and 1 or -1
  260. if off == 1 and idx < #vars_list or off == -1 and idx > 1 then
  261. local v = vars_list[ idx ]
  262. vars_list[ idx ] = vars_list[ idx + off ]
  263. vars_list[ idx + off ] = v
  264. var_index = idx + off
  265. minetest.update_form( name, get_formspec( fields.buffer ) )
  266. end
  267. elseif fields.var_is_auto then
  268. local var_name = vars_list[ var_index ]
  269. vars[ var_name ].is_auto = ( fields.var_is_auto == "true" )
  270. elseif fields.set_var then
  271. local oper = translate( string.trim( fields.var_value ), vars )
  272. local var_name = vars_list[ var_index ]
  273. if oper and var_name == "__debug" and datatypes[ oper.type ] then
  274. -- debug variable can be any value/type
  275. vars.__debug = oper
  276. elseif oper and oper.type == vars[ var_name ].type then
  277. vars[ var_name ].value = oper.value
  278. end
  279. minetest.update_form( name, get_formspec( fields.buffer ) )
  280. end
  281. end
  282. temp_filter.add_preset_vars( vars )
  283. vars.clock.is_auto = true
  284. update_vars( )
  285. minetest.create_form( nil, name, get_formspec( "pass now\n" ), on_close )
  286. return true
  287. end,
  288. } )
  289. minetest.register_chatcommand( "auth", {
  290. description = "Open the authentication database management console.",
  291. privs = { server = true },
  292. func = function( name, param )
  293. local base_filter = GenericFilter( )
  294. local epoch = os.time( { year = 1970, month = 1, day = 1, hour = 0 } )
  295. local is_sort_reverse = false
  296. local vars_list = { "username", "password", "oldlogin", "newlogin", "lifetime", "total_sessions", "total_failures", "total_attempts", "assigned_privs" }
  297. local columns_list = { "$username", "$oldlogin->cal('D-MM-YY')", "$newlogin->cal('D-MM-YY')", "$lifetime->when('h')", "$total_sessions->str()", "$total_attempts->str()", "$total_failures->str()", "$assigned_privs->join(',')" }
  298. local results_list
  299. local selects_list
  300. local var_index = 1
  301. local var_input = ""
  302. local select_index
  303. local select_input = ""
  304. local result_index
  305. local results_horz
  306. local results_vert
  307. local column_index = 1
  308. local column_macro = ""
  309. base_filter.define_func( "str", FILTER_TYPE_STRING, { FILTER_TYPE_NUMBER },
  310. function ( v, a ) return tostring( a ) end )
  311. base_filter.define_func( "join", FILTER_TYPE_STRING, { FILTER_TYPE_SERIES, FILTER_TYPE_STRING },
  312. function ( v, a, b ) return table.concat( a, b ) end )
  313. base_filter.define_func( "when", FILTER_TYPE_STRING, { FILTER_TYPE_PERIOD, FILTER_TYPE_STRING },
  314. function ( v, a, b ) local f = { y = 31536000, w = 604800, d = 86400, h = 3600, m = 60, s = 1 }; return f[ b ] and ( math.floor( a / f[ b ] ) .. b ) or "?" end )
  315. base_filter.define_func( "cal", FILTER_TYPE_STRING, { FILTER_TYPE_MOMENT, FILTER_TYPE_STRING },
  316. function ( v, a, b ) local f = { ["Y"] = "%y", ["YY"] = "%Y", ["M"] = "%m", ["MM"] = "%b", ["D"] = "%d", ["DD"] = "%a", ["h"] = "%H", ["m"] = "%M", ["s"] = "%S" }; return os.date( string.gsub( b, "%a+", f ), a ) end )
  317. local function get_record_vars( username )
  318. local rec = auth_db.select_record( username )
  319. return rec and {
  320. username = { value = username, type = FILTER_TYPE_STRING },
  321. password = { value = rec.password, type = FILTER_TYPE_STRING },
  322. oldlogin = { value = rec.oldlogin, type = FILTER_TYPE_MOMENT },
  323. newlogin = { value = rec.newlogin, type = FILTER_TYPE_MOMENT },
  324. lifetime = { value = rec.lifetime, type = FILTER_TYPE_PERIOD },
  325. total_sessions = { value = rec.total_sessions, type = FILTER_TYPE_NUMBER },
  326. total_failures = { value = rec.total_failures, type = FILTER_TYPE_NUMBER },
  327. total_attempts = { value = rec.total_attempts, type = FILTER_TYPE_NUMBER },
  328. assigned_privs = { value = rec.assigned_privs, type = FILTER_TYPE_SERIES },
  329. } or { username = { value = username, type = FILTER_TYPE_STRING } }
  330. end
  331. local function reset_results( )
  332. result_index = 1
  333. results_vert = 0
  334. results_horz = 0
  335. results_list = auth_db.search( false )
  336. select_index = 1
  337. selects_list = { { input = "(default)", cache = results_list } }
  338. end
  339. local function query_results( input )
  340. local stmt = string.split( base_filter.tokenize( input ), " ", false )
  341. if #stmt ~= 4 then
  342. return "Invalid 'if' or 'unless' statement in selector"
  343. end
  344. local cond = ( { ["if"] = FILTER_COND_TRUE, ["unless"] = FILTER_COND_FALSE } )[ stmt[ 1 ] ]
  345. local comp = ( { ["eq"] = FILTER_COMP_EQ, ["gt"] = FILTER_COMP_GT, ["lt"] = FILTER_COMP_LT, ["gte"] = FILTER_COMP_GTE, ["lte"] = FILTER_COMP_LTE, ["in"] = FILTER_COMP_IN, ["is"] = FILTER_COMP_IS, ["has"] = FILTER_COMP_HAS } )[ stmt[ 3 ] ]
  346. if not cond or not comp then
  347. return "Unrecognized keywords in selector"
  348. end
  349. -- initalize variables prior to loop (huge performance boost)
  350. local vars = {
  351. username = { type = FILTER_TYPE_STRING },
  352. password = { type = FILTER_TYPE_STRING },
  353. oldlogin = { type = FILTER_TYPE_MOMENT },
  354. newlogin = { type = FILTER_TYPE_MOMENT },
  355. lifetime = { type = FILTER_TYPE_PERIOD },
  356. total_sessions = { type = FILTER_TYPE_NUMBER },
  357. total_failures = { type = FILTER_TYPE_NUMBER },
  358. total_attempts = { type = FILTER_TYPE_NUMBER },
  359. assigned_privs = { type = FILTER_TYPE_SERIES },
  360. }
  361. base_filter.add_preset_vars( vars )
  362. local refs1, refs2, proc1, proc2, oper1, oper2
  363. local get_result = base_filter.get_result
  364. local get_operand_parser = base_filter.get_operand_parser
  365. local select_record = auth_db.select_record
  366. local res = { }
  367. for i, username in ipairs( results_list ) do
  368. local rec = select_record( username )
  369. if not rec then
  370. return "Attempt to index a non-existent record"
  371. end
  372. vars.username.value = username
  373. vars.password.value = rec.password
  374. vars.oldlogin.value = rec.oldlogin
  375. vars.newlogin.value = rec.newlogin
  376. vars.lifetime.value = rec.lifetime
  377. vars.total_sessions.value = rec.total_sessions
  378. vars.total_failures.value = rec.total_failures
  379. vars.total_attempts.value = rec.total_attempts
  380. vars.assigned_privs.value = rec.assigned_privs
  381. if not oper1 then
  382. -- get parser on first iteration
  383. if not proc1 then
  384. proc1, refs1 = get_operand_parser( stmt[ 2 ] )
  385. end
  386. oper1 = proc1 and proc1( refs1, vars )
  387. end
  388. if not oper2 then
  389. -- get parser on first iteration
  390. if not proc2 then
  391. proc2, refs2 = get_operand_parser( stmt[ 4 ] )
  392. end
  393. oper2 = proc2 and proc2( refs2, vars )
  394. end
  395. if not oper1 or not oper2 then
  396. return "Unrecognized operands in selector"
  397. end
  398. local expr = get_result( cond, comp, oper1, oper2 )
  399. if expr == nil then
  400. return "Mismatched operands in selector"
  401. end
  402. -- add matching records to results
  403. if expr then
  404. table.insert( res, username )
  405. end
  406. -- cache operands that are constant
  407. if not oper1.const then oper1 = nil end
  408. if not oper2.const then oper2 = nil end
  409. end
  410. result_index = 1
  411. results_list = res
  412. results_vert = 0
  413. select_index = select_index + 1
  414. table.insert( selects_list, select_index, { input = input, cache = results_list } )
  415. end
  416. local function format_value( oper )
  417. if oper.type == FILTER_TYPE_STRING then
  418. return "\"" .. oper.value .. "\""
  419. elseif oper.type == FILTER_TYPE_NUMBER then
  420. return tostring( oper.value )
  421. elseif oper.type == FILTER_TYPE_MOMENT then
  422. return "+" .. tostring( math.max( 0, oper.value - epoch ) ) .. "s"
  423. elseif oper.type == FILTER_TYPE_PERIOD then
  424. return tostring( math.abs( oper.value ) ) .. "s"
  425. elseif oper.type == FILTER_TYPE_SERIES then
  426. return "(" .. string.gsub( table.concat( oper.value, "," ), "[^,]+", "\"%1\"" ) .. ")"
  427. end
  428. end
  429. local function get_escaped_fields( username )
  430. local fields = { }
  431. local vars = get_record_vars( username )
  432. base_filter.add_preset_vars( vars )
  433. for i = 1 + results_horz, #columns_list do
  434. local oper = base_filter.translate( columns_list[ i ], vars )
  435. table.insert( fields, minetest.formspec_escape(
  436. oper and oper.type == FILTER_TYPE_STRING and oper.value or "?" )
  437. )
  438. end
  439. return fields
  440. end
  441. local function sort_results( )
  442. local cache = { }
  443. local field = vars_list[ var_index ]
  444. local select_record = auth_db.select_record
  445. for i, v in ipairs( results_list ) do
  446. local rec = select_record( v )
  447. if rec then
  448. cache[ v ] = ( field == "username" and v or field == "assigned_privs" and #rec[ field ] or rec[ field ] )
  449. end
  450. end
  451. table.sort( results_list, function ( a, b )
  452. local value1, value2 = cache[ a ], cache[ b ]
  453. -- deleted records are lowest sort order
  454. if not value1 then return false end
  455. if not value2 then return true end
  456. if is_sort_reverse then
  457. return value1 > value2
  458. else
  459. return value1 < value2
  460. end
  461. end )
  462. result_index = 1
  463. results_vert = 0
  464. end
  465. local function get_formspec( err )
  466. local fs = minetest.formspec_escape
  467. local horz = ( #columns_list > 1 and ( 1000 / ( #columns_list - 1 ) * results_horz ) or 0 )
  468. local vert = ( #results_list > 1 and ( 1000 / ( #results_list - 1 ) * results_vert ) or 0 )
  469. local formspec = "size[13.5,9.0]"
  470. .. default.gui_bg
  471. .. default.gui_bg_img
  472. .. "label[0.1,0.0;Results (" .. #results_list .. " Records Selected):]"
  473. .. "checkbox[6.5,-0.2;is_sort_reverse;Reverse Sort;" .. tostring( is_sort_reverse ) .. "]"
  474. .. "tablecolumns[color" .. string.rep( ";text,width=10", #columns_list - results_horz ) .. "]"
  475. .. "table[0.1,0.5;8.6,7.3;results_list;#66DD66"
  476. for i = 1 + results_horz, #columns_list do
  477. formspec = formspec .. "," .. fs( string.sub( columns_list[ i ], 1, 18 ) )
  478. end
  479. for i = 1 + results_vert, math.min( #results_list, 15 + results_vert ) do
  480. formspec = formspec .. ",#FFFFFF," .. table.concat( get_escaped_fields( results_list[ i ] ), "," )
  481. end
  482. formspec = formspec .. ";" .. result_index .. "]"
  483. .. "scrollbar[0.1,7.8;8.6,0.4;horizontal;results_horz;" .. horz .. "]"
  484. .. "scrollbar[8.7,0.5;0.37,7.2;vertical;results_vert;" .. vert .. "]"
  485. if err then
  486. formspec = formspec .. "box[0.1,8.4;7.8,0.7;#555555]"
  487. .. "label[0.3,8.5;" .. minetest.colorize( "#CCCC22", "ERROR: " ) .. fs( err ) .. "]"
  488. .. "button[8.1,8.3;1.2,1;okay;Okay]"
  489. else
  490. formspec = formspec .. "dropdown[0.1,8.4;2.4,1;var_index;" .. table.concat( vars_list, "," ) .. ";" .. var_index .. "]"
  491. .. "field[2.8,9.0;3.7,0.25;var_input;;" .. fs( var_input ) .. "]"
  492. .. "button[6.1,8.3;1,1;set_records;Set]"
  493. .. "button[7.0,8.3;1,1;del_records;Del]"
  494. .. "button[8.1,8.3;1.2,1;sort_records;Sort]"
  495. end
  496. formspec = formspec .. "label[9.4,0.0;Columns:]"
  497. .. "textlist[9.4,0.5;2.9,2.7;columns_list"
  498. for i, v in ipairs( columns_list ) do
  499. formspec = formspec .. ( i == 1 and ";" or "," ) .. fs( v )
  500. end
  501. formspec = formspec .. ";" .. column_index .. ";false]"
  502. .. "button[12.4,0.4;1,1;prev_column;<<]"
  503. .. "button[12.4,1.2;1,1;next_column;>>]"
  504. .. "button[12.4,2.0;1,1;del_column;Del]"
  505. .. "button[12.4,3.2;1,1;add_column;Add]"
  506. .. "field[9.7,3.9;3.1,0.25;column_macro;;" .. fs( column_macro ) .. "]"
  507. .. "label[9.4,4.6;Selectors:]"
  508. .. "textlist[9.4,5.1;3.8,2.3;selects_list"
  509. for i, v in ipairs( selects_list ) do
  510. formspec = formspec .. ( i == 1 and ";" or "," ) .. fs( v.input )
  511. end
  512. formspec = formspec .. ";" .. select_index .. ";false]"
  513. .. "field[9.7,8.1;4.0,0.25;select_input;;" .. fs( select_input ) .. "]"
  514. .. "button[9.4,8.3;1.4,1;reset_results;Clear]"
  515. .. "button[12.0,8.3;1.4,1;query_results;Query]"
  516. return formspec
  517. end
  518. local function on_close( meta, player, fields )
  519. -- check single-operation elements first
  520. if fields.okay then
  521. minetest.update_form( name, get_formspec( ) )
  522. elseif fields.is_sort_reverse then
  523. is_sort_reverse = ( fields.is_sort_reverse == "true" )
  524. elseif fields.columns_list then
  525. local event = minetest.explode_textlist_event( fields.columns_list )
  526. if event.type == "CHG" then
  527. column_index = event.index
  528. elseif event.type == "DCL" then
  529. column_macro = columns_list[ column_index ]
  530. minetest.update_form( name, get_formspec( ) )
  531. end
  532. elseif fields.selects_list then
  533. local event = minetest.explode_textlist_event( fields.selects_list )
  534. if event.type == "CHG" then
  535. select_index = event.index
  536. results_list = selects_list[ event.index ].cache
  537. results_vert = 0
  538. minetest.update_form( name, get_formspec( ) )
  539. elseif event.type == "DCL" and select_index > 1 then
  540. select_input = selects_list[ event.index ].input
  541. minetest.update_form( name, get_formspec( ) )
  542. end
  543. elseif fields.results_list then
  544. local event = minetest.explode_table_event( fields.results_list )
  545. if event.type == "CHG" then
  546. result_index = event.row
  547. elseif event.type == "DCL" and result_index > 1 then
  548. local vars = get_record_vars( results_list[ results_vert + result_index - 1 ] )
  549. local oper = vars[ vars_list[ var_index ] ]
  550. var_input = oper and format_value( oper ) or ""
  551. minetest.update_form( name, get_formspec( ) )
  552. end
  553. elseif fields.next_column or fields.prev_column then
  554. local idx = column_index
  555. local off = fields.next_column and 1 or -1
  556. if off == 1 and idx < #columns_list or off == -1 and idx > 1 then
  557. local v = columns_list[ idx ]
  558. columns_list[ idx ] = columns_list[ idx + off ]
  559. columns_list[ idx + off ] = v
  560. column_index = idx + off
  561. minetest.update_form( name, get_formspec( ) )
  562. end
  563. elseif fields.del_column then
  564. if #columns_list > 1 then
  565. table.remove( columns_list, column_index )
  566. column_index = math.min( column_index, #columns_list )
  567. results_horz = 0
  568. minetest.update_form( name, get_formspec( ) )
  569. end
  570. elseif fields.add_column and fields.column_macro then
  571. if string.match( fields.column_macro, "%S+" ) and #columns_list < 10 then
  572. table.insert( columns_list, string.trim( fields.column_macro ) )
  573. column_macro = ""
  574. column_index = #columns_list
  575. minetest.update_form( name, get_formspec( ) )
  576. end
  577. elseif fields.del_records then
  578. local delete_record = auth_db.delete_record
  579. if result_index == 1 then
  580. for i, username in ipairs( results_list ) do
  581. delete_record( username )
  582. end
  583. else
  584. delete_record( results_list[ results_vert + result_index - 1 ] )
  585. end
  586. minetest.update_form( name, get_formspec( ) )
  587. elseif fields.sort_records then
  588. sort_results( )
  589. minetest.update_form( name, get_formspec( ) )
  590. elseif fields.query_results and fields.select_input then
  591. if string.match( fields.select_input, "%S+" ) and #selects_list < 5 then
  592. local input = string.trim( fields.select_input )
  593. local err = query_results( input )
  594. select_input = ( not err and "" or input )
  595. minetest.update_form( name, get_formspec( err ) )
  596. end
  597. elseif fields.reset_results then
  598. reset_results( )
  599. select_input = ""
  600. minetest.update_form( name, get_formspec( ) )
  601. -- check dual-operation elements last
  602. elseif fields.results_horz and fields.results_vert then
  603. local horz_event = minetest.explode_scrollbar_event( fields.results_horz )
  604. local vert_event = minetest.explode_scrollbar_event( fields.results_vert )
  605. if horz_event.type == "CHG" then
  606. local offset = horz_event.value - 1000 / ( #columns_list - 1 ) * results_horz
  607. if offset > 10 then
  608. results_horz = #columns_list - 1
  609. elseif offset < -10 then
  610. results_horz = 0
  611. elseif offset > 0 then
  612. results_horz = results_horz + 1
  613. elseif offset < 0 then
  614. results_horz = results_horz - 1
  615. end
  616. minetest.update_form( name, get_formspec( ) )
  617. elseif vert_event.type == "CHG" then
  618. -- TODO: Fix offset calculation to be more accurate?
  619. local offset = vert_event.value - 1000 / ( #results_list - 1 ) * results_vert
  620. if offset > 10 then
  621. results_vert = math.min( #results_list - 1, results_vert + 100 )
  622. elseif offset < -10 then
  623. results_vert = math.max( 0, results_vert - 100 )
  624. elseif offset > 0 then
  625. results_vert = math.min( #results_list - 1, results_vert + 10 )
  626. elseif offset < 0 then
  627. results_vert = math.max( 0, results_vert - 10 )
  628. end
  629. result_index = 1
  630. minetest.update_form( name, get_formspec( ) )
  631. end
  632. var_index = ( { ["username"] = 1, ["password"] = 2, ["oldlogin"] = 3, ["newlogin"] = 4, ["lifetime"] = 5, ["total_sessions"] = 6, ["total_failures"] = 7, ["total_attempts"] = 8, ["assigned_privs"] = 9 } )[ fields.var_index ] or 1 -- sanity check
  633. end
  634. end
  635. reset_results( )
  636. minetest.create_form( nil, name, get_formspec( ), on_close )
  637. end,
  638. } )
  639. return function ( import )
  640. auth_db = import.auth_db
  641. auth_filter = import.auth_filter
  642. end