Wednesday, August 24, 2011

Nitrogen, an Erlang web application library/framework Part 10


For those just now encountering this series, you'll want to start with Part 1, and at least have Introduction to Nitrogen open in another tab.


Slide 80: Extending Nitrogen: Handlers - Part 3 - continued


I floundered around in the code until I found a mention of the logging that Nitrogen does. Logging! The first thing I should have looked at. There, in /home/sps/erlang-libs/nitrogen/rel/nitrogen/log/sasl-error.log I found this:

exception throw: {must_define_a_nitrogen_behaviour,my_security_handler}
in function wf_handler:set_handler/2
in call from nitrogen_mochiweb:loop/1
in call from mochiweb_http:headers/5

But I did define a nitrogen behavior! Except, apparently, Erlang shows its european heritage by using the British spelling of behavioUr. Fix that. Compile. Run. Behave as expected!

The End


That's the end of the tutorial, and thus the end of this series. I'll start a new series when I encounter a new tutorial to study thoroughly.

Sunday, August 7, 2011

Nitrogen, an Erlang web application library/framework Part 9


For those just now encountering this series, you'll want to start with Part 1, and at least have Introduction to Nitrogen open in another tab.


Slide 78: Extending Nitrogen: Handlers - Part 1


Ok, good, you can override nearly anything you might care about.


Slide 79: Extending Nitrogen: Handlers - Part 2


Do not attempt to override something in a stage you haven't reached yet. Pages go through ten stages - but how do you specify where in the process your code executes?


Slide 80: Extending Nitrogen: Handlers - Part 3


Answer: you specify a behavior corresponding to the stage of processing you're affecting, and write functions appropriate to that behavior. It's not clear where the new code is supposed to live. Is there other code that appears similar to my_security handler?

bash-4.1$ pwd
/home/sps/erlang-libs/nitrogen/rel/nitrogen/site
bash-4.1$ find . -name '*security*' -print
bash-4.1$ cd ..
bash-4.1$ find . -name '*security*' -print
bash-4.1$ cd ..
bash-4.1$ find . -name '*security*' -print
bash-4.1$ cd ..
bash-4.1$ find . -name '*security*' -print
./Quickstart/src/demos/demos_security_restricted.erl
./Quickstart/src/demos/demos_security_login.erl
./Quickstart/src/demos/demos_security.erl
./apps/nitrogen/src/handlers/security
./apps/nitrogen/src/handlers/security/http_basic_auth_security_handler.erl
./apps/nitrogen/src/handlers/security/security_handler.erl
./apps/nitrogen/src/handlers/security/default_security_handler.erl
./apps/nitrogen/ebin/default_security_handler.beam
./apps/nitrogen/ebin/security_handler.beam
./apps/nitrogen/ebin/http_basic_auth_security_handler.beam
bash-4.1$ pwd
/home/sps/erlang-libs/nitrogen

Hmm. I don't want to plunk it down with the other canonical source. When we added a new action, it went in site/src/actions. Let's put our handler in site/src/handlers.


Slide 80: Extending Nitrogen: Handlers - Part 3 (more)


Um, nitrogen_inets.erl. That's located...
bash-4.1$ pwd
/home/sps/erlang-libs/nitrogen/rel/nitrogen/site
bash-4.1$ find . -name nitrogen_inets.erl -print
bash-4.1$ cd ..; find . -name nitrogen_inets.erl -print
bash-4.1$ cd ..; find . -name nitrogen_inets.erl -print
./overlay_inets/site/src/nitrogen_inets.erl
bash-4.1$

Ok, I think that was created when we created the site. I'm going to guess that a new file in our src directory will get combined with the existing code in some useful way, so I'm going to put this in /home/sps/erlang-libs/nitrogen/rel/nitrogen/site. Looking at the other nitrogen_inets.erl file, we see that there are (of course) -module and -export statements in the file as well. Also, looking at that file, it appears we're replacing its function instead of augmenting it.

Let's fire up nitrogen again:



bash-4.1$ cd bin
/home/sps/erlang-libs/nitrogen/rel/nitrogen/bin
bash-4.1$ ls
dev nitrogen
bash-4.1$ ./nitrogen console
Exec: /home/sps/erlang-libs/nitrogen/rel/nitrogen/erts-5.8.2/bin/erlexec -boot /home/sps/erlang-libs/nitrogen/rel/nitrogen/releases/2.0.4/nitrogen -embedded -config /home/sps/erlang-libs/nitrogen/rel/nitrogen/etc/app.config -config /home/sps/erlang-libs/nitrogen/rel/nitrogen/etc/mochiweb.config -args_file /home/sps/erlang-libs/nitrogen/rel/nitrogen/etc/vm.args -- console
Root: /home/sps/erlang-libs/nitrogen/rel/nitrogen
Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:5] [kernel-poll:true]

Eshell V5.8.2 (abort with ^G)
(nitrogen@127.0.0.1)1> Starting Mochiweb Server (nitrogen) on 0.0.0.0:8000, root: './site/static'

...and in another window

bash-4.1$ ./dev compile
:: MAKE - site
Recompile: ./src/nitrogen_inets
Recompile: ./src/handlers/my_security_handler
:: MAKE - ./site
:: Done!
bash-4.1$

And now, did all that work? Pointing the web browser at http://localhost:8000, we get prompted for a login.

Not behaving as expected: I can still get to http://localhost:8000/chat, which, not being prefixed by "my_", shouldn't work.


WHOOPS!


At some point I must have played with using the mochiweb server instead of inets, and that means nitrogen_inets.erl is getting ignored. Let's check the git log to see if I can back up to where I switched.


Way, way back at the 4th screen! Which I didn't even think was worth mentioning. Sheesh. Ok, so now I've bought myself some, uh, experience, and I'll need to come up with nitrogen_mochiweb.erl instead of nitrogen_inets.erl. Let's see if I can pull that off. How about doing this:



bash-4.1$ git diff
diff --git a/rel/nitrogen/site/src/nitrogen_mochiweb.erl b/rel/nitrogen/site/src
index 0ee336a..f2c7dd0 100644
--- a/rel/nitrogen/site/src/nitrogen_mochiweb.erl
+++ b/rel/nitrogen/site/src/nitrogen_mochiweb.erl
@@ -25,4 +25,5 @@ loop(Req) ->
RequestBridge = simple_bridge:make_request(mochiweb_request_bridge, {Req, D
ResponseBridge = simple_bridge:make_response(mochiweb_response_bridge, {Req
nitrogen:init_request(RequestBridge, ResponseBridge),
+ nitrogen:handler(my_security_handler, []),
nitrogen:run().

Stop the nitrogen server, compile that, then start nitrogen again. OK, the index now gives a blank page, probably because it isn't prefixed with "my_". But http://localhost:8000/my/page doesn't work either. I'm going to need a deeper dive into the code to make this work.

Monday, June 27, 2011

Nitrogen, an Erlang web application library/framework Part 8


For those just now encountering this series, you'll want to start with Part 1, and at least have Introduction to Nitrogen open in another tab.


Slide 75: Extending Nitrogen: Custom Actions - Part 1


As described in the tutorial, create the file for the action, with its dummied code.


bash-4.1$ ./dev action my_action
Created action: ./site/src/actions/action_my_action.erl
Remember to recompile!
bash-4.1$

Slide 76: Extending Nitrogen: Custom Actions - Part 2



OK, a record and one mandatory function. Again following the suggestion of the comment, I move the record to ${Root}/include/records.hrl. I also change the dummy code in render_action/1 to match the tutorial example. Not much to typo there, but let's see...



Nonetheless, I manage to make one: the dummy function in action_my_action.erl has an underscore prefixed to the matching variable _Record, but since we actually use that variable in the function body, it must not be anonymous (as an underscore declares it). Fix that, and I get a clean compile:


bash-4.1$ ./dev compile
:: MAKE - site
Recompile: ./src/my_page
Recompile: ./src/login
Recompile: ./src/chat
Recompile: ./src/elements/element_my_element
Recompile: ./src/actions/action_my_action
:: MAKE - ./site
:: Done!

Slide 77: Extending Nitrogen: Custom Actions - Part 3



Now to use it in my_page.erl. Yup, that worked, after I removed an extraneous -> I'd introduced (what was I thinking?).

Tuesday, March 29, 2011

Nitrogen, an Erlang web application library/framework Part 7

Slide 71: Extending Nitrogen: Custom Elements - Part 1


Picking up again, ./dev element my_element won't run unless nitrogen is running. So in one window:

bash-4.1$ ./dev element my_element
Node is not running!
bash-4.1$ ./nitrogen console
Exec: /home/sps/erlang-libs/nitrogen/rel/nitrogen/erts-5.8.2/bin/erlexec -boot /home/sps/erlang-libs/nitrogen/rel/nitrogen/releases/2.0.4/nitrogen -embedded -config /home/sps/erlang-libs/nitrogen/rel/nitrogen/etc/app.config -config /home/sps/erlang-libs/nitrogen/rel/nitrogen/etc/mochiweb.config -args_file /home/sps/erlang-libs/nitrogen/rel/nitrogen/etc/vm.args -- console
Root: /home/sps/erlang-libs/nitrogen/rel/nitrogen
Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:5] [kernel-poll:true]

Eshell V5.8.2 (abort with ^G)
(nitrogen@127.0.0.1)1> Starting Mochiweb Server (nitrogen) on 0.0.0.0:8000, root: './site/static'

...and in the other window:

bash-4.1$ ./dev element my_element
Created element: ./site/src/elements/element_my_element.erl
Remember to recompile!
bash-4.1$

Slide 72: Extending Nitrogen: Custom Elements - Part 2



OK, record and two mandatory functions.

Slide 73: Extending Nitrogen: Custom Elements - Part 3



Hmmmm. The tutorial presents only one of the two mandatory functions, and no record. Let's see how creative my typos are this time.



Ah, the missing pieces are dummied in the ${Root}/src/elements/element_my_element.erl file, along with an instruction to move the record definition to the ${Root}/include/records.hrl. I'll leave them be, and alter only the render_element function.


Slide 74: Extending Nitrogen: Custom Elements - Part 4



After replacing the contents of my_page.erl following the title/0 definition with the recommended code, and recompiling (fixing variable name typos until getting a clean compile),
the page http://localhost:8000/my/page works as expected.



OK; so if you have a number of form elements that repeatedly occur together, then you can bundle them up with this kind of "custom" element. It's not yet clear to me if this would be useful for an entirely new kind of element, e.g., a "treebutton" which would toggle between expanded and collapsed representation of a tree.

Saturday, January 22, 2011

Nitrogen, an Erlang web application library/framework Part 6

Slide 60: Overview of Nitrogen Validation (#is_required{})



wf:wire/3 associates the validation with its first argument, here a specific element labeled "submit", and specifies the validation target according to element label. The wf:wire/3 calls occur before the definition of the elements.

Slide 61: Overview of Nitrogen Validation (#custom{} and #js_custom{})


#custom{} for the server, #js_custom{} for the client. My experience in this exercise is an uninteresting series of typos, syntax errors, and misremembered names. The biggest take away is that Internal server error usually means I've called an undefined function - specifically, misremembered the name of a system function. To reorient myself after my absence, here's the full login.erl code:

%% -*- mode: nitrogen -*-
-module (login).
-compile(export_all).
-include_lib("nitrogen/include/wf.hrl").
-include("records.hrl").

main() -> #template { file="./site/templates/bare.html" }.

title() -> "Hello from login.erl!".

body() ->
wf:wire(submit, username, #validate { validators = [
#is_required { text = "Required" }
]}),
wf:wire(submit, password, #validate { validators = [
#is_required { text = "Required" },
#custom {
text = "Invalid password.",
function = fun(_, Value) -> Value == "password" end
}
]}),
#panel { style="margin: 50px;", body=[
#flash {},
#label { text="Username" },
#textbox { id=username, next=password },
#br {},
#label { text="Password" },
#password { id=password, next=submit },
#br {},
#button { text="Login", id=submit, postback=login }
]}.

event(login) ->
wf:role(managers, true),
wf:redirect_from_login("/").


Slide 66: A Comet Counter



How boring: I typed it correctly, and it worked as described :-).


Slide 67: Comet Pools



Functions wf:comet/2 and wf:send/2. Vague: does a function have to execute a receive to receive these messages, or does the message appear as the argument to the function?


Slide 68: Comet Pool Scope



Normally, scoped by page and user, with a refresh replacing old scope with new one. With wf:comet_global/2 and wf:send_global/2, they live for the life of the server.


Slide 69: The Simplest Chatroom Ever Constructed



Answer revealed! Functions do have to invoke receive to get pool messages. To observe the semi-obvious: the nitrogen console process must be in the foreground (e.g., running and attached to the terminal) for dev page newpage to function correctly.

Saturday, January 15, 2011

Nitrogen, an Erlang web application library/framework Part 5

Slide 48: Session State


I"m going to do minor variant on the exercise: the incrementing counter will belong to the session, the doubling counter will belong to the page. That way we're a few clicks further away from overflow.... Here's my result:

body() ->
#panel { style="margin: 50px", body=[
#button { id=mybutton, text="Submit", postback=click },
#panel { id=placeholder1, body=wf:f("~B", [wf:session_default(click_counter, 0)]) },
#panel { id=placeholder2, body="1" }
]}.

event(click) ->
Counter = wf:session_default(click_counter, 0) + 1,
wf:update(placeholder1, wf:f("~B", [Counter])),
wf:session(click_counter, Counter),
Doubler = wf:state_default(click_doubler, 1) * 2,
wf:update(placeholder2, wf:f("~B", [Doubler])),
wf:state(click_doubler, Doubler).

Slide 49: Their answer


Note: second argument to wf:update/2 is a string; I originally surrounded it with a #panel (i.e., div element) so I could say body=, but that was unnecessary, so the result above incorporates Slide 49's simplified answer (although I'm using the helper function wf:f because it's less to type than io_lib:format).

Slide 50: Security


Slide 51: Limiting Access to a Page (roles)


Slide 52: Authentication and Authorization Functions


wf:user/0 and wf:user/1 seem straightforward. wf:role/1 and wf:role/2 are opaque to me. Hypothesis: wf:role(Role) returns true/false whether the session has that role; wf:role(Role, IsInRole) uses IsInRole false to revoke that role from the session or IsInRole true to add that role to the session. It looks liek they're going to make me read the manual.

Slide 53: Page Redirection functions


wf:redirect/1

wf:redirect_to_login/1

wf:redirect_from_login/1

There is apparently something special about the LoginUrl.

Slde 54: Creating a Secure Page - Step 1


They don't give the name of this page. I'm going to name it managers.erl.

Note! The main/0 function has to this point done nothing but specify the html template. This code is orthogonal to that for title, body, and events.


bash-4.1$ ./dev page managers
Created page: ./site/src/managers.erl
Remember to recompile!

Slide 55: Creating a Secure Page - Step 2



bash-4.1$ ./dev page login
Created page: ./site/src/login.erl
Remember to recompile!

First - hey, I guessed correctly as to how wf:role works! Second, since the example redirects a successful login to /, I infer that the main/0 function that was supposed to have the authorization guard was index.erl. I'll go fix that page.

I'm getting an error on the login page, somewhere in the click event handler. My broken code:

body() ->
#button { text="Login", postback=login }.

event(click) ->
wf:role(managers, true),
wf:redirect_from_login("/").

Let's review: Events have types. Types have triggers. Triggers are associated with elements. The element in this case is a button which has an implicit type of "click". The associated trigger is named "login". In the event/1 definition, I need to specify the trigger, not the type. So replace event(click) with event(login). That worked.

Slide 56: Creating a Secure Page - Step 3


When I first tried the example, it let me right in. Roles are associated with sessions, and I'd already acquired the "managers" role with the previous example. Let's kill off the Nitrogen server and start it back up. Yup, that was it. "flash{}" wasn't the obnoxious tag I was afraid of :-).

Slide 57: Creating a Secure Page - Step 4


Ok. Where to put a "logout" button? index.erl and my_page; not login since you don't get there unless you're not logged in. index.erl looks like this:

body() ->
#container_12 { body=[
#grid_8 { alpha=true, prefix=2, suffix=2, omega=true, body=inner_body() }
]}.

inner_body() ->
[
#h1 { text="Welcome to My Nitrogen Tutorial" },
#p{},
"
If you can see this page, then your Nitrogen server is up and
running. Click the button below to test postbacks.
",
#p{},
#button { id=button, text="Click me!", postback=click },
#p{},
"
Run ./bin/dev help to see some useful developer commands.
",
#p{},
#button { text="Logout", postback=logout }
].

event(logout) ->
wf:logout(),
wf:redirect("/");
event(click) ->
wf:replace(button, #panel {
body="You clicked the button!",
actions=#effect { effect=highlight }
}).

HOWEVER, a logout button is something you'd want on every page. Is there a way to do that with the template "bare.html"?

The page template might interpolate [[[logout:logout_body]]] to put the logout button on the page, but how do we augment every event/1 function with the event(logout) action?

Friday, January 14, 2011

Nitrogen, an Erlang web application library/framework Part 4

Slide 40: More Event Examples


"mouseout" is distinguished only as being the last of the three event types to be mentioned within the mybutton list. Let's see what happens when we exchange that line with the line for the "click" type.

Aha; now only the click type has a function associated with it. Those "#event" elements look as if they're contained in a list. List elements are normally separated by a comma (","). Let's try putting a comma at the end of the first two "#event" lines.

Yes! Now we get all three events observed, and the page is working as intended.

Slide 41: More Event Examples


Key takeaway: wf:q(ElementID) lets you process data returned to the Nitrogen server.

Slide 42: Modifying Elements


A few things to note: to make it easier to see what was being replaced, I gave the initial panel some contents like so:

body() ->
#panel { style="margin: 50px", body=[
#button { id=mybutton, text="Submit", postback=click },
#panel { id=placeholder, body=[ #p { body="Text to replace" } ] }
]}.

Another thing: the "click" event can be triggered multiple times; when it does so, the "placeholder" element gets replaced each time - you see the time get changed.

Slide 46: Page State


So: the "body" function generates the page, the "event" functions dictates what the page, in cooperation with the Nitrogen server, will do (which may include changing the page). In this exercise, we want to have a counter start at some value (let's use 0), increment in response to a "click" type event on a "Submit" element, and everything should start over on a page reload. wf:state_default/2 needs to be called early. I can think of two alternatives: during body generation (so in the body/0 function), or in an "onpageload" event (I think that's the name; I'll need to look it up). I'll try using body/0 first.

Here's the first try, which fails when initially trying to generate the page body. The code:

body() ->
wf:state_default(click_counter, 0),
#panel { style="margin: 50px", body=[
#button { id=mybutton, text="Submit", postback=click },
#panel { id=placeholder, body=[ #p { body=io_lib:format("~B", [wf:state(click_counter)]) } ] }
]}.

event(click) ->
wf:state(click_counter, wf:state(click_counter)+1),
wf:update(placeholder, [
#p { body=io_lib:format("~B", [wf:state(click_counter)]) }
]).

...which compiles, but generates this error:

=INFO REPORT==== 15-Jan-2011::13:15:13 ===
{error,error,badarg,
[{io_lib,format,["~B",[undefined]]},
{my_page,body,0},
{element_function,call_next_function,1},
{wf_render_elements,call_element_render,2},
{wf_render_elements,render_element,1},
{wf_render_elements,render_elements,2},
{lists,foldl,3},
{wf_render_elements,render_elements,2}]}

If I replace the initial value reference, wf:state('click_counter') with the constant 0, the page renders, but it doesn't update. The page source shows a script that looks like it fires, but the only effect is to generate this message in the Firefox console:

Warning: Unknown pseudo-class or pseudo-element 'input'.
Source File: http://localhost:8000/my/page
Line: 0

Quick research into the "onpageload" event shows that it's different between IE and everyone else (IE: document.attachEvent('onload',startClock);; everyone else: document.addEventListener('load',startClock,false);). That looks like a dead end.

OK, I give up.

Slide 47: Page State (answer to exercise)


O.K. So wf:state_default/2 doesn't work at all as I imagined. Instead, if the atom's state is not defined by the page, it returns the default, and if it is defined by the page it returns that value. I like this minor variant on their answer better:

body() ->
#panel { style="margin: 50px", body=[
#button { id=mybutton, text="Submit", postback=click },
#panel { id=placeholder, body="0" }
]}.

event(click) ->
Counter = wf:state_default(click_counter, 0) + 1,
wf:update(placeholder, [
#panel { body=wf:f("~B", [Counter]) }
]),
wf:state(click_counter, Counter).

It appears that the Firefox console complaint is "normal" - the page is working as expected, but the complaint still occurs.