require 'thread' # Revision stores changes as a diff against the more recent content version class Revision attr_reader :number, :author, :page attr_reader :changes, :created_on alias :revised_on :created_on def initialize( page, number, changes, author, created_on = Time.now ) @page, @number, @changes, @author, @created_on = page, number, changes, author, created_on end # Recreates the content of the page AFTER this revision had been made. # Done by recursively applying diffs to more recent versions. def content page.revision( @number + 1 ) ? page.revision( @number + 1 ).previous_content : page.content end # Recreateds the content of the page BEFORE this revision had been made def previous_content content.split("\n").unpatch!(@changes).join("\n") end # Delegates to the Page object so that can see previous versions def textile( content = self.content ) page.textile( content ) end def method_missing( symbol, *args ) raise(ArgumentError, "Revision does not respond to #{symbol}", caller) unless @page && @page.respond_to?( symbol ) @page.send symbol, *args end # To allow the contents to be dumped in a class independent way. # Must match the order of the variables used in initialize def to_a() [@number, @changes, @author, @created_on] end end class Page attr_accessor :content_lock, :name, :content, :revisions attr_accessor :links_lock, :links_from, :links_to, :inserted_into alias :to_s :name # Returns an empty version of itself. def self.empty( name ) empty = self.new( name ) empty.revise( $MESSAGES[:Type_what_you_want_here_and_click_save], "NoOne" ) class << empty def textile; "[[#{$MESSAGES[:Create]} #{name} => /edit/#{name} ]]" end def empty?; true; end end empty end def initialize( name ) @content_lock, @links_lock = Mutex.new, Mutex.new @name, @content, @revisions = name, "", [] @links_from, @links_to = [], [] @inserted_into = [] end # Revises the content of this page, creating a new revision class that stores the changes def revise( new_content, author ) return nil if new_content == @content #I've abandoned this, because it tends to confuse users. #if the_same_author_recently_revised_this_page( author ) # changes = changes_between( previous_content, new_content ) # @revisions[-1] = Revision.new( self, @revisions.length - 1, changes , author ) #else changes = changes_between( @content, new_content ) return nil if changes.empty? @revisions << Revision.new( self, @revisions.length, changes , author ) #end @content = new_content @revisions.last end # Returns the content of this page to that of a previous version def rollback( number, author ) revise( ( number < 0 ) ? $MESSAGES[:page_deleted] : @revisions[ number ].content, author ) end def revision( number ) @revisions[ number ] end def deleted? ( content =~ /^#{$MESSAGES[:page_deleted]}/i ) || ( content =~ /^#{$MESSAGES[:content_moved_to]} /i ) end def empty?; @revisions.empty? end def <=>( otherpage ) self.score <=> otherpage.score end def score; @links_from.size + @links_to.size end def created_on; @revisions.first.created_on end def is_inserted_into( page ) @links_lock.synchronize { @inserted_into << page unless @inserted_into.include? page } end def textile( content = @content ) content end def name_for_index; name.downcase end # Any unhandled calls are passed onto the latest revision (e.g. author, creation time etc) def method_missing( symbol, *args ) if @revisions.last && @revisions.last.respond_to?(symbol) @revisions.last.send symbol, *args else raise ArgumentError,"Page does not respond to #{symbol}", caller end end private def changes_between( old_content, new_content ) old_content.split("\n").diff( new_content.split("\n") ).map { |changeset| changeset.map { |change| change.to_a } } end # This is no longer used, because I've found it confuses people. def the_same_author_recently_revised_this_page( author ) return false if empty? return false unless author == self.author ((Time.now - self.revised_on) < (60*5) ) # 5 Minutes end end #Serves as a marker, so ImagePage and AttachmentPage can re-use the same view templates class UploadPage < Page end class ImagePage < UploadPage def textile( content = @content ) deleted? ? content : "!#{$SETTINGS[:url]}#{content.strip}!:#{$SETTINGS[:url]}/view/#{@name.url_encode}" end def name_for_index; @name[10..-1].strip.downcase end end class AttachmentPage < UploadPage def textile( content = @content ) deleted? ? content : %Q{[[ #{name} => #{$SETTINGS[:url]}#{content} ]]\n} end def name_for_index; @name[ 9..-1].strip.downcase end end # This class has turned into a behmoth, need to refactor. class Wiki include WikiFlatFileStore include Enumerable include Notify # Will notify any watchers if underlying files change PAGE_CLASSES = [ [ /^picture of/i, ImagePage ], [ /^attached/i, AttachmentPage ], [ /.*/, Page ] ] def initialize( folder ) @folder = folder @pages = {} @shutting_down = false load_all_pages start_watching_files end def shutdown sleep(1) until event_queue.empty? @shutting_down = true # Stop further modifications end def page( name ) page_named( name )|| new_page( name, :empty ) end def each( exclude_deleted = true ) @pages.each { |name, page| yield [name, page] unless exclude_deleted && page.deleted? } end def exists?( name ) @pages.include?( name.downcase ) && !page( name.downcase ).deleted? end def revise( pagename, content, author ) raise "Sorry! Shutting down..." if @shutting_down check_disk_for_updated_page pagename mutate( pagename ) { |page| page.revise( content, author ) } end def rollback( pagename, number, author ) raise "Sorry! Shutting down..." if @shutting_down check_disk_for_updated_page pagename mutate( pagename ) { |page| page.rollback( number, author ) } end private def start_watching_files return unless $SETTINGS[:check_files_every] Thread.new do loop do sleep $SETTINGS[:check_files_every] load_all_pages end end.priority = -5 end def new_page( name, initializer = :new ) PAGE_CLASSES.each do |regex,klass| return klass.send( initializer, name) if name =~ regex end end def mutate( pagename ) didexist = exists? pagename page = page_named( pagename ) || new_page( pagename ) revision = nil page.content_lock.synchronize do revision, dont_save = yield page save page if revision && dont_save != :dont_save end if revision notify :page_revised, page, revision if page.deleted? notify :page_deleted, page, revision elsif !didexist notify :page_created, page, revision end end end def add_page_to_index( page ) @pages[ page.name.downcase ] = page page end def page_named( pagename ) @pages[ pagename.downcase ] end end