LSL http server/examples/hypermedia

From Second Life Wiki
Jump to navigation Jump to search

HyperMedia Toolkit

What

This is a series of scripts that allow you to create a (limited) HTML web server in a prim. This script can handle hyperlinks, form requests, invalid URL's. This system uses HTTP-In and Shared Media to serve and display pages on the prim the scripts are contained in.

How

I wrote this years back when I was still active, shortly after HTTP-In became live on Agni everyone began playing with different methods to get around the forced plain-text mime-type. This system combines a number of methods to allow full HTML serving and rendering from a single prim. Please excuse my lack of thorough documentation as it has been a few years since I was active in SL, and longer since I wrote this.

The HTML rendering is achieved by using a data URI to change the mimetype, in this URI I use a bit of javascript to load the HTML content from the prim's URL, which is then rendered as HTML. Things like form submissions and hyperlinks are handled by parsing data send back to the HTTP-In URL and parsing them in the script.

You should be able to drop all of these scripts into a prim (with the names given here), reset the object, and see the index HTML drawn on the prims face using Shared Media.

The Scripts

_Shared Media Library.lsl

// HyperMedia Shared Media Library
// By Zetaphor
/*
Link Numbers
500000 - Startup to page read scripts to read all notecards
500001 - Returned from read scripts to indicate notecard read
100000 - Page request (string pagename, key HTTPResponse ID)
100001 - Sent back from page request to check against current ID, str contains formatted page data
*/
key current_response;
string url;
string gurl; //Gateway URL
string current_page;
integer readystates;
string formr;
string errorp;
list uservars;

// This wraps the html you want to display so that it will be shown from links 
// made with build_url
string build_response(string body)
{
    return "function init() {document.getElementsByTagName('body')[0].innerHTML='" + body + "';}";
}

string check_tags(string text)
{
    while(llSubStringIndex(text,"%BURL%")!=-1)
    {
        integer index = llSubStringIndex(text,"%BURL%");
        text = llDeleteSubString(text,index,index+5);
        text = llInsertString(text,index,gurl);
    }
    while(llSubStringIndex(text,"%URL%")!=-1)
    {
        integer index = llSubStringIndex(text,"%URL%");
        text = llDeleteSubString(text,index,index+4);
        text = llInsertString(text,index,url);
    }
    integer i;
    integer count = fncStrideCount(uservars,2);
    for (i=0; i<count; i++) //Uservars replace
    {
        integer index;
        list var = fncGetStride(uservars, i, 2);
        while (llSubStringIndex(text,"%"+llList2String(var,0)+"%")!=-1)
        {
            index = llSubStringIndex(text,"%"+llList2String(var,0)+"%");
            text = llDeleteSubString(text,index,index+llStringLength(llList2String(var,0))+1);
            text = llInsertString(text,index,llList2String(var,1));         
        }
    } 
    
    if(llSubStringIndex(text,"%FORMR:")!=-1)
    {
        integer count=1;
        integer index = llSubStringIndex(text,"%FORMR:");
        integer found;
        string check;
        integer end_index;
        while (found==FALSE)
        {
            check = llGetSubString(text,index+count,index+count);
            if (check=="%"){found=TRUE;end_index = index+count;}
            count++;
        }
        formr = llGetSubString(text,index,end_index);
        list temp = llParseString2List(formr,[":","%"],[""]);
        formr = llList2String(temp,1);
        text = llDeleteSubString(text,index,end_index);
    }
    else
    {
        formr = "";
    }
    return text;
}

// Find a Stride within a List (returns stride index, and item subindex)
list fncFindStride(list lstSource, list lstItem, integer intStride)
{
  integer intListIndex = llListFindList(lstSource, lstItem);
  
  if (intListIndex == -1) { return [-1, -1]; }
  
  integer intStrideIndex = intListIndex / intStride;
  integer intSubIndex = intListIndex % intStride;
  
  return [intStrideIndex, intSubIndex];
}

// Returns number of Strides in a List
integer fncStrideCount(list lstSource, integer intStride)
{
  return llGetListLength(lstSource) / intStride;
}

// Replace a Stride in a List
list fncReplaceStride(list lstSource, list lstStride, integer intIndex, integer intStride)
{
  integer intNumStrides = fncStrideCount(lstSource, intStride);
  
  if (llGetListLength(lstStride) != intStride) { return lstSource; }
  
  if (intNumStrides != 0 && intIndex < intNumStrides)
  {
    integer intOffset = intIndex * intStride;
    return llListReplaceList(lstSource, lstStride, intOffset, intOffset + (intStride - 1));
  }
  return lstSource;
}

// Deletes a Stride from a List
list fncDeleteStride(list lstSource, integer intIndex, integer intStride)
{
  integer intNumStrides = fncStrideCount(lstSource, intStride);
  
  if (intNumStrides != 0 && intIndex < intNumStrides)
  {
    integer intOffset = intIndex * intStride;
    return llDeleteSubList(lstSource, intOffset, intOffset + (intStride - 1));
  }
  return lstSource;
}

// Returns a Stride from a List
list fncGetStride(list lstSource, integer intIndex, integer intStride)
{
  integer intNumStrides = fncStrideCount(lstSource, intStride);
  
  if (intNumStrides != 0 && intIndex < intNumStrides)
  {
    integer intOffset = intIndex * intStride;
    return llList2List(lstSource, intOffset, intOffset + (intStride - 1));
  }
  return [];
}


default
{
    state_entry()
    {
        llOwnerSay("Resetting scripts");
        llSetScriptState("_Storage Controller",TRUE);
        llSleep(1.0);        
        llSetScriptState("_Gateway Controller",TRUE);        
        llSleep(1.0);
        llResetOtherScript("_Storage Controller");
        llSleep(1.0);
        llMessageLinked(LINK_THIS,510000,"","");
        llSleep(1.0);        
        integer i;
        integer storcheck = llGetInventoryNumber(INVENTORY_SCRIPT);
        for (i=0; i<storcheck; i++)
        {
            if (llListFindList(llParseString2List(llGetInventoryName(INVENTORY_SCRIPT,i),[" "],[]),["~StorageObject"]) != -1)
            {
                llResetOtherScript(llGetInventoryName(INVENTORY_SCRIPT,i));
                llSetScriptState(llGetInventoryName(INVENTORY_SCRIPT,i),TRUE);
            }
        }        
        state startup;
    }
}

state shutdown
{
    state_entry()
    {
        llOwnerSay("Shutting Down");
        llSetScriptState("_Gateway Controller",FALSE);
        llSleep(1.0);
        llSetScriptState("_Storage Controller",FALSE);
        llSleep(1.0);        
        integer i;
        integer storcheck = llGetInventoryNumber(INVENTORY_SCRIPT);
        llResetOtherScript("_Gateway Controller");
        llSleep(1.0);        
        for (i=0; i<storcheck; i++)
        {
            if (llListFindList(llParseString2List(llGetInventoryName(INVENTORY_SCRIPT,i),[" "],[]),["~StorageObject"]) != -1)
            {
                llSetScriptState(llGetInventoryName(INVENTORY_SCRIPT,i),FALSE);
                llSleep(1.0);                
                llResetOtherScript(llGetInventoryName(INVENTORY_SCRIPT,i));
                llSleep(1.0);                                
            }
        }
        llOwnerSay("Shutdown complete");
    }
    
    link_message(integer se, integer n, string str, key id)
    {
        if (n == 1000001)
        {
            llResetScript();
        }
    }
}

state startup
{
    state_entry()
    {
        llOwnerSay("Starting up...");
        readystates = 0;
        llMessageLinked(LINK_THIS,500000,"","");
    }
    
    link_message(integer se, integer num, string str, key id)
    {
        if (num == 500001)
        {
            errorp = str;
            ++readystates;
            if (readystates == 2)
            {
                state running;
            }
        }
        else if (num == 200000)
        {
            gurl = str;
            ++readystates;
            if (readystates == 2)
            {
                state running;
            }
        }
    }
}

state running
{
    state_entry()
    {
        llOwnerSay("Requesting Server URL...");
        llRequestURL();
    }
    
    http_request(key id, string method, string body)
    {
        if (method == URL_REQUEST_GRANTED)
        {
            url = body;
            llMessageLinked(LINK_THIS,200001,body,"");            
        }
        else if (method == URL_REQUEST_DENIED)
        {
            llSay(0, "Something went wrong, no url. " + body);
        }
        else if (method == "GET")
        {
            current_response = id;
            list path = llParseString2List(llGetHTTPHeader(id,"x-path-info"),["/"],[]);            
            if (llGetListLength(path)!=0)
            {
                if (llList2String(path,0) == "link")
                {
                    current_page = llList2String(path,1);
                    llMessageLinked(LINK_THIS,100000,llList2String(path,1),id);
                }                    
            }
            else
            {
                current_page = "index";                
                llMessageLinked(LINK_THIS,100000,"index",id);
            }
        }
        else if (method=="POST")
        {
            current_response = id;
            string response = body;
            while (llSubStringIndex(response,"+")!=-1)
            {
                integer index = llSubStringIndex(response,"+");
                response = llDeleteSubString(response,index,index);
                response = llInsertString(response,index,"%20");
            }
            llMessageLinked(LINK_SET,600000,response,"");
            if (formr == "")
            {           
                llMessageLinked(LINK_THIS,700000,current_page,"");
            }
            else
            {
                current_page = formr;                
                llMessageLinked(LINK_THIS,700000,formr,"");
            }                
        }
        else
        {
            llHTTPResponse(id,405,"Unsupported Method");
        }
    }
    
    link_message(integer se, integer num, string str, key id)
    {
        if (num == 100001)
        {
            if (id == current_response)
            {
                if (str!="404")
                {
                    str = check_tags(str);
                    llHTTPResponse(id,200,build_response(str));
                }
                else
                {
                    llMessageLinked(LINK_THIS,700000,errorp,"");
                }
            }
        }
        else if (num == 800000)
        {
            list data = llParseString2List(str,["^"],[]);
            list indx = fncFindStride(uservars, [llList2String(data,0)], 2);
            if (llList2Integer(indx,0)!=-1)
            {
                if (llList2Integer(indx,1)==0)
                {
                    //llOwnerSay("Updated! "+llList2String(data,0)+": "+llList2String(data,1));
                    uservars = fncReplaceStride(uservars, [llList2String(data,0),llList2String(data,1)], llList2Integer(indx,0), 2);
                    //llOwnerSay("Results: "+llDumpList2String(uservars,","));
                }
            }
            else
            {
                //llOwnerSay("Created! "+llList2String(data,0)+": "+llList2String(data,1));                
                uservars += [llList2String(data,0),llList2String(data,1)];
                //llOwnerSay("Results: "+llDumpList2String(uservars,","));                
            }
        }
        else if (num == 800001)
        {
            //llOwnerSay("Delete Stride");            
            list indx = fncFindStride(uservars, [str], 2);            
            uservars = fncDeleteStride(uservars, llList2Integer(indx,0), 3);
            //llOwnerSay("Result: "+llDumpList2String(uservars,","));
        }
        else if (num == 1000000)
        {
            state shutdown;
        }
    }
}

_Gateway Controller.lsl

// HyperMedia Gateway Controller
// By Zetaphor
string my_url;
string main_url;
string current_page;
integer r;
integer display_face;
integer interactperm = PRIM_MEDIA_PERM_OWNER;

show(string html, integer face)
{
    html += "<span " + (string)((++r) % 10) + "/>";
 
    llSetPrimMediaParams(face,                  // Side to display the media on.
            [PRIM_MEDIA_AUTO_PLAY,TRUE,      // Show this page immediately
             PRIM_MEDIA_CURRENT_URL,html,    // The url if they hit 'home'
             PRIM_MEDIA_HOME_URL,html,       // The url currently showing
             PRIM_MEDIA_PERMS_INTERACT,interactperm,
             PRIM_MEDIA_PERMS_CONTROL,PRIM_MEDIA_PERM_NONE
             //PRIM_MEDIA_HEIGHT_PIXELS,512,   // Height/width of media texture will be
             //PRIM_MEDIA_WIDTH_PIXELS,512
             ]);  //   rounded up to nearest power of 2.
}
 
// This creates a data: url that will render the output of the http-in url 
// given.
string build_url(string burl)
{
    return "data:text/html," 
        + llEscapeURL("<html><head><script src='" + burl 
        + "' type='text/javascript'></script></head><body onload='init()'></body></html>");
}
 
// This wraps the html you want to display so that it will be shown from links 
// made with build_url
string build_response(string body)
{
    return "function init() {document.getElementsByTagName('body')[0].innerHTML='" + body + "';}";
}

default
{
    state_entry()
    {
        display_face = (integer)llGetObjectDesc();
    }
    
    http_request(key id, string method, string body)
    {
        if (method == URL_REQUEST_GRANTED)
        {
            my_url = body;
            llMessageLinked(LINK_THIS,200000,body,"");            
        }
        else if (method == URL_REQUEST_DENIED)
        {
            llOwnerSay("Something went wrong, no url. " + body);
        }
        else if (method == "GET")
        {
            list path = llParseString2List(llGetHTTPHeader(id,"x-path-info"),["/"],[]);            
            if (llGetListLength(path)!=0)
            {
                if (llList2String(path,0) == "link")
                {
                    if (llList2String(path,1) != current_page)
                    {
                        current_page = llList2String(path,1);
                        show(build_url(main_url+"/link/"+current_page),display_face);
                        llOwnerSay("Loading URL...");                        
                    }
                }                    
            }
            else
            {
                current_page = "index";
                show(build_url(main_url),display_face);
                llOwnerSay("Loading URL...");
            }
        }
        else
        {
            llHTTPResponse(id,405,"Unsupported Method");
        }
    }
    
    link_message(integer se, integer n, string str, key id)
    {
        if (n == 200001)
        {
            llOwnerSay("Server started, displaying index page");
            main_url = str;
            show(build_url(main_url),display_face);
        }
        else if (n == 500000)
        {
            if (my_url!="")
            {
                llReleaseURL(my_url);
            }
            llRequestURL();            
        }            
        else if (n == 700000)
        {
            show(build_url(main_url+"/link/"+str),display_face);
        }  
        else if (n == 900000)
        {
            interactperm = (integer)str;
        }                  
    }
}

_Storage Controller.lsl

// Storage Controller
// By Zetaphor
integer storage_num; //Number of storage scripts in inventory
integer storage_ready; //Integer of number of storage scripts loaded
string page_note = "_Pages";
string config_note = "_Config";

integer nline;
key nquery;
list pages;
string read = "config";
string errorp;

setperms(string perm)
{
    if (llToLower(perm) == "owner")
    {
        llMessageLinked(LINK_THIS,900000,(string)PRIM_MEDIA_PERM_OWNER,"");
    }
    else if (llToLower(perm) == "group")
    {
        llMessageLinked(LINK_THIS,900000,(string)PRIM_MEDIA_PERM_GROUP,"");        
    }
    else if (llToLower(perm) == "all")
    {
        llMessageLinked(LINK_THIS,900000,(string)PRIM_MEDIA_PERM_ANYONE,"");        
    }
    else if (llToLower(perm) == "none")
    {
        llMessageLinked(LINK_THIS,900000,(string)PRIM_MEDIA_PERM_NONE,"");        
    }
}

default
{    
    state_entry()
    {
        integer i;
        integer storcheck = llGetInventoryNumber(INVENTORY_SCRIPT);
        for (i=0; i<storcheck; i++)
        {
            if (llListFindList(llParseString2List(llGetInventoryName(INVENTORY_SCRIPT,i),[" "],[]),["~StorageObject"]) != -1)
            {
                ++storage_num;
            }
        }
    }
    
    link_message(integer se, integer n, string str, key id)
    {
        if (n==500000)
        {
            if (llGetInventoryType(page_note)!=INVENTORY_NONE)
            {
                llOwnerSay("Loading Configuration...");
                nline = 0;
                read = "config";
                nquery = llGetNotecardLine(config_note,0);
            }
        }
        else if (n==100000)
        {
            integer i;
            integer found;
            for (i=0; i<llGetListLength(pages); ++i)
            {
                if (str == llList2String(pages,i))
                {
                    found = TRUE;
                    i = llGetListLength(pages)+1;
                }
            }
            if (found == FALSE)
            {
                llMessageLinked(LINK_THIS,100001,"404",id);
            }
        }
        else if (id == "22222222-2222-2222-2222-222222222222")
        {
            ++storage_ready;
            if (storage_ready == storage_num)
            {
                llOwnerSay("Pages read into storage memory, starting server");
                llMessageLinked(LINK_THIS,500001,errorp,"");
            }
            else
            {
                llOwnerSay("Read "+(string)storage_ready+"/"+(string)storage_num);
            }
        }
    }
    
    dataserver(key q, string str)
    {
        if (q == nquery)
        {
            if (str!=EOF)
            {
                if (read == "pages")
                {                
                    list pdata = llParseString2List(str,[","],[]);
                    pages = llListInsertList(pages,[llList2String(pdata,1)],llList2Integer(pdata,0));
                    ++nline;
                    nquery = llGetNotecardLine(page_note,nline);
                }
                else if (read == "config")
                {
                    list data = llParseString2List(str,[","],[]);
                    if (llList2String(data,0) == "404")
                    {
                        errorp = llList2String(data,1);
                        llOwnerSay("404 page set to: "+errorp);                        
                    }
                    else if (llList2String(data,0) == "Interact")
                    {
                        setperms(llList2String(data,1));
                        llOwnerSay("Interact permission set to "+llList2String(data,1));
                    }
                    ++nline;
                    nquery = llGetNotecardLine(config_note,nline);                    
                }
            }
            else
            {
                if (read == "config")
                {
                    read = "pages";
                    llOwnerSay("Initializing Storage Objects");
                    nline = 0;
                    nquery = llGetNotecardLine(page_note,0);
                }
                else if (read == "pages")
                {
                    integer i;
                    for (i=0; i<llGetListLength(pages); i++)
                    {
                        llMessageLinked(LINK_THIS,i,llList2String(pages,i),"11111111-1111-1111-1111-111111111111");
                    } 
                }
            }
        }
    }
}

~Storage Object.lsl

Add more of these to give the object more memory to handle additional notecards. Example: ~Storage Object, ~Storage Object 1, ~Storage Object 2

// HyperMedia Storage Object
// By Zetaphor
string page_name;
integer page_num;
key lquery;
integer nline;

string page_data;

string check_line(string text)
{
    list parcelinfo = llGetParcelDetails(llGetPos(),[PARCEL_DETAILS_NAME,PARCEL_DETAILS_DESC]);
    while(llSubStringIndex(text,"%SIM%")!=-1)
    {
        integer index = llSubStringIndex(text,"%SIM%");
        text = llDeleteSubString(text,index,index+4);
        text = llInsertString(text,index,llGetRegionName());
    }
    while(llSubStringIndex(text,"%POSITION%")!=-1)
    {
        integer index = llSubStringIndex(text,"%POSITION%");
        text = llDeleteSubString(text,index,index+9);
        text = llInsertString(text,index,(string)llGetPos());
    }
    while(llSubStringIndex(text,"%PARCELNAME%")!=-1)
    {
        integer index = llSubStringIndex(text,"%PARCELNAME%");
        text = llDeleteSubString(text,index,index+11);
        text = llInsertString(text,index,llList2String(parcelinfo,0));
    }
    while(llSubStringIndex(text,"%PARCELDESC%")!=-1)
    {
        integer index = llSubStringIndex(text,"%PARCELDESC%");
        text = llDeleteSubString(text,index,index+11);
        text = llInsertString(text,index,llList2String(parcelinfo,1));
    }
    while(llSubStringIndex(text,"%ONAME%")!=-1)
    {
        integer index = llSubStringIndex(text,"%ONAME%");
        text = llDeleteSubString(text,index,index+6);
        text = llInsertString(text,index,llGetObjectName());
    }  
    while(llSubStringIndex(text,"%APPURL%")!=-1)
    {
        integer index = llSubStringIndex(text,"%APPURL%");
        text = llDeleteSubString(text,index,index+7);
        vector pos = llGetPos();
        text = llInsertString(text,index,"secondlife://"+llGetRegionName()+"/"+(string)llFloor(pos.x)+"/"+(string)llFloor(pos.x)+"/"+(string)llFloor(pos.z));
    }             
    while(llSubStringIndex(text,"%ODESC%")!=-1)
    {
        integer index = llSubStringIndex(text,"%ODESC%");
        text = llDeleteSubString(text,index,index+6);
        text = llInsertString(text,index,llGetObjectDesc());
    }             
    while(llSubStringIndex(text,"%TEXTUREID:")!=-1)
    {
        integer count=1;
        integer index = llSubStringIndex(text,"%TEXTUREID:");
        integer found;
        string check;
        integer end_index;
        while (found==FALSE)
        {
            check = llGetSubString(text,index+count,index+count);
            if (check=="%"){found=TRUE;end_index = index+count;}
            count++;
        }
        string link = llGetSubString(text,index,end_index);
        text = llDeleteSubString(text,index,end_index);
        list temp = llParseString2List(link,[":","%"],[""]);
        text = llInsertString(text,index,"http://secondlife.com/app/image/"+llList2String(temp,1)+"/1");
    }                
    while(llSubStringIndex(text,"%SLURL%")!=-1)
    {
        integer index = llSubStringIndex(text,"%SLURL%");
        text = llDeleteSubString(text,index,index+6);
        vector pos = llGetPos();
        text = llInsertString(text,index,"http://slurl.com/secondlife/"+llEscapeURL(llGetRegionName())+"/"+(string)llFloor(pos.x)+"/"+(string)llFloor(pos.y)+"/"+(string)llFloor(pos.z)+"/?title="+llEscapeURL(llList2String(parcelinfo,0)));
    }        
   
    return text;
}

default
{
    state_entry()
    {
        list namecheck = llParseString2List(llGetScriptName(),[" "],[]);
        if (llGetListLength(namecheck) == 2)
        {
            page_num = llList2Integer(namecheck,1);
        }
        else
        {
            page_num = 0;
        }
        llOwnerSay("Page Num: "+(string)page_num);
        llOwnerSay("Free Memory: "+(string)llGetFreeMemory()+" bytes");
    }
    
    link_message(integer se, integer n, string str, key id)
    {
        if (n == page_num && id == "11111111-1111-1111-1111-111111111111")
        {
            page_name = str;
            lquery = llGetNotecardLine(page_name,0);
        }
        else if (n == 100000 && str==page_name)
        {
            llMessageLinked(LINK_THIS,100001,page_data,id);
        }            
    }
    
    dataserver(key q, string str)
    {
        if (q == lquery)
        {
            if (str != EOF)
            {
                ++nline;
                page_data+=check_line(str);
                lquery = llGetNotecardLine(page_name,nline);
            }
            else
            {
                llMessageLinked(LINK_THIS,page_num,"","22222222-2222-2222-2222-222222222222");
            }
        }
    }            
}

(Example) Form Data Processor.lsl

list data;
default
{
    link_message(integer s, integer n, string str, key id)
    {
        if (n==600000)
        {
            llOwnerSay("Recieved POST data from a form");
            data = llParseString2List(str,["&"],[""]);
            integer i;
            for (i=0; i<llGetListLength(data); i++)
            {
                string temp_string = llList2String(data,i);
                list temp = llParseString2List(temp_string,["="],[""]);
                llOwnerSay("Field Name: "+llUnescapeURL(llList2String(temp,0))+" Data: "+llUnescapeURL(llList2String(temp,1)));
            }
        }
    }            
}

(Example) User Variables.lsl

default
{
    touch_start(integer total_number)
    {
        llMessageLinked(LINK_THIS,800000,"dsa^123","");
        llMessageLinked(LINK_THIS,800000,"blah^example","");
        llMessageLinked(LINK_THIS,800000,"this^works","");                
    }
}

Power On-Off.lsl

integer on = TRUE;
default
{
    state_entry()
    {
        llSay(0, "Hello, Avatar!");
    }

    touch_start(integer total_number)
    {
        if (on)
        {
            llMessageLinked(LINK_THIS,1000000,"","");
            on = FALSE;
        }
        else
        {
            on = TRUE;
            llMessageLinked(LINK_THIS,1000001,"","");
        }            
    }
}

The Notecards

_Config.notecard

404,NotFound
Interact,None

_Pages.notecard

0,index
1,test
2,NotFound

NotFound.notecard

<h1>404 Page Not Found!</h1><br />
<a href = "%BURL%/link/index">Index</a>

test.notecard

<base href="%BURL%/"> 
<a href = "link/index"><h1>Index</h1></a>

index.notecard

Sim Name: %SIM%<br />
Position: %POSITION%<br />
Parcel Name: %PARCELNAME%<br />
Parcel Description: %PARCELDESC%<br />
SLURL: <a href = "%SLURL%">Open SLURL</a><br />
SL App URL: <a href="%APPURL%">Open Place Info in Client</a><br/ >
Object Name: %ONAME%<br />
Object Desc: %ODESC%<br />
<br />
<img src = "%TEXTUREID:0d280a69-558d-15a9-66a3-2dcfc6fae236%">
<a href = "%BURL%/link/test"><h1>Test</h1></a><br />
<br />
%FORMR:test%
<form name="input" action="%URL%" method="post">
Text: <input type="text" name="txtbox" />
<input type="submit" value="Submit" />
</form> 
<br />Test Uservars: %dsa%
<br />%blah%
<br />%wtf%<br />
<a href = "%BURL%/link/bullshit">Test 404</a>