require "cgi" require "redcloth_for_tex" RenderedTodo = Struct.new( :text, :context, :due_date ) class WikiController < ActionControllerServlet EXPORT_DIRECTORY = WikiService.storage_path def index if web_address check_external_req_and_redirect web elsif !wiki.setup? redirect_path "/new_system/" elsif wiki.webs.length == 1 check_external_req_and_redirect wiki.webs.values.first else wiki.default_web ? check_external_req_and_redirect(wiki.webs[wiki.default_web]) : redirect_path("/web_list/") end end def check_external_req_and_redirect web if web.default_to_published and @req.addr[2] != @req.peeraddr[2] redirect_action("published/", web.address) else redirect_show("HomePage", web.address) end end # Administrating the Instiki setup -------------------------------------------- def new_system wiki.setup? ? redirect_path("/") : render end def new_web redirect_path("/") if wiki.system["password"].nil? end def create_system wiki.setup(@params["password"], @params["web_name"], @params["web_address"]) unless wiki.setup? redirect_path "/" end def create_web redirect_path("/") unless wiki.authenticate(@params["system_password"]) wiki.create_web(@params["name"], @params["address"]) redirect_show("HomePage", @params["address"]) end # Outside a single web -------------------------------------------------------- def web_list @system, @webs = wiki.system, wiki.webs.values render "wiki/web_list" end def login render "wiki/login" end def authenticate password_check(@params["password"]) ? redirect_show("HomePage") : redirect_action("login") end def static_style_sheet() render "static_style_sheet" end def stop redirect_show("HomePage") unless wiki.authenticate(@params["system_password"]) begin secs = @params['seconds'].to_i raise if secs.zero? @logger.warn "Pimki server will stop in #{secs} seconds!" WikiService.request_stop render_text "Pimki server will stop in #{secs} seconds!" Thread.new { sleep secs; Kernel.exit! } rescue redirect_show("HomePage") end end # Within a single web --------------------------------------------------------- def parse_category @categories = web.categories @category = @params["category"] @pages_in_category = web.select { |page| page.in_category?(@category) } @pages_without_category = web.select { |page| page.categories.length == 0 } if @category == 'none' @pages_in_category = @pages_without_category end @set_name = ( @categories.include?(@category) ? "category '#{@category}'" : "the web" ) @category_links = @categories.map do |c| (@category == c ? "[#{c}]" : "#{c}") end end def search set_menu_pages @query = @params["query"] search_content = [nil, 'both', 'contents'].include? @params['fields'] search_names = [nil, 'both', 'names'].include? @params['fields'] case @params['expression'] when 'regex', nil rex = Regexp.new @query, (Regexp::IGNORECASE unless @params['case']) @results = if [nil, 'all', 'pages', nil].include? @params['where'] web.select do |page| (search_names and rex.match(page.name)) or (search_content and rex.match(page.content)) end else [] end @bliki_results = if [nil, 'all', 'bliki'].include? @params['where'] web.bliki.values.select do |entry| (search_names and rex.match(entry.name)) or (search_content and rex.match(entry.content)) end else [] end when 'all' words = @query.split(/\s/).reject { |w| w.empty? } @results = if [nil, 'all', 'pages'].include? @params['where'] web.select do |page| words.all? do |word| (search_names and page.name.index(word)) or (search_content and page.content.index(word)) end end else [] end @bliki_results = if [nil, 'all', 'bliki'].include? @params['where'] web.bliki.values.select do |entry| words.all? do |word| (search_names and entry.name.index(word)) or (search_content and entry.content.index(word)) end end else [] end when 'exact' @results = if [nil, 'all', 'pages'].include? @params['where'] web.select do |page| (search_names and page.name.index(@query)) or (search_content and page.content.index(@query)) end else [] end @bliki_results = if [nil, 'all', 'bliki'].include? @params['where'] web.bliki.values.select do |entry| (search_names and entry.name.index(@query)) or (search_content and entry.content.index(@query)) end else [] end end if !@params['category'].nil? and @params['category'] != 'noselect' @selected_categories = parse_multi_select 'category' @results.reject! { |page| (page.categories & @selected_categories).empty? } @bliki_results.reject! { |page| (page.categories & @selected_categories).empty? } end if !@params['author'].nil? and @params['author'] != 'noselect' @selected_authors = parse_multi_select 'author' @results.reject! { |page| (page.authors & @selected_authors).empty? } @bliki_results.reject! { |page| (page.authors & @selected_authors).empty? } end redirect_show(@results.first.name) if @results.length == 1 && @bliki_results.length == 0 redirect_path("/#{web_address}/bliki_revision/#{@bliki_results.first.name}?rev=#{@bliki_results.first.revisions.size-1}") if @results.length == 0 && @bliki_results.length == 1 render_action "search" end def glossary set_menu_pages rex = %r{([A-Z\d]+)\(([\w\s]+)\)} scan_results = web.select.map { |page| [page.link, page.content.scan(rex)] } scan_results += web.bliki.values.map { |entry| [link_to_bliki(entry), entry.content.scan(rex)] } results = Hash.new { Array.new } scan_results.each { |page, acronyms| acronyms.each { |ac| results[ac] += [page] } } @results = results.map{ |(ac, df), pg|[[ac,df], pg.uniq] }.sort_by{ |(ac, df), pg| ac } acronyms = @results.map { |(ac, df), pg| ac } rex = %r{(#{acronyms.join('|')})[^\(]} results = web.select.map { |page| [page.link, page.content.scan(rex)] } results += web.bliki.values.map { |entry| [link_to_bliki(entry), entry.content.scan(rex)] } @undefined_on = Hash.new { Array.new } results.each { |page, acronyms| acronyms.each { |ac| @undefined_on[ac[0]] += [page] } } @undefined_on = @undefined_on.inject({}) { |hsh, (k, v)| hsh[k] = v.uniq; hsh } end def authors set_menu_pages @authors = web.select.authors end def recently_revised parse_category set_menu_pages @pages_by_revision = @pages_in_category.by_revision end def rss_with_content @pages_by_revision = @rss_bliki_only ? [] : web.select.by_revision.first(15) @bliki_entries = web.bliki_entries_by_date @uri = @req.request_uri host = @req.meta_vars["HTTP_X_FORWARDED_HOST"] || "#{@uri.host}:#{@uri.port.to_s}" @web_url = "#{@uri.scheme}://#{host}/#{@web.address}" @res["Content-Type"] = "text/xml" render "wiki/rss_feed" end def rss_with_headlines @hide_description = true rss_with_content end def rss_bliki_only @rss_bliki_only = true rss_with_content end def rss_todo_items todo @display_todo = true rss_with_content end def feeds set_menu_pages end def list parse_category set_menu_pages @pages_by_name = @pages_in_category.by_name @page_names_that_are_wanted = @pages_in_category.wanted_pages @pages_that_are_orphaned = @pages_in_category.orphaned_pages if @req.query['Action'] # redirect_action 'list/' if web.check_pass_on_edit and not password_check(@params['password']) case @req.query['Action'] when 'Delete' # Handle page deletion wiki.delete_page(web_address, @req.query['del_sel_page_name']) clear_render_cache true redirect_action "list/" when 'Create' # Handle page creation redirect_show @req.query['newpage'] when 'Rename' # Handle page rename wiki.rename_page(web_address, @req.query['ren_sel_page_name'], @req.query['ren_newpage']) clear_render_cache true redirect_action "list/" end end end def export set_menu_pages end def export_html file_name = "#{web.address}-html-#{web.revised_on.strftime("%Y-%m-%d-%H-%M")}.zip" file_path = File.join EXPORT_DIRECTORY, file_name export_pages_to_zip_file(file_path) unless FileTest.exists?(file_path) send_export(file_name, file_path) end def export_markup file_name = "#{web.address}-markup-#{web.revised_on.strftime("%Y-%m-%d-%H-%M")}.zip" file_path = File.join EXPORT_DIRECTORY, file_name export_markup_to_zip_file(file_path) unless FileTest.exists?(file_path) send_export(file_name, file_path) end def export_pdf file_name = "#{web.address}-tex-#{web.revised_on.strftime('%Y-%m-%d-%H-%M')}" file_path = File.join EXPORT_DIRECTORY, file_name export_web_to_tex(file_path + ".tex") unless FileTest.exists?(file_path + ".tex") convert_tex_to_pdf(file_path + ".tex") send_export(file_name + ".pdf", file_path + ".pdf") end def export_tex file_name = "#{web.address}-tex-#{web.revised_on.strftime('%Y-%m-%d-%H-%M')}.tex" file_path = File.join EXPORT_DIRECTORY, file_name export_web_to_tex(file_path) unless FileTest.exists?(file_path) send_export(file_name, file_path) end def edit_web #{{{ parse_category set_mm_options @snapshot_interval = WikiService.snapshot_interval_hours end #}}} def update_web redirect_show("HomePage") unless wiki.authenticate(@params["system_password"]) parse_category set_mm_options wiki.update_web( web.address, @params["address"], @params["name"], @params["markup"].intern, @params["color"], @params["additional_style"], @params["safe_mode"] ? true : false, @params["password"].empty? ? nil : @params["password"], @params["published"] ? true : false, @params['default_to_published'] ? true : false, @params["brackets_only"] ? true : false, @params["count_pages"] ? true : false, @params['mind_map_size'], @params['symbols_map'], @params['links_map'], @params['snapshots_interval'], @params['enable_dclick_edit'] ? true : false, @params['enable_menu'] ? true : false, @params['check_pass_on_edit'] == 'each_edit', @prog, @graph_type, @missing, @show_authors, @show_leaves, @selected_categories ) redirect_show("HomePage", @params["address"]) end def administrate @logger.info "Taking administrative action: #{@params['action']}" redirect_show 'HomePage' unless wiki.authenticate(@params['system_password']) case @params['action'] when 'Delete Orphan Pages' wiki.remove_orphaned_pages(web_address) when 'Clear Render Cache' clear_render_cache true when 'Force Data Snapshot' WikiService.take_snapshot when 'Clean Storage' WikiService.clean_old_snapshots when 'Make This Web Default' wiki.default_web = web_address when 'Remove This Web' wiki.webs.delete web_address redirect_path '/' end @message = 'Operation succeeded' redirect_action 'edit_web/' end FAR_FUTURE = Date.new(4000,1,1).freeze TODO_RE = %r{TODO(?:[^:]*)?: (.*?)} def todo #{{{ parse_category set_menu_pages @context = @params['context'] @sort_order = @params['sort_order'] # the clear_render_cache hack to make sure we don't have old-style rendered # todos is no longer needed - can go thru the edit web to explicitely clear the cache. @todo_items = analyse_rendered_todo_items @pages_in_category.by_name @bliki_todo_items = analyse_rendered_todo_items web.bliki.values @context_links = @todo_items.clone.update(@bliki_todo_items).map { |page, items| # 'items' contain the full 'todo' info items.map { |item| item.context } }.flatten.compact.uniq.reject { |c| c.nil? || c.empty? }.map { |context| @context == context ? "[#{context}]" : "#{context}" } @todo_items = sort_and_filter_todo_items @todo_items, @sort_order, @context @bliki_todo_items = sort_and_filter_todo_items @bliki_todo_items, @sort_order, @context end #}}} def analyse_rendered_todo_items pages items = Hash.new { Array.new } pages.each do |page| if page.has_todos? # Page has todo items. Get the rendered version (marked-up and with links): # I specifically don't use the todo chunkss because I want the fully marked-up # text of the item. content = page.revisions.last.display_content items[page] = content.scan(TODO_RE).map { |match| RenderedTodo.new match[2], (match[0].empty? ? [] : match[0].split(',') ), (Date.parse(match[1]) rescue FAR_FUTURE) } end end items end def sort_and_filter_todo_items items, sort_order, context case sort_order when 'due_date' result = items.map { |page, todos| # sort the to items themselves [page, todos.sort_by { |i| i.due_date } ] }.sort_by { |page, todos| # sort the pages by the one with the most urgent todo todos.map{ |i| i.due_date }.min } else # default = sort by page name result = items.sort_by { |page, items| page.name } end if context # filter the items to those that are in context result = result.map { |page, items| [ page, items.select { |item| item.context.include? context } ] }.select { |page, items| not items.empty? } end result end def get_todo_display_style due_date # default is the muted 'darkred', to prevent to many bright red # items on one page: (See also chunks/todo.rb) (due_date <=> Date.today) > -1 ? "todoFuture" : "todo" end def clear_render_cache dont_redirect=false web.refresh_revisions redirect_path "/#{web_address}/edit_web/" unless dont_redirect end def set_menu_pages published = false #{{{ parse_category @all_pages = web.select { true } @menu_pages = case web.menu_type when 'all' then @all_pages.by_name when 'recent' then @all_pages.by_last_visited when 'viewed' then @all_pages.by_most_viewed when 'revised' then @all_pages.by_revision when 'category' then web.select { |page| page.in_category?(web.menu_category) } when 'user' @menu_content = if Page === web.menu_content web.menu_content.revisions.last.display_content else web.menu_content end @menu_content = @menu_content.gsub('/show/', '/published/').gsub( %r{\?}, '') if published nil when 'linkers' web.select { |page| page.wiki_words.size > 0 }.sort_by { |page| page.name } end if web.menu_limit && @menu_pages @menu_pages = @menu_pages[0..web.menu_limit] end end #}}} def bliki #{{{ set_menu_pages @entries = web.bliki_entries_by_date unless @req.query['authorname'].nil? || @req.query['authorname'] == 'noselect' @entries = @entries.select { |page| page.authors.include?(@req.query['authorname']) } end unless @req.query['regexp'].nil? @entries = @entries.select { |page| page.content =~ /#{@req.query['regexp']}/i } end @color = web.color @authors = web.authors end #}}} def mind #{{{ parse_category set_menu_pages set_mm_options @pngFile = @mapFile = nil case @graph_type when 'normal' @pngFile, @mapFile = web.create_mind_map(@prog, @missing, @show_authors, @show_leaves, @selected_categories, @mm_size) when 'author' @pngFile, @mapFile = web.create_author_graph(@prog, @selected_categories, @mm_size) when 'category' @pngFile, @mapFile = web.create_category_graph(@prog, @show_authors, @selected_categories, @mm_size) end end #}}} def set_mm_options #{{{ if @req.query.empty? @prog = web.mm_prog @graph_type = web.mm_graph_type @missing = @pages_in_category.wanted_pages if web.mm_show_missing @show_authors = web.mm_show_authors @show_leaves = web.mm_show_leaves @selected_categories = web.mm_selected_categories @mm_size = web.mind_map_size else @prog = @req.query['draw_type'] || 'neato' @graph_type = @req.query['graph_type'] || 'normal' @missing = @pages_in_category.wanted_pages if @req.query['missing'] @show_authors = @req.query['show_authors'] == 'on' @show_leaves = @req.query.empty? || @req.query['show_leaves'] == 'on' @selected_categories = parse_multi_select 'selected_categs' @selected_categories = [] if @selected_categories.include? 'all' @mm_size = @params['mind_map_size'] || web.mind_map_size end end #}}} def parse_multi_select field #{{{ if @req.body @req.body.split('&').map { |pair| pair.split('=') }.select { |k,v| k == field }.map { |k,v| v } else [] end end #}}} def edit_menu #{{{ @menu_type = web.menu_type @menu_content = web.menu_content @list_limit = web.menu_limit @list_limit += 1 if @list_limit >= -1 @author = default_author end #}}} def save_menu #{{{ redirect_show("HomePage") unless wiki.authenticate(@params["system_password"]) unless @req.query['action'] == 'Cancel Update' type = @req.query['type'] content = @req.query['content'] sel_category = @req.query['sel_category'] author = @req.query['author'] limit = @req.query['limit'].to_i rescue nil limit = 20 unless limit limit -= 1 if limit >= 0 # need to go through the WikiService to persist the command: wiki.save_menu_pref(web_address, type, limit, content, sel_category, Author.new(author, remote_ip)) end redirect_action 'edit_web/' # go back to web options page end #}}} def get_map_img file_name = "map.png" file_path = File.join WikiService.storage_path, file_name send_export(file_name, file_path, "image/png") end def adv_search parse_category set_menu_pages end # Within a single page -------------------------------------------------------- def show set_menu_pages if @page = wiki.read_page(web_address, page_name) unless page_name == 'HomePage' # HomePage should not be in the menu as there's a link at the top. @page.last_visited = Time.now @page.viewed += 1 end begin render_action "page" rescue => e $stderr << e.backtrace.join("\n") redirect_action "edit/#{CGI.escape(page_name)}?msg=#{CGI.escape(e.message)}" end else if page_name redirect_action "new/#{CGI.escape(page_name)}" else redirect_show "HomePage" end end end def published set_menu_pages true if web.published then @page = wiki.read_page(web_address, page_name || "HomePage") else redirect_show("HomePage") end end def print @page = wiki.read_page(web_address, page_name) end def tex @page = wiki.read_page(web_address, page_name) @tex_content = RedClothForTex.new(@page.content).to_tex end def pdf page = wiki.read_page(web_address, page_name) safe_page_name = page.name.gsub(/\W/, "") file_name = "#{safe_page_name}-#{web.address}-#{page.created_at.strftime("%Y-%m-%d-%H-%M")}" file_path = File.join EXPORT_DIRECTORY, file_name export_page_to_tex(file_path + ".tex") unless FileTest.exists?(file_path + ".tex") convert_tex_to_pdf(file_path + ".tex") send_export(file_name + ".pdf", file_path + ".pdf") end def new # redirect_show("HomePage") if web.check_pass_on_edit and not password_check(@params['password']) @page_name, @author = page_name, default_author end def edit @page = wiki.read_page(web_address, page_name) if !@page.locked?(Time.now) || @params["break_lock"] @page.lock(Time.now, default_author) @author = default_author render else render "wiki/locked" end end def cancel_edit @page = wiki.read_page(web_address, page_name) @page.unlock redirect_show end def save # if web.check_pass_on_edit and not password_check(@params['password']) # wiki.read_page(web_address, page_name).unlock if web.pages[page_name] # redirect_show("HomePage") # end if web.pages[page_name] page = wiki.revise_page( web_address, page_name, @params["content"], Time.now, Author.new(@params["author"], remote_ip), @params['edit_type'] ) page.unlock else page = wiki.write_page( web_address, page_name, @params["content"], Time.now, Author.new(@params["author"], remote_ip) ) end write_cookie("author", @params["author"], true) redirect_show(page_name) end def revision redirect_show 'HomePage' if page_name.nil? set_menu_pages @page = wiki.read_page(web_address, page_name) @revision = @page.revisions[@params["rev"].to_i] end def rollback # redirect_show("HomePage") if web.check_pass_on_edit and not password_check(@params['password']) @page = wiki.read_page(web_address, page_name) @revision = @page.revisions[@params["rev"].to_i] end # Bliki ---------------------------------------------------------------------- def bliki_delete # redirect_bliki if web.check_pass_on_edit and not password_check(@params['password']) wiki.delete_bliki_entry(web_address, page_name) clear_render_cache true redirect_bliki end def bliki_edit # redirect_bliki if web.check_pass_on_edit and not password_check(@params['password']) @page = wiki.read_bliki_entry(web_address, page_name) if !@page.locked?(Time.now) || @params["break_lock"] @page.lock(Time.now, default_author) @author = default_author render else @bliki_entry = true render "wiki/locked" end end def cancel_bliki_edit @page = wiki.read_bliki_entry(web_address, page_name) @page.unlock if @page redirect_bliki end def bliki_new @entry_name = @params['entry_name'] @author = default_author end def bliki_save # redirect_bliki if web.check_pass_on_edit and not password_check(@params['password']) pname = page_name || @params["pagename"] if web.bliki[pname] page = wiki.revise_bliki_entry(web_address, pname, @params["content"], Time.now, @params["author"]) page.unlock else page = wiki.write_bliki_entry(web_address, pname, @params["content"], Time.now, @params["author"]) end write_cookie("author", @params["author"]) redirect_bliki end def bliki_revision parse_category set_menu_pages @page = wiki.read_bliki_entry(web_address, page_name || @params['pagename']) if @page.nil? render_text "Unable to find bliki entry #{page_name || @params['pagename']}" return end @revision = @page.revisions[@params["rev"].to_i] || @page.revisions.last end def rollback_bliki # redirect_bliki if web.check_pass_on_edit and not password_check(@params['password']) @page = wiki.read_bliki_entry(web_address, page_name) wiki.rollback_bliki_entry(web_address, page_name, @params["rev"].to_i, Time.now) redirect_bliki end # ---------------------------------------------------------------------------- protected def before_action if in_a_web? && !authorized?(web_address) && !%w( login authenticate published ).include?(action_name) redirect_action("login") return false elsif in_a_web? @web, @page_name, @action_name = web, page_name, action_name end end def action_name if in_a_web? request_path[1] elsif action_methods.include?(request_path[0]) request_path[0] else "index" end end def redirect_show(page = @page.name, web = web_address) redirect_path "/#{web}/show/#{CGI.escape(page)}" end def redirect_bliki redirect_path "/#{web_address}/bliki/" end def redirect_action(action, web = web_address) redirect_path "/#{web}/#{action}" end def link_to_bliki(entry, web = web_address) #{{{ "#{entry.name}" end #}}} private def wiki WikiService.instance end def web wiki.webs[web_address] end def in_a_web? request_path.length > 1 end def web_address request_path[0] end def page_name CGI.unescape(request_path[2]) if request_path[2] end def authorized?(web_address) web.nil? || web.password.nil? || (read_cookie(web_address) && read_cookie(web_address) == web.password) || password_check(@params["password"]) end def default_author read_cookie("author") || "AnonymousCoward" end def password_check(password) web && @params["password"] == web.password && write_cookie(web_address, @params["password"]) end def export_pages_to_zip_file(zip_file_path) Zip::ZipOutputStream.open(zip_file_path) do |zos| web.select.by_name.each do |@page| zos.put_next_entry(@page.name + ".html") zos.puts(template_engine("print").result(binding)) end zos.put_next_entry "pages-metadata.txt" web.select.by_name.each do |page| zos.puts "#{page.name}: by #{page.author}, created on #{page.pretty_created_at}" end web.select_bliki.by_name.each do |@page| zos.put_next_entry("bliki/#{@page.name}.html") zos.puts(template_engine("print").result(binding)) end zos.put_next_entry "bliki/bliki-metadata.txt" web.select_bliki.by_name.each do |page| zos.puts "#{page.name}: by #{page.author}, created on #{page.revisions.first.pretty_created_at}" end zos.put_next_entry("index.html") zos.puts('') end end def export_markup_to_zip_file(zip_file_path) Zip::ZipOutputStream.open(zip_file_path) do |zos| web.select.by_name.each do |page| zos.put_next_entry(page.name + ".#{web.markup}") zos.puts(page.content) end zos.put_next_entry "pages-metadata.txt" web.select.by_name.each do |page| zos.puts "#{page.name}: by #{page.author}, created on #{page.pretty_created_at}" end web.select_bliki.by_name.each do |page| zos.put_next_entry("bliki/#{page.name}.#{web.markup}") zos.puts(page.content) end zos.put_next_entry "bliki/bliki-metadata.txt" web.select_bliki.by_name.each do |page| zos.puts "#{page.name}: by #{page.author}, created on #{page.revisions.first.pretty_created_at}" end end end def export_web_to_tex(file_path) @web_name = web.name @tex_content = table_of_contents(web.pages["HomePage"].content.dup, render_tex_web) File.open(file_path, "w") { |f| f.write(template_engine("tex_web").result(binding)) } end def render_tex_web pages = web.select.by_name.inject({}) do |tex_web, page| tex_web[page.name] = RedClothForTex.new(page.content).to_tex tex_web end bliki_entries = web.select_bliki.by_name.inject({}) do |tex_web, page| tex_web["bliki/#{page.name}"] = RedClothForTex.new(page.content).to_tex tex_web end pages.update bliki_entries end def export_page_to_tex(file_path) tex File.open(file_path, "w") { |f| f.write(template_engine("tex").result(binding)) } end def convert_tex_to_pdf(tex_path) `cd #{File.dirname(tex_path)}; pdflatex --interaction=scrollmode '#{File.basename(tex_path)}'` end def truncate(text, length = 30, truncate_string = "...") if text.length > length then text[0..(length - 3)] + truncate_string else text end end def render_markup_help if web markup = web.markup markup = 'markdown' if markup.to_s =~ /markdown/ sub_template("#{markup}_help") else '' end end def send_export(file_name, file_path, content_type = "application/zip") @res["Content-Type"] = content_type @res["Content-Disposition"] = "attachment; filename=#{file_name}" @res["Content-Length"] = File.size(file_path) File.open(file_path, "rb") { |f| @res.body = f.read } end def template_engine(template_name) ERB.new(IO.readlines(action_template_path(template_name)).join) end def remote_ip $stderr << "#{@req.meta_vars['HTTP_X_FORWARDED_FOR']} || #{@req.meta_vars['REMOTE_ADDR']}" @req.meta_vars["HTTP_X_FORWARDED_FOR"] || @req.meta_vars["REMOTE_ADDR"] end end # jEdit :folding=indent:collapseFolds=2: