icfs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,74 @@
1
+ #
2
+ # Investigative Case File System
3
+ #
4
+ # Copyright 2019 by Graham A. Field
5
+ #
6
+ # See LICENSE.txt for licensing information.
7
+ #
8
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
9
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
+
11
+ require 'rack'
12
+
13
+ module ICFS
14
+ module Demo
15
+
16
+ ##########################################################################
17
+ # Test authentication - Rack middleware
18
+ #
19
+ # @deprecated This is a horrible security implementation, DO NOT USE
20
+ # for anything other than testing.
21
+ #
22
+ class Auth
23
+
24
+
25
+ ###############################################
26
+ # New instance
27
+ #
28
+ # @param app [Object] The rack app
29
+ # @param api [Object] the ICFS API
30
+ #
31
+ def initialize(app, api)
32
+ @app = app
33
+ @api = api
34
+ end
35
+
36
+
37
+ ###############################################
38
+ # Handle the calls
39
+ #
40
+ def call(env)
41
+
42
+ # login
43
+ if env['PATH_INFO'] == '/login'.freeze
44
+ user = env['QUERY_STRING']
45
+ body = 'User set'
46
+
47
+ # set the cookie
48
+ rsp = Rack::Response.new( body, 200, {} )
49
+ rsp.set_cookie( 'icfs-user', {
50
+ value: user,
51
+ expires: Time.now + 24*60*60
52
+ })
53
+ return rsp.finish
54
+ end
55
+
56
+ # pull the username from the cookie
57
+ cookies = Rack::Request.new(env).cookies
58
+ user = cookies['icfs-user']
59
+ if !user
60
+ return [400, {'Content-Type' => 'text/plain'}, ['Login first'.freeze]]
61
+ end
62
+ @api.user = user
63
+ env['icfs'] = @api
64
+ return @app.call(env)
65
+
66
+ rescue ICFS::Error::NotFound, ICFS::Error::Value => err
67
+ return [400, {'Content-Type' => 'text/plain'}, [err.message]]
68
+ end # def call()
69
+
70
+ end # class ICFS::Demo::Auth
71
+
72
+ end # module ICFS::Demo
73
+
74
+ end # module ICFS
@@ -0,0 +1,59 @@
1
+ #
2
+ # Investigative Case File System
3
+ #
4
+ # Copyright 2019 by Graham A. Field
5
+ #
6
+ # See LICENSE.txt for licensing information.
7
+ #
8
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
9
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
+
11
+ #
12
+ module ICFS
13
+
14
+
15
+ ##########################################################################
16
+ # Demonstration only files
17
+ module Demo
18
+
19
+ ##########################################################################
20
+ # Serve static files - Rack middleware
21
+ #
22
+ # @deprecated This is a horrible implementation, DO NOT USE
23
+ # for anything other than testing.
24
+ #
25
+ class Static
26
+
27
+ ###############################################
28
+ # New instance
29
+ #
30
+ # @param stat [Hash] maps path to Hash with :path and :mime
31
+ # @param app [Object] the next rack app
32
+ #
33
+ def initialize(app, stat)
34
+ @stat = stat
35
+ @app = app
36
+ end
37
+
38
+ # Process requests
39
+ def call(env)
40
+
41
+ # see if we have a static file to serve
42
+ st = @stat[env['PATH_INFO']]
43
+ if st
44
+ cont = File.read(st['path'])
45
+ head = {
46
+ 'Content-Type' => st['mime'],
47
+ 'Content-Length' => cont.bytesize.to_s
48
+ }
49
+ return [200, head, [cont]]
50
+ end
51
+
52
+ return @app.call(env)
53
+
54
+ end
55
+
56
+ end # class ICFS::Demo::Static
57
+
58
+ end # module ICFS::Demo
59
+ end # module ICFS
@@ -0,0 +1,38 @@
1
+ #
2
+ # Investigative Case File System
3
+ #
4
+ # Copyright 2019 by Graham A. Field
5
+ #
6
+ # See LICENSE.txt for licensing information.
7
+ #
8
+ # This program is distributed WITHOUT ANY WARRANTY; without even the
9
+ # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10
+
11
+ #
12
+ module ICFS
13
+ module Demo
14
+
15
+ ##########################################################################
16
+ # Set timezone - Rack middleware
17
+ #
18
+ # @deprecated This does nothing but set a static timezone. Do not use for
19
+ # anything other than testing.
20
+ #
21
+ class Timezone
22
+
23
+ # New instance
24
+ def initialize(app, tz)
25
+ @app = app
26
+ @tz = tz.freeze
27
+ end
28
+
29
+ # process requests
30
+ def call(env)
31
+ env['icfs.tz'] = @tz
32
+ @app.call(env)
33
+ end
34
+
35
+ end # class ICFS::Demo::Timezone
36
+
37
+ end # module ICFS::Demo
38
+ end # module ICFS
@@ -0,0 +1,83 @@
1
+ #
2
+ # Investigative Case File System
3
+ #
4
+ # Copyright 2019 by Graham A. Field
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License version 3.
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
+ #
13
+ module ICFS
14
+
15
+ ##########################################################################
16
+ # Shared Elasticsearch methods
17
+ #
18
+ module Elastic
19
+
20
+ private
21
+
22
+ ###############################################
23
+ # Read an item
24
+ #
25
+ # @param ix [Symbol] the index to read
26
+ # @param id [String] the id to read
27
+ #
28
+ # @return [String] JSON encoded object
29
+ #
30
+ def _read(ix, id)
31
+ url = '%s/_doc/%s/_source'.freeze % [ @map[ix], CGI.escape(id)]
32
+ resp = @es.run_request(:get, url, ''.freeze, {})
33
+ if resp.status == 404
34
+ return nil
35
+ elsif !resp.success?
36
+ raise('Elasticsearch read failed'.freeze)
37
+ end
38
+ return resp.body
39
+ end # def _read()
40
+
41
+
42
+ ###############################################
43
+ # Write an item
44
+ #
45
+ # @param ix [Symbol] the index to write
46
+ # @param id [String] the id to write
47
+ # @param item [String] JSON encoded object to write
48
+ #
49
+ def _write(ix, id, item)
50
+ url = '%s/_doc/%s'.freeze % [ @map[ix], CGI.escape(id)]
51
+ head = {'Content-Type'.freeze => 'application/json'.freeze}.freeze
52
+ resp = @es.run_request(:put, url, item, head)
53
+ if !resp.success?
54
+ raise('Elasticsearch index failed'.freeze)
55
+ end
56
+ end # def _write()
57
+
58
+
59
+ public
60
+
61
+
62
+ ###############################################
63
+ # Create ES indices
64
+ # @param maps [Hash] symbol to Elasticsearch mapping
65
+ #
66
+ def create(maps)
67
+ head = {'Content-Type'.freeze => 'application/json'.freeze}.freeze
68
+ maps.each do |ix, map|
69
+ url = @map[ix]
70
+ resp = @es.run_request(:put, url, map, head)
71
+ if !resp.success?
72
+ puts 'URL: %s' % url
73
+ puts map
74
+ puts resp.body
75
+ raise('Elasticsearch index create failed: %s'.freeze % ix.to_s)
76
+ end
77
+ end
78
+ end # def create()
79
+
80
+
81
+ end # module ICFS::Elastic
82
+
83
+ end # module ICFS
data/lib/icfs/items.rb ADDED
@@ -0,0 +1,653 @@
1
+ #
2
+ # Investigative Case File System
3
+ #
4
+ # Copyright 2019 by Graham A. Field
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License version 3.
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
+ #
13
+ module ICFS
14
+
15
+
16
+ ##########################################################################
17
+ # Items
18
+ #
19
+ module Items
20
+
21
+ ##############################################################
22
+ # Base fields
23
+ ##############################################################
24
+
25
+ # ICFS version
26
+ FieldIcfs = {
27
+ method: :integer,
28
+ min: 1,
29
+ max: 1,
30
+ }.freeze
31
+
32
+
33
+ # Caseid
34
+ FieldCaseid = {
35
+ method: :string,
36
+ min: 1,
37
+ max: 32,
38
+ invalid: /[[:cntrl:][:space:]"\#$%'()*+\/;<=>?@\[\]\\^`{|}~]/.freeze,
39
+ }.freeze
40
+
41
+
42
+ # Title
43
+ FieldTitle = {
44
+ method: :string,
45
+ min: 1,
46
+ max: 128,
47
+ invalid: /[[:cntrl:]]/.freeze,
48
+ }.freeze
49
+
50
+
51
+ # Tag
52
+ # No control characters
53
+ # may not start with brackets or whitespace
54
+ FieldTag = {
55
+ method: :string,
56
+ min: 1,
57
+ max: 32,
58
+ invalid: /[[:cntrl:]]|^[\[\{ ]/.freeze,
59
+ }.freeze
60
+
61
+
62
+ # Tag for Entry
63
+ FieldTagEntry = {
64
+ method: :string,
65
+ min: 1,
66
+ max: 32,
67
+ allowed: Set[
68
+ ICFS::TagAction,
69
+ ICFS::TagIndex,
70
+ ICFS::TagCase,
71
+ ].freeze,
72
+ invalid: /[[:cntrl:]]|^[\[\{ ]/.freeze,
73
+ }.freeze
74
+
75
+
76
+ # Special tags
77
+ FieldTagSpecial = {
78
+ method: :string,
79
+ allowed: Set[
80
+ ICFS::TagNone,
81
+ ICFS::TagAction,
82
+ ICFS::TagIndex,
83
+ ICFS::TagCase,
84
+ ].freeze,
85
+ whitelist: true,
86
+ }.freeze
87
+
88
+
89
+ # Any tag, including empty
90
+ FieldTagAny = {
91
+ method: :any,
92
+ check: [
93
+ FieldTag,
94
+ FieldTagSpecial
95
+ ].freeze,
96
+ }.freeze
97
+
98
+
99
+ # a normal perm name
100
+ # No control characters
101
+ # may not start with square brackets, curly brackets, or whitespace
102
+ FieldPermNormal = {
103
+ method: :string,
104
+ min: 1,
105
+ max: 64,
106
+ invalid: /[[:cntrl:]]|^[\[\{ ]/.freeze,
107
+ }.freeze
108
+
109
+
110
+ # A reserved case name
111
+ # square brackets
112
+ FieldPermReserve = {
113
+ method: :string,
114
+ allowed: Set[
115
+ ICFS::PermRead,
116
+ ICFS::PermWrite,
117
+ ICFS::PermManage,
118
+ ICFS::PermAction
119
+ ].freeze,
120
+ whitelist: true
121
+ }.freeze
122
+
123
+
124
+ # A global permission
125
+ # curly brackets, no control characters
126
+ FieldPermGlobal = {
127
+ method: :string,
128
+ min: 1,
129
+ max: 64,
130
+ valid: /^\{[^[:cntrl:]]+\}$/.freeze,
131
+ whitelist: true,
132
+ }.freeze
133
+
134
+
135
+ # A case perm
136
+ FieldPermCase = {
137
+ method: :any,
138
+ check: [
139
+ FieldPermNormal,
140
+ FieldPermReserve,
141
+ ].freeze
142
+ }.freeze
143
+
144
+
145
+ # Any perm
146
+ FieldPermAny = {
147
+ method: :any,
148
+ check: [
149
+ FieldPermNormal,
150
+ FieldPermReserve,
151
+ FieldPermGlobal,
152
+ ].freeze
153
+ }.freeze
154
+
155
+
156
+ # a user/group name
157
+ # No control characters
158
+ # no space, no punctuation except , - : _
159
+ FieldUsergrp = {
160
+ method: :string,
161
+ min: 3,
162
+ max: 32,
163
+ invalid: /[\x00-\x2b\x2e\x2f\x3b-\x40\x5b-\x5e\x60\x7b-\x7f]/.freeze,
164
+ }.freeze
165
+
166
+
167
+ # a hash
168
+ FieldHash = {
169
+ method: :string,
170
+ min: 64,
171
+ max: 64,
172
+ invalid: /[^0-9a-f]/.freeze
173
+ }.freeze
174
+
175
+
176
+ # Content
177
+ FieldContent = {
178
+ method: :string,
179
+ min: 1,
180
+ max: 8*1024,
181
+ invalid: /[^[:graph:][:space:]]/.freeze,
182
+ }.freeze
183
+
184
+
185
+ # a stat name
186
+ FieldStat = {
187
+ method: :string,
188
+ min: 1,
189
+ max: 32,
190
+ invalid: /[[:cntrl:]\t\r\n\v\f]|^_/.freeze,
191
+ }.freeze
192
+
193
+
194
+ # a filename
195
+ FieldFilename = {
196
+ method: :string,
197
+ min: 1,
198
+ max: 128,
199
+ invalid: /[[:cntrl:]\\\/]|^\./.freeze
200
+ }.freeze
201
+
202
+
203
+ ##############################################################
204
+ # Sub-item parts
205
+ ##############################################################
206
+
207
+ # Empty Tags
208
+ #
209
+ SubTagsEmpty = {
210
+ method: :array,
211
+ min: 1,
212
+ max: 1,
213
+ check: {
214
+ method: :equals,
215
+ check: ICFS::TagNone
216
+ }.freeze
217
+ }.freeze
218
+
219
+
220
+ # Entry Tags
221
+ #
222
+ SubTagsEntry = {
223
+ method: :array,
224
+ min: 1,
225
+ check: FieldTagEntry
226
+ }.freeze
227
+
228
+
229
+ # Tags
230
+ SubTagsNormal = {
231
+ method: :array,
232
+ min: 1,
233
+ check: FieldTag
234
+ }.freeze
235
+
236
+
237
+ # Tags
238
+ SubTags = {
239
+ method: :any,
240
+ check: [ SubTagsEmpty, SubTagsNormal ].freeze
241
+ }.freeze
242
+
243
+
244
+ # Grant
245
+ SubGrant = {
246
+ method: :hash,
247
+ required: {
248
+ 'perm' => FieldPermCase,
249
+ 'grant' => {
250
+ method: :array,
251
+ min: 1,
252
+ check: FieldUsergrp
253
+ }.freeze
254
+ }.freeze
255
+ }.freeze
256
+
257
+
258
+ # Access
259
+ SubAccess = {
260
+ method: :array,
261
+ min: 1,
262
+ check: SubGrant
263
+ }.freeze
264
+
265
+
266
+ # Case stats
267
+ SubCaseStats = {
268
+ method: :array,
269
+ min:1,
270
+ check: FieldStat,
271
+ }.freeze
272
+
273
+
274
+ # An item in a log
275
+ SubLogItem = {
276
+ method: :hash,
277
+ required: {
278
+ 'num' => Validate::IsIntPos,
279
+ 'hash' => FieldHash,
280
+ }.freeze
281
+ }.freeze
282
+
283
+
284
+ # Indexes
285
+ SubIndexes = {
286
+ method: :array,
287
+ min: 1,
288
+ check: Validate::IsIntPos,
289
+ }.freeze
290
+
291
+
292
+ # Perms
293
+ SubPerms = {
294
+ method: :array,
295
+ min: 1,
296
+ check: FieldPermAny
297
+ }.freeze
298
+
299
+
300
+ # Stats
301
+ SubStats = {
302
+ method: :array,
303
+ min: 1,
304
+ check: {
305
+ method: :hash,
306
+ required: {
307
+ "name" => FieldStat,
308
+ "value" => Validate::IsFloat,
309
+ "credit" => {
310
+ method: :array,
311
+ min: 1,
312
+ max: 32,
313
+ check: FieldUsergrp
314
+ }.freeze
315
+ }.freeze
316
+ }.freeze
317
+ }.freeze
318
+
319
+
320
+ # An old file
321
+ SubFileOld = {
322
+ method: :hash,
323
+ required: {
324
+ 'log' => Validate::IsIntPos,
325
+ 'num' => Validate::IsIntUns,
326
+ 'name' => FieldFilename,
327
+ }.freeze
328
+ }.freeze
329
+
330
+
331
+ # Case task
332
+ SubTaskCase = {
333
+ method: :hash,
334
+ required: {
335
+ 'assigned' => {
336
+ method: :string,
337
+ allowed: Set[ ICFS::UserCase ].freeze,
338
+ whitelist: true
339
+ }.freeze,
340
+ 'title' => FieldTitle,
341
+ 'status' => Validate::IsBoolean,
342
+ 'flag' => Validate::IsBoolean,
343
+ 'time' => Validate::IsIntPos,
344
+ 'tags' => SubTags
345
+ }.freeze
346
+ }.freeze
347
+
348
+
349
+ # Normal task
350
+ SubTaskNormal = {
351
+ method: :hash,
352
+ required: {
353
+ 'assigned' => FieldUsergrp,
354
+ 'title' => FieldTitle,
355
+ 'status' => Validate::IsBoolean,
356
+ 'flag' => Validate::IsBoolean,
357
+ 'time' => Validate::IsIntPos,
358
+ 'tags' => SubTags
359
+ }.freeze
360
+ }.freeze
361
+
362
+
363
+ # Tasks
364
+ SubTasks = {
365
+ method: :array,
366
+ min: 1,
367
+ 0 => SubTaskCase,
368
+ check: SubTaskNormal
369
+ }.freeze
370
+
371
+
372
+ # Case task
373
+ SubTaskEditCase = {
374
+ method: :hash,
375
+ required: {
376
+ 'assigned' => {
377
+ method: :string,
378
+ allowed: Set[ ICFS::UserCase ].freeze,
379
+ whitelist: true
380
+ }.freeze,
381
+ 'title' => FieldTitle,
382
+ 'status' => Validate::IsBoolean,
383
+ 'flag' => Validate::IsBoolean,
384
+ 'time' => Validate::IsIntPos,
385
+ }.freeze,
386
+ optional: {
387
+ 'tags' => SubTags
388
+ }.freeze
389
+ }.freeze
390
+
391
+
392
+ # Normal task
393
+ SubTaskEditNormal = {
394
+ method: :hash,
395
+ required: {
396
+ 'assigned' => FieldUsergrp,
397
+ 'title' => FieldTitle,
398
+ 'status' => Validate::IsBoolean,
399
+ 'flag' => Validate::IsBoolean,
400
+ 'time' => Validate::IsIntPos,
401
+ }.freeze,
402
+ optional: {
403
+ 'tags' => SubTags
404
+ }.freeze
405
+ }.freeze
406
+
407
+
408
+ # TasksEdit
409
+ SubTasksEdit = {
410
+ method: :array,
411
+ min: 1,
412
+ 0 => SubTaskEditCase,
413
+ check: SubTaskEditNormal,
414
+ }.freeze
415
+
416
+
417
+ # A new file
418
+ SubFileNew = {
419
+ method: :hash,
420
+ required: {
421
+ 'temp' => Validate::IsTempfile,
422
+ 'name' => FieldFilename,
423
+ }.freeze
424
+ }.freeze
425
+
426
+
427
+ ##############################################################
428
+ # Check Items
429
+ ##############################################################
430
+
431
+
432
+ # Case - Edit
433
+ ItemCaseEdit = {
434
+ method: :hash,
435
+ required: {
436
+ 'template' => Validate::IsBoolean,
437
+ 'status' => Validate::IsBoolean,
438
+ 'title' => FieldTitle,
439
+ 'access' => SubAccess
440
+ }.freeze,
441
+ optional: {
442
+ 'tags' => SubTags,
443
+ 'stats' => SubCaseStats,
444
+ }.freeze
445
+ }.freeze
446
+
447
+
448
+ # Entry - New only
449
+ ItemEntryNew = {
450
+ method: :hash,
451
+ required: {
452
+ 'caseid' => FieldCaseid,
453
+ 'title' => FieldTitle,
454
+ 'content' => FieldContent,
455
+ }.freeze,
456
+ optional: {
457
+ 'time' => Validate::IsIntPos,
458
+ 'tags' => SubTagsNormal,
459
+ 'index' => SubIndexes,
460
+ 'action' => Validate::IsIntPos,
461
+ 'perms' => SubPerms,
462
+ 'stats' => SubStats,
463
+ 'files' => {
464
+ method: :array,
465
+ min: 1,
466
+ check: SubFileNew
467
+ }.freeze
468
+ }.freeze
469
+ }.freeze
470
+
471
+
472
+ # Entry - Edit or New
473
+ ItemEntryEdit = {
474
+ method: :hash,
475
+ required: {
476
+ 'caseid' => FieldCaseid,
477
+ 'title' => FieldTitle,
478
+ 'content' => FieldContent,
479
+ }.freeze,
480
+ optional: {
481
+ 'entry' => Validate::IsIntPos,
482
+ 'time' => Validate::IsIntPos,
483
+ 'tags' => SubTagsEntry,
484
+ 'index' => SubIndexes,
485
+ 'action' => Validate::IsIntPos,
486
+ 'perms' => SubPerms,
487
+ 'stats' => SubStats,
488
+ 'files' => {
489
+ method: :array,
490
+ min: 1,
491
+ check: {
492
+ method: :any,
493
+ check: [ SubFileOld, SubFileNew ].freeze
494
+ }.freeze
495
+ }.freeze
496
+ }.freeze
497
+ }.freeze
498
+
499
+
500
+ # Action - Edit or New
501
+ ItemActionEdit = {
502
+ method: :hash,
503
+ required: {
504
+ 'tasks' => SubTasksEdit
505
+ }.freeze,
506
+ optional: {
507
+ 'action' => Validate::IsIntPos
508
+ }.freeze
509
+ }.freeze
510
+
511
+
512
+ # Index - Edit or New
513
+ ItemIndexEdit = {
514
+ method: :hash,
515
+ required: {
516
+ 'title' => FieldTitle,
517
+ 'content' => FieldContent,
518
+ }.freeze,
519
+ optional: {
520
+ 'index' => Validate::IsIntPos,
521
+ 'tags' => SubTags,
522
+ }.freeze
523
+ }.freeze
524
+
525
+
526
+ ##############################################################
527
+ # Recorded items
528
+ ##############################################################
529
+
530
+ # Case
531
+ ItemCase = {
532
+ method: :hash,
533
+ required: {
534
+ 'icfs' => FieldIcfs,
535
+ 'caseid' => FieldCaseid,
536
+ 'log' => Validate::IsIntPos,
537
+ 'template' => Validate::IsBoolean,
538
+ 'status' => Validate::IsBoolean,
539
+ 'title' => FieldTitle,
540
+ 'tags' => SubTags,
541
+ 'access' => SubAccess,
542
+ }.freeze,
543
+ optional: {
544
+ 'stats' => SubCaseStats,
545
+ }.freeze,
546
+ }.freeze
547
+
548
+
549
+ # Log
550
+ ItemLog = {
551
+ method: :hash,
552
+ required: {
553
+ 'icfs' => FieldIcfs,
554
+ 'caseid' => FieldCaseid,
555
+ 'log' => Validate::IsIntPos,
556
+ 'prev' => FieldHash,
557
+ 'time' => Validate::IsIntPos,
558
+ 'user' => FieldUsergrp,
559
+ 'entry' => SubLogItem,
560
+ }.freeze,
561
+ optional: {
562
+ 'index' => SubLogItem,
563
+ 'action' => SubLogItem,
564
+ 'case_hash' => FieldHash,
565
+ 'files_hash' => {
566
+ method: :array,
567
+ min: 1,
568
+ check: FieldHash
569
+ }.freeze,
570
+ }.freeze,
571
+ }.freeze
572
+
573
+
574
+ # Entry
575
+ ItemEntry = {
576
+ method: :hash,
577
+ required: {
578
+ 'icfs' => FieldIcfs,
579
+ 'caseid' => FieldCaseid,
580
+ 'entry' => Validate::IsIntPos,
581
+ 'log' => Validate::IsIntPos,
582
+ 'user' => FieldUsergrp,
583
+ 'time' => Validate::IsIntPos,
584
+ 'title' => FieldTitle,
585
+ 'content' => FieldContent,
586
+ 'tags' => {
587
+ method: :any,
588
+ check: [
589
+ SubTagsEmpty,
590
+ SubTagsEntry,
591
+ ].freeze
592
+ }.freeze,
593
+ }.freeze,
594
+ optional: {
595
+ 'index' => SubIndexes,
596
+ 'action' => Validate::IsIntPos,
597
+ 'perms' => SubPerms,
598
+ 'stats' => SubStats,
599
+ 'files' => {
600
+ method: :array,
601
+ min: 1,
602
+ check: SubFileOld
603
+ }.freeze
604
+ }.freeze
605
+ }.freeze
606
+
607
+
608
+ # Action
609
+ ItemAction = {
610
+ method: :hash,
611
+ required: {
612
+ 'icfs' => FieldIcfs,
613
+ 'caseid' => FieldCaseid,
614
+ 'action' => Validate::IsIntPos,
615
+ 'log' => Validate::IsIntPos,
616
+ 'tasks' => SubTasks
617
+ }.freeze
618
+ }.freeze
619
+
620
+
621
+ # Index
622
+ ItemIndex = {
623
+ method: :hash,
624
+ required: {
625
+ 'icfs' => FieldIcfs,
626
+ 'caseid' => FieldCaseid,
627
+ 'index' => Validate::IsIntPos,
628
+ 'log' => Validate::IsIntPos,
629
+ 'title' => FieldTitle,
630
+ 'content' => FieldContent,
631
+ 'tags' => SubTags
632
+ }.freeze,
633
+ }.freeze
634
+
635
+
636
+ # Current
637
+ ItemCurrent = {
638
+ method: :hash,
639
+ required: {
640
+ 'icfs' => FieldIcfs,
641
+ 'caseid' => FieldCaseid,
642
+ 'log' => Validate::IsIntPos,
643
+ 'hash' => FieldHash,
644
+ 'entry' => Validate::IsIntPos,
645
+ 'action' => Validate::IsIntUns,
646
+ 'index' => Validate::IsIntUns,
647
+ }.freeze
648
+ }.freeze
649
+
650
+
651
+ end # module ICFS::Items
652
+
653
+ end # module ICFS