sgfa 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1190 @@
1
+ #
2
+ # Simple Group of Filing Applications
3
+ # Web interface to Binder
4
+ #
5
+ # Copyright (C) 2015 by Graham A. Field.
6
+ #
7
+ # See LICENSE.txt for licensing information.
8
+ #
9
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
10
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
+
12
+ require 'rack'
13
+ require 'time'
14
+
15
+ require_relative 'base'
16
+ require_relative '../error'
17
+
18
+ module Sgfa
19
+ module Web
20
+
21
+
22
+ #####################################################################
23
+ # Binder web interface
24
+ #
25
+ # @todo Add a docket view
26
+ class Binder < Base
27
+
28
+ #####################################
29
+ # Request
30
+ #
31
+ # @param env [Hash] The Rack environment for this request, with app
32
+ # specific options
33
+ # @option env [String] 'sgfa.binder.url' URL encoded binder name
34
+ # @option env [String] 'sgfa.binder.name' The name of the binder
35
+ # @option env [Binder] 'sgfa.binder' The binder
36
+ # @option env [String] 'sgfa.user' The user name
37
+ # @option env [Array] 'sgfa.groups' Array of groups the user belongs to
38
+ #
39
+ def call(env)
40
+ _call(env)
41
+ return response(env)
42
+
43
+ rescue Error::Permission => exp
44
+ env['sgfa.status'] = :deny
45
+ env['sgfa.html'] = _escape_html(exp.message)
46
+ return response(env)
47
+
48
+ rescue Error::Conflict => exp
49
+ env['sgfa.status'] = :conflict
50
+ env['sgfa.html'] = _escape_html(exp.message)
51
+ return response(env)
52
+
53
+ rescue Error::NonExistent => exp
54
+ env['sgfa.status'] = :notfound
55
+ env['sgfa.html'] = _escape_html(exp.message)
56
+ return response(env)
57
+
58
+ rescue Error::Limits => exp
59
+ env['sgfa.status'] = :badreq
60
+ env['sgfa.html'] = _escape_html(exp.message)
61
+ return response(env)
62
+
63
+ rescue Error::Corrupt => exp
64
+ env['sgfa.status'] = :servererror
65
+ env['sgfa.html'] = _escape_html(exp.message)
66
+ return response(env)
67
+
68
+ end # def request()
69
+
70
+
71
+ #####################################
72
+ # Process the request
73
+ #
74
+ # This is a seperate method to simplify the flow control by using return.
75
+ def _call(env)
76
+
77
+ # defaults
78
+ env['sgfa.status'] = :badreq
79
+ env['sgfa.title'] = 'SFGA Error'
80
+ env['sfga.navbar'] = ''
81
+ env['sgfa.html'] = 'Invalid request'
82
+
83
+ path = env['PATH_INFO'].split('/')
84
+ if path.empty?
85
+ jacket = nil
86
+ else
87
+ path.shift if path[0].empty?
88
+ jacket = path.shift
89
+ end
90
+
91
+ # just the binder
92
+ if !jacket
93
+ case env['REQUEST_METHOD']
94
+ when 'GET'; return _get_jackets(env, path)
95
+ when 'POST'; return _post_binder(env)
96
+ else; return
97
+ end
98
+ end
99
+
100
+ # special binder pages
101
+ if jacket[0] == '_'
102
+ return if env['REQUEST_METHOD'] != 'GET'
103
+ case jacket
104
+ when '_jackets'
105
+ return _get_jackets(env, path)
106
+ when '_users'
107
+ return _get_users(env, path)
108
+ when '_values'
109
+ return _get_values(env, path)
110
+ when '_info'
111
+ return _get_binder(env, path)
112
+ else
113
+ return
114
+ end
115
+ end
116
+
117
+ # jacket info stored
118
+ env['sgfa.jacket.url'] = jacket
119
+ env['sgfa.jacket.name'] = _escape_un(jacket)
120
+ cmd = path.shift
121
+
122
+ # just the jacket
123
+ if !cmd
124
+ case env['REQUEST_METHOD']
125
+ when 'GET'; return _get_tag(env, path)
126
+ when 'POST'; return _post_jacket(env)
127
+ else; return
128
+ end
129
+ end
130
+
131
+ # jacket stuff
132
+ return if env['REQUEST_METHOD'] != 'GET'
133
+ case cmd
134
+ when '_edit'; return _get_edit(env, path)
135
+ when '_entry'; return _get_entry(env, path)
136
+ when '_history'; return _get_history(env, path)
137
+ when '_attach'; return _get_attach(env, path)
138
+ when '_tag'; return _get_tag(env, path)
139
+ when '_log'; return _get_log(env, path)
140
+ when '_list'; return _get_list(env, path)
141
+ when '_info'; return _get_info(env, path)
142
+ else; return
143
+ end
144
+
145
+ end # def _call()
146
+
147
+
148
+ NavBarBinder = [
149
+ ['Jackets', '_jackets'],
150
+ ['Users', '_users'],
151
+ ['Values', '_values'],
152
+ ['Binder', '_info'],
153
+ ]
154
+
155
+
156
+ #####################################
157
+ # Generate navigation bar for a binder
158
+ def _navbar_binder(env, act)
159
+ env['sgfa.title'] = 'SGFA Binder %s &mdash; %s' %
160
+ [act, _escape_html(env['sgfa.binder.name'])]
161
+ base = env['SCRIPT_NAME']
162
+ txt = _navbar(env, act, NavBarBinder, base)
163
+ if env['sgfa.cabinet.url']
164
+ txt << "<div class='link'><a href='%s'>Cabinet</a></div>\n" %
165
+ env['sgfa.cabinet.url']
166
+ end
167
+ env['sgfa.navbar'] = txt
168
+ end # def _navbar_binder()
169
+
170
+
171
+ NavBarJacket = [
172
+ ['Tag', '_tag'],
173
+ ['List', '_list'],
174
+ ['Entry', nil],
175
+ ['Edit', '_edit'],
176
+ ['History', nil],
177
+ ['Attachment', nil],
178
+ ['Jacket', '_info'],
179
+ ['Log', '_log'],
180
+ ]
181
+
182
+ #####################################
183
+ # Generate navbar for a jacket
184
+ def _navbar_jacket(env, act)
185
+ env['sgfa.title'] = 'SGFA Jacket %s &mdash; %s : %s' % [
186
+ act,
187
+ _escape_html(env['sgfa.binder.name']),
188
+ _escape_html(env['sgfa.jacket.name'])
189
+ ]
190
+ base = env['SCRIPT_NAME'] + '/' + env['sgfa.jacket.url']
191
+ txt = _navbar(env, act, NavBarJacket, base)
192
+ txt << "<div class='link'><a href='%s'>Binder</a></div>\n" %
193
+ env['SCRIPT_NAME']
194
+ env['sgfa.navbar'] = txt
195
+ end # def _navbar_jacket()
196
+
197
+
198
+ #####################################
199
+ # Link to a jacket
200
+ def _link_jacket(env, jnam, disp)
201
+ "<a href='%s/%s'>%s</a>" % [env['SCRIPT_NAME'], _escape(jnam), disp]
202
+ end # def _link_jacket()
203
+
204
+
205
+ #####################################
206
+ # Link to edit entry
207
+ def _link_edit(env, enum, disp)
208
+ "<a href='%s/%s/_edit/%d'>%s</a>" % [
209
+ env['SCRIPT_NAME'],
210
+ env['sgfa.jacket.url'],
211
+ enum,
212
+ disp
213
+ ]
214
+ end # def _link_edit()
215
+
216
+
217
+ #####################################
218
+ # Link to display an entry
219
+ def _link_entry(env, enum, disp)
220
+ "<a href='%s/%s/_entry/%d'>%s</a>" % [
221
+ env['SCRIPT_NAME'],
222
+ env['sgfa.jacket.url'],
223
+ enum,
224
+ disp
225
+ ]
226
+ end # def _link_entry()
227
+
228
+
229
+ #####################################
230
+ # Link to a specific revision
231
+ def _link_revision(env, enum, rnum, disp)
232
+ "<a href='%s/%s/_entry/%d/%d'>%s</a>" % [
233
+ env['SCRIPT_NAME'],
234
+ env['sgfa.jacket.url'],
235
+ enum,
236
+ rnum,
237
+ disp
238
+ ]
239
+ end # def _link_revision()
240
+
241
+
242
+ #####################################
243
+ # Link to a history item
244
+ def _link_history(env, hnum, disp)
245
+ "<a href='%s/%s/_history/%d'>%s</a>" % [
246
+ env['SCRIPT_NAME'],
247
+ env['sgfa.jacket.url'],
248
+ hnum, disp
249
+ ]
250
+ end # def _link_history()
251
+
252
+
253
+ #####################################
254
+ # Link to a tag
255
+ def _link_tag(env, tag, disp)
256
+ "<a href='%s/%s/_tag/%s'>%s</a>" % [
257
+ env['SCRIPT_NAME'],
258
+ env['sgfa.jacket.url'],
259
+ _escape(tag),
260
+ disp
261
+ ]
262
+ end # def _link_tag()
263
+
264
+
265
+ #####################################
266
+ # Link to a tag prefix
267
+ def _link_prefix(env, pre, disp)
268
+ "<a href='%s/%s/_list/%s'>%s</a>" % [
269
+ env['SCRIPT_NAME'],
270
+ env['sgfa.jacket.url'],
271
+ _escape(pre),
272
+ disp
273
+ ]
274
+ end # def _link_prefix()
275
+
276
+
277
+ #####################################
278
+ # Link to an attachments
279
+ def _link_attach(env, enum, anum, hnum, name, disp)
280
+ "<a href='%s/%s/_attach/%d-%d-%d/%s'>%s</a>" % [
281
+ env['SCRIPT_NAME'],
282
+ env['sgfa.jacket.url'],
283
+ enum,
284
+ anum,
285
+ hnum,
286
+ _escape(name),
287
+ disp
288
+ ]
289
+ end # def _link_attach()
290
+
291
+
292
+ JacketsTable =
293
+ "<table class='list'>" +
294
+ "<tr><th>Jacket</th><th>Title</th><th>Perms</th></tr>\n" +
295
+ "%s</table>"
296
+
297
+ JacketsRow =
298
+ "<tr><td class='name'>%s</td>" +
299
+ "<td class='title'>%s</td><td class='perms'>%s</td></tr>\n"
300
+
301
+ JacketsForm =
302
+ "\n<hr>\n<form class='edit' method='post' action='%s' " +
303
+ "enctype='multipart/form-data'>\n" +
304
+ "<fieldset><legend>Create or Edit Jacket</legend>\n" +
305
+ "<label for='jacket'>Name:</label>" +
306
+ "<input class='jacket' name='jacket' type='text'><br>\n" +
307
+ "<label for='newname'>Rename:</label>" +
308
+ "<input class='jacket' name='newname' type='text'><br>\n" +
309
+ "<label for='title'>Title:</label>" +
310
+ "<input class='title' name='title' type='text'><br>\n" +
311
+ "<label for='perms'>Perms:</label>" +
312
+ "<input class='perms' name='perms' type='text'><br>\n" +
313
+ "</fieldset>\n" +
314
+ "<input type='submit' name='create' value='Create/Edit'>\n" +
315
+ "</form>\n"
316
+
317
+ #####################################
318
+ # Get jacket list
319
+ def _get_jackets(env, path)
320
+ _navbar_binder(env, 'Jackets')
321
+
322
+ if !path.empty?
323
+ env['sgfa.status'] = :badreq
324
+ env['sgfa.html'] = 'Invalid URL requested'
325
+ return
326
+ end
327
+
328
+ tr = _trans(env)
329
+ info = env['sgfa.binder'].binder_info(tr)
330
+
331
+ env['sgfa.status'] = :ok
332
+ env['sgfa.html'] = _disp_jackets(env, info[:jackets], tr)
333
+ end # def _get_jackets()
334
+
335
+
336
+ #####################################
337
+ # Display jacket list
338
+ def _disp_jackets(env, jackets, tr)
339
+
340
+ rows = ''
341
+ jackets.each do |jnam, jinfo|
342
+ perms = jinfo[:perms]
343
+ ps = perms.empty? ? '-' : _escape_html(perms.join(', '))
344
+ rows << JacketsRow % [_link_jacket(env, jnam, _escape_html(jnam)),
345
+ _escape_html(jinfo[:title]), ps]
346
+ end
347
+ html = JacketsTable % rows
348
+ html << JacketsForm % env['SCRIPT_NAME'] if tr[:perms].include?('manage')
349
+
350
+ return html
351
+ end # def _disp_jackets()
352
+
353
+
354
+ BinderTable =
355
+ "<table>\n" +
356
+ "<tr><td>Hash ID:</td><td>%s</td></tr>\n" +
357
+ "<tr><td>Text ID:</td><td>%s</td></tr>\n" +
358
+ "<tr><td>Jackets:</td><td>%d</td></tr>\n" +
359
+ "<tr><td>Users:</td><td>%d</td></tr>\n" +
360
+ "<tr><td>Values:</td><td>%d</td></tr>\n" +
361
+ "<tr><td>User Permissions:</td><td>%s</td></tr>\n" +
362
+ "</table>\n"
363
+
364
+ #####################################
365
+ # Get binder info
366
+ def _get_binder(env, path)
367
+ _navbar_binder(env, 'Binder')
368
+ return if !path.empty?
369
+
370
+ tr = _trans(env)
371
+ info = env['sgfa.binder'].binder_info(tr)
372
+
373
+ env['sgfa.status'] = :ok
374
+ env['sgfa.html'] = BinderTable % [
375
+ info[:id_hash], _escape_html(info[:id_text]),
376
+ info[:jackets].size, info[:users].size, info[:values].size,
377
+ _escape_html(tr[:perms].join(', '))
378
+ ]
379
+ end # def _get_binder()
380
+
381
+
382
+ UsersTable =
383
+ "<table class='list'>\n" +
384
+ "<tr><th>User</th><th>Permissions</th></tr>\n" +
385
+ "%s</table>\n"
386
+
387
+ UsersRow =
388
+ "<tr><td>%s</td><td>%s</td></tr>\n"
389
+
390
+ UsersForm =
391
+ "\n<hr>\n<form class='edit' method='post' action='%s' " +
392
+ "enctype='multipart/form-data'>\n" +
393
+ "<fieldset><legend>Set User or Group Permissions</legend>\n" +
394
+ "<label for='user'>Name:</label>" +
395
+ "<input class='user' name='user' type='text'><br>\n" +
396
+ "<label for='perms'>Perms:</label>" +
397
+ "<input class='perms' name='perms' type='text'><br>\n" +
398
+ "</fieldset>\n" +
399
+ "<input type='submit' name='set' value='Set'>\n" +
400
+ "</form>\n"
401
+
402
+ #####################################
403
+ # Get users
404
+ def _get_users(env, path)
405
+ _navbar_binder(env, 'Users')
406
+
407
+ if !path.empty?
408
+ env['sgfa.status'] = :badreq
409
+ env['sgfa.html'] = 'Invalid URL requested'
410
+ return
411
+ end
412
+
413
+ tr = _trans(env)
414
+ info = env['sgfa.binder'].binder_info(tr)
415
+
416
+ env['sgfa.status'] = :ok
417
+ env['sgfa.html'] = _disp_users(env, info[:users], tr)
418
+ end # def _get_users()
419
+
420
+
421
+ #####################################
422
+ # Display users
423
+ def _disp_users(env, users, tr)
424
+ rows = ''
425
+ users.each do |unam, pl|
426
+ perms = pl.empty? ? '-' : pl.join(', ')
427
+ rows << UsersRow % [
428
+ _escape_html(unam), _escape_html(perms)
429
+ ]
430
+ end
431
+ html = UsersTable % rows
432
+ html << (UsersForm % env['SCRIPT_NAME']) if tr[:perms].include?('manage')
433
+ return html
434
+ end # def _disp_users()
435
+
436
+
437
+ ValuesTable =
438
+ "<table class='list'>\n" +
439
+ "<tr><th>Value</th><th>State</th></tr>\n" +
440
+ "%s\n</table>\n"
441
+
442
+ ValuesRow =
443
+ "<tr><td>%s</td><td>%s</td></tr>\n"
444
+
445
+ ValuesForm =
446
+ "\n<hr>\n<form class='edit' method='post' action='%s' " +
447
+ "enctype='multipart/form-data'>\n" +
448
+ "<fieldset><legend>Assign Binder Values</legend>\n" +
449
+ "<label for='value'>Value:</label>" +
450
+ "<input class='value' name='value' type='text'><br>\n" +
451
+ "<label for='state'>State:</label>" +
452
+ "<input class='state' name='state' type='text'><br>\n" +
453
+ "</fieldset>\n" +
454
+ "<input type='submit' name='assign' value='Assign'>\n" +
455
+ "</form>\n"
456
+
457
+ #####################################
458
+ # Get values
459
+ def _get_values(env, path)
460
+ _navbar_binder(env, 'Values')
461
+
462
+ if !path.empty?
463
+ env['sgfa.status'] = :badreq
464
+ env['sgfa.html'] = 'Invalid URL requested'
465
+ return
466
+ end
467
+
468
+ tr = _trans(env)
469
+ info = env['sgfa.binder'].binder_info(tr)
470
+
471
+ env['sgfa.status'] = :ok
472
+ env['sgfa.html'] = _disp_values(env, info[:values], tr)
473
+ end # def _get_values()
474
+
475
+
476
+ #####################################
477
+ # Display values
478
+ def _disp_values(env, values, tr)
479
+ rows = ''
480
+ values.each do |vnam, vset|
481
+ rows << ValuesRow % [
482
+ _escape_html(vnam), _escape_html(vset)
483
+ ]
484
+ end
485
+ html = ValuesTable % rows
486
+ html << (ValuesForm % env['SCRIPT_NAME']) if tr[:perms].include?('manage')
487
+
488
+ return html
489
+ end # def _disp_values()
490
+
491
+
492
+ TagTable =
493
+ "<div class='title'>Tag: %s</div>\n" +
494
+ "<table class='list'>\n<tr>" +
495
+ "<th>Time</th><th>Title</th><th>Files</th><th>Tags</th><th>Edit</th>" +
496
+ "</tr>\n%s</table>\n"
497
+
498
+ TagRow =
499
+ "<tr><td class='time'>%s</td><td class='title'>%s</td>" +
500
+ "<td class='num'>%d</td><td class='num'>%d</td>" +
501
+ "<td class='act'>%s</td></tr>\n"
502
+
503
+ PageSize = 25
504
+ PageSizeMax = 100
505
+
506
+ #####################################
507
+ # Get a tag
508
+ def _get_tag(env, path)
509
+ _navbar_jacket(env, 'Tag')
510
+
511
+ if path.empty?
512
+ tag = '_all'
513
+ else
514
+ tag = _escape_un(path.shift)
515
+ end
516
+ page = path.empty? ? 1 : path.shift.to_i
517
+ page = 1 if page == 0
518
+ rck = Rack::Request.new(env)
519
+ params = rck.GET
520
+ per = params['perpage'] ? params['perpage'].to_i : 0
521
+ if per == 0 || per > PageSizeMax
522
+ per = PageSize
523
+ end
524
+
525
+ tr = _trans(env)
526
+ size, ents = env['sgfa.binder'].read_tag(tr, tag, (page-1)*per, per)
527
+ if ents.size == 0
528
+ html = 'No entries'
529
+ else
530
+ rows = ''
531
+ ents.reverse_each do |enum, rnum, time, title, tcnt, acnt|
532
+ rows << TagRow % [
533
+ time.localtime.strftime("%F %T %z"),
534
+ _link_entry(env, enum, _escape_html(title)),
535
+ acnt, tcnt,
536
+ _link_edit(env, enum, 'edit')
537
+ ]
538
+ end
539
+ html = TagTable % [_escape_html(tag), rows]
540
+ end
541
+
542
+ link = '%s/%s/_tag/%s' % [
543
+ env['SCRIPT_NAME'],
544
+ env['sgfa.jacket.url'],
545
+ _escape(tag)
546
+ ]
547
+ query = (per != PageSize) ? { 'perpage' => per.to_s } : nil
548
+ pages = _link_pages(page, per, size, link, query)
549
+
550
+ env['sgfa.status'] = :ok
551
+ env['sgfa.html'] = html + pages
552
+ end # def _get_tag()
553
+
554
+
555
+ LogTable =
556
+ "<table class='list'>\n<tr>" +
557
+ "<th>History</th><th>Date/Time</th><th>User</th><th>Entries</th>" +
558
+ "<th>Attachs</th></tr>\n%s</table>\n"
559
+
560
+ LogRow =
561
+ "<tr><td class='hnum'>%d</td><td class='time'>%s</td>" +
562
+ "<td class='user'>%s</td><td>%d</td><td>%d</td></tr>\n"
563
+
564
+ #####################################
565
+ # Get the log
566
+ def _get_log(env, path)
567
+ _navbar_jacket(env, 'Log')
568
+
569
+ page = path.empty? ? 1 : path.shift.to_i
570
+ page = 1 if page == 0
571
+ return if !path.empty?
572
+ rck = Rack::Request.new(env)
573
+ params = rck.GET
574
+ per = params['perpage'] ? params['perpage'].to_i : 0
575
+ if per == 0 || per > PageSizeMax
576
+ per = PageSize
577
+ end
578
+
579
+ tr = _trans(env)
580
+ size, hsts = env['sgfa.binder'].read_log(tr, (page-1)*per, per)
581
+ if hsts.size == 0
582
+ env['sgfa.html'] = 'No history'
583
+ env['sgfa.status'] = :notfound
584
+ else
585
+ rows = ''
586
+ hsts.each do |hnum, time, user, ecnt, acnt|
587
+ rows << LogRow % [
588
+ hnum,
589
+ _link_history(env, hnum, time.localtime.strftime('%F %T %z')),
590
+ _escape_html(user),
591
+ ecnt, acnt
592
+ ]
593
+ end
594
+ link = '%s/%s/_log' % [env['SCRIPT_NAME'], env['sgfa.jacket.url']]
595
+ query = (per != PageSize) ? { 'perpage' => per.to_s } : nil
596
+ env['sgfa.status'] = :ok
597
+ env['sgfa.html'] = (LogTable % rows) +
598
+ _link_pages(page, per, size, link, query)
599
+ end
600
+
601
+ end # def _get_log()
602
+
603
+
604
+ #####################################
605
+ # Get an entry
606
+ def _get_entry(env, path)
607
+ _navbar_jacket(env, 'Entry')
608
+
609
+ return if path.empty?
610
+ enum = path.shift.to_i
611
+ rnum = path.empty? ? 0 : path.shift.to_i
612
+ return if enum == 0
613
+
614
+ tr = _trans(env)
615
+ ent = env['sgfa.binder'].read_entry(tr, enum, rnum)
616
+
617
+ env['sgfa.status'] = :ok
618
+ env['sgfa.html'] = _disp_entry(env, ent)
619
+ end # def _get_entry()
620
+
621
+
622
+ EntryDisp =
623
+ "<div class='title'>%s</div>\n" +
624
+ "<div class='body'><pre>%s</pre></div>\n" +
625
+ "<div class='sidebar'>\n" +
626
+ "<div class='time'>%s</div>\n" +
627
+ "<div class='history'>Revision: %d %s %s<br>History: %s %s</div>\n" +
628
+ "<div class='tags'>%s</div>\n" +
629
+ "<div class='attach'>%s</div>\n" +
630
+ "</div>\n" +
631
+ "<div class='hash'>Hash: %s<br>Jacket: %s</div>\n"
632
+
633
+ #####################################
634
+ # Display an entry
635
+ def _disp_entry(env, ent)
636
+
637
+ enum = ent.entry
638
+ rnum = ent.revision
639
+ hnum = ent.history
640
+
641
+ tl = ent.tags
642
+ tags = "Tags:<br>\n"
643
+ if tl.empty?
644
+ tags << "none\n"
645
+ else
646
+ tl.each do |tag|
647
+ tags << _link_tag(env, tag, _escape_html(tag)) + "<br>\n"
648
+ end
649
+ end
650
+
651
+ al = ent.attachments
652
+ att = "Attachments:<br>\n"
653
+ if al.empty?
654
+ att << "none\n"
655
+ else
656
+ al.each do |anum, hnum, name|
657
+ att << _link_attach(env, enum, anum, hnum, name, _escape_html(name)) +
658
+ "<br>\n"
659
+ end
660
+ end
661
+ if rnum == 1
662
+ prev = 'previous'
663
+ else
664
+ prev = _link_revision(env, enum, rnum-1, 'previous')
665
+ end
666
+ curr = _link_entry(env, enum, 'current')
667
+ edit = _link_edit(env, enum, 'edit')
668
+
669
+ body = EntryDisp % [
670
+ _escape_html(ent.title),
671
+ _escape_html(ent.body),
672
+ ent.time.localtime.strftime('%F %T %z'),
673
+ rnum, prev, curr, _link_history(env, hnum, hnum.to_s), edit,
674
+ tags,
675
+ att,
676
+ ent.hash, ent.jacket
677
+ ]
678
+
679
+ return body
680
+ end # def _disp_entry()
681
+
682
+
683
+ HistoryDisp =
684
+ "<div class='title'>History %d</div>\n" +
685
+ "<div class='body'>%s</div>" +
686
+ "<div class='sidebar'>\n" +
687
+ "<div class='time'>%s</div>\n" +
688
+ "<div class='user'>%s</div>\n" +
689
+ "<div class='nav'>%s %s</div>\n" +
690
+ "</div>\n<div class='hash'>Hash: %s<br>Jacket: %s</div>\n"
691
+
692
+ HistoryTable =
693
+ "<table class='list'>\n<tr><th>Item</th><th>Hash</th></tr>\n" +
694
+ "%s</table>\n"
695
+
696
+ HistoryItem =
697
+ "<tr><td>%s</td><td class='hash'>%s</td></tr>\n"
698
+
699
+ #####################################
700
+ # Display a history item
701
+ def _get_history(env, path)
702
+ _navbar_jacket(env, 'History')
703
+
704
+ return if path.empty?
705
+ hnum = path.shift.to_i
706
+ return if hnum == 0
707
+
708
+ tr = _trans(env)
709
+ hst = env['sgfa.binder'].read_history(tr, hnum)
710
+ hnum = hst.history
711
+ plnk = (hnum == 1) ? 'Previous' : _link_history(env, hnum-1, 'Previous')
712
+ nlnk = _link_history(env, hnum+1, 'Next')
713
+
714
+ rows = ""
715
+ hst.entries.each do |enum, rnum, hash|
716
+ disp = "Entry %d-%d" % [enum, rnum]
717
+ rows << (HistoryItem % [_link_revision(env, enum, rnum, disp), hash])
718
+ end
719
+ hst.attachments.each do |enum, anum, hash|
720
+ disp = "Attach %d-%d-%d" % [enum, anum, hnum]
721
+ rows << HistoryItem %
722
+ [_link_attach(env, enum, anum, hnum, hash + '.bin', disp), hash]
723
+ end
724
+ tab = HistoryTable % rows
725
+
726
+ body = HistoryDisp % [
727
+ hnum, tab, hst.time.localtime.strftime('%F %T %z'),
728
+ _escape_html(hst.user), plnk, nlnk, hst.hash, hst.jacket
729
+ ]
730
+
731
+ env['sgfa.status'] = :ok
732
+ env['sgfa.html'] = body
733
+ end # def _get_history()
734
+
735
+
736
+ ListTable =
737
+ "<table class='list'>\n<tr>" +
738
+ "<th>Tag</th><th>Number</th></tr>\n" +
739
+ "%s\n</table>\n"
740
+
741
+ ListPrefix =
742
+ "<tr><td class='prefix'>%s: prefix</td><td>%d tags</td></tr>\n"
743
+
744
+ ListTag =
745
+ "<tr><td class='tag'>%s</td><td>%d entries</td></tr>\n"
746
+
747
+ #####################################
748
+ # Get list of tags
749
+ def _get_list(env, path)
750
+ _navbar_jacket(env, 'List')
751
+
752
+ bnd = env['sgfa.binder']
753
+ tr = _trans(env)
754
+ lst = bnd.read_list(tr)
755
+
756
+ # sort into prefixed & regular
757
+ prefix = {}
758
+ regular = []
759
+ lst.each do |tag|
760
+ idx = tag.index(':')
761
+ if !idx
762
+ regular.push tag
763
+ next
764
+ end
765
+ pre = tag[0,idx].strip
766
+ if prefix[pre]
767
+ prefix[pre].push tag
768
+ else
769
+ prefix[pre] = [tag]
770
+ end
771
+ end
772
+
773
+ # regular & prefix list
774
+ rows = ''
775
+ if path.empty?
776
+ prefix.keys.sort.each do |pre|
777
+ size = prefix[pre].size
778
+ rows << ListPrefix %
779
+ [_link_prefix(env, pre, _escape_html(pre)), size]
780
+ end
781
+ regular.sort.each do |tag|
782
+ size, ents = bnd.read_tag(tr, tag, 0, 0)
783
+ rows << ListTag %
784
+ [_link_tag(env, tag, _escape_html(tag)), size]
785
+ end
786
+
787
+ # list entire prefix
788
+ else
789
+ pre = _escape_un(path.shift)
790
+ return if !path.empty?
791
+ if !prefix[pre]
792
+ env['sgfa.status'] = :notfound
793
+ env['sgfa.html'] = 'Tag prefix not found'
794
+ return
795
+ end
796
+
797
+ prefix[pre].sort.each do |tag|
798
+ size, ents = bnd.read_tag(tr, tag, 0, 0)
799
+ rows << ListTag %
800
+ [_link_tag(env, tag, _escape_html(tag)), size]
801
+ end
802
+ end
803
+
804
+ env['sgfa.status'] = :ok
805
+ env['sgfa.html'] = ListTable % rows
806
+ end # def _get_list
807
+
808
+
809
+ InfoTable =
810
+ "<table>\n" +
811
+ "<tr><td>Text ID:</td><td>%s</td></tr>\n" +
812
+ "<tr><td>Hash ID:</td><td>%s</td></tr>\n" +
813
+ "<tr><td>Last Edit:</td><td>%s</td></tr>\n" +
814
+ "<tr><td>History:</td><td>%d</td></tr>\n" +
815
+ "<tr><td>Entries:</td><td>%d</td></tr>\n" +
816
+ "</table>\n"
817
+
818
+ #####################################
819
+ # Get jacket info
820
+ def _get_info(env, path)
821
+ _navbar_jacket(env, 'Jacket')
822
+ return if !path.empty?
823
+ tr = _trans(env)
824
+
825
+ info = env['sgfa.binder'].binder_info(tr)
826
+ hst = env['sgfa.binder'].read_history(tr, 0)
827
+ if hst
828
+ hmax = hst.history
829
+ emax = hst.entry_max
830
+ time = hst.time.localtime.strftime('%F %T %z')
831
+ else
832
+ hmax = 0
833
+ emax = 0
834
+ time = 'none'
835
+ end
836
+
837
+ jinf = info[:jackets][env['sgfa.jacket.name']]
838
+ env['sgfa.status'] = :ok
839
+ env['sgfa.html'] = InfoTable % [
840
+ jinf[:id_text], jinf[:id_hash],
841
+ time, hmax, emax
842
+ ]
843
+ end # def _get_info()
844
+
845
+
846
+ #####################################
847
+ # Get an attachment
848
+ def _get_attach(env, path)
849
+ _navbar_jacket(env, 'Attachment')
850
+
851
+ spec = path.shift
852
+ return if !spec
853
+ ma = /^(\d+)-(\d+)-(\d+)$/.match(spec)
854
+ return if !ma
855
+ name = path.shift
856
+ return if !name
857
+ return if !path.empty?
858
+ enum, anum, hnum = ma[1,3].map{|st| st.to_i}
859
+ name = _escape_un(name)
860
+
861
+ ext = name.rpartition('.')[2]
862
+ if ext.empty?
863
+ mime = 'application/octet-stream'
864
+ else
865
+ mime = Rack::Mime.mime_type('.' + ext)
866
+ end
867
+
868
+ tr = _trans(env)
869
+ file = env['sgfa.binder'].read_attach(tr, enum, anum, hnum)
870
+
871
+ env['sgfa.status'] = :ok
872
+ env['sgfa.headers'] = {
873
+ 'Content-Length' => file.size.to_s,
874
+ 'Content-Type' => mime,
875
+ 'Content-Disposition' => 'attachment',
876
+ }
877
+ env['sgfa.file'] = FileBody.new(file)
878
+
879
+ end # def _get_attach()
880
+
881
+
882
+ EditForm =
883
+ "<form class='edit' method='post' action='%s/%s' " +
884
+ "enctype='multipart/form-data'>\n" +
885
+ "<input name='entry' type='hidden' value='%d'>\n" +
886
+ "<input name='revision' type='hidden' value='%d'>\n" +
887
+
888
+ "<div class='edit'>\n" +
889
+
890
+ "<fieldset><legend>Basic Info</legend>\n" +
891
+
892
+ "<label for='title'>Title:</label>" +
893
+ "<input class='title' name='title' type='text' value='%s'><br>\n" +
894
+
895
+ "<label for='time'>Time:</label>" +
896
+ "<input name='time' type='text' value='%s'><br>\n" +
897
+
898
+ "<label for='body'>Body:</label>" +
899
+ "<textarea class='body' name='body'>%s</textarea>\n" +
900
+
901
+ "</fieldset>\n" +
902
+ "<fieldset><legend>Attachments</legend>\n%s</fieldset>\n" +
903
+ "<fieldset><legend>Tags</legend>\n%s</fieldset>\n" +
904
+
905
+ "<input type='submit' name='save' value='Save Changes'>\n" +
906
+ "</div></form>\n"
907
+
908
+ EditFilePre =
909
+ "<table class='edit_file'>\n" +
910
+ "<tr><th>Name</th><th>Upload/Replace</th></tr>\n"
911
+
912
+ EditFileEach =
913
+ "<tr><td><input name='attname%d' type='text' value='%s'>" +
914
+ "<input name='attnumb%d' type='hidden' value='%d'></td>" +
915
+ "<td><input name='attfile%d' type='file'></td></tr>\n"
916
+
917
+ EditFileCnt =
918
+ "</table>\n<input name='attcnt' type='hidden' value='%d'>\n"
919
+
920
+ EditTagOld =
921
+ "<input name='tag%d' type='text' value='%s'><br>\n"
922
+
923
+ EditTagNew =
924
+ "<input name='tag%d' type='text'><br>\n"
925
+
926
+ EditTagSel =
927
+ "%s: <select name='tag%d'>" +
928
+ "<option value='' selected></option>%s</select><br>\n"
929
+
930
+ EditTagOpt =
931
+ "<option value='%s: %s'>%s</option>"
932
+
933
+ EditTagCnt =
934
+ "<input name='tagcnt' type='hidden' value='%d'>\n"
935
+
936
+ #####################################
937
+ # Get edit form
938
+ def _get_edit(env, path)
939
+ _navbar_jacket(env, 'Edit')
940
+
941
+ tr = _trans(env)
942
+
943
+ if path.empty?
944
+ enum = 0
945
+ rnum = 0
946
+ ent = Entry.new
947
+ ent.title = 'Title'
948
+ ent.body = 'Body'
949
+ ent.time = Time.now
950
+ else
951
+ enum = path.shift.to_i
952
+ return if enum == 0 || !path.empty?
953
+ ent = env['sgfa.binder'].read_entry(tr, enum)
954
+ rnum = ent.revision
955
+ end
956
+
957
+ lst = env['sgfa.binder'].read_list(tr)
958
+ prefix = {}
959
+ lst.each do |tag|
960
+ idx = tag.index(':')
961
+ next unless idx
962
+ pre = tag[0,idx].strip
963
+ post = tag[idx+1..-1].strip
964
+ if prefix[pre]
965
+ prefix[pre].push post
966
+ else
967
+ prefix[pre] = [post]
968
+ end
969
+ end
970
+
971
+ tags = ''
972
+ cnt = 0
973
+ ent.tags.each do |tag|
974
+ tags << EditTagOld % [cnt, _escape_html(tag)]
975
+ cnt += 1
976
+ end
977
+ prefix.each do |pre, lst|
978
+ px = _escape_html(pre)
979
+ opts = ''
980
+ lst.each do |post|
981
+ ex = _escape_html(post)
982
+ opts << EditTagOpt % [px, ex, ex]
983
+ end
984
+ tags << EditTagSel % [px, cnt, opts]
985
+ cnt += 1
986
+ end
987
+ 5.times do |tg|
988
+ tags << EditTagNew % cnt
989
+ cnt += 1
990
+ end
991
+ tags << EditTagCnt % cnt
992
+
993
+ atts = "Attachments go here\n"
994
+ atts = EditFilePre.dup
995
+ cnt = 0
996
+ ent.attachments.each do |anum, hnum, name|
997
+ atts << EditFileEach % [cnt, _escape_html(name), cnt, anum, cnt]
998
+ cnt += 1
999
+ end
1000
+ 5.times do |ix|
1001
+ atts << EditFileEach % [cnt, '', cnt, 0, cnt]
1002
+ cnt += 1
1003
+ end
1004
+ atts << EditFileCnt % cnt
1005
+
1006
+ html = EditForm % [
1007
+ env['SCRIPT_NAME'], env['sgfa.jacket.url'], enum, rnum,
1008
+ _escape_html(ent.title), ent.time.localtime.strftime('%F %T %z'),
1009
+ _escape_html(ent.body), atts, tags,
1010
+ ]
1011
+
1012
+ env['sgfa.status'] = :ok
1013
+ env['sgfa.html'] = html
1014
+ end # def _get_edit
1015
+
1016
+ JacketPost = [
1017
+ 'entry',
1018
+ 'revision',
1019
+ 'time',
1020
+ 'tagcnt',
1021
+ 'attcnt',
1022
+ ]
1023
+
1024
+ #####################################
1025
+ # Handle jacket post
1026
+ def _post_jacket(env)
1027
+ _navbar_jacket(env, 'Edit')
1028
+
1029
+ rck = Rack::Request.new(env)
1030
+ params = rck.POST
1031
+
1032
+ # validate fields present
1033
+ JacketPost.each do |fn|
1034
+ next if params[fn]
1035
+ raise Error::Limits, 'Bad form submission'
1036
+ end
1037
+ tagcnt = params['tagcnt'].to_i
1038
+ attcnt = params['attcnt'].to_i
1039
+ tagcnt.times do |ix|
1040
+ next if params['tag%d' % ix]
1041
+ raise Error::Limits, 'Bad form submission'
1042
+ end
1043
+ attcnt.times do |ix|
1044
+ next if params['attnumb%d' % ix]
1045
+ raise Error::Limits, 'Bad form submission'
1046
+ end
1047
+
1048
+ # get the entry being edited
1049
+ enum = params['entry'].to_i
1050
+ rnum = params['revision'].to_i
1051
+ tr = _trans(env)
1052
+ if enum != 0
1053
+ ent = env['sgfa.binder'].read_entry(tr, enum, rnum)
1054
+ else
1055
+ ent = Entry.new
1056
+ end
1057
+
1058
+ # tags
1059
+ oldt = ent.tags
1060
+ newt = []
1061
+ tagcnt.times do |ix|
1062
+ tx = 'tag%d' % ix
1063
+ if !params[tx].empty?
1064
+ newt.push params[tx]
1065
+ end
1066
+ end
1067
+
1068
+ # attachments
1069
+ attcnt.times do |ix|
1070
+ anum = params['attnumb%d' % ix].to_i
1071
+ name = params['attname%d' % ix]
1072
+ file = params['attfile%d' % ix]
1073
+
1074
+ # copy uploaded file
1075
+ if file && file != ''
1076
+ ftmp = env['sgfa.binder'].temp
1077
+ IO::copy_stream(file[:tempfile], ftmp)
1078
+ file[:tempfile].close!
1079
+ else
1080
+ ftmp = nil
1081
+ end
1082
+
1083
+ # new file
1084
+ if anum == 0
1085
+ next if !ftmp
1086
+ name = file[:filename] if name == ''
1087
+ ent.attach(name, ftmp)
1088
+
1089
+ # old file
1090
+ else
1091
+ ent.rename(anum, name) if name != ''
1092
+ ent.replace(anum, ftmp) if ftmp
1093
+ end
1094
+
1095
+ end
1096
+
1097
+ # general
1098
+ ent.title = params['title']
1099
+ ent.body = params['body']
1100
+ begin
1101
+ time = Time.parse(params['time'])
1102
+ ent.time = time
1103
+ rescue ArgumentError
1104
+ end
1105
+ oldt.each{|tag| ent.untag(tag) if !newt.include?(tag) }
1106
+ newt.each{|tag| ent.tag(tag) if !oldt.include?(tag) }
1107
+ env['sgfa.binder'].write(tr, [ent])
1108
+
1109
+ env['sgfa.status'] = :ok
1110
+ env['sgfa.message'] = 'Entry edited.'
1111
+ env['sgfa.html'] = _disp_entry(env, ent)
1112
+
1113
+ end # def _post_jacket()
1114
+
1115
+
1116
+ #####################################
1117
+ # Handle binder post
1118
+ def _post_binder(env)
1119
+ _navbar_binder(env, 'Edit')
1120
+
1121
+ rck = Rack::Request.new(env)
1122
+ params = rck.POST
1123
+
1124
+ tr = _trans(env)
1125
+ tr[:title] = 'Test title'
1126
+ tr[:body] = 'Test description of action'
1127
+
1128
+ bnd = env['sgfa.binder']
1129
+
1130
+ # jacket
1131
+ if params['create']
1132
+ _navbar_binder(env, 'Jackets')
1133
+ ['jacket', 'newname', 'title', 'perms'].each do |fn|
1134
+ next if params[fn]
1135
+ raise Error::Limits, 'Bad form submission'
1136
+ end
1137
+ perms = params['perms'].split(',').map{|it| it.strip }
1138
+ title = params['title']
1139
+ newname = params['newname']
1140
+ jacket = params['jacket']
1141
+ tr[:jacket] = jacket
1142
+
1143
+ info = bnd.binder_info(tr)
1144
+ oj = info[:jackets][jacket]
1145
+ if oj
1146
+ newname ||= jacket
1147
+ title = oj['title'] if title.empty?
1148
+ jck = bnd.jacket_edit(tr, newname, title, perms)
1149
+ env['sgfa.message'] = 'Jacket edited.'
1150
+ else
1151
+ jck = bnd.jacket_create(tr, title, perms)
1152
+ env['sgfa.message'] = 'Jacket created.'
1153
+ end
1154
+ env['sgfa.status'] = :ok
1155
+ env['sgfa.html'] = _disp_jackets(env, jck, tr)
1156
+
1157
+ # user
1158
+ elsif params['set']
1159
+ _navbar_binder(env, 'Users')
1160
+ ['user', 'perms'].each do |fn|
1161
+ next if params[fn]
1162
+ raise Error::Limits, 'Bad form submission'
1163
+ end
1164
+ perms = params['perms'].split(',').map{|it| it.strip }
1165
+ users = bnd.binder_user(tr, params['user'], perms)
1166
+ env['sgfa.status'] = :ok
1167
+ env['sgfa.message'] = 'User edited.'
1168
+ env['sgfa.html'] = _disp_users(env, users, tr)
1169
+
1170
+ # binder
1171
+ elsif params['assign']
1172
+ _navbar_binder(env, 'Values')
1173
+ ['value', 'state'].each do |fn|
1174
+ next if params[fn]
1175
+ raise Error::Limits, 'Bad form submission'
1176
+ end
1177
+ vals = { params['value'] => params['state'] }
1178
+ values = bnd.binder_values(tr, vals)
1179
+ env['sgfa.status'] = :ok
1180
+ env['sgfa.message'] = 'Values assigned.'
1181
+ env['sgfa.html'] = _disp_values(env, values, tr)
1182
+
1183
+ end
1184
+
1185
+ end # def _post_binder()
1186
+
1187
+ end # class Binder
1188
+
1189
+ end # module Web
1190
+ end # module Sgfa