Layout::Base: git_commit_reload_resources config parameter
[kivitendo-erp.git] / SL / Layout / Base.pm
1 package SL::Layout::Base;
2
3 use strict;
4 use parent qw(Rose::Object);
5
6 use File::Slurp qw(read_file);
7 use List::MoreUtils qw(uniq);
8 use Time::HiRes qw();
9
10 use Rose::Object::MakeMethods::Generic (
11   'scalar --get_set_init' => [ qw(menu auto_reload_resources_param sub_layouts_by_name) ],
12   'scalar'                => qw(focus),
13   'array'                 => [
14     'add_stylesheets_inline' => { interface => 'add', hash_key => 'stylesheets_inline' },
15     'add_javascripts_inline' => { interface => 'add', hash_key => 'javascripts_inline' },
16     'sub_layouts',           => { interface => 'get_set_init' },
17     'add_sub_layouts'        => { interface => 'add', hash_key => 'sub_layouts' },
18   ],
19 );
20
21 use SL::Menu;
22 use SL::Presenter;
23 use SL::System::Process;
24
25 my %menu_cache;
26
27 sub new {
28   my ($class, @slurp) = @_;
29
30   my $self = $class->SUPER::new(@slurp);
31 }
32
33 sub init_menu {
34   SL::Menu->new('user');
35 }
36
37 sub init_sublayouts_by_name {
38   {}
39 }
40
41 sub get {
42   $_[0]->sub_layouts;
43   return grep { $_ } ($_[0]->sub_layouts_by_name->{$_[1]});
44 }
45
46 sub init_auto_reload_resources_param {
47   if ($::lx_office_conf{debug}->{auto_reload_resources}) {
48     return sprintf('?rand=%d-%d-%d', Time::HiRes::gettimeofday(), int(rand 1000000000000));
49   }
50
51   if ($::lx_office_conf{debug}{git_commit_reload_resources}) {
52     my $git_dir = SL::System::Process::exe_dir() . '/.git';
53
54     return '' unless -d $git_dir;
55
56     my $content = eval { scalar(read_file($git_dir . '/HEAD')) };
57
58     return '' unless ($content // '') =~ m{\Aref: ([^\r\n]+)};
59
60     $content = eval { scalar(read_file($git_dir . '/' . $1)) };
61
62     return '' unless ($content // '') =~ m{\A([0-9a-fA-F]+)};
63
64     return '?rand=' . $1;
65   }
66
67   return '';
68 }
69
70 ##########################################
71 #  inheritable/overridable
72 ##########################################
73
74 sub pre_content {
75   join '', map { $_->pre_content } $_[0]->sub_layouts;
76 }
77
78 sub start_content {
79   join '', map { $_->start_content } $_[0]->sub_layouts;
80 }
81
82 sub end_content {
83   join '', map { $_->end_content } $_[0]->sub_layouts;
84 }
85
86 sub post_content {
87   join '', map { $_->post_content } $_[0]->sub_layouts;
88 }
89
90 sub stylesheets_inline {
91   uniq ( map { $_->stylesheets_inline } $_[0]->sub_layouts ),
92   @{ $_[0]->{stylesheets_inline} || [] };
93 }
94
95 sub javascripts_inline {
96   uniq ( map { $_->javascripts_inline } $_[0]->sub_layouts ),
97   @{ $_[0]->{javascripts_inline} || [] };
98 }
99
100 sub init_sub_layouts { [] }
101
102 sub init_sub_layouts_by_name { +{} }
103
104
105 #########################################
106 # Interface
107 ########################################
108
109 sub add_stylesheets {
110   &use_stylesheet;
111 }
112
113 sub use_stylesheet {
114   my $self = shift;
115   push @{ $self->{stylesheets} ||= [] }, @_ if @_;
116   @{ $self->{stylesheets} ||= [] };
117 }
118
119 sub stylesheets {
120   my ($self) = @_;
121   my $css_path = $self->get_stylesheet_for_user;
122
123   return uniq grep { $_ } map { $self->_find_stylesheet($_, $css_path)  }
124     $self->use_stylesheet, map { $_->stylesheets } $self->sub_layouts;
125 }
126
127 sub _find_stylesheet {
128   my ($self, $stylesheet, $css_path) = @_;
129
130   return "$css_path/$stylesheet" if -f "$css_path/$stylesheet";
131   return "css/$stylesheet"       if -f "css/$stylesheet";
132   return $stylesheet             if -f $stylesheet;
133   return $stylesheet             if $stylesheet =~ /^http/; # external
134 }
135
136 sub get_stylesheet_for_user {
137   my $css_path = 'css';
138   if (my $user_style = $::myconfig{stylesheet}) {
139     $user_style =~ s/\.css$//; # nuke trailing .css, this is a remnant of pre 2.7.0 stylesheet handling
140     if (-d "$css_path/$user_style" &&
141         -f "$css_path/$user_style/main.css") {
142       $css_path = "$css_path/$user_style";
143     } else {
144       $css_path = "$css_path/kivitendo";
145     }
146   } else {
147     $css_path = "$css_path/kivitendo";
148   }
149
150   return $css_path;
151 }
152
153 sub add_javascripts {
154   &use_javascript
155 }
156
157 sub use_javascript {
158   my $self = shift;
159   push @{ $self->{javascripts} ||= [] }, @_ if @_;
160   @{ $self->{javascripts} ||= [] };
161 }
162
163 sub javascripts {
164   my ($self) = @_;
165
166   return uniq grep { $_ } map { $self->_find_javascript($_)  }
167     map({ $_->javascripts } $self->sub_layouts), $self->use_javascript;
168 }
169
170 sub _find_javascript {
171   my ($self, $javascript) = @_;
172
173   return "js/$javascript"        if -f "js/$javascript";
174   return $javascript             if -f $javascript;
175   return $javascript             if $javascript =~ /^http/;
176 }
177
178
179 ############################################
180 # track state of form header
181 ############################################
182
183 sub header_done {
184   $_[0]{_header_done} = 1;
185 }
186
187 sub need_footer {
188   $_[0]{_header_done};
189 }
190
191 sub presenter {
192   SL::Presenter->get;
193 }
194
195 1;
196
197 __END__
198
199 =encoding utf-8
200
201 =head1 NAME
202
203 SL::Layout::Base - Base class for layouts
204
205 =head1 SYNOPSIS
206
207   package SL::Layout::MyLayout;
208
209   use parent qw(SL::Layout::Base);
210
211 =head1 DESCRIPTION
212
213 For a description of the external interface of layouts in general see
214 L<SL::Layout::Dispatcher>.
215
216 This is a base class for layouts in general. It provides the basic interface
217 and some capabilities to extend and cascade layouts.
218
219
220 =head1 IMPLEMENTING LAYOUT CALLBACKS
221
222 There are eight callbacks (C<pre_content>, C<post_content>, C<start_content>,
223 C<end_content>, C<stylesheets>, C<stylesheets_inline>, C<javascripts>,
224 C<javascripts_inline>) which are documented in L<SL::Layout::Dispatcher>. If
225 you are writing a new simple layout, you can just override some of them like
226 this:
227
228   package SL::Layout::MyEvilLayout;
229
230   sub pre_content {
231     '<h1>This is MY page now</h1>'
232   }
233
234   sub post_content {
235     '<p align="right"><small><em>Brought to you by my own layout class</em></small></p>'
236   }
237
238
239 To preserve the sanitizing effects of C<stylesheets> and C<javascripts> you should instead do the following:
240
241   sub stylesheets {
242     $_[0]->add_stylesheets(qw(mystyle1.css mystyle2.css);
243     $_[0]->SUPER::stylesheets;
244   }
245
246 If you want to add something to a different layout, you should write a sub
247 layout and add it to the other layouts.
248
249
250 =head1 SUB LAYOUTS
251
252 Layouts can be aggregated, so that common elements can be used in different
253 layouts. Currently this is used for the L<None|SL::Layout::None> sub layout,
254 which contains a lot of the stylesheets and javascripts necessary. Another
255 example is the L<Top|SL::Layout::Top> layout, which is used to generate a
256 common top bar for all menu types.
257
258 To add a sub layout to your layout just overwrite the sub_layout method:
259
260   package SL::Layout::MyFinalLayout;
261
262   sub init_sub_layout {
263     [
264       SL::Layout::None->new,
265       SL::Layout::MyEvilLayout->new,
266     ]
267   }
268
269 You can also add a sublayout at runtime:
270
271   $layout->add_sub_layout(SL::Layout::SideBar->new);
272
273 The standard implementation for the callbacks will see to it that the contents
274 of all sub layouts will get rendered.
275
276
277 =head1 COMBINING SUB LAYOUTS AND OWN BEHAVIOUR
278
279 This is still somewhat rough, and improvements are welcome.
280
281 For the C<*_content> callbacks this works if you just remember to dispatch to the base method:
282
283   sub post_content {
284     return $_[0]->render_status_bar .
285     $_[0]->SUPER::post_content
286   }
287
288 For the stylesheet and javascript callbacks things are hard, because of the
289 backwards compatibility, and the built-in sanity checks. The best way currently
290 is to just add your content and dispatch to the base method.
291
292   sub stylesheets {
293     $_[0]->add_stylesheets(qw(mystyle1.css mystyle2.css);
294     $_[0]->SUPER::stylesheets;
295   }
296
297 =head1 GORY DETAILS ABOUT JAVASCRIPT AND STYLESHEET OVERLOADING
298
299 The original code used to store one stylesheet in C<< $form->{stylesheet} >> and
300 allowed/expected authors of potential C<bin/mozilla/> controllers to change
301 that into their own modified stylesheet.
302
303 This was at some point cleaned up into a method C<use stylesheet> which took a
304 string of space separated stylesheets and processed them into the response.
305
306 A lot of controllers are still using this method so the layout interface
307 supports it to change as little controller code as possible, while providing the
308 more intuitive C<add_stylesheets> method.
309
310 At the same time the following things need to be possible:
311
312 =over 4
313
314 =item 1.
315
316 Runtime additions.
317
318   $layout->add_stylesheets(...)
319
320 Since add_stylesheets adds to C<< $self->{stylesheets} >> there must be a way to read
321 from it. Currently this is the deprecated C<use_stylesheet>.
322
323 =item 2.
324
325 Overriding Callbacks
326
327 A leaf layout should be able to override a callback to return a list.
328
329 =item 3.
330
331 Sanitizing
332
333 C<stylesheets> needs to retain its sanitizing behaviour.
334
335 =item 4.
336
337 Aggregation
338
339 The standard implementation should be able to collect from sub layouts.
340
341 =item 5.
342
343 Preserving Inclusion Order
344
345 Since there is currently no standard way of mixing own content and including
346 sub layouts, this has to be done manually. Certain things like jquery get added
347 in L<SL::Layout::None> so that they get rendered first.
348
349 =back
350
351 The current implementation provides no good candidate for overriding in sub
352 classes, which should be changed. The other points work pretty well.
353
354 =head1 BUGS
355
356 * stylesheet/javascript interface is a horrible mess.
357
358 * It's currently not possible to do compositor layouts without assupmtions
359 about the position of the content. That's because the content will return
360 control to the actual controller, so the layouts need to know where to split
361 pre- and post-content.
362
363 =head1 AUTHOR
364
365 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
366
367 =cut