xolo-admin 1.0.0
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +177 -0
- data/README.md +5 -0
- data/bin/xadm +114 -0
- data/lib/xolo/admin/command_line.rb +432 -0
- data/lib/xolo/admin/configuration.rb +196 -0
- data/lib/xolo/admin/connection.rb +191 -0
- data/lib/xolo/admin/cookie_jar.rb +81 -0
- data/lib/xolo/admin/credentials.rb +212 -0
- data/lib/xolo/admin/highline_terminal.rb +81 -0
- data/lib/xolo/admin/interactive.rb +762 -0
- data/lib/xolo/admin/jamf_pro.rb +75 -0
- data/lib/xolo/admin/options.rb +1139 -0
- data/lib/xolo/admin/processing.rb +1329 -0
- data/lib/xolo/admin/progress_history.rb +117 -0
- data/lib/xolo/admin/title.rb +285 -0
- data/lib/xolo/admin/title_editor.rb +57 -0
- data/lib/xolo/admin/validate.rb +1032 -0
- data/lib/xolo/admin/version.rb +221 -0
- data/lib/xolo/admin.rb +139 -0
- data/lib/xolo-admin.rb +8 -0
- metadata +139 -0
|
@@ -0,0 +1,191 @@
|
|
|
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 Admin
|
|
14
|
+
|
|
15
|
+
# connection to the xolo server from xadm
|
|
16
|
+
module Connection
|
|
17
|
+
|
|
18
|
+
# Constants
|
|
19
|
+
##############################
|
|
20
|
+
##############################
|
|
21
|
+
|
|
22
|
+
TIMEOUT = 300
|
|
23
|
+
OPEN_TIMEOUT = 10
|
|
24
|
+
|
|
25
|
+
PING_ROUTE = '/ping'
|
|
26
|
+
PING_RESPONSE = 'pong'
|
|
27
|
+
|
|
28
|
+
LOGIN_ROUTE = '/auth/login'
|
|
29
|
+
|
|
30
|
+
# Module methods
|
|
31
|
+
##############################
|
|
32
|
+
##############################
|
|
33
|
+
|
|
34
|
+
# when this module is included
|
|
35
|
+
def self.included(includer)
|
|
36
|
+
Xolo.verbose_include includer, self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Instance Methods
|
|
40
|
+
##############################
|
|
41
|
+
##############################
|
|
42
|
+
|
|
43
|
+
# Authenticate to the Xolo server and get a session token
|
|
44
|
+
# (maintained by Faraday via the Xolo::Admin::CookieJar middleware)
|
|
45
|
+
#
|
|
46
|
+
##############
|
|
47
|
+
def login(test: false)
|
|
48
|
+
return if !test && cmd_details[:no_login]
|
|
49
|
+
|
|
50
|
+
hostname = config.hostname
|
|
51
|
+
admin = config.admin
|
|
52
|
+
pw = fetch_pw
|
|
53
|
+
xadm = Xolo::Admin::EXECUTABLE_FILENAME
|
|
54
|
+
|
|
55
|
+
raise Xolo::MissingDataError, "No xolo server hostname. Please run '#{xadm} config'" unless hostname
|
|
56
|
+
raise Xolo::MissingDataError, "No xolo admin username. Please run '#{xadm} config'" unless admin
|
|
57
|
+
|
|
58
|
+
payload = { admin: admin, password: pw }
|
|
59
|
+
|
|
60
|
+
payload[:proxy_admin] = global_opts[:proxy_admin] if global_opts[:proxy_admin]
|
|
61
|
+
|
|
62
|
+
# provide the hostname to make a persistent Faraday connection object
|
|
63
|
+
# so in the future we just call server_cnx with no hostname to get the same
|
|
64
|
+
# connection object
|
|
65
|
+
resp = server_cnx.post Xolo::Admin::Connection::LOGIN_ROUTE, payload
|
|
66
|
+
|
|
67
|
+
if resp.success?
|
|
68
|
+
@logged_in = true
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
case resp.status
|
|
73
|
+
when 401
|
|
74
|
+
raise Xolo::AuthenticationError, resp.body[:error]
|
|
75
|
+
else
|
|
76
|
+
raise Xolo::ServerError, "#{resp.status}: #{resp.body}"
|
|
77
|
+
end
|
|
78
|
+
rescue Faraday::UnauthorizedError
|
|
79
|
+
raise Xolo::AuthenticationError, 'Invalid username or password'
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @pararm host [String] an alternate hostname to use, defaults to config.hostname
|
|
83
|
+
#
|
|
84
|
+
# @return [URI] The server base URL
|
|
85
|
+
#################
|
|
86
|
+
def server_url(host: nil)
|
|
87
|
+
@server_url = nil if host
|
|
88
|
+
return @server_url if @server_url
|
|
89
|
+
|
|
90
|
+
@server_url = URI.parse "https://#{host || config.hostname}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A connection for requests without any file uploads
|
|
94
|
+
#
|
|
95
|
+
# None of our GET routes expected any request body, so it doesn't matter if
|
|
96
|
+
# its set to be JSON.
|
|
97
|
+
#
|
|
98
|
+
# For our POST routes that dont upload files (e.g. setloglevel), the request
|
|
99
|
+
# body, if any, will be JSON.
|
|
100
|
+
#
|
|
101
|
+
# @param host [String] The hostname of the Xolo server. Must be provided the
|
|
102
|
+
# first time this is called (usually for logging in)
|
|
103
|
+
#
|
|
104
|
+
# @return [Faraday::Connection]
|
|
105
|
+
##################################
|
|
106
|
+
def server_cnx(host: nil)
|
|
107
|
+
@server_cnx = nil if host
|
|
108
|
+
return @server_cnx if @server_cnx
|
|
109
|
+
|
|
110
|
+
@server_cnx = Faraday.new(server_url(host: host)) do |cnx|
|
|
111
|
+
cnx.options[:timeout] = TIMEOUT
|
|
112
|
+
cnx.options[:open_timeout] = OPEN_TIMEOUT
|
|
113
|
+
|
|
114
|
+
cnx.request :json
|
|
115
|
+
|
|
116
|
+
cnx.use Xolo::Admin::CookieJar
|
|
117
|
+
|
|
118
|
+
cnx.response :json, parser_options: { symbolize_names: true }
|
|
119
|
+
cnx.response :raise_error
|
|
120
|
+
|
|
121
|
+
cnx.adapter :net_http
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @server_cnx.headers['X-Proxy-Admin'] = global_opts[:proxy_admin] if global_opts[:proxy_admin]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A connection for responses that stream the progress of a long server
|
|
128
|
+
# process.
|
|
129
|
+
#
|
|
130
|
+
# @param host [String] The hostname of the Xolo server. Must be provided the
|
|
131
|
+
# first time this is called (usually for logging in)
|
|
132
|
+
#
|
|
133
|
+
# @return [Faraday::Connection]
|
|
134
|
+
##################################
|
|
135
|
+
def streaming_cnx(host: nil)
|
|
136
|
+
@streaming_cnx = nil if host
|
|
137
|
+
return @streaming_cnx if @streaming_cnx
|
|
138
|
+
|
|
139
|
+
# this proc every time we get a chunk, just print it to stdout
|
|
140
|
+
# and make note if any of them conain an error
|
|
141
|
+
streaming_proc = proc do |chunk, _size, _env|
|
|
142
|
+
puts chunk
|
|
143
|
+
@streaming_error ||= chunk.include? STREAMING_OUTPUT_ERROR
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
req_opts = { on_data: streaming_proc }
|
|
147
|
+
|
|
148
|
+
@streaming_cnx = Faraday.new(server_url(host: host), request: req_opts) do |cnx|
|
|
149
|
+
cnx.options[:timeout] = TIMEOUT
|
|
150
|
+
cnx.options[:open_timeout] = OPEN_TIMEOUT
|
|
151
|
+
cnx.use Xolo::Admin::CookieJar
|
|
152
|
+
cnx.response :raise_error
|
|
153
|
+
cnx.adapter :net_http
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# A connection for POST requests with file uploads
|
|
158
|
+
#
|
|
159
|
+
# The request body will be multipart/url-encoded
|
|
160
|
+
# and authentication is required
|
|
161
|
+
#
|
|
162
|
+
# @param host [String] The hostname of the Xolo server. Must be provided the
|
|
163
|
+
# first time this is called (usually for logging in)
|
|
164
|
+
#
|
|
165
|
+
# @return [Faraday::Connection]
|
|
166
|
+
##################################
|
|
167
|
+
def upload_cnx(host: nil)
|
|
168
|
+
@upload_cnx = nil if host
|
|
169
|
+
return @upload_cnx if @upload_cnx
|
|
170
|
+
|
|
171
|
+
@upload_cnx = Faraday.new(server_url(host: host)) do |cnx|
|
|
172
|
+
cnx.options[:timeout] = TIMEOUT
|
|
173
|
+
cnx.options[:open_timeout] = OPEN_TIMEOUT
|
|
174
|
+
|
|
175
|
+
cnx.request :multipart
|
|
176
|
+
cnx.request :url_encoded
|
|
177
|
+
|
|
178
|
+
cnx.use Xolo::Admin::CookieJar
|
|
179
|
+
|
|
180
|
+
cnx.response :json, parser_options: { symbolize_names: true }
|
|
181
|
+
cnx.response :raise_error
|
|
182
|
+
|
|
183
|
+
cnx.adapter :net_http
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
end # module Connection
|
|
188
|
+
|
|
189
|
+
end # module Admin
|
|
190
|
+
|
|
191
|
+
end # module Xolo
|
|
@@ -0,0 +1,81 @@
|
|
|
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 Admin
|
|
14
|
+
|
|
15
|
+
# Faraday Middleware for storing and sending the only cookie we
|
|
16
|
+
# use with the Xolo server.
|
|
17
|
+
#
|
|
18
|
+
# See https://lostisland.github.io/faraday/#/middleware/custom-middleware
|
|
19
|
+
#
|
|
20
|
+
class CookieJar < Faraday::Middleware
|
|
21
|
+
|
|
22
|
+
# Constants
|
|
23
|
+
##########################
|
|
24
|
+
##########################
|
|
25
|
+
|
|
26
|
+
COOKIE_HEADER = 'Cookie'
|
|
27
|
+
SET_COOKIE_HEADER = 'set-cookie'
|
|
28
|
+
SESSION_COOKIE_NAME = 'rack.session'
|
|
29
|
+
SESSION_COOKIE_EXPIRES_NAME = 'expires'
|
|
30
|
+
|
|
31
|
+
# Class Methods
|
|
32
|
+
##########################
|
|
33
|
+
##########################
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
|
|
37
|
+
# runtime storage for our single cookie
|
|
38
|
+
attr_accessor :session_cookie, :session_expires
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Instance Methods
|
|
43
|
+
##########################
|
|
44
|
+
##########################
|
|
45
|
+
|
|
46
|
+
# we only send back the rack.session cookie, as long as it
|
|
47
|
+
# exists and hasn't expired (it lasts an hour)
|
|
48
|
+
#####################
|
|
49
|
+
def on_request(env)
|
|
50
|
+
return unless self.class.session_cookie && self.class.session_expires
|
|
51
|
+
|
|
52
|
+
raise Xolo::InvalidTokenError, 'Server Session Expired' if Time.now > self.class.session_expires
|
|
53
|
+
|
|
54
|
+
env[:request_headers][COOKIE_HEADER] = "#{SESSION_COOKIE_NAME}=#{self.class.session_cookie}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The server only ever sends one cookie,
|
|
58
|
+
# and we only care about 2 values:
|
|
59
|
+
# rack.session, and expires
|
|
60
|
+
####################
|
|
61
|
+
def on_complete(env)
|
|
62
|
+
raw_cookie = env[:response_headers][SET_COOKIE_HEADER]
|
|
63
|
+
return unless raw_cookie
|
|
64
|
+
|
|
65
|
+
tepid_cookie = raw_cookie.split(Xolo::SEMICOLON_SEP_RE)
|
|
66
|
+
tepid_cookie.each do |part|
|
|
67
|
+
name, value = part.split('=')
|
|
68
|
+
case name
|
|
69
|
+
when SESSION_COOKIE_NAME
|
|
70
|
+
self.class.session_cookie = value
|
|
71
|
+
when SESSION_COOKIE_EXPIRES_NAME
|
|
72
|
+
self.class.session_expires = Time.parse(value).localtime
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
end # module Converters
|
|
78
|
+
|
|
79
|
+
end # module Admin
|
|
80
|
+
|
|
81
|
+
end # module Xolo
|
|
@@ -0,0 +1,212 @@
|
|
|
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 Admin
|
|
14
|
+
|
|
15
|
+
# Personal credentials for users of 'xadm', stored in the login keychain
|
|
16
|
+
#
|
|
17
|
+
module Credentials
|
|
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
|
+
# The 'kind' of item in the keychain
|
|
36
|
+
XOLO_CREDS_KIND = 'Xolo::Admin::Password'
|
|
37
|
+
|
|
38
|
+
# the Service for the generic 'Xolo::Admin::Credentials' keychain entry
|
|
39
|
+
XOLO_CREDS_SVC = 'com.pixar.xolo.password'
|
|
40
|
+
|
|
41
|
+
# the Label for the generic 'Xolo::Admin::Credentials' keychain entry
|
|
42
|
+
XOLO_CREDS_LBL = '"Xolo Admin Password"'
|
|
43
|
+
|
|
44
|
+
# Module methods
|
|
45
|
+
##############################
|
|
46
|
+
##############################
|
|
47
|
+
|
|
48
|
+
# when this module is included
|
|
49
|
+
def self.included(includer)
|
|
50
|
+
Xolo.verbose_include includer, self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Instance Methods
|
|
54
|
+
##########################
|
|
55
|
+
##########################
|
|
56
|
+
|
|
57
|
+
# If the keychain is not accessible, prompt for the password
|
|
58
|
+
#
|
|
59
|
+
# @return [String] Get the admin's password from the login keychain
|
|
60
|
+
#
|
|
61
|
+
##############################################
|
|
62
|
+
def fetch_pw
|
|
63
|
+
return config.data_from_command_file_or_string(config.pw, enforce_secure_mode: true) if config.no_gui
|
|
64
|
+
|
|
65
|
+
cmd = ['find-generic-password']
|
|
66
|
+
cmd << '-s'
|
|
67
|
+
cmd << XOLO_CREDS_SVC
|
|
68
|
+
cmd << '-l'
|
|
69
|
+
cmd << XOLO_CREDS_LBL
|
|
70
|
+
cmd << '-w'
|
|
71
|
+
run_security(cmd.map { |i| security_escape i }.join(' '))
|
|
72
|
+
|
|
73
|
+
# If we can't access the keychain, prompt for the password. This is usually
|
|
74
|
+
# when we're running in a non-GUI session, e.g. via ssh.
|
|
75
|
+
rescue Xolo::KeychainError
|
|
76
|
+
raise unless @security_exit_status.exitstatus == SEC_STATUS_NO_GUI_ERROR
|
|
77
|
+
raise unless STDOUT.isatty
|
|
78
|
+
|
|
79
|
+
question = "Keychain not accessible.\nPlease enter the xolo admin password for #{config.admin}: "
|
|
80
|
+
highline_cli.ask(question) do |q|
|
|
81
|
+
q.echo = false
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Store an item in the default keychain
|
|
86
|
+
#
|
|
87
|
+
# @param acct [String] The username for the password.
|
|
88
|
+
# xadm doesn't use this, it uses the admin name from the
|
|
89
|
+
# configuration. But the keychain item requires a value here.
|
|
90
|
+
#
|
|
91
|
+
# @param pw [String] The password to store
|
|
92
|
+
#
|
|
93
|
+
# @return [String] the location where the password is stored
|
|
94
|
+
##############################################
|
|
95
|
+
def store_pw(acct, pw)
|
|
96
|
+
# delete the item first if its there
|
|
97
|
+
delete_pw
|
|
98
|
+
|
|
99
|
+
cmd = ['add-generic-password']
|
|
100
|
+
cmd << '-a'
|
|
101
|
+
cmd << acct
|
|
102
|
+
cmd << '-s'
|
|
103
|
+
cmd << XOLO_CREDS_SVC
|
|
104
|
+
cmd << '-w'
|
|
105
|
+
cmd << pw
|
|
106
|
+
cmd << '-l'
|
|
107
|
+
cmd << XOLO_CREDS_LBL
|
|
108
|
+
cmd << '-D'
|
|
109
|
+
cmd << XOLO_CREDS_KIND
|
|
110
|
+
|
|
111
|
+
run_security(cmd.map { |i| security_escape i }.join(' '))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# delete the xolo admin password from the login keychain
|
|
115
|
+
# @return [void]
|
|
116
|
+
##############################################
|
|
117
|
+
def delete_pw
|
|
118
|
+
cmd = ['delete-generic-password']
|
|
119
|
+
cmd << '-s'
|
|
120
|
+
cmd << XOLO_CREDS_SVC
|
|
121
|
+
cmd << '-l'
|
|
122
|
+
cmd << XOLO_CREDS_LBL
|
|
123
|
+
|
|
124
|
+
run_security(cmd.map { |i| security_escape i }.join(' '))
|
|
125
|
+
rescue Xolo::NoSuchItemError
|
|
126
|
+
nil
|
|
127
|
+
rescue RuntimeError => e
|
|
128
|
+
raise e unless e.to_s == 'No matching keychain item was found'
|
|
129
|
+
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Run the security command in interactive mode on a given keychain,
|
|
134
|
+
# passing in a subcommand and its arguments. so that they don't appear in the
|
|
135
|
+
# `ps` output
|
|
136
|
+
#
|
|
137
|
+
# @param cmd [String] the subcommand being passed to 'security' with
|
|
138
|
+
# all needed options. It will not be visible outide this process, so
|
|
139
|
+
# its OK to put passwords into the options.
|
|
140
|
+
#
|
|
141
|
+
# @return [String] the stdout of the 'security' command.
|
|
142
|
+
#
|
|
143
|
+
######
|
|
144
|
+
def run_security(cmd)
|
|
145
|
+
output = Xolo::BLANK
|
|
146
|
+
errs = Xolo::BLANK
|
|
147
|
+
|
|
148
|
+
Open3.popen3("#{SEC_COMMAND} -i") do |stdin, stdout, stderr, wait_thr|
|
|
149
|
+
# pid = wait_thr.pid # pid of the started process.
|
|
150
|
+
stdin.puts cmd
|
|
151
|
+
stdin.close
|
|
152
|
+
|
|
153
|
+
output = stdout.read
|
|
154
|
+
errs = stderr.read
|
|
155
|
+
|
|
156
|
+
@security_exit_status = wait_thr.value # Process::Status object returned.
|
|
157
|
+
end
|
|
158
|
+
# exit 44 is 'The specified item could not be found in the keychain'
|
|
159
|
+
return output.chomp if @security_exit_status.success?
|
|
160
|
+
|
|
161
|
+
case @security_exit_status.exitstatus
|
|
162
|
+
when SEC_STATUS_AUTH_ERROR
|
|
163
|
+
raise Xolo::KeychainError, 'Problem accessing login keychain. Is it locked?'
|
|
164
|
+
|
|
165
|
+
when SEC_STATUS_NOT_FOUND_ERROR
|
|
166
|
+
raise Xolo::NoSuchItemError, "No xolo admin password. Please run 'xadm config'"
|
|
167
|
+
|
|
168
|
+
else
|
|
169
|
+
errs.chomp!
|
|
170
|
+
errs =~ /: returned\s+(-?\d+)$/
|
|
171
|
+
errnum = Regexp.last_match(1)
|
|
172
|
+
desc = errnum ? security_error_desc(errnum) : errs
|
|
173
|
+
desc ||= errs
|
|
174
|
+
raise Xolo::KeychainError, "#{desc.gsub("\n", '; ')}; exit status #{@security_exit_status.exitstatus}"
|
|
175
|
+
end # case
|
|
176
|
+
end # run_security
|
|
177
|
+
|
|
178
|
+
# use `security error` to get a description of an error number
|
|
179
|
+
##############
|
|
180
|
+
def security_error_desc(num)
|
|
181
|
+
desc = `#{SEC_COMMAND} error #{num}`
|
|
182
|
+
return if desc.include?('unknown error')
|
|
183
|
+
|
|
184
|
+
desc.chomp.split(num).last
|
|
185
|
+
rescue StandardError
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# given a string, wrap it in single quotes and escape internal single quotes
|
|
190
|
+
# and backslashes so it can be used in the interactive 'security' command
|
|
191
|
+
#
|
|
192
|
+
# @param str[String] the string to escape
|
|
193
|
+
#
|
|
194
|
+
# @return [String] the escaped string
|
|
195
|
+
###################
|
|
196
|
+
def security_escape(str)
|
|
197
|
+
# first escape backslashes
|
|
198
|
+
str = str.to_s.gsub '\\', '\\\\\\'
|
|
199
|
+
|
|
200
|
+
# then single quotes
|
|
201
|
+
str.gsub! "'", "\\\\'"
|
|
202
|
+
|
|
203
|
+
# if other things need escaping, add them here
|
|
204
|
+
|
|
205
|
+
"'#{str}'"
|
|
206
|
+
end # security_escape
|
|
207
|
+
|
|
208
|
+
end # module Prefs
|
|
209
|
+
|
|
210
|
+
end # module Admin
|
|
211
|
+
|
|
212
|
+
end # module Xolo
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
# MonkeyPatch HighLine::Terminal#readline_read so that Readline
|
|
11
|
+
# lines can be case-insensitive, and have a prompt.
|
|
12
|
+
#
|
|
13
|
+
# To use a prompt, put it in the environtment variable 'XADM_HIGHLINE_READLINE_PROMPT'
|
|
14
|
+
#
|
|
15
|
+
# To make the readline completion case-insensitive, set the environment
|
|
16
|
+
# variable XADM_HIGHLINE_READLINE_CASE_INSENSITIVE to anything.
|
|
17
|
+
#
|
|
18
|
+
# This really only modifies the Regexp used for the completion_proc to make it
|
|
19
|
+
# case insensitive if desired (adding an 'i' to the end)
|
|
20
|
+
# and sets the prompt when calling Readline.readline
|
|
21
|
+
#
|
|
22
|
+
# TODO: Do this 'smartly' as with the monkeypatches in pixar-ruby-extensions
|
|
23
|
+
#
|
|
24
|
+
class HighLine
|
|
25
|
+
|
|
26
|
+
class Terminal
|
|
27
|
+
|
|
28
|
+
# Use readline to read one line
|
|
29
|
+
# @param question [HighLine::Question] question from where to get
|
|
30
|
+
# autocomplete candidate strings
|
|
31
|
+
#################################
|
|
32
|
+
def readline_read(question)
|
|
33
|
+
# prep auto-completion
|
|
34
|
+
unless question.selection.empty?
|
|
35
|
+
Readline.completion_proc = lambda do |str|
|
|
36
|
+
regex = ENV['XADM_HIGHLINE_READLINE_CASE_INSENSITIVE'] ? /\A#{Regexp.escape(str)}/i : /\A#{Regexp.escape(str)}/
|
|
37
|
+
question.selection.grep(regex)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# work-around ugly readline() warnings
|
|
42
|
+
old_verbose = $VERBOSE
|
|
43
|
+
$VERBOSE = nil
|
|
44
|
+
|
|
45
|
+
raw_answer = run_preserving_stty do
|
|
46
|
+
Readline.readline(ENV['XADM_HIGHLINE_READLINE_PROMPT'].to_s, true)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
$VERBOSE = old_verbose
|
|
50
|
+
|
|
51
|
+
raw_answer
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get one line from terminal using default #gets method.
|
|
55
|
+
##############################
|
|
56
|
+
# def get_line_default(highline)
|
|
57
|
+
# raise EOFError, 'The input stream is exhausted.' if highline.track_eof? && highline.input.eof?
|
|
58
|
+
|
|
59
|
+
# highline.output.print "#{ENV['XADM_HIGHLINE_LINE_PROMPT']}" if ENV['XADM_HIGHLINE_LINE_PROMPT']
|
|
60
|
+
|
|
61
|
+
# highline.input.gets
|
|
62
|
+
# end
|
|
63
|
+
|
|
64
|
+
end # terminal
|
|
65
|
+
|
|
66
|
+
# # Deals with the task of "asking" a question
|
|
67
|
+
# class QuestionAsker
|
|
68
|
+
|
|
69
|
+
# alias ask_once_real ask_once
|
|
70
|
+
|
|
71
|
+
# # Gets just one answer, as opposed to #gather_answers
|
|
72
|
+
# #
|
|
73
|
+
# # @return [String] answer
|
|
74
|
+
# def ask_once
|
|
75
|
+
# @highline.output.print "#{ENV['XADM_HIGHLINE_LINE_PROMPT']}" if ENV['XADM_HIGHLINE_LINE_PROMPT']
|
|
76
|
+
# ask_once_real
|
|
77
|
+
# end
|
|
78
|
+
|
|
79
|
+
# end # question asker
|
|
80
|
+
|
|
81
|
+
end
|