LSL http server/examples/hypermedia

From Second Life Wiki
< LSL http server‎ | examples
Revision as of 12:26, 12 August 2014 by Zetaphor Wirefly (talk | contribs) (Added initial content)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
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

<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;
       }
   }

} </lsl>

Gateway Controller.lsl

<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 += "";

   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;
       }                  
   }

} </lsl>

Storage Controller.lsl

<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");
                   } 
               }
           }
       }
   }

} </lsl>

~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 <lsl> // 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");
           }
       }
   }            

} </lsl>

(Example) Form Data Processor.lsl

<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)));
           }
       }
   }            

} </lsl>

(Example) User Variables.lsl

<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","");                
   }

} </lsl>

Power On-Off.lsl

<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,"","");
       }            
   }

} </lsl

The Notecards

_Config.notecard

<lsl> 404,NotFound Interact,None </lsl>

_Pages.notecard

<lsl> 0,index 1,test 2,NotFound </lsl>

NotFound.notecard

<lsl>

404 Page Not Found!


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

test.notecard

<lsl> <base href="%BURL%/">

<a href = "link/index">

Index

</a>

</lsl>

index.notecard

<lsl> Sim Name: %SIM%
Position: %POSITION%
Parcel Name: %PARCELNAME%
Parcel Description: %PARCELDESC%
SLURL: <a href = "%SLURL%">Open SLURL</a>
SL App URL: <a href="%APPURL%">Open Place Info in Client</a>
Object Name: %ONAME%
Object Desc: %ODESC%

<img src = "%TEXTUREID:0d280a69-558d-15a9-66a3-2dcfc6fae236%">

<a href = "%BURL%/link/test">

Test

</a>


%FORMR:test% <form name="input" action="%URL%" method="post"> Text: <input type="text" name="txtbox" /> <input type="submit" value="Submit" /> </form>
Test Uservars: %dsa%
%blah%
%wtf%
<a href = "%BURL%/link/bullshit">Test 404</a> </lsl>