File Coverage

blib/lib/Rhetoric.pm
Criterion Covered Total %
statement 9 9 100.0
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 12 12 100.0


line stmt bran cond sub pod time code
1             package Rhetoric;
2 1     1   1526 use 5.010_0;
  1         3  
  1         37  
3 1     1   5 use common::sense;
  1         1  
  1         8  
4 1     1   1709 use aliased 'Squatting::H';
  1         910  
  1         7  
5             use Squatting;
6             use Try::Tiny;
7              
8             use Rhetoric::Helpers ':all';
9             use Rhetoric::Widgets;
10             use Rhetoric::Meta;
11              
12             our $VERSION = '0.06';
13              
14             # global config for our blogging app
15             our %CONFIG = (
16             'base' => '.', # config directory - metadata, menus, widgets, pages
17             'user' => undef, # used for rhetoric.al accounts but otherwise optional
18             'time_format' => '%b %e, %Y %I:%M%P',
19             'archive_format' => '%B %Y', # TODO - use this!
20             'posts_per_page' => 8,
21              
22             'theme' => 'BrownStone', # Rhetoric::Theme::____
23             'theme.base' => './share/theme',
24              
25             'login' => 'admin',
26             'password' => 'admin',
27              
28             'storage' => 'File', # Rhetoric::Storage::____
29             'storage.file.path' => '.',
30             # TODO
31             'storage.couchdb.url' => undef, # URL for CouchDB database
32             # TODO
33             'storage.mysql.connect' => undef, # connect string suitable for DBI->connect
34             'storage.mysql.user' => undef,
35             'storage.mysql.password' => undef,
36              
37             # just for continuity
38             docroot => 'share',
39             );
40              
41             sub continue {
42             my $app = shift;
43             $app->next::method(
44             docroot => $CONFIG{'docroot'},
45             staticp => sub { $_[0]->url =~ m/\.(jpg|jpeg|gif|png|css|ico|js|swf)$/ },
46             @_
47             );
48             }
49              
50             # service() is run on every request (just like in Camping).
51             sub service {
52             my ($class, $c, @args) = @_;
53             $c->view = $c->state->{theme} // $CONFIG{theme};
54             my $v = $c->v;
55             my $s = $c->env->{storage} = storage($CONFIG{storage});
56             H->bless($v);
57             H->bless($c->input);
58             H->bless($c->env);
59             $v->{title} = $s->meta('title');
60             $v->{subtitle} = $s->meta('subtitle');
61             $v->{copy} = $s->meta('copy');
62             $v->{menu} = $s->menu;
63             $v->{request_path} = $c->env->{REQUEST_PATH};
64             $v->{time_format} = $CONFIG{time_format};
65             $v->{hostname} = $CONFIG{hostname} // $c->env->{HTTP_HOST};
66             $v->{state} = $c->state; # XXX - Should Squatting be doing this automatically?
67              
68             # hack to help rh-export
69             if ($c->state->{mock_request}) {
70             # the RIGHT THING(tm) would be to change how we store menu information.
71             # I suppose I need to support perl expressions in there instead of
72             # just strings.
73             # FIXME
74             for my $menu (@{$v->{menu}}) {
75             my $href = $menu->url;
76             if (($href !~ qr{^https?:}) && ($href !~ qr{\.html$}) && ($href ne '/')) {
77             $href .= ".html";
78             $menu->url($href);
79             }
80             }
81             }
82              
83             if (exists $CONFIG{relocated}) {
84             for (@{ $v->menu }) {
85             $_->url($CONFIG{relocated} . $_->url);
86             }
87             $v->{relocated} = $CONFIG{relocated};
88             }
89             for my $position ($s->widgets->positions) {
90             $v->{widgets}{$position} = [ $s->widgets->content_for($position, $c, @args) ];
91             }
92             $class->next::method($c, @args);
93             }
94              
95             # initialize app
96             sub init {
97             my ($class) = @_;
98              
99             # TODO - Make absolutely sure the Page controller is at $C[-1].
100             if ($Rhetoric::Controllers::C[-1]->name ne 'Page') {
101             # find index of Page controller
102             # splice it out
103             # push it back on to the end
104             }
105              
106             # view initialization
107             Rhetoric::Views::init();
108              
109             $class->next::method();
110             }
111              
112             # Return an object that handles the storage for blog data based on
113             # what $CONFIG{storage} dictates.
114             sub storage {
115             no strict 'refs';
116             my $impl = shift;
117             my $path = "Rhetoric/Storage/$impl.pm";
118             my $package = "Rhetoric::Storage::$impl";
119             require($path); # let it die if it fails.
120              
121             # the stuff that's ALWAYS in the filesystem (besides widgets)
122             # menus and pages might get split out later
123             my $meta = $Rhetoric::Meta::meta;
124              
125             # where posts and comments are stored
126             my $storage = ${"${package}::storage"};
127             $storage->init(\%CONFIG);
128              
129             # widgets
130             my $widgets = $Rhetoric::Widgets::widgets;
131             $widgets->init(\%CONFIG);
132              
133             my $blog = H->new({
134             base => $CONFIG{base},
135             widgets => $widgets,
136             %$meta,
137             %$storage,
138             });
139             }
140              
141             #_____________________________________________________________________________
142             package Rhetoric::Controllers;
143             use common::sense;
144             use aliased 'Squatting::H';
145             use Method::Signatures::Simple;
146             use Rhetoric::Helpers ':all';
147             use Data::Dump 'pp';
148             use MIME::Base64;
149             use Ouch;
150             use Try::Tiny;
151              
152             sub authorized {
153             my $self = shift;
154             return undef unless defined $self->env->{HTTP_AUTHORIZATION};
155             my $auth = $self->env->{HTTP_AUTHORIZATION};
156             $auth =~ s/Basic\s*//;
157             warn $auth;
158             my $login_pass = encode_base64("$CONFIG{login}:$CONFIG{password}", '');
159             warn $login_pass;
160             if ($auth eq $login_pass) {
161             return 1;
162             } else {
163             return 0;
164             }
165             }
166              
167             our @C = (
168              
169             C(
170             Home => [ '/', '/page/(\d+)' ],
171             get => method($page) {
172             my $v = $self->v;
173             my $storage = $self->env->storage;
174             $page //= 1;
175             ($v->{posts}, $v->{pager}) = $storage->posts($CONFIG{posts_per_page}, $page);
176             $self->render('index');
177             },
178             ),
179              
180             C(
181             Feed => [ '/feed' ],
182             get => method {
183             my $v = $self->v;
184             my $storage = $self->env->storage;
185             ($v->{posts}, $v->{pager}) = $storage->posts($CONFIG{posts_per_page}, 1);
186             $self->render('index', 'AtomFeed');
187             },
188             ),
189              
190             C(
191             Post => [ '/(\d+)/(\d+)/([\w-]+)' ],
192             get => method($year, $month, $slug) {
193             my $v = $self->v;
194             my $storage = $self->env->storage;
195             $v->{post} = $storage->post($year, $month, $slug);
196             $v->{comments} = $storage->comments($v->{post});
197             $self->render('post');
198             },
199             post => method($year, $month, $slug) {
200             my $v = $self->v;
201             my $storage = $self->env->storage;
202             my $post = $v->{post} = $storage->post($year, $month, $slug);
203             # XXX - modify post and redirect
204             }
205             ),
206              
207             # XXX - replace with Rhetoric::Admin->squat('/admin')
208             C(
209             NewPost => [ '/admin' ],
210             get => method {
211             if (authorized($self)) {
212             $self->render('new_post');
213             } else {
214             $self->status = 401;
215             $self->headers->{'WWW-Authenticate'} = 'Basic realm="Secret"';
216             "auth yourself";
217             }
218             },
219             post => method {
220             if (authorized($self)) {
221             my $storage = $self->env->storage;
222             my $input = $self->input;
223             try {
224             $storage->new_post({
225             title => $input->title,
226             body => $input->body,
227             format => $input->format,
228             });
229             }
230             catch {
231             if (kiss('InvalidPost', $_)) {
232             $self->state->{errors} = $_->data;
233             }
234             else {
235             }
236             };
237             $self->redirect(R('NewPost'));
238             } else {
239             $self->redirect(R('Home'));
240             }
241             },
242             ),
243              
244             C(
245             NewComment => [ '/comment' ],
246             post => method {
247             my $input = $self->input;
248             my $year = $input->year;
249             my $month = $input->month;
250             my $slug = $input->slug;
251             my $name = $input->name;
252             my $email = $input->email;
253             my $url = $input->url;
254             my $body = $input->body;
255             my $format = $input->format // 'pod';
256             my $storage = $self->env->storage;
257             my $state = $self->state;
258             warn pp $state;
259              
260             $state->{name} = $name;
261             $state->{email} = $email;
262             $state->{url} = $url;
263              
264             my $result;
265             try {
266             $result = $storage->new_comment($year, $month, $slug, {
267             name => $name,
268             email => $email,
269             url => $url,
270             body => $body,
271             format => $format,
272             });
273             }
274             catch {
275             if (kiss('InvalidComment'), $_) {
276             $self->state->{errors} = $_->data;
277             }
278             else {
279             warn $_;
280             }
281             };
282             $self->redirect(R('Post', $year, $month, $slug));
283             }
284             ),
285              
286             C(
287             Category => [ '/category/([\w-]+)' ],
288             get => method($category) {
289             my $v = $self->v;
290             my $storage = $self->env->storage;
291             ($v->{posts}, $v->{pager}) = $storage->category_posts($category);
292             $self->render('index');
293             }
294             ),
295              
296             C(
297             Archive => [ '/archive/(\d+)/(\d+)' ],
298             get => method($year, $month) {
299             my $v = $self->v;
300             my $storage = $self->env->storage;
301             ($v->{posts}, $v->{pager}) = $storage->archive_posts($year, $month);
302             $self->render('index');
303             }
304             ),
305              
306             C(
307             Env => [ '/env' ],
308             get => method {
309             use Data::Dump 'pp';
310             $self->headers->{'Content-Type'} = 'text/plain';
311             return pp($self->env);
312             }
313             ),
314              
315             C(
316             Theme => [ '/t', '/t/(.*)' ],
317             get => method($name) {
318             if ($name) {
319             $self->state->{theme} = $name;
320             }
321             return $self->env->{HTTP_HOST} . " => " .
322             ($self->state->{theme} // $CONFIG{theme}) . "\n";
323             }
324             ),
325              
326             # Everything else that's not static is a page to be rendered through the view.
327             # This controller has to be last!
328             C(
329             Page => [ '/(.*)' ],
330             get => method($path) {
331             if ($path =~ /\.\./) {
332             $self->status = 404;
333             return "GTFO";
334             }
335             my $v = $self->v;
336             $self->render($path);
337             }
338             ),
339              
340             );
341              
342             #_____________________________________________________________________________
343             package Rhetoric::Views;
344             use common::sense;
345             use Method::Signatures::Simple;
346             use Template;
347             use XML::Atom::Feed;
348             use XML::Atom::Entry;
349             use Module::Find;
350              
351             *CONFIG = \%Rhetoric::CONFIG;
352              
353             # Someday, there may be too many themes for the call to usesub to be practical.
354             # That would be a good problem to have.
355             our @themes = usesub('Rhetoric::Theme');
356              
357             our @V = (
358              
359             (map { $_->view } @themes),
360              
361             V(
362             'AtomFeed',
363              
364             _atom_id => method($post) {
365             my $hostname = $CONFIG{hostname};
366             sprintf('tag:%s,%d-%02d-%02d:%s',
367             $hostname,
368             $post->year, $post->month, $post->day,
369             R('Post', $post->year, $post->month, $post->slug)
370             );
371             },
372              
373             _link => method($post) {
374             my $link = XML::Atom::Link->new();
375             my $hostname = $CONFIG{hostname};
376             $link->type('text/html');
377             $link->rel('alternate');
378             $link->href(sprintf(
379             'http://%s%s',
380             $hostname,
381             R('Post', $post->year, $post->month, $post->slug)
382             ));
383             $link;
384             },
385              
386             index => method($v) {
387             my $feed = XML::Atom::Feed->new();
388             my $hostname = $CONFIG{hostname};
389             my $since = $CONFIG{since};
390             $feed->id(sprintf('tag:%s,%d:feed-id', $hostname, $since));
391             for my $post (@{ $v->posts }) {
392             my $entry = XML::Atom::Entry->new();
393             $entry->id($self->_atom_id($post));
394             $entry->add_link($self->_link($post));
395             $entry->title($post->title);
396             $entry->content($post->body);
397             $feed->add_entry($entry);
398             }
399             $feed->as_xml;
400             }
401              
402             ),
403              
404             );
405              
406             sub init {
407             my $i = 0;
408             for my $name (@themes) {
409             $name =~ s/^.*::(\w*)$/$1/;
410             $V[$i]->_init([ "$CONFIG{'base'}/pages", "$CONFIG{'theme.base'}/$name" ]);
411             $i++;
412             }
413             }
414              
415             1;
416              
417             =head1 NAME
418              
419             Rhetoric - a simple blogging system for perl
420              
421             =head1 SYNOPSIS
422              
423             Setting up a blog
424              
425             mkdir -p /var/www/myblog.org
426             cd /var/www/myblog.org
427             rh init
428              
429             Running the blog
430              
431             plackup rhetoric.psgi
432              
433             =head1 DESCRIPTION
434              
435             Rhetoric is a simple CPAN-friendly blogging system for Perl.
436              
437             It's simple because...
438              
439             It's CPAN-friendly because:
440              
441             =over 4
442              
443             =item * You can install it from CPAN and setup a blog in minutes.
444              
445             =item * Themes can also be installed from CPAN.
446              
447             =back
448              
449             =head1 API
450              
451             =head2 Home
452              
453             /
454              
455             /page/(\d+)
456              
457             =head3 get
458              
459              
460             =head2 Post
461              
462             /(\d+)/(\d+)/(\w+)
463              
464             =head3 get
465              
466              
467              
468             =head2 NewPost
469              
470             /post
471              
472             =head3 get
473              
474             =head3 post
475              
476              
477              
478             =head2 Comment
479              
480             /comment
481              
482             =head3 post
483              
484              
485              
486             =head2 Category
487              
488             /category/(\w+)
489              
490             =head3 get
491              
492              
493              
494             =head2 Archive
495              
496             /archive/(\d+)/(\d+)
497              
498             =head3 get
499              
500              
501              
502             =head2 Page
503              
504             /(.*)
505              
506             =head3 get
507              
508              
509              
510             =head1 AUTHOR
511              
512             John BEPPU Ebeppu@cpan.orgE
513              
514              
515             =head1 COPYRIGHT
516              
517             MIT
518              
519             =cut