2c5e5a0015
Move the truncate() method into ApplicationHelper. Move another method around, for no particularly good reason. Controllers really shouldn't have public methods that don't correspond to actions.
527 lines
17 KiB
Ruby
527 lines
17 KiB
Ruby
require 'fileutils'
|
|
require 'maruku'
|
|
require 'maruku/ext/math'
|
|
require 'zip/zip'
|
|
require 'stringsupport'
|
|
require 'resolv'
|
|
|
|
class WikiController < ApplicationController
|
|
|
|
before_filter :load_page
|
|
before_filter :dnsbl_check, :only => [:edit, :new, :save, :export_html, :export_markup]
|
|
caches_action :show, :published, :authors, :tex, :s5, :print, :recently_revised, :list, :file_list, :source,
|
|
:history, :revision, :atom_with_content, :atom_with_headlines, :if => Proc.new { |c| c.send(:do_caching?) }
|
|
cache_sweeper :revision_sweeper
|
|
|
|
layout 'default', :except => [:atom_with_content, :atom_with_headlines, :atom, :source, :tex, :s5, :export_html]
|
|
|
|
def index
|
|
if @web_name
|
|
redirect_home
|
|
elsif not @wiki.setup?
|
|
redirect_to :controller => 'admin', :action => 'create_system'
|
|
elsif @wiki.webs.size == 1
|
|
redirect_home @wiki.webs.values.first.address
|
|
else
|
|
redirect_to :action => 'web_list'
|
|
end
|
|
end
|
|
|
|
# Outside a single web --------------------------------------------------------
|
|
|
|
def authenticate
|
|
if password_check(params['password'])
|
|
redirect_home
|
|
else
|
|
flash[:info] = password_error(params['password'])
|
|
redirect_to :action => 'login', :web => @web_name
|
|
end
|
|
end
|
|
|
|
def login
|
|
# to template
|
|
end
|
|
|
|
def web_list
|
|
@webs = wiki.webs.values.sort_by { |web| web.name }
|
|
end
|
|
|
|
|
|
# Within a single web ---------------------------------------------------------
|
|
|
|
def authors
|
|
@page_names_by_author = @web.page_names_by_author
|
|
@authors = @page_names_by_author.keys.sort
|
|
end
|
|
|
|
def file_list
|
|
sort_order = params['sort_order'] || 'file_name'
|
|
case sort_order
|
|
when 'file_name'
|
|
@alt_sort_order = 'created_at'
|
|
@alt_sort_name = 'date'
|
|
else
|
|
@alt_sort_order = 'file_name'
|
|
@alt_sort_name = 'filename'
|
|
end
|
|
@file_list = @web.file_list(sort_order)
|
|
end
|
|
|
|
def export_html
|
|
stylesheet = Rails.root.join('public', 'stylesheets', 'instiki.css').read
|
|
export_pages_as_zip(html_ext) do |page|
|
|
|
|
renderer = PageRenderer.new(page.revisions.last)
|
|
rendered_page = <<-EOL
|
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg-flat.dtd" >
|
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
<head>
|
|
<title>#{page.plain_name} in #{@web.name}</title>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
|
|
<style type="text/css">
|
|
h1#pageName, .newWikiWord a, a.existingWikiWord, .newWikiWord a:hover {
|
|
color: ##{@web ? @web.color : "393" };
|
|
}
|
|
.newWikiWord { background-color: white; font-style: italic; }
|
|
#{stylesheet}
|
|
</style>
|
|
<style type="text/css">
|
|
#{@web.additional_style}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1 id="pageName">
|
|
<span class="webName">#{@web.name}</span><br />
|
|
#{page.plain_name}
|
|
</h1>
|
|
#{renderer.display_content_for_export}
|
|
<div class="byline">
|
|
#{page.revisions? ? "Revised" : "Created" } on #{ page.revised_at.strftime('%B %d, %Y %H:%M:%S') }
|
|
by
|
|
#{ UrlGenerator.new(self).make_link(@web, page.author.name, @web, nil, { :mode => :export }) }
|
|
</div>
|
|
</body>
|
|
</html>
|
|
EOL
|
|
rendered_page
|
|
end
|
|
end
|
|
|
|
def export_markup
|
|
export_pages_as_zip(@web.markup) { |page| page.content }
|
|
end
|
|
|
|
# def export_pdf
|
|
# file_name = "#{@web.address}-tex-#{@web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}"
|
|
# file_path = File.join(@wiki.storage_path, file_name)
|
|
#
|
|
# export_web_to_tex "#{file_path}.tex" unless FileTest.exists? "#{file_path}.tex"
|
|
# convert_tex_to_pdf "#{file_path}.tex"
|
|
# send_file "#{file_path}.pdf"
|
|
# end
|
|
|
|
# def export_tex
|
|
# file_name = "#{@web.address}-tex-#{@web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}.tex"
|
|
# file_path = File.join(@wiki.storage_path, file_name)
|
|
# export_web_to_tex(file_path) unless FileTest.exists?(file_path)
|
|
# send_file file_path
|
|
# end
|
|
|
|
def feeds
|
|
@rss_with_content_allowed = rss_with_content_allowed?
|
|
# show the template
|
|
end
|
|
|
|
def list
|
|
parse_category
|
|
@page_names_that_are_wanted = @pages_in_category.wanted_pages
|
|
@pages_that_are_orphaned = @pages_in_category.orphaned_pages
|
|
end
|
|
|
|
def recently_revised
|
|
parse_category
|
|
@pages_by_revision = @pages_in_category.by_revision
|
|
@pages_by_day = Hash.new { |h, day| h[day] = [] }
|
|
@pages_by_revision.each do |page|
|
|
day = Date.new(page.revised_at.year, page.revised_at.month, page.revised_at.day)
|
|
@pages_by_day[day] << page
|
|
end
|
|
end
|
|
|
|
def atom_with_content
|
|
if rss_with_content_allowed?
|
|
render_atom(hide_description = false)
|
|
else
|
|
render :text => 'Atom feed with content for this web is blocked for security reasons. ' +
|
|
'The web is password-protected and not published', :status => 403, :layout => 'error'
|
|
end
|
|
end
|
|
|
|
def atom_with_headlines
|
|
render_atom(hide_description = true)
|
|
end
|
|
|
|
def search
|
|
@query = params['query'].purify
|
|
@title_results = @web.select { |page| page.name =~ /#{@query}/i }.sort
|
|
@results = @web.select { |page| page.content =~ /#{@query}/i }.sort
|
|
all_pages_found = (@results + @title_results).uniq
|
|
if all_pages_found.size == 1
|
|
redirect_to_page(all_pages_found.first.name)
|
|
end
|
|
end
|
|
|
|
# Within a single page --------------------------------------------------------
|
|
|
|
def cancel_edit
|
|
@page.unlock
|
|
redirect_to_page(@page_name)
|
|
end
|
|
|
|
def edit
|
|
if @page.nil?
|
|
redirect_home
|
|
elsif @page.locked?(Time.now) and not params['break_lock']
|
|
redirect_to :web => @web_name, :action => 'locked', :id => @page_name
|
|
else
|
|
@page.lock(Time.now, @author)
|
|
end
|
|
end
|
|
|
|
def locked
|
|
# to template
|
|
end
|
|
|
|
def new
|
|
# to template
|
|
end
|
|
|
|
# def pdf
|
|
# page = wiki.read_page(@web_name, @page_name)
|
|
# safe_page_name = @page.name.gsub(/\W/, '')
|
|
# file_name = "#{safe_page_name}-#{@web.address}-#{@page.revised_at.strftime('%Y-%m-%d-%H-%M-%S')}"
|
|
# file_path = File.join(@wiki.storage_path, file_name)
|
|
#
|
|
# export_page_to_tex("#{file_path}.tex") unless FileTest.exists?("#{file_path}.tex")
|
|
# # NB: this is _very_ slow
|
|
# convert_tex_to_pdf("#{file_path}.tex")
|
|
# send_file "#{file_path}.pdf"
|
|
# end
|
|
|
|
def print
|
|
if @page.nil?
|
|
redirect_home
|
|
end
|
|
@link_mode ||= :show
|
|
@renderer = PageRenderer.new(@page.revisions.last)
|
|
# to template
|
|
end
|
|
|
|
def published
|
|
if not @web.published?
|
|
render(:text => "Published version of web '#{@web_name}' is not available", :status => 404, :layout => 'error')
|
|
return
|
|
end
|
|
|
|
@page_name ||= 'HomePage'
|
|
@page ||= wiki.read_page(@web_name, @page_name)
|
|
@link_mode ||= :publish
|
|
if @page
|
|
@renderer = PageRenderer.new(@page.revisions.last)
|
|
else
|
|
real_page = WikiReference.page_that_redirects_for(@web, @page_name)
|
|
if real_page
|
|
flash[:info] = "Redirected from \"#{@page_name}\"."
|
|
redirect_to :web => @web_name, :action => 'published', :id => real_page, :status => 301
|
|
else
|
|
render(:text => "Page '#{@page_name}' not found", :status => 404, :layout => 'error')
|
|
end
|
|
end
|
|
end
|
|
|
|
def revision
|
|
get_page_and_revision
|
|
@show_diff = (params[:mode] == 'diff')
|
|
@renderer = PageRenderer.new(@revision)
|
|
end
|
|
|
|
def rollback
|
|
get_page_and_revision
|
|
if @page.locked?(Time.now) and not params['break_lock']
|
|
redirect_to :web => @web_name, :action => 'locked', :id => @page_name
|
|
else
|
|
@page.lock(Time.now, @author)
|
|
end
|
|
end
|
|
|
|
def save
|
|
render(:status => 404, :text => 'Undefined page name', :layout => 'error') and return if @page_name.nil?
|
|
unless (request.post? || ENV["RAILS_ENV"] == "test")
|
|
headers['Allow'] = 'POST'
|
|
render(:status => 405, :text => 'You must use an HTTP POST', :layout => 'error')
|
|
return
|
|
end
|
|
author_name = params['author'].purify
|
|
author_name = 'AnonymousCoward' if author_name =~ /^\s*$/
|
|
|
|
begin
|
|
the_content = params['content'].purify
|
|
prev_content = ''
|
|
filter_spam(the_content)
|
|
raise Instiki::ValidationError.new('Your name cannot contain a "."') if author_name.include? '.'
|
|
cookies['author'] = { :value => author_name.dup.as_bytes, :expires => Time.utc(2030) }
|
|
if @page
|
|
new_name = params['new_name'] ? params['new_name'].purify : @page_name
|
|
prev_content = @page.current_revision.content
|
|
raise Instiki::ValidationError.new('Your new title cannot contain a "."') if new_name.include? '.'
|
|
raise Instiki::ValidationError.new('A page named "' + new_name.escapeHTML + '" already exists.') if
|
|
@page_name != new_name && @web.has_page?(new_name)
|
|
wiki.revise_page(@web_name, @page_name, new_name, the_content, Time.now,
|
|
Author.new(author_name, remote_ip), PageRenderer.new)
|
|
@page.unlock
|
|
@page_name = new_name
|
|
else
|
|
wiki.write_page(@web_name, @page_name, the_content, Time.now,
|
|
Author.new(author_name, remote_ip), PageRenderer.new)
|
|
end
|
|
redirect_to_page @page_name
|
|
rescue Instiki::ValidationError => e
|
|
flash[:error] = e.to_s
|
|
logger.error e
|
|
param_hash = {:web => @web_name, :id => @page_name}
|
|
# Work around Rails bug: flash will not display if query string is longer than 10192 bytes
|
|
param_hash.update( :content => the_content ) if the_content &&
|
|
CGI::escape(the_content).length < 10183 && the_content != prev_content
|
|
if @page
|
|
@page.unlock
|
|
redirect_to param_hash.update( :action => 'edit' )
|
|
else
|
|
redirect_to param_hash.update( :action => 'new' )
|
|
end
|
|
end
|
|
end
|
|
|
|
def show
|
|
if @page
|
|
begin
|
|
@renderer = PageRenderer.new(@page.revisions.last)
|
|
@show_diff = (params[:mode] == 'diff')
|
|
render :action => 'page'
|
|
# TODO this rescue should differentiate between errors due to rendering and errors in
|
|
# the application itself (for application errors, it's better not to rescue the error at all)
|
|
rescue => e
|
|
logger.error e
|
|
flash[:error] = e.to_s
|
|
if in_a_web?
|
|
redirect_to :action => 'edit', :web => @web_name, :id => @page_name
|
|
else
|
|
raise e
|
|
end
|
|
end
|
|
else
|
|
if not @page_name.nil? and not @page_name.empty?
|
|
real_page = WikiReference.page_that_redirects_for(@web, @page_name)
|
|
if real_page
|
|
flash[:info] = "Redirected from \"#{@page_name}\"."
|
|
redirect_to :web => @web_name, :action => 'show', :id => real_page, :status => 301
|
|
else
|
|
flash[:info] = "Page \"#{@page_name}\" does not exist.\n" +
|
|
"Please create it now, or hit the \"back\" button in your browser."
|
|
redirect_to :web => @web_name, :action => 'new', :id => @page_name
|
|
end
|
|
else
|
|
render :text => 'Page name is not specified', :status => 404, :layout => 'error'
|
|
end
|
|
end
|
|
end
|
|
|
|
def history
|
|
if @page
|
|
@revisions_by_day = Hash.new { |h, day| h[day] = [] }
|
|
@revision_numbers = Hash.new { |h, id| h[id] = [] }
|
|
revision_number = @page.revisions.size
|
|
@page.revisions.reverse.each do |rev|
|
|
day = Date.new(rev.revised_at.year, rev.revised_at.month, rev.revised_at.day)
|
|
@revisions_by_day[day] << rev
|
|
@revision_numbers[rev.id] = revision_number
|
|
revision_number = revision_number - 1
|
|
end
|
|
render :action => 'history'
|
|
else
|
|
if not @page_name.nil? and not @page_name.empty?
|
|
redirect_to :web => @web_name, :action => 'new', :id => @page_name
|
|
else
|
|
render :text => 'Page name is not specified', :status => 404, :layout => 'error'
|
|
end
|
|
end
|
|
end
|
|
|
|
def source
|
|
#to template
|
|
end
|
|
|
|
def tex
|
|
if [:markdownMML, :markdownPNG, :markdown].include?(@web.markup)
|
|
@tex_content = Maruku.new(@page.content).to_latex
|
|
else
|
|
@tex_content = 'TeX export only supported with the Markdown text filters.'
|
|
end
|
|
end
|
|
|
|
def s5
|
|
if [:markdownMML, :markdownPNG, :markdown].include?(@web.markup)
|
|
my_rendered = PageRenderer.new(@page.revisions.last)
|
|
@s5_content = my_rendered.display_s5
|
|
@s5_theme = my_rendered.s5_theme
|
|
else
|
|
@s5_content = "S5 not supported with this text filter"
|
|
@s5_theme = "default"
|
|
end
|
|
end
|
|
|
|
def html_ext
|
|
if xhtml_enabled? && request.env.include?('HTTP_ACCEPT') &&
|
|
Mime::Type.parse(request.env["HTTP_ACCEPT"]).include?(Mime::XHTML)
|
|
'xhtml'
|
|
else
|
|
'html'
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def do_caching?
|
|
flash.empty?
|
|
end
|
|
|
|
def load_page
|
|
@page_name = params['id'] ? params['id'].purify : nil
|
|
@page = @wiki.read_page(@web_name, @page_name) if @page_name
|
|
end
|
|
|
|
private
|
|
|
|
# def convert_tex_to_pdf(tex_path)
|
|
# # TODO remove earlier PDF files with the same prefix
|
|
# # TODO handle gracefully situation where pdflatex is not available
|
|
# begin
|
|
# wd = Dir.getwd
|
|
# Dir.chdir(File.dirname(tex_path))
|
|
# logger.info `pdflatex --interaction=nonstopmode #{File.basename(tex_path)}`
|
|
# ensure
|
|
# Dir.chdir(wd)
|
|
# end
|
|
# end
|
|
|
|
def export_page_to_tex(file_path)
|
|
if @web.markup == :markdownMML || @web.markup == :markdownPNG
|
|
@tex_content = Maruku.new(@page.content).to_latex
|
|
else
|
|
@tex_content = 'TeX export only supported with the Markdown text filters.'
|
|
end
|
|
File.open(file_path, 'w') { |f| f.write(render_to_string(:template => 'wiki/tex', :layout => 'tex')) }
|
|
end
|
|
|
|
def export_pages_as_zip(file_type, &block)
|
|
|
|
file_prefix = "#{@web.address}-#{file_type}-"
|
|
timestamp = @web.revised_at.strftime('%Y-%m-%d-%H-%M-%S')
|
|
file_path = @wiki.storage_path.join(file_prefix + timestamp + '.zip')
|
|
tmp_path = "#{file_path}.tmp"
|
|
|
|
Zip::ZipOutputStream.open(tmp_path) do |zip_out|
|
|
@web.select.by_name.each do |page|
|
|
zip_out.put_next_entry("#{CGI.escape(page.name)}.#{file_type}")
|
|
zip_out.puts(block.call(page))
|
|
end
|
|
# add an index file, if exporting to HTML
|
|
if file_type.to_s.downcase == html_ext
|
|
zip_out.put_next_entry "index.#{html_ext}"
|
|
zip_out.puts "<html xmlns='http://www.w3.org/1999/xhtml'><head>" +
|
|
"<META HTTP-EQUIV=\"Refresh\" CONTENT=\"0;URL=HomePage.#{file_type}\"></head></html>"
|
|
end
|
|
end
|
|
FileUtils.rm_rf(Dir[@wiki.storage_path.join(file_prefix + '*.zip').to_s])
|
|
FileUtils.mv(tmp_path, file_path)
|
|
send_file file_path
|
|
end
|
|
|
|
# def export_web_to_tex(file_path)
|
|
# if @web.markup == :markdownMML
|
|
# @tex_content = Maruku.new(@page.content).to_latex
|
|
# else
|
|
# @tex_content = 'TeX export only supported with the Markdown text filters.'
|
|
# end
|
|
# @tex_content = table_of_contents(@web.page('HomePage').content, render_tex_web)
|
|
# File.open(file_path, 'w') { |f| f.write(render_to_string(:template => 'wiki/tex_web', :layout => tex)) }
|
|
# end
|
|
|
|
def get_page_and_revision
|
|
if params['rev']
|
|
@revision_number = params['rev'].to_i
|
|
else
|
|
@revision_number = @page.revisions.size
|
|
end
|
|
@revision = @page.revisions[@revision_number - 1]
|
|
end
|
|
|
|
def parse_category
|
|
@categories = WikiReference.list_categories(@web).sort
|
|
@category = params['category']
|
|
if @category
|
|
@set_name = "category '#{@category}'"
|
|
pages = WikiReference.pages_in_category(@web, @category).sort.map { |page_name| @web.page(page_name) }
|
|
@pages_in_category = PageSet.new(@web, pages)
|
|
else
|
|
# no category specified, return all pages of the web
|
|
@pages_in_category = @web.select_all.by_name
|
|
@set_name = 'the web'
|
|
end
|
|
end
|
|
|
|
def remote_ip
|
|
ip = request.remote_ip
|
|
logger.info(ip)
|
|
ip.dup.gsub!(Regexp.union(Resolv::IPv4::Regex, Resolv::IPv6::Regex), '\0') || 'bogus address'
|
|
end
|
|
|
|
def render_atom(hide_description = false, limit = 15)
|
|
@pages_by_revision = @web.select.by_revision.first(limit)
|
|
@hide_description = hide_description
|
|
@link_action = @web.password ? 'published' : 'show'
|
|
render :action => 'atom'
|
|
end
|
|
|
|
def render_tex_web
|
|
@web.select.by_name.inject({}) do |tex_web, page|
|
|
if @web.markup == :markdownMML || @web.markup == :markdownPNG
|
|
tex_web[page.name] = Maruku.new(page.content).to_latex
|
|
else
|
|
tex_web[page.name] = 'TeX export only supported with the Markdown text filters.'
|
|
end
|
|
tex_web
|
|
end
|
|
end
|
|
|
|
def rss_with_content_allowed?
|
|
@web.password.nil? or @web.published?
|
|
end
|
|
|
|
def filter_spam(content)
|
|
@@spam_patterns ||= load_spam_patterns
|
|
@@spam_patterns.each do |pattern|
|
|
raise Instiki::ValidationError.new("Your edit was blocked by spam filtering") if content =~ pattern
|
|
end
|
|
end
|
|
|
|
def load_spam_patterns
|
|
spam_patterns_file = Rails.root.join('config', 'spam_patterns.txt')
|
|
if File.exists?(spam_patterns_file)
|
|
spam_patterns_file.readlines.inject([]) { |patterns, line| patterns << Regexp.new(line.chomp, Regexp::IGNORECASE) }
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
end
|