[Home] [Downloads] [Search] [Help/forum]

Gammon Software Solutions forum

See www.mushclient.com/spam for dealing with forum spam. Please read the MUSHclient FAQ!

[Folder]  Entire forum
-> [Folder]  Programming
. -> [Folder]  General
. . -> [Subject]  Writing dynamic web pages using Lua

Home  |  Users  |  Search  |  FAQ
Username:
Register forum user name
Password:
Forgotten password?
(New message)
Subject: Writing dynamic web pages using Lua
Name:
Your forum user name.
Register forum user name
Password:
Your forum password.
Forgotten password?
Message:
Message to be posted (in English, please).
Forum codes:
Check this if your message uses 'forum codes' or templates (auto-detected for new posts).
Forum codes Templates

Save this message ...


Subject review (reverse sequence)

Pages: 1 2  

Posted by Nick Gammon   Australia  (18,770 posts)  [Biography] bio   Forum Administrator
Date Sat 09 Sep 2006 10:03 PM (UTC)  quote  ]
Message
I also found, using thttpd, that with some clients they hung with large amounts being returned, if you didn't send another header (Connection: close), plus thttpd didn't identify itself or send the date. A couple of extra lines fixes that:

In cgiutils.lua, after the line:


 print ("Content-Type: text/html; charset=iso-8859-1")


... add these lines:


  print ("Date: " .. os.date ("%a, %d %b %Y %H:%M:%S GMT"))
  print ("Server: " .. "thttpd/2.25b")  --> or whatever it is
  print ("Connection: close")


- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (18,770 posts)  [Biography] bio   Forum Administrator
Date Fri 08 Sep 2006 04:30 AM (UTC)  quote  ]
Message
I have made a minor change to the cgiutils.lua file presented further back.

When testing it with the thttpd server it hung on the line which retrieved the post data. It seems that I should only be reading from stdin if the method is actually POST, otherwise it can just hang. This fixes it:



  if os.getenv ("REQUEST_METHOD") == "POST" then
    for _, v in ipairs (split (io.read ("*a"), "&")) do
      assemble_value (v, post_data)
    end -- for
  end -- if post


- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (18,770 posts)  [Biography] bio   Forum Administrator
Date Thu 11 May 2006 12:04 AM (UTC)  quote  ]
Message
This test script can be used in conjunction with the "Lua execute" page presented above. It is a slight variation on the "tprint" utility which is included with MUSHclient. It is a recursive table-printer. In this case it prints the _G table, showing all of the functions and variables made available in the sandbox to the script writer.


function tprint (t, indent, done)
  -- show strings differently to distinguish them from numbers
  local function show (val)
    if type (val) == "string" then
      return '"' .. val .. '"'
    else
      return tostring (val)
    end -- if
  end -- show
  -- entry point here
  done = done or {}
  indent = indent or 0
  for key, value in pairs (t) do
    io.write (string.rep (" ", indent)) -- indent it
    if type (value) == "table" and not done [value] then
      done [value] = true
      print(show (key), ":");
      tprint (value, indent + 2, done)
    else
      io.write (show (key), "=")
      print (show (value))
    end
  end
end


tprint (_G)



Output for me was:


Output


"string" : 
  "sub"=function: 0x8066758 
  "gfind"=function: 0x80668e8 
  "rep"=function: 0x80667c0 
  "find"=function: 0x80668b0 
  "char"=function: 0x8066790 
  "dump"=function: 0x8066878 
  "byte"=function: 0x8066960 
  "upper"=function: 0x8066840 
  "gsub"=function: 0x8066920 
  "format"=function: 0x8066998 
  "len"=function: 0x8066728 
  "lower"=function: 0x8066808 
"xpcall"=function: 0x8063d18 
"tprint"=function: 0x8072f80 
"table" : 
  "setn"=function: 0x8065308 
  "insert"=function: 0x80654d8 
  "getn"=function: 0x80653b8 
  "foreachi"=function: 0x8065380 
  "remove"=function: 0x8065510 
  "sort"=function: 0x8065340 
  "concat"=function: 0x80650b8 
  "foreach"=function: 0x80652d0 
"rawset"=function: 0x8063ca8 
"gcinfo"=function: 0x8063d90 
"tonumber"=function: 0x8064838 
"print"=function: 0x806b298 
"coroutine" : 
  "resume"=function: 0x80650f0 
  "yield"=function: 0x8065128 
  "status"=function: 0x8065078 
  "wrap"=function: 0x8065040 
  "create"=function: 0x8064fc8 
"os" : 
  "setlocale"=function: 0x8064aa8 
  "getenv"=function: 0x80649d8 
  "difftime"=function: 0x8064a38 
  "time"=function: 0x8064ae0 
  "clock"=function: 0x8065000 
  "date"=function: 0x8065470 
  "tmpname"=function: 0x8064b18 
"__pow"=function: 0x80671e0 
"math" : 
  "log"=function: 0x8066058 
  "atan"=function: 0x8067010 
  "ldexp"=function: 0x8067248 
  "deg"=function: 0x80670e0 
  "tan"=function: 0x8064e78 
  "cos"=function: 0x8064e48 
  "abs"=function: 0x8066bd8 
  "random"=function: 0x8067178 
  "rad"=function: 0x8067140 
  "frexp"=function: 0x8064f50 
  "pi"=3.1415926535898 
  "floor"=function: 0x8064ee8 
  "acos"=function: 0x8064df0 
  "max"=function: 0x80672e0 
  "sqrt"=function: 0x8067280 
  "atan2"=function: 0x8067048 
  "asin"=function: 0x8064db8 
  "min"=function: 0x80672b0 
  "mod"=function: 0x8064f18 
  "log10"=function: 0x8067080 
  "exp"=function: 0x80670b0 
  "pow"=function: 0x8067110 
  "randomseed"=function: 0x80671b0 
  "sin"=function: 0x8064da0 
  "ceil"=function: 0x8064eb0 
"_G" : 
  "string"=table: 0x8072f30 
  "xpcall"=function: 0x8063d18 
  "tprint"=function: 0x8072f80 
  "table"=table: 0x8072f58 
  "rawset"=function: 0x8063ca8 
  "gcinfo"=function: 0x8063d90 
  "tonumber"=function: 0x8064838 
  "print"=function: 0x806b298 
  "coroutine"=table: 0x8063b78 
  "os"=table: 0x8063c08 
  "__pow"=function: 0x80671e0 
  "math"=table: 0x8063be0 
  "_G"=table: 0x80686f0 
  "pcall"=function: 0x8063ce0 
  "_VERSION"="Lua 5.0.2" 
  "unpack"=function: 0x8064918 
  "type"=function: 0x80648a8 
  "pairs"=function: 0x80647c8 
  "next"=function: 0x8064758 
  "_TRACEBACK"=function: 0x8067898 
  "assert"=function: 0x80648e0 
  "tostring"=function: 0x8064870 
  "ipairs"=function: 0x8064790 
  "loadstring"=function: 0x8063e38 
  "io" : 
    "write"=function: 0x806b2b8 
  "rawequal"=function: 0x8064950 
  "rawget"=function: 0x8064988 
  "collectgarbage"=function: 0x8063d58 
  "setmetatable"=function: 0x80646b0 
  "getmetatable"=function: 0x8064670 
  "error"=function: 0x8064630 
"pcall"=function: 0x8063ce0 
"_VERSION"="Lua 5.0.2" 
"unpack"=function: 0x8064918 
"type"=function: 0x80648a8 
"pairs"=function: 0x80647c8 
"next"=function: 0x8064758 
"_TRACEBACK"=function: 0x8067898 
"assert"=function: 0x80648e0 
"tostring"=function: 0x8064870 
"ipairs"=function: 0x8064790 
"loadstring"=function: 0x8063e38 
"io"=table: 0x8065d58 
"rawequal"=function: 0x8064950 
"rawget"=function: 0x8064988 
"collectgarbage"=function: 0x8063d58 
"setmetatable"=function: 0x80646b0 
"getmetatable"=function: 0x8064670 
"error"=function: 0x8064630 

Completed OK 

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (18,770 posts)  [Biography] bio   Forum Administrator
Date Wed 10 May 2006 10:42 PM (UTC)  quote  ]

Amended on Fri 12 May 2006 04:28 AM (UTC) by Nick Gammon

Message
How to write a page that executes user-entered Lua code

This is an interesting project. I often want to experiment with Lua snippets but am not sitting at my PC with Lua installed. This page here demonstrates how you can let someone execute Lua code from their web browser.

To make it fairly robust, it has a number of interesting features:


  • Each time you run it, it remembers what code you entered last time (so you can fix syntax errors)

  • Any output from the executed code (via the 'print' statement) is output to the page, however with HTML properly escaped (that is, "<" turned into "&lt;" and so on)

  • A syntax error during the compile phase is shown as such

  • Runtime errors are detected and shown with a stack traceback

  • To hopefully stop malicious code, the entire code is executed in a specially-constructed "sandbox" which has most of the library Lua functions in it, however omitting things like "os.execute" and "os.remove" which might cause grief.

  • To guard against runaway code (like "repeat until false") hanging the web server, there is code to detect that.

  • To guard against someone writing thousands of lines of output, and making a web page that browsers probably could not load, there is code to stop that (built into the "print" function)

  • To guard against someone writing thousands of bytes, without starting a new line, there is also a test on the number of bytes printed.

  • Added a simulated io.write function to help people who want to build up an output line in pieces.


Here is the code (I called it "execute.lua" which is referred to internally in the <form> data) ...


-- limits
runaway_instruction_limit = 100000  -- how many instructions
linelimit = 500                     -- how many lines can be printed
outputlimit = 10000                 -- how many bytes can be printed

-- this is called if 'runaway_instruction_limit' instructions are done
function hook ()
  error ("Runaway instruction limit reached")
end -- hook

-- heading for errors
function heading (s)
  print '<p><span style="font-family: sans-serif; font-weight: bolder;">'
  print (fixhtml (s))
  print '</span></p>'
end -- heading

linecount = 0    -- how many lines they have printed so far
outputcount = 0  -- how many bytes they have printed so far

-- local print function for use inside executed code, uses fixhtml on the string
-- does a newline at the end, adds a space between arguments
function myprint (...)
  local i
  
  -- check for runaway outputting
  linecount = linecount + 1  
  assert (linecount <= linelimit, "too many lines printed")
  
  -- I am not using ipairs because that stops on a nil argument
  for i = 1, arg.n do
    local s = fixhtml (tostring (arg [i]))
    outputcount = outputcount + string.len (s) + 1
    assert (outputcount <= outputlimit, "too many bytes printed")
    io.write (s .. " ")
  end -- for
  print ""  -- final end of line
  
end -- myprint

-- local io.write function for use inside executed code, uses fixhtml on the string
-- doesn't do a newline, doesn't add a space between arguments
function mywrite (...)
  local i

  -- I am not using ipairs because that stops on a nil argument
  for i = 1, arg.n do
    local s = fixhtml (tostring (arg [i]))
    outputcount = outputcount + string.len (s)
    assert (outputcount <= outputlimit, "too many bytes printed")
    io.write (s)
  end -- for
end -- mywrite

-- makes a sandbox to make execution safer
function MakeSandbox ()
  local box = {}
  local _, name

  -- add in global functions we consider safe
  for _, name in ipairs 
  {
   -- functions 
   "_TRACEBACK", "__pow", -- helper functions
   "assert", "error", -- error management
   "collectgarbage", "gcinfo", -- garbage collection
   "getmetatable", "setmetatable", -- metatables
   "ipairs", "pairs", "next", -- looping through tables
   "loadstring", "pcall", "xpcall", -- calling functions
   "rawequal", "rawget", "rawset", -- raw table management 
   "tonumber", "tostring", "type", -- type management
   "unpack", -- argument management
   
   -- version string
   "_VERSION",
   }
   do
    box [name] = _G [name]
   end -- for each global function
    
   box._G = box  -- _G points to itself
   box.print = myprint -- special print function escapes HTML codes
   box.io = {}
   box.io.write = mywrite -- my special write function
   
  -- now other libraries
  -- we will omit io - don't want file io
  -- also omit debug, that could be dangerous to play with
  for _, name in ipairs 
  { "coroutine", "math", "os", "string", "table", }
   do
     box [name] = {}  -- make library sub-table
     
     -- copy functions into the table
     local k, v
     for k, v in pairs (_G [name]) do
       box [name] [k] = v
     end -- for each entry in the library table 
   end -- for each library
  
  -- omit a couple of the os functions we don't want them to play with
  box.os.execute = nil  -- no executing arbitrary code
  box.os.exit = nil     -- don't terminate us
  box.os.remove = nil   -- don't remove files
  box.os.rename = nil   -- don't rename files
  box.os.getenv = nil   -- don't leak information about our environment
    
  return box

end -- MakeSandbox

-- executes the code they entered, in a "sandbox" environment
function Execute (code)

  local ok
  
  -- this is called for a runtime error
  local function error_handler (err)
    heading ("Runtime error")
    print (fixhtml (err))
    local s = debug.traceback ()
    -- strip traceback from the xpcall upwards
    s = string.gsub (s, "%[C%]%: in function `xpcall'.*", "")
    print (fixhtml (s))
  end -- function error_handler
  
  -- try compiling the code
  local f, err = loadstring (code, "Code")
  
  -- error on compile
  if not f then
    heading ("Syntax error")
    print ("<pre><code>" .. fixhtml (err) .. "</code></pre>")
    return  -- cannot execute it
  end -- syntax error

  -- sandbox them so they don't start deleting files
  setfenv (f, MakeSandbox ())
  
  -- put limit on runaway loops
  debug.sethook (hook, "", runaway_instruction_limit)
  
  -- try running the code
  heading ("Output")
  print ("<pre><code>")
  ok = xpcall (f, error_handler)
  print ("</code></pre>")

  return ok
end -- function Execute

-->>>> CODE STARTS HERE <<<<--

-- HTTP and HTML headers
show_html_header ("Execute Lua code")
  
print [[
<form METHOD="post" ACTION="execute.lua">
<p>Enter Lua code to be executed:</p> 
<p>
<textarea name="code" wrap="virtual" rows="20" cols="80" >]]
io.write (fixhtml (post_data.code or ""))  -- what they had last time
print [[
</textarea>
</p>
<input Type=submit Name=Submit Value="Execute">
</form>
]]

-- now execute it

if post_data.code then
  if Execute (string.gsub (post_data.code, "\r", "")) then
    heading "Completed OK"
  end -- if finished ok
end -- if

show_html_footer ()



This code assumes you are using the method described earlier in this thread of executing using cgiutils.lua. This is the URL I used to test it:


http://10.0.0.2/cgi-bin/cgiutils.lua/testing/execute.lua


If the executed code completes successfully you see the heading "Completed OK", to confirm the script finished.

I have made a couple of edits to the code, taking out getfenv and setfenv, on the grounds that they might be used for escaping from the sandbox environment.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (18,770 posts)  [Biography] bio   Forum Administrator
Date Fri 05 May 2006 04:55 AM (UTC)  quote  ]

Amended on Fri 05 May 2006 07:51 AM (UTC) by Nick Gammon

Message
Making writing HTML easier

Now that we have got this far, let's make generating the actual HTML easier. One thing page designers do an awful lot of is stuff like this:


<h1> My page heading &amp; other stuff </h1>


It would be nice to simplify this. We need to group together:


  • The opening tag (eg. <h1> )
  • The text in the middle, escaping out things like <, > and &
  • The closing tag (eg. </h1> )


A simple function will do this:


function tag (name, contents)

  print ("<" .. name .. ">" .. 
         fixhtml (contents) .. "</" .. name .. ">")

end -- function tag


We can call it like this:

tag ("h1", "My page heading & other stuff")

However, this function make a couple of assumptions:


  • That the tag doesn't have arguments (like align="left")
  • That the contents are a simple string


For more complex things like tables, this isn't really true.

Let's make a more complex function, that will still handle the simple case, but also handles tag arguments, nested tags, and so on.

This is my next attempt:


function tag (name, ...)

local i = 1  -- 2nd argument may be parameters, or simply the thing to be displayed

  io.write ("<" .. name)

  -- handle parameters (like align="left")
  if type (arg [i]) == "table" then
    for k, v in pairs (arg [i]) do
      -- boolean attributes (eg. selected=true) have to be shown simply as "selected" 
      -- or omitted if false
      if type (v) == "boolean" then
        if v then
          io.write (' ' .. k)
        end -- value is true
      else      
        io.write (' ' .. k .. '="' .. fixhtml (v) .. '"')
      end -- if boolean or not
    end -- for each parameter 
    i = i + 1
  end -- tag parameters supplied
  
  -- if no contents assume closed tag
  if not arg [i] then
    print ("/>")
    return
  end -- no argument - must be tag with no contents (eg. <br/>)

  -- finished the opening tag
  io.write (">")
  
  -- now the contents of the tag
  -- if a function, call it, passing it any further arguments
  
  if type (arg [i]) == "function" then
    local f = arg [i]
    local j
    -- remove elements up to the function arguments
    for j = 1, i do
      table.remove (arg, 1)
    end -- removing argument
    print ()  -- may as well have a newline for neatness
    f (unpack (arg))
  else
    io.write (fixhtml (arg [i]))
  end -- if
  
  -- close the tag (and do the newline)
  print ("</" .. name .. ">")

end -- function tag




The first argument to "tag" is still the name of the tag (eg. hr, table, tr, th).

The next argument can optionally be a table of parameters. If the second argument is a table, it is expanded out, as arguments to the tag. For example:


tag ("hr", {size=10, width=110, align="left"} )


This function call generates this line:


<hr align="left" width="110" size="10"/>


In this case the absence of any further arguments generates an "empty" tag, that is one with no contents.

Now let's try something with contents.



tag ("p", {align="center"}, "This is my paragraph" )


This function call generates this line:


<p align="center">This is my paragraph</p>


Now we have the opening and closing tag.

Finally we can call a function rather than have straight text. This allows us to nest calls, to do something like iterate through a table. Here is an example:


function showrow (k, v) 
  tag ("th", k)
  tag ("td", v)
end -- showrow

function showtable (t)
  for k, v in ipairs (t) do
    tag ("tr", {align="left"}, showrow, k, v)  
  end -- for
end -- showtable

tag ("table",  {border = 1, cellpadding = 5}, 
     showtable, 
     { "every", "good", "boy", "deserves", "fruit" }
    ) 


This example uses the tag function a number of times. The last one (the one that is called first) sets up a table with the "table" tag, and requests that the "showtable" function be called. The arguments to showtable are the inlined table.

The function showtable then iterates through each table item, doing a "tr" tag, and for each one calls "showrow", passing to that two arguments, the key and value for that particular row.

Finally the function showrow calls tag twice, to do the "th" and "td" data elements. The generated HTML from all this is;


<table border="1" cellpadding="5">
<tr align="left">
<th>1</th>

<td>every</td>
</tr>
<tr align="left">
<th>2</th>
<td>good</td>
</tr>
<tr align="left">
<th>3</th>
<td>boy</td>
</tr>
<tr align="left">
<th>4</th>

<td>deserves</td>
</tr>
<tr align="left">
<th>5</th>
<td>fruit</td>
</tr>
</table>





Overall I think that the tag function simplifies the generation of HTML, including complex nested calls, and ensures that each opening tag is balanced by a closing tag.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (18,770 posts)  [Biography] bio   Forum Administrator
Date Thu 04 May 2006 11:31 PM (UTC)  quote  ]
Message
Making the URL look nicer

One minor problem with the technique described above is that the URL that the web page user sees looks a bit strange, like this:


http://www.yoursite.com/cgi-bin/cgiutils.lua/testing/test12.lua


The "problem" is the two lots of ".lua" items in the URL. It works, but looks strange. The simple solution is to simply rename cgiutils.lua as something that looks like a directory, eg. "scripts". Then the URL becomes:


http://www.yoursite.com/cgi-bin/scripts/testing/test12.lua


Now you are hiding from the users of your web site that the script is really the file "scripts". It just makes the URL look a bit more natural.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (18,770 posts)  [Biography] bio   Forum Administrator
Date Thu 04 May 2006 01:19 AM (UTC)  quote  ]

Amended on Thu 04 May 2006 01:20 AM (UTC) by Nick Gammon

Message
Example of using cookies

This page (test13.lua) demonstrates getting a cookie value, and setting it from a form. This is a simple demonstration of how you can use a cookie to remember a value from one time the user goes to a page, to another (like in this forum, when you log on).


-- Example of using cookies

-- if they changed it use the new value, otherwise take the cookie value
foo = post_data.foo or cookie_data.foo

-- HTTP header
show_http_header { foo = foo }

-- HTML header
show_html_header ("Cookie example")
  
-- Page header
print "<h1>Testing setting a cookie</h1>"
 
-- let them enter a new cookie value
print [[
<form METHOD="post" ACTION="test13.lua">
<p>Enter cookie value: <input type=text Name="foo" size=20 maxlength=20
]]
if foo then
  print ('value="' .. foo .. '"')
end -- if cookie already set
print [[
> 
<input Type=submit Name=Submit Value="Show">
</p>
</form>
]]



Since cookies are sent as part of the HTTP header, they must be changed (if you are planning to set or change them) as the very first thing you do, before sending any other HTML code. Thus the first thing this script does is set the variable "foo" to be either the post data (in case it has just been changed) or the cookie data (from last time).

Then we call:


show_http_header { foo = foo }


The optional argument to show_http_header lets you specify a table of cookie name/value pairs. You could enhance this by adding expiry dates etc., but for simplicity I haven't here.

Then the page displays a form showing the current value and lets you change it.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (18,770 posts)  [Biography] bio   Forum Administrator
Date Wed 03 May 2006 09:49 PM (UTC)  quote  ]

Amended on Wed 03 May 2006 09:51 PM (UTC) by Nick Gammon

Message
Accessing the GET and POST data to display an individual record

The next example (which implements test12.lua which is linked to in the previous post) shows how you can access the variable data which the user chooses.

The important line in this file is:


show_widget (post_data.id or get_data.id)


The tables post_data and get_data have been automatically set up by cgiutils.lua before loading this page. If the user clicks on the "Gyro spanner", then this is the hyperlink:


http://10.0.0.2/cgi-bin/cgiutils.lua/testing/test12.lua?id=1


The important part here is "?id=1" which puts the value "1" into the key "id" in the get_data table. Recall that get_data is the data from the URL.

However the displayed page also lets the user enter another widget ID by entering the number into the form box and clicking on the "Show" button. In this case the widget id ends up in the post_data table. The expression "post_data.id or get_data.id" used above means we prefer the post_data to the get_data, if there is a clash.

The function show_widget:


  • Exits if no widget supplied (ie test12.lua called with no id on the URL or form data)

  • Checks the widget id is numeric (to avoid problems with the SQL syntax)

  • Opens the database

  • Locates the desired widget with a SELECT statement

  • Displays an error if that widget is not on file

  • Otherwise displays its data

  • Closes the database


The main script calls show_widget and then displays a form to let the user choose a different widget ID (even if the previous one was not there).


function show_widget (which)
  
  -- exit if nothing
  if not which then
    return
  end -- no widget ID supplied

  -- check a number
  local id = tonumber (which)
  
  if not id then
    show_error (string.format ("Widget ID '%s' is not numeric", tostring (which)))
    return
  end -- not numeric
  
  -- load the MySQL dll
  assert (loadlib ("/usr/local/lib/libmysql.2.0.1.so", "luaopen_luasqlmysql")) ()
  
  -- create environment object
  local env = assert (luasql.mysql())
  
  -- connect to data source
  local con = assert (env:connect ("databasename", "username", "password", "localhost"))
  
  -- retrieve a cursor
  local cur = assert (con:execute ("SELECT * from widgets WHERE widget_id = " .. id))
  
  -- fetch the row
  local row = cur:fetch ({}, "a")
  
  if row then
    show_table (row)
  else
    show_error (string.format ("Widget %i not on file", id))
  end -- if
  
  -- close everything
  cur:close()
  con:close()
  env:close()  

end -- show_widget

-- HTTP and HTML headers
show_html_header ("Form example")
  
-- Page header
print "<h1>Display one widget</h1>"

-- show requested record  
show_widget (post_data.id or get_data.id)
 
-- let them enter a new ID
print [[
<form METHOD="post" ACTION="test12.lua">
<p>Enter Widget ID: <input type=text Name="id" size=6 maxlength=6>
<input Type=submit Name=Submit Value="Show">
</p>
</form>
]]

-- wrap up page 
show_html_footer ()




Example output:


Display one widget

widget_id	1
price	102
widget_name	Gyro spanner
weight	15

Enter Widget ID: ______ [Show]

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (18,770 posts)  [Biography] bio   Forum Administrator
Date Wed 03 May 2006 08:55 PM (UTC)  quote  ]

Amended on Wed 03 May 2006 09:07 PM (UTC) by Nick Gammon

Message
Accessing the MySQL database from the dynamic web page

Now let's look at accessing a database. This is simple enough, using the luasql library available from the LuaForge site. Assuming we have installed the shared library, we use loadlib to load it into our dynamic page. I have made up an example database using these SQL commands:


CREATE TABLE widgets (
  widget_id int NOT NULL auto_increment primary key,
  widget_name varchar(64) NOT NULL,
  weight int,
  price double
  ) 

INSERT INTO widgets VALUES (1,'Gyro spanner',15,102);
INSERT INTO widgets VALUES (2,'Packet of things',22,14);
INSERT INTO widgets VALUES (3,'Roll of paper',42,95);



The page below will display the first 100 records as a table.

This page makes the widget_id a hyperlink, so we can see more details about an individual widget if we click on the link. The page assumes you are calling it using the method described earlier using the cgiutils.lua as the calling module. The URL I used was:


http://10.0.0.2/cgi-bin/cgiutils.lua/testing/test11.lua


Thus, the file below was called test11.lua and placed in the "testing" subdirectory below where cgiutils.lua resides.


-- load the MySQL dll
assert (loadlib ("/usr/local/lib/libmysql.2.0.1.so", "luaopen_luasqlmysql")) ()

-- HTTP and HTML headers
show_html_header ("Database example")
  
-- Page header
print "<h1>Listing of Widgets</h1>"
  
-- create environment object
env = assert (luasql.mysql())

-- connect to data source
con = assert (env:connect ("databasename", "username", "password", "localhost"))

-- retrieve a cursor
cur = assert (con:execute ("SELECT * from widgets LIMIT 100" ))

-- table header row
print [[
<table border=1 cellpadding=3>
<tr>
<th>ID</th><th>Name</th>
</tr>
]]


-- print all rows, the rows will be indexed by field names
row = cur:fetch ({}, "a")

while row do
  
  -- start new row
  print "<tr>"

  print (string.format (
          '<td><a href="test12.lua?id=%i">%i</a></td>', 
          row.widget_id, 
          row.widget_id))
   
  -- Widget name
  print (string.format (
         '<td>%s</td>', 
         fixhtml (row.widget_name)))

  -- end of row
  print "</tr>"
         
  -- reusing the table of results
  row = cur:fetch (row, "a")
  
end

-- end of table
print "</table>"

-- close everything
cur:close()
con:close()
env:close()  
  
-- wrap up page 
show_html_footer ()


Output was (however displayed as an HTML table):


Listing of Widgets

ID	Name
1 	Gyro spanner
2 	Packet of things
3 	Roll of paper


- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Nick Gammon   Australia  (18,770 posts)  [Biography] bio   Forum Administrator
Date Tue 02 May 2006 05:54 AM (UTC)  quote  ]

Amended on Wed 03 May 2006 06:43 AM (UTC) by Nick Gammon

Message

Well, I try *grin*.

I have done my own documentation on Lua (version 5.0.2) base, coroutines, debug, io, math, os, string, and table functions.


- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by Sylaer   (20 posts)  [Biography] bio
Date Tue 02 May 2006 05:20 AM (UTC)  quote  ]
Message
Nick-
I grinned when you mentioned the same issues that griped me when I was learning PHP (which, incidentally, was before I started learning C). A year or two ago I could be seen at my desk with my nose to the screen in frustration trying to figure out how many escape characters to put into a string to store it in a mySQL database. (I got up to eight once, no kidding. Strangely, it worked.) Dumping cookie and form variables into the url was a pain too, and ultimately, I think, what caused me to give up PHP as a gaming language. Thus I find myself here.
Anyway, I suspect you'll one day be an authority on Lua, and again it's great to have you here. In that spirit, please continue your discussion on Lua :D
[Go to top] top

Posted by Nick Gammon   Australia  (18,770 posts)  [Biography] bio   Forum Administrator
Date Mon 01 May 2006 11:24 PM (UTC)  quote  ]

Amended on Tue 02 May 2006 05:52 AM (UTC) by Nick Gammon

Message
Quote:

For what it's worth, congratulations on all your hard work :)


Thanks for the complimentary remarks. I am pleased that the forum in general attracts a high standard of posts, there are quite a few knowledgeable and helpful people posting regularly here. :)

Quote:

I have to ask though, with your extensive PHP background, why learn Lua at all?


That is a good question. I should make it clear that so far my Lua web development is at an experimental stage, all of this site here is still in PHP. However, to answer in more detail, here are some considerations:


  • When I first started developing dynamic web pages, I hadn't heard of Lua (it may not have existed then), so I chose the language which was well-known, and fairly close in syntax to C, namely PHP.

  • There is extensive documentation about developing web pages in PHP, virtually none about doing it in Lua. There is indeed a project (now) for doing that, CGILua, however I found the lack of examples on their pages made it very hard to get things going. I am starting to understand more what they are doing, now that I have done a similar thing myself. To give credit to the "official" project, I refer you to this page:


    http://www.keplerproject.org/cgilua/


  • The further I went into adding Lua into MUSHclient, and also using it as a stand-alone tool for other things (like converting old emails from one format to another), the more I liked Lua.

  • Lua has a fundamental simple elegance that appeals to me. The core language is simple (by design) and largely absent of frills. The designers have made a conscious effort to keep Lua minimal, as they have made it simple (and well documented) to add your own extensions by writing your own DLLs (or shared libraries in Unix).

    On my Linux PC, the comparable sizes of Lua to PHP are:


    • Lua: 109 Kb PHP: 1.3 Mb


    On my Windows PC, the comparable sizes of Lua to PHP are:


    • Lua: 104 Kb PHP: 4.07 Mb


  • Lua can be extended easily (for example, you can add MySQL database queries by simply loading a shared library).

  • Various design decisions about PHP that annoy me, but I had learnt to live with, see below.



I don't think PHP is going to be replaced in a long time, if ever. Hundreds of thousands of web pages are developed in it, it has extensive helper functions built in, lots of documentation, and a very good support web site.




Things about PHP that annoy me

The things described below are not bugs, they are design decisions by the PHP developers. As such, PHP is working as designed, and there are always arguments about whether or not a design decision is correct. Much can probably be said in defence of the original design. However, here are my comments about them:



  • The decision (initially) to dump all of the get, post, and cookie variables into the PHP global address space. This was for the ease of programming, so that, if we take this URL as an example:


    http://10.0.0.2/myfile.php?type=meat&date=Friday


    PHP would set global variables:


    $type = "meat";
    $date = "Friday";


    This was certainly convenient, but it could be confusing if you needed to distinguish a variable that came from a cookie, to one that came from the URL. Also, it made scripts a bit more vulnerable to attacks like this:


    http://10.0.0.2/myfile.php?administrator=1


    If the script incautiously assumed that administrator would default to 0, and only set it to 1 if certain checks were passed, then users could assume administrator status (set the global administrator variable) by simply setting it on their URL.

  • The decision to make all variables in a function local, unless specified as global, which is the reverse to what most languages do. For example:

    
    
    Lua
    
    function test ()
      foo = 22 -- sets global foo
      
      local bar
      
      bar = 42  -- sets local bar
    end -- test
    
    PHP
    
    function test ()
      {
      $foo = 22; // sets local foo, global foo is unchanged
      
      global $bar;
      
      $bar = 42; // sets global bar
      }
    


    Personally I think this is messy. Most functions will want to access lots of global variables, but may occasionally use a local variable for looping, or calculations. This decision forces PHP developers to put heaps of "global this-and-that" at the start of each function. If you forget one, or add a new line to a script later on, it will not work correctly. Also, doing something simple like taking a piece of a function and making it into its own function (for readability) won't work by doing a simple copy and paste, unless you carefully inspect it for any references to global variables.

  • The decision (initially) to add slashes automatically to variables from web pages. Thus if you had in a form:


    Nick's cat


    The script actually received:


    Nick\'s cat


    This extra backslash was intended to be helpful if you were going to write the variable to an SQL database, where it would be needed to avoid premature closing of quotes. However if you were not going to do that, you needed to "strip the slashes", and then add them back later.

    With extra slashes being added "behind the scenes" scripts soon take on a nightmare proportion of adding and subtracting slashes, in ways which seem unbalanced (because some are implicit, not explicit).

    Making things worse, this became an option, so that scripts that work on one site stop working on another because the option was reversed.


  • The requirement that all variables must start with a $, which grates with programmers who are used to C. In other words you must do:


    $foo = $bar;

    // not -->

    foo = bar;


    Symbols without the $ prefix are also, rather bizarrely, treated as strings, like this:


    echo hello; // echoes "hello"


    This leads to rather confusing error messages if you forget the $ prefix.


  • The decision to allow variables inside strings, like this:


    $foo = "world";
    $bar = "hello, $foo";


    Again, this is convenient, but what if:


    • You need to put a $ sign inside the string?
    • You need to follow the variable directly by a letter (eg. "hello, worlds"), then this won't work:



    $foo = "world";
    $bar = "hello, $foos";


    In this example we don't get "hello, worlds", because it is trying to find the variable "foos".

    I know you can use single-quoted strings to avoid this behaviour, but it is still a bit messy.


- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

Posted by David Haley   USA  (3,881 posts)  [Biography] bio   Moderator
Date Mon 01 May 2006 09:36 PM (UTC)  quote  ]
Message
Lua is a much more elegant language than PHP. In fact, PHP as a language has a number of issues with it; issues of inconsistency, hastily implemented features, etc. Of course, PHP has a much more extensive set of helper functions specifically designed for web scripting, but Lua is slowly developing its own set of modules.

In any case, a lot of it really is that Lua is a much nicer language, with much more precise, consistent, and elegant language structures.

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
[Go to top] top

Posted by Sylaer   (20 posts)  [Biography] bio
Date Mon 01 May 2006 09:11 PM (UTC)  quote  ]
Message
First of all, it's extremely encouraging to share a forum with someone of your calibre. I've heard mention of you before and didn't realize until now that YOU were that guy. For what it's worth, congratulations on all your hard work :)
I have to ask though, with your extensive PHP background, why learn Lua at all? Admittedly I know little about Lua, but PHP seems to be an ideal dynamic web code. Had to wonder what attracted you to it.
[Go to top] top

Posted by Nick Gammon   Australia  (18,770 posts)  [Biography] bio   Forum Administrator
Date Mon 01 May 2006 07:16 AM (UTC)  quote  ]

Amended on Tue 12 Sep 2006 06:41 AM (UTC) by Nick Gammon

Message
Enhanced cgiutils.lua file

Below is the improved cgiutils.lua file. It is similar to the earlier one, with extra functions for automating the production of the http and html headers and footers.


#! /usr/local/bin/lua

-- Lua utilities for CGI web use

-- Written by Nick Gammon - May 2006.

--[[
Copyright © 2006 Nick Gammon.

Permission is hereby granted, free of charge, to any person obtaining a copy 
of this software and associated documentation files (the "Software"), to deal 
in the Software without restriction, including without limitation the rights 
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
copies of the Software, and to permit persons to whom the Software is 
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.    

The software is provided "as is", without warranty of any kind, express or 
implied, including but not limited to the warranties of merchantability, 
fitness for a particular purpose and noninfringement. In no event shall the 
authors or copyright holders be liable for any claim, damages or other 
liability, whether in an action of contract, tort or otherwise, arising from, 
out of or in connection with the software or the use or other dealings in the 
software. 
--]]

-- global flags

done_http_header = false  -- have we sent the HTTP header yet?
done_html_header = false  -- have we sent the HTML header yet?
done_html_footer = false  -- have we sent the HTTP footer yet?

-- split a string with delimiters into a table (reverse of table.concat)
function split (s, delim)

  local t = {}  -- results table

  if s then   
    assert (type (delim) == "string" and string.len (delim) > 0,
            "bad delimiter")
  
    local start = 1
    local pos
  
    -- find each instance of a string followed by the delimiter
  
    repeat
      pos = string.find (s, delim, start, true) -- plain find
  
      if pos then
        table.insert (t, string.sub (s, start, pos - 1))
        start = pos + string.len (delim)
      end
    until not pos
  
    -- insert final one (after last delimiter)
  
    table.insert (t, string.sub (s, start))
  end -- not nil string
  
  return t
 
end -- function split

-- trim leading and trailing spaces from a string
function trim (s)
  return (string.gsub (string.gsub (s, "%s+$", ""), "^%s+", ""))
end -- trim

mysql_replacements = { 
   ["\0"] = "\\0",
   ["\n"] = "\\n",
   ["\r"] = "\\r",
   ["\'"] = "\\\'",
   ["\""] = "\\\"",
   ["\026"] = "\\Z",
   ["\b"] = "\\b",
   ["\t"] = "\\t",
   }

-- Fix SQL text by converting various characters to the format MySQL 
--  will recognise in its string processor
--
-- Note that not all the escapes are necessary for internal SQL use, 
-- however if data is being dumped to disk (eg. as SQL statements) 
-- then it is handy for have things like \n and \r made more readable
--   See: http://dev.mysql.com/doc/refman/5.1/en/string-syntax.html

function fixsql (s)

  return (string.gsub (tostring (s), "[%z\n\r\'\"\026\b\t]", 
    function (str)
      return mysql_replacements [str] or str
    end ))

end -- fixsql

html_replacements = { 
   ["<"] = "&lt;",
   [">"] = "&gt;",
   ["&"] = "&amp;",
   ["\""] = "&quot;",
   }

-- fix text so that < > and & are escaped
function fixhtml (s)

  return (string.gsub (tostring (s), "[<>&\"]", 
    function (str)
      return html_replacements [str] or str
    end ))

end -- fixhtml

-- convert + to space
-- convert %xx where xx is hex characters, to the equivalent byte
function urldecode (s)
  return (string.gsub (string.gsub (s, "+", " "), 
          "%%(%x%x)", 
         function (str)
          return string.char (tonumber (str, 16))
         end ))
end -- function urldecode

-- reverse of urldecode - converts characters that are not alphanumeric
--  into the form %xx (for use in cookies etc.)
function urlencode (s)
 return (string.gsub (s, "%W", 
        function (str)
          return string.format ("%%%02X", string.byte (str))
        end  ))
end -- function urlencode
        
-- process a single key=value pair from a GET line (or cookie, etc.)
function assemble_value (s, t)
  assert (type (t) == "table")
  local _, _, key, value = string.find (s, "(.-)=(.+)")

  if key then
    t [trim (urldecode (key))] = trim (urldecode (value))
  end -- if we had key=value

end -- assemble_value

-- output a Lua table as an HTML table
function show_table (t)
  local k, v
  assert (type (t) == "table")
  print "<table border=1 cellpadding=3>"
  for k, v in pairs (t) do
    print "<tr>"
    print ("<th align=left >" .. fixhtml (k) .. "</th>" .. 
           "<td align=left >" .. fixhtml (v) .. "</td>")
    print "</tr>"
  end -- for
  print "</table>"
end -- show_table


-- get query data, cookie data, post data
function get_user_input ()
local _, v

  -- query data (stuff after the ? )
 
  local get_data = {}
  
  for _, v in ipairs (split (os.getenv ("QUERY_STRING"), "&")) do
    assemble_value (v, get_data)
  end -- for
  
  -- cookies
 
  local cookie_data = {}
  
  for _, v in ipairs (split (os.getenv ("HTTP_COOKIE"), ";")) do
    assemble_value (v, cookie_data)
  end -- for
  
  -- posted data (from a form)
  
  local post_data = {}

  local post_length = tonumber (os.getenv ("CONTENT_LENGTH")) or 0
  if os.getenv ("REQUEST_METHOD") == "POST" and post_length > 0 then
    for _, v in ipairs (split (io.read (post_length), "&")) do
      assemble_value (v, post_data)
    end -- for
  end -- if post
  
 return get_data, cookie_data, post_data, os.getenv ("REQUEST_METHOD")
 
 end -- function get_user_input
 
 -- show a standard HTTP header with optional cookies
function show_http_header (cookies)

  assert (not done_http_header, "Too many HTTP headers")
  
  print ("Content-Type: text/html; charset=iso-8859-1")
  print ("X-Powered-By: cgiutils.lua written by Nick Gammon")
  
  -- output a Set-Cookie line for each cookie in the table
  if cookies then
    assert (type (cookies) == "table")
    for k, v in pairs (cookies) do
      print ("Set-Cookie: " .. urlencode (k) .. "=" .. urlencode (v))
    end -- for each cookie
  end -- we have cookies

  print ("")  -- blank line separates header from body
  done_http_header = true

end -- function show_http_header 

-- show a standard HTML header with optional title, and other free-format things
--  the "other" stuff could be things like stylesheets, links, etc.

function show_html_header (title, other)

  assert (not done_html_header, "Too many HTML headers")

  if not done_http_header then
    show_http_header ()
  end 

  print ('<?xml version="1.0" encoding="iso-8859-1"?>')
  print ('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">')
  print ('<html lang="en">')
  print ('<head>')
  if title then
    print ('<title>' .. fixhtml (title) .. '</title>')
  end -- having title
  if other then  
    print (other)
  end -- having other stuff
  print ('</head>')
  print ('<body>')

  done_html_header = true
end -- function show_html_header

-- wrap up with an HTML footer
function show_html_footer ()

  assert (not done_html_footer, "Too many HTML footers")

  print ('</body>')
  print ('</html>')
  done_html_footer = true
end -- function show_html_footer

-- helper function to show an error in inverse bold coloured text
function show_error (theerror)
 
  if not done_html_header then
    show_html_header ("Error on web page")
  end 
 
  local colour_error_text = "#FFFFFF"  -- white
  local colour_error_bgnd = "#A52A2A"  -- brown
  local family = "sans-serif"
  local size = "150%"
  local weight = "bolder"
  
  -- style string for the error message
  local style = string.format (
                'color: %s; ' ..
                'background: %s; ' ..
                'font-family: %s; ' ..
                'font-size: %s; ' ..
                'font-weight: %s;',
                
                  colour_error_text,
                  colour_error_bgnd,
                  family,
                  size,
                  weight)
  
  -- show error in desired style
  print (string.format ('<p><span style="%s">&nbsp;%s&nbsp;</span></p>',
         style,
         fixhtml (theerror)))
          
end -- end of show_error
  
-- Call this for things like script errors - it shows the error and exits
function major_problem (why)

  if not done_html_header then
    show_html_header ("Script error on web page")
  end 
  
  print ("<h3>We apologise that there has been a problem with the web server ...</h3>\n")
  print ("<hr>")
  print ("<code><pre>" .. fixhtml (why) .. "</pre></code>")
  print ("<p>Error occurred on " .. os.date ("%#x at %X") .. "</p>\n")
  print ("<hr>")
  
  show_html_footer ()
  
  os.exit (1)  -- let's stop while we are ahead :)
end -- function major_problem
 
-- this function is run in protected mode in case the script page has errors
function load_page ()

  -- get local locale
  
  os.setlocale ("", "time")
  
  -- get GET, COOKIE, POST data, and request method
  
  get_data, cookie_data, post_data, request_method  = get_user_input ()
 
  local scriptname = os.getenv ("PATH_INFO")
  
  -- validity check on requested filename
  if not scriptname or
         string.find (scriptname, "..", 1, true) or
         not string.find (scriptname, "/%a+/[%a%d_%-]+%.lua")
         then
    return show_error ("That URL is not valid")
  end -- if
     
  scriptname = "." .. scriptname  -- make local path
  
  -- check page exists, to avoid annoying user with Lua traceback message
  local f, err1, err2 = io.open (scriptname)
   
  if f then
    f:close ()  -- we found the file, close it now
  else
    return show_error ("The requested page does not exist")
  end -- if
      
  -- run the file specified in the rest of the URL
  dofile (scriptname)
  
end -- load_page


-- main code starts here

ok, error = xpcall (load_page, _TRACEBACK)

if not ok then
  major_problem (error)
end -- error in page

-- ensure footer shown
if done_html_header and not done_html_footer then
  show_html_footer ()
end -- if



[EDIT]

Changed on 4th May 2006, to add various extra functionality:


  • Checking inside show_http_header, show_html_header and show_html_footer to ensure none of these are done twice.

  • Automatic footer generation if not done by the called script.

  • New function urlencode - this encodes a URL (reverse of urldecode) so that things like ';' are converted to '%3B'. This is used internally when setting a cookie.

  • Improved show_error function - colours the error message using <span> rather than a single-item table.

  • Improvements to load_page to do further checks on the requested script file name (using a regular expression). It also checks if the requested script file exists and if not gives a specific error message to that effect.

  • Fixed bug in split function (pos cannot be a local variable)

  • Added license so that you may use this software on your own web pages.


[EDIT]

Changed around 12 September 2006 to fix problems with getting POST data from some web servers using certain browsers. *cough**cough*Safari*cough*

- Nick Gammon

www.gammon.com.au, www.mushclient.com
[Go to top] top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


35,229 views.

This is page 1, subject is 2 pages long: 1 2  [Next page]

[Reply to this subject]  Reply to this subject   [New subject]  Start a new subject   [Refresh] Refresh page

Go to topic:           Search the forum


[Go to top] top

[Home]

Written by Nick Gammon - 5K

Comments to: Gammon Software support
[RH click to get RSS URL] Forum RSS feed ( http://www.gammon.com.au/rss/forum.xml )

[Best viewed with any browser - 2K]    [Internet Contents Rating Association (ICRA) - 2K]    [Web site powered by FutureQuest.Net]