sequenceserver 3.1.3 → 3.2.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sequenceserver/api_errors.rb +18 -0
  3. data/lib/sequenceserver/blast/hit.rb +1 -2
  4. data/lib/sequenceserver/blast/job.rb +15 -5
  5. data/lib/sequenceserver/blast/report.rb +5 -1
  6. data/lib/sequenceserver/database.rb +5 -1
  7. data/lib/sequenceserver/job.rb +26 -10
  8. data/lib/sequenceserver/makeblastdb.rb +2 -1
  9. data/lib/sequenceserver/routes.rb +37 -12
  10. data/lib/sequenceserver/version.rb +1 -1
  11. data/lib/sequenceserver/zip_file_generator.rb +1 -1
  12. data/public/404.html +8 -16
  13. data/public/css/app.css +794 -111
  14. data/public/css/app.min.css +1 -1
  15. data/public/css/sequenceserver.min.css +1 -8
  16. data/public/js/circos.js +103 -96
  17. data/public/{packages/circosJS@1.7.0.js → js/circosjs.js} +156 -778
  18. data/public/js/cloud_share_modal.js +55 -58
  19. data/public/js/collapse_preferences.js +13 -15
  20. data/public/js/databases.js +4 -3
  21. data/public/js/databases_tree.js +51 -30
  22. data/public/js/error_modal.js +35 -22
  23. data/public/js/exporter.js +2 -0
  24. data/public/js/form.js +43 -7
  25. data/public/js/grapher.js +94 -92
  26. data/public/js/hit.js +21 -19
  27. data/public/js/hits.js +45 -38
  28. data/public/js/hits_overview.js +8 -27
  29. data/public/js/hsp.js +140 -144
  30. data/public/js/jquery_world.js +24 -15
  31. data/public/js/kablammo.js +14 -10
  32. data/public/js/length_distribution.js +23 -37
  33. data/public/js/null_plugins/report_plugins.js +3 -5
  34. data/public/js/options.js +17 -19
  35. data/public/js/query.js +36 -35
  36. data/public/js/report/alignment_results.js +18 -0
  37. data/public/js/report/graphical_overview.js +38 -0
  38. data/public/js/report/report_header.js +28 -0
  39. data/public/js/report/run_summary.js +43 -0
  40. data/public/js/report.js +62 -102
  41. data/public/js/report_root.js +3 -3
  42. data/public/js/search.js +5 -2
  43. data/public/js/search_button.js +85 -78
  44. data/public/js/sequence.js +11 -22
  45. data/public/js/sequence_modal.js +38 -37
  46. data/public/js/share_url.js +5 -5
  47. data/public/js/sidebar.js +148 -86
  48. data/public/js/svgExporter.js +8 -3
  49. data/public/js/tests/form.spec.js +1 -1
  50. data/public/js/tests/visualisation_helpers.spec.js +28 -0
  51. data/public/js/visualisation_helpers.js +24 -27
  52. data/public/sequenceserver-report.min.js +1 -1
  53. data/public/sequenceserver-report.min.js.LICENSE.txt +1 -88
  54. data/public/sequenceserver-report.min.js.map +1 -1
  55. data/public/sequenceserver-search.min.js +1 -1
  56. data/public/sequenceserver-search.min.js.LICENSE.txt +0 -30
  57. data/public/sequenceserver-search.min.js.map +1 -1
  58. data/views/error.erb +1 -1
  59. data/views/layout.erb +123 -130
  60. data/views/report.erb +3 -2
  61. data/views/search.erb +10 -14
  62. metadata +13 -26
  63. data/public/css/fonts.css +0 -31
  64. data/public/css/grapher.css +0 -670
  65. data/public/css/sequenceserver.css +0 -509
  66. data/public/vendor/github/mbostock/d3@3.5.6/d3.js +0 -9506
  67. data/public/vendor/github/mbostock/d3@3.5.6.js +0 -1
  68. data/views/search_layout.erb +0 -153
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc2b537e7c30e2f882bbf34228d607b5cd51ee3b2b23de202bc472e6977638e6
4
- data.tar.gz: 1d0a3c11b9d468fb5ad30df26fd58637f866afcbf3a00f7586244f4c65dca146
3
+ metadata.gz: f52305fc6b02d855f14730d480c99e940cf384c78b43f8791b789d4fd92b10bf
4
+ data.tar.gz: 2b8467a02de662d8a8e74f15916973000da735440a5651705fa3cce0df1c4b15
5
5
  SHA512:
6
- metadata.gz: 98113d4caa432621cc0e5e0749649f11cebc2db613f8e1ecffe3149570b5a35050e8095961ab80cf231eab7b3ca9845f291eefcd7c0f314b4fc3f04e5e2d0b0d
7
- data.tar.gz: 8260f22915cbfe511f9bd2a8b11a6f900478bde088162bc38aba49f22150936419f0b3e889f475ed391621e3804a7e7cffcd79dba4ef0988e7cc486a0a37c28c
6
+ metadata.gz: 53226eb96fce945402ea6889b188aff8431c15672b8fd57686143aaca48bacda86acfaa3c15e7843c91cdc9113545b0d042910ccbd36d32cb9d478e5b1b67da9
7
+ data.tar.gz: a2df8167ce52ce9461c0079f000b41f0508dcb36a1ab751a46012737dd9b9a34e953a327093457d1f72dcdc51079b478235cfc641b1fcbdd3085699c72a2f5ce
@@ -128,6 +128,24 @@ module SequenceServer
128
128
  attr_reader :more_info
129
129
  end
130
130
 
131
+ class XmlImportError < APIError
132
+ def http_status
133
+ 400
134
+ end
135
+
136
+ def title
137
+ 'XML import failed'
138
+ end
139
+
140
+ def message
141
+ 'Please make sure the XML file is a valid BLAST output.'
142
+ end
143
+
144
+ def more_info
145
+ 'Make sure the XML you are importing is generated by blast with -outfmt 5 option.'
146
+ end
147
+ end
148
+
131
149
  # Errors caused by everything other than invalid user input.
132
150
  class SystemError < APIError
133
151
  def initialize(more_info = nil)
@@ -38,8 +38,7 @@ module SequenceServer
38
38
  # instance methods of the Links module.
39
39
  def links
40
40
  links = Links.instance_methods.map { |m| send m }
41
- links.compact!
42
- links.sort_by { |link| [link[:order], link[:title]] }
41
+ links.flatten.compact.sort_by { |link| [link[:order], link[:title]] }
43
42
  end
44
43
 
45
44
  # Returns the database type (nucleotide or protein).
@@ -26,9 +26,9 @@ module SequenceServer
26
26
  @qfile = store('query.fa', params[:sequence])
27
27
  @databases = Database[params[:databases]]
28
28
  @advanced = params[:advanced].to_s.strip
29
- @options = @advanced + defaults
29
+ @num_threads = params[:num_threads] || config[:num_threads]
30
+ @options = @advanced + defaults
30
31
  # The following params are for analytics only
31
- @num_threads = config[:num_threads]
32
32
  @query_length = calculate_query_size
33
33
  @number_of_query_sequences = calculate_number_of_sequences
34
34
  @databases_ncharacters_total = calculate_databases_ncharacters_total
@@ -62,8 +62,9 @@ module SequenceServer
62
62
  # Returns the command that will be executed. Job super class takes care
63
63
  # of actual execution.
64
64
  def command
65
- @command ||= "#{method} -db '#{databases.map(&:name).join(' ')}'" \
66
- " -query '#{qfile}' #{options}"
65
+ "#{method} -db '#{databases.map(&:name).join(' ')}'" \
66
+ " -query '#{qfile}' #{options}" \
67
+ " -num_threads #{@num_threads || config[:num_threads]}"
67
68
  end
68
69
 
69
70
  def raise!
@@ -114,10 +115,11 @@ module SequenceServer
114
115
  validate_sequences params[:sequence]
115
116
  validate_databases params[:databases]
116
117
  validate_options params[:advanced]
118
+ validate_num_threads params[:num_threads]
117
119
  end
118
120
 
119
121
  def defaults
120
- " -outfmt '11 qcovs qcovhsp' -num_threads #{config[:num_threads]}"
122
+ " -outfmt '11 qcovs qcovhsp'"
121
123
  end
122
124
 
123
125
  def validate_method(method)
@@ -159,6 +161,14 @@ module SequenceServer
159
161
  true
160
162
  end
161
163
 
164
+ # num_threads is interpolated unquoted into the shell command, so it
165
+ # must be a positive integer. nil means: use the configured default.
166
+ def validate_num_threads(num_threads)
167
+ return true if num_threads.nil? || num_threads.to_s.match?(/\A[1-9][0-9]*\z/)
168
+
169
+ fail InputError, 'num_threads should be a positive integer.'
170
+ end
171
+
162
172
  def allowed_chars
163
173
  /\A[a-z0-9\-_. ',]*\Z/i
164
174
  end
@@ -93,7 +93,11 @@ module SequenceServer
93
93
  def xml_ir
94
94
  @xml_ir ||=
95
95
  if job.imported_xml_file
96
- parse_xml File.read(job.imported_xml_file)
96
+ parse_xml(File.read(job.imported_xml_file)).tap do |xml_ir|
97
+ if xml_ir.size < 9 || !xml_ir[0].is_a?(String) || !xml_ir[1].is_a?(String) || !xml_ir[7].is_a?(Array) || !xml_ir[8].is_a?(Array)
98
+ raise SequenceServer::XmlImportError
99
+ end
100
+ end
97
101
  else
98
102
  job.raise!
99
103
  parse_xml(xml_formatter.read_file)
@@ -45,10 +45,14 @@ module SequenceServer
45
45
  cmd = "blastdbcmd -db #{name} -entry '#{accession}'"
46
46
 
47
47
  if coords
48
+ # The regex MUST be anchored (\A ... \z). An unanchored match would
49
+ # accept any string that merely *contains* a numeric range, letting an
50
+ # attacker smuggle shell metacharacters after it (e.g. "1-1;id") into
51
+ # the unquoted `-range` argument below, which `sys` runs via /bin/sh.
48
52
  fail(
49
53
  InvalidParameterError,
50
54
  "Invalid range coordinates: #{coords}"
51
- ) unless coords =~ /[0-9]+-[0-9]*/
55
+ ) unless coords =~ /\A[0-9]+-[0-9]*\z/
52
56
 
53
57
  cmd << " -range #{coords}"
54
58
  end
@@ -21,6 +21,14 @@ module SequenceServer
21
21
  # Singleton methods provide the facility to create and queue a job,
22
22
  # fetch a job or all jobs, and delete a job.
23
23
  class Job
24
+ # Job ids are used to build paths under DOTDIR and end up in shell
25
+ # commands, so only allow characters that are safe in both. Ids may have
26
+ # several path segments (test fixtures do), but no segment may start with
27
+ # a dot, which rules out '.', '..' and hidden directories, and the id may
28
+ # not start with '/', so it always stays inside DOTDIR.
29
+ ID_SEGMENT = '[a-zA-Z0-9_-][a-zA-Z0-9_.-]*'.freeze
30
+ ID_PATTERN = %r{\A#{ID_SEGMENT}(/#{ID_SEGMENT})*\z}
31
+
24
32
  class << self
25
33
  # Creates and queues a job. Returns created job object.
26
34
  def create(params)
@@ -40,6 +48,8 @@ module SequenceServer
40
48
 
41
49
  # Fetches job with the given id.
42
50
  def fetch(id)
51
+ return nil unless id.to_s.match?(ID_PATTERN)
52
+
43
53
  job_file = File.join(DOTDIR, id, 'job.yaml')
44
54
  return nil unless File.exist?(job_file)
45
55
 
@@ -86,17 +96,23 @@ module SequenceServer
86
96
  # Subclasses should extend `initialize` as per requirement.
87
97
  def initialize(params = {})
88
98
  @id = params.fetch(:id, SecureRandom.uuid)
99
+ # Validate before anything touches the filesystem: the rescue below
100
+ # removes `dir`, which must never be derived from a bad id.
101
+ fail ArgumentError, "Invalid job id: #{@id}" unless @id.to_s.match?(ID_PATTERN)
102
+
89
103
  @submitted_at = Time.now
90
- mkdir_p dir
91
- yield if block_given?
92
- save
93
- rescue Errno::ENOSPC
94
- raise SystemError, 'Not enough disk space to start a new job'
95
- rescue Errno::EACCES
96
- raise SystemError, "Permission denied to write to #{DOTDIR}"
97
- rescue StandardError => e
98
- rm_rf dir
99
- raise e
104
+ begin
105
+ mkdir_p dir
106
+ yield if block_given?
107
+ save
108
+ rescue Errno::ENOSPC
109
+ raise SystemError, 'Not enough disk space to start a new job'
110
+ rescue Errno::EACCES
111
+ raise SystemError, "Permission denied to write to #{DOTDIR}"
112
+ rescue StandardError => e
113
+ rm_rf dir
114
+ raise e
115
+ end
100
116
  end
101
117
 
102
118
  attr_reader :id, :submitted_at, :completed_at, :exitstatus
@@ -1,4 +1,5 @@
1
1
  require 'find'
2
+ require 'shellwords'
2
3
  require 'forwardable'
3
4
 
4
5
  module SequenceServer
@@ -144,7 +145,7 @@ module SequenceServer
144
145
  # directory. Returns the output of `blastdbcmd`. This method is called
145
146
  # by `determine_formatted_fastas`.
146
147
  def blastdbcmd
147
- cmd = "blastdbcmd -recursive -list #{config[:database_dir]}" \
148
+ cmd = "blastdbcmd -recursive -list #{Shellwords.escape(config[:database_dir])}" \
148
149
  ' -list_outfmt "%f %t %p %n %l %d %v"'
149
150
  out, err = sys(cmd, path: config[:bin])
150
151
  errpat = /BLAST Database error/
@@ -35,7 +35,9 @@ module SequenceServer
35
35
  # e.g. for example.org/our-sequenceserver set to '/our-sequenceserver'
36
36
  set :root_path_prefix, ''
37
37
 
38
- set :search_layout, :'search_layout'
38
+ set :asset_prefix, ''
39
+
40
+ set :layout, :'layout'
39
41
  end
40
42
 
41
43
  # See
@@ -66,7 +68,7 @@ module SequenceServer
66
68
  secret: ENV.fetch('SESSION_SECRET') { SecureRandom.alphanumeric(64) }
67
69
  )
68
70
 
69
- use Rack::Csrf, raise: true, skip: ['POST:/cloud_share']
71
+ use Rack::Csrf, raise: true, skip: ['POST:/cloud_share'] unless ENV['SKIP_CSRF_PROTECTION'] == 'true'
70
72
  end
71
73
 
72
74
  unless ENV['SEQUENCE_SERVER_COMPRESS_RESPONSES'] == 'false'
@@ -86,7 +88,7 @@ module SequenceServer
86
88
 
87
89
  # Returns base HTML. Rest happens client-side: rendering the search form.
88
90
  get '/' do
89
- erb :search, layout: settings.search_layout
91
+ erb :search, layout: settings.layout
90
92
  end
91
93
 
92
94
  # Returns data that is used to render the search form client side. These
@@ -113,9 +115,9 @@ module SequenceServer
113
115
  post '/' do
114
116
  if params[:input_sequence]
115
117
  @input_sequence = params[:input_sequence]
116
- erb :search, layout: settings.search_layout
118
+ erb :search, layout: settings.layout
117
119
  else
118
- job = Job.create(params)
120
+ job = Job.create(job_params)
119
121
  redirect to("/#{job.id}")
120
122
  end
121
123
  end
@@ -143,7 +145,7 @@ module SequenceServer
143
145
  job = Job.fetch(jid)
144
146
  halt 404, File.read(File.join(settings.root, 'public/404.html')) if job.nil?
145
147
 
146
- erb :report, layout: true
148
+ erb :report, layout: settings.layout
147
149
  end
148
150
  # @params sequence_ids: whitespace separated list of sequence ids to
149
151
  # retrieve
@@ -156,7 +158,7 @@ module SequenceServer
156
158
  # in identifiers) and retreival_databases (we don't allow whitespace in a
157
159
  # database's name, so it's safe).
158
160
  get '/get_sequence/' do
159
- sequence_ids = params[:sequence_ids].to_s.split(',')
161
+ sequence_ids = params[:sequence_ids].to_s.split(',').uniq
160
162
  database_ids = params[:database_ids].to_s.split(',')
161
163
  if sequence_ids.empty?
162
164
  status 422
@@ -172,7 +174,7 @@ module SequenceServer
172
174
  end
173
175
 
174
176
  post '/get_sequence' do
175
- sequence_ids = params['sequence_ids'].to_s.split(',')
177
+ sequence_ids = params['sequence_ids'].to_s.split(',').uniq
176
178
  database_ids = params['database_ids'].to_s.split(',')
177
179
 
178
180
  if sequence_ids.empty?
@@ -289,11 +291,9 @@ module SequenceServer
289
291
  error.class.name
290
292
  end
291
293
 
292
- # If error object has a more_info method, use that. If the error does not
293
- # have more_info, use backtrace.join("\n") as more_info.
294
- if error.respond_to? :more_info
294
+ if error.respond_to?(:more_info)
295
295
  error_data[:more_info] = error.more_info
296
- elsif error.respond_to? :backtrace
296
+ elsif error.respond_to?(:backtrace) && !settings.production?
297
297
  error_data[:more_info] = error.backtrace.join("\n")
298
298
  end
299
299
 
@@ -358,6 +358,18 @@ module SequenceServer
358
358
  def root_path_prefix
359
359
  settings.root_path_prefix.to_s
360
360
  end
361
+
362
+ def css_asset_path
363
+ File.join(settings.asset_prefix, 'css', 'app.min.css') + '?v=' + SequenceServer::VERSION
364
+ end
365
+
366
+ def asset_path(file)
367
+ File.join(settings.asset_prefix, file)
368
+ end
369
+
370
+ def asset_prefix
371
+ settings.asset_prefix
372
+ end
361
373
  end
362
374
 
363
375
  private
@@ -365,5 +377,18 @@ module SequenceServer
365
377
  def fetch_job(job_id)
366
378
  Job.fetch(job_id)
367
379
  end
380
+
381
+ # Only these request parameters may reach Job.create. Job#initialize and
382
+ # BLAST::Job#initialize also read :id, :xml and :num_threads, which end up
383
+ # in file paths and in the shell command, so they must never come from the
384
+ # request.
385
+ def job_params
386
+ {
387
+ method: params[:method],
388
+ sequence: params[:sequence],
389
+ databases: params[:databases],
390
+ advanced: params[:advanced]
391
+ }
392
+ end
368
393
  end
369
394
  end
@@ -1,4 +1,4 @@
1
1
  # Define version number.
2
2
  module SequenceServer
3
- VERSION = '3.1.3'.freeze
3
+ VERSION = '3.2.0'.freeze
4
4
  end
@@ -23,7 +23,7 @@ class ZipFileGenerator
23
23
  def write
24
24
  entries = Dir.entries(@input_dir) - %w[. ..]
25
25
 
26
- ::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile|
26
+ ::Zip::File.open(@output_file, create: true) do |zipfile|
27
27
  write_entries entries, '', zipfile
28
28
  end
29
29
  end
data/public/404.html CHANGED
@@ -4,24 +4,16 @@
4
4
  <head>
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
-
8
7
  <title>SequenceServer: Not Found</title>
9
-
10
- <!-- Bootstrap CSS -->
11
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
12
- integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
8
+ <link rel="stylesheet" media="screen,print" type="text/css" href="css/app.min.css"/>
13
9
  </head>
14
10
 
15
- <body class="bg-light text-dark text-center">
16
-
17
- <img src="/sequenceserver_logo.webp" alt="SequenceServer Logo" class="mt-5 mb-3" style="max-width: 200px;">
18
-
19
- <div class="container" style="margin-top: 10%;">
20
- <h1 class="display-4">404 Not Found</h1>
21
- <p class="lead">Sorry, the page you are looking for could not be found.</p>
22
- <a href="/" class="btn btn-primary mt-3">Start over</a>
11
+ <body class="bg-white text-black text-center">
12
+ <img src="/sequenceserver_logo.webp" alt="SequenceServer Logo" class="mt-12 mb-4 w-48">
13
+ <div class="container mt-12">
14
+ <h1 class="text-4xl font-light leading-tight">404 Not Found</h1>
15
+ <p class="text-xl font-light mb-4">Sorry, the page you are looking for could not be found.</p>
16
+ <a href="/" class="py-2 px-4 rounded-md shadow-sm text-white bg-seqblue">Start over</a>
23
17
  </div>
24
-
25
18
  </body>
26
-
27
- </html>
19
+ </html>