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   11137 use feature ':5.26';
  14     12   31  
  14         1976  
  12         80  
  12         29  
  12         1204  
3 14     14   897 use Mojo::Base 'Mojolicious', -signatures;
  14     12   192303  
  14         114  
  12         76  
  12         23  
  12         78  
4              
5 14     14   404734 use Mojo::Util 'class_to_path';
  14     12   36  
  14         980  
  12         848680  
  12         35  
  12         584  
6 14     14   108 use Mojo::File 'path';
  14     12   29  
  14         643  
  12         79  
  12         31  
  12         501  
7 14     14   88 use Mojo::Collection 'c';
  14     12   42  
  14         588  
  12         72  
  12         193  
  12         576  
8 14     14   1236 use Slovo::Controller::Auth;
  14     12   31  
  14         134  
  12         7220  
  12         39  
  12         187  
9 14     14   1284 use Slovo::Validator;
  14     12   31  
  14         94  
  12         5747  
  12         44  
  12         135  
10 14     14   1296 use Slovo::Cache;
  14     12   28  
  14         88  
  12         5922  
  12         29  
  12         107  
11 14     14   1465 use Time::Piece;
  14     12   14507  
  14         131  
  12         7282  
  12         86559  
  12         62  
12              
13             our $AUTHORITY = 'cpan:BEROV';
14             our $VERSION = '2023.03.22';
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 95656 my $class_or_self = shift;
27 145 100 100     1046 if (ref($class_or_self) && $class_or_self->{home}) { return $class_or_self->{home}; }
  131         1733  
28 14 50       65 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         1740 my $m = lc $CLASS; # moniker *should* have the same name
36 14   66     66 while (($r = $r->dirname) && @{$r->to_array} > 2) {
  40         6424  
37 27 50 66     516 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       60 if (ref($class_or_self)) { return $class_or_self->{home} = $r; }
  0         0  
43             else {
44 1         7 return $r;
45             }
46             }
47             ;
48             }
49             return
50             ref($class_or_self)
51 13 50       372 ? ($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 352470 sub startup ($app) {
  13         37  
  13         24  
74 13         54 $app->log->debug("Starting $CLASS $VERSION|$CODENAME");
75 13         482 $app->controller_class('Slovo::Controller');
76 13         142 $app->commands->namespaces(
77             ['Slovo::Command::Author', 'Slovo::Command', 'Mojolicious::Command']);
78             ## no critic qw(Subroutines::ProtectPrivateSubs)
79 13         1110 $app->hook(around_action => \&_around_action);
80 13         208 $app->hook(around_dispatch => \&_around_dispatch);
81 13         139 $app->hook(before_dispatch => \&_before_dispatch);
82 13         155 $app->_set_routes_attrs->_load_config->_load_pugins->_default_paths->_add_media_types();
83 13         130 my $cache = Slovo::Cache->new();
84 13         84 $app->renderer->cache($cache);
85 13         218 $app->routes->cache($cache);
86 13         258 $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         3788 return $app;
100             }
101              
102 87     87   2919 sub _before_dispatch ($c) {
  87         138  
  87         130  
103 87         225 state $guest = $c->users->find_by_login_name('guest');
104 5         381 state $auth_config = c(@{$c->config('load_plugins')})->first(sub {
105 15 50   15   446 ref $_ eq 'HASH' and exists $_->{Authentication};
106 87         27116 });
107 87         180 state $session_key = $auth_config->{Authentication}{session_key};
108 87         144 state $current_user_fn = $auth_config->{Authentication}{current_user_fn};
109 87 100       340 unless ($c->session->{$session_key}) {
110              
111             #set the guest user as default to always have a user
112 42         14920 $c->$current_user_fn($guest);
113             }
114 87         19857 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 97     97   168528 sub _around_action ($next, $c, $action, $last) {
  97         201  
  97         168  
  97         183  
  97         194  
  97         145  
120 97 100 66     702 if ($last && $c->current_route !~ /^api\./) {
121 85         3197 my $stash = $c->stash;
122 85   33     1263 $stash->{l} //= $c->language; # current language of the text being edited
123 85   33     23009 $stash->{user} //= $c->user; # current user
124             }
125 97         142658 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 87     87   904703 sub _around_dispatch ($next, $c) {
  87         194  
  87         188  
  87         138  
131 87         211 state $app = $c->app;
132 87         225 state $root = $app->config('domove_root');
133              
134 87         227 state $s_paths = $app->static->paths;
135 87         213 state $r_paths = $app->renderer->paths;
136 87         166 state $cache = $app->renderer->cache;
137             my $dom
138 87   50     183 = 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 87         3914 unshift @{$s_paths}, "$root/$dom->{domain}/public";
  87         552  
146 87 50       390 if (my $tpls = $dom->{templates}) {
147              
148             # absolute path
149 87 50 33     446 if ($tpls =~ m|^/| && -d $tpls) {
150 0         0 unshift @{$r_paths}, $tpls;
  0         0  
151             }
152             else {
153 87         139 unshift @{$r_paths}, "$root/$dom->{domain}/templates";
  87         436  
154              
155             # try to find the relative path to the theme in the list of paths
156 87         167 for my $path (@{$r_paths}) {
  87         210  
157 348 50       5587 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 87         677 $cache->key_prefix($dom->{domain});
174 87         978 $c->stash(domain => $dom);
175 87         1945 $next->();
176 87         168442 shift @{$s_paths};
  87         251  
177 87         186 shift @{$r_paths};
  87         190  
178 87         269 return;
179             }
180              
181 13     13   25 sub _load_config ($app) {
  13         21  
  13         23  
182 13         51 my $etc = $app->resources->child('etc');
183 13         2544 my $moniker = $app->moniker;
184 13         674 my $mode = $app->mode;
185 13         107 my $home = $app->home;
186              
187             # Load configuration from hash returned by "slovo.conf"
188 13         80 my $file = $etc->child("$moniker.conf");
189 13         305 my $mode_file = $etc->child("$moniker.$mode.conf");
190 13         264 my $home_file = $home->child("$moniker.conf");
191 13         249 my $home_mode_file = $home->child("$moniker.$mode.conf");
192             $ENV{MOJO_CONFIG}
193 13   33     304 //= (-f $home_mode_file && $home_mode_file)
      66        
194             || (-f $home_file && $home_file)
195             || (-f $mode_file ? $mode_file : $file);
196              
197 13         1429 my $config = $app->plugin('Config');
198 13   50     4227 for my $class (@{$config->{load_classes} // []}) {
  13         78  
199 0         0 $app->load_class($class);
200             }
201 13         129 $app->secrets($config->{secrets});
202              
203             # Enable response compression
204 13 50       107 if ($config->{response_compression}) {
205 0         0 $app->renderer->compress(1);
206             }
207              
208 13   50     31 for my $setting (@{$config->{sessions} // []}) {
  13         55  
209 26         541 my ($a, $v) = (keys %$setting, values %$setting);
210 26         118 $app->sessions->$a($v);
211             }
212              
213 13         215 return $app;
214             }
215              
216 13     13   29 sub _load_pugins ($app) {
  13         25  
  13         23  
217              
218             # Namespaces to load plugins from
219             # See /perldoc/Mojolicious#plugins
220             # See /perldoc/Mojolicious/Plugins#PLUGINS
221 13         43 $app->plugins->namespaces(['Slovo::Plugin', 'Slovo', 'Mojolicious::Plugin']);
222 13   50     158 my $plugins = $app->config('load_plugins') // [];
223 13         165 foreach my $plugin (@$plugins) {
224 155 100       2314 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         274 my $plug;
229 155 100       523 if (ref $plugin eq 'HASH') {
    50          
230 116         255 my $value = (values %$plugin)[0];
231 116 100       1265 $plug = $app->plugin($name => ref $value eq 'CODE' ? $value->() : $value);
232             }
233             elsif (!ref($plugin)) {
234 39         140 $plug = $app->plugin($name);
235             }
236              
237             # Make OpenAPI specification allways available!
238 155 100       6946791 if ($name eq 'OpenAPI') {
239 294         428 $app->helper(
240 294     294   377 openapi_spec => sub ($c_or_app, $path = '/') {
  294         18517  
  294         539  
241 294         973 $plug->validator->get($path);
242 13         136 });
243             }
244             }
245 13         112 $app->routes->any('/*page_alias')->to('stranici#execute')->name('catch_all');
246              
247 13         5771 return $app;
248             }
249              
250 13     13   35 sub _default_paths ($app) {
  13         31  
  13         25  
251              
252             # Fallback "public" directory
253 13         29 push @{$app->static->paths}, $app->resources->child('public')->to_string;
  13         74  
254              
255             # Fallback templates directory
256             # See /perldoc/Mojolicious/Renderer#paths
257 13         772 push @{$app->renderer->paths}, $app->resources->child('templates')->to_string;
  13         51  
258              
259             # Current heme
260 13         465 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   31 sub _set_routes_attrs ($app) {
  13         28  
  13         25  
268 13         54 my $r = $app->routes;
269 13         57 push @{$r->base_classes}, $app->controller_class;
  13         80  
270 13         214 $r->namespaces($r->base_classes);
271 13         162 my $w = qr/[\w\-]+/;
272 13         62 @{$r->types}{qw(lng str cel fl_token)}
  13         74  
273             = (qr/[A-z]{2}(?:\-[A-z]{2})?/a, $w, $w, qr/[a-f0-9]{40}/);
274 13         221 return $app;
275             }
276              
277             # Add more media types
278 13     13   28 sub _add_media_types ($app) {
  13         27  
  13         25  
279 13         206 $app->types->type(woff => ['application/font-woff', 'font/woff']);
280 13         1214 $app->types->type(woff2 => ['application/font-woff2', 'font/woff2']);
281 13         238 return $app;
282             }
283              
284 78     78 1 143 sub load_class ($app, $class) {
  78         124  
  78         111  
  78         98  
285              
286             # state $log = $app->log;
287             # $log->debug("Loading $class");
288 78 50       216 if (my $e = Mojo::Loader::load_class $class) {
289 0 0       0 Carp::croak ref $e ? "Exception: $e" : "$class - Not found!";
290             }
291 78         1014582 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