web-console-3.6.2/0000755000004100000410000000000013273331137013772 5ustar www-datawww-dataweb-console-3.6.2/CHANGELOG.markdown0000644000004100000410000001470413273331137017033 0ustar www-datawww-data# CHANGELOG ## master (unreleased) ## 3.6.2 * [#255](https://github.com/rails/web-console/pull/255) Fix the truncated HTML body, because of wrong Content-Length header ([@timomeh]) ## 3.6.1 * [#252](https://github.com/rails/web-console/pull/252) Fix improper injection in Rack bodies like ActionDispatch::Response::RackBody ([@gsamokovarov]) ## 3.6.0 * [#254](https://github.com/rails/web-console/pull/254) Rescue ActionDispatch::RemoteIp::IpSpoofAttackError ([@wjordan]) * [#250](https://github.com/rails/web-console/pull/250) Close original body to comply with Rack SPEC ([@wagenet]) * [#249](https://github.com/rails/web-console/pull/249) Update for frozen-string-literal friendliness ([@pat]) * [#248](https://github.com/rails/web-console/pull/248) Fix copy on Safari ([@ybart]) * [#246](https://github.com/rails/web-console/pull/246) International keyboard special character input fixes ([@fl0l0u]) * [#244](https://github.com/rails/web-console/pull/244) Let WebConsole.logger respect Rails.logger ([@gsamokovarov]) ## 3.5.1 * [#239](https://github.com/rails/web-console/pull/239) Fix the ActionDispatch::DebugExceptions integration ([@gsamokovarov]) ## 3.5.0 * [#237](https://github.com/rails/web-console/pull/237) Bindex integration for JRuby 9k support ([@gsamokovarov]) * [#236](https://github.com/rails/web-console/pull/236) Remove unused Active Support lazy load hook ([@betesh]) * [#230](https://github.com/rails/web-console/pull/230) Handle invalid remote addresses ([@akirakoyasu]) ## 3.4.0 * [#205](https://github.com/rails/web-console/pull/205) Introduce autocompletion ([@sh19910711]) ## 3.3.1 Drop support for Rails `4.2.0`. ## 3.3.0 * [#203](https://github.com/rails/web-console/pull/203) Map bindings to traces based on the trace __FILE__ and __LINE__ ([@gsamokovarov]) ## 3.2.1 * [#202](https://github.com/rails/web-console/pull/202) Use first binding when there is no application binding ([@sh19910711]) ## 3.2.0 * [#198](https://github.com/rails/web-console/pull/198) Pick the first application trace binding on errors ([@sh19910711]) * [#189](https://github.com/rails/web-console/pull/189) Silence ActionView rendering information ([@gsamokovarov]) ## 3.1.1 * [#185](https://github.com/rails/web-console/pull/185) Fix `rails console` startup ([@gsamokovarov]) ## 3.1.0 * [#182](https://github.com/rails/web-console/pull/182) Let `#console` live in `Kernel` ([@schneems]) * [#181](https://github.com/rails/web-console/pull/181) Log internal Web Console errors ([@gsamokovarov]) * [#180](https://github.com/rails/web-console/pull/180) Autoload Web Console constants for faster Rails boot time ([@herminiotorres]) ## 3.0.0 * [#173](https://github.com/rails/web-console/pull/173) Revert "Change config.development_only default until 4.2.4 is released" ([@gsamokovarov]) * [#171](https://github.com/rails/web-console/pull/171) Fixed blocked IP logging ([@gsamokovarov]) * [#162](https://github.com/rails/web-console/pull/162) Render the console inside the body tag ([@gsamokovarov]) * [#165](https://github.com/rails/web-console/pull/165) Revamped integrations for CRuby and Rubinius ([@gsamokovarov]) ## 2.3.0 This is mainly a Rails 5 compatibility release. If you have the chance, please go to 3.1.0 instead. * [#181](https://github.com/rails/web-console/pull/181) Log internal Web Console errors ([@schneems]) * [#150](https://github.com/rails/web-console/pull/150) Revert #150. ([@gsamokovarov]) ## 2.2.1 * [#150](https://github.com/rails/web-console/pull/150) Change config.development_only default until 4.2.4 is released ([@gsamokovarov]) ## 2.2.0 * [#140](https://github.com/rails/web-console/pull/140) Add the ability to close the console on each page ([@sh19910711]) * [#135](https://github.com/rails/web-console/pull/135) Run the console only in development mode and raise warning in tests ([@frenesim]) * [#134](https://github.com/rails/web-conscle/pull/134) Force development only web console by default ([@gsamokovarov]) * [#123](https://github.com/rails/web-console/pull/123) Replace deprecated `alias_method_chain` with `alias_method` ([@jonatack]) ## 2.1.3 * Fix remote code execution vulnerability in Web Console. CVE-2015-3224. ## 2.1.2 * [#115](https://github.com/rails/web-console/pull/115) Show proper binding when raising an error in a template ([@gsamokovarov]) * [#114](https://github.com/rails/web-console/pull/114) Fix templates non rendering, because of missing template suffix ([@gsamokovarov]) ## 2.1.1 * [#112](https://github.com/rails/web-console/pull/112) Always allow application/x-www-form-urlencoded content type ([@gsamokovarov]) ## 2.1.0 * [#109](https://github.com/rails/web-console/pull/109) Revamp unavailable session response message ([@gsamokovarov]) * [#107](https://github.com/rails/web-console/pull/107) Fix pasting regression for all browsers ([@parterburn]) * [#105](https://github.com/rails/web-console/pull/105) Lock scroll bottom on console window resize ([@noahpatterson]) * [#104](https://github.com/rails/web-console/pull/104) Always whitelist localhost and inform users why no console is displayed ([@gsamokovarov]) * [#100](https://github.com/rails/web-console/pull/100) Accept text/plain as acceptable content type for Puma ([@gsamokovarov]) * [#98](https://github.com/rails/web-console/pull/98) Add arbitrary big z-index to the console ([@bglbruno]) * [#88](https://github.com/rails/web-console/pull/88) Spelling fixes ([@jeffnv]) * [#86](https://github.com/rails/web-console/pull/86) Disable autofocus when initializing the console ([@ryandao]) * [#84](https://github.com/rails/web-console/pull/84) Allow Rails 5 as dependency in gemspec ([@jonatack]) * [#69](https://github.com/rails/web-console/pull/69) Introduce middleware for request dispatch and console rendering ([@gsamokovarov]) [@jonatack]: https://github.com/jonatack [@ryandao]: https://github.com/ryandao [@jeffnv]: https://github.com/jeffnv [@gsamokovarov]: https://github.com/gsamokovarov [@bglbruno]: https://github.com/bglbruno [@noahpatterson]: https://github.com/noahpatterson [@parterburn]: https://github.com/parterburn [@sh19910711]: https://github.com/sh19910711 [@frenesim]: https://github.com/frenesim [@herminiotorres]: https://github.com/herminiotorres [@schneems]: https://github.com/schneems [@betesh]: https://github.com/betesh [@akirakoyasu]: https://github.com/akirakoyasu [@wagenet]: https://github.com/wagenet [@wjordan]: https://github.com/wjordan [@pat]: https://github.com/pat [@ybart]: https://github.com/ybart [@fl0l0u]: https://github.com/fl0l0u [@timomeh]: https://github.com/timomeh web-console-3.6.2/Rakefile0000644000004100000410000000114613273331137015441 0ustar www-datawww-data# frozen_string_literal: true begin require "bundler/setup" rescue LoadError puts "You must `gem install bundler` and `bundle install` to run rake tasks" end require "socket" require "rake/testtask" require "tmpdir" require "securerandom" require "json" require "web_console/testing/erb_precompiler" EXPANDED_CWD = File.expand_path(File.dirname(__FILE__)) Rake::TestTask.new(:test) do |t| t.libs << "lib" t.libs << "test" t.pattern = "test/**/*_test.rb" t.verbose = false end Dir["lib/web_console/tasks/**/*.rake"].each { |task| load task } Bundler::GemHelper.install_tasks task default: :test web-console-3.6.2/lib/0000755000004100000410000000000013273331137014540 5ustar www-datawww-dataweb-console-3.6.2/lib/web-console.rb0000644000004100000410000000006513273331137017303 0ustar www-datawww-data# frozen_string_literal: true require "web_console" web-console-3.6.2/lib/web_console/0000755000004100000410000000000013273331137017037 5ustar www-datawww-dataweb-console-3.6.2/lib/web_console/session.rb0000644000004100000410000000422113273331137021046 0ustar www-datawww-data# frozen_string_literal: true module WebConsole # A session lets you persist an +Evaluator+ instance in memory associated # with multiple bindings. # # Each newly created session is persisted into memory and you can find it # later by its +id+. # # A session may be associated with multiple bindings. This is used by the # error pages only, as currently, this is the only client that needs to do # that. class Session cattr_reader :inmemory_storage @@inmemory_storage = {} class << self # Finds a persisted session in memory by its id. # # Returns a persisted session if found in memory. # Raises NotFound error unless found in memory. def find(id) inmemory_storage[id] end # Create a Session from an binding or exception in a storage. # # The storage is expected to respond to #[]. The binding is expected in # :__web_console_binding and the exception in :__web_console_exception. # # Can return nil, if no binding or exception have been preserved in the # storage. def from(storage) if exc = storage[:__web_console_exception] new(ExceptionMapper.new(exc)) elsif binding = storage[:__web_console_binding] new([binding]) end end end # An unique identifier for every REPL. attr_reader :id def initialize(bindings) @id = SecureRandom.hex(16) @bindings = bindings @evaluator = Evaluator.new(@current_binding = bindings.first) store_into_memory end # Evaluate +input+ on the current Evaluator associated binding. # # Returns a string of the Evaluator output. def eval(input) @evaluator.eval(input) end # Switches the current binding to the one at specified +index+. # # Returns nothing. def switch_binding_to(index) @evaluator = Evaluator.new(@current_binding = @bindings[index.to_i]) end # Returns context of the current binding def context(objpath) Context.new(@current_binding).extract(objpath) end private def store_into_memory inmemory_storage[id] = self end end end web-console-3.6.2/lib/web_console/template.rb0000644000004100000410000000136413273331137021203 0ustar www-datawww-data# frozen_string_literal: true module WebConsole # A facade that handles template rendering and composition. # # It introduces template helpers to ease the inclusion of scripts only on # Rails error pages. class Template # Lets you customize the default templates folder location. cattr_accessor :template_paths @@template_paths = [ File.expand_path("../templates", __FILE__) ] def initialize(env, session) @env = env @session = session @mount_point = Middleware.mount_point end # Render a template (inferred from +template_paths+) as a plain string. def render(template) view = View.new(template_paths, instance_values) view.render(template: template, layout: false) end end end web-console-3.6.2/lib/web_console/version.rb0000644000004100000410000000011113273331137021042 0ustar www-datawww-data# frozen_string_literal: true module WebConsole VERSION = "3.6.2" end web-console-3.6.2/lib/web_console/tasks/0000755000004100000410000000000013273331137020164 5ustar www-datawww-dataweb-console-3.6.2/lib/web_console/tasks/extensions.rake0000644000004100000410000000350313273331137023230 0ustar www-datawww-data# frozen_string_literal: true namespace :ext do rootdir = Pathname("extensions") desc "Build Chrome Extension" task chrome: "chrome:build" namespace :chrome do dist = Pathname("dist/crx") extdir = rootdir.join(dist) manifest_json = rootdir.join("chrome/manifest.json") directory extdir task build: [ extdir, "lib:templates" ] do cd rootdir do cp_r [ "img/", "tmp/lib/" ], dist `cd chrome && git ls-files`.split("\n").each do |src| dest = dist.join(src) mkdir_p dest.dirname cp Pathname("chrome").join(src), dest end end end # Generate a .crx file. task crx: [ :build, :npm ] do out = "crx-web-console-#{JSON.parse(File.read(manifest_json))["version"]}.crx" cd(extdir) { sh "node \"$(npm bin)/crx\" pack ./ -p ../crx-web-console.pem -o ../#{out}" } end # Generate a .zip file for Chrome Web Store. task zip: [ :build ] do version = JSON.parse(File.read(manifest_json))["version"] cd(extdir) { sh "zip -r ../crx-web-console-#{version}.zip ./" } end desc "Launch a browser with the chrome extension." task run: [ :build ] do cd(rootdir) { sh "sh ./script/run_chrome.sh --load-extension=#{dist}" } end end task :npm do cd(rootdir) { sh "npm install --silent" } end namespace :lib do templates = Pathname("lib/web_console/templates") tmplib = rootdir.join("tmp/lib/") js_erb = FileList.new(templates.join("**/*.js.erb")) dirs = js_erb.pathmap("%{^#{templates},#{tmplib}}d") task templates: dirs + js_erb.pathmap("%{^#{templates},#{tmplib}}X") dirs.each { |d| directory d } rule ".js" => [ "%{^#{tmplib},#{templates}}X.js.erb" ] do |t| File.write(t.name, WebConsole::Testing::ERBPrecompiler.new(t.source).build) end end end web-console-3.6.2/lib/web_console/tasks/templates.rake0000644000004100000410000000270513273331137023032 0ustar www-datawww-data# frozen_string_literal: true namespace :templates do desc "Run tests for templates" task test: [ :daemonize, :npm, :rackup, :wait, :mocha, :kill, :exit ] task serve: [ :npm, :rackup ] workdir = Pathname(EXPANDED_CWD).join("test/templates") pid = Pathname(Dir.tmpdir).join("web_console_test.pid") runner = URI.parse("http://#{ENV['IP'] || '127.0.0.1'}:#{ENV['PORT'] || 29292}/html/test_runner.html") rackup = "rackup --host #{runner.host} --port #{runner.port}" result = nil browser = "phantomjs" def need_to_wait?(uri) Net::HTTP.start(uri.host, uri.port) { |http| http.get(uri.path) } rescue Errno::ECONNREFUSED retry if yield end task :daemonize do rackup += " -D --pid #{pid}" end task npm: [ :phantomjs ] do Dir.chdir(workdir) { system "npm install --silent" } end task :phantomjs do unless system("which #{browser} >/dev/null") browser = "./node_modules/.bin/phantomjs" Dir.chdir(workdir) { system("test -f #{browser} || npm install --silent phantomjs-prebuilt") } end end task :rackup do Dir.chdir(workdir) { system rackup } end task :wait do cnt = 0 need_to_wait?(runner) { sleep 1; cnt += 1; cnt < 5 } end task :mocha do Dir.chdir(workdir) { result = system("#{browser} ./node_modules/mocha-phantomjs-core/mocha-phantomjs-core.js #{runner} dot") } end task :kill do system "kill #{File.read pid}" end task :exit do exit result end end web-console-3.6.2/lib/web_console/injector.rb0000644000004100000410000000125013273331137021177 0ustar www-datawww-data# frozen_string_literal: true module WebConsole # Injects content into a Rack body. class Injector def initialize(body, headers) @body = "".dup body.each { |part| @body << part } body.close if body.respond_to?(:close) @headers = headers end def inject(content) # Remove any previously set Content-Length header because we modify # the body. Otherwise the response will be truncated. @headers.delete("Content-Length") [ if position = @body.rindex("") [ @body.insert(position, content) ] else [ @body << content ] end, @headers ] end end end web-console-3.6.2/lib/web_console/whiny_request.rb0000644000004100000410000000142413273331137022273 0ustar www-datawww-data# frozen_string_literal: true module WebConsole # Noisy wrapper around +Request+. # # If any calls to +from_whitelisted_ip?+ and +acceptable_content_type?+ # return false, an info log message will be displayed in users' logs. class WhinyRequest < SimpleDelegator def from_whitelisted_ip? whine_unless request.from_whitelisted_ip? do "Cannot render console from #{request.strict_remote_ip}! " \ "Allowed networks: #{request.whitelisted_ips}" end end private def whine_unless(condition) unless condition logger.info { yield } end condition end def logger env["action_dispatch.logger"] || WebConsole.logger end def request __getobj__ end end end web-console-3.6.2/lib/web_console/railtie.rb0000644000004100000410000000426113273331137021020 0ustar www-datawww-data# frozen_string_literal: true require "rails/railtie" module WebConsole class Railtie < ::Rails::Railtie config.web_console = ActiveSupport::OrderedOptions.new config.web_console.whitelisted_ips = %w( 127.0.0.1 ::1 ) initializer "web_console.initialize" do require "bindex" require "web_console/extensions" end initializer "web_console.development_only" do unless (config.web_console.development_only == false) || Rails.env.development? abort <<-END.strip_heredoc Web Console is activated in the #{Rails.env} environment. This is usually a mistake. To ensure it's only activated in development mode, move it to the development group of your Gemfile: gem 'web-console', group: :development If you still want to run it in the #{Rails.env} environment (and know what you are doing), put this in your Rails application configuration: config.web_console.development_only = false END end end initializer "web_console.insert_middleware" do |app| app.middleware.insert_before ActionDispatch::DebugExceptions, Middleware end initializer "web_console.mount_point" do if mount_point = config.web_console.mount_point Middleware.mount_point = mount_point.chomp("/") end if root = Rails.application.config.relative_url_root Middleware.mount_point = File.join(root, Middleware.mount_point) end end initializer "web_console.template_paths" do if template_paths = config.web_console.template_paths Template.template_paths.unshift(*Array(template_paths)) end end initializer "web_console.whitelisted_ips" do if whitelisted_ips = config.web_console.whitelisted_ips Request.whitelisted_ips = Whitelist.new(whitelisted_ips) end end initializer "web_console.whiny_requests" do if config.web_console.key?(:whiny_requests) Middleware.whiny_requests = config.web_console.whiny_requests end end initializer "i18n.load_path" do config.i18n.load_path.concat(Dir[File.expand_path("../locales/*.yml", __FILE__)]) end end end web-console-3.6.2/lib/web_console/templates/0000755000004100000410000000000013273331137021035 5ustar www-datawww-dataweb-console-3.6.2/lib/web_console/templates/console.js.erb0000644000004100000410000006465513273331137023624 0ustar www-datawww-data/** * Constructor for command storage. * It uses localStorage if available. Otherwise fallback to normal JS array. */ function CommandStorage() { this.previousCommands = []; var previousCommandOffset = 0; var hasLocalStorage = typeof window.localStorage !== 'undefined'; var STORAGE_KEY = "web_console_previous_commands"; var MAX_STORAGE = 100; if (hasLocalStorage) { this.previousCommands = JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; previousCommandOffset = this.previousCommands.length; } this.addCommand = function(command) { previousCommandOffset = this.previousCommands.push(command); if (previousCommandOffset > MAX_STORAGE) { this.previousCommands.splice(0, 1); previousCommandOffset = MAX_STORAGE; } if (hasLocalStorage) { localStorage.setItem(STORAGE_KEY, JSON.stringify(this.previousCommands)); } }; this.navigate = function(offset) { previousCommandOffset += offset; if (previousCommandOffset < 0) { previousCommandOffset = -1; return null; } if (previousCommandOffset >= this.previousCommands.length) { previousCommandOffset = this.previousCommands.length; return null; } return this.previousCommands[previousCommandOffset]; } } function Autocomplete(_words, prefix) { this.words = prepareWords(_words); this.current = -1; this.left = 0; // [left, right) this.right = this.words.length; this.confirmed = false; function createSpan(label, className) { var el = document.createElement('span'); addClass(el, className); el.innerText = label; return el; } function prepareWords(words) { // convert into an object with priority and element var res = new Array(words.length); for (var i = 0, ind = 0; i < words.length; ++i) { res[i] = new Array(words[i].length); for (var j = 0; j < words[i].length; ++j) { res[i][j] = { word: words[i][j], priority: i, element: createSpan(words[i][j], 'trimmed keyword') }; } } // flatten and sort by alphabetical order to refine incrementally res = flatten(res); res.sort(function(a, b) { return a.word == b.word ? 0 : (a.word < b.word ? -1 : 1); }); for (var i = 0; i < res.length; ++i) res[i].element.dataset.index = i; return res; } this.view = document.createElement('pre'); addClass(this.view, 'auto-complete console-message'); this.view.appendChild(this.prefix = createSpan('...', 'trimmed keyword')); this.view.appendChild(this.stage = document.createElement('span')); this.elements = this.stage.children; this.view.appendChild(this.suffix = createSpan('...', 'trimmed keyword')); this.refine(prefix || ''); } Autocomplete.prototype.getSelectedWord = function() { return this.lastSelected && this.lastSelected.innerText; }; Autocomplete.prototype.onFinished = function(callback) { this.onFinishedCallback = callback; if (this.confirmed) callback(this.confirmed); }; Autocomplete.prototype.onKeyDown = function(ev) { var self = this; if (!this.elements.length) return; function move(nextCurrent) { if (self.lastSelected) removeClass(self.lastSelected, 'selected'); addClass(self.lastSelected = self.elements[nextCurrent], 'selected'); self.trim(self.current, true); self.trim(nextCurrent, false); self.current = nextCurrent; } switch (ev.keyCode) { case 69: if (ev.ctrlKey) { move(this.current + 1 >= this.elements.length ? 0 : this.current + 1); return true; } return false; case 9: // Tab if (ev.shiftKey) { // move back move(this.current - 1 < 0 ? this.elements.length - 1 : this.current - 1); } else { // move next move(this.current + 1 >= this.elements.length ? 0 : this.current + 1); } return true; case 13: // Enter this.finish(); return true; case 27: // Esc this.cancel(); return true; case 37: case 38: case 39: case 40: // disable using arrow keys on completing return true; } return false; }; Autocomplete.prototype.trim = function(from, needToTrim) { var self = this; var num = 5; if (this.elements.length > num) { (0 < from ? removeClass : addClass)(this.prefix, 'trimmed'); (from + num < this.elements.length ? removeClass : addClass)(this.suffix, 'trimmed'); } else { addClass(this.prefix, 'trimmed'); addClass(this.suffix, 'trimmed'); } function iterate(x) { for (var i = 0; i < num; ++i, ++x) if (0 <= x && x < self.elements.length) { toggleClass(self.elements[x], 'trimmed'); } } var toggleClass = needToTrim ? addClass : removeClass; if (from < 0) { iterate(0); } else if (from + num - 1 >= this.elements.length) { iterate(this.elements.length - num); } else { iterate(from); } }; Autocomplete.prototype.refine = function(prefix) { if (this.confirmed) return; var inc = !this.prev || (prefix.length >= this.prev.length); this.prev = prefix; var self = this; function remove(parent, child) { if (parent == child.parentNode) parent.removeChild(child); } function toggle(el) { return inc ? remove(self.stage, el) : self.stage.appendChild(el); } function startsWith(str, prefix) { return !prefix || str.substr(0, prefix.length) === prefix; } function moveRight(l, r) { while (l < r && inc !== startsWith(self.words[l].word, prefix)) toggle(self.words[l++].element); return l; } function moveLeft(l, r) { while (l < r - 1 && inc !== startsWith(self.words[r-1].word, prefix)) toggle(self.words[--r].element); return r; } self.trim(self.current, true); // reset trimming // Refine the range of words having same prefix if (inc) { self.left = moveRight(self.left, self.right); self.right = moveLeft(self.left, self.right); } else { self.left = moveLeft(-1, self.left); self.right = moveRight(self.right, self.words.length); } // Render elements with sorting by scope groups var words = this.words.slice(this.left, this.right); words.sort(function(a, b) { return a.priority == b.priority ? (a.word < b.word ? -1 : 1) : (a.priority < b.priority ? -1 : 1); }); removeAllChildren(this.elements); for (var i = 0; i < words.length; ++i) { this.stage.appendChild(words[i].element); } // Keep a previous selected element if the refined range includes the element if (this.lastSelected && this.left <= this.lastSelected.dataset.index && this.lastSelected.dataset.index < this.right) { this.current = Array.prototype.indexOf.call(this.elements, this.lastSelected); this.trim(this.current, false); } else { if (this.lastSelected) removeClass(this.lastSelected, 'selected'); this.lastSelected = null; this.current = -1; this.trim(0, false); } if (self.left + 1 == self.right) { self.current = 0; self.finish(); } else if (self.left == self.right) { self.cancel(); } }; Autocomplete.prototype.finish = function() { if (0 <= this.current && this.current < this.elements.length) { this.confirmed = this.elements[this.current].innerText; if (this.onFinishedCallback) this.onFinishedCallback(this.confirmed); this.removeView(); } else { this.cancel(); } }; Autocomplete.prototype.cancel = function() { if (this.onFinishedCallback) this.onFinishedCallback(); this.removeView(); }; Autocomplete.prototype.removeView = function() { if (this.view.parentNode) this.view.parentNode.removeChild(this.view); removeAllChildren(this.view); } // HTML strings for dynamic elements. var consoleInnerHtml = <%= render_inlined_string '_inner_console_markup.html' %>; var promptBoxHtml = <%= render_inlined_string '_prompt_box_markup.html' %>; // CSS var consoleStyleCss = <%= render_inlined_string 'style.css' %>; // Insert a style element with the unique ID var styleElementId = 'sr02459pvbvrmhco'; // REPLConsole Constructor function REPLConsole(config) { function getConfig(key, defaultValue) { return config && config[key] || defaultValue; } this.commandStorage = new CommandStorage(); this.prompt = getConfig('promptLabel', ' >>'); this.mountPoint = getConfig('mountPoint'); this.sessionId = getConfig('sessionId'); this.autocomplete = false; } REPLConsole.prototype.getSessionUrl = function(path) { var parts = [ this.mountPoint, 'repl_sessions', this.sessionId ]; if (path) { parts.push(path); } // Join and remove duplicate slashes. return parts.join('/').replace(/([^:]\/)\/+/g, '$1'); }; REPLConsole.prototype.contextRequest = function(keyword, callback) { putRequest(this.getSessionUrl(), 'context=' + getContext(keyword), function(xhr) { if (xhr.status == 200) { callback(null, JSON.parse(xhr.responseText)); } else { callback(xhr.statusText); } }); }; REPLConsole.prototype.commandHandle = function(line, callback) { var self = this; var params = 'input=' + encodeURIComponent(line); callback = callback || function() {}; function isSuccess(status) { return status >= 200 && status < 300 || status === 304; } function parseJSON(text) { try { return JSON.parse(text); } catch (e) { return null; } } function getErrorText(xhr) { if (!xhr.status) { return "Connection Refused"; } else { return xhr.status + ' ' + xhr.statusText; } } putRequest(self.getSessionUrl(), params, function(xhr) { var response = parseJSON(xhr.responseText); var result = isSuccess(xhr.status); if (result) { self.writeOutput(response.output); } else { if (response && response.output) { self.writeError(response.output); } else { self.writeError(getErrorText(xhr)); } } callback(result, response); }); }; REPLConsole.prototype.uninstall = function() { this.container.parentNode.removeChild(this.container); }; REPLConsole.prototype.install = function(container) { var _this = this; document.onkeydown = function(ev) { if (_this.focused) { _this.onKeyDown(ev); } }; document.onkeypress = function(ev) { if (_this.focused) { _this.onKeyPress(ev); } }; document.addEventListener('mousedown', function(ev) { var el = ev.target || ev.srcElement; if (el) { do { if (el === container) { _this.focus(); return; } } while (el = el.parentNode); _this.blur(); } }); // Render the console. container.innerHTML = consoleInnerHtml; var consoleOuter = findChild(container, 'console-outer'); var consoleActions = findChild(consoleOuter, 'console-actions'); addClass(container, 'console'); addClass(container.getElementsByClassName('layer'), 'pos-absolute border-box'); addClass(container.getElementsByClassName('button'), 'border-box'); addClass(consoleActions, 'pos-fixed pos-right'); // Make the console resizable. function resizeContainer(ev) { var startY = ev.clientY; var startHeight = parseInt(document.defaultView.getComputedStyle(container).height, 10); var scrollTopStart = consoleOuter.scrollTop; var clientHeightStart = consoleOuter.clientHeight; var doDrag = function(e) { container.style.height = (startHeight + startY - e.clientY) + 'px'; consoleOuter.scrollTop = scrollTopStart + (clientHeightStart - consoleOuter.clientHeight); shiftConsoleActions(); }; var stopDrag = function(e) { document.documentElement.removeEventListener('mousemove', doDrag, false); document.documentElement.removeEventListener('mouseup', stopDrag, false); }; document.documentElement.addEventListener('mousemove', doDrag, false); document.documentElement.addEventListener('mouseup', stopDrag, false); } function closeContainer(ev) { container.parentNode.removeChild(container); } var shifted = false; function shiftConsoleActions() { if (consoleOuter.scrollHeight > consoleOuter.clientHeight) { var widthDiff = document.documentElement.clientWidth - consoleOuter.clientWidth; if (shifted || ! widthDiff) return; shifted = true; consoleActions.style.marginRight = widthDiff + 'px'; } else if (shifted) { shifted = false; consoleActions.style.marginRight = '0px'; } } // Initialize this.container = container; this.outer = consoleOuter; this.inner = findChild(this.outer, 'console-inner'); this.clipboard = findChild(container, 'clipboard'); this.suggestWait = 1500; this.newPromptBox(); this.insertCss(); findChild(container, 'resizer').addEventListener('mousedown', resizeContainer); findChild(consoleActions, 'close-button').addEventListener('click', closeContainer); consoleOuter.addEventListener('DOMNodeInserted', shiftConsoleActions); REPLConsole.currentSession = this; }; // Add CSS styles dynamically. This probably doesnt work for IE <8. REPLConsole.prototype.insertCss = function() { if (document.getElementById(styleElementId)) { return; // already inserted } var style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = consoleStyleCss; style.id = styleElementId; document.getElementsByTagName('head')[0].appendChild(style); }; REPLConsole.prototype.focus = function() { if (! this.focused) { this.focused = true; if (! hasClass(this.inner, "console-focus")) { addClass(this.inner, "console-focus"); } this.scrollToBottom(); } }; REPLConsole.prototype.blur = function() { this.focused = false; removeClass(this.inner, "console-focus"); }; /** * Add a new empty prompt box to the console. */ REPLConsole.prototype.newPromptBox = function() { // Remove the caret from previous prompt display if any. if (this.promptDisplay) { this.removeCaretFromPrompt(); } var promptBox = document.createElement('div'); promptBox.className = "console-prompt-box"; promptBox.innerHTML = promptBoxHtml; this.promptLabel = promptBox.getElementsByClassName('console-prompt-label')[0]; this.promptDisplay = promptBox.getElementsByClassName('console-prompt-display')[0]; // Render the prompt box this.setInput(""); this.promptLabel.innerHTML = this.prompt; this.inner.appendChild(promptBox); this.scrollToBottom(); }; /** * Remove the caret from the prompt box, * mainly before adding a new prompt box. * For simplicity, just re-render the prompt box * with caret position -1. */ REPLConsole.prototype.removeCaretFromPrompt = function() { this.setInput(this._input, -1); }; REPLConsole.prototype.getSuggestion = function(keyword) { var self = this; function show(found) { if (!found) return; var hint = self.promptDisplay.childNodes[1]; hint.className = 'console-hint'; hint.dataset.keyword = found; hint.innerText = found.substr(self.suggestKeyword.length); // clear hinting information after timeout in a few time if (self.suggestTimeout) clearTimeout(self.suggestTimeout); self.suggestTimeout = setTimeout(function() { self.renderInput() }, self.suggestWait); } function find(context) { var k = self.suggestKeyword; for (var i = 0; i < context.length; ++i) if (context[i].substr(0, k.length) === k) { if (context[i] === k) return; return context[i]; } } function request(keyword, callback) { self.contextRequest(keyword, function(err, res) { if (err) throw new Error(err); var c = flatten(res['context']); c.sort(); callback(c); }); } self.suggestKeyword = keyword; var input = getContext(keyword); if (keyword.length - input.length < 3) return; if (self.suggestInput !== input) { self.suggestInput = input; request(keyword, function(c) { show(find(self.suggestContext = c)); }); } else if (self.suggestContext) { show(find(self.suggestContext)); } }; REPLConsole.prototype.getHintKeyword = function() { var hint = this.promptDisplay.childNodes[1]; return hint.className === 'console-hint' && hint.dataset.keyword; }; REPLConsole.prototype.setInput = function(input, caretPos) { if (input == null) return; // keep value if input is undefined this._caretPos = caretPos === undefined ? input.length : caretPos; this._input = input; if (this.autocomplete) this.autocomplete.refine(this.getCurrentWord()); this.renderInput(); if (!this.autocomplete && input.length == this._caretPos) this.getSuggestion(this.getCurrentWord()); }; /** * Add some text to the existing input. */ REPLConsole.prototype.addToInput = function(val, caretPos) { caretPos = caretPos || this._caretPos; var before = this._input.substring(0, caretPos); var after = this._input.substring(caretPos, this._input.length); var newInput = before + val + after; this.setInput(newInput, caretPos + val.length); }; /** * Render the input prompt. This is called whenever * the user input changes, sometimes not very efficient. */ REPLConsole.prototype.renderInput = function() { // Clear the current input. removeAllChildren(this.promptDisplay); var before, current, after; var center = document.createElement('span'); if (this._caretPos < 0) { before = this._input; current = after = ""; } else if (this._caretPos === this._input.length) { before = this._input; current = "\u00A0"; after = ""; } else { before = this._input.substring(0, this._caretPos); current = this._input.charAt(this._caretPos); after = this._input.substring(this._caretPos + 1, this._input.length); } this.promptDisplay.appendChild(document.createTextNode(before)); this.promptDisplay.appendChild(center); this.promptDisplay.appendChild(document.createTextNode(after)); var hint = this.autocomplete && this.autocomplete.getSelectedWord(); addClass(center, hint ? 'console-hint' : 'console-cursor'); center.appendChild(document.createTextNode(hint ? hint.substr(this.getCurrentWord().length) : current)); }; REPLConsole.prototype.writeOutput = function(output) { var consoleMessage = document.createElement('pre'); consoleMessage.className = "console-message"; consoleMessage.innerHTML = escapeHTML(output); this.inner.appendChild(consoleMessage); this.newPromptBox(); return consoleMessage; }; REPLConsole.prototype.writeError = function(output) { var consoleMessage = this.writeOutput(output); addClass(consoleMessage, "error-message"); return consoleMessage; }; REPLConsole.prototype.onEnterKey = function() { var input = this._input; if(input != "" && input !== undefined) { this.commandStorage.addCommand(input); } this.commandHandle(input); }; REPLConsole.prototype.onTabKey = function() { var self = this; var hintKeyword; if (hintKeyword = self.getHintKeyword()) { self.swapCurrentWord(hintKeyword); return; } if (self.autocomplete) return; self.autocomplete = new Autocomplete([]); self.contextRequest(self.getCurrentWord(), function(err, obj) { if (err) return self.autocomplete = false; self.autocomplete = new Autocomplete(obj['context'], self.getCurrentWord()); self.inner.appendChild(self.autocomplete.view); self.autocomplete.onFinished(function(word) { self.swapCurrentWord(word); self.autocomplete = false; }); self.scrollToBottom(); }); }; REPLConsole.prototype.onNavigateHistory = function(offset) { var command = this.commandStorage.navigate(offset) || ""; this.setInput(command); }; /** * Handle control keys like up, down, left, right. */ REPLConsole.prototype.onKeyDown = function(ev) { if (this.autocomplete && this.autocomplete.onKeyDown(ev)) { this.renderInput(); ev.preventDefault(); ev.stopPropagation(); return; } switch (ev.keyCode) { case 69: // Ctrl-E if (ev.ctrlKey) { this.onTabKey(); ev.preventDefault(); } break; case 9: // Tab this.onTabKey(); ev.preventDefault(); break; case 13: // Enter key this.onEnterKey(); ev.preventDefault(); break; case 80: // Ctrl-P if (! ev.ctrlKey) break; case 38: // Up arrow this.onNavigateHistory(-1); ev.preventDefault(); break; case 78: // Ctrl-N if (! ev.ctrlKey) break; case 40: // Down arrow this.onNavigateHistory(1); ev.preventDefault(); break; case 37: // Left arrow var caretPos = this._caretPos > 0 ? this._caretPos - 1 : this._caretPos; this.setInput(this._input, caretPos); ev.preventDefault(); break; case 39: // Right arrow var length = this._input.length; var caretPos = this._caretPos < length ? this._caretPos + 1 : this._caretPos; this.setInput(this._input, caretPos); ev.preventDefault(); break; case 8: // Delete this.deleteAtCurrent(); ev.preventDefault(); break; default: break; } if (ev.ctrlKey || ev.metaKey) { if (ev.keyCode == 86) { // Set focus to our clipboard when they hit the "v" key this.clipboard.focus(); // Pasting to clipboard doesn't happen immediately, // so we have to wait for a while to get the pasted text. var _this = this; setTimeout(function() { _this.addToInput(_this.clipboard.value); _this.clipboard.value = ""; _this.clipboard.blur(); }, 10); } } ev.stopPropagation(); }; /** * Handle input key press. */ REPLConsole.prototype.onKeyPress = function(ev) { // Only write to the console if it's a single key press. if (ev.ctrlKey && !ev.altKey || ev.metaKey) { return; } var keyCode = ev.keyCode || ev.which; this.insertAtCurrent(String.fromCharCode(keyCode)); ev.stopPropagation(); ev.preventDefault(); }; /** * Delete a character at the current position. */ REPLConsole.prototype.deleteAtCurrent = function() { if (this._caretPos > 0) { var caretPos = this._caretPos - 1; var before = this._input.substring(0, caretPos); var after = this._input.substring(this._caretPos, this._input.length); this.setInput(before + after, caretPos); if (!this._input) { this.autocomplete && this.autocomplete.cancel(); this.autocomplete = false; } } }; /** * Insert a character at the current position. */ REPLConsole.prototype.insertAtCurrent = function(char) { var before = this._input.substring(0, this._caretPos); var after = this._input.substring(this._caretPos, this._input.length); this.setInput(before + char + after, this._caretPos + 1); }; REPLConsole.prototype.swapCurrentWord = function(next) { function right(s, pos) { var x = s.indexOf(' ', pos); return x === -1 ? s.length : x; } function swap(s, pos) { return s.substr(0, s.lastIndexOf(' ', pos) + 1) + next + s.substr(right(s, pos)) } if (!next) return; var swapped = swap(this._input, this._caretPos); this.setInput(swapped, this._caretPos + swapped.length - this._input.length); }; REPLConsole.prototype.getCurrentWord = function() { return (function(s, pos) { var left = s.lastIndexOf(' ', pos); if (left === -1) left = 0; var right = s.indexOf(' ', pos) if (right === -1) right = s.length - 1; return s.substr(left, right - left + 1).replace(/^\s+|\s+$/g,''); })(this._input, this._caretPos); }; REPLConsole.prototype.scrollToBottom = function() { this.outer.scrollTop = this.outer.scrollHeight; }; // Change the binding of the console REPLConsole.prototype.switchBindingTo = function(frameId, callback) { var url = this.getSessionUrl('trace'); var params = "frame_id=" + encodeURIComponent(frameId); postRequest(url, params, callback); }; /** * Install the console into the element with a specific ID. * Example: REPLConsole.installInto("target-id") */ REPLConsole.installInto = function(id, options) { var consoleElement = document.getElementById(id); options = options || {}; for (var prop in consoleElement.dataset) { options[prop] = options[prop] || consoleElement.dataset[prop]; } var replConsole = new REPLConsole(options); replConsole.install(consoleElement); return replConsole; }; // This is to store the latest single session, and the stored session // is updated by the REPLConsole#install() method. // It allows to operate the current session from the other scripts. REPLConsole.currentSession = null; // This line is for the Firefox Add-on, because it doesn't have XMLHttpRequest as default. // And so we need to require a module compatible with XMLHttpRequest from SDK. REPLConsole.XMLHttpRequest = typeof XMLHttpRequest === 'undefined' ? null : XMLHttpRequest; REPLConsole.request = function request(method, url, params, callback) { var xhr = new REPLConsole.XMLHttpRequest(); xhr.open(method, url, true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.setRequestHeader("Accept", "<%= Mime[:web_console_v2] %>"); xhr.send(params); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { callback(xhr); } }; }; // DOM helpers function hasClass(el, className) { var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g'); return el.className && el.className.match(regex); } function isNodeList(el) { return typeof el.length === 'number' && typeof el.item === 'function'; } function addClass(el, className) { if (isNodeList(el)) { for (var i = 0; i < el.length; ++ i) { addClass(el[i], className); } } else if (!hasClass(el, className)) { el.className += " " + className; } } function removeClass(el, className) { var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g'); el.className = el.className.replace(regex, ''); } function removeAllChildren(el) { while (el.firstChild) { el.removeChild(el.firstChild); } } function findChild(el, className) { for (var i = 0; i < el.childNodes.length; ++ i) { if (hasClass(el.childNodes[i], className)) { return el.childNodes[i]; } } } function escapeHTML(html) { return html .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/`/g, '`'); } // XHR helpers function postRequest() { REPLConsole.request.apply(this, ["POST"].concat([].slice.call(arguments))); } function putRequest() { REPLConsole.request.apply(this, ["PUT"].concat([].slice.call(arguments))); } if (typeof exports === 'object') { exports.REPLConsole = REPLConsole; } else { window.REPLConsole = REPLConsole; } // Split string by module operators of ruby function getContext(s) { var methodOp = s.lastIndexOf('.'); var moduleOp = s.lastIndexOf('::'); var x = methodOp > moduleOp ? methodOp : moduleOp; return x !== -1 ? s.substr(0, x) : ''; } function flatten(arrays) { return Array.prototype.concat.apply([], arrays); } web-console-3.6.2/lib/web_console/templates/style.css.erb0000644000004100000410000000460013273331137023456 0ustar www-datawww-data.console .pos-absolute { position: absolute; } .console .pos-fixed { position: fixed; } .console .pos-right { right: 0; } .console .border-box { box-sizing: border-box; } .console .layer { width: 100%; height: 100%; } .console .layer.console-outer { z-index: 1; } .console .layer.resizer { z-index: 2; } .console { position: fixed; left: 0; bottom: 0; width: 100%; height: 148px; padding: 0; margin: 0; background: none repeat scroll 0% 0% #333; z-index: 9999; } .console .console-outer { overflow: auto; padding-top: 4px; } .console .console-inner { font-family: monospace; font-size: 11px; width: 100%; height: 100%; overflow: none; background: #333; } .console .console-prompt-box { color: #FFF; } .console .console-message { color: #1AD027; margin: 0; border: 0; white-space: pre-wrap; background-color: #333; padding: 0; } .console .console-message.error-message { color: #fc9; } .console .console-message.auto-complete { word-break: break-all; } .console .console-message.auto-complete .keyword { margin-right: 11px; } .console .console-message.auto-complete .keyword.selected { background: #FFF; color: #000; } .console .console-message.auto-complete .hidden { display: none; } .console .console-message.auto-complete .trimmed { display: none; } .console .console-hint { color: #096; } .console .console-focus .console-cursor { background: #FEFEFE; color: #333; font-weight: bold; } .console .resizer { background: #333; width: 100%; height: 4px; cursor: ns-resize; } .console .console-actions { padding-right: 3px; } .console .console-actions .button { float: left; } .console .button { cursor: pointer; border-radius: 1px; font-family: monospace; font-size: 13px; width: 14px; height: 14px; line-height: 14px; text-align: center; color: #ccc; } .console .button:hover { background: #666; color: #fff; } .console .button.close-button:hover { background: #966; } .console .clipboard { height: 0px; padding: 0px; margin: 0px; width: 0px; margin-left: -1000px; } .console .console-prompt-label { display: inline; color: #FFF; background: none repeat scroll 0% 0% #333; border: 0; padding: 0; } .console .console-prompt-display { display: inline; color: #FFF; background: none repeat scroll 0% 0% #333; border: 0; padding: 0; } .console.full-screen { height: 100%; } .console.full-screen .console-outer { padding-top: 3px; } .console.full-screen .resizer { display: none; } .console.full-screen .close-button { display: none; } web-console-3.6.2/lib/web_console/templates/_prompt_box_markup.html.erb0000644000004100000410000000012613273331137026400 0ustar www-datawww-data

web-console-3.6.2/lib/web_console/templates/_markup.html.erb0000644000004100000410000000020413273331137024124 0ustar  www-datawww-data
web-console-3.6.2/lib/web_console/templates/index.html.erb0000644000004100000410000000025213273331137023600 0ustar www-datawww-data<%= render 'markup' %> <%= render_javascript 'console' %> <%= render_javascript 'main' %> <% only_on_error_page do %> <%= render_javascript 'error_page' %> <% end %> web-console-3.6.2/lib/web_console/templates/layouts/0000755000004100000410000000000013273331137022535 5ustar www-datawww-dataweb-console-3.6.2/lib/web_console/templates/layouts/inlined_string.erb0000644000004100000410000000002113273331137026230 0ustar www-datawww-data"<%= j yield %>" web-console-3.6.2/lib/web_console/templates/layouts/javascript.erb0000644000004100000410000000016713273331137025401 0ustar www-datawww-data web-console-3.6.2/lib/web_console/templates/error_page.js.erb0000644000004100000410000000465013273331137024274 0ustar www-datawww-data// Try intercept traces links in Rails 4.2. var traceFrames = document.getElementsByClassName('trace-frames'); var selectedFrame, currentSource = document.getElementById('frame-source-0'); // Add click listeners for all stack frames for (var i = 0; i < traceFrames.length; i++) { traceFrames[i].addEventListener('click', function(e) { e.preventDefault(); var target = e.target; var frameId = target.dataset.frameId; // Change the binding of the console. changeBinding(frameId, function() { if (selectedFrame) { selectedFrame.className = selectedFrame.className.replace("selected", ""); } target.className += " selected"; selectedFrame = target; }); // Change the extracted source code changeSourceExtract(frameId); }); } function changeBinding(frameId, callback) { REPLConsole.currentSession.switchBindingTo(frameId, callback); } function changeSourceExtract(frameId) { var el = document.getElementById('frame-source-' + frameId); if (currentSource && el) { currentSource.className += " hidden"; el.className = el.className.replace(" hidden", ""); currentSource = el; } } // Push the error page body upwards the size of the console. // // While, I wouldn't like to do that on every custom page (so I don't screw // user's layouts), I think a lot of developers want to see all of the content // on the default Rails error page. // // Since it's quite special as is now, being a bit more special in the name of // better user experience, won't hurt. document.addEventListener('DOMContentLoaded', function() { var consoleElement = document.getElementById('console'); var resizerElement = consoleElement.getElementsByClassName('resizer')[0]; var containerElement = document.getElementById('container'); function setContainerElementBottomMargin(pixels) { containerElement.style.marginBottom = pixels + 'px'; } var currentConsoleElementHeight = consoleElement.offsetHeight; setContainerElementBottomMargin(currentConsoleElementHeight); resizerElement.addEventListener('mousedown', function(event) { function recordConsoleElementHeight(event) { resizerElement.removeEventListener('mouseup', recordConsoleElementHeight); var currentConsoleElementHeight = consoleElement.offsetHeight; setContainerElementBottomMargin(currentConsoleElementHeight); } resizerElement.addEventListener('mouseup', recordConsoleElementHeight); }); }); web-console-3.6.2/lib/web_console/templates/_inner_console_markup.html.erb0000644000004100000410000000037113273331137027046 0ustar www-datawww-data
x
web-console-3.6.2/lib/web_console/templates/main.js.erb0000644000004100000410000000004413273331137023064 0ustar www-datawww-dataREPLConsole.installInto('console'); web-console-3.6.2/lib/web_console/errors.rb0000644000004100000410000000040013273331137020672 0ustar www-datawww-data# frozen_string_literal: true module WebConsole # The base class for every Web Console related error. Error = Class.new(StandardError) # Raised when there is an attempt to render a console more than once. DoubleRenderError = Class.new(Error) end web-console-3.6.2/lib/web_console/request.rb0000644000004100000410000000320213273331137021051 0ustar www-datawww-data# frozen_string_literal: true module WebConsole # Web Console tailored request object. class Request < ActionDispatch::Request # Configurable set of whitelisted networks. cattr_accessor :whitelisted_ips @@whitelisted_ips = Whitelist.new # Define a vendor MIME type. We can call it using Mime[:web_console_v2]. Mime::Type.register "application/vnd.web-console.v2", :web_console_v2 # Returns whether a request came from a whitelisted IP. # # For a request to hit Web Console features, it needs to come from a white # listed IP. def from_whitelisted_ip? whitelisted_ips.include?(strict_remote_ip) end # Determines the remote IP using our much stricter whitelist. def strict_remote_ip GetSecureIp.new(self, whitelisted_ips).to_s rescue ActionDispatch::RemoteIp::IpSpoofAttackError "[Spoofed]" end # Returns whether the request is acceptable. def acceptable? xhr? && accepts.any? { |mime| Mime[:web_console_v2] == mime } end private class GetSecureIp < ActionDispatch::RemoteIp::GetIp def initialize(req, proxies) # After rails/rails@07b2ff0 ActionDispatch::RemoteIp::GetIp initializes # with a ActionDispatch::Request object instead of plain Rack # environment hash. Keep both @req and @env here, so we don't if/else # on Rails versions. @req = req @env = req.env @check_ip = true @proxies = proxies end def filter_proxies(ips) ips.reject do |ip| @proxies.include?(ip) end end end end end web-console-3.6.2/lib/web_console/context.rb0000644000004100000410000000202213273331137021044 0ustar www-datawww-data# frozen_string_literal: true module WebConsole # A context lets you get object names related to the current session binding. class Context def initialize(binding) @binding = binding end # Extracts entire objects which can be called by the current session unless # the inputs is present. # # Otherwise, it extracts methods and constants of the object specified by # the input. def extract(input = nil) input.present? ? local(input) : global end private GLOBAL_OBJECTS = [ "instance_variables", "local_variables", "methods", "class_variables", "Object.constants", "global_variables" ] def global GLOBAL_OBJECTS.map { |cmd| eval(cmd) } end def local(input) [ eval("#{input}.methods").map { |m| "#{input}.#{m}" }, eval("#{input}.constants").map { |c| "#{input}::#{c}" }, ] end def eval(cmd) @binding.eval(cmd) rescue [] end end end web-console-3.6.2/lib/web_console/testing/0000755000004100000410000000000013273331137020514 5ustar www-datawww-dataweb-console-3.6.2/lib/web_console/testing/fake_middleware.rb0000644000004100000410000000172413273331137024150 0ustar www-datawww-data# frozen_string_literal: true require "action_view" require "web_console" require "web_console/testing/helper" Mime = { web_console_v2: "fake" } module WebConsole module Testing class FakeMiddleware I18n.load_path.concat(Dir[Helper.gem_root.join("lib/web_console/locales/*.yml")]) DEFAULT_HEADERS = { "Content-Type" => "application/javascript" } def initialize(opts) @headers = opts.fetch(:headers, DEFAULT_HEADERS) @req_path_regex = opts[:req_path_regex] @view_path = opts[:view_path] end def call(env) [ 200, @headers, [ render(req_path(env)) ] ] end def view @view = View.new(@view_path) end private # extract target path from REQUEST_PATH def req_path(env) env["REQUEST_PATH"].match(@req_path_regex)[1] end def render(template) view.render(template: template, layout: nil) end end end end web-console-3.6.2/lib/web_console/testing/helper.rb0000644000004100000410000000030513273331137022316 0ustar www-datawww-data# frozen_string_literal: true module WebConsole module Testing module Helper def self.gem_root Pathname(File.expand_path("../../../../", __FILE__)) end end end end web-console-3.6.2/lib/web_console/testing/erb_precompiler.rb0000644000004100000410000000121513273331137024211 0ustar www-datawww-data# frozen_string_literal: true require "web_console/testing/helper" require "web_console/testing/fake_middleware" module WebConsole module Testing # This class is to pre-compile 'templates/*.erb'. class ERBPrecompiler def initialize(path) @erb = ERB.new(File.read(path)) @view = FakeMiddleware.new( view_path: Helper.gem_root.join("lib/web_console/templates"), ).view end def build @erb.result(binding) end def method_missing(name, *args, &block) return super unless @view.respond_to?(name) @view.send(name, *args, &block) end end end end web-console-3.6.2/lib/web_console/exception_mapper.rb0000644000004100000410000000145413273331137022732 0ustar www-datawww-data# frozen_string_literal: true module WebConsole class ExceptionMapper def initialize(exception) @backtrace = exception.backtrace @bindings = exception.bindings end def first guess_the_first_application_binding || @bindings.first end def [](index) guess_binding_for_index(index) || @bindings[index] end private def guess_binding_for_index(index) file, line = @backtrace[index].to_s.split(":") line = line.to_i @bindings.find do |binding| binding.eval("__FILE__") == file && binding.eval("__LINE__") == line end end def guess_the_first_application_binding @bindings.find do |binding| binding.eval("__FILE__").to_s.start_with?(Rails.root.to_s) end end end end web-console-3.6.2/lib/web_console/extensions.rb0000644000004100000410000000327013273331137021565 0ustar www-datawww-data# frozen_string_literal: true module Kernel module_function # Instructs Web Console to render a console in the specified binding. # # If +binding+ isn't explicitly given it will default to the binding of the # previous frame. E.g. the one that invoked +console+. # # Raises DoubleRenderError if a double +console+ invocation per request is # detected. def console(binding = Bindex.current_bindings.second) raise WebConsole::DoubleRenderError if Thread.current[:__web_console_binding] Thread.current[:__web_console_binding] = binding # Make sure nothing is rendered from the view helper. Otherwise # you're gonna see unexpected # in the # templates. nil end end module ActionDispatch class DebugExceptions def render_exception_with_web_console(request, exception) render_exception_without_web_console(request, exception).tap do backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner") error = ExceptionWrapper.new(backtrace_cleaner, exception).exception # Get the original exception if ExceptionWrapper decides to follow it. Thread.current[:__web_console_exception] = error # ActionView::Template::Error bypass ExceptionWrapper original # exception following. The backtrace in the view is generated from # reaching out to original_exception in the view. if error.is_a?(ActionView::Template::Error) Thread.current[:__web_console_exception] = error.cause end end end alias_method :render_exception_without_web_console, :render_exception alias_method :render_exception, :render_exception_with_web_console end end web-console-3.6.2/lib/web_console/locales/0000755000004100000410000000000013273331137020461 5ustar www-datawww-dataweb-console-3.6.2/lib/web_console/locales/en.yml0000644000004100000410000000111313273331137021602 0ustar www-datawww-dataen: errors: unavailable_session: | Session %{id} is no longer available in memory. If you happen to run on a multi-process server (like Unicorn or Puma) the process this request hit doesn't store %{id} in memory. Consider turning the number of processes/workers to one (1) or using a different server in development. unacceptable_request: | A supported version is expected in the Accept header. connection_refused: | Oops! Failed to connect to the Web Console middleware. Please make sure a rails development server is running. web-console-3.6.2/lib/web_console/view.rb0000644000004100000410000000341613273331137020342 0ustar www-datawww-data# frozen_string_literal: true module WebConsole class View < ActionView::Base # Execute a block only on error pages. # # The error pages are special, because they are the only pages that # currently require multiple bindings. We get those from exceptions. def only_on_error_page(*args) yield if Thread.current[:__web_console_exception].present? end # Render JavaScript inside a script tag and a closure. # # This one lets write JavaScript that will automatically get wrapped in a # script tag and enclosed in a closure, so you don't have to worry for # leaking globals, unless you explicitly want to. def render_javascript(template) assign(template: template) render(template: template, layout: "layouts/javascript") end # Render inlined string to be used inside of JavaScript code. # # The inlined string is returned as an actual JavaScript string. You # don't need to wrap the result yourself. def render_inlined_string(template) render(template: template, layout: "layouts/inlined_string") end # Custom ActionView::Base#render wrapper which silences all the log # printings. # # Helps to keep the Rails logs clean during errors. def render(*) if (logger = WebConsole.logger) && logger.respond_to?(:silence) WebConsole.logger.silence { super } else super end end # Override method for ActionView::Helpers::TranslationHelper#t. # # This method escapes the original return value for JavaScript, since the # method returns a HTML tag with some attributes when the key is not found, # so it could cause a syntax error if we use the value in the string literals. def t(key, options = {}) j super end end end web-console-3.6.2/lib/web_console/evaluator.rb0000644000004100000410000000175113273331137021372 0ustar www-datawww-data# frozen_string_literal: true module WebConsole # Simple Ruby code evaluator. # # This class wraps a +Binding+ object and evaluates code inside of it. The # difference of a regular +Binding+ eval is that +Evaluator+ will always # return a string and will format exception output. class Evaluator # Cleanses exceptions raised inside #eval. cattr_reader :cleaner @@cleaner = ActiveSupport::BacktraceCleaner.new @@cleaner.add_silencer { |line| line.start_with?(File.expand_path("..", __FILE__)) } def initialize(binding = TOPLEVEL_BINDING) @binding = binding end def eval(input) "=> #{@binding.eval(input).inspect}\n" rescue Exception => exc format_exception(exc) end private def format_exception(exc) backtrace = cleaner.clean(Array(exc.backtrace) - caller) format = "#{exc.class.name}: #{exc}\n".dup format << backtrace.map { |trace| "\tfrom #{trace}\n" }.join format end end end web-console-3.6.2/lib/web_console/whitelist.rb0000644000004100000410000000216413273331137021403 0ustar www-datawww-data# frozen_string_literal: true require "ipaddr" module WebConsole # Whitelist of allowed networks that can access Web Console. # # Networks are represented by standard IPAddr and can be either IPv4 or IPv6 # networks. class Whitelist # IPv4 and IPv6 localhost should be always whitelisted. ALWAYS_WHITELISTED_NETWORKS = %w( 127.0.0.0/8 ::1 ) def initialize(networks = nil) @networks = normalize_networks(networks).map(&method(:coerce_network_to_ipaddr)).uniq end def include?(network) @networks.any? { |whitelist| whitelist.include?(network.to_s) } rescue IPAddr::InvalidAddressError false end def to_s @networks.map(&method(:human_readable_ipaddr)).join(", ") end private def normalize_networks(networks) Array(networks).concat(ALWAYS_WHITELISTED_NETWORKS) end def coerce_network_to_ipaddr(network) if network.is_a?(IPAddr) network else IPAddr.new(network) end end def human_readable_ipaddr(ipaddr) ipaddr.to_range.to_s.split("..").uniq.join("/") end end end web-console-3.6.2/lib/web_console/middleware.rb0000644000004100000410000001010513273331137021476 0ustar www-datawww-data# frozen_string_literal: true require "active_support/core_ext/string/strip" module WebConsole class Middleware TEMPLATES_PATH = File.expand_path("../templates", __FILE__) cattr_accessor :mount_point @@mount_point = "/__web_console" cattr_accessor :whiny_requests @@whiny_requests = true def initialize(app) @app = app end def call(env) app_exception = catch :app_exception do request = create_regular_or_whiny_request(env) return call_app(env) unless request.from_whitelisted_ip? if id = id_for_repl_session_update(request) return update_repl_session(id, request) elsif id = id_for_repl_session_stack_frame_change(request) return change_stack_trace(id, request) end status, headers, body = call_app(env) if (session = Session.from(Thread.current)) && acceptable_content_type?(headers) headers["X-Web-Console-Session-Id"] = session.id headers["X-Web-Console-Mount-Point"] = mount_point template = Template.new(env, session) body, headers = Injector.new(body, headers).inject(template.render("index")) end [ status, headers, body ] end rescue => e WebConsole.logger.error("\n#{e.class}: #{e}\n\tfrom #{e.backtrace.join("\n\tfrom ")}") raise e ensure # Clean up the fiber locals after the session creation. Object#console # uses those to communicate the current binding or exception to the middleware. Thread.current[:__web_console_exception] = nil Thread.current[:__web_console_binding] = nil raise app_exception if Exception === app_exception end private def acceptable_content_type?(headers) Mime::Type.parse(headers["Content-Type"].to_s).first == Mime[:html] end def json_response(opts = {}) status = opts.fetch(:status, 200) headers = { "Content-Type" => "application/json; charset = utf-8" } body = yield.to_json [ status, headers, [ body ] ] end def json_response_with_session(id, request, opts = {}) return respond_with_unacceptable_request unless request.acceptable? return respond_with_unavailable_session(id) unless session = Session.find(id) json_response(opts) { yield session } end def create_regular_or_whiny_request(env) request = Request.new(env) whiny_requests ? WhinyRequest.new(request) : request end def repl_sessions_re @_repl_sessions_re ||= %r{#{mount_point}/repl_sessions/(?[^/]+)} end def update_re @_update_re ||= %r{#{repl_sessions_re}\z} end def binding_change_re @_binding_change_re ||= %r{#{repl_sessions_re}/trace\z} end def id_for_repl_session_update(request) if request.xhr? && request.put? update_re.match(request.path) { |m| m[:id] } end end def id_for_repl_session_stack_frame_change(request) if request.xhr? && request.post? binding_change_re.match(request.path) { |m| m[:id] } end end def update_repl_session(id, request) json_response_with_session(id, request) do |session| if input = request.params[:input] { output: session.eval(input) } elsif input = request.params[:context] { context: session.context(input) } end end end def change_stack_trace(id, request) json_response_with_session(id, request) do |session| session.switch_binding_to(request.params[:frame_id]) { ok: true } end end def respond_with_unavailable_session(id) json_response(status: 404) do { output: format(I18n.t("errors.unavailable_session"), id: id) } end end def respond_with_unacceptable_request json_response(status: 406) do { output: I18n.t("errors.unacceptable_request") } end end def call_app(env) @app.call(env) rescue => e throw :app_exception, e end end end web-console-3.6.2/lib/web_console.rb0000644000004100000410000000116413273331137017366 0ustar www-datawww-data# frozen_string_literal: true require "active_support/dependencies/autoload" require "active_support/logger" module WebConsole extend ActiveSupport::Autoload autoload :View autoload :Evaluator autoload :ExceptionMapper autoload :Session autoload :Injector autoload :Request autoload :WhinyRequest autoload :Whitelist autoload :Template autoload :Middleware autoload :Context autoload_at "web_console/errors" do autoload :Error autoload :DoubleRenderError end def self.logger Rails.logger || (@logger ||= ActiveSupport::Logger.new($stderr)) end end require "web_console/railtie" web-console-3.6.2/README.markdown0000644000004100000410000001262113273331137016475 0ustar www-datawww-data

Documentation for: v1.0.4 v2.2.1

# Web Console [![Build Status](https://travis-ci.org/rails/web-console.svg?branch=master)](https://travis-ci.org/rails/web-console) _Web Console_ is a debugging tool for your Ruby on Rails applications. - [Installation](#installation) - [Configuration](#configuration) - [Usage](#usage) - [FAQ](#faq) - [Credits](#credits) ## Installation Add the following to your `Gemfile`. ```ruby group :development do gem 'web-console' end ``` ## Usage The web console allows you to create an interactive Ruby session in your browser. Those sessions are launched automatically in case of an error, but they can also be launched manually in any page. For example, calling `console` in a view will display a console in the current page in the context of the view binding. ```html <% console %> ``` Calling `console` in a controller will result in a console in the context of the controller action: ```ruby class PostsController < ApplicationController def new console @post = Post.new end end ``` The method is defined in `Kernel` and you can invoke it any application code. Only one `console` invocation per request is allowed. If you happen to have multiple ones, `WebConsole::DoubleRenderError` will be raised. ## Configuration _Web Console_ allows you to execute arbitrary code on the server, so you should be very careful, who you give access to. ### config.web_console.whitelisted_ips By default, only requests coming from IPv4 and IPv6 localhosts are allowed. `config.web_console.whitelisted_ips` lets you control which IP's have access to the console. You can whitelist single IP's or whole networks. Say you want to share your console with `192.168.0.100`. You can do this: ```ruby class Application < Rails::Application config.web_console.whitelisted_ips = '192.168.0.100' end ``` If you want to whitelist the whole private network, you can do: ```ruby Rails.application.configure do config.web_console.whitelisted_ips = '192.168.0.0/16' end ``` Take a note that IPv4 and IPv6 localhosts are always allowed. This wasn't the case in 2.0. ### config.web_console.whiny_requests When a console cannot be shown for a given IP address or content type, a messages like the following is printed in the server logs: > Cannot render console from 192.168.1.133! Allowed networks: > 127.0.0.0/127.255.255.255, ::1 If you don't wanna see this message anymore, set this option to `false`: ```ruby Rails.application.configure do config.web_console.whiny_requests = false end ``` ### config.web_console.template_paths If you wanna style the console yourself, you can place `style.css` at a directory pointed by `config.web_console.template_paths`: ```ruby Rails.application.configure do config.web_console.template_paths = 'app/views/web_console' end ``` You may wanna check the [templates] folder at the source tree for the files you may override. ### config.web_console.mount_point Usually the middleware of _Web Console_ is mounted at `/__web_console`. If you wanna change the path for some reasons, you can specify it by `config.web_console.mount_point`: ```ruby Rails.application.configure do config.web_console.mount_point = '/path/to/web_console' end ``` ## FAQ ### Where did /console go? The remote terminal emulator was extracted in its own gem that is no longer bundled with _Web Console_. If you miss this feature, check out [rvt]. ### Why I constantly get unavailable session errors? All of _Web Console_ sessions are stored in memory. If you happen to run on a multi-process server (like Unicorn) you may get unavailable session errors while the server is still running. This is because a request may hit a different worker (process) that doesn't have the desired session in memory. To avoid that, if you use such servers in development, configure them so they server requests only out of one process. ### How to inspect local and instance variables? The interactive console executes Ruby code. Invoking `instance_variables` and `local_variables` will give you what you want. ### Why does console only appear on error pages but not when I call it? This can be happening if you are using `Rack::Deflater`. Be sure that `WebConsole::Middleware` is used after `Rack::Deflater`. The easiest way to do this is to insert `Rack::Deflater` as early as possible ```ruby Rails.application.configure do config.middleware.insert(0, Rack::Deflater) end ``` ### Why I'm getting an undefined method `web_console`? Make sure your configuration lives in `config/environments/development.rb`. ## Credits * Shoutout to [Charlie Somerville] for [better_errors]. * Kudos to [John Mair] for [binding_of_caller] and [debug_inspector]. * Thanks to [Charles Oliver Nutter] for all the _JRuby_ feedback. * Hugs and kisses to all of our [contributors]! [better_errors]: https://github.com/charliesome/better_errors [debug_inspector]: https://github.com/banister/debug_inspector [binding_of_caller]: https://github.com/banister/binding_of_caller [Charlie Somerville]: https://github.com/charliesome [John Mair]: https://github.com/banister [Charles Oliver Nutter]: https://github.com/headius [templates]: https://github.com/rails/web-console/tree/master/lib/web_console/templates [rvt]: https://github.com/gsamokovarov/rvt [contributors]: https://github.com/rails/web-console/graphs/contributors web-console-3.6.2/web-console.gemspec0000644000004100000410000000671513273331137017565 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: web-console 3.6.2 ruby lib Gem::Specification.new do |s| s.name = "web-console".freeze s.version = "3.6.2" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Charlie Somerville".freeze, "Genadi Samokovarov".freeze, "Guillermo Iguaran".freeze, "Ryan Dao".freeze] s.date = "2018-04-29" s.email = ["charlie@charliesomerville.com".freeze, "gsamokovarov@gmail.com".freeze, "guilleiguaran@gmail.com".freeze, "daoduyducduong@gmail.com".freeze] s.files = ["CHANGELOG.markdown".freeze, "MIT-LICENSE".freeze, "README.markdown".freeze, "Rakefile".freeze, "lib/web-console.rb".freeze, "lib/web_console.rb".freeze, "lib/web_console/context.rb".freeze, "lib/web_console/errors.rb".freeze, "lib/web_console/evaluator.rb".freeze, "lib/web_console/exception_mapper.rb".freeze, "lib/web_console/extensions.rb".freeze, "lib/web_console/injector.rb".freeze, "lib/web_console/locales/en.yml".freeze, "lib/web_console/middleware.rb".freeze, "lib/web_console/railtie.rb".freeze, "lib/web_console/request.rb".freeze, "lib/web_console/session.rb".freeze, "lib/web_console/tasks/extensions.rake".freeze, "lib/web_console/tasks/templates.rake".freeze, "lib/web_console/template.rb".freeze, "lib/web_console/templates/_inner_console_markup.html.erb".freeze, "lib/web_console/templates/_markup.html.erb".freeze, "lib/web_console/templates/_prompt_box_markup.html.erb".freeze, "lib/web_console/templates/console.js.erb".freeze, "lib/web_console/templates/error_page.js.erb".freeze, "lib/web_console/templates/index.html.erb".freeze, "lib/web_console/templates/layouts/inlined_string.erb".freeze, "lib/web_console/templates/layouts/javascript.erb".freeze, "lib/web_console/templates/main.js.erb".freeze, "lib/web_console/templates/style.css.erb".freeze, "lib/web_console/testing/erb_precompiler.rb".freeze, "lib/web_console/testing/fake_middleware.rb".freeze, "lib/web_console/testing/helper.rb".freeze, "lib/web_console/version.rb".freeze, "lib/web_console/view.rb".freeze, "lib/web_console/whiny_request.rb".freeze, "lib/web_console/whitelist.rb".freeze] s.homepage = "https://github.com/rails/web-console".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.2.2".freeze) s.rubygems_version = "2.5.2.1".freeze s.summary = "A debugging tool for your Ruby on Rails applications.".freeze if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_runtime_dependency(%q.freeze, [">= 5.0"]) s.add_runtime_dependency(%q.freeze, [">= 5.0"]) s.add_runtime_dependency(%q.freeze, [">= 0.4.0"]) s.add_runtime_dependency(%q.freeze, [">= 5.0"]) else s.add_dependency(%q.freeze, [">= 5.0"]) s.add_dependency(%q.freeze, [">= 5.0"]) s.add_dependency(%q.freeze, [">= 0.4.0"]) s.add_dependency(%q.freeze, [">= 5.0"]) end else s.add_dependency(%q.freeze, [">= 5.0"]) s.add_dependency(%q.freeze, [">= 5.0"]) s.add_dependency(%q.freeze, [">= 0.4.0"]) s.add_dependency(%q.freeze, [">= 5.0"]) end end web-console-3.6.2/MIT-LICENSE0000644000004100000410000000213313273331137015425 0ustar www-datawww-dataCopyright 2014-2016 Charlie Somerville, Genadi Samokovarov, Guillermo Iguaran and Ryan Dao Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.