xolo-server 1.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +42 -4
  3. data/bin/xoloserver +3 -0
  4. data/data/client/xolo +1233 -0
  5. data/lib/optimist_with_insert_blanks.rb +1216 -0
  6. data/lib/xolo/core/base_classes/configuration.rb +238 -0
  7. data/lib/xolo/core/base_classes/server_object.rb +112 -0
  8. data/lib/xolo/core/base_classes/title.rb +884 -0
  9. data/lib/xolo/core/base_classes/version.rb +641 -0
  10. data/lib/xolo/core/constants.rb +85 -0
  11. data/lib/xolo/core/exceptions.rb +52 -0
  12. data/lib/xolo/core/json_wrappers.rb +43 -0
  13. data/lib/xolo/core/loading.rb +59 -0
  14. data/lib/xolo/core/output.rb +292 -0
  15. data/lib/xolo/core/security_cmd.rb +128 -0
  16. data/lib/xolo/core/version.rb +21 -0
  17. data/lib/xolo/core.rb +47 -0
  18. data/lib/xolo/server/app.rb +7 -0
  19. data/lib/xolo/server/configuration.rb +243 -38
  20. data/lib/xolo/server/constants.rb +10 -0
  21. data/lib/xolo/server/helpers/auth.rb +19 -2
  22. data/lib/xolo/server/helpers/autopkg.rb +157 -0
  23. data/lib/xolo/server/helpers/client_data.rb +90 -60
  24. data/lib/xolo/server/helpers/file_transfers.rb +412 -82
  25. data/lib/xolo/server/helpers/jamf_pro.rb +31 -7
  26. data/lib/xolo/server/helpers/log.rb +2 -0
  27. data/lib/xolo/server/helpers/maintenance.rb +1 -0
  28. data/lib/xolo/server/helpers/notification.rb +4 -3
  29. data/lib/xolo/server/helpers/pkg_signing.rb +16 -12
  30. data/lib/xolo/server/helpers/progress_streaming.rb +9 -12
  31. data/lib/xolo/server/helpers/subscriptions.rb +119 -0
  32. data/lib/xolo/server/helpers/titles.rb +27 -3
  33. data/lib/xolo/server/helpers/versions.rb +23 -11
  34. data/lib/xolo/server/mixins/changelog.rb +9 -16
  35. data/lib/xolo/server/mixins/title_jamf_access.rb +375 -390
  36. data/lib/xolo/server/mixins/title_ted_access.rb +50 -8
  37. data/lib/xolo/server/mixins/version_jamf_access.rb +118 -129
  38. data/lib/xolo/server/mixins/version_ted_access.rb +34 -4
  39. data/lib/xolo/server/object_locks.rb +2 -1
  40. data/lib/xolo/server/routes/auth.rb +2 -2
  41. data/lib/xolo/server/routes/jamf_pro.rb +11 -1
  42. data/lib/xolo/server/routes/maint.rb +2 -1
  43. data/lib/xolo/server/routes/subscriptions.rb +126 -0
  44. data/lib/xolo/server/routes/title_editor.rb +1 -1
  45. data/lib/xolo/server/routes/titles.rb +26 -11
  46. data/lib/xolo/server/routes/uploads.rb +0 -14
  47. data/lib/xolo/server/routes/versions.rb +14 -13
  48. data/lib/xolo/server/routes.rb +15 -23
  49. data/lib/xolo/server/title.rb +100 -77
  50. data/lib/xolo/server/version.rb +178 -18
  51. data/lib/xolo/server.rb +8 -0
  52. metadata +20 -11
@@ -0,0 +1,85 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+
6
+ # frozen_string_literal: true
7
+
8
+ module Xolo
9
+
10
+ module Core
11
+
12
+ # Constants useful throughout Xolo
13
+ #####################################
14
+ module Constants
15
+
16
+ # Empty strings are used in various places
17
+ BLANK = ''
18
+
19
+ # The value to use when unsetting an option
20
+ NONE = 'none'
21
+
22
+ OK = 'OK'
23
+
24
+ ERROR = 'ERROR'
25
+
26
+ # Several things use x
27
+ X = 'x'
28
+
29
+ # CLI options and other things use dashes
30
+ DASH = '-'
31
+
32
+ # Several things use dots
33
+ DOT = '.'
34
+
35
+ # Cancelling is often an option
36
+ CANCEL = 'Cancel'
37
+
38
+ # and we check for things ending with .app
39
+ DOTAPP = '.app'
40
+
41
+ DOTJSON = '.json'
42
+
43
+ # These are handy for testing values without making new arrays, strings, etc every time.
44
+ TRUE_FALSE = [true, false].freeze
45
+
46
+ # lots of things get split on commmas
47
+ COMMA_SEP_RE = /\s*,\s*/.freeze
48
+
49
+ # lots of things get joined with commas
50
+ COMMA_JOIN = ', '
51
+
52
+ # Some things get split on semicolons
53
+ SEMICOLON_SEP_RE = /\s*;\s*/.freeze
54
+
55
+ # Once a thing has been uploaded and saved, this
56
+ # is what the server returns as the attr value
57
+ ITEM_UPLOADED = 'uploaded'
58
+
59
+ DOT_PKG = '.pkg'
60
+
61
+ UNKNOWN = 'unknown'
62
+
63
+ # Installer packages must have one of these extensions
64
+ OK_PKG_EXTS = [DOT_PKG]
65
+
66
+ # The value to use when all computers are the release-targets
67
+ # and for all manual-install policies
68
+ TARGET_ALL = 'all'
69
+
70
+ # Title Types
71
+ MANAGED = :managed
72
+ SUBSCRIBED = :subscribed
73
+ TITLE_TYPES = [MANAGED, SUBSCRIBED].freeze
74
+ DEFAULT_TITLE_TYPE = :managed
75
+
76
+ # when this module is included
77
+ def self.included(includer)
78
+ Xolo.verbose_include includer, self
79
+ end
80
+
81
+ end # module constants
82
+
83
+ end # module core
84
+
85
+ end # module Xolo
@@ -0,0 +1,52 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ module Xolo
9
+
10
+ module Core
11
+
12
+ module Exceptions
13
+
14
+ # General errors
15
+
16
+ class MissingDataError < RuntimeError; end
17
+
18
+ class InvalidDataError < RuntimeError; end
19
+
20
+ class NoSuchItemError < RuntimeError; end
21
+
22
+ class UnsupportedError < RuntimeError; end
23
+
24
+ class KeychainError < RuntimeError; end
25
+
26
+ class ActionRequiredError < RuntimeError; end
27
+
28
+ # Connections & Access
29
+
30
+ class ConnectionError < RuntimeError; end
31
+
32
+ class NotConnectedError < ConnectionError; end
33
+
34
+ class TimeoutError < ConnectionError; end
35
+
36
+ class AuthenticationError < ConnectionError; end
37
+
38
+ class PermissionError < ConnectionError; end
39
+
40
+ class InvalidTokenError < ConnectionError; end
41
+
42
+ class ServerError < ConnectionError; end
43
+
44
+ # Parsing errors
45
+
46
+ class DisallowedYAMLDumpClass; end
47
+
48
+ end # module Exceptions
49
+
50
+ end # module Core
51
+
52
+ end # module Xolo
@@ -0,0 +1,43 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # frozen_string_literal: true
9
+
10
+ # main module
11
+ module Xolo
12
+
13
+ module Core
14
+
15
+ # constants and methods for consistent JSON processing on the server
16
+ module JSONWrappers
17
+
18
+ # when this module is extended
19
+ def self.extended(extender)
20
+ Xolo.verbose_extend extender, self
21
+ end
22
+
23
+ # when this module is included
24
+ def self.included(includer)
25
+ Xolo.verbose_include includer, self
26
+ end
27
+
28
+ # A wrapper for JSON.parse that always uses :symbolize_names
29
+ # def self.parse_json(str)
30
+ # JSON.parse str, symbolize_names: true
31
+ # end
32
+
33
+ # A wrapper for JSON.parse that always uses :symbolize_names
34
+ # and ensures UTF-8 encoding
35
+ def parse_json(str)
36
+ JSON.parse str.force_encoding('UTF-8'), symbolize_names: true
37
+ end
38
+
39
+ end # JSON
40
+
41
+ end # Core
42
+
43
+ end # module Xolo
@@ -0,0 +1,59 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ module Xolo
10
+
11
+ module Core
12
+
13
+ module Loading
14
+
15
+ # touch this file to make mixins send text to stderr as things load
16
+ # or get mixed in
17
+ VERBOSE_LOADING_FILE = Pathname.new('/tmp/xolo-verbose-loading')
18
+
19
+ # Or, set this ENV var to also make mixins send text to stderr
20
+ VERBOSE_LOADING_ENV = 'XOLO_VERBOSE_LOADING'
21
+
22
+ def self.extended(extender)
23
+ Xolo.verbose_extend extender, self
24
+ end
25
+
26
+ # Only look at the filesystem once.
27
+ def verbose_loading?
28
+ return @verbose_loading unless @verbose_loading.nil?
29
+
30
+ @verbose_loading = VERBOSE_LOADING_FILE.file?
31
+ @verbose_loading ||= ENV.include? VERBOSE_LOADING_ENV
32
+ @verbose_loading
33
+ end
34
+
35
+ # Send a message to stderr if verbose loading is enabled
36
+ def load_msg(msg)
37
+ warn msg if verbose_loading?
38
+ end
39
+
40
+ # Mention that a module is being included into something
41
+ def verbose_include(includer, includee)
42
+ load_msg "--> #{includer} is including #{includee}"
43
+ end
44
+
45
+ # Mention that a module is being extended into something
46
+ def verbose_extend(extender, extendee)
47
+ load_msg "--> #{extender} is extending #{extendee}"
48
+ end
49
+
50
+ # Mention that a module is being extended into something
51
+ def verbose_inherit(child_class, parent_class)
52
+ load_msg "--> #{child_class} is a Subclass inheriting from #{parent_class}"
53
+ end
54
+
55
+ end # module
56
+
57
+ end # module
58
+
59
+ end # module Xolo
@@ -0,0 +1,292 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'io/console'
10
+
11
+ module Xolo
12
+
13
+ module Core
14
+
15
+ # Methods for formattng and sending output to stdout
16
+ # Should be included in classes as needed
17
+ #
18
+ # NOTE: Help output is auto-generated by 'optimist'
19
+ # The methods here are mostly for presenting info like
20
+ # columnizd lists and reports and the like.
21
+ module Output
22
+
23
+ # Constants
24
+ #############################
25
+ #############################
26
+
27
+ # This is used when we are not outputting to a terminal
28
+ # usually we're being piped or not running in a terminal
29
+ # so the lines should be as long as they want
30
+ DEFAULT_LINE_WIDTH = 2000
31
+
32
+ # Module methods
33
+ #
34
+ # These are available as module methods but not as 'helper'
35
+ # methods in sinatra routes & views.
36
+ #
37
+ ##############################
38
+ ##############################
39
+
40
+ # when this module is included
41
+ ##############################
42
+ def self.included(includer)
43
+ Xolo.verbose_include includer, self
44
+ end
45
+
46
+ # when this module is extended
47
+ def self.extended(extender)
48
+ Xolo.verbose_extend extender, self
49
+ end
50
+
51
+ # Instance methods
52
+ #
53
+ # These are available directly in sinatra routes and views
54
+ #
55
+ ##############################
56
+ ##############################
57
+
58
+ # @return [Integer] how many rows high is our terminal?
59
+ #########################
60
+ def terminal_height
61
+ IO.console.winsize.first
62
+ end
63
+
64
+ # @return [Integer] how many columns wide is our terminal?
65
+ #########################
66
+ def terminal_width
67
+ IO.console.winsize.last
68
+ end
69
+
70
+ # @return [Integer] how wide is our word wrap? terminal-width minus 5
71
+ #########################
72
+ def terminal_word_wrap
73
+ @terminal_word_wrap ||= terminal_width - 5
74
+ end
75
+
76
+ # format a multi-line value by prepending the desired indentation
77
+ # to all but the first line, which is expected to be indented in-place where
78
+ # its being used.
79
+ #
80
+ # @param value [String] the value to format
81
+ # @param indent [Integer] the number of spaces to indent all but the first line
82
+ # @return [String] the formatted value
83
+ #######################
84
+ def format_multiline_indent(value, indent:)
85
+ value = value.to_s
86
+ return value unless value.include? "\n"
87
+
88
+ lines = value.split("\n")
89
+ lines[1..-1].each { |line| line.prepend ' ' * indent }
90
+ lines.join("\n")
91
+ end
92
+
93
+ # TODO: Move this out of Xolo::Core
94
+ # Display a list of items in as many columns as possible
95
+ # based on terminal width, e.g. with 3 cols:
96
+ #
97
+ # a thing another thing third thing
98
+ # thing2 line2 thing third line 2
99
+ # line3 thing another one and yet a 3rd
100
+ # oh my line4 4 line ok? yes, this is it
101
+ #
102
+ # and if the list is longer than terminal height,
103
+ # pipe it through 'less'
104
+ #
105
+ # @param header [String] A string to display at the top prepended with a '#'
106
+ # and appended with a newline and a line of ######'s of the same length.
107
+ #
108
+ # @param list [Array<String>] the items to list
109
+ #
110
+ # @return [void]
111
+ ############################
112
+ def list_in_cols(header, list)
113
+ longest_list_item = list.map(&:size).max
114
+ use_columns = (longest_list_item + 5) < terminal_width
115
+
116
+ list_to_display = use_columns ? highline_cli.list(list, :columns_down) : list.join("\n")
117
+
118
+ output = +"# #{header}\n"
119
+ output << '#' * (terminal_width - 5)
120
+ output << "\n"
121
+ output << list_to_display
122
+
123
+ show_text output
124
+ end
125
+
126
+ # Generate a report of rowed/columned data, either fixed-width or tab-delimited.
127
+ #
128
+ # Title and header lines are pre-pended with '# ' for easier exclusion
129
+ # when using the report as input for some other program.
130
+ # If the :type is :fixed, so will the column header line.
131
+ # (however, for parsing this data, try using the --json option)
132
+ #
133
+ # @param lines [Array<Array>] the rows and columns of data
134
+ #
135
+ # @param type [Symbol] :fixed or :tab, defaults to :fixed
136
+ #
137
+ # @params title [String] a descriptive text or title, shown above the
138
+ # column headers. Every line is pre-pended with '# '.
139
+ # Only used on :fixed reports.
140
+ #
141
+ # @params header_row [Array<String>] the column headers. optional.
142
+ #
143
+ # @return [String] the formatted report.
144
+ #
145
+ ############################
146
+ def generate_report(lines, type: :fixed, header_row: [], title: nil)
147
+ return Xolo::BLANK if lines.pix_empty?
148
+
149
+ raise ArgumentError, 'The first argument must be an Array' unless lines.is_a?(Array)
150
+ raise ArgumentError, 'The header_row must be an Array' unless header_row.is_a? Array
151
+
152
+ # tab delim is easy
153
+ if type == :tab
154
+ report_tab = header_row.join("\t")
155
+ lines.each { |line| report_tab += "\n#{line.join("\t")}" }
156
+ return report_tab.strip
157
+ end # if :tab
158
+
159
+ # below here, fixed width
160
+
161
+ line_width, format_str = width_and_format(lines, header_row)
162
+
163
+ # title if given
164
+ report = title ? +"# #{title}\n" : +''
165
+
166
+ unless header_row.empty?
167
+ unless header_row.size == lines[0].size
168
+ raise ArgumentError,
169
+ "Header row must have #{lines[0].count} items"
170
+ end
171
+
172
+ # then the header line if given
173
+ report += format_str % header_row
174
+ # add a separator
175
+ report += '#' + ('-' * (line_width - 1)) + "\n"
176
+ end
177
+ # add the rows
178
+ lines.each { |line| report += format_str % line }
179
+
180
+ report
181
+ end # generate report
182
+
183
+ # Given an Array of Arrays representing rows and columns of data,
184
+ # figure out the appropriate line-width for the longest line
185
+ # and the printf format string to create the columns
186
+ #
187
+ #
188
+ # @param lines [Array<Array>] The rows and columns of data
189
+ #
190
+ # @param header_row [Array] An optional header row to include in the
191
+ # width calculation.
192
+ #
193
+ # @return [Array<Integer, String>] the line width and format string
194
+ #
195
+ ############################
196
+ def width_and_format(lines, header_row = [])
197
+ # below here, fixed width
198
+ format_str = +''
199
+ line_width = 0
200
+ header_row[0] = "# #{header_row[0]}"
201
+
202
+ col_widths(lines, header_row).each do |w|
203
+ # make sure there's a space between columns
204
+ col_width = w + 1
205
+
206
+ # add the column to the printf format
207
+ format_str += "%-#{col_width}s"
208
+ line_width += col_width
209
+ end
210
+ format_str += "\n"
211
+
212
+ # if needed, limit the total line width for the header the width of the terminal
213
+ max_width = $stdout.tty? ? terminal_word_wrap : DEFAULT_LINE_WIDTH
214
+ line_width = max_width if line_width > max_width
215
+
216
+ [line_width, format_str]
217
+ end
218
+
219
+ # Given an Array of Arrays representing rows and columns of data
220
+ # figure out the widest width of each column and return an array
221
+ # of integers representing those widths
222
+ #
223
+ # @param data [Array<Array>] The rows and columns of data
224
+ #
225
+ # @param header_row [Array] An optional header row to include in the
226
+ # width calculation.
227
+ #
228
+ # @return [Array<Integer>] the max widths of each column of data.
229
+ #
230
+ ############################
231
+ def col_widths(data, header_row = [])
232
+ widths = header_row.map { |c| c.to_s.length }
233
+ data.each do |row|
234
+ row.each_index do |col|
235
+ this_width = row[col].to_s.length
236
+ widths[col] = this_width if this_width > widths[col].to_i
237
+ end # do field
238
+ end # do line
239
+ widths
240
+ end
241
+
242
+ # Should a given string be displayed via /usr/bin/less?
243
+ # true if stdout is a tty AND the string is > (terminal height - 2)
244
+ # The - 2 accounts for the final newline and an extra line at the
245
+ # bottom of the terminal, for better visual results
246
+ #########################
247
+ def use_less?(text)
248
+ $stdout.tty? && text.lines.size > (terminal_height - 2)
249
+ end
250
+
251
+ # Send a string to the terminal, possibly piping it through 'less'
252
+ # if the number of lines is greater than the number of terminal lines
253
+ #
254
+ # @param text[String] the text to send to the terminal
255
+ #
256
+ # @param show_help[Boolean] should the text have a line at the top
257
+ # showing basic 'less' key commands.
258
+ #
259
+ # @result [void]
260
+ #
261
+ ############################
262
+ def show_text(text, show_help = true)
263
+ unless use_less?(text)
264
+ puts text
265
+ return
266
+ end
267
+
268
+ if show_help
269
+ help = "# -- Using /usr/bin/less: ' ' next, 'b' prev, 'q' exit, 'h' help --"
270
+ text = "#{help}\n#{text}"
271
+ end
272
+
273
+ # point stdout through less, print, then restore stdout
274
+ less = IO.popen('/usr/bin/less', 'w')
275
+
276
+ begin
277
+ less.puts text
278
+
279
+ # this catches the quitting of 'less' before all the output
280
+ # is displayed
281
+ rescue Errno::EPIPE
282
+ true
283
+ ensure
284
+ less&.close
285
+ end
286
+ end
287
+
288
+ end
289
+
290
+ end
291
+
292
+ end # module
@@ -0,0 +1,128 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # frozen_string_literal: true
9
+
10
+ # main module
11
+ module Xolo
12
+
13
+ module Core
14
+
15
+ # Personal credentials for users of 'xadm', stored in the login keychain
16
+ #
17
+ module SecurityCmd
18
+
19
+ # Constants
20
+ ##############################
21
+ ##############################
22
+
23
+ # The security command
24
+ SEC_COMMAND = '/usr/bin/security'
25
+
26
+ # exit status when the login keychain can't be accessed because we aren't in a GUI session
27
+ SEC_STATUS_NO_GUI_ERROR = 36
28
+
29
+ # exit status when the keychain password provided is incorrect
30
+ SEC_STATUS_AUTH_ERROR = 51
31
+
32
+ # exit status when the desired item isn't found in the keychain
33
+ SEC_STATUS_NOT_FOUND_ERROR = 44
34
+
35
+ # Module methods
36
+ ##############################
37
+ ##############################
38
+
39
+ # when this module is included
40
+ def self.included(includer)
41
+ Xolo.verbose_include includer, self
42
+ end
43
+
44
+ # Instance Methods
45
+ ##########################
46
+ ##########################
47
+
48
+ # Run the security command in interactive mode on a given keychain,
49
+ # passing in a subcommand and its arguments. so that they don't appear in the
50
+ # `ps` output
51
+ #
52
+ # @param cmd [String] the subcommand being passed to 'security' with
53
+ # all needed options. It will not be visible outide this process, so
54
+ # its OK to put passwords into the options.
55
+ #
56
+ # @return [String] the stdout of the 'security' command.
57
+ #
58
+ ######
59
+ def run_security(cmd)
60
+ output = Xolo::BLANK
61
+ errs = Xolo::BLANK
62
+
63
+ Open3.popen3("#{SEC_COMMAND} -i") do |stdin, stdout, stderr, wait_thr|
64
+ # pid = wait_thr.pid # pid of the started process.
65
+ stdin.puts cmd
66
+ stdin.close
67
+
68
+ output = stdout.read
69
+ errs = stderr.read
70
+
71
+ @security_exit_status = wait_thr.value # Process::Status object returned.
72
+ end
73
+
74
+ # exit 44 is 'The specified item could not be found in the keychain'
75
+ return output.chomp if @security_exit_status.success?
76
+
77
+ case @security_exit_status.exitstatus
78
+ when SEC_STATUS_AUTH_ERROR
79
+ raise Xolo::KeychainError, 'Problem accessing login keychain. Is it locked?'
80
+
81
+ when SEC_STATUS_NOT_FOUND_ERROR
82
+ raise Xolo::NoSuchItemError, "No xolo admin password. Please run 'xadm config'"
83
+
84
+ else
85
+ errs.chomp!
86
+ errs =~ /: returned\s+(-?\d+)$/
87
+ errnum = Regexp.last_match(1)
88
+ desc = errnum ? security_error_desc(errnum) : errs
89
+ desc ||= errs
90
+ raise Xolo::KeychainError, "#{desc.gsub("\n", '; ')}; exit status #{@security_exit_status.exitstatus}"
91
+ end # case
92
+ end # run_security
93
+
94
+ # use `security error` to get a description of an error number
95
+ ##############
96
+ def security_error_desc(num)
97
+ desc = `#{SEC_COMMAND} error #{num}`
98
+ return if desc.include?('unknown error')
99
+
100
+ desc.chomp.split(num).last
101
+ rescue
102
+ nil
103
+ end
104
+
105
+ # given a string, wrap it in single quotes and escape internal single quotes
106
+ # and backslashes so it can be used in the interactive 'security' command
107
+ #
108
+ # @param str[String] the string to escape
109
+ #
110
+ # @return [String] the escaped string
111
+ ###################
112
+ def security_escape(str)
113
+ # first escape backslashes
114
+ str = str.to_s.gsub '\\', '\\\\\\'
115
+
116
+ # then single quotes
117
+ str.gsub! "'", "\\\\'"
118
+
119
+ # if other things need escaping, add them here
120
+
121
+ "'#{str}'"
122
+ end # security_escape
123
+
124
+ end # module Prefs
125
+
126
+ end # module Admin
127
+
128
+ end # module Xolo
@@ -0,0 +1,21 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # main module
9
+ module Xolo
10
+
11
+ module Core
12
+
13
+ module Version
14
+
15
+ VERSION = '2.0.2'.freeze
16
+
17
+ end
18
+
19
+ end
20
+
21
+ end # module