Web server and MOAP - Part I (Basics)

The objects with these scripts are available at SLua Yardang 178,2,23 (SLua Class Study Area):

Web server and MOAP

You are welcome to the Study Groups all Saturdays from 11AM to 1PM SLT with your questions, practices, and projects to chat about this and anything else scripting related.

Media viewer

Displays an external webpage using Media-on-a-prim.

Say the link to the webpage in public chat, starting with “http”.

Media viewer

-- Media viewer

local FACE_MEDIA = 2

local function show(url)
    ll.SetPrimMediaParams(FACE_MEDIA, {
        PRIM_MEDIA_CURRENT_URL, url,
        PRIM_MEDIA_HOME_URL, url,
        PRIM_MEDIA_AUTO_ZOOM, false,
        PRIM_MEDIA_FIRST_CLICK_INTERACT, true,
        PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE,
        PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE,
        PRIM_MEDIA_AUTO_PLAY, true
    })
end

local function initialize()
    ll.Listen(0, "", "", "")
end

function listen(channel, name, id, message)
    if channel == 0 then
        if message:sub(1,7) == "http://" or message:sub(1,8) == "https://" then
            show(message)
        end
    end
end

initialize()

URL with HTML

We can set HTML code to the media face, adding “data:text/html,” in front of the html code when we set the url. No headers, only the html that would go inside the <body>.

The maximum length of this “url” is 1024 characters.

URL with HTMLHTML

The html string would usually be between quotes, I'm using [=[ to identify it as html for the syntax highlghter

-- using HTML in the URL

local DATA_URL = "data:text/html,"
local FACE_MEDIA = 2

local html = [=[<h1>Welcome to My Page!</h1><p>Hello, world! Isn't HTML amazing?</p><p>HTML stands for <strong>Hyper Text Markup Language</strong>.</p><p>Thanks for visiting! Have a <em>fantastic</em> day!</p>]=]

local function show(url)
    ll.SetPrimMediaParams(FACE_MEDIA, {
        PRIM_MEDIA_CURRENT_URL, url,
        PRIM_MEDIA_HOME_URL, url,
        PRIM_MEDIA_AUTO_ZOOM, false,
        PRIM_MEDIA_FIRST_CLICK_INTERACT, true,
        PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE,
        PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE,
        PRIM_MEDIA_AUTO_PLAY, true,
        PRIM_MEDIA_WIDTH_PIXELS, 512,
        PRIM_MEDIA_HEIGHT_PIXELS, 256
    })
end

show(DATA_URL .. html)

Displaying a notecard on MOAP

Uses an in-world URL and XHTML code to serve a page from the script.

URL’s are assigned to the script and stop working when the script is reset or its object derezed or the region restarts.

We use XHTML because pages served as HTML are only visible to the owner of the object, to other people or opening the link in a web browser shows the HTML code.

Notecard displayHTML

Names between @...@ in the XHTML are to be replaced with generated XHTML code before serving the page

-- Notecard display

local FACE_MEDIA = 2

local url = ""

local htmlNotecard = [=[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>@NOTECARD_NAME@</title>
</head>
<body>
  <h1>@NOTECARD_NAME@</h1>
  @NOTECARD_LINES@
</body>
</html>
]=]

local htmlNotecardLines = [=[
  <p>@NOTECARD_LINE@</p>
]=]

local notecardName = ""
local notecardLine = 0
local requestLineId = NULL_KEY
local notecard = {}

local function show(url)
    ll.SetPrimMediaParams(FACE_MEDIA, {
        PRIM_MEDIA_CURRENT_URL, url,
        PRIM_MEDIA_HOME_URL, url,
        PRIM_MEDIA_AUTO_ZOOM, false,
        PRIM_MEDIA_FIRST_CLICK_INTERACT, true,
        PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE,
        PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE,
        PRIM_MEDIA_AUTO_PLAY, true
    })
end

local function sayUrl(url)
    ll.OwnerSay(url)
end

local function initialize()
    notecardName = ll.GetInventoryName(INVENTORY_NOTECARD, 0)
    if notecardName ~= "" then
        notecard = {}
        notecardLine = 0
        requestLineId = ll.GetNotecardLine(notecardName, notecardLine)
    else
        ll.OwnerSay("No notecards in the contents")
    end
end

function dataserver(request, data)
    if request == requestLineId then
        repeat
            if data ~= EOF then
                table.insert(notecard, ll.ReplaceSubString(htmlNotecardLines, "@NOTECARD_LINE@", data, 0))
                notecardLine += 1
                data = ll.GetNotecardLineSync(notecardName, notecardLine)
                if data == NAK then
                    requestLineId = ll.GetNotecardLine(notecardName, notecardLine)
                end
            end
        until data == EOF or data == NAK
        if data == EOF then
            ll.RequestURL()
        end
    end
end

function http_request(id, method, body)
    if method == URL_REQUEST_GRANTED then
        url = body
        sayUrl(url)
        show(url)
    elseif method == URL_REQUEST_DENIED then
        ll.OwnerSay("Unable to get URL!")
    elseif method == "GET" then
        local html = ll.ReplaceSubString(htmlNotecard, "@NOTECARD_NAME@", notecardName, 0)
        html = ll.ReplaceSubString(html, "@NOTECARD_LINES@", table.concat(notecard), 0)
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, html)
    end
end

function on_rez(start_param)
    ll.ResetScript()
end

function changed(change)
    if bit32.btest(change, bit32.bor(CHANGED_REGION_START, CHANGED_OWNER, CHANGED_INVENTORY)) then
        ll.ResetScript()
    end
end

initialize()

Displaying random quotes in single or multi view

The pages are not rendered by the server. They are rendered by our local viewer (Firestorm, etc).

When the script sets an url with llSetPrimMediaParams(), the server stores this url, as part of the object, in the info of the face of the object.
The server does nothing else with the url, only storing it. It doesn’t use the url, it doesn’t make the request, anything.
It’s our local viewer that requests the url and displays the answer.

When the media object comes into our view, our local viewer receives all the info of the object, including the media url, from the server.
Our local viewer opens its internal web browser, and embeds it in the face showing the media. The viewer requests the url and the viewer’s web browser displays the media on the face of the object.
Each face with media needs its own web browser instance.

In the script, when we use an in-world URL, its http_request is triggered once for each avatar looking at it. If nobody is looking http_request is not be triggered.

The next scripts request a new random quote from an external server at owner’s touch. The first script shows the same quote to everyone, the second one shows a different quote to each viewer or internal/external browser using the link.

Random quotes: single viewHTML

Requesting the quote on touch, serving the same one

It adds a time to the media URL that is not used, but we need to set a different URL to make it reload

-- Random quotes (single view)

local url = ""

local htmlQuote = [=[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>Quote of the Moment</title>
</head>
<body>
  <h1 style="color: green;">@QUOTE@</h1>
  <h1 style="color: green;">@AUTHOR@</h1>
</body>
</html>
]=]

local WEB_API = "https://zenquotes.io/api/random"
local FACE_MEDIA = 2

local httpRequestId = NULL_KEY
local html = ""
local requestCount = 0

local function show(url)
    ll.SetPrimMediaParams(FACE_MEDIA, {
        PRIM_MEDIA_CURRENT_URL, url,
        PRIM_MEDIA_HOME_URL, url,
        PRIM_MEDIA_AUTO_ZOOM, false,
        PRIM_MEDIA_FIRST_CLICK_INTERACT, true,
        PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE,
        PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE,
        PRIM_MEDIA_AUTO_PLAY, true,
        PRIM_MEDIA_WIDTH_PIXELS, 1024,
        PRIM_MEDIA_HEIGHT_PIXELS, 256
    })
end

local function initialize()
    ll.ClearPrimMedia(FACE_MEDIA)
    requestCount = 0
    ll.RequestURL()
end

function touch_start(num_detected)
    if ll.DetectedKey(0) == ll.GetOwner() then
        if requestCount ~= 0 then
            ll.OwnerSay(`{requestCount} requests to the previous page`)
            requestCount = 0
        end
        httpRequestId = ll.HTTPRequest(WEB_API, {}, "")
    end
end

function http_response(request_id, status, metadata, body)
    if httpRequestId == request_id then
        local json = lljson.decode(body)
        html = ll.ReplaceSubString(htmlQuote, "@QUOTE@", json[1].q, 0)
        html = ll.ReplaceSubString(html, "@AUTHOR@", json[1].a, 0)
        show(`{url}/?time={os.time()}`)
    end
end

function http_request(id, method, body)
    if method == URL_REQUEST_GRANTED then
        url = body
        ll.OwnerSay(url)
        httpRequestId = ll.HTTPRequest(WEB_API, {}, "")
    elseif method == URL_REQUEST_DENIED then
        ll.OwnerSay("Unable to get URL!")
    elseif method == "GET" then
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, html)
        requestCount += 1
    end
end

function on_rez(start_param)
    ll.ResetScript()
end

function changed(change)
    if bit32.btest(change, bit32.bor(CHANGED_REGION_START, CHANGED_OWNER, CHANGED_INVENTORY)) then
        ll.ResetScript()
    end
end

initialize()

Random quotes: multi viewHTML

Requesting the quote on request, serving different ones

-- Random quotes (multi view)

local url = ""

local htmlQuote = [=[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>Quote of the Moment</title>
</head>
<body>
  <h1 style="color: blue;">@QUOTE@</h1>
  <h1 style="color: blue;">@AUTHOR@</h1>
</body>
</html>
]=]

local WEB_API = "https://zenquotes.io/api/random"
local FACE_MEDIA = 2

local requests = {}
local requestCount = 0

local function show(url)
    ll.SetPrimMediaParams(FACE_MEDIA, {
        PRIM_MEDIA_CURRENT_URL, url,
        PRIM_MEDIA_HOME_URL, url,
        PRIM_MEDIA_AUTO_ZOOM, false,
        PRIM_MEDIA_FIRST_CLICK_INTERACT, true,
        PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE,
        PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE,
        PRIM_MEDIA_AUTO_PLAY, true,
        PRIM_MEDIA_WIDTH_PIXELS, 1024,
        PRIM_MEDIA_HEIGHT_PIXELS, 256
    })
end

local function initialize()
    ll.ClearPrimMedia(FACE_MEDIA)
    requests = {}
    requestCount = 0
    ll.RequestURL()
end

function touch_start(num_detected)
    if ll.DetectedKey(0) == ll.GetOwner() then
        requests = {}
        if requestCount ~= 0 then
            ll.OwnerSay(`{requestCount} requests to the previous page`)
            requestCount = 0
        end
        show(`{url}/?time={os.time()}`)
    end
end

function http_response(request_id, status, metadata, body)
    if requests[request_id] then
        local id = requests[request_id]
        local json = lljson.decode(body)
        local html = ll.ReplaceSubString(htmlQuote, "@QUOTE@", json[1].q, 0)
        html = ll.ReplaceSubString(html, "@AUTHOR@", json[1].a, 0)
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, html)
        requests[request_id] = nil
    end
end

function http_request(id, method, body)
    if method == URL_REQUEST_GRANTED then
        url = body
        ll.OwnerSay(url)
    elseif method == URL_REQUEST_DENIED then
        ll.OwnerSay("Unable to get URL!")
    elseif method == "GET" then
        requests[ll.HTTPRequest(WEB_API, {}, "")] = id
        requestCount += 1
    end
end

function on_rez(start_param)
    ll.ResetScript()
end

function changed(change)
    if bit32.btest(change, bit32.bor(CHANGED_REGION_START, CHANGED_OWNER, CHANGED_INVENTORY)) then
        ll.ResetScript()
    end
end

initialize()

List of Visitors

Four scripts showing a list of the visitors in the region with progressive improvements:

  • HTML only, no styling (looks ugly).
  • HTML and CSS styling.
  • Multipage single view (index, visitors and language) using links. When a page is changed, it changes in all the viewers.
  • Multipage multi view using buttons. Viewers have independent navigation, each one can be in a different page.

List of visitors, single page, no stylingHTML

-- List of visitors, single page, no styling

local FACE_MEDIA = 2

local url = ""

local htmlVisitorsList = [=[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>Table of Visitors</title>
</head>
<body>
  <h1>Table of Visitors</h1>
  <table border="1">
    <thead>
      <tr>
        <th>Name</th>
        <th>Username</th>
      </tr>
    </thead>
    <tfoot>
      <tr>
        <td colspan="2">Total Visitors: @TOTAL_VISITORS@</td>
      </tr>
    </tfoot>
    <tbody>
      @TABLE@
    </tbody>
  </table>
</body>
</html>
]=]

local htmlVisitorsListTable = [=[
      <tr>
        <td>@NAME@</td>
        <td>@USERNAME@</td>
      </tr>
]=]

local function show(url)
    ll.SetPrimMediaParams(FACE_MEDIA, {
        PRIM_MEDIA_CURRENT_URL, url,
        PRIM_MEDIA_HOME_URL, url,
        PRIM_MEDIA_AUTO_ZOOM, false,
        PRIM_MEDIA_FIRST_CLICK_INTERACT, true,
        PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE,
        PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE,
        PRIM_MEDIA_AUTO_PLAY, true
    })
end

local function sayUrl(url)
    ll.OwnerSay(url)
end

local function tableVisitors(html)
    local rows = {}
    local visitors = ll.GetAgentList(AGENT_LIST_REGION, {})
    for _, visitor in visitors do
        local row = htmlVisitorsListTable
        row = ll.ReplaceSubString(row, "@NAME@", ll.GetDisplayName(visitor), 0)
        row = ll.ReplaceSubString(row, "@USERNAME@", ll.GetUsername(visitor), 0)
        table.insert(rows, row)
    end
    html = ll.ReplaceSubString(html, "@TOTAL_VISITORS@", tostring(#visitors), 0)
    html = ll.ReplaceSubString(html, "@TABLE@", table.concat(rows), 0)
    return html
end

local function initialize()
    ll.RequestURL()
end

function http_request(id, method, body)
    if method == URL_REQUEST_GRANTED then
        url = body
        sayUrl(url)
        show(url)
    elseif method == URL_REQUEST_DENIED then
        ll.OwnerSay("Unable to get URL!")
    elseif method == "GET" then
        local html = tableVisitors(htmlVisitorsList)
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, html)
    end
end

function on_rez(start_param)
    ll.ResetScript()
end

function changed(change)
    if bit32.btest(change, bit32.bor(CHANGED_REGION_START, CHANGED_OWNER, CHANGED_INVENTORY)) then
        ll.ResetScript()
    end
end

initialize()

List of visitors, single page, CSS stylingHTMLCSS

-- List of visitors, single page, CSS styling

local FACE_MEDIA = 2

local url = ""

local htmlVisitorsList = [=[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>Table of Visitors</title>
  <style type="text/css">
    body {
      font-family: Arial, sans-serif;
      background-color: #f4f4f4;
      margin: 0;
      padding: 20px;
      text-align: center;
    }

    h1 {
      color: #333;
      font-size: 24px;
      margin-bottom: 20px;
    }

    table {
      width: 80%;
      margin: 0 auto;
      border-collapse: collapse;
      background-color: #fff;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    }

    th, td {
      padding: 12px 15px;
      text-align: left;
      border-bottom: 1px solid #ddd;
    }

    th {
      background-color: #4CAF50;
      color: white;
      text-transform: uppercase;
      letter-spacing: 0.1em;
    }

    tr:hover {
      background-color: #f1f1f1;
    }

    tfoot td {
      font-weight: bold;
      background-color: #f9f9f9;
    }

    tfoot td:first-child {
      text-align: right;
    }

  </style>
</head>
<body>
  <h1>Table of Visitors</h1>
  <table>
    <thead>
      <tr>
        <th>Name</th>
        <th>Username</th>
      </tr>
    </thead>
    <tfoot>
      <tr>
        <td colspan="2">Total Visitors: @TOTAL_VISITORS@</td>
      </tr>
    </tfoot>
    <tbody>
      @TABLE@
    </tbody>
  </table>
</body>
</html>
]=]

local htmlVisitorsListTable = [=[
      <tr>
        <td>@NAME@</td>
        <td>@USERNAME@</td>
      </tr>
]=]

local function show(url)
    ll.SetPrimMediaParams(FACE_MEDIA, {
        PRIM_MEDIA_CURRENT_URL, url,
        PRIM_MEDIA_HOME_URL, url,
        PRIM_MEDIA_AUTO_ZOOM, false,
        PRIM_MEDIA_FIRST_CLICK_INTERACT, true,
        PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE,
        PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE,
        PRIM_MEDIA_AUTO_PLAY, true
    })
end

local function tableVisitors(html)
    local rows = {}
    local visitors = ll.GetAgentList(AGENT_LIST_REGION, {})
    for _, visitor in visitors do
        local row = htmlVisitorsListTable
        row = ll.ReplaceSubString(row, "@NAME@", ll.GetDisplayName(visitor), 0)
        row = ll.ReplaceSubString(row, "@USERNAME@", ll.GetUsername(visitor), 0)
        table.insert(rows, row)
    end
    html = ll.ReplaceSubString(html, "@TOTAL_VISITORS@", tostring(#visitors), 0)
    html = ll.ReplaceSubString(html, "@TABLE@", table.concat(rows), 0)
    return html
end

local function initialize()
    ll.RequestURL()
end

function http_request(id, method, body)
    if method == URL_REQUEST_GRANTED then
        url = body
        ll.OwnerSay(url)
        show(url)
    elseif method == URL_REQUEST_DENIED then
        ll.OwnerSay("Unable to get URL!")
    elseif method == "GET" then
        local html = tableVisitors(htmlVisitorsList)
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, html)
    end
end

function on_rez(start_param)
    ll.ResetScript()
end

function changed(change)
    if bit32.btest(change, bit32.bor(CHANGED_REGION_START, CHANGED_OWNER, CHANGED_INVENTORY)) then
        ll.ResetScript()
    end
end

initialize()

List of visitors, multipage, single view, CSS stylingHTMLCSS

Links send GET requests and change the media URL

-- List of visitors, multipage, single view, CSS styling

local FACE_MEDIA = 2

local url = ""

local htmlHeader = [=[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>@TITLE@</title>
  @STYLE@
</head>
<body>
  @BODY@
</body>
</html>
]=]

local htmlStyle = [=[
  <style type="text/css">
    body {
      font-family: Arial, sans-serif;
      background-color: #f4f4f4;
      margin: 0;
      padding: 20px;
      text-align: center;
    }
    h1 {
      color: #333;
      font-size: 24px;
      margin-bottom: 20px;
    }
    table {
      width: 80%;
      margin: 0 auto;
      border-collapse: collapse;
      background-color: #fff;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    }
    th, td {
      padding: 12px 15px;
      text-align: left;
      border-bottom: 1px solid #ddd;
    }
    th {
      background-color: #4CAF50;
      color: white;
      text-transform: uppercase;
      letter-spacing: 0.1em;
    }
    tr:hover {
      background-color: #f1f1f1;
    }
    tfoot td {
      font-weight: bold;
      background-color: #f9f9f9;
    }
    tfoot td:first-child {
      text-align: right;
    }
  </style>
]=]

local htmlLinks = [=[
  <h1>Links Page</h1>
  <ul>
    <li><a href="visitors" title="Go to the visitors list">Visitors</a></li>
    <li><a href="languages" title="Go to the languages list">Languages</a></li>
  </ul>
]=]

local htmlLinksTitle = "Links Page"

local htmlVisitors = [=[
  <h1>Table of Visitors</h1>
  <table>
    <thead>
      <tr>
        <th>Name</th>
        <th>Username</th>
      </tr>
    </thead>
    <tfoot>
      <tr>
        <td colspan="2">Total Visitors: @TOTAL_VISITORS@</td>
      </tr>
    </tfoot>
    <tbody>
      @TABLE@
    </tbody>
  </table>
  <a href="links" title="Go back to the links page">Back</a>
]=]

local htmlVisitorsTable = [=[
      <tr>
        <td>@NAME@</td>
        <td>@USERNAME@</td>
      </tr>
]=]

local htmlVisitorsTitle = "Table of Visitors"

local htmlLanguages = [=[
  <h1>Table of Languages</h1>
  <table>
    <thead>
      <tr>
        <th>Language</th>
        <th>Visitors</th>
      </tr>
    </thead>
    <tfoot>
      <tr>
        <td colspan="2">Total Languages: @TOTAL_LANGUAGES@</td>
      </tr>
    </tfoot>
    <tbody>
      @TABLE@
    </tbody>
  </table>
  <a href="links" title="Go back to the links page">Back</a>
]=]

local htmlLanguagesTable = [=[
      <tr>
        <td>@LANGUAGE@</td>
        <td>@VISITORS@</td>
      </tr>
]=]

local htmlLanguagesTitle = "Table of Languages"

local function show(url)
    ll.SetPrimMediaParams(FACE_MEDIA, {
        PRIM_MEDIA_CURRENT_URL, url,
        PRIM_MEDIA_HOME_URL, url,
        PRIM_MEDIA_AUTO_ZOOM, false,
        PRIM_MEDIA_FIRST_CLICK_INTERACT, true,
        PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_OWNER,
        PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE,
        PRIM_MEDIA_AUTO_PLAY, true
    })
end

local function tableVisitors(html)
    local rows = {}
    local visitors = ll.GetAgentList(AGENT_LIST_REGION, {})
    for _, visitor in visitors do
        local row = htmlVisitorsTable
        row = ll.ReplaceSubString(row, "@NAME@", ll.GetDisplayName(visitor), 0)
        row = ll.ReplaceSubString(row, "@USERNAME@", ll.GetUsername(visitor), 0)
        table.insert(rows, row)
    end
    html = ll.ReplaceSubString(html, "@TOTAL_VISITORS@", tostring(#visitors), 0)
    html = ll.ReplaceSubString(html, "@TABLE@", table.concat(rows), 0)
    return html
end

local LANGUAGES = {
    en = "English",   da = "Danish",  de = "German",   es = "Spanish",    fr = "French",  it = "Italian",
    hu = "Hungarian", nl = "Dutch",   pl = "Polish",   pt = "Portuguese", ru = "Russian", tr = "Turkish",
    uk = "Ukrainian", zh = "Chinese", ja = "Japanese", ko = "Korean",     [""] = "unknown"
}

local function tableLanguages(html)
    local languages = {}
    local visitors = ll.GetAgentList(AGENT_LIST_REGION, {})
    for _, visitor in visitors do
        local language = ll.GetAgentLanguage(visitor)
        languages[language] = (languages[language] or 0) + 1
    end
    local lang = {}
    for code, count in languages do
        table.insert(lang, {code = code, count = count})
    end
    table.sort(lang, function(a, b)
        return if a.count ~= b.count then
            a.count > b.count
        else
            a.code < b.code
    end)
    local rows = {}
    for _, language in lang do
        local languageName = LANGUAGES[language.code] or language.code
        local row = htmlLanguagesTable
        row = ll.ReplaceSubString(row, "@LANGUAGE@", languageName, 0)
        row = ll.ReplaceSubString(row, "@VISITORS@", tostring(language.count), 0)
        table.insert(rows, row)
    end
    html = ll.ReplaceSubString(html, "@TOTAL_LANGUAGES@", tostring(#lang - if languages[""] then 1 else 0), 0)
    html = ll.ReplaceSubString(html, "@TABLE@", table.concat(rows), 0)
    return html
end

local function initialize()
    ll.RequestURL()
end

function http_request(id, method, body)
    if method == URL_REQUEST_GRANTED then
        url = body .. "/links"
        ll.OwnerSay(url)
        show(url)
    elseif method == URL_REQUEST_DENIED then
        ll.OwnerSay("Unable to get URL!")
    elseif method == "GET" then
        local path = ll.ToLower(ll.GetHTTPHeader(id, "x-path-info"))
        local query = ll.ToLower(ll.GetHTTPHeader(id, "x-query-string"))
        local html = ll.ReplaceSubString(htmlHeader, "@STYLE@", htmlStyle, 0)
        if path == "/links" then
            html = ll.ReplaceSubString(html, "@TITLE@", htmlLinksTitle, 0)
            html = ll.ReplaceSubString(html, "@BODY@", htmlLinks, 0)
        elseif path == "/visitors" then
            html = ll.ReplaceSubString(html, "@TITLE@", htmlVisitorsTitle, 0)
            html = ll.ReplaceSubString(html, "@BODY@", tableVisitors(htmlVisitors), 0)
        elseif path == "/languages" then
            html = ll.ReplaceSubString(html, "@TITLE@", htmlLanguagesTitle, 0)
            html = ll.ReplaceSubString(html, "@BODY@", tableLanguages(htmlLanguages), 0)
        end
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, html)
    end
end

function on_rez(start_param)
    ll.ResetScript()
end

function changed(change)
    if bit32.btest(change, bit32.bor(CHANGED_REGION_START, CHANGED_OWNER, CHANGED_INVENTORY)) then
        ll.ResetScript()
    end
end

initialize()

List of visitors, multipage, multi view, CSS stylingHTMLCSS

Buttons in forms with method="POST" and action="" show the page returned to the request and don't change the media URL

-- List of visitors, multipage, multi view, CSS styling

local FACE_MEDIA = 2

local url = ""

local htmlHeader = [=[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>@TITLE@</title>
  @STYLE@
</head>
<body>
  @BODY@
</body>
</html>
]=]

local htmlStyle = [=[
  <style type="text/css">
    body {
      margin: 0;
      padding: 0;
      font-family: Arial, sans-serif;
      background: linear-gradient(135deg, #1e3c72, #2a5298);
      color: #ffffff;
      text-align: center;
      display: flex;
      flex-direction: column;
      justify-content: center;
      height: 100vh;
    }

    h1 {
      font-size: 2.5em;
      text-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
      margin-bottom: 1em;
    }

    form {
      display: inline-block;
      margin: 0.5em;
    }

    table {
      margin: 0 auto;
      border-collapse: collapse;
      width: 80%;
      margin-bottom: 2em;
      background: #f9f9f9;
      border-radius: 12px;
      overflow: hidden;
      box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);
    }

    th, td {
      padding: 0.25em;
      text-align: left;
      font-size: 1.75em;
      border: 1px solid #ddd;
    }

    th {
      background: linear-gradient(135deg, #ff7e5f, #feb47b);
      color: #ffffff;
      font-weight: bold;
      text-transform: uppercase;
      letter-spacing: 1px;
    }

    td {
      background: #ffffff;
      color: #333;
      transition: background-color 0.3s ease;
    }

    td:nth-child(even) {
      background: #f4f7fb;
    }

    tr:nth-child(odd) td {
      background: #fefefe;
    }

    tr:hover td {
      background: linear-gradient(135deg, #ff7e5f, #feb47b);
      color: #fff;
    }

    tfoot {
      background: #2a5298;
      color: #fff;
      font-weight: bold;
      text-align: center;
    }

    tfoot td {
      padding: 0.5em;
      font-size: 1.75em;
    }

    button {
      background: linear-gradient(135deg, #ff7e5f, #feb47b);
      border: none;
      padding: 1em 2em;
      font-size: 1.75em;
      font-weight: bold;
      color: #fff;
      border-radius: 50px;
      box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
      cursor: pointer;
      transition: transform 0.2s ease, box-shadow 0.2s ease;
    }

    button:hover {
      transform: translateY(-5px);
      box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
    }

    button:active {
      transform: translateY(0);
      box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
    }

    button[title]::after {
      content: attr(title);
      display: block;
      font-size: 1em;
      color: #d4d4d4;
      margin-top: 0.5em;
    }

    @keyframes fadeIn {
      from { opacity: 0; transform: translateY(-20px); }
      to { opacity: 1; transform: translateY(0); }
    }

    h1, table, form {
      animation: fadeIn 1s ease forwards;
    }
  </style>
]=]

local htmlLinks = [=[
  <h1>Links Page</h1>
  <form action="" method="POST">
    <button type="submit" name="button" value="visitors" title="Go to the visitors list">Visitors</button>
  </form>
  <form action="" method="POST">
    <button type="submit" name="button" value="languages" title="Go to the languages list">Languages</button>
  </form>
]=]

local htmlLinksTitle = "Links Page"

local htmlVisitors = [=[
  <h1>Table of Visitors</h1>
  <table>
    <thead>
      <tr>
        <th>Name</th>
        <th>Username</th>
      </tr>
    </thead>
    <tfoot>
      <tr>
        <td colspan="2">Total Visitors: @TOTAL_VISITORS@</td>
      </tr>
    </tfoot>
    <tbody>
      @TABLE@
    </tbody>
  </table>
  <form action="" method="POST">
    <button type="submit" name="button" value="links" title="Go back to the links page">Back</button>
  </form>
]=]

local htmlVisitorsTable = [=[
      <tr>
        <td>@NAME@</td>
        <td>@USERNAME@</td>
      </tr>
]=]

local htmlVisitorsTitle = "Table of Visitors"

local htmlLanguages = [=[
  <h1>Table of Languages</h1>
  <table>
    <thead>
      <tr>
        <th>Language</th>
        <th>Visitors</th>
      </tr>
    </thead>
    <tfoot>
      <tr>
        <td colspan="2">Total Languages: @TOTAL_LANGUAGES@</td>
      </tr>
    </tfoot>
    <tbody>
      @TABLE@
    </tbody>
  </table>
  <form action="" method="POST">
    <button type="submit" name="button" value="links" title="Go back to the links page">Back</button>
  </form>
]=]

local htmlLanguagesTable = [=[
      <tr>
        <td>@LANGUAGE@</td>
        <td>@VISITORS@</td>
      </tr>
]=]

local htmlLanguagesTitle = "Table of Languages"

local function show(url)
    ll.SetPrimMediaParams(FACE_MEDIA, {
        PRIM_MEDIA_CURRENT_URL, url,
        PRIM_MEDIA_HOME_URL, url,
        PRIM_MEDIA_AUTO_ZOOM, false,
        PRIM_MEDIA_FIRST_CLICK_INTERACT, true,
        PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE,
        PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE,
        PRIM_MEDIA_AUTO_PLAY, true
    })
end

local function parseQuery(query)
    local params = {}
    for key, value in query:gmatch("([^&=]+)=?([^&]*)") do
        params[ll.UnescapeURL(key)] = ll.UnescapeURL((value:gsub("+"," ")))
    end
    return params
end

local function tableVisitors(html)
    local rows = {}
    local visitors = ll.GetAgentList(AGENT_LIST_REGION, {})
    for _, visitor in visitors do
        local row = htmlVisitorsTable
        row = ll.ReplaceSubString(row, "@NAME@", ll.GetDisplayName(visitor), 0)
        row = ll.ReplaceSubString(row, "@USERNAME@", ll.GetUsername(visitor), 0)
        table.insert(rows, row)
    end
    html = ll.ReplaceSubString(html, "@TOTAL_VISITORS@", tostring(#visitors), 0)
    html = ll.ReplaceSubString(html, "@TABLE@", table.concat(rows), 0)
    return html
end

local LANGUAGES = {
    en = "English",   da = "Danish",  de = "German",   es = "Spanish",    fr = "French",  it = "Italian",
    hu = "Hungarian", nl = "Dutch",   pl = "Polish",   pt = "Portuguese", ru = "Russian", tr = "Turkish",
    uk = "Ukrainian", zh = "Chinese", ja = "Japanese", ko = "Korean",     [""] = "unknown"
}

local function tableLanguages(html)
    local languages = {}
    local visitors = ll.GetAgentList(AGENT_LIST_REGION, {})
    for _, visitor in visitors do
        local language = ll.GetAgentLanguage(visitor)
        languages[language] = (languages[language] or 0) + 1
    end
    local lang = {}
    for code, count in languages do
        table.insert(lang, {code = code, count = count})
    end
    table.sort(lang, function(a, b)
        return if a.count ~= b.count then
            a.count > b.count
        else
            a.code < b.code
    end)
    local rows = {}
    for _, language in lang do
        local languageName = LANGUAGES[language.code] or language.code
        local row = htmlLanguagesTable
        row = ll.ReplaceSubString(row, "@LANGUAGE@", languageName, 0)
        row = ll.ReplaceSubString(row, "@VISITORS@", tostring(language.count), 0)
        table.insert(rows, row)
    end
    html = ll.ReplaceSubString(html, "@TOTAL_LANGUAGES@", tostring(#lang - if languages[""] then 1 else 0), 0)
    html = ll.ReplaceSubString(html, "@TABLE@", table.concat(rows), 0)
    return html
end

local function responsePage(id, page)
    local html = ll.ReplaceSubString(htmlHeader, "@STYLE@", htmlStyle, 0)
    if page == "links" then
        html = ll.ReplaceSubString(html, "@TITLE@", htmlLinksTitle, 0)
        html = ll.ReplaceSubString(html, "@BODY@", htmlLinks, 0)
    elseif page == "visitors" then
        html = ll.ReplaceSubString(html, "@TITLE@", htmlVisitorsTitle, 0)
        html = ll.ReplaceSubString(html, "@BODY@", tableVisitors(htmlVisitors), 0)
    elseif page == "languages" then
        html = ll.ReplaceSubString(html, "@TITLE@", htmlLanguagesTitle, 0)
        html = ll.ReplaceSubString(html, "@BODY@", tableLanguages(htmlLanguages), 0)
    end
    ll.SetContentType(id, CONTENT_TYPE_XHTML)
    ll.HTTPResponse(id, 200, html)
end

local function initialize()
    ll.RequestURL()
end

function http_request(id, method, body)
    if method == URL_REQUEST_GRANTED then
        url = body .. "/links"
        ll.OwnerSay(url)
        show(url)
    elseif method == URL_REQUEST_DENIED then
        ll.OwnerSay("Unable to get URL!")
    elseif method == "GET" then
        local path = ll.ToLower(ll.GetHTTPHeader(id, "x-path-info"))
        local query = ll.ToLower(ll.GetHTTPHeader(id, "x-query-string"))
        responsePage(id, path:sub(2))
    elseif method == "POST" then
        responsePage(id, parseQuery(body).button)
    end
end

function on_rez(start_param)
    ll.ResetScript()
end

function changed(change)
    if bit32.btest(change, bit32.bor(CHANGED_REGION_START, CHANGED_OWNER, CHANGED_INVENTORY)) then
        ll.ResetScript()
    end
end

initialize()

Form Poll

Touches on a media face don’t trigger touch events. The script can’t know who is using the media.

The three scripts with a form to enter info use different user checks:

  • The user enters the name, no user check.
  • The user enters the username, the script opens a menu to the user to confirm.
  • No MOAP, the script sends a link on touch (containing a random code) to open and fill in the browser.

Form Poll, no user checkHTMLCSS

-- Form Poll, no user check

local FACE_MEDIA = 2

local url = ""

local htmlHeader = [=[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>@TITLE@</title>
  @STYLE@
</head>
<body>
  @BODY@
</body>
</html>
]=]

local htmlStyle = [=[
  <style type="text/css">
    body {
      font-family: Arial, sans-serif;
      background-color: #f0f8ff;
      color: #333;
      margin: 0;
      padding: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
    }
    h1 {
      text-align: center;
      color: #333;
      font-size: 2em;
      margin-bottom: 20px;
    }
    form {
      background-color: #fff;
      border-radius: 10px;
      box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
      padding: 20px;
      width: 300px;
    }
    label {
      font-weight: bold;
      color: #666;
    }
    input[type="text"] {
      width: 100%;
      padding: 10px;
      margin-top: 5px;
      margin-bottom: 20px;
      border-radius: 5px;
      border: 1px solid #ccc;
      box-sizing: border-box;
    }
    input[type="radio"] {
      margin-right: 10px;
    }
    input[type="submit"] {
      width: 100%;
      padding: 10px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 5px;
      font-size: 1em;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    input[type="submit"]:hover {
      background-color: #45a049;
    }
    p {
      font-size: 1.1em;
      color: #333;
      margin-bottom: 10px;
    }
  </style>
]=]

local htmlFruits = [=[
  <form action="fruits" method="post">
    <h1>Select Your Favorite Fruit</h1>
    <label for="name">Your name:</label>
    <input type="text" id="name" name="name" />
    <p>Please select your favorite fruit:</p>
      <input type="radio" id="apple" name="fruit" value="apple" />
      <label for="apple">Apple</label>
    <br />
      <input type="radio" id="banana" name="fruit" value="banana" />
      <label for="banana">Banana</label>
    <br />
      <input type="radio" id="orange" name="fruit" value="orange" />
      <label for="orange">Orange</label>
    <br />
      <input type="radio" id="grape" name="fruit" value="grape" />
      <label for="grape">Grape</label>
    <br /><br />
    <input type="submit" value="Submit" />
  </form>
]=]

local htmlFruitsTitle = "Fruit Selection Form"

local function show(url)
    ll.SetPrimMediaParams(FACE_MEDIA, {
        PRIM_MEDIA_CURRENT_URL, url,
        PRIM_MEDIA_HOME_URL, url,
        PRIM_MEDIA_AUTO_ZOOM, false,
        PRIM_MEDIA_FIRST_CLICK_INTERACT, true,
        PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE,
        PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE,
        PRIM_MEDIA_AUTO_PLAY, true,
        PRIM_MEDIA_WIDTH_PIXELS, 512,
        PRIM_MEDIA_HEIGHT_PIXELS, 512
    })
end

local function getPage(path)
    local html = ""
    if path == "/fruits" then
        html = ll.ReplaceSubString(htmlHeader, "@STYLE@", htmlStyle, 0)
        html = ll.ReplaceSubString(html, "@TITLE@", htmlFruitsTitle, 0)
        html = ll.ReplaceSubString(html, "@BODY@", htmlFruits, 0)
    end
    return html
end

local function sayParams(params)
    local paramList = {}
    for key, value in params:gmatch("([^&=]+)=?([^&]*)") do
        table.insert(paramList, `{ll.UnescapeURL(key)}={ll.UnescapeURL((value:gsub("+"," ")))}`)
    end
    ll.Say(0, `Parameters: \n{table.concat(paramList, "\n")}`)
end


local function initialize()
    ll.RequestURL()
end

function http_request(id, method, body)
    if method == URL_REQUEST_GRANTED then
        url = body .. "/fruits"
        ll.OwnerSay(url)
        show(url)
    elseif method == URL_REQUEST_DENIED then
        ll.OwnerSay("Unable to get URL!")
    elseif method == "GET" then
        local path = ll.ToLower(ll.GetHTTPHeader(id, "x-path-info"))
        local query = ll.ToLower(ll.GetHTTPHeader(id, "x-query-string"))
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, getPage(path))
    elseif method == "POST" then
        sayParams(body)
        local path = ll.ToLower(ll.GetHTTPHeader(id, "x-path-info"))
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, getPage(path))
    end
end

function on_rez(start_param)
    ll.ResetScript()
end

function changed(change)
    if bit32.btest(change, bit32.bor(CHANGED_REGION_START, CHANGED_OWNER, CHANGED_INVENTORY)) then
        ll.ResetScript()
    end
end

initialize()

Form Poll, user check with username and menuHTMLCSS

-- Form Poll, user check with username and menu

local FACE_MEDIA = 2

local url = ""

local htmlHeader = [=[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>@TITLE@</title>
  @STYLE@
</head>
<body>
  @BODY@
</body>
</html>
]=]

local htmlStyle = [=[
  <style type="text/css">
    body {
      font-family: Arial, sans-serif;
      background-color: #f0f8ff;
      color: #333;
      margin: 0;
      padding: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
    }
    h1 {
      text-align: center;
      color: #333;
      font-size: 2em;
      margin-bottom: 20px;
    }
    form {
      background-color: #fff;
      border-radius: 10px;
      box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
      padding: 20px;
      width: 300px;
    }
    label {
      font-weight: bold;
      color: #666;
    }
    input[type="text"] {
      width: 100%;
      padding: 10px;
      margin-top: 5px;
      margin-bottom: 20px;
      border-radius: 5px;
      border: 1px solid #ccc;
      box-sizing: border-box;
    }
    input[type="radio"] {
      margin-right: 10px;
    }
    input[type="submit"] {
      width: 100%;
      padding: 10px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 5px;
      font-size: 1em;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    input[type="submit"]:hover {
      background-color: #45a049;
    }
    p {
      font-size: 1.1em;
      color: #333;
      margin-bottom: 10px;
    }
  </style>
]=]

local htmlFruits = [=[
  <form action="fruits" method="post">
    <h1>Select Your Favorite Fruit</h1>
    <label for="name">Your username:</label>
    <input type="text" id="name" name="name" />
    <p>Please select your favorite fruit:</p>
      <input type="radio" id="apple" name="fruit" value="apple" />
      <label for="apple">Apple</label>
    <br />
      <input type="radio" id="banana" name="fruit" value="banana" />
      <label for="banana">Banana</label>
    <br />
      <input type="radio" id="orange" name="fruit" value="orange" />
      <label for="orange">Orange</label>
    <br />
      <input type="radio" id="grape" name="fruit" value="grape" />
      <label for="grape">Grape</label>
    <br /><br />
    <input type="submit" value="Submit" />
  </form>
]=]

local htmlFruitsTitle = "Fruit Selection Form"

local menuChannel = 0
local selection = {}

local function show(url)
    ll.SetPrimMediaParams(FACE_MEDIA, {
        PRIM_MEDIA_CURRENT_URL, url,
        PRIM_MEDIA_HOME_URL, url,
        PRIM_MEDIA_AUTO_ZOOM, false,
        PRIM_MEDIA_FIRST_CLICK_INTERACT, true,
        PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE,
        PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE,
        PRIM_MEDIA_AUTO_PLAY, true,
        PRIM_MEDIA_WIDTH_PIXELS, 512,
        PRIM_MEDIA_HEIGHT_PIXELS, 512
    })
end

local function getPage(path)
    local html = ""
    if path == "/fruits" then
        html = ll.ReplaceSubString(htmlHeader, "@STYLE@", htmlStyle, 0)
        html = ll.ReplaceSubString(html, "@TITLE@", htmlFruitsTitle, 0)
        html = ll.ReplaceSubString(html, "@BODY@", htmlFruits, 0)
    end
    return html
end

local function sayParams(userId, selFruit)
    ll.OwnerSay(`Parameters:\nname={ll.GetUsername(userId)}\nfruit={selFruit}`)
end

local function parseQuery(query)
    local params = {}
    for key, value in query:gmatch("([^&=]+)=?([^&]*)") do
        params[ll.UnescapeURL(key)] = ll.UnescapeURL((value:gsub("+"," ")))
    end
    return params
end

local function checkUser(params)
    local userId = ll.Name2Key(params.name)
    if userId.istruthy then
        local selFruit = params.fruit
        local message = "Poll Board\n\nPlease confirm that your selection of fruit is:\n" .. selFruit
        selection[userId] = selFruit
        ll.Dialog(userId, message, {"Yes", "No"}, menuChannel)
    end
end

local function initialize()
    menuChannel = tonumber(bit32.bor(integer(0x80000000), integer("0x" .. tostring(ll.GetKey()))))
    ll.Listen(menuChannel, "", "", "")
    selection = {}
    ll.RequestURL()
end

function listen(channel, name, id, message)
    if channel == menuChannel then
        if selection[id] then
            if message == "Yes" then
                sayParams(id, selection[id])
            end
            selection[id] = nil
        end
    end
end

function http_request(id, method, body)
    if method == URL_REQUEST_GRANTED then
        url = body .. "/fruits"
        ll.OwnerSay(url)
        show(url)
    elseif method == URL_REQUEST_DENIED then
        ll.OwnerSay("Unable to get URL!")
    elseif method == "GET" then
        local path = ll.ToLower(ll.GetHTTPHeader(id, "x-path-info"))
        local query = ll.ToLower(ll.GetHTTPHeader(id, "x-query-string"))
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, getPage(path))
    elseif method == "POST" then
        checkUser(parseQuery(body))
        local path = ll.ToLower(ll.GetHTTPHeader(id, "x-path-info"))
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, getPage(path))
    end
end

function on_rez(start_param)
    ll.ResetScript()
end

function changed(change)
    if bit32.btest(change, bit32.bor(CHANGED_REGION_START, CHANGED_OWNER, CHANGED_INVENTORY)) then
        ll.ResetScript()
    end
end

initialize()

Form Poll, user check with code and browserHTMLCSS

-- Form Poll, user check with code and browser

local url = ""

local htmlHeader = [=[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>@TITLE@</title>
  @STYLE@
</head>
<body>
  @BODY@
</body>
</html>
]=]

local htmlStyle = [=[
  <style type="text/css">
    body {
      font-family: Arial, sans-serif;
      background-color: #f0f8ff;
      color: #333;
      margin: 0;
      padding: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
    }
    h1 {
      text-align: center;
      color: #333;
      font-size: 2em;
      margin-bottom: 20px;
    }
    form {
      background-color: #fff;
      border-radius: 10px;
      box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
      padding: 20px;
      width: 300px;
    }
    label {
      font-weight: bold;
      color: #666;
    }
    input[type="radio"] {
      margin-right: 10px;
    }
    input[type="submit"] {
      width: 100%;
      padding: 10px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 5px;
      font-size: 1em;
      cursor: pointer;
      transition: background-color 0.3s;
    }
    input[type="submit"]:hover {
      background-color: #45a049;
    }
    p {
      font-size: 1.1em;
      color: #333;
      margin-bottom: 10px;
    }
  </style>
]=]

local htmlFruits = [=[
  <form action="fruits" method="post">
    <h1>Select Your Favorite Fruit, @DISPLAY_NAME@</h1>
    <p>Please select your favorite fruit:</p>
      <input type="radio" id="apple" name="fruit" value="apple" />
      <label for="apple">Apple</label>
    <br />
      <input type="radio" id="banana" name="fruit" value="banana" />
      <label for="banana">Banana</label>
    <br />
      <input type="radio" id="orange" name="fruit" value="orange" />
      <label for="orange">Orange</label>
    <br />
      <input type="radio" id="grape" name="fruit" value="grape" />
      <label for="grape">Grape</label>
    <br /><br />
    <input type="hidden" name="userCode" value="@USER_CODE@" />
    <input type="submit" value="Submit" />
  </form>
]=]

local htmlFruitsTitle = "Fruit Selection Form"

local htmlThanks = [=[
  <h1>Fantastic selection of fruit, @DISPLAY_NAME@!</h1>
]=]

local htmlThanksTitle = "Thanks for your selection"

local userCodes = {}

local function getUserCode()
    local code = 0
    repeat
        code = tonumber(integer(ll.Frand(1000000000))) 
    until not userCodes[code]
    return code
end

local function parseQuery(query)
    local params = {}
    for key, value in query:gmatch("([^&=]+)=?([^&]*)") do
        params[ll.UnescapeURL(key)] = ll.UnescapeURL((value:gsub("+"," ")))
    end
    return params
end

local function getPage(path, params)
    local html = ""
    local userCode = tonumber(params.userCode)
    if userCodes[userCode] then
        local displayName = userCodes[userCode].displayName
        html = ll.ReplaceSubString(htmlHeader, "@STYLE@", htmlStyle, 0)
        if path == "/fruits" then
            html = ll.ReplaceSubString(html, "@TITLE@", htmlFruitsTitle, 0)
            html = ll.ReplaceSubString(html, "@BODY@", htmlFruits, 0)
            html = ll.ReplaceSubString(html, "@USER_CODE@", tostring(userCode), 0)
        elseif path == "/thanks" then
            html = ll.ReplaceSubString(html, "@TITLE@", htmlThanksTitle, 0)
            html = ll.ReplaceSubString(html, "@BODY@", htmlThanks, 0)
        end
        html = ll.ReplaceSubString(html, "@DISPLAY_NAME@", displayName, 0)
    end
    return html
end

local function sayParams(params)
    local paramList = {}
    for key, value in params:gmatch("([^&=]+)=?([^&]*)") do
        table.insert(paramList, `{ll.UnescapeURL(key)}={ll.UnescapeURL((value:gsub("+"," ")))}`)
    end
    ll.OwnerSay(`Parameters: \n{table.concat(paramList, "\n")}`)
end

local function initialize()
    ll.SetText("Touch to select your favorite fruit", vector(1, 1, 1), 1)
    userCodes = {}
    ll.RequestURL()
end

function touch_start(num_detected)
    local userId = ll.DetectedKey(0)
    local userCode = getUserCode()
    local displayName = ll.GetDisplayName(userId)
    userCodes[userCode] = { userId = userId, displayName = displayName }
    local urlParams = `{url}?userCode={userCode}`
    ll.RegionSayTo(userId, 0, urlParams)
    ll.LoadURL(userId, "please go to this link to select your fruit", urlParams)
end

function http_request(id, method, body)
    if method == URL_REQUEST_GRANTED then
        url = body .. "/fruits"
    elseif method == URL_REQUEST_DENIED then
        ll.OwnerSay("Unable to get URL!")
    elseif method == "GET" then
        local path = ll.ToLower(ll.GetHTTPHeader(id, "x-path-info"))
        local query = ll.GetHTTPHeader(id, "x-query-string")
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, getPage(path, parseQuery(query)))
    elseif method == "POST" then
        local userCode = tonumber(parseQuery(body).userCode)
        if userCodes[userCode] then
            local userInfo = userCodes[userCode]
            ll.OwnerSay(`User: {userInfo.userId} {userInfo.displayName}`)
            sayParams(body)
            ll.SetContentType(id, CONTENT_TYPE_XHTML)
            ll.HTTPResponse(id, 200, getPage("/thanks", parseQuery(body)))
            userCodes[userCode] = nil
        else
            ll.SetContentType(id, CONTENT_TYPE_XHTML)
            ll.HTTPResponse(id, 200, getPage("/fruits", parseQuery(body)))
        end
    end
end

function on_rez(start_param)
    ll.ResetScript()
end

function changed(change)
    if bit32.btest(change, bit32.bor(CHANGED_REGION_START, CHANGED_OWNER, CHANGED_INVENTORY)) then
        ll.ResetScript()
    end
end

initialize()

Form Event

A script using most of the controls available in HTML Forms

Form Event with variety of controlsHTMLCSS

-- Form Event with variety of controls

local FACE_MEDIA = 2

local url = ""

local htmlHeader = [=[
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>@TITLE@</title>
  @STYLE@
</head>
<body>
  @BODY@
</body>
</html>
]=]

local htmlStyle = [=[
  <style type="text/css">
    body {
      font-family: Arial, sans-serif;
      background: linear-gradient(to right, #ffecd2, #fcb69f);
      margin: 0;
      padding: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      overflow: hidden; /* Ensure no scrollbars appear on body */
    }

    .form-container {
      background: white;
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0px 10px 15px rgba(0, 0, 0, 0.1);
      width: 100%;
      max-width: 600px;
      box-sizing: border-box;
      overflow: auto; /* Allow scrolling if content overflows */
      max-height: 90vh; /* Limit maximum height */
    }

    .form-container h2 {
      text-align: center;
      color: #333;
      margin-bottom: 20px;
    }

    .form-group {
      margin-bottom: 15px;
    }

    .form-group label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
      color: #555;
    }

    .form-group input[type="text"],
    .form-group input[type="email"],
    .form-group input[type="number"],
    .form-group input[type="date"],
    .form-group input[type="time"],
    .form-group input[type="url"],
    .form-group input[type="tel"],
    .form-group input[type="password"],
    .form-group select,
    .form-group textarea {
      width: 100%;
      padding: 10px;
      border-radius: 5px;
      border: 1px solid #ccc;
      font-size: 14px;
      margin-top: 5px;
      box-sizing: border-box;
    }

    .form-group textarea {
      resize: vertical;
      height: 100px;
    }

    .form-group input[type="file"] {
      padding: 5px;
    }

    .form-group input[type="radio"],
    .form-group input[type="checkbox"] {
      margin-right: 10px;
    }

    .form-group button {
      width: 100%;
      padding: 12px;
      background-color: #ff7e5f;
      border: none;
      border-radius: 5px;
      color: white;
      font-size: 16px;
      cursor: pointer;
      transition: background-color 0.3s;
    }

    .form-group button:hover {
      background-color: #feb47b;
    }
  </style>
]=]

local htmlEvents = [=[
  <div class="form-container">
    <h2>Event Registration Form</h2>
    <form action="events" method="post">
      <!-- Text Input -->
      <div class="form-group">
        <label for="eventName">Event Name:</label>
        <input type="text" id="eventName" name="eventName" required="required" />
      </div>
      <!-- Email Input -->
      <div class="form-group">
        <label for="email">Contact Email:</label>
        <input type="email" id="email" name="email" />
      </div>
      <!-- Password Input -->
      <div class="form-group">
        <label for="password">Password:</label>
        <input type="password" id="password" name="password" />
      </div>
      <!-- Number Input -->
      <div class="form-group">
        <label for="attendees">Number of Attendees:</label>
        <input type="number" id="attendees" name="attendees" min="1" max="5000" />
      </div>
      <!-- Date Input -->
      <div class="form-group">
        <label for="eventDate">Event Date:</label>
        <input type="date" id="eventDate" name="eventDate" />
      </div>
      <!-- Time Input -->
      <div class="form-group">
        <label for="eventTime">Event Time:</label>
        <input type="time" id="eventTime" name="eventTime" />
      </div>
      <!-- URL Input -->
      <div class="form-group">
        <label for="eventWebsite">Event Website:</label>
        <input type="url" id="eventWebsite" name="eventWebsite" />
      </div>
      <!-- Telephone Input -->
      <div class="form-group">
        <label for="contactNumber">Contact Number:</label>
        <input type="tel" id="contactNumber" name="contactNumber" />
      </div>
      <!-- File Input -->
      <div class="form-group">
        <label for="eventBanner">Event Banner:</label>
        <input type="file" id="eventBanner" name="eventBanner" />
      </div>
      <!-- Radio Buttons -->
      <div class="form-group">
        <label>Event Type:</label>
        <input type="radio" id="online" name="eventType" value="Online" />
        <label for="online">Online</label>
        <input type="radio" id="offline" name="eventType" value="Offline" />
        <label for="offline">Offline</label>
      </div>
      <!-- Checkbox -->
      <div class="form-group">
        <label>Additional Services:</label>
        <input type="checkbox" id="catering" name="services" value="Catering" />
        <label for="catering">Catering</label>
        <input type="checkbox" id="photography" name="services" value="Photography" />
        <label for="photography">Photography</label>
        <input type="checkbox" id="soundSystem" name="services" value="Sound System" />
        <label for="soundSystem">Sound System</label>
      </div>
      <!-- Dropdown -->
      <div class="form-group">
        <label for="eventLocation">Event Location:</label>
        <select id="eventLocation" name="eventLocation">
          <option value="europe">Europe</option>
          <option value="america">America</option>
          <option value="asia">Asia</option>
          <option value="africa">Africa</option>
          <option value="oceania">Oceania</option>
        </select>
      </div>
      <!-- Range Slider -->
      <div class="form-group">
        <label for="budget">Event Budget:</label>
        <input type="range" id="budget" name="budget" min="500" max="50000" step="500" value="5000" />
      </div>
      <!-- Color Picker -->
      <div class="form-group">
        <label for="themeColor">Event Theme Color:</label>
        <input type="color" id="themeColor" name="themeColor" value="#ff5733" />
      </div>
      <!-- Textarea -->
      <div class="form-group">
        <label for="eventDescription">Event Description:</label>
        <textarea id="eventDescription" name="eventDescription"></textarea>
      </div>
      <!-- Submit Button -->
      <div class="form-group">
        <button type="submit">Register Event</button>
        <button type="reset">Reset Form</button>
      </div>
    </form>
  </div>
]=]

local htmlEventsTitle = "Event Registration Form"

local htmlThanks = [=[
  <h1>Thanks for registering the event</h1>
]=]

local htmlThanksTitle = "Thanks for your info"

local function show(url)
    ll.SetPrimMediaParams(FACE_MEDIA, {
        PRIM_MEDIA_CURRENT_URL, url,
        PRIM_MEDIA_HOME_URL, url,
        PRIM_MEDIA_AUTO_ZOOM, false,
        PRIM_MEDIA_FIRST_CLICK_INTERACT, true,
        PRIM_MEDIA_PERMS_INTERACT, PRIM_MEDIA_PERM_ANYONE,
        PRIM_MEDIA_PERMS_CONTROL, PRIM_MEDIA_PERM_NONE,
        PRIM_MEDIA_AUTO_PLAY, true
    })
end

local function getPage(path, params)
    local html = ll.ReplaceSubString(htmlHeader, "@STYLE@", htmlStyle, 0)
    if path == "/events" then
        html = ll.ReplaceSubString(html, "@TITLE@", htmlEventsTitle, 0)
        html = ll.ReplaceSubString(html, "@BODY@", htmlEvents, 0)
    elseif path == "/thanks" then
        html = ll.ReplaceSubString(html, "@TITLE@", htmlThanksTitle, 0)
        html = ll.ReplaceSubString(html, "@BODY@", htmlThanks, 0)
    end
    return html
end

local function sayParams(params)
    local paramList = {}
    for key, value in params:gmatch("([^&=]+)=?([^&]*)") do
        table.insert(paramList, `{ll.UnescapeURL(key)}={ll.UnescapeURL((value:gsub("+"," ")))}`)
    end
    ll.Say(0, `Parameters: \n{table.concat(paramList, "\n")}`)
end

local function initialize()
    ll.RequestURL()
end

function http_request(id, method, body)
    if method == URL_REQUEST_GRANTED then
        url = body .. "/events"
        ll.Say(0, url)
        show(url)
    elseif method == URL_REQUEST_DENIED then
        ll.OwnerSay("Unable to get URL!")
    elseif method == "GET" then
        local path = ll.ToLower(ll.GetHTTPHeader(id, "x-path-info"))
        local query = ll.GetHTTPHeader(id, "x-query-string")
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, getPage(path, query))
    elseif method == "POST" then
        sayParams(body)
        local path = ll.ToLower(ll.GetHTTPHeader(id, "x-path-info"))
        ll.SetContentType(id, CONTENT_TYPE_XHTML)
        ll.HTTPResponse(id, 200, getPage("/thanks", body))
    end
end

function on_rez(start_param)
    ll.ResetScript()
end

function changed(change)
    if bit32.btest(change, bit32.bor(CHANGED_REGION_START, CHANGED_OWNER, CHANGED_INVENTORY)) then
        ll.ResetScript()
    end
end

initialize()