File: gmHooks.py

package info (click to toggle)
gnumed-server 22.15-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 46,556 kB
  • sloc: sql: 1,217,005; python: 15,469; sh: 1,553; makefile: 20
file content (207 lines) | stat: -rw-r--r-- 5,470 bytes parent folder | download | duplicates (5)
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
"""GNUmed hooks framework.

This module provides convenience functions and definitions
for accessing the GNUmed hooks framework.

This framework calls the script

	~/.gnumed/scripts/hook_script.py

at various times during client execution. The script must
contain a function

def run_script(hook=None):
	pass

which accepts a single argument <hook>. That argument will
contain the hook that is being activated.
"""
# ========================================================================
__author__  = "K. Hilbert <Karsten.Hilbert@gmx.net>"
__license__ = "GPL v2 or later (details at http://www.gnu.org)"

# stdlib
import os
import sys
import stat
import logging
import io


# GNUmed libs
if __name__ == '__main__':
	sys.path.insert(0, '../../')
from Gnumed.pycommon import gmDispatcher
from Gnumed.pycommon import gmTools


_log = logging.getLogger('gm.hook')
# ========================================================================
known_hooks = [
	'post_patient_activation',
	'post_person_creation',

	'after_waiting_list_modified',

	'shutdown-post-GUI',
	'startup-after-GUI-init',
	'startup-before-GUI',

	'request_user_attention',
	'app_activated_startup',
	'app_activated',
	'app_deactivated',

	'after_substance_intake_modified',
	'after_test_result_modified',
	'after_soap_modified',
	'after_code_link_modified',

	'after_new_doc_created',
	'before_print_doc',
	'before_fax_doc',
	'before_mail_doc',
	'before_print_doc_part',
	'before_fax_doc_part',
	'before_mail_doc_part',
	'before_external_doc_access',

	'db_maintenance_warning'
]

_log.debug('known hooks:')
for hook in known_hooks:
	_log.debug(hook)

# ========================================================================
hook_module = None

def import_hook_module(reimport=False):

	global hook_module
	if not reimport:
		if hook_module is not None:
			return True

	# hardcoding path and script name allows us to
	# not need configuration for it, the environment
	# can always be detected at runtime (workplace etc)
	script_name = 'hook_script.py'
	script_path = os.path.expanduser(os.path.join('~', '.gnumed', 'scripts'))
	full_script = os.path.join(script_path, script_name)

	if not os.access(full_script, os.F_OK):
		_log.warning('creating default hook script')
		f = io.open(full_script, mode = 'wt', encoding = 'utf8')
		f.write("""
# known hooks:
#  %s

def run_script(hook=None):
	pass
""" % '#  '.join(known_hooks))
		f.close()
		os.chmod(full_script, 384)

	if os.path.islink(full_script):
		gmDispatcher.send (
			signal = 'statustext',
			msg = _('Script must not be a link: [%s].') % full_script
		)
		return False

	if not os.access(full_script, os.R_OK):
		gmDispatcher.send (
			signal = 'statustext',
			msg = _('Script must be readable by the calling user: [%s].') % full_script
		)
		return False

	script_stat_val = os.stat(full_script)
	_log.debug('hook script stat(): %s', script_stat_val)
	script_perms = stat.S_IMODE(script_stat_val.st_mode)
	_log.debug('hook script mode: %s (oktal: %s)', script_perms, oct(script_perms))
	if script_perms != 384:				# octal 0600
		if os.name in ['nt']:
			_log.warning('this platform does not support os.stat() file permission checking')
		else:
			gmDispatcher.send (
				signal = 'statustext',
				msg = _('Script must be readable by the calling user only (permissions "0600"): [%s].') % full_script
			)
			return False

	try:
		tmp = gmTools.import_module_from_directory(script_path, script_name)
	except Exception:
		_log.exception('cannot import hook script')
		return False

	hook_module = tmp
#	if reimport:
#		imp.reload(tmp)			# this has well-known shortcomings !

	_log.info('hook script: %s', full_script)
	return True

# ========================================================================
__current_hook_stack = []

def run_hook_script(hook=None):
	# NOTE: this just *might* be a huge security hole

	_log.info('told to pull hook [%s]', hook)

	if hook not in known_hooks:
		raise ValueError('run_hook_script(): unknown hook [%s]' % hook)

	if not import_hook_module(reimport = False):
		_log.debug('cannot import hook module, not pulling hook')
		return False

	if hook in __current_hook_stack:
		_log.error('hook-code cycle detected, aborting')
		_log.error('current hook stack: %s', __current_hook_stack)
		return False

	__current_hook_stack.append(hook)

	try:
		hook_module.run_script(hook = hook)
	except Exception:
		_log.exception('error running hook script for [%s]', hook)
		gmDispatcher.send (
			signal = 'statustext',
			msg = _('Error running hook [%s] script.') % hook,
			beep = True
		)
		if __current_hook_stack[-1] != hook:
			_log.error('hook nesting errror detected')
			_log.error('latest hook: expected [%s], found [%s]', hook, __current_hook_stack[-1])
			_log.error('current hook stack: %s', __current_hook_stack)
		else:
			__current_hook_stack.pop()
		return False

	if __current_hook_stack[-1] != hook:
		_log.error('hook nesting errror detected')
		_log.error('latest hook: expected [%s], found [%s]', hook, __current_hook_stack[-1])
		_log.error('current hook stack: %s', __current_hook_stack)
	else:
		__current_hook_stack.pop()

	return True

# ========================================================================
if __name__ == '__main__':

	if len(sys.argv) < 2:
		sys.exit()

	if sys.argv[1] != 'test':
		sys.exit()

	run_hook_script(hook = 'shutdown-post-GUI')
	run_hook_script(hook = 'invalid hook')

# ========================================================================