Chapter 3.6: Before

before runs just before calling action on the model. Similar to preset, you can process incoming data here. The difference is that in before, you have the initial model object which has the database handler, as the first argument. So you can process database data, which you can’t do it in preset.

The second and third arguments extra and nextextras are optional. You can set up extra for use in model’s action method. If you are going to run trigger in model, you may need nextextras as an array reference, in which each element is a hash reference for the corresponding page. The order of the array should match the way the pages are called.

sub before {
  my $self = shift;
  my $err = $self->SUPER::before(@_);
  return $err if $err;

  my $ARGS = $self->{ARGS};
  my $reqs = $self->{R};
  my $action = $ARGS->{g_action};
  my $role = $ARGS->{g_role};

  my ($model, $extra, $nextextras) = @_;
  my $dbh = $model->{DBH};

  do_something

  return;
}

Note that if you have enabled no_db, $model will be defined but not $model->{DBH}. If you have no_method, then $model will be undef.

Chapter 3.5: Preset

The input data are collected and stored into $self->{ARGS}. They are classified into 3 types:

  1. Input data in query, POST body and login cookie. These variables are saved in their original names. For example, if you pass user_id=1234 in query, then you will have $self->{ARGS}->{user_id} = 1234. Multiple values will be saved as array reference.
  2. Navigation data, starting with “g_“.
    • g_role: the role group of the visitor
    • g_action: the action
    • g_component: the component it acts on
    • g_server, g_scriptg_scriptfullg_query_string: current URL’s information.
    • g_json_url: the corresponding URL for JSON API.
  3. Supplementary data,  starting with “_g“. They are used for internal purpose. Unlike the above 2 types, they are not export to HTML template or API. Only if you dump $self->{ARGS} will you see them.

preset is the phase in which you can further do data process, such as adding new variables,  modifying existing ones, or enabling pagination. Since it inherits from the project, you should first call project’s preset. The usage is:

sub preset {
  my $self = shift;
  my $err = $self->SUPER::preset(@_);
  return $err if $err;

  my $ARGS = $self->{ARGS};
  my $reqs = $self->{R};
  my $action = $ARGS->{g_action};
  my $role = $ARGS->{g_role};

  do_something
  # enable pagination: $ARGS->{rowcounts} = 50;

  return;
}

If the run is successful, return nothing. If something goes wrong, return the error code or an explanation string. Genelet will stop immediately and pop up the error page.

If you need to access 3rd-part API, preset is a good phase to do that.

Chapter 3.4: Get Action

In rare cases, you’d like to calculate action in your own way, or to modify attributes actions and fks in Filter.pm. You can do these by overriding get_action. Just to make sure you return a two-element array: the action and its hash value.

The overridden get_action should be:

sub get_action {
  my $self = shift;
 
  my $actions = $self->{ACTIONS};
  my $fks = $self->{FKS};
  do_something 

# if you get $action
  return $action, $actions->{$action};
# or after you have modified the attributes, pass to SUPER
  return $self->SUPER::get_action(@_);
}

Note that get_action runs at Phase 6 in the life circle, before the data collection, ACL and RLS. So at the time of this method, $self->{ARGS} is empty. If you’d like to change something in the 3 later phases, which you can’t access directly, you may do some data manipulations here.

Chapter 3.3: RLS

There are many situations where RLS (Row Level Security) becomes a security concern.

Here is a simple data model of shopping website. There is a member table, a product table for products, a purchase table for members’ purchase history, and an item table that shows which products are in each purchase. Their PKs are member_id, product_idpurchase_id and item_id respectively. The 4 tables have been programmed as 4 components in Genelet: Member, Product, Purchase and Item.

create table member (
  member_id int not null auto_increment primary key,
  email varchar(32) not null,
  passwd varchar(32) not null,
  member_name varchar(255) not null,
  unique key (email)
);

create table product (
 product_id not null auto_increment primary key,
 product_name varchar(255),
 unit_price double
);
 
create table purchase (
  purchase_id int not null auto_increment primary key,
  member_id int not null,
  purchase_time datetime not nul,
  total_price double not null,
  index (member_id)
);

create table item (
  item_id int not null auto_increment primary key,
  purchase_id int not null,
  product_id int not null,
  num_product int default 1,
  index (product_id),
  index (purchase_id)
);

Now, a member logs in and would like to see her purchase history. This is the RESTful action topics on Purchase, using member_id as a constraint. Since member_id is de-crypted from her login cookie, which can’t be spoofed, she would be restricted to view only her own purchases. Up to now, the authentication and ACL work well in protecting topics, i.e. to guarantee that whatever she tries, she can’t steal other member’s purchase records.

Next, she’d like to see the product list of a specific purchase. This is the RESTful topics on Item, using purchase_id as a constraint. Now,  a problem arises. If she sends a purchase_id owned by somebody else, surprisingly, she can see that person’s purchase history! Because purchase_id is passed in URL or POST form in plain text, she could maliciously pickup any id for topics.

This is a typical case of RLS, as discussion by Oracle here.

You could solve the problem by (1) add member_id to the item table, and use it as additional constraint; (2) INNER JOIN Item and Purchase, so you can use member_id in the purchase table as the constraint. However, solution 1 would result in redundant member_id.  Solution 2 is less practical. For a large database, many tables could be separated from the member table by 2 or more relations. To write JOIN SQLs for every RESTful action is tedious and easily mistaken.

Genelet’s solution to RLS is a two-step procedure. First, it digitally signs upline’s PK with role’s id and an optional time stamp. In the above example, it signs SHA1 of purchase_id and member_id using a secret word. Second, it checks if the signature of the key, which usually appears as a FK in the current table, is validate. If verified, the key is guaranteed to belong to the member. Just randomly passing an arbitrary key by malicious hacker can’t make the correct signature.

The fks is thus defined to

fks => {
  ROLE => [in_id, in_sha1, out_id, out_sha1],
 ...
}

where ROLE is a login role like memberin_id is a key to be constrained, usually a FK in the current table. You should pass the value of in_id and its digital signature in_sha1 as two incoming variables, so Genelet can verify that in_id is indeed owned by member_id by inspecting in_sha1.

By default, in_id will always be used as a constraint in the model.

out_id is a different column in the table, usually the PK.  You need to pass it to downlines with signature, named out_sha1. Genelet internally will scan out_id in $self->{LISTS} so every out_id will have an associated out_sha1, which are ready to use for next clicks.

Note that in_id and out_id are field name in the table, which are fixed,  but you are free to choose whatever names for in_sha1 and our_sha1.

If role’s id is used as a constraint, you don’t need its signature because it is already proofed in the login cookie. So fks will look like:

fks => {
  role => [role_id, undef, out_id, out_sha1],
  ...
}

If you don’t need out_id:

fks => {
  role => [in_id, in_sha1],
 ...
}

If you only want out_id:

fks => {
  role => [unde, undef, out_id, out_sha1],
  ...
}

 

Finally note that in the Genelet life circle, RLS checking is made at Phase 8, followed by presest(), and outgoing RLS signature at Phase 15, followed by after().

Chapter 3.2: File Upploading

Assuming in a HTML form, you have two file-upload fields, one for image and one for music, and the web action is insert:

<input type=hidden name="action" value="insert" />
<input type=file name="field1" />
<input type=file name="field2" />

To use the uploading feature in Genelet, you should define an upload hash in this format in corresponding action’s ACTION_hash:

upload => {
  html_field => {filename, directory, renamed},
  html_field => {filename, directory, renamed},
  ...
}

If renamed is not defined, the variable filename (of the uploaded file) will be determined by the server. If it is specified, the file will always be renamed to the given name.

In the above example, you should add upload to insert:

__PACKAGE__->setup_accessors(
  actions => {
    topics   => {...},
    upload   => {...},
    insert   => {
      upload => {
        field1 => ["filename_image", "/home/www/ht_docs/images"],
        field2 => ["filename_music", "/home/www/ht_docs/music", "kids.mp4"]
      },
      groups => [...],
      musts  => [...]
    },
    edit     => {...},
    'delete' => {...}
  },
  fks => {...}
);

In the upload map, key is the HTML input name; value is a 3-element array reference.

After the form is submitted to Genelet, you will receive two incoming variables filename_image and filename_music. Their values are uploaded files’ names, somehow arbitrarily assigned by the server. The image file will be located in directory /home/www/ht_docs/images, the music file in directory /home/www/ht_docs/music. You may manually choose their name as the third argument, like kids.mp4.

Chapter 3.1: Accessors and ACL

In component’s Filter,pm, there are two attributes to be set up using setup_accessors: actions and fks. The former defines features for specific actions, and the later defines Row Level Security (RLS). Here it is

package Myproject::Mycomponent::Filter;

use strict;
use Myproject::Filter;
use vars qw(@ISA);
@ISA=('Myproject::Filter');

__PACKAGE__->setup_accessors(
  actions => {
    'insert' => ACTION_hash,
    'topics' => ACTION_hash,
    ...
  },

  fks => {
    'role1' => RLS_hash,
    'role2' => RLS_hash,
    ...
  }
);
...
 
1;

 

where ACTION_hash is:

{groups => [], aliases => [], upload => {}, validate => [], no_db => 0, no_method => 0}

The keys are:

  • groups: an array reference, to control which roles are allowed to access the action, i.e. Access Control List (ACL)
  • aliases: an array reference, for alias names the action may have.
  • upload: a hash reference, for file uploading explained in Section 3.2.
  • validate: an array reference, for incoming variables that can’t be empty.
  • no_db: 0 or 1, default 0. Genelet opens a database handler for each request by default, which comes with costs in speed and resource. If your action does not need database, just set it no_db=>1.
  • no_method: 0 or 1, default 0. If your action does not have corresponding method in model, set it no_method=>1.

Here are some notes:

groups (ACL)

Roles that are allowed to run the action on Model.pm must be defined in ACL. Those who are not permitted will trigger 404 error.

The roles having admin’s privilege can always run any action, not restricted by the ACL rules.

validate (Form Validation)

You may create your own form validation in preset, but this simply form will help you to avoid a common fatal error in database: “Field not found”.

 

Chapter 3: Controller

  1. Introduction
  2. Model
  3. Filter
    1. Accessors and ACL
    2. File Uploading
    3. RLS
    4. Get Action
    5. Preset
    6. Before
    7. After
    8. Sending Email

 

Filter.pm is a localized version of Controller. Among about 20 logic phases, 3/4 of them have already been handled by Genelet, so we rename the local Controller to be Filter.

 

Project’s Filter

Just like model, you should first inherit project’s Filter.pm from Genelet::Filter, then inherit component’s Filter.pm from that of the project.

When doing project inheritance, you should specify the HTML template system and email delivery module. Genelet::Template and Genelet::SMTP are the standard choices, which are based on TT2 and Net::SMTP respectively. The generic form of Filter.pm is:

package Myproject::Filter;

use strict;
use Genelet::Filter;
use Genelet::Template;
use Genelet::SMTP;

use vars qw(@ISA);

@ISA = qw(Genelet::Filter Genelet::Template Genelet::SMTP);

__PACKAGE__->setup_accessors(
  r    => {...},  
  args => {...},
  actions => {},
  fks  => {}
);

sub get_action {
  my $self = shift;

  return;
}

sub preset {
  my $self = shift;

  return;
}

sub before {
  my $self = shift;
  my ($model, $extra, $nextextras) = @_;

  return;
}

sub after {
  my $self = shift;
  my ($model) = @_;

  return;
}
 
1;

where $self->{R} is the original request object defined in CGI.pm$self->{ARSG} has collected all incoming data. $self->{ACTIONS} and $self->{FKS} should be defined in component, so are methods get_action, preset, before and after which are 4 specific phases in Genelet’s life cycle.

In case you don’t have any special setup in project, you may simply use:

package Myproject::Filter;

use strict;
use Genelet::Filter;
use Genelet::Template;
use Genelet::SMTP;

use vars qw(@ISA);

@ISA = qw(Genelet::Filter Genelet::Template Genelet::SMTP);

1;

 

Project’s Filter.pm is a good place to put algorithms that are common in components. For example, calculating visitor’s IP.

Chapter 2.7: Triggers

After acting on component, we may execute another action on a different component immediately, just like running trigger in database.

To use trigger, follow the two steps.

Step 1, Define a New Attribute, nextpages in Model.pm
__PACKAGE__->setup_accessors(
  nextpages => {
    action => [ PAGE, PAGE, ... ],
    action => [ PAGE, PAGE, ... ],
    ...
  },

  insert_pars => [...],
  ...
}

Where action is the class method that would trigger other models;  PAGE is a hash ref defined in call_once and call_nextpage in Section 2.1:

{
  model  => STRING,
  action => STRING, 
  relate_item => {this_field=>other_field, this_field=>other_field, ...},
  manual => {field=>value, field=>value, ...}
}

As you could see, one action could trigger multiple models.

 

Step 2, Return process_after

The inherited RESTful verbs have always triggers enabled, as soon as a verb is defined in nextpages.  To have trigger for your own class method, return the method process_after:

return $self->process_after(action, extra);

in replace of

return;

where action is a method name, usually the current method. process_after will simply look at nextpages, and run call_once or call_nextpage accordingly.

After a successful run, relevant data fields will be collected into $self->{LISTS} and $self->{OTHER}.

A proper use of trigger could minimize redundant codes, keep better programming logic, and take advantages of built-in pagination and row-level-security.

Chapter 2.6: JOIN Tables

So far, you are assumed to run topics for a single database table. For relational database, a search usually requires multiple table joins.

While you can build raw SQL to do this, as described in Section 2.2,  Genelet has a built-in solution which is cleaner and can take advantage of pagination etc. as well. Here is it.

 

Step 1: Add New Attribute current_tables to Model.pm

This array reference current_tables contains all joint tables, each of which is represented by a map having the following key-values:

  • name: the table name
  • alias: optional, the alias of the table
  • type: one of join types – INNER or LEFT
  • using: to use USING clause in joining the table
  • on: to use ON clause in joining the table

The first item in current_tables is for the main table having only name and alias.

 

Step 2: Re-define topics_pars to be HASH

By default, the searched fields will use their table (or table alias) names as prefixes. You can assign labels to these cumbersome fields, by re-defining topics_pars as a hash.

 

Example

You’d like to get a list of all children in full names from a family table and a children table.

create table family (
  family_id int not null auto_increment,
  last_name varchar(255) not null,
  mother varchar(255),
  father varchar(255),
  primary key (family_id)
);

create table child (
  child_id int not null auto_increment,
  family_id int not null,
  first_name varchar(255) not null,
  primary key (child_id),
  index (family_id)
);

This SQL will produce the correct list:

SELECT f.family_id AS family_id, c.child_id as child_id, f.last_name AS last_name, c.first_name as first_name
FROM family f
INNER JOIN child c ON (f.family_id=c.family_id)

You may override the topics method in Child’s Model.pm to achieve the goal. Then you have to set up own pagination.

Alternatively, you may use:

__PACKAGE__->setup_accessors(
  ...
  current_tables => [
    {name=>"family", alias=>"f"},
    {name=>"child",  alias=>"c", type=>"INNER", on=>"f.family_id=c.family_id"}
  ],
  topics_pars    => {
    "f.family_id" => "family_id",
    "f.last_name" => "last_name",
    "c.child_id"  => "child_id",
    "c.first_name"=> "first_name"
  }
);

which will use the inherited topics to search and do other functions, as if it were a single table.

Chapter 2.5: Pagination

If rowcount is passed in query or manually added in Filter.pm, Genelet will display searched results in pages (i.e. pagination). Specifically,

  1. sending a request with rowcount=20 will turn on pagination
  2. if no pageno, then pageno=1 will be taken as the default.
  3. if total_force=1 in the model, the total number of rows and the total number of pages, totalno and maxpageno respectively, will be calculated for pageno=1.
  4. Use pageno=2 for the second page, and so on.
  5. sortby should be specifically defined, since the order of rows returned would depend on how they are sorted.
  6. If totalno is expected to change in real-time, you should arrange pagination in such a way that the first page, case 2 above, could be clicked often, so as to have totalno and maxpageno refreshed.