File Coverage

blib/lib/Slovo.pm
Criterion Covered Total %
statement 205 217 94.4
branch 25 40 62.5
condition 22 41 53.6
subroutine 31 31 100.0
pod 3 3 100.0
total 286 332 86.1


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