Module: PluginTool

Defined in:
lib/auth.rb,
lib/misc.rb,
lib/check.rb,
lib/custom.rb,
lib/plugin.rb,
lib/server.rb,
lib/packing.rb,
lib/manifest.rb,
lib/minimise.rb,
lib/watchers.rb,
lib/new_plugin.rb,
lib/local_config.rb,
lib/notifications.rb,
lib/syntax_checking.rb,
lib/schema_requirements.rb

Defined Under Namespace

Modules: LocalConfig Classes: CustomBehaviour, Minimiser, Plugin, SchemaRequirements, WatcherPoll

Constant Summary collapse

LOCAL_CUSTOM_BEHAVIOUR_FILENAME =
"server.behaviour.rb"
TRUSTED_CODE_DIGESTS_FILENAME =

Digests of trusted code are stored outside the source code repo, so it can’t be written by the repo contents

"~/.haplo-plugin-tool-trusted.json"
PACKING_ACCEPTABLE_FILENAME =
/\A(js|static|template|test|data)\/([a-z0-9_-]+\/)*[a-z0-9_-]+\.[a-z0-9]+\z/
PACKING_ACCEPTABLE_EXCEPTIONS =
['plugin.json', 'requirements.schema', 'global.js', 'certificates-temp-http-api.pem']
ALLOWED_PLUGIN_DIRS =

NOTE: Also update packing.rb if this changes

['js', 'static', 'template', 'test', 'data']
Context =
Java::OrgMozillaJavascript::Context
@@keys_pathname =
"#{Dir.getwd}/.server.json"
@@custom =
CustomBehaviour.new
@@http =
nil
@@notification_options =
nil
@@notification_have_suppressed_first_system_audit_entry =
false
@@notification_announce_reconnect =
false
@@syntax_check_queue =
[]
@@syntax_check_queue_lock =
Mutex.new
@@syntax_check_queue_semaphore =
Mutex.new
@@syntax_check_queue_semaphore_var =
ConditionVariable.new

Class Method Summary collapse

Class Method Details

.beepObject



4
5
6
# File 'lib/misc.rb', line 4

def self.beep
  $stdout.write("\07")
end

.check_file_result(plugin, name, result) ⇒ Object



48
49
50
51
52
53
54
55
# File 'lib/check.rb', line 48

def self.check_file_result(plugin, name, result)
  line = "  #{plugin.plugin_dir}/#{name}: #{result}"
  @@check_report << line
  @@check_report << "\n"
  @@check_ok = false if result == :FAIL
  @@check_warn = true if result == :WARN
  puts line
end

.check_for_certificate_fileObject



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/server.rb', line 14

def self.check_for_certificate_file
  @@server_ca = nil
  hostname_parts = (@@server_hostname || '').split('.')
  search_list = (0..hostname_parts.length).map do |n|
    filename = "server"
    search_parts = hostname_parts[n..hostname_parts.length]
    filename << '.' unless search_parts.empty?
    filename << search_parts.join('.')
    filename << ".crt"
  end
  @@server_ca = search_list.find { |f| File.file?(f) }
  if @@server_ca
    puts "NOTICE: Using alternative CAs for SSL from #{@@server_ca}"
  else
    # Use build in certificate bundle
    @@server_ca = "#{File.dirname(__FILE__)}/CertificateBundle.pem"
  end
end

.check_plugin(plugin) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/check.rb', line 27

def self.check_plugin(plugin)
  plugin_dir = plugin.plugin_dir
  Dir.glob("#{plugin_dir}/**/*").each do |pathname|
    next unless File.file?(pathname)
    plugin_relative_name = pathname[plugin_dir.length+1, pathname.length]
    if pathname =~ /\.js\z/
      # Check JavaScript
      report = syntax_check_one_file(plugin, plugin_relative_name)
      if report == nil
        check_file_result(plugin, plugin_relative_name, :OK)
      else
        puts "**** #{plugin_relative_name} has errors:\n#{report}\n"
        check_file_result(plugin, plugin_relative_name, (plugin_relative_name =~ /\Ajs\//) ? :FAIL : :WARN)
      end
    else
      # TODO: Checks for other file types, including the plugin.json
      check_file_result(plugin, plugin_relative_name, :OK)        
    end
  end
end

.check_plugins(plugins) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/check.rb', line 4

def self.check_plugins(plugins)
  init_syntax_checking
  @@check_report = ''
  @@check_ok = true
  @@check_warn = false
  plugins.each { |p| check_plugin(p) }
  if @@check_warn || !(@@check_ok)
    # Output the report again because it may have been obscured by the errors
    puts "--------------------"
    puts @@check_report
  end
  # Output the verdict
  puts
  if @@check_ok && !@@check_warn
    puts "PASSED"
  elsif @@check_warn
    puts "PASSED WITH WARNINGS"
  else
    puts "FAILED"
    exit 1
  end
end

.cmd_auth(options) ⇒ Object




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
# File 'lib/auth.rb', line 39

def self.cmd_auth(options)
  end_on_error "No hostname given on command line" if options.args.empty?
  hostname, port, url_base, server_name = parse_hostname_with_port(options.args.first)

  keys = load_keys_file()
  if keys['keys'].has_key?(server_name) && !options.force
    puts
    puts "Already authorised with #{server_name}"
    puts "Use the --force argument to reauthorise with the server."
    return
  end

  set_server(hostname, port, nil)
  check_for_certificate_file()
  http = get_http()

  this_hostname = java.net.InetAddress.getLocalHost().getHostName() || "unknown"

  puts "Requesting token from #{url_base} ..."
  start_auth_path = "/api/plugin-tool-auth/start-auth?name=#{URI.encode(this_hostname)}"
  request = Net::HTTP::Get.new(start_auth_path)
  setup_request(request)
  token = nil
  begin
    response = http.request(request)
    end_on_error "Server returned an error. Check hostname and port." unless response.code == "200"
    parsed_json = JSON.parse(response.body)
    token = parsed_json['token']
    end_on_error "Server doesn't look like a ONEIS server with plugin debugging enabled" unless parsed_json['ONEIS'] == 'plugin-tool-auth' && token
  rescue => e
    end_on_error "Failed to start authorisation process. Check hostname and port."
  end

  # Check token looks OK so we don't form dodgy URLs
  end_on_error "Bad token" unless token =~ /\A[a-z0-9A-Z_-]+\z/

  user_url = "#{url_base}/do/plugin-tool-auth/create/#{token}"
  poll_path = "/api/plugin-tool-auth/poll/#{token}"

  puts
  if java.lang.System.getProperty("os.name") == 'Mac OS X'
    puts "Attempting to open the following URL in your browser."
    puts "If the browser does not open, please visit this URL in your browser."
    system "open #{user_url}"
  else
    puts "Please visit this URL in your browser, and authenticate if necessary."
  end
  puts "  #{user_url}"
  puts

  # Poll for a few minutes, waiting for the user to authenticate
  puts "Waiting for server to authorise..."
  poll_count = 0
  key = nil
  while poll_count < 60 && !key
    delay = if poll_count < 10
      2
    elsif poll_count < 20
      4
    else
      8
    end
    sleep delay
    begin
      request = Net::HTTP::Get.new(poll_path)
      setup_request(request)
      response = http.request(request)
      parsed_json = JSON.parse(response.body)
      case parsed_json['status']
      when 'wait'
        # poll again
      when 'available'
        key = parsed_json['key']
      else
        end_on_error "Authorisation process failed."
      end
    rescue => e
      end_on_error "Error communicating with server"
    end
  end
  finish_with_connection()

  end_on_error "Didn't managed to authorise with server." unless key

  puts "Successfully authorised with server."

  keys['default'] = server_name
  keys['keys'][server_name] = key
  save_keys_file(keys)

  puts
  puts "Key stored in #{@@keys_pathname}"
  puts "#{server_name} selected as default server."
end

.cmd_server(options) ⇒ Object




27
28
29
30
31
32
33
34
35
# File 'lib/auth.rb', line 27

def self.cmd_server(options)
  end_on_error "No server name substring given on command line" if options.args.empty?
  keys = load_keys_file()
  server = select_server(keys, options.args.first)
  end_on_error "No server found for substring '#{options.args.first}" unless server
  keys['default'] = server
  puts "Selected server #{server}"
  save_keys_file(keys)
end

.custom_behaviourObject



22
23
24
# File 'lib/custom.rb', line 22

def self.custom_behaviour
  @@custom
end

.decode_and_handle_notifications(encoded) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
# File 'lib/notifications.rb', line 49

def self.decode_and_handle_notifications(encoded)
  size = encoded.length
  pos = 0
  while pos < (size - 12)
    type = encoded[pos, 4]
    data_size = encoded[pos + 4, 8].to_i(16)
    data = encoded[pos + 12, data_size]
    pos += 12 + data_size
    handle_notification(type, data)
  end
end

.determine_manifest_changes(from, to) ⇒ Object



32
33
34
35
36
37
38
39
40
41
# File 'lib/manifest.rb', line 32

def self.determine_manifest_changes(from, to)
  changes = []
  from.each_key do |name|
    changes << [name, :delete] unless to.has_key?(name)
  end
  to.each do |name,hash|
    changes << [name, hash] unless from[name] == hash
  end
  changes
end

.do_notificationsObject



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/notifications.rb', line 24

def self.do_notifications
  http = make_http_connection
  puts "NOTICE: Notification connection established." if @@notification_announce_reconnect
  while true
    sleep(0.25) # small throttle of requests
    path = '/api/development-plugin-loader/get-notifications'
    path << "?queue=#{@@notification_queue_name}" if @@notification_queue_name
    request = Net::HTTP::Get.new(path)
    setup_request(request)
    # Server uses long-polling, and will respond after a long timeout, or when a notification
    # has been queued for sending to this process.
    response = http.request(request)
    if response.kind_of?(Net::HTTPOK)
      @@notification_queue_name = response['X-Queue-Name']
      begin
        decode_and_handle_notifications response.body
      rescue
        puts "NOTICE: Error handling notification from server."
      end
    else
      raise "Bad response"
    end
  end
end

.do_syntax_checkingObject



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/syntax_checking.rb', line 80

def self.do_syntax_checking
  init_syntax_checking
  while(true)
    plugin, file = @@syntax_check_queue_lock.synchronize do
      @@syntax_check_queue.shift
    end
    if file == nil
      # Wait for another file
      @@syntax_check_queue_semaphore.synchronize { @@syntax_check_queue_semaphore_var.wait(@@syntax_check_queue_semaphore) }
    else
      report = syntax_check_one_file(plugin, file)
      if report != nil
        puts "\nJavaScript file #{plugin.plugin_dir}/#{file} has syntax errors:\n#{report}\n\n"
        beep
      end
    end
  end
end

.finish_with_connectionObject



56
57
58
59
60
61
# File 'lib/server.rb', line 56

def self.finish_with_connection
  if @@http != nil
    @@http.finish
    @@http = nil
  end
end

.generate_manifest(directory) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/manifest.rb', line 7

def self.generate_manifest(directory)
  manifest = Hash.new
  Dir.glob("#{directory}/**/*").sort.each do |pathname|
    # Ignore directories
    next unless File.file? pathname
    # Ignore Emacs backup files
    next if pathname =~ /\~\z/
    # Check file
    filename = pathname.slice(directory.length + 1, pathname.length)
    raise "Bad filename for #{filename}" unless filename =~ /\A([a-zA-Z0-9_\/\-]+\/)?([a-z0-9_-]+\.[a-z0-9]+)\z/
    dir = $1
    name = $2
    if dir != nil
      dir = dir.gsub(/\/\z/,'')
      raise "Bad directory #{dir}" unless dir =~ /\A([a-zA-Z0-9_\-]+)[a-zA-Z0-9_\/\-]*\z/
      raise "Bad root directory #{$1}" unless ALLOWED_PLUGIN_DIRS.include?($1)
    end
    # Get hash of file
    digest = File.open(pathname) { |f| Digest::SHA256.hexdigest(f.read) }
    # And add to manifest
    manifest[filename] = digest
  end
  manifest
end

.get(path) ⇒ Object



68
69
70
71
72
73
# File 'lib/server.rb', line 68

def self.get(path)
  http = get_http
  request = Net::HTTP::Get.new(path)
  setup_request(request)
  http.request(request).body
end

.get_application_infoObject



139
140
141
# File 'lib/server.rb', line 139

def self.get_application_info
  @@server_application_info ||= JSON.parse(get("/api/development-plugin-loader/application-info"))
end

.get_httpObject



52
53
54
# File 'lib/server.rb', line 52

def self.get_http
  @@http ||= make_http_connection
end

.get_server_hostnameObject



10
11
12
# File 'lib/server.rb', line 10

def self.get_server_hostname
  @@server_hostname
end

.get_with_json_response(path) ⇒ Object



75
76
77
# File 'lib/server.rb', line 75

def self.get_with_json_response(path)
  report_errors_from_server(JSON.parse(get(path)))
end

.handle_notification(type, data) ⇒ Object



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
# File 'lib/notifications.rb', line 61

def self.handle_notification(type, data)
  case type
  when 'log '
    # Output from console.log()
    puts "LOG:#{data}"
  when 'audt'
    decoded = JSON.parse(data)
    kind = decoded.find { |name,value| name == 'auditEntryType' }.last
    if kind =~ /\A[A-Z\-]+\z/
      # System audit entry - suppressed by default
      unless @@notification_options.show_system_audit
        unless @@notification_have_suppressed_first_system_audit_entry
          @@notification_have_suppressed_first_system_audit_entry = true
          puts "NOTICE: System audit trail entries are not being shown. Run with --show-system-audit to display."
        end
        return
      end
    end
    puts "AUDIT -------------------------------------------"
    decoded.each do |key, value|
      puts sprintf("%22s: %s", key, value.to_s)
    end
  else
    puts "WARNING: Unknown notification received from server. Upgrade the plugin tool using 'jgem update oneis'."
    sleep(5) # throttle problematic responses
  end
end

.init_syntax_checkingObject



22
23
24
25
26
27
28
29
30
31
32
# File 'lib/syntax_checking.rb', line 22

def self.init_syntax_checking
  # Set up interpreter and get a syntax checker function
  raise "Another JS Context is active" unless nil == Context.getCurrentContext()
  @@cx = Context.enter();
  @@javascript_scope = @@cx.initStandardObjects();
  jshint = File.open("#{File.dirname(__FILE__)}/jshint.js") { |f| f.read }
  @@cx.evaluateString(@@javascript_scope, jshint, "<jshint.js>", 1, nil);
  testerfn = File.open("#{File.dirname(__FILE__)}/js_syntax_test.js") { |f| f.read }
  @@cx.evaluateString(@@javascript_scope, testerfn, "<js_syntax_test.js>", 1, nil);
  @@syntax_tester = @@javascript_scope.get("syntax_tester", @@javascript_scope);
end

.load_keys_fileObject




149
150
151
152
153
154
155
# File 'lib/auth.rb', line 149

def self.load_keys_file
  if File.exist?(@@keys_pathname)
    File.open(@@keys_pathname) { |f| JSON.parse(f.read) }
  else
    {"_" => "Contains server keys. DO NOT COMMIT TO SOURCE CONTROL.", "default" => nil, "keys" => {}}
  end
end

.make_http_connectionObject



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/server.rb', line 33

def self.make_http_connection
  http = Net::HTTP.new(@@server_hostname, @@server_port)
  ssl_ca = OpenSSL::X509::Store.new
  unless ssl_ca.respond_to? :add_file
    puts
    puts "jruby-openssl gem is not installed or bouncy castle crypto not available on CLASSPATH."
    puts "See installation instructions."
    puts
    exit 1
  end
  ssl_ca.add_file(@@server_ca)
  http.use_ssl = true
  http.verify_mode = OpenSSL::SSL::VERIFY_PEER
  http.cert_store = ssl_ca
  http.start
  http
end

.make_new_plugin(plugin_name) ⇒ Object



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
# File 'lib/new_plugin.rb', line 4

def self.make_new_plugin(plugin_name)
  unless plugin_name =~ /\A[a-z0-9_]+\z/ && plugin_name.length > 8
    end_on_error "Bad plugin name - must use a-z0-9_ only, and more than 8 characters."
  end
  if File.exist?(plugin_name)
    end_on_error "File or directory #{plugin_name} already exists"
  end
  FileUtils.mkdir(plugin_name)
  ['js', 'static', 'template', 'test'].each do |dir|
    FileUtils.mkdir("#{plugin_name}/#{dir}")
  end
  random = java.security.SecureRandom.new()
  rbytes = Java::byte[20].new
  random.nextBytes(rbytes)
  install_secret = String.from_java_bytes(rbytes).unpack('H*').join
  plugin_url_fragment = plugin_name.gsub('_','-')
  File.open("#{plugin_name}/plugin.json",'w') do |file|
    file.write(<<__E)
{
"pluginName": "#{plugin_name}",
"pluginAuthor": "TODO Your Company",
"pluginVersion": 1,
"displayName": "#{plugin_name.split('_').map {|e| e.capitalize} .join(' ')}",
"displayDescription": "TODO Longer description of plugin",
"installSecret": "#{install_secret}",
"apiVersion": 4,
"load": [
    "js/#{plugin_name}.js"
  ],
"respond": ["/do/#{plugin_url_fragment}"]
}
__E
  end
  File.open("#{plugin_name}/js/#{plugin_name}.js",'w') do |file|
    file.write(<<__E)

P.respond("GET", "/do/#{plugin_url_fragment}/example", [
], function(E) {
  E.render({
      pageTitle: "Example page"
  });
});

__E
  end
  File.open("#{plugin_name}/template/example.html",'w') do |file|
    file.write(<<__E)
<p>This is an example template.</p>
__E
  end
  File.open("#{plugin_name}/test/#{plugin_name}_test1.js",'w') do |file|
    file.write(<<__E)

t.test(function() {

  // For documentation, see
  //   http://docs.oneis.co.uk/dev/plugin/tests

  t.assert(true);

});

__E
  end
  File.open("#{plugin_name}/requirements.schema",'w') do |file|
    file.write("\n\n\n")
  end
  puts <<__E

Plugin #{plugin_name} has been created. Run

oneis-plugin -p #{plugin_name}

to upload it to the server, then visit

https://<HOSTNAME>/do/#{plugin_url_fragment}/example

to see a sample page.

See http://docs.oneis.co.uk/dev/plugin for more information.

__E
end

.make_watcher(dirs) ⇒ Object



32
33
34
35
36
37
# File 'lib/watchers.rb', line 32

def self.make_watcher(dirs)
  # TODO: Option to use external watcher task
  # pipe = IO.popen(watcher_cmd)
  # wait with pipe.read
  WatcherPoll.new(dirs)
end

.pack_plugin(plugin_name, output_directory) ⇒ Object



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
# File 'lib/packing.rb', line 7

def self.pack_plugin(plugin_name, output_directory)
  # Get filenames and sort
  files = Dir.glob("#{plugin_name}/**/*").map do |filename|
    if File.file? filename
      filename[plugin_name.length+1, filename.length]
    else
      nil
    end
  end .compact.sort
  # Check each filename is acceptable
  files.each do |filename|
    unless filename =~ PACKING_ACCEPTABLE_FILENAME || PACKING_ACCEPTABLE_EXCEPTIONS.include?(filename)
      puts "File '#{filename}' has an unacceptable filename"
      exit 1
    end
  end
  # Clean output directory
  output_plugin_dir = "#{output_directory}/#{plugin_name}"
  puts "Output directory: #{output_plugin_dir}"
  if File.exist? output_plugin_dir
    puts "Removing old output directory #{output_plugin_dir}"
    FileUtils.rm_r(output_plugin_dir)
  end
  # Make file structure
  FileUtils.mkdir(output_plugin_dir)
  # Process each file, building a manifest
  puts "Processing files:"
  manifest = ''
  minimiser = PluginTool::Minimiser.new
  files.each do |filename|
    puts "  #{filename}"
    data = File.open("#{plugin_name}/#{filename}") { |f| f.read }
    # Minimise file?
    unless filename =~ /\Ajs\//
      data = minimiser.process(data, filename)
    end
    hash = Digest::SHA256.hexdigest(data)
    # Make sure output directory exists, write file
    output_pathname = "#{output_plugin_dir}/#{filename}"
    output_directory = File.dirname(output_pathname)
    FileUtils.mkdir_p(output_directory) unless File.directory?(output_directory)
    File.open(output_pathname, "w") { |f| f.write data }
    # Filename entry in Manifest
    manifest << "F #{hash} #{filename}\n"
  end
  minimiser.finish
  # Write manifest and version
  File.open("#{output_plugin_dir}/manifest", "w") { |f| f.write manifest }
  version = Digest::SHA256.hexdigest(manifest)
  File.open("#{output_plugin_dir}/version", "w") { |f| f.write "#{version}\n" }
  # All done
  puts "Version: #{version}\nPlugin packed."
end

.parse_hostname_with_port(hostname_with_port) ⇒ Object




136
137
138
139
140
141
142
143
144
145
# File 'lib/auth.rb', line 136

def self.parse_hostname_with_port(hostname_with_port)
  hostname_with_port = hostname_with_port.downcase.strip
  unless hostname_with_port =~ /\A([a-z0-9\.-]+)(:(\d+))?\z/
    end_on_error "Bad hostname #{hostname_with_port}"
  end
  hostname = $1
  port = $3 ? $3.to_i : 443
  server_name = "#{hostname}#{$3 ? ":#{$3}" : ''}"
  [hostname, port, "https://#{server_name}", server_name]
end

.post(path, params = nil, files = nil) ⇒ Object



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
# File 'lib/server.rb', line 79

def self.post(path, params = nil, files = nil)
  http = get_http
  request = Net::HTTP::Post.new(path)
  setup_request(request)
  if files == nil
    request.set_form_data(params) if params != nil
  else
    boundary = "----------XnJLe9ZIbbGUYtzPQJ16u1"
    request["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
    body = ''
    if params != nil
      params.each do |key,value|
      body << <<-EOF
--#{boundary}\r
Content-Disposition: form-data; name="#{key}"\r
\r
#{value}\r
EOF
      end
    end
    files.each do |key,info|
      pathname, data = info
      body << <<-EOF
--#{boundary}\r
Content-Disposition: form-data; name="#{key}"; filename="#{pathname}"\r
Content-Type: application/octet-stream\r
Content-Length: #{data.length}\r
\r
#{data}\r
EOF
    end
    body << "--#{boundary}--\r"
    request.body = body
  end
  http.request(request).body
end

.post_with_json_response(path, params = nil, files = nil) ⇒ Object



116
117
118
# File 'lib/server.rb', line 116

def self.post_with_json_response(path, params = nil, files = nil)
  report_errors_from_server(JSON.parse(post(path, params, files)))
end

.report_errors_from_server(r) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/server.rb', line 120

def self.report_errors_from_server(r)
  unless r.kind_of? Hash
    r = {
      "result" => 'error',
      "protocol_error" => true,
      "message" => "Unknown error. Either the server is not enabled for development, or the credentials in #{SERVER_INFO} are not valid."
    }
  end
  if r["result"] != 'success' && r.has_key?("message")
    puts "\n\n**************************************************************"
    puts "                   ERROR REPORTED BY SERVER"
    puts "**************************************************************\n\n"
    puts r["message"]
    puts "\n**************************************************************\n\n"
    beep
  end
  r
end

.save_keys_file(keys) ⇒ Object



157
158
159
160
161
# File 'lib/auth.rb', line 157

def self.save_keys_file(keys)
  pn = "#{@@keys_pathname}.n"
  File.open(pn, "w") { |f| f.write(JSON.pretty_generate(keys)) }
  File.rename(pn, @@keys_pathname)
end

.select_server(keys, substring) ⇒ Object



163
164
165
166
# File 'lib/auth.rb', line 163

def self.select_server(keys, substring)
  s = substring.downcase.strip
  keys['keys'].keys.sort { |a,b| (a.length == b.length) ? (a <=> b) : (a.length <=> b.length) } .find { |a| a.include? s }
end

.set_custom_behaviour(custom) ⇒ Object



18
19
20
# File 'lib/custom.rb', line 18

def self.set_custom_behaviour(custom)
  @@custom = custom
end

.set_server(hostname, port, key) ⇒ Object



4
5
6
7
8
# File 'lib/server.rb', line 4

def self.set_server(hostname, port, key)
  @@server_hostname = hostname
  @@server_port = port
  @@server_key = key
end

.setup_auth(options) ⇒ Object




8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/auth.rb', line 8

def self.setup_auth(options)
  keys = load_keys_file()
  server = keys['default']
  if options.server_substring
    server = select_server(keys, options.server_substring)
    end_on_error "No server found for substring '#{options.server_substring}" unless server
  end
  key = keys['keys'][server]
  if key
    hostname, port, url_base = parse_hostname_with_port(server)
    set_server(hostname, port, key)
    puts "Application: #{url_base}"
  else
    end_on_error "No server authorised. Run oneis-plugin auth SERVER_NAME"
  end
end

.setup_request(req) ⇒ Object



63
64
65
66
# File 'lib/server.rb', line 63

def self.setup_request(req)
  req['User-Agent'] = 'plugin-tool'
  req['X-ONEIS-Key'] = @@server_key if @@server_key
end

.start_notifications(options) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/notifications.rb', line 8

def self.start_notifications(options)
  @@notification_options = options
  @@notification_queue_name = nil
  Thread.new do
    while true
      begin
        self.do_notifications
      rescue => e
        puts "NOTICE: Lost notification connection, will attempt to reconnect soon."
        @@notification_announce_reconnect = true
        sleep(5)  # throttle errors
      end
    end
  end
end

.start_syntax_checkObject



11
12
13
14
15
16
17
18
19
20
# File 'lib/syntax_checking.rb', line 11

def self.start_syntax_check
  Thread.new do
    begin
      do_syntax_checking
    rescue => e
      puts "EXCEPTION IN SYNTAX CHECKER\n#{e.inspect}"
      exit 1
    end      
  end
end

.syntax_check(plugin, filename) ⇒ Object



99
100
101
102
103
104
105
# File 'lib/syntax_checking.rb', line 99

def self.syntax_check(plugin, filename)
  @@syntax_check_queue_lock.synchronize do
    @@syntax_check_queue << [plugin, filename]
    @@syntax_check_queue.uniq!
  end
  @@syntax_check_queue_semaphore.synchronize { @@syntax_check_queue_semaphore_var.signal }
end

.syntax_check_one_file(plugin, file) ⇒ Object



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
# File 'lib/syntax_checking.rb', line 34

def self.syntax_check_one_file(plugin, file)
  # Load the plugin.json file
  plugin_dir = plugin.plugin_dir
  plugin_json = File.open("#{plugin_dir}/plugin.json") { |f| JSON.parse(f.read) }
  api_version = (plugin_json["apiVersion"] || '0').to_i
  # Determine kind of file
  kind = (file =~ /\A(\w+)\//) ? $1 : file
  # Is this file referenced?
  report = nil
  unless kind != 'js' || plugin_json['load'].include?(file)
    puts "\nWARNING: #{plugin_dir}/plugin.json doesn't mention #{file} in the load directive.\n"
    report = "Couldn't check file - not mentioned in plugin.json"
  else
    schema_requirements = (api_version < 4) ? nil : SchemaRequirements.new("#{plugin_dir}/requirements.schema")
    extra_globals = schema_requirements ? schema_requirements.locals.dup : []
    # Load the file
    js = File.open("#{plugin_dir}/#{file}") { |f| f.read }
    if api_version < 3
      # If it's not the first file, need to tell JSHint about the extra var
      unless plugin_json['load'].first == file
        extra_globals << plugin_json["pluginName"];
      end
    else
      if kind == 'js' || kind == 'test'
        extra_globals << plugin_json["pluginName"]
        extra_globals << 'P'
        locals = plugin_json["locals"]
        if locals
          locals.each_key { |local| extra_globals << local }
        end
      end
      if kind == 'test'
        extra_globals << 't'
      end
      # global.js requires server side syntax checking, but doesn't have any extra globals, not even the plugin name.
      kind = 'js' if kind == 'global.js'
    end
    # Do syntax checking
    lint_report = @@syntax_tester.call(@@cx, @@javascript_scope, @@javascript_scope, [js, kind, JSON.generate(extra_globals)])
    schema_report = schema_requirements ? schema_requirements.report_usage(js) : nil
    report = [lint_report, schema_report].compact.join("\n\n")
    report = nil if report.empty?
  end
  report
end

.try_load_customObject



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
# File 'lib/custom.rb', line 26

def self.try_load_custom
  return unless File.exist?(LOCAL_CUSTOM_BEHAVIOUR_FILENAME)

  trusted_code = nil
  untrusted_code = File.open(LOCAL_CUSTOM_BEHAVIOUR_FILENAME) { |f| f.read }
  untrusted_code_digest = Digest::SHA256.hexdigest(untrusted_code)

  trusted_code_digests = {"trust" => []}
  trusted_code_filename = File.expand_path(TRUSTED_CODE_DIGESTS_FILENAME)
  if File.exist?(trusted_code_filename)
    trusted_code_digests = JSON.parse(File.open(trusted_code_filename) { |f| f.read })
  end

  unless trusted_code_digests["trust"].include?(untrusted_code_digest)
    # Make sure the user wants to run this code. Otherwise running the plugin tool in a repo you've just
    # downloaded could unexpectedly execute code on your local machine.
    if ARGV.length == 2 && ARGV[0] == 'trust' && ARGV[1] =~ /\A[0-9a-z]{64}\z/ && ARGV[1] == untrusted_code_digest
      trusted_code_digests["trust"].push(untrusted_code_digest)
      File.open(trusted_code_filename,"w") { |f| f.write JSON.pretty_generate(trusted_code_digests) }
      puts "Stored trust for #{LOCAL_CUSTOM_BEHAVIOUR_FILENAME} with contents #{untrusted_code_digest}."
      exit 0
    else
      puts
      puts "-------------------------------------------------------------------------------------------"
      puts "  Do you trust the code in #{LOCAL_CUSTOM_BEHAVIOUR_FILENAME} to be run every time you run the"
      puts "  plugin tool? If yes, run"
      puts "      oneis-plugin trust #{untrusted_code_digest}"
      puts "  to permanently trust this version of #{LOCAL_CUSTOM_BEHAVIOUR_FILENAME}"
      puts "-------------------------------------------------------------------------------------------"
      puts
      PluginTool.beep
      exit 1
    end
  end

  if ARGV.length > 0 && ARGV[0] == "trust"
    puts "Unexpected trust command."
    exit 1
  end

  # User trusts the code, run it
  # There is a race condition here, but we're trying to protect against code in repositories, not
  # against software running on the local machine.
  load LOCAL_CUSTOM_BEHAVIOUR_FILENAME

end