instiki/lib/wiki_content.rb
Jacques Distler 9e909d5be3 Update Rails, rails_xss and Bundler
Update Bundler to 1.0.15.
Update Rails to 2.3.12.
Update rails_xss plugin.

The latter two were the
source of a considerable
amount of grief, as rails_xss
is now MUCH stricter about what
string methods can be used.

Also made it possible to use
rake 0.9.x with Instiki. But
you probably REALLY want to use

 ruby bundle exec rake ...

instead of just saying

 rake ....
2011-06-15 00:43:38 -05:00

232 lines
6.9 KiB
Ruby

require 'cgi'
require 'chunks/engines'
require 'chunks/category'
require_dependency 'chunks/include'
require_dependency 'chunks/redirect'
require_dependency 'chunks/wiki'
require_dependency 'chunks/literal'
require 'chunks/nowiki'
require 'sanitizer'
require 'instiki_stringsupport'
# Wiki content is just a string that can process itself with a chain of
# actions. The actions can modify wiki content so that certain parts of
# it are protected from being rendered by later actions.
#
# When wiki content is rendered, it can be interrogated to find out
# which chunks were rendered. This means things like categories, wiki
# links, can be determined.
#
# Exactly how wiki content is rendered is determined by a number of
# settings that are optionally passed in to a constructor. The current
# options are:
# * :engine
# => The structural markup engine to use (Textile, Markdown, RDoc)
# * :engine_opts
# => A list of options to pass to the markup engines (safe modes, etc)
# * :pre_engine_actions
# => A list of render actions or chunks to be processed before the
# markup engine is applied. By default this is:
# Category, Include, URIChunk, WikiChunk::Link, WikiChunk::Word
# * :post_engine_actions
# => A list of render actions or chunks to apply after the markup
# engine. By default these are:
# Literal::Pre, Literal::Tags
# * :mode
# => How should the content be rendered? For normal display (show),
# publishing (:publish) or export (:export)?
module ChunkManager
attr_reader :chunks_by_type, :chunks_by_id, :chunks, :chunk_id
ACTIVE_CHUNKS = [ NoWiki, Category, Redirect, WikiChunk::Link,
WikiChunk::Word ]
HIDE_CHUNKS = [ Literal::Pre, Literal::Tags, Literal::Math ]
MASK_RE = {
ACTIVE_CHUNKS => Chunk::Abstract.mask_re(ACTIVE_CHUNKS),
HIDE_CHUNKS => Chunk::Abstract.mask_re(HIDE_CHUNKS)
}
def init_chunk_manager
@chunks_by_type = Hash.new
Chunk::Abstract::derivatives.each{|chunk_type|
@chunks_by_type[chunk_type] = Array.new
}
@chunks_by_id = Hash.new
@chunks = []
@chunk_id = 0
end
def add_chunk(c)
@chunks_by_type[c.class] << c
@chunks_by_id[c.object_id] = c
@chunks << c
@chunk_id += 1
end
def delete_chunk(c)
@chunks_by_type[c.class].delete(c)
@chunks_by_id.delete(c.object_id)
@chunks.delete(c)
end
def merge_chunks(other)
other.chunks.each{|c| add_chunk(c)}
end
def scan_chunkid(text)
text.scan(MASK_RE[ACTIVE_CHUNKS]){|a| yield a[0] }
end
def find_chunks(chunk_type)
@chunks.select { |chunk| chunk.kind_of?(chunk_type) and chunk.rendered? }
end
end
# A simplified version of WikiContent. Useful to avoid recursion problems in
# WikiContent.new
class WikiContentStub < String
attr_reader :web, :options
include ChunkManager
def initialize(content, web, options)
super(content)
@web = web
@options = options
init_chunk_manager
end
# Detects the mask strings contained in the text of chunks of type chunk_types
# and yields the corresponding chunk ids
# example: content = "chunk123categorychunk <pre>chunk456categorychunk</pre>"
# inside_chunks(Literal::Pre) ==> yield 456
def inside_chunks(chunk_types)
chunk_types.each{|chunk_type| chunk_type.apply_to(self) }
chunk_types.each{|chunk_type| @chunks_by_type[chunk_type].each{|hide_chunk|
scan_chunkid(hide_chunk.text){|id| yield id }
}
}
end
end
class WikiContent < ActiveSupport::SafeBuffer
include ChunkManager
include Sanitizer
DEFAULT_OPTS = {
:active_chunks => ACTIVE_CHUNKS,
:hide_chunks => HIDE_CHUNKS,
:engine => Engines::MarkdownMML,
:engine_opts => [],
:mode => :show
}.freeze
attr_reader :web, :options, :revision, :not_rendered, :pre_rendered, :url_generator
# Create a new wiki content string from the given one.
# The options are explained at the top of this file.
def initialize(revision, url_generator, options = {})
@revision = revision
@url_generator = url_generator
@web = @revision.page.web
@options = DEFAULT_OPTS.dup.merge(options)
@options[:engine] = Engines::MAP[@web.markup]
@options[:engine_opts] = [:filter_html, :filter_styles] if @web.safe_mode?
@options[:active_chunks] = (ACTIVE_CHUNKS - [WikiChunk::Word] ) if @web.brackets_only?
@options[:hide_chunks] = (HIDE_CHUNKS - [Literal::Math] ) unless
[Engines::MarkdownMML, Engines::MarkdownPNG].include?(@options[:engine])
if @options[:engine] == Engines::MarkdownPNG
@options[:png_url] =
@options[:mode] == :export ? 'files/pngs/' :
(@url_generator.url_for :controller => 'file', :web => @web.address,
:action => 'file', :id => 'pngs', :only_path => true) + '/'
end
@not_rendered = @pre_rendered = nil
super(@revision.content)
init_chunk_manager
build_chunks
@not_rendered = String.new(self)
end
# Call @web.page_link using current options.
def page_link(web_name, name, text, link_type)
web = Web.find_by_name(web_name) || Web.find_by_address(web_name) || @web
@options[:link_type] = (link_type || :show)
@url_generator.make_link(@web, name, web, text, @options)
end
def build_chunks
# create and mask Includes and "active_chunks" chunks
NoWiki.apply_to(self) if @options[:active_chunks].include?(NoWiki)
Include.apply_to(self)
@options[:active_chunks].each{|chunk_type| chunk_type.apply_to(self) unless chunk_type == NoWiki}
# Handle hiding contexts like "pre" and "code" etc..
# The markup (textile, rdoc etc) can produce such contexts with its own syntax.
# To reveal them, we work on a copy of the content.
# The copy is rendered and used to detect the chunks that are inside protecting context
# These chunks are reverted on the original content string.
copy = WikiContentStub.new(self, @web, @options)
@options[:engine].apply_to(copy)
copy.inside_chunks(@options[:hide_chunks]) do |id|
@chunks_by_id[id.to_i].revert if @chunks_by_id[id.to_i]
end
end
def delete_chunks!(types)
types.each do |t|
@chunks_by_type[t].each do |c|
@pre_rendered.sub!(c.mask, '') if @pre_rendered
@chunks.delete(c)
end
end
self
end
def pre_render!
unless @pre_rendered
@chunks_by_type[Include].each{|chunk| chunk.unmask }
@pre_rendered = String.new(self)
end
@pre_rendered
end
def render!
pre_render!
@options[:engine].apply_to(self)
as_utf8
# unmask in one go. $~[1] is the chunk id
text = self.to_str
text.gsub!(MASK_RE[ACTIVE_CHUNKS]) do
chunk = @chunks_by_id[$~[1].to_i]
if chunk.nil?
# if we match a chunkmask that existed in the original content string
# just keep it as it is
$~[0]
else
chunk.unmask_text
end
end
self.replace xhtml_sanitize(text)
self.html_safe
end
def page_name
@revision.page.name
end
end