web-console 2.2.1 → 2.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c224d95365158b5d6c2c74a723e0f980d6ec2cc1
4
- data.tar.gz: 96c1809b1e11969901c710bd3d6f316bbc1221c5
3
+ metadata.gz: 02d40821ae813ac4e5893b4c853725833017cb46
4
+ data.tar.gz: 56b2826919677b8a93e6ecedce69a176949d15ee
5
5
  SHA512:
6
- metadata.gz: 43fb7f5387311dde55c6464a64ff9f9498486b7afc35f7c0254c851ce77027d288b190cfdd73c56bfebf7987e87d3f33315f7c626fef97b4da044892dbf135c5
7
- data.tar.gz: 51395dfa02548a44395e5bffa801d3c28a4e95b556fa1982ba7045d279871e1c98de4e0bf1fd1b6a5273348ac5ad3c405bde530ffa394d6becab9ea39cf24bc3
6
+ metadata.gz: 6c6a07f241b5bfad39c7a588ac92bccc8f0786db1b807d3ab94b4ec13464d7d8514493634c88247a868e78e43169094d216da6f01c3559be45af39854a76a0a0
7
+ data.tar.gz: 4b656f08efb441715996f541f94806d9f6249171502fded529041c5fc5cead0a73ce4f155fe246f0c4a36c4a0b54f9bdd65fb3091ed5e9b9bfa86b9d95f3a552
data/CHANGELOG.markdown CHANGED
@@ -2,9 +2,14 @@
2
2
 
3
3
  ## master (unreleased)
4
4
 
5
+ ## 2.3.0
6
+
7
+ * [#181](https://github.com/rails/web-console/pull/181) Log internal Web Console errors ([@schneems])
8
+ * [#150](https://github.com/rails/web-console/pull/150) Revert #150. ([@gsamokovarov])
9
+
5
10
  ## 2.2.1
6
11
 
7
- * [#150](https://github.com/rails/web-console/pull/150) Change config.development_only default until 4.2.4 is released.
12
+ * [#150](https://github.com/rails/web-console/pull/150) Change config.development_only default until 4.2.4 is released. ([@gsamokovarov])
8
13
 
9
14
  ## 2.2.0
10
15
 
@@ -48,3 +53,4 @@
48
53
  [@parterburn]: https://github.com/parterburn
49
54
  [@sh19910711]: https://github.com/sh19910711
50
55
  [@frenesim]: https://github.com/frenesim
56
+ [@schneems]: https://github.com/schneems
data/README.markdown CHANGED
@@ -161,6 +161,18 @@ end
161
161
  You may wanna check the [templates] folder at the source tree for the files you
162
162
  may override.
163
163
 
164
+ ### config.web_console.mount_point
165
+
166
+ Usually the middleware of _Web Console_ is mounted at `/__web_console`.
167
+ If you wanna change the path for some reasons, you can specify it
168
+ by `config.web_console.mount_point`:
169
+
170
+ ```ruby
171
+ Rails.application.configure do
172
+ config.web_console.mount_point = '/path/to/web_console'
173
+ end
174
+ ```
175
+
164
176
  ## FAQ
165
177
 
166
178
  ### Where did /console go?
data/Rakefile CHANGED
@@ -8,6 +8,8 @@ require 'socket'
8
8
  require 'rake/testtask'
9
9
  require 'tmpdir'
10
10
  require 'securerandom'
11
+ require 'json'
12
+ require 'web_console/testing/erb_precompiler'
11
13
 
12
14
  EXPANDED_CWD = File.expand_path(File.dirname(__FILE__))
13
15
 
@@ -18,40 +20,7 @@ Rake::TestTask.new(:test) do |t|
18
20
  t.verbose = false
19
21
  end
20
22
 
21
- namespace :test do
22
- desc "Run tests for templates"
23
- task :templates => "templates:all"
24
-
25
- namespace :templates do
26
- task :all => [:daemonize, :npm, :rackup, :mocha, :kill]
27
- task :serve => [:npm, :rackup]
28
-
29
- work_dir = Pathname(__FILE__).dirname.join("test/templates")
30
- pid_file = Pathname(Dir.tmpdir).join("web_console.#{SecureRandom.uuid}.pid")
31
- server_port = 29292
32
- rackup_opts = "-p #{server_port}"
33
-
34
- task :daemonize do
35
- rackup_opts += " -D -P #{pid_file}"
36
- end
37
-
38
- task :npm do
39
- Dir.chdir(work_dir) { system "npm install --silent" }
40
- end
41
-
42
- task :rackup do
43
- Dir.chdir(work_dir) { system "bundle exec rackup #{rackup_opts}" }
44
- end
45
-
46
- task :mocha do
47
- Dir.chdir(work_dir) { system "$(npm bin)/mocha-phantomjs http://localhost:#{server_port}/html/spec_runner.html" }
48
- end
49
-
50
- task :kill do
51
- system "kill #{File.read pid_file}"
52
- end
53
- end
54
- end
23
+ Dir['lib/web_console/tasks/**/*.rake'].each { |task| load task }
55
24
 
56
25
  Bundler::GemHelper.install_tasks
57
26
 
data/lib/web_console.rb CHANGED
@@ -13,6 +13,8 @@ require 'web_console/template'
13
13
  require 'web_console/middleware'
14
14
  require 'web_console/whitelist'
15
15
  require 'web_console/request'
16
+ require 'web_console/response'
17
+ require 'web_console/view'
16
18
  require 'web_console/whiny_request'
17
19
 
18
20
  module WebConsole
@@ -1,6 +1,9 @@
1
1
  ActionDispatch::DebugExceptions.class_eval do
2
- def render_exception_with_web_console(env, exception)
3
- render_exception_without_web_console(env, exception).tap do
2
+ def render_exception_with_web_console(request, exception)
3
+ render_exception_without_web_console(request, exception).tap do
4
+ # Retain superficial Rails 5 compatibility.
5
+ env = Hash === request ? request : request.env
6
+
4
7
  error = ActionDispatch::ExceptionWrapper.new(env, exception).exception
5
8
 
6
9
  # Get the original exception if ExceptionWrapper decides to follow it.
@@ -0,0 +1,15 @@
1
+ en:
2
+ errors:
3
+ unavailable_session: |
4
+ Session %{id} is is no longer available in memory.
5
+
6
+ If you happen to run on a multi-process server (like Unicorn or Puma) the process
7
+ this request hit doesn't store %{id} in memory. Consider turning the number of
8
+ processes/workers to one (1) or using a different server in development.
9
+
10
+ unacceptable_request: |
11
+ A supported version is expected in the Accept header.
12
+
13
+ connection_refused: |
14
+ Oops! Failed to connect to the Web Console middleware.
15
+ Please make sure a rails development server is running.
@@ -4,60 +4,60 @@ module WebConsole
4
4
  class Middleware
5
5
  TEMPLATES_PATH = File.expand_path('../templates', __FILE__)
6
6
 
7
- DEFAULT_OPTIONS = {
8
- update_re: %r{/repl_sessions/(?<id>.+?)\z},
9
- binding_change_re: %r{/repl_sessions/(?<id>.+?)/trace\z}
10
- }
11
-
12
- UNAVAILABLE_SESSION_MESSAGE = <<-END.strip_heredoc
13
- Session %{id} is is no longer available in memory.
14
-
15
- If you happen to run on a multi-process server (like Unicorn) the process
16
- this request hit doesn't store %{id} in memory.
17
- END
18
-
19
- UNACCEPTABLE_REQUEST_MESSAGE = "A supported version is expected in the Accept header."
7
+ cattr_accessor :mount_point
8
+ @@mount_point = '/__web_console'
20
9
 
21
10
  cattr_accessor :whiny_requests
22
11
  @@whiny_requests = true
23
12
 
24
- def initialize(app, options = {})
25
- @app = app
26
- @options = DEFAULT_OPTIONS.merge(options)
13
+ def initialize(app)
14
+ @app = app
27
15
  end
28
16
 
29
17
  def call(env)
30
- request = create_regular_or_whiny_request(env)
31
- return @app.call(env) unless request.from_whitelited_ip?
32
-
33
- if id = id_for_repl_session_update(request)
34
- return update_repl_session(id, request)
35
- elsif id = id_for_repl_session_stack_frame_change(request)
36
- return change_stack_trace(id, request)
37
- end
18
+ app_exception = catch :app_exception do
19
+ request = create_regular_or_whiny_request(env)
20
+ return @app.call(env) unless request.from_whitelited_ip?
21
+
22
+ if id = id_for_repl_session_update(request)
23
+ return update_repl_session(id, request)
24
+ elsif id = id_for_repl_session_stack_frame_change(request)
25
+ return change_stack_trace(id, request)
26
+ end
38
27
 
39
- status, headers, body = @app.call(env)
28
+ status, headers, body = @app.call(env)
40
29
 
41
- if exception = env['web_console.exception']
42
- session = Session.from_exception(exception)
43
- elsif binding = env['web_console.binding']
44
- session = Session.from_binding(binding)
45
- end
30
+ if exception = env['web_console.exception']
31
+ session = Session.from_exception(exception)
32
+ elsif binding = env['web_console.binding']
33
+ session = Session.from_binding(binding)
34
+ end
46
35
 
47
- if session && request.acceptable_content_type?
48
- headers["X-Web-Console-Session-Id"] = session.id
49
- response = Rack::Response.new(body, status, headers)
50
- template = Template.new(env, session)
36
+ if session && acceptable_content_type?(headers)
37
+ response = Response.new(body, status, headers)
38
+ template = Template.new(env, session)
51
39
 
52
- response.write(template.render('index'))
53
- response.finish
54
- else
55
- [ status, headers, body ]
40
+ response.headers["X-Web-Console-Session-Id"] = session.id
41
+ response.headers["X-Web-Console-Mount-Point"] = mount_point
42
+ response.write(template.render('index'))
43
+ response.finish
44
+ else
45
+ [ status, headers, body ]
46
+ end
56
47
  end
48
+ rescue => e
49
+ WebConsole.logger.error("\n#{e.class}: #{e}\n\tfrom #{e.backtrace.join("\n\tfrom ")}")
50
+ raise e
51
+ ensure
52
+ raise app_exception if Exception === app_exception
57
53
  end
58
54
 
59
55
  private
60
56
 
57
+ def acceptable_content_type?(headers)
58
+ Mime::Type.parse(headers['Content-Type']).first == Mime::HTML
59
+ end
60
+
61
61
  def json_response(opts = {})
62
62
  status = opts.fetch(:status, 200)
63
63
  headers = { 'Content-Type' => 'application/json; charset = utf-8' }
@@ -67,17 +67,10 @@ module WebConsole
67
67
  end
68
68
 
69
69
  def json_response_with_session(id, request, opts = {})
70
- json_response(opts) do
71
- unless request.acceptable?
72
- return respond_with_unacceptable_request
73
- end
70
+ return respond_with_unacceptable_request unless request.acceptable?
71
+ return respond_with_unavailable_session(id) unless session = Session.find(id)
74
72
 
75
- unless session = Session.find(id)
76
- return respond_with_unavailable_session(id)
77
- end
78
-
79
- yield session
80
- end
73
+ json_response(opts) { yield session }
81
74
  end
82
75
 
83
76
  def create_regular_or_whiny_request(env)
@@ -85,23 +78,27 @@ module WebConsole
85
78
  whiny_requests ? WhinyRequest.new(request) : request
86
79
  end
87
80
 
81
+ def repl_sessions_re
82
+ @_repl_sessions_re ||= %r{#{mount_point}/repl_sessions/(?<id>[^/]+)}
83
+ end
84
+
88
85
  def update_re
89
- @options[:update_re]
86
+ @_update_re ||= %r{#{repl_sessions_re}\z}
90
87
  end
91
88
 
92
89
  def binding_change_re
93
- @options[:binding_change_re]
90
+ @_binding_change_re ||= %r{#{repl_sessions_re}/trace\z}
94
91
  end
95
92
 
96
93
  def id_for_repl_session_update(request)
97
94
  if request.xhr? && request.put?
98
- update_re.match(request.path_info) { |m| m[:id] }
95
+ update_re.match(request.path) { |m| m[:id] }
99
96
  end
100
97
  end
101
98
 
102
99
  def id_for_repl_session_stack_frame_change(request)
103
100
  if request.xhr? && request.post?
104
- binding_change_re.match(request.path_info) { |m| m[:id] }
101
+ binding_change_re.match(request.path) { |m| m[:id] }
105
102
  end
106
103
  end
107
104
 
@@ -121,14 +118,20 @@ module WebConsole
121
118
 
122
119
  def respond_with_unavailable_session(id)
123
120
  json_response(status: 404) do
124
- { output: format(UNAVAILABLE_SESSION_MESSAGE, id: id)}
121
+ { output: format(I18n.t('errors.unavailable_session'), id: id)}
125
122
  end
126
123
  end
127
124
 
128
125
  def respond_with_unacceptable_request
129
126
  json_response(status: 406) do
130
- { error: UNACCEPTABLE_REQUEST_MESSAGE }
127
+ { output: I18n.t('errors.unacceptable_request') }
131
128
  end
132
129
  end
130
+
131
+ def call_app(env)
132
+ @app.call(env)
133
+ rescue => e
134
+ throw :app_exception, e
135
+ end
133
136
  end
134
137
  end
@@ -5,10 +5,6 @@ module WebConsole
5
5
  config.web_console = ActiveSupport::OrderedOptions.new
6
6
  config.web_console.whitelisted_ips = %w( 127.0.0.1 ::1 )
7
7
 
8
- # See rails/web-console#150 and rails/rails#20319. Revert when Ruby on
9
- # Rails 4.2.4 is released.
10
- config.web_console.development_only = false
11
-
12
8
  initializer 'web_console.initialize' do
13
9
  require 'web_console/extensions'
14
10
 
@@ -43,6 +39,12 @@ module WebConsole
43
39
  app.middleware.insert_before ActionDispatch::DebugExceptions, Middleware
44
40
  end
45
41
 
42
+ initializer 'web_console.mount_point' do
43
+ if mount_point = config.web_console.mount_point
44
+ Middleware.mount_point = mount_point.chomp('/')
45
+ end
46
+ end
47
+
46
48
  initializer 'web_console.template_paths' do
47
49
  if template_paths = config.web_console.template_paths
48
50
  Template.template_paths.unshift(*Array(template_paths))
@@ -61,12 +63,8 @@ module WebConsole
61
63
  end
62
64
  end
63
65
 
64
- # Leave this undocumented so we treat such content type misses as bugs,
65
- # while still being able to help the affected users in the meantime.
66
- initializer 'web_console.acceptable_content_types' do
67
- if acceptable_content_types = config.web_console.acceptable_content_types
68
- Request.acceptable_content_types.concat(Array(acceptable_content_types))
69
- end
66
+ initializer 'i18n.load_path' do
67
+ config.i18n.load_path.concat(Dir[File.expand_path('../locales/*.yml', __FILE__)])
70
68
  end
71
69
  end
72
70
  end
@@ -1,11 +1,6 @@
1
1
  module WebConsole
2
2
  # Web Console tailored request object.
3
3
  class Request < ActionDispatch::Request
4
- # While most of the servers will return blank content type if none given,
5
- # Puma will return text/plain.
6
- cattr_accessor :acceptable_content_types
7
- @@acceptable_content_types = [Mime::HTML, Mime::TEXT, Mime::URL_ENCODED_FORM]
8
-
9
4
  # Configurable set of whitelisted networks.
10
5
  cattr_accessor :whitelisted_ips
11
6
  @@whitelisted_ips = Whitelist.new
@@ -23,16 +18,7 @@ module WebConsole
23
18
 
24
19
  # Determines the remote IP using our much stricter whitelist.
25
20
  def strict_remote_ip
26
- GetSecureIp.new(env, whitelisted_ips).to_s
27
- end
28
-
29
- # Returns whether the request is from an acceptable content type.
30
- #
31
- # We can render a console for HTML and TEXT by default. If a client didn't
32
- # specified any content type and the server returned it as blank, we'll
33
- # render it as well.
34
- def acceptable_content_type?
35
- content_type.blank? || content_type.in?(acceptable_content_types)
21
+ GetSecureIp.new(self, whitelisted_ips).to_s
36
22
  end
37
23
 
38
24
  # Returns whether the request is acceptable.
@@ -40,18 +26,25 @@ module WebConsole
40
26
  xhr? && accepts.any? { |mime| Mime::WEB_CONSOLE_V2 == mime }
41
27
  end
42
28
 
43
- class GetSecureIp < ActionDispatch::RemoteIp::GetIp
44
- def initialize(env, proxies)
45
- @env = env
46
- @check_ip = true
47
- @proxies = proxies
48
- end
29
+ private
30
+
31
+ class GetSecureIp < ActionDispatch::RemoteIp::GetIp
32
+ def initialize(req, proxies)
33
+ # After rails/rails@07b2ff0 ActionDispatch::RemoteIp::GetIp initializes
34
+ # with a ActionDispatch::Request object instead of plain Rack
35
+ # environment hash. Keep both @req and @env here, so we don't if/else
36
+ # on Rails versions.
37
+ @req = req
38
+ @env = req.env
39
+ @check_ip = true
40
+ @proxies = proxies
41
+ end
49
42
 
50
- def filter_proxies(ips)
51
- ips.reject do |ip|
52
- @proxies.include?(ip)
43
+ def filter_proxies(ips)
44
+ ips.reject do |ip|
45
+ @proxies.include?(ip)
46
+ end
53
47
  end
54
48
  end
55
- end
56
49
  end
57
50
  end
@@ -0,0 +1,23 @@
1
+ module WebConsole
2
+ # A response object that writes content before the closing </body> tag, if
3
+ # possible.
4
+ #
5
+ # The object quacks like Rack::Response.
6
+ class Response < Struct.new(:body, :status, :headers)
7
+ def write(content)
8
+ raw_body = Array(body).first.to_s
9
+
10
+ if position = raw_body.rindex('</body>')
11
+ raw_body.insert(position, content)
12
+ else
13
+ raw_body << content
14
+ end
15
+
16
+ self.body = raw_body
17
+ end
18
+
19
+ def finish
20
+ Rack::Response.new(body, status, headers).finish
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,60 @@
1
+ namespace :ext do
2
+ rootdir = Pathname('extensions')
3
+
4
+ desc 'Build Chrome Extension'
5
+ task chrome: 'chrome:build'
6
+
7
+ namespace :chrome do
8
+ dist = Pathname('dist/crx')
9
+ extdir = rootdir.join(dist)
10
+ manifest_json = rootdir.join('chrome/manifest.json')
11
+
12
+ directory extdir
13
+
14
+ task build: [ extdir, 'lib:templates' ] do
15
+ cd rootdir do
16
+ cp_r [ 'img/', 'tmp/lib/' ], dist
17
+ `cd chrome && git ls-files`.split("\n").each do |src|
18
+ dest = dist.join(src)
19
+ mkdir_p dest.dirname
20
+ cp Pathname('chrome').join(src), dest
21
+ end
22
+ end
23
+ end
24
+
25
+ # Generate a .crx file.
26
+ task crx: [ :build, :npm ] do
27
+ out = "crx-web-console-#{JSON.parse(File.read(manifest_json))["version"]}.crx"
28
+ cd(extdir) { sh "node \"$(npm bin)/crx\" pack ./ -p ../crx-web-console.pem -o ../#{out}" }
29
+ end
30
+
31
+ # Generate a .zip file for Chrome Web Store.
32
+ task zip: [ :build ] do
33
+ version = JSON.parse(File.read(manifest_json))["version"]
34
+ cd(extdir) { sh "zip -r ../crx-web-console-#{version}.zip ./" }
35
+ end
36
+
37
+ desc 'Launch a browser with the chrome extension.'
38
+ task run: [ :build ] do
39
+ cd(rootdir) { sh "sh ./script/run_chrome.sh --load-extension=#{dist}" }
40
+ end
41
+ end
42
+
43
+ task :npm do
44
+ cd(rootdir) { sh "npm install --silent" }
45
+ end
46
+
47
+ namespace :lib do
48
+ templates = Pathname('lib/web_console/templates')
49
+ tmplib = rootdir.join('tmp/lib/')
50
+ js_erb = FileList.new(templates.join('**/*.js.erb'))
51
+ dirs = js_erb.pathmap("%{^#{templates},#{tmplib}}d")
52
+
53
+ task templates: dirs + js_erb.pathmap("%{^#{templates},#{tmplib}}X")
54
+
55
+ dirs.each { |d| directory d }
56
+ rule '.js' => [ "%{^#{tmplib},#{templates}}X.js.erb" ] do |t|
57
+ File.write(t.name, WebConsole::Testing::ERBPrecompiler.new(t.source).build)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,50 @@
1
+ namespace :test do
2
+ desc "Run tests for templates"
3
+ task templates: "templates:all"
4
+
5
+ namespace :templates do
6
+ task all: [ :daemonize, :npm, :rackup, :wait, :mocha, :kill, :exit ]
7
+ task serve: [ :npm, :rackup ]
8
+
9
+ work_dir = Pathname(EXPANDED_CWD).join("test/templates")
10
+ pid_file = Pathname(Dir.tmpdir).join("web_console.#{SecureRandom.uuid}.pid")
11
+ runner_uri = URI.parse("http://localhost:29292/html/spec_runner.html")
12
+ rackup_opts = "-p #{runner_uri.port}"
13
+ test_result = nil
14
+
15
+ def need_to_wait?(uri)
16
+ Net::HTTP.start(uri.host, uri.port) { |http| http.get(uri.path) }
17
+ rescue Errno::ECONNREFUSED
18
+ retry if yield
19
+ end
20
+
21
+ task :daemonize do
22
+ rackup_opts += " -D -P #{pid_file}"
23
+ end
24
+
25
+ task :npm do
26
+ Dir.chdir(work_dir) { system "npm install --silent" }
27
+ end
28
+
29
+ task :rackup do
30
+ Dir.chdir(work_dir) { system "bundle exec rackup #{rackup_opts}" }
31
+ end
32
+
33
+ task :wait do
34
+ cnt = 0
35
+ need_to_wait?(runner_uri) { sleep 1; cnt += 1; cnt < 5 }
36
+ end
37
+
38
+ task :mocha do
39
+ Dir.chdir(work_dir) { test_result = system("$(npm bin)/mocha-phantomjs #{runner_uri}") }
40
+ end
41
+
42
+ task :kill do
43
+ system "kill #{File.read pid_file}"
44
+ end
45
+
46
+ task :exit do
47
+ exit test_result
48
+ end
49
+ end
50
+ end
@@ -4,33 +4,6 @@ module WebConsole
4
4
  # It introduces template helpers to ease the inclusion of scripts only on
5
5
  # Rails error pages.
6
6
  class Template
7
- class Context < ActionView::Base
8
- # Execute a block only on error pages.
9
- #
10
- # The error pages are special, because they are the only pages that
11
- # currently require multiple bindings. We get those from exceptions.
12
- def only_on_error_page(*args)
13
- yield if @env['web_console.exception'].present?
14
- end
15
-
16
- # Render JavaScript inside a script tag and a closure.
17
- #
18
- # This one lets write JavaScript that will automatically get wrapped in a
19
- # script tag and enclosed in a closure, so you don't have to worry for
20
- # leaking globals, unless you explicitly want to.
21
- def render_javascript(template)
22
- render(template: template, layout: 'layouts/javascript')
23
- end
24
-
25
- # Render inlined string to be used inside of JavaScript code.
26
- #
27
- # The inlined string is returned as an actual JavaScript string. You
28
- # don't need to wrap the result yourself.
29
- def render_inlined_string(template)
30
- render(template: template, layout: 'layouts/inlined_string')
31
- end
32
- end
33
-
34
7
  # Lets you customize the default templates folder location.
35
8
  cattr_accessor :template_paths
36
9
  @@template_paths = [ File.expand_path('../templates', __FILE__) ]
@@ -38,12 +11,13 @@ module WebConsole
38
11
  def initialize(env, session)
39
12
  @env = env
40
13
  @session = session
14
+ @mount_point = Middleware.mount_point
41
15
  end
42
16
 
43
17
  # Render a template (inferred from +template_paths+) as a plain string.
44
18
  def render(template)
45
- context = Context.new(template_paths, instance_values)
46
- context.render(template: template, layout: false)
19
+ view = View.new(template_paths, instance_values)
20
+ view.render(template: template, layout: false)
47
21
  end
48
22
  end
49
23
  end
@@ -1,4 +1,5 @@
1
1
  <div id="console"
2
- data-remote-path='<%= "console/repl_sessions/#{@session.id}" %>'
3
- data-initial-prompt='>> '>
2
+ data-mount-point='<%= @mount_point %>'
3
+ data-session-id='<%= @session.id %>'
4
+ data-prompt-label='>> '>
4
5
  </div>
@@ -54,11 +54,69 @@ var styleElementId = 'sr02459pvbvrmhco';
54
54
 
55
55
  // REPLConsole Constructor
56
56
  function REPLConsole(config) {
57
+ function getConfig(key, defaultValue) {
58
+ return config && config[key] || defaultValue;
59
+ }
60
+
57
61
  this.commandStorage = new CommandStorage();
58
- this.prompt = config && config.promptLabel ? config.promptLabel : ' >>';
59
- this.commandHandle = config && config.commandHandle ? config.commandHandle : function() { return this; }
62
+ this.prompt = getConfig('promptLabel', ' >>');
63
+ this.mountPoint = getConfig('mountPoint');
64
+ this.sessionId = getConfig('sessionId');
60
65
  }
61
66
 
67
+ REPLConsole.prototype.getSessionUrl = function(path) {
68
+ var parts = [ this.mountPoint, 'repl_sessions', this.sessionId ];
69
+ if (path) {
70
+ parts.push(path);
71
+ }
72
+ // Join and remove duplicate slashes.
73
+ return parts.join('/').replace(/([^:]\/)\/+/g, '$1');
74
+ };
75
+
76
+ REPLConsole.prototype.commandHandle = function(line, callback) {
77
+ var self = this;
78
+ var params = 'input=' + encodeURIComponent(line);
79
+ callback = callback || function() {};
80
+
81
+ function isSuccess(status) {
82
+ return status >= 200 && status < 300 || status === 304;
83
+ }
84
+
85
+ function parseJSON(text) {
86
+ try {
87
+ return JSON.parse(text);
88
+ } catch (e) {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ function getErrorText(xhr) {
94
+ if (!xhr.status) {
95
+ return "<%= t 'errors.connection_refused' %>";
96
+ } else {
97
+ return xhr.status + ' ' + xhr.statusText;
98
+ }
99
+ }
100
+
101
+ putRequest(self.getSessionUrl(), params, function(xhr) {
102
+ var response = parseJSON(xhr.responseText);
103
+ var result = isSuccess(xhr.status);
104
+ if (result) {
105
+ self.writeOutput(response.output);
106
+ } else {
107
+ if (response && response.output) {
108
+ self.writeError(response.output);
109
+ } else {
110
+ self.writeError(getErrorText(xhr));
111
+ }
112
+ }
113
+ callback(result, response);
114
+ });
115
+ };
116
+
117
+ REPLConsole.prototype.uninstall = function() {
118
+ this.container.parentNode.removeChild(this.container);
119
+ };
62
120
 
63
121
  REPLConsole.prototype.install = function(container) {
64
122
  var _this = this;
@@ -99,8 +157,8 @@ REPLConsole.prototype.install = function(container) {
99
157
 
100
158
  // Make the console resizable.
101
159
  function resizeContainer(ev) {
102
- var startY = ev.clientY;
103
- var startHeight = parseInt(document.defaultView.getComputedStyle(container).height, 10);
160
+ var startY = ev.clientY;
161
+ var startHeight = parseInt(document.defaultView.getComputedStyle(container).height, 10);
104
162
  var scrollTopStart = consoleOuter.scrollTop;
105
163
  var clientHeightStart = consoleOuter.clientHeight;
106
164
 
@@ -137,10 +195,10 @@ REPLConsole.prototype.install = function(container) {
137
195
  }
138
196
 
139
197
  // Initialize
198
+ this.container = container;
140
199
  this.outer = consoleOuter;
141
200
  this.inner = findChild(this.outer, 'console-inner');
142
201
  this.clipboard = findChild(container, 'clipboard');
143
- this.remotePath = container.dataset.remotePath;
144
202
  this.newPromptBox();
145
203
  this.insertCss();
146
204
 
@@ -263,6 +321,13 @@ REPLConsole.prototype.writeOutput = function(output) {
263
321
  consoleMessage.innerHTML = escapeHTML(output);
264
322
  this.inner.appendChild(consoleMessage);
265
323
  this.newPromptBox();
324
+ return consoleMessage;
325
+ };
326
+
327
+ REPLConsole.prototype.writeError = function(output) {
328
+ var consoleMessage = this.writeOutput(output);
329
+ addClass(consoleMessage, "error-message");
330
+ return consoleMessage;
266
331
  };
267
332
 
268
333
  REPLConsole.prototype.onEnterKey = function() {
@@ -385,7 +450,7 @@ REPLConsole.prototype.scrollToBottom = function() {
385
450
 
386
451
  // Change the binding of the console
387
452
  REPLConsole.prototype.switchBindingTo = function(frameId, callback) {
388
- var url = this.remotePath + "/trace";
453
+ var url = this.getSessionUrl('trace');
389
454
  var params = "frame_id=" + encodeURIComponent(frameId);
390
455
  postRequest(url, params, callback);
391
456
  };
@@ -394,22 +459,16 @@ REPLConsole.prototype.switchBindingTo = function(frameId, callback) {
394
459
  * Install the console into the element with a specific ID.
395
460
  * Example: REPLConsole.installInto("target-id")
396
461
  */
397
- REPLConsole.installInto = function(id) {
462
+ REPLConsole.installInto = function(id, options) {
398
463
  var consoleElement = document.getElementById(id);
399
- var remotePath = consoleElement.dataset.remotePath;
400
- var replConsole = new REPLConsole({
401
- promptLabel: consoleElement.dataset.initialPrompt,
402
- commandHandle: function(line) {
403
- var _this = this;
404
- var url = remotePath;
405
- var params = "input=" + encodeURIComponent(line);
406
- putRequest(url, params, function(xhr) {
407
- var response = JSON.parse(xhr.responseText);
408
- _this.writeOutput(response.output);
409
- });
410
- }
411
- });
412
464
 
465
+ options = options || {};
466
+
467
+ for (var prop in consoleElement.dataset) {
468
+ options[prop] = options[prop] || consoleElement.dataset[prop];
469
+ }
470
+
471
+ var replConsole = new REPLConsole(options);
413
472
  replConsole.install(consoleElement);
414
473
  return replConsole;
415
474
  };
@@ -419,6 +478,26 @@ REPLConsole.installInto = function(id) {
419
478
  // It allows to operate the current session from the other scripts.
420
479
  REPLConsole.currentSession = null;
421
480
 
481
+ // This line is for the Firefox Add-on, because it doesn't have XMLHttpRequest as default.
482
+ // And so we need to require a module compatible with XMLHttpRequest from SDK.
483
+ REPLConsole.XMLHttpRequest = typeof XMLHttpRequest === 'undefined' ? null : XMLHttpRequest;
484
+
485
+ REPLConsole.request = function request(method, url, params, callback) {
486
+ var xhr = new REPLConsole.XMLHttpRequest();
487
+
488
+ xhr.open(method, url, true);
489
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
490
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
491
+ xhr.setRequestHeader("Accept", "<%= Mime::WEB_CONSOLE_V2 %>");
492
+ xhr.send(params);
493
+
494
+ xhr.onreadystatechange = function() {
495
+ if (xhr.readyState === 4) {
496
+ callback(xhr);
497
+ }
498
+ };
499
+ };
500
+
422
501
  // DOM helpers
423
502
  function hasClass(el, className) {
424
503
  var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g');
@@ -470,28 +549,16 @@ function escapeHTML(html) {
470
549
  }
471
550
 
472
551
  // XHR helpers
473
- function request(method, url, params, callback) {
474
- var xhr = new XMLHttpRequest();
475
-
476
- xhr.open(method, url, true);
477
- xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
478
- xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
479
- xhr.setRequestHeader("Accept", "<%= Mime::WEB_CONSOLE_V2 %>");
480
- xhr.send(params);
481
-
482
- xhr.onreadystatechange = function() {
483
- if (xhr.readyState === 4) {
484
- callback(xhr);
485
- }
486
- }
552
+ function postRequest() {
553
+ REPLConsole.request.apply(this, ["POST"].concat([].slice.call(arguments)));
487
554
  }
488
555
 
489
- function postRequest(url, params, callback) {
490
- request("POST", url, params, callback);
556
+ function putRequest() {
557
+ REPLConsole.request.apply(this, ["PUT"].concat([].slice.call(arguments)));
491
558
  }
492
559
 
493
- function putRequest(url, params, callback) {
494
- request("PUT", url, params, callback);
560
+ if (typeof exports === 'object') {
561
+ exports.REPLConsole = REPLConsole;
562
+ } else {
563
+ window.REPLConsole = REPLConsole;
495
564
  }
496
-
497
- window.REPLConsole = REPLConsole;
@@ -10,6 +10,7 @@
10
10
  .console .console-inner { font-family: monospace; font-size: 11px; width: 100%; height: 100%; overflow: none; background: #333; }
11
11
  .console .console-prompt-box { color: #FFF; }
12
12
  .console .console-message { color: #1AD027; margin: 0; border: 0; white-space: pre-wrap; background-color: #333; padding: 0; }
13
+ .console .console-message.error-message { color: #fc9; }
13
14
  .console .console-focus .console-cursor { background: #FEFEFE; color: #333; font-weight: bold; }
14
15
  .console .resizer { background: #333; width: 100%; height: 4px; cursor: ns-resize; }
15
16
  .console .console-actions { padding-right: 3px; }
@@ -20,3 +21,7 @@
20
21
  .console .clipboard { height: 0px; padding: 0px; margin: 0px; width: 0px; margin-left: -1000px; }
21
22
  .console .console-prompt-label { display: inline; color: #FFF; background: none repeat scroll 0% 0% #333; border: 0; padding: 0; }
22
23
  .console .console-prompt-display { display: inline; color: #FFF; background: none repeat scroll 0% 0% #333; border: 0; padding: 0; }
24
+ .console.full-screen { height: 100%; }
25
+ .console.full-screen .console-outer { padding-top: 3px; }
26
+ .console.full-screen .resizer { display: none; }
27
+ .console.full-screen .close-button { display: none; }
@@ -0,0 +1,25 @@
1
+ require 'web_console/testing/helper'
2
+ require 'web_console/testing/fake_middleware'
3
+
4
+ module WebConsole
5
+ module Testing
6
+ # This class is to pre-compile 'templates/*.erb'.
7
+ class ERBPrecompiler
8
+ def initialize(path)
9
+ @erb = ERB.new(File.read(path))
10
+ @view = FakeMiddleware.new(
11
+ view_path: Helper.gem_root.join('lib/web_console/templates'),
12
+ ).view
13
+ end
14
+
15
+ def build
16
+ @erb.result(binding)
17
+ end
18
+
19
+ def method_missing(name, *args, &block)
20
+ return super unless @view.respond_to?(name)
21
+ @view.send(name, *args, &block)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,43 @@
1
+ require 'action_view'
2
+ require 'action_dispatch'
3
+ require 'active_support/core_ext/string/access'
4
+ require 'json'
5
+ require 'web_console/whitelist'
6
+ require 'web_console/request'
7
+ require 'web_console/view'
8
+ require 'web_console/testing/helper'
9
+
10
+ module WebConsole
11
+ module Testing
12
+ class FakeMiddleware
13
+ I18n.load_path.concat(Dir[Helper.gem_root.join('lib/web_console/locales/*.yml')])
14
+
15
+ DEFAULT_HEADERS = { "Content-Type" => "application/javascript" }
16
+
17
+ def initialize(opts)
18
+ @headers = opts.fetch(:headers, DEFAULT_HEADERS)
19
+ @req_path_regex = opts[:req_path_regex]
20
+ @view_path = opts[:view_path]
21
+ end
22
+
23
+ def call(env)
24
+ [ 200, @headers, [ render(req_path(env)) ] ]
25
+ end
26
+
27
+ def view
28
+ @view ||= View.new(@view_path)
29
+ end
30
+
31
+ private
32
+
33
+ # extract target path from REQUEST_PATH
34
+ def req_path(env)
35
+ env["REQUEST_PATH"].match(@req_path_regex)[1]
36
+ end
37
+
38
+ def render(template)
39
+ view.render(template: template, layout: nil)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ module WebConsole
2
+ module Testing
3
+ module Helper
4
+ def self.gem_root
5
+ Pathname(File.expand_path('../../../../', __FILE__))
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,3 +1,3 @@
1
1
  module WebConsole
2
- VERSION = '2.2.1'
2
+ VERSION = '2.3.0'
3
3
  end
@@ -0,0 +1,37 @@
1
+ module WebConsole
2
+ class View < ActionView::Base
3
+ # Execute a block only on error pages.
4
+ #
5
+ # The error pages are special, because they are the only pages that
6
+ # currently require multiple bindings. We get those from exceptions.
7
+ def only_on_error_page(*args)
8
+ yield if @env['web_console.exception'].present?
9
+ end
10
+
11
+ # Render JavaScript inside a script tag and a closure.
12
+ #
13
+ # This one lets write JavaScript that will automatically get wrapped in a
14
+ # script tag and enclosed in a closure, so you don't have to worry for
15
+ # leaking globals, unless you explicitly want to.
16
+ def render_javascript(template)
17
+ render(template: template, layout: 'layouts/javascript')
18
+ end
19
+
20
+ # Render inlined string to be used inside of JavaScript code.
21
+ #
22
+ # The inlined string is returned as an actual JavaScript string. You
23
+ # don't need to wrap the result yourself.
24
+ def render_inlined_string(template)
25
+ render(template: template, layout: 'layouts/inlined_string')
26
+ end
27
+
28
+ # Override method for ActionView::Helpers::TranslationHelper#t.
29
+ #
30
+ # This method escapes the original return value for JavaScript, since the
31
+ # method returns a HTML tag with some attributes when the key is not found,
32
+ # so it could cause a syntax error if we use the value in the string literals.
33
+ def t(key, options = {})
34
+ j super
35
+ end
36
+ end
37
+ end
@@ -11,13 +11,6 @@ module WebConsole
11
11
  end
12
12
  end
13
13
 
14
- def acceptable_content_type?
15
- whine_unless request.acceptable_content_type? do
16
- "Cannot render console with content type #{request.content_type}" \
17
- "Allowed content types: #{request.acceptable_content_types}"
18
- end
19
- end
20
-
21
14
  private
22
15
 
23
16
  def whine_unless(condition)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web-console
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.1
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charlie Somerville
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2015-07-10 00:00:00.000000000 Z
14
+ date: 2016-01-27 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: railties
@@ -127,10 +127,14 @@ files:
127
127
  - lib/web_console/integration/cruby.rb
128
128
  - lib/web_console/integration/jruby.rb
129
129
  - lib/web_console/integration/rubinius.rb
130
+ - lib/web_console/locales/en.yml
130
131
  - lib/web_console/middleware.rb
131
132
  - lib/web_console/railtie.rb
132
133
  - lib/web_console/request.rb
134
+ - lib/web_console/response.rb
133
135
  - lib/web_console/session.rb
136
+ - lib/web_console/tasks/extensions.rake
137
+ - lib/web_console/tasks/test_templates.rake
134
138
  - lib/web_console/template.rb
135
139
  - lib/web_console/templates/_inner_console_markup.html.erb
136
140
  - lib/web_console/templates/_markup.html.erb
@@ -142,8 +146,11 @@ files:
142
146
  - lib/web_console/templates/layouts/javascript.erb
143
147
  - lib/web_console/templates/main.js.erb
144
148
  - lib/web_console/templates/style.css.erb
145
- - lib/web_console/tracer.rb
149
+ - lib/web_console/testing/erb_precompiler.rb
150
+ - lib/web_console/testing/fake_middleware.rb
151
+ - lib/web_console/testing/helper.rb
146
152
  - lib/web_console/version.rb
153
+ - lib/web_console/view.rb
147
154
  - lib/web_console/whiny_request.rb
148
155
  - lib/web_console/whitelist.rb
149
156
  homepage: https://github.com/rails/web-console
@@ -166,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
166
173
  version: '0'
167
174
  requirements: []
168
175
  rubyforge_project:
169
- rubygems_version: 2.4.5
176
+ rubygems_version: 2.5.1
170
177
  signing_key:
171
178
  specification_version: 4
172
179
  summary: A debugging tool for your Ruby on Rails applications.
@@ -1,11 +0,0 @@
1
- module WebConsole
2
- class BindingTracer
3
- def initialize(exception)
4
- @bindings = exception.bindings
5
- @backtrace = exception.backtrace
6
- end
7
-
8
- def binding_for_trace(trace)
9
- end
10
- end
11
- end