Revision 197

Date:
2012/03/15 18:28:50
Author:
ahitrov
Revision Log:
Session plugin
Files:

Legend:

 
Added
 
Removed
 
Modified
  • utf8/plugins/session/comps/contenido/session/dhandler

     
    1 % if (@call)
    2 % {
    3 <& @call &>
    4 % }
    5 <%INIT>
    6
    7 use vars qw( $keeper $request $project );
    8 $r->content_type('text/html');
    9
    10 my @path = split('/', $m->dhandler_arg());
    11 my @call = ();
    12
    13 if (length($path[0]) < 1) { $path[0] = 'index.html' };
    14 @call = (join('/',@path), %ARGS);
    15
    16 if (! $m->comp_exists($call[0]))
    17 {
    18 $m->clear_buffer();
    19 $m->abort(404);
    20 }
    21
    22 </%INIT>
  • utf8/plugins/session/comps/contenido/session/index.html

     
    1 <& "/contenido/components/header.msn", style => 'index' &>
    2
    3 <div style="text-align:center; padding:180px 20px;">
    4 % if ( $keeper->{session}->state->storage eq 'POSTGRES' ) {
    5 <form action="./index.html" method="POST"
    6 onsubmit="return confirm('Вы действительно собираетесь грохнуть все пользовательские сессии?\nЭто приведет к принудительному выходу пользователей из системы!')">
    7 <input type="submit" class="input_btn" name="clear" value="Удалить все пользовательские сессии">
    8
    9 </form>
    10 % }
    11 </div>
    12
    13 </body>
    14 </html>
    15 <%args>
    16
    17 $clear => undef
    18
    19 </%args>
    20 <%init>
    21
    22 if ( $clear && $keeper->{session}->state->storage eq 'POSTGRES' ) {
    23 warn "delete from sessions\n";
    24 my $req = $keeper->SQL->do('delete from sessions', {}) || $keeper->t_abort();
    25 $m->redirect ('./');
    26 }
    27
    28 </%init>
  • utf8/plugins/session/config.proto

     
    1 #############################################################################
    2 #
    3 # Параметры данного шаблона необходимо ВРУЧНУЮ добавить в config.mk проекта
    4 # и привести в соответствие с требованиями проекта
    5 #
    6 #############################################################################
    7
    8 PLUGINS += session
    9 PROJECT_REQUIRED += Apache-Session
    10 PROJECT_REQUIRED += JSON-XS
    11 PROJECT_REQUIRED += P-WebFetcher
    12
    13 ### Необязательный параметр, default = lsid
    14 SESSION_COOKIE_NAME = lsid
    15 REWRITE += SESSION_COOKIE_NAME
    16
    17 ifeq (${DEVELOPMENT}, YES)
    18
    19 SESSION_DOMAIN =
    20 SESSION_STORAGE = POSTGRES
    21 SESSION_LIFETIME = 24
    22 SESSION_EXPIRES = +10d
    23
    24 else
    25
    26 SESSION_DOMAIN =
    27 SESSION_STORAGE = POSTGRES
    28 SESSION_LIFETIME = 24
    29 SESSION_EXPIRES = +10d
    30
    31 endif
    32
    33 REWRITE += SESSION_STORAGE SESSION_DOMAIN SESSION_LIFETIME SESSION_EXPIRES
    34
    35 ########################################################################
    36 #
    37 # SESSION_DOMAIN
    38 # Домен, на котором отвечает проект. Можно не указывать
    39 # SESSION_STORAGE
    40 # Контейнер для хранения сессий. Варианты: POSTGRES и FILE
    41 # SESSION_LIFETIME
    42 # Время жизни сессий до автоочистки. Задается в часах, используется
    43 # при включении в crontab проекта скрипта из services
    44 # SESSION_EXPIRES
    45 # Время жизни куки сессии. Задается в формате Apache
    46 #
    47 ########################################################################
    48
    49
    50 ### AUTH::FaceBook
    51 ######################################
    52 FACEBOOK_APP_ID =
    53 FACEBOOK_APP_KEY =
    54 FACEBOOK_APP_SECRET =
    55 FACEBOOK_AUTHOTIZE_URL = https://graph.facebook.com/oauth/authorize
    56 FACEBOOK_ACCESS_TOKEN_URL = https://graph.facebook.com/oauth/access_token
    57 FACEBOOK_USER_INFO_URL = https://graph.facebook.com/me
    58 FACEBOOK_REDIRECT_URL =
    59 FACEBOOK_USER_POST_URL =
    60
    61 REWRITE += FACEBOOK_AUTHOTIZE_URL FACEBOOK_ACCESS_TOKEN_URL FACEBOOK_USER_INFO_URL
    62
    63 CONNECTION_TIMEOUT = 3
    64
    65 PROJECT_REQUIRED += Crypt-SSLeay
    66
  • utf8/plugins/session/lib/session/Apache.pm

     
    1 package session::Apache;
    2
    3 use strict;
    4 use warnings 'all';
    5
    6 use session::State;
    7 use Contenido::Globals;
    8
    9
    10 sub child_init {
    11 # встраиваем keeper плагина в keeper проекта
    12 $keeper->{session} = session::Keeper->new($state->session);
    13 }
    14
    15 sub request_init {
    16 }
    17
    18 sub child_exit {
    19 }
    20
    21 1;
  • utf8/plugins/session/lib/session/AUTH/FaceBook.pm

     
    1 package session::AUTH::FaceBook;
    2
    3 use strict;
    4 use warnings;
    5 use LWP::UserAgent;
    6 use JSON::XS;
    7 use Data::Dumper;
    8 use URI;
    9 use URI::QueryParam;
    10 use Encode;
    11 use Contenido::Globals;
    12
    13 use vars qw($VERSION);
    14 $VERSION = '4.1';
    15
    16 =for rem
    17 facebook:
    18 auto_create_user: 1
    19 app_id: 122117614500563
    20 app_key: 3da06301715b0efc5c873535c56c2c33
    21 app_secret: 656bd1369486b902e9bf831a9a08132b
    22 authorize_url: https://graph.facebook.com/oauth/authorize
    23 access_token_url: https://graph.facebook.com/oauth/access_token
    24 user_info_url: https://graph.facebook.com/me
    25 user_post_url: ~
    26 store:
    27 class: "+Comments::Authentication::Store"
    28 type: facebook
    29
    30 =cut
    31
    32 our $JSON = JSON::XS->new->utf8;
    33
    34 =for rem SCHEMA
    35
    36 $m->redirect ( $fb_connect->fb_authorize_url( redirect_uri => ... ) );
    37
    38
    39 =cut
    40
    41
    42 sub new {
    43 my ($class, %config) = @_;
    44 my $self = bless {}, $class;
    45 for (qw(facebook_app_id facebook_app_key facebook_app_secret facebook_authorize_url facebook_access_token_url facebook_user_info_url)) {
    46 $self->{$_} = $config{$_} || $state->{session}->{$_} || return undef;
    47 }
    48 $self->{timeout} = $state->{session}->{connection_timeout} || 3;
    49 for (qw(facebook_user_post_url facebook_redirect_uri)) {
    50 $self->{$_} = $config{$_} || $state->{session}->{$_};
    51 }
    52 return $self;
    53 }
    54
    55 sub fb_authorize_url {
    56 my $self = shift;
    57 my (%args) = @_;
    58 my $go = URI->new( $self->{facebook_authorize_url} );
    59 warn Dumper($go);
    60 $go->query_param( client_id => $self->{facebook_app_key} );
    61 $go->query_param( scope => "publish_stream" );
    62 $args{redirect_uri} ||= $self->{facebook_redirect_uri};
    63 for ( keys %args ) {
    64 $go->query_param( $_ => $args{$_} );
    65 }
    66 $keeper->{session}->store_value( facebook_redirect_url => $self->{facebook_redirect_uri} );
    67 return $go;
    68 }
    69
    70 sub authenticate {
    71 my ( $self, %authinfo ) = @_;
    72 warn "FB.authenticate" if $DEBUG;
    73 # TODO: we need callback url
    74 #warn "user_session=".dumper( $c->user_session )." ";
    75 my $local_session = $session || $keeper->{session}->get_session;
    76 my $redirect_uri = $local_session->{facebook_redirect_url};
    77
    78 my $access_token = $local_session->{facebook_access_token};
    79 my $expires = $local_session->{facebook_expires};
    80 if ($access_token and $expires > time) {
    81 warn "Already have access_token" if $DEBUG;
    82 } else {
    83 undef $access_token;
    84 }
    85 my $code = $authinfo{'code'};
    86 unless ( $code ) {
    87 warn "Call to authenticate without code";
    88 return undef;
    89 }
    90 my $ua = LWP::UserAgent->new;
    91 $ua->timeout($self->{timeout});
    92 unless ($access_token) {
    93 my $req = URI->new( $self->{facebook_access_token_url});
    94 $req->query_param( client_id => $self->{facebook_app_id} );
    95 $req->query_param( redirect_uri => $redirect_uri );
    96 $req->query_param( client_secret=> $self->{facebook_app_secret} );
    97 $req->query_param( code => $code);
    98 warn "Get $req";
    99 my $res = $ua->get($req);
    100 unless ($res->code == 200) {
    101 warn "access_token request failed: ".$res->status_line;
    102 return undef;
    103 }
    104 my %res = eval { URI->new("?".$res->content)->query_form };
    105 warn Dumper(\%res);
    106 unless ($access_token = $res{access_token}) {
    107 warn "No access token in response: ".$res->content;
    108 return undef;
    109 }
    110 $keeper->{session}->store_value( facebook_access_token => $access_token );
    111 $local_session->{facebook_access_token} = $access_token;
    112 if( my $expires = $res{expires} ) {
    113 $local_session->{facebook_expires} = time + $expires;
    114 $keeper->{session}->store_value( facebook_expires => $local_session->{facebook_expires} );
    115 } else {
    116 #$c->user_session->{'expires'} = time + 3600*24;
    117 }
    118 warn "FB: requested access token";
    119 } else {
    120 warn "FB: have access token";
    121 }
    122
    123 my $req = URI->new( $self->{facebook_user_info_url} );
    124 $req->query_param( access_token => $access_token );
    125
    126 warn "Fetching user $req";
    127 my $res = $ua->get($req);
    128 unless ($res->code == 200) {
    129 warn "user request failed: ".$res->status_line;
    130 return undef;
    131 }
    132 my $info;
    133 unless ( $info = eval { JSON::XS->new->utf8->decode($res->content) } ) {
    134 warn "user '".$res->content."' decode failed: $@";
    135 return undef;
    136 }
    137 warn "Userhash = ".Dumper($info);
    138 #warn "facebook: user=$info->{name} / $info->{id} / $info->{gender}";
    139
    140 $keeper->{session}->delete_key( 'facebook_redirect_url' );
    141 delete $local_session->{facebook_redirect_url};
    142
    143 my @plugins = split (/[\ |\t]+/, $state->{plugins});
    144 if ( grep { $_ eq 'users' } @plugins ) {
    145 my $user = $keeper->{users}->get_profile( login => 'facebook:'.$info->{id} );
    146 unless ( ref $user ) {
    147 my $user_class = $state->{users}->profile_document_class;
    148 $user = $user_class->new( $keeper );
    149 $user->login( 'facebook:'.$info->{id} );
    150 my $name = Encode::encode('utf-8', $info->{name});
    151 $user->name( $name );
    152 $user->status( 1 );
    153 $user->type( 0 );
    154 $user->login_method('facebook');
    155 $user->country( $info->{locale} );
    156 $user->email( undef );
    157 $user->store;
    158 }
    159 my %data = (
    160 id => $user->id,
    161 name => $user->name,
    162 login => $user->login,
    163 status => $user->status,
    164 type => $user->type,
    165 ltime => time,
    166 );
    167 $keeper->{session}->store_value ( %data );
    168 while ( my ( $key, $value ) = each %data ) {
    169 $local_session->{$key} = $value;
    170 }
    171 }
    172 return $local_session;
    173 }
    174
    175 1;
  • utf8/plugins/session/lib/session/Init.pm

     
    1 package session::Init;
    2
    3 use strict;
    4 use warnings 'all';
    5
    6 use session::Apache;
    7 use session::Keeper;
    8 use session::AUTH::FaceBook;
    9
    10 # загрузка всех необходимых плагину классов
    11 # session::SQL::SomeTable
    12 # session::SomeClass
    13 Contenido::Init::load_classes(qw(
    14 ));
    15
    16 sub init {
    17 0;
    18 }
    19
    20 1;
  • utf8/plugins/session/lib/session/Keeper.pm

     
    1 package session::Keeper;
    2
    3 use strict;
    4 use warnings 'all';
    5 use base qw(Contenido::Keeper);
    6
    7 use Apache::Cookie;
    8 use Apache::Session::File;
    9 use Apache::Session::Postgres;
    10 use Contenido::Globals;
    11 use Data::Dumper;
    12
    13
    14 sub logon {
    15 my $self = shift;
    16 my %opts = @_;
    17
    18 return if !($opts{login} || $opts{email}) && !$opts{passwd};
    19
    20 my $res;
    21 my @plugins = split (/[\ |\t]+/, $state->{plugins});
    22 if ( grep { $_ eq 'users' } @plugins ) {
    23 #### Авторизация через плагин users
    24 #########################################
    25 $res = $keeper->{users}->login (
    26 $opts{login} ? (login => $opts{login}) : (),
    27 $opts{email} ? (email => lc($opts{email})) : (),
    28 passwd => $opts{passwd},
    29 );
    30 return unless $res;
    31 } else {
    32 #### Авторизация иным способом
    33
    34
    35
    36 }
    37 if ( ref $res ) {
    38 my %data = (
    39 id => $res->id,
    40 name => $res->name,
    41 email => $res->email,
    42 login => $res->login,
    43 status => $res->status,
    44 ltime => time,
    45 );
    46 $self->store_value ( %data );
    47 }
    48 return $self->get_session();
    49 }
    50
    51
    52 sub logoff {
    53 my $self = shift;
    54 my %opts = @_;
    55
    56 my $sid = _get_session_id ();
    57 my $session = _get_session_object ( $sid );
    58 return unless ref $session;
    59
    60 my $session_id = $session->{_session_id};
    61 if (!$sid || $sid ne $session_id) {
    62 warn "LOGOFF: New or deprecated session. Old sid = '$sid', new sid = '$session_id'" if $DEBUG;
    63 _store_session_id ($session_id)
    64 } else {
    65 my @clear = qw( id email login name nick type status ltime );
    66 push @clear, @{ $opts{clear} } if exists $opts{clear} && ref $opts{clear} eq 'ARRAY' && @{ $opts{clear} };
    67 foreach my $key ( @clear ) {
    68 delete $session->{$key};
    69 }
    70 }
    71 untie %$session;
    72 return 1;
    73 }
    74
    75
    76 sub get_value {
    77
    78 my ($self, $name) = @_;
    79 my $sid = _get_session_id ();
    80 my $session = _get_session_object ( $sid );
    81 return unless ref $session;
    82
    83 my $session_id = $session->{_session_id};
    84 my $value = $session->{$name};
    85 if (!$sid || $sid ne $session_id) {
    86 warn "GET_VALUE: New or deprecated session. Old sid = '$sid', new sid = '$session_id'" if $DEBUG;
    87 _store_session_id ($session_id);
    88 }
    89 untie %$session;
    90 return $value;
    91 }
    92
    93
    94 sub store_value {
    95
    96 my ($self, %opts) = @_;
    97 my $sid = _get_session_id ();
    98 my $session = _get_session_object ( $sid );
    99 return unless ref $session;
    100
    101 foreach my $key ( keys %opts ) {
    102 $session->{$key} = $opts{$key};
    103 }
    104
    105 my $session_id = $session->{_session_id};
    106 if (!$sid || $sid ne $session_id) {
    107 warn "STORE_VALUE: New or deprecated session. Old sid = '$sid', new sid = '$session_id'" if $DEBUG;
    108 _store_session_id ($session_id);
    109 }
    110 untie %$session;
    111 return 1;
    112 }
    113
    114
    115 sub delete_key {
    116
    117 my ($self, $key) = @_;
    118 return unless $key;
    119
    120 my $sid = _get_session_id ();
    121 my $session = _get_session_object ( $sid );
    122 return unless ref $session;
    123
    124 my $session_id = $session->{_session_id};
    125 if (!$sid || $sid ne $session_id) {
    126 warn "DELETE_VALUE: New or deprecated session. Old sid = '$sid', new sid = '$session_id'" if $DEBUG;
    127 _store_session_id ($session_id);
    128 } else {
    129 delete $session->{$key} if exists $session->{$key};
    130 }
    131 untie %$session;
    132 return 1;
    133 }
    134
    135
    136 sub get_session {
    137
    138 my $self = shift;
    139
    140 my $sid = _get_session_id () || '';
    141 my $session = _get_session_object ($sid);
    142 return unless ref $session;
    143
    144 my $session_id = $session->{_session_id};
    145 my %ret = %$session;
    146 if (!$sid || $sid ne $session_id) {
    147 warn "\nGET_SESSION: New or deprecated session. Old sid = '$sid', new sid = '$session_id'\n" if $DEBUG;
    148 _store_session_id ($session_id);
    149 }
    150 untie %$session;
    151
    152 return \%ret;
    153 }
    154
    155
    156 ## Внутренние функции
    157 ######################################################################################
    158 sub _store_session_id {
    159
    160 my $sid = shift;
    161 return unless $sid;
    162 my $cookie = Apache::Cookie->new ($request->r(),
    163 -domain => $state->{session}->domain,
    164 -name => $state->{session}->cookie,
    165 -expires=> $state->{session}->expires,
    166 -value => $sid,
    167 -path => '/',
    168 );
    169 $cookie->bake();
    170
    171 }
    172
    173
    174 sub _get_session_id {
    175
    176 my %cookies = Apache::Cookie->fetch;
    177 warn Dumper(\%cookies) if $DEBUG;
    178 my $cookie = $cookies{$state->{session}->cookie};
    179
    180 # Вытаскиваем SID из куки
    181 my $sid = $cookie->value() || '' if $cookie;
    182 warn "\nSession_id = $sid\n" if $DEBUG;
    183
    184 return $sid;
    185 }
    186
    187
    188 sub _get_session_object {
    189
    190 my $sid = shift;
    191
    192 my %session;
    193 my $now = time;
    194 if ( $state->{session}->storage eq 'POSTGRES' ) {
    195 eval {
    196 tie %session, 'Apache::Session::Postgres', $sid, {
    197 Handle => $keeper->SQL,
    198 };
    199 };
    200 } else {
    201 eval {
    202 tie %session, 'Apache::Session::File', $sid, {
    203 Directory => $state->session->session_dir,
    204 };
    205 };
    206 }
    207 if ($@) {
    208 warn "Session data is not accessible: $@";
    209 undef $sid;
    210 } elsif ( $state->{session}->lifetime ) {
    211 unless ( exists $session{_timestamp} ) {
    212 $session{_timestamp} = $now;
    213 } elsif ( ($now - $session{_timestamp}) > $state->{session}->lifetime ) {
    214 undef $sid;
    215 } elsif ( ($now - $session{_timestamp}) > $state->{session}->checkout ) {
    216 $session{_timestamp} = $now;
    217 }
    218 }
    219 unless ( $sid ) {
    220 if ( $state->{session}->storage eq 'POSTGRES' ) {
    221 eval {
    222 tie %session, 'Apache::Session::Postgres', undef, {
    223 Handle => $keeper->SQL,
    224 };
    225 };
    226 } else {
    227 eval {
    228 tie %session, 'Apache::Session::File', undef, {
    229 Directory => $state->session->session_dir,
    230 };
    231 };
    232 }
    233 $session{_timestamp} = $now;
    234 }
    235
    236 return \%session;
    237 }
    238
    239
    240 sub _drop_session_object {
    241
    242 my (%session) = @_;
    243
    244 untie %session;
    245
    246 }
    247
    248 1;
  • utf8/plugins/session/lib/session/State.pm.proto

     
    1 package session::State;
    2
    3 use strict;
    4 use warnings 'all';
    5 use vars qw($AUTOLOAD);
    6
    7
    8 sub new {
    9 my ($proto) = @_;
    10 my $class = ref($proto) || $proto;
    11 my $self = {};
    12 bless $self, $class;
    13
    14 # зашитая конфигурация плагина
    15 $self->{db_type} = 'none';
    16 $self->{storage} = '@SESSION_STORAGE@' || 'FILE'; ## Значения: FILE POSTGRES MEMCACHED
    17 $self->{session_dir} = '@SESSIONS@';
    18 $self->{session_directory} = '@SESSIONS@';
    19
    20 $self->{domain} = '@SESSION_DOMAIN@';
    21 $self->{cookie} = 'lsid';
    22 $self->{expires} = '@SESSION_EXPIRES@' || '';
    23
    24 $self->{lifetime} = '@SESSION_LIFETIME@';
    25 $self->{lifetime} *= 3600;
    26 $self->{checkout} = $self->{lifetime} - int ($self->{lifetime} / 2);
    27
    28 $self->{db_keepalive} = 0;
    29 $self->{db_host} = '';
    30 $self->{db_name} = '';
    31 $self->{db_user} = '';
    32 $self->{db_password} = '';
    33 $self->{db_port} = '';
    34
    35 $self->{data_directory} = '';
    36 $self->{images_directory} = '';
    37 $self->{binary_directory} = '';
    38 $self->{preview} = '';
    39 $self->{debug} = '';
    40 $self->{store_method} = '';
    41 $self->{cascade} = '';
    42 $self->{memcached_enable} = '';
    43
    44 $self->{facebook_app_id} = '@FACEBOOK_APP_ID@';
    45 $self->{facebook_app_key} = '@FACEBOOK_APP_KEY@';
    46 $self->{facebook_app_secret} = '@FACEBOOK_APP_SECRET@';
    47 $self->{facebook_authorize_url} = '@FACEBOOK_AUTHORIZE_URL@';
    48 $self->{facebook_access_token_url} = '@FACEBOOK_ACCESS_TOKEN_URL@';
    49 $self->{facebook_user_info_url} = '@FACEBOOK_USER_INFO_URL@';
    50 $self->{facebook_redirect_uri} = '@FACEBOOK_REDIRECT_URL@';
    51 $self->{facebook_user_post_url} = '@FACEBOOK_USER_POST_URL@';
    52
    53 $self->_init_();
    54 $self;
    55 }
    56
    57 sub info {
    58 my $self = shift;
    59 return unless ref $self;
    60
    61 for (sort keys %{$self->{attributes}}) {
    62 my $la = length $_;
    63 warn "\t$_".("\t" x (2-int($la/8))).": $self->{$_}\n";
    64 }
    65 }
    66
    67 sub _init_ {
    68 my $self = shift;
    69
    70 # зашитая конфигурация плагина
    71 $self->{attributes}->{$_} = 'SCALAR' for qw(
    72 db_type
    73 session_dir
    74 session_directory
    75 domain
    76 cookie
    77 expires
    78 storage
    79 lifetime
    80 checkout
    81 db_keepalive
    82 db_host
    83 db_port
    84 db_name
    85 db_user
    86 db_password
    87 data_directory images_directory binary_directory preview debug store_method cascade memcached_enable
    88 );
    89 }
    90
    91 sub AUTOLOAD {
    92 my $self = shift;
    93 my $attribute = $AUTOLOAD;
    94
    95 $attribute =~ s/.*:://;
    96 return unless $attribute =~ /[^A-Z]/; # Отключаем методы типа DESTROY
    97
    98 if (!exists $self->{attributes}->{$attribute}) {
    99 warn "Contenido Error (session::State): Вызов метода, для которого не существует обрабатываемого свойства: ->$attribute()\n";
    100 return;
    101 }
    102
    103 $self->{$attribute} = shift @_ if $#_>=0;
    104 $self->{$attribute};
    105 }
    106
    107 1;
  • utf8/plugins/session/sql/TOAST/session.sql

     
    1 CREATE TABLE sessions (
    2 id char(32) not null primary key,
    3 dtime timestamp not null default now(),
    4 a_session text
    5 );

Небольшая справка по веткам

cnddist – контейнер, в котором хранятся все дистрибутивы всех библиотек и программных пакетов, которые использовались при построении различных версий Contenido. Если какой-то библиотеки в данном хранилище нет, инсталлятор сделает попытку "подтянуть" ее с веба (например, с CPAN). Если библиотека слишком старая, есть очень большая вероятность, что ее там уже нет. Поэтому мы храним весь хлам от всех сборок. Если какой-то дистрибутив вдруг отсутствует в cnddist - напишите нам, мы положим его туда.

koi8 – отмирающая ветка, чей код, выдача и все внутренние библиотеки заточены на кодировку KOI8-R. Вносятся только те дополнения, которые касаются внешнего вида и функционала админки, баги ядра, обязательные обновления портов и мелочи, которые легко скопипастить. В дальнейшем планируется полная остановка поддержки по данной ветке.

utf8 – актуальная ветка, заточенная под UTF-8.

Внутри каждой ветки: core – исходники ядра; install – скрипт установки инсталляции; plugins – плагины; samples – "готовые к употреблению" проекты, которые можно поставить, запустить и посмотреть, как они работают.