This file is indexed.

/usr/share/tdiary/misc/plugin/my-sequel.rb is in tdiary-plugin 3.2.2-6.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
#
# my-sequel.rb
#
# show links to follow-up entries
#
# Copyright 2006 zunda <zunda at freeshell.org> and
#                NISHIMURA Takashi <nt at be.to>
#
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work under the terms
# of GPL version 2.
#
# Language resources can be found in the middle of thie file.
# Please search a line with `language resource'
#
require 'pstore'
unless defined?(ERB)
	require 'erb'
end

class MySequel
	include ERB::Util
	extend ERB::Util

	class Conf
		include ERB::Util
		Prefix = 'my_sequel.'

		unless @conf then
			def self::to_native(str)
				return str
			end
		else
			def self::to_native(str)
				@conf.to_native(str)
			end
		end

		def self::handler_escape(string)
			string.gsub(/\r/n, '').gsub(/&/n, '&amp;').gsub(/"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;').gsub(/\n/n, '\n')
		end

		def self::handler_scriptlet
			return <<'_END'
function unescape(string) {
	return string.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&amp;/g, '&');
}
function uncheck(element) {
	document.getElementById(element.id+".reset").checked = false;
}
function restore(element) {
	var text_id = element.id.replace(/\.reset$/, "")
	if (element.checked) {
		document.getElementById(text_id).value = unescape(default_values[text_id]);
	}
}
_END
		end

		def initialize(conf_hash)
			@default_hash = conf_hash
			@conf_hash = Hash.new
		end

		# takes configuration from @options trusting the input
		def merge_hash(hash)
			@default_hash.each_key do |key|
				prefixed = Prefix + key.to_s
				@conf_hash[key] = hash[prefixed] if hash.has_key?(prefixed)
			end
		end

		# takes configuration from @cgi.params
		def merge_params(params)
			@default_hash.each_key do |key|
				keystr = key.to_s
				if params[keystr+'.reset'] and params[keystr+'.reset'][0] then
					@conf_hash.delete(key)
				elsif params[keystr] then
					@conf_hash[key] = params[keystr][0]
				end
			end
		end

		# returns current configuration
		def [](key)
			if @conf_hash.has_key?(key) then
				return @conf_hash[key]
			else
				return @default_hash[key][:default]
			end
		end

		# returns hash of configured values
		def to_conf_hash(target_hash)
			@default_hash.each_key do |key|
				target_hash.delete(Prefix + key.to_s)
			end
			@conf_hash.each_pair do |key, value|
				target_hash[Prefix + key.to_s] = value
			end
		end

		# returns an HTML sniplet for configuration interface
		def html(restore_default_label, mobile = false)
			return @default_hash.keys.sort_by{|k| @default_hash[k][:index]}.map{|k|
				idattr = mobile ? '' : %Q| id="#{h k.to_s}"|
				idattr_reset = mobile ? '' : %Q| id="#{h k.to_s}.reset"|
				uncheck = mobile ? '' : ' onfocus="uncheck(this)"'
				restore = mobile ? '' : ' onchange="restore(this)" onclick="restore(this)"'
				r = %Q|\t<h3 class="subtitle">#{h @default_hash[k][:title]}</h3>\n|
				description = @default_hash[k][:description]
				r += %Q|\t<p>#{h description}</p>\n| if description and not mobile
				unless @default_hash[k][:textarea]
					r += %Q|\t<p><input name="#{h k.to_s}"#{idattr} type="text" value="#{h(Conf.to_native(self[k]))}"#{uncheck}>|
				else
					cols = 70
					rows = 10
					if @default_hash[k][:textarea].respond_to?(:[]) then
						cols = @default_hash[k][:textarea][:cols] || cols
						rows = @default_hash[k][:textarea][:rows] || rows
					end
					r += %Q|\t<p><textarea name="#{h k.to_s}"#{idattr} cols="#{h cols}" rows="#{h rows}"#{uncheck}>#{h(Conf.to_native(self[k]))}</textarea>|
				end
				name = "#{h k.to_s}.reset"
				r += %Q|&nbsp;-&nbsp;<label for="#{name}"><input id="#{name}" name="#{name}"#{idattr_reset} type="checkbox" value="t"#{restore}>#{restore_default_label}</label></p>\n|
				r
			}.join
		end

		# Javascript hash literal for default values
		def default_js_hash
			r = "default_values = {\n"
			r += @default_hash.keys.sort_by{|k| @default_hash[k][:index]}.map{|k|
				%Q|\t"#{h k}": "#{Conf::handler_escape(@default_hash[k][:default])}"|
			}.join(",\n")
			r += "\n};\n"
			return r
		end

		def handler_block
			return <<"_END"
<script type="text/javascript"><!--
#{default_js_hash}#{Conf::handler_scriptlet}// --></script>
_END
		end
	end

	# CSS sniplet for sequels
	def self::css(inner_css)
		unless inner_css.strip.empty?
			return <<"_END"
\t<style type="text/css" media="all"><!--
\tdiv.sequel {
#{h(inner_css.gsub(/^\s*/, "\t\t").gsub(/\r?\n/, "\n"))}\t}
\t--></style>
_END
		else
			return ''
		end
	end

	# cache directory for this plguin
	def self::cache_dir(cache_path)
		return File.join(cache_path, 'my_sequel')
	end

	# cache file for a month: #{yyyy}/#{yyyymm}.#{src or dst}.dat
	def self::cache_file(cache_path, anchor, direction)
		return File.join(MySequel.cache_dir(cache_path), MySequel.year(anchor), "#{MySequel.month(anchor)}.#{direction}.dat")
	end

	# unique for each month
	def self::cache_key(anchor)
		return MySequel.month(anchor)
	end

	# for each cache key for dates
	def self::each_cache_key(dates)
		dates = dates.is_a?(String) ? [dates] : dates
		dates.map{|ymd| MySequel.cache_key(ymd)}.uniq.each do |cache_file|
			yield(cache_file)
		end
	end

	# yyyy
	def self::year(anchor)
		return anchor.scan(/\d{4,4}/)[0]
	end

	# yyyymm
	def self::month(anchor)
		return anchor.scan(/\d{6,6}/)[0]
	end

	# yyyymmdd
	def self::date(anchor)
		if anchor.respond_to?(:localtime)
			return anchor.localtime.strftime("%Y%m%d")
		else
			return anchor.scan(/\d{8,8}/)[0]
		end
	end

	# add an entry to Array value of hash, making new Array if needed
	def self::push_to_hash(hash, key, element)
		unless hash.has_key?(key)
			hash[key] = Array.new
			begin
				hash[key].taint
			rescue SecurityError
			end
		end
		hash[key] << element
		hash
	end

	def initialize(cache_path)
		@link_srcs = Hash.new.taint	# key:dst anchor value:Array of src anchors
		@current_dsts = Hash.new.taint	# key:src anchor value:Array of dst anchors
		@cached_dsts = Hash.new.taint	# for restore_dsts and clean_srcs
		@vanished_dsts = Hash.new.taint	# key:src date value:Array of dst anchors
		@cache_path = cache_path
	end

	def restore(dates)
		restore_srcs(dates)
		restore_dsts(dates)
	end

	# HTML sniplet for sequels
	def html(dst_anchor, date_format, label)
		anchors = srcs(dst_anchor)
		if anchors and not anchors.empty? then
			r = %Q|<div class="sequel">#{h label}|
			r += anchors.map{|src_anchor|
				yield(src_anchor, Time.local(*(src_anchor.scan(/(\d{4,4})(\d\d)(\d\d)/)[0])).strftime(date_format))
			}.join(', ')
			r += "</div>\n"
			return r
		else
			return ''
		end
	end

	# Array of source anchors for a destination anchor, nil if none
	def srcs(dst_anchor)
		a = @link_srcs[dst_anchor]
		return nil if not a or a.empty?
		return a.uniq.sort
	end

	# starts a day - get ready to scan the diary for the section
	def clean_dsts(date)
		datestr = MySequel.date(date)
		@current_dsts.keys.each do |src_anchor|
			next unless MySequel.date(src_anchor) == datestr
			@current_dsts[src_anchor] = Array.new
			begin
				@current_dsts[src_anchor].taint
			rescue SecurityError
			end
		end
	end

	# adds a link
	def add(src_anchor, dst_anchor)
		MySequel.push_to_hash(@link_srcs, dst_anchor, src_anchor)
		MySequel.push_to_hash(@current_dsts, src_anchor, dst_anchor)
	end

	# detect vanished links
	def clean_srcs
		(@cached_dsts.keys + @current_dsts.keys).uniq.each do |src_anchor|
			if @cached_dsts[src_anchor] then
				if @current_dsts[src_anchor] then
					@vanished_dsts[src_anchor] = @cached_dsts[src_anchor] - @current_dsts[src_anchor]
				else
					@vanished_dsts[src_anchor] = @cached_dsts[src_anchor]
				end
			end
			@cached_dsts[src_anchor] = @current_dsts[src_anchor].dup
		end
	end

	# restores cached data for a month
	# calls the block for each root giving key and value
	def each_cached(anchor, direction)
		path = MySequel.cache_file(@cache_path, anchor, direction)
		begin
			PStore.new(path).transaction(true) do |db|
				db.roots.each do |cached_anchor|
					yield(cached_anchor, db[cached_anchor])
				end
			end
		rescue TypeError	# corrupted PStore data
			File.unlink(path)
		rescue PStore::Error	# corrupted PStore data
			begin
				File.unlink(path)
			rescue Errno::ENOENT
			end
		rescue Errno::ENOENT	# no cache yet
		end
	end
	private :each_cached

	# restores cached sources for a month
	def restore_srcs(dates)
		@srcs_loaded ||= Hash.new
		MySequel.each_cache_key(dates) do |cache_key|
			unless @srcs_loaded[cache_key] then
				each_cached(cache_key, 'src') do |anchor, array|
					unless @link_srcs.has_key?(anchor)
						@link_srcs[anchor] = array.taint
					else
						@link_srcs[anchor] += array.taint
					end
				end
				@srcs_loaded[cache_key] = true
			end
		end
	end

	# restores cached destinations
	def restore_dsts(dates)
		@dsts_loaded ||= Hash.new
		MySequel.each_cache_key(dates) do |cache_key|
			unless @dsts_loaded[cache_key] then
				each_cached(cache_key, 'dst') do |anchor, array|
					array.taint
					@cached_dsts[anchor] = array
					@current_dsts[anchor] = array.dup
				end
				@dsts_loaded[cache_key] = true
			end
		end
	end

	# hash for storing cache
	# key: path to cache
	# value: Hash
	#   key: anchor
	#   value: compacted and uniqed Array of anchor on the other side of link
	def hash_for_cache(link_hash, direction)
		r = Hash.new
		link_hash.each_pair do |pivot_anchor, anchor_array|
			c = anchor_array.compact.uniq
			path = MySequel.cache_file(@cache_path, pivot_anchor, direction)
			r[path] ||= Hash.new
			r[path][pivot_anchor] = c
		end
		return r
	end
	private :hash_for_cache

	# stores the data
	def store(cache_hash)
		cache_hash.each_pair do |path, h|
			d = File.dirname(path)
			Dir.mkdir(d) unless File.exist?(d)
			PStore.new(path).transaction do |db|
				h.each_pair do |k, v|
					unless v.empty? then
						db[k] = v
					else
						db.delete(k)
					end
				end
			end
		end
	end
	private :store

	# commits on-memory results to files
	def commit
		d = MySequel.cache_dir(@cache_path)
		Dir.mkdir(d) unless File.exist?(d)

		restore_srcs(@link_srcs.keys)

		restore_srcs(@vanished_dsts.values.flatten)
		@vanished_dsts.each_pair do |src_anchor, dst_anchors|
			dst_anchors.uniq.each do |dst_anchor|
				@link_srcs[dst_anchor].reject!{|anchor| anchor == src_anchor}
			end
		end

		store(hash_for_cache(@link_srcs, 'src'))
		store(hash_for_cache(@current_dsts, 'dst'))
		@vanished_dsts = Hash.new.taint
	end

end

# register this plguin to tDiary
unless defined?(Test::Unit)
	# language resource and configuration
	@my_sequel_plugin_name ||= 'Link to follow ups'
	@my_sequel_description ||= <<_END
<p>Shows links to follow-up entries,
which have `my' link to the entry in the past.</p>
<p>Do not forget to push the OK button to store the changes.</p>
_END
	@my_sequel_label_conf ||= 'Link label'
	@my_sequel_label ||= 'Follow up: '
	@my_sequel_restore_default_label ||= 'Restore default'
	@my_sequel_default_hash ||= {
		:label => {
			:title => 'Link label',
			:default => 'Follow up: ',
			:description => 'Prefix for links to the follow-ups',
			:index => 1,
		},
		:date_format => {
			:title => 'Link format',
			:default => @date_format,
			:description => 'Time format of links to the follow-ups. Sequences of % and a charactor are converted as follows: "%Y" to year, "%m" to month in number, "%b" to short name of month, "%B" to full name of month, "%d" to day of month, "%a" to short name of day of week, and "%A" to full name of day of week, for the follow-up.',
			:index => 2,
		},
		:inner_css => {
			:title => 'CSS',
			:default => <<'_END',
font-size: 75%;
text-align: right;
margin: 0px;
_END
			:description => 'CSS for the links. The followoing is applied to <code>div.sequel</code>.',
			:index => 3,
			:textarea => {:rows => 5},
		},
	}

	@my_sequel_conf = MySequel::Conf.new(@my_sequel_default_hash)
	@my_sequel_conf.merge_hash(@options)

	# configuration interface
	add_conf_proc( 'my-sequel', @my_sequel_plugin_name ) do
		if @mode == 'saveconf' then
			@my_sequel_conf.merge_params(@cgi.params)
			@my_sequel_conf.to_conf_hash(@conf)
		end
		<<"_HTML"
#{@my_sequel_conf.handler_block}
<h3>#{@my_sequel_plugin_name}</h3>
#{@my_sequel_description}
#{@my_sequel_conf.html(@my_sequel_restore_default_label, @cgi.mobile_agent?).chomp}
_HTML
	end

	@my_sequel = MySequel.new(@cache_path)
	@my_sequel_active = false

	# activate this plugin if header procs are called
	# - This avoids being called from makerss.rb
	add_header_proc do
		if not bot? and not @cgi.mobile_agent? then
			@my_sequel_active = true
			@my_sequel.restore(@diaries.keys)
			MySequel.css(@my_sequel_conf[:inner_css])
		end
	end

	# preparation for a day
	add_body_enter_proc do |date|
		if @my_sequel_active then
			if date then
				@my_sequel_date = MySequel.date(date)
				@my_sequel.clean_dsts(@my_sequel_date)
			else
				@my_sequel_date = nil
			end
		end
		''
	end

	# preparation for a section
	add_section_enter_proc do |date, index|
		if @my_sequel_active and @my_sequel_date then
			@my_sequel_anchor = "#{@my_sequel_date}#p#{'%02d' % index}"
		end
		''
	end

	# plugin function to be called from within sections
	alias :my_sequel_orig_my :my unless defined?(my_sequel_orig_my)
	def my(*args)
		if @my_sequel_active and @my_sequel_date and @my_sequel_anchor and @mode != 'preview' then
			dst_date, frag = args[0].scan(/(\d{8,8})(?:[^\d]*)(?:#?p(\d+))?$/)[0]
			if dst_date and dst_date < @my_sequel_date then
				dst_anchor = "#{dst_date}#{frag ? "#p%02d" % frag.to_i : ''}"
				@my_sequel.add(@my_sequel_anchor, dst_anchor)
			end
		end
		my_sequel_orig_my(*args)
	end

	# show sequels when leaving a section
	add_section_leave_proc do
		r = ''
		if @my_sequel_active and @my_sequel_date and @my_sequel_anchor and not bot? and not @cgi.mobile_agent? then
			r = @my_sequel.html(@my_sequel_anchor, @my_sequel_conf[:date_format], @my_sequel_conf[:label]){|src_anchor, anchor_str|
				my_sequel_orig_my(src_anchor, anchor_str)
			}
		end
		@my_sequel_anchor = nil
		r
	end

	# show sequels when leaving a day
	add_body_leave_proc do
		r = ''
		if @my_sequel_active and @my_sequel_date then
			if not bot? and not @cgi.mobile_agent? then
				r = @my_sequel.html(@my_sequel_anchor, @my_sequel_conf[:date_format], @my_sequel_conf[:label]){|src_anchor, anchor_str|
					my_sequel_orig_my(src_anchor, anchor_str)
				}
			end
		end
		@my_sequel_date = nil
		r
	end

	# commit changes
	add_footer_proc do
		if @my_sequel_active then
			@my_sequel.clean_srcs
			@my_sequel.commit
		end
		''
	end
end

# Local Variables:
# mode: ruby
# indent-tabs-mode: t
# tab-width: 3
# ruby-indent-level: 3
# End: