File Coverage

lib/Slovo.pm
Criterion Covered Total %
statement 166 176 94.3
branch 19 28 67.8
condition 12 26 46.1
subroutine 21 21 100.0
pod 2 2 100.0
total 220 253 86.9


line stmt bran cond sub pod time code
1             package Slovo;
2 14     14   1624836 use feature ':5.26';
  14         37  
  14         1792  
3 14     14   473 use Mojo::Base 'Mojolicious', -signatures;
  14         160526  
  14         107  
4              
5 14     14   1056344 use Mojo::Util 'class_to_path';
  14         55  
  14         646  
6 14     14   74 use Mojo::File 'path';
  14         24  
  14         519  
7 14     14   86 use Mojo::Collection 'c';
  14         27  
  14         603  
8 14     14   7524 use Slovo::Controller::Auth;
  14         35  
  14         275  
9 14     14   6269 use Slovo::Validator;
  14         39  
  14         123  
10 14     14   8745 use Slovo::Cache;
  14         34  
  14         100  
11 14     14   6712 use Time::Piece;
  14         81750  
  14         73  
12              
13             our $AUTHORITY = 'cpan:BEROV';
14             our $VERSION = '2022.02.02';
15             our $CODENAME = 'U+2C15 GLAGOLITIC CAPITAL LETTER TVRIDO (Ⱅ)';
16             my $CLASS = __PACKAGE__;
17              
18             has resources => sub {
19             path($INC{class_to_path $CLASS})->sibling("$CLASS/resources")->realpath;
20             };
21             has validator => sub { Slovo::Validator->new };
22              
23             # We prefer $MOJO_HOME to be the folder where the folders bin or script
24             # reside.
25             has home => sub {
26             if ($ENV{MOJO_HOME}) { return Mojo::Home->new($ENV{MOJO_HOME}); }
27             my $r = Mojo::Home->new($INC{class_to_path $CLASS})->dirname->to_abs;
28             my $m = $_[0]->moniker;
29             while (($r = $r->dirname) && @{$r->to_array} > 2) {
30             if (-x $r->child("bin/$m") || -x $r->child("script/$m")) {
31             return $r;
32             }
33             }
34             return $_[0]->SUPER::home;
35             };
36              
37             # Writes to $home/log/slovo.log if $home/log/ exists and is writable.
38             has log => sub {
39             my $self = shift;
40              
41             my $mode = $self->mode;
42             my $log = Mojo::Log->new;
43              
44             my $home = $self->home;
45             if (-d $home->child('log') && -w _) {
46             $log->path($home->child('log', $self->moniker . ".log"));
47             }
48              
49             # Reduced log output outside of development mode
50             return $log->level($ENV{MOJO_LOG_LEVEL}) if $ENV{MOJO_LOG_LEVEL};
51             return $mode eq 'development' ? $log : $log->level('info');
52             };
53              
54             # This method will run once at server start
55 14     14 1 273225 sub startup ($app) {
  14         36  
  14         22  
56 14         53 $app->log->debug("Starting $CLASS $VERSION|$CODENAME");
57 14         405 $app->controller_class('Slovo::Controller');
58 14         168 $app->commands->namespaces(
59             ['Slovo::Command::Author', 'Slovo::Command', 'Mojolicious::Command']);
60             ## no critic qw(Subroutines::ProtectPrivateSubs)
61 14         1054 $app->hook(around_action => \&_around_action);
62 14         168 $app->hook(around_dispatch => \&_around_dispatch);
63 14         123 $app->hook(before_dispatch => \&_before_dispatch);
64 14         136 $app->_set_routes_attrs->_load_config->_load_pugins->_default_paths->_add_media_types();
65 14         132 my $cache = Slovo::Cache->new();
66 14         44 $app->renderer->cache($cache);
67 14         171 $app->routes->cache($cache);
68 14         332 $app->defaults(
69              
70             # layout => 'default'
71             boxes => $app->openapi_spec('/parameters/box/enum'),
72             data_formats => $app->openapi_spec('/parameters/data_format/enum'),
73             data_types => $app->openapi_spec('/parameters/data_type/enum'),
74             lang => 'bg-bg',
75             languages => $app->languages, # /parameters/language/enum
76             page_types => $app->openapi_spec('/parameters/page_type/enum'),
77             permissions => $app->openapi_spec('/parameters/permissions/enum'),
78             stranici_columns =>
79             $app->openapi_spec('/paths/~1stranici/get/parameters/4/items/enum'),
80             );
81 14         3289 return $app;
82             }
83              
84 143     143   4268 sub _before_dispatch ($c) {
  143         228  
  143         179  
85 143         311 state $guest = $c->users->find_by_login_name('guest');
86 6         439 state $auth_config = c(@{$c->config('load_plugins')})->first(sub {
87 18 50   18   518 ref $_ eq 'HASH' and exists $_->{Authentication};
88 143         27273 });
89 143         302 state $session_key = $auth_config->{Authentication}{session_key};
90 143         238 state $current_user_fn = $auth_config->{Authentication}{current_user_fn};
91 143 100       548 unless ($c->session->{$session_key}) {
92              
93             #set the guest user as default to always have a user
94 51         16598 $c->$current_user_fn($guest);
95             }
96 143         34071 return;
97             }
98              
99             # Make some variables available to the templates being rendered, so
100             # these do not need to be set in the actions.
101 203     203   311823 sub _around_action ($next, $c, $action, $last) {
  203         350  
  203         358  
  203         250  
  203         294  
  203         278  
102 203 100 66     1223 if ($last && $c->current_route !~ /^api\./) {
103 138         4702 my $stash = $c->stash;
104 138   33     2272 $stash->{l} //= $c->language; # current language of the text being edited
105 138   33     32357 $stash->{user} //= $c->user; # current user
106             }
107 203         107804 return $next->();
108             }
109              
110             # This code is executed on every request, so we try to save as much as possible
111             # method calls.
112 143     143   2126625 sub _around_dispatch ($next, $c) {
  143         341  
  143         228  
  143         223  
113 143         318 state $app = $c->app;
114 143         275 state $root = $app->config('domove_root');
115              
116 143         348 state $s_paths = $app->static->paths;
117 143         315 state $r_paths = $app->renderer->paths;
118 143         273 state $cache = $app->renderer->cache;
119             my $dom
120 143   50     274 = eval { $c->domove->find_by_host($c->host_only) }
121             || die 'No such Host ('
122             . $c->host_only
123             . ')! Looks like a Proxy Server misconfiguration, readonly'
124             . " database file or a missing domain alias in table domove.\n";
125              
126             # Use domain specific public and templates' paths with priority.
127 143         4318 unshift @{$s_paths}, "$root/$dom->{domain}/public";
  143         815  
128 143 50       532 if (my $tpls = $dom->{templates}) {
129              
130             # absolute path
131 143 50 33     705 if ($tpls =~ m|^/| && -d $tpls) {
132 0         0 unshift @{$r_paths}, $tpls;
  0         0  
133             }
134             else {
135 143         215 unshift @{$r_paths}, "$root/$dom->{domain}/templates";
  143         548  
136              
137             # try to find the relative path to the theme in the list of paths
138 143         278 for my $path (@{$r_paths}) {
  143         333  
139 575 50       9728 if (-d "$path/$tpls") {
140 0         0 unshift @{$r_paths}, "$path/$tpls";
  0         0  
141 0         0 last;
142             }
143             }
144             }
145             }
146             else {
147 0         0 unshift @{$r_paths}, "$root/$dom->{domain}/templates";
  0         0  
148             }
149              
150             # Templates and routes are cached per domain. By the 'key_prefix' trick we
151             # can provide different templates with the same name to the renderer. This is
152             # how a long running application can switch to different themes per domain
153             # and have different templates although looking like having "the same" path.
154             # This is transparent for the renderer.
155 143         1068 $cache->key_prefix($dom->{domain});
156 143         1365 $c->stash(domain => $dom);
157 143         3002 $next->();
158 142         149475 shift @{$s_paths};
  142         333  
159 142         314 shift @{$r_paths};
  142         294  
160 142         465 return;
161             }
162              
163 14     14   22 sub _load_config ($app) {
  14         22  
  14         21  
164 14         48 my $etc = $app->resources->child('etc');
165 14         2297 my $moniker = $app->moniker;
166 14         87 my $mode = $app->mode;
167 14         66 my $home = $app->home;
168              
169             # Load configuration from hash returned by "slovo.conf"
170 14         75 my $file = $etc->child("$moniker.conf");
171 14         228 my $mode_file = $etc->child("$moniker.$mode.conf");
172 14         207 my $home_file = $home->child("$moniker.conf");
173 14         211 my $home_mode_file = $home->child("$moniker.$mode.conf");
174             $ENV{MOJO_CONFIG}
175 14   33     272 //= (-f $home_mode_file && $home_mode_file)
      66        
176             || (-f $home_file && $home_file)
177             || (-f $mode_file ? $mode_file : $file);
178              
179 14         1209 my $config = $app->plugin('Config');
180 14   50     4424 for my $class (@{$config->{load_classes} // []}) {
  14         102  
181 0         0 $app->load_class($class);
182             }
183 14         145 $app->secrets($config->{secrets});
184              
185             # Enable response compression
186 14 50       118 if ($config->{response_compression}) {
187 0         0 $app->renderer->compress(1);
188             }
189              
190 14   50     29 for my $setting (@{$config->{sessions} // []}) {
  14         57  
191 28         636 my ($a, $v) = (keys %$setting, values %$setting);
192 28         111 $app->sessions->$a($v);
193             }
194              
195 14         248 return $app;
196             }
197              
198 14     14   32 sub _load_pugins ($app) {
  14         425  
  14         28  
199              
200             # Namespaces to load plugins from
201             # See /perldoc/Mojolicious#plugins
202             # See /perldoc/Mojolicious/Plugins#PLUGINS
203 14         48 $app->plugins->namespaces(['Slovo::Plugin', 'Slovo', 'Mojolicious::Plugin']);
204 14   50     212 my $plugins = $app->config('load_plugins') // [];
205 14         219 foreach my $plugin (@$plugins) {
206 167 100       2523 my $name = (ref $plugin ? (keys %$plugin)[0] : $plugin);
207              
208             # $app->log->debug('Loading Plugin ' . $name);
209             # some plugins return $self and we are going to abuse this.
210 167         268 my $plug;
211 167 100       509 if (ref $plugin eq 'HASH') {
    50          
212 125         250 my $value = (values %$plugin)[0];
213 125 100       1301 $plug = $app->plugin($name => ref $value eq 'CODE' ? $value->() : $value);
214             }
215             elsif (!ref($plugin)) {
216 42         149 $plug = $app->plugin($name);
217             }
218              
219             # Make OpenAPI specification allways available!
220 167 100       6103730 if ($name eq 'OpenAPI') {
221 356         487 $app->helper(
222 356     356   436 openapi_spec => sub ($c_or_app, $path = '/') {
  356         17054  
  356         569  
223 356         1382 $plug->validator->get($path);
224 14         148 });
225             }
226             }
227 14         87 $app->routes->any('/*page_alias')->to('stranici#execute')->name('catch_all');
228              
229 14         5803 return $app;
230             }
231              
232 14     14   36 sub _default_paths ($app) {
  14         28  
  14         22  
233              
234             # Fallback "public" directory
235 14         29 push @{$app->static->paths}, $app->resources->child('public')->to_string;
  14         52  
236              
237             # Fallback templates directory
238             # See /perldoc/Mojolicious/Renderer#paths
239 14         790 push @{$app->renderer->paths}, $app->resources->child('templates')->to_string;
  14         51  
240              
241             # Current heme
242 14         399 return $app;
243             }
244              
245              
246             # Set Mojolicious::Routes object attributes and types
247             # See Mojolicious::Routes#base_classes
248             # See Mojolicious::Guides::Routing#Placeholder-types
249 14     14   26 sub _set_routes_attrs ($app) {
  14         26  
  14         20  
250 14         57 my $r = $app->routes;
251 14         52 push @{$r->base_classes}, $app->controller_class;
  14         66  
252 14         193 $r->namespaces($r->base_classes);
253 14         159 my $w = qr/[\w\-]+/;
254 14         60 @{$r->types}{qw(lng str cel fl_token)}
  14         49  
255             = (qr/[A-z]{2}(?:\-[A-z]{2})?/a, $w, $w, qr/[a-f0-9]{40}/);
256 14         184 return $app;
257             }
258              
259             # Add more media types
260 14     14   27 sub _add_media_types ($app) {
  14         25  
  14         24  
261 14         95 $app->types->type(woff => ['application/font-woff', 'font/woff']);
262 14         1258 $app->types->type(woff2 => ['application/font-woff2', 'font/woff2']);
263 14         196 return $app;
264             }
265              
266 84     84 1 138 sub load_class ($app, $class) {
  84         125  
  84         130  
  84         91  
267              
268             # state $log = $app->log;
269             # $log->debug("Loading $class");
270 84 50       213 if (my $e = Mojo::Loader::load_class $class) {
271 0 0       0 Carp::croak ref $e ? "Exception: $e" : "$class - Not found!";
272             }
273 84         932582 return;
274             }
275              
276             1;
277              
278             =encoding utf8
279              
280             =head1 NAME
281              
282             Slovo - Искони бѣ Слово
283              
284             =head1 SYNOPSIS
285              
286             Install Slovo locally with all dependencies in less than two minutes
287              
288             time curl -L https://cpanmin.us | perl - -M https://cpan.metacpan.org \
289             -q -n -l ~/opt/slovo Slovo
290              
291             Run slovo for the first time in debug mode
292              
293             morbo ~/opt/slovo/bin/slovo
294              
295             Visit L.
296             For help visit L.
297              
298             =head1 DESCRIPTION
299              
300             L is a simple to install and extensible L
301             L
302             with nice core features, listed below.
303              
304             This is a usable release, yet B and B
305             pieces>! The project is in active development, so expect often breaking changes.
306              
307             =over
308              
309             =item * On the fly generation of static pages under Apache/CGI – perfect for
310             cheap shared hosting and blogging – BETA;
311              
312             =item * Multi-domain support - BETA;
313              
314             =item * Multi-language pages - WIP;
315              
316             =item * Cached published pages and content - DONE;
317              
318             =item * Multi-user support - DONE;
319              
320             =item * User onboarding - WIP;
321              
322             =item * User sign in - DONE;
323              
324             =item * Managing pages, content, domains, users - WIP;
325              
326             =item * Managing groups - BASIC;
327              
328             =item * Multiple groups per user - DONE;
329              
330             =item * Ownership and permissions management per page and it's content - BETA;
331              
332             =item * Automatic 301 and 308 (Moved Permanently) redirects for renamed pages
333             and content - DONE;
334              
335             =item * Embedded fonts for displaying all
336             L and
337             L characters -
338             DONE;
339              
340             =item * OpenAPI 2/3.0 (Swagger) REST API - BASIC;
341              
342             =item * Embedded Trumbowyg - L;
343              
344             =item * Embedded Editor.md - L
345             (component), based on CodeMirror & jQuery &
346             Marked|http://editor.md.ipandao.com/>;
347              
348             =item * Example startup scripts for slovo and slovo_minion services
349             for L, L
350             2.4|https://httpd.apache.org/docs/2.4/> and NGINX vhost configuration files.
351              
352             =item * Inflatable embedded themes support - BETA;
353              
354             =item * and more to come…
355              
356             =back
357              
358             By default Slovo comes with SQLite database, but support for PostgreSQL or
359             MySQL is about to be added when needed. It is just a question of making
360             compatible and/or translating some limited number of SQL queries to the
361             corresponding SQL dialects. Contributors are welcome.
362              
363             The word "slovo" (слово) has one unchanged meaning during the last millennium
364             among all slavic languages. It is actually one language that started splitting
365             apart less than one thousand years ago. The meaning is "word" – the God's word
366             (when used with capital letter). Hence the self-naming of this group of people
367             C - people who have been given the God's word or
368             people who can speak. All others were considered "mute", hence the naming
369             (немци)...
370              
371             =head1 INSTALL
372              
373             All you need is a one-liner, it takes less than a minute.
374              
375             $ curl -L https://cpanmin.us | perl - -M https://cpan.metacpan.org -n -l ~/opt/slovo Slovo
376              
377             We recommend the use of a L environment.
378              
379             If you already downloaded it and you have L.
380              
381             $ cpanm -l ~/opt/slovo Slovo-XXXX.XX.XX.tar.gz
382              
383             Or even if you don't have C. Note that you need to install dependencies first.
384             Set C, remove old Slovo installation, make, test, install, create
385             data directory for sqlite database and run slovo to see available commands.
386              
387             tar zxf Slovo-XXXX.XX.XX.tar.gz
388             cd Slovo-XXXX.XX.XX
389             INSTALL_BASE=~/opt/slovo && rm -rf $INSTALL_BASE && make distclean; \
390             perl Makefile.PL INSTALL_BASE=$INSTALL_BASE && make && make test && make install \
391             && $INSTALL_BASE/bin/slovo eval 'app->home->child("data")->make_path({mode => 0700});' \
392             && $INSTALL_BASE/bin/slovo
393              
394             Use cpanm to install or update into a custom location as self contained application and
395             run slovo to see how it's going.
396              
397             # From metacpan. org
398             export PREFIX=~/opt/slovo;
399             cpanm -M https://cpan.metacpan.org -n --self-contained -l $PREFIX Slovo \
400             $PREFIX/bin/slovo eval 'app->home->child("data")->make_path({mode => 0700});' \
401             $PREFIX/bin/slovo
402              
403             # From the directory where you unpacked Slovo
404             export PREFIX=~/opt/slovo;
405             cpanm . -n --self-contained -l $PREFIX Slovo
406             $PREFIX/bin/slovo eval 'app->home->child("data")->make_path({mode => 0700});'
407             $PREFIX/bin/slovo
408              
409             Start the development server and open a browser
410              
411             morbo ./script/slovo -l http://*:3000 & sleep 1 exo-open http://localhost:3000
412              
413             =head1 USAGE
414              
415             cd /path/to/installed/slovo
416             # ...and see various options
417             ./bin/slovo
418              
419             =head1 CONFIGURATION, PATHS and UPGRADING
420              
421             L is a L application which means that everything
422             applying to Mojolicious applies to it too. Slovo main configuration file is
423             in C. You can use your own by setting
424             C<$ENV{MOJO_CONFIG}> or by just copying C to $ENV{MOJO_HOME} and
425             modify it as you wish. Routes can be added or removed in C. See
426             L for details and examples. New plugins can
427             be added per deployment in C section in C.
428              
429             C<$ENV{MOJO_HOME}> (L) is automatically
430             detected and used. All paths, used in the application, are expected to be its
431             children. You can add your own templates in C<$ENV{MOJO_HOME}/templates> and
432             they will be loaded and used with priority. You can theme your own instance of
433             Slovo by just copying C<$ENV{MOJO_HOME}/lib/Slovo/resources/templates> to
434             C<$ENV{MOJO_HOME}/templates> and modify them. You can add your own static files
435             to C<$ENV{MOJO_HOME}/public>. You can create custom themes by forking
436             L and using it as a starting point.
437              
438             You can have separate static files and templates per domain under
439             C<$ENV{MOJO_HOME}/domove/your.domain/public>,
440             C<$ENV{MOJO_HOME}/domove/your.other.domain/templates>, etc. See
441             C<$ENV{MOJO_HOME}/domove/localhost> for example.
442              
443             You can switch between different themes by just selecting the theme in the
444             form for editing domains.
445              
446             Last but not least, you can add your own classes into
447             C<$ENV{MOJO_HOME}/site/lib> and (why not) replace entirely some Slovo classes
448             or just extend them. C<$ENV{MOJO_HOME}/bin/slovo> will load them with priority.
449              
450             With all the above, you can upgrade L by just installing new versions
451             over it and your files will not be touched. And of course, we know that you are
452             using versioning just in case anything goes wrong. See L.
453              
454             =head1 ATTRIBUTES
455              
456             L inherits all attributes from L and implements
457             the following new ones.
458              
459             =head2 home
460              
461             L detects where B is not like L by where
462             C is but by where the C