| 1 | # redMine - project management software
|
| 2 | # Copyright (C) 2006-2007 Jean-Philippe Lang
|
| 3 | #
|
| 4 | # This program is free software; you can redistribute it and/or
|
| 5 | # modify it under the terms of the GNU General Public License
|
| 6 | # as published by the Free Software Foundation; either version 2
|
| 7 | # of the License, or (at your option) any later version.
|
| 8 | #
|
| 9 | # This program is distributed in the hope that it will be useful,
|
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 12 | # GNU General Public License for more details.
|
| 13 | #
|
| 14 | # You should have received a copy of the GNU General Public License
|
| 15 | # along with this program; if not, write to the Free Software
|
| 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
| 17 |
|
| 18 | require 'active_record'
|
| 19 | require 'iconv'
|
| 20 | require 'pp'
|
| 21 |
|
| 22 | namespace :redmine do
|
| 23 | desc 'MediaWiki migration script'
|
| 24 | task :migrate_from_mediawiki => :environment do
|
| 25 |
|
| 26 | module MWMigrate
|
| 27 |
|
| 28 | class MWText < ActiveRecord::Base
|
| 29 | set_table_name :text
|
| 30 | set_primary_key :old_id
|
| 31 | end
|
| 32 |
|
| 33 | class MWRev < ActiveRecord::Base
|
| 34 | set_table_name :revision
|
| 35 | set_primary_key :rev_id
|
| 36 | belongs_to :page, :class_name => "MWPage", :foreign_key => :rev_page
|
| 37 | belongs_to :text, :class_name => "MWText", :foreign_key => :rev_text_id
|
| 38 | end
|
| 39 |
|
| 40 | class MWPage < ActiveRecord::Base
|
| 41 | set_table_name :page
|
| 42 | set_primary_key :page_id
|
| 43 | has_many :revisions, :class_name => "MWRev", :foreign_key => :rev_page, :order => "rev_timestamp DESC"
|
| 44 | end
|
| 45 |
|
| 46 | def self.find_or_create_user(email, project_member = false)
|
| 47 | u = User.find_by_mail(email)
|
| 48 | if !u
|
| 49 | u = User.find_by_mail(@@mw_default_user)
|
| 50 | end
|
| 51 | if(!u)
|
| 52 | # Create a new user if not found
|
| 53 | mail = email[0,limit_for(User, 'mail')]
|
| 54 | mail = "#{mail}@fortna.com" unless mail.include?("@")
|
| 55 | name = email[0,email.index("@")];
|
| 56 | u = User.new :firstname => name[0,limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
|
| 57 | :lastname => '-',
|
| 58 | :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-')
|
| 59 | u.login = email[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
|
| 60 | u.password = 'bugzilla'
|
| 61 | u.admin = false
|
| 62 | # finally, a default user is used if the new user is not valid
|
| 63 | puts "Created User: "+ u.to_yaml
|
| 64 | u = User.find(:first) unless u.save
|
| 65 | else
|
| 66 | # puts "Found User: " + u.to_yaml
|
| 67 | end
|
| 68 | # Make sure he is a member of the project
|
| 69 | ## if project_member && !u.member_of?(@target_project)
|
| 70 | ## role = ROLE_MAPPING['developer']
|
| 71 | ## Member.create(:user => u, :project => @target_project, :role => role)
|
| 72 | ## u.reload
|
| 73 | ## end
|
| 74 | u
|
| 75 | end
|
| 76 |
|
| 77 |
|
| 78 | # Basic wiki syntax conversion
|
| 79 | def self.convert_wiki_text(text)
|
| 80 | # Titles
|
| 81 | text = text.gsub(/^(\=+)\s*([^=]+)\s*\=+\s*$/) {|s| "\nh#{$1.length}. #{$2}\n"}
|
| 82 |
|
| 83 | # Internal links
|
| 84 | text = text.gsub(/\[\[(.*)\s+\|(.*)\]\]/) {|s| "[[#{$1}|#{$2}]]"}
|
| 85 |
|
| 86 | # External Links
|
| 87 | text = text.gsub(/\[(http[^\s]+)\s+([^\]]+)\]/) {|s| "\"#{$2}\":#{$1}"}
|
| 88 | text = text.gsub(/\[(http[^\s]+)\]/) {|s| "#{$1}"}
|
| 89 |
|
| 90 | # Highlighting
|
| 91 | text = text.gsub(/'''''([^\s])/, '_*\1')
|
| 92 | text = text.gsub(/([^\s])'''''/, '\1*_')
|
| 93 | text = text.gsub(/'''([^\s])/, '*\1')
|
| 94 | text = text.gsub(/([^\s])'''/, '\1*')
|
| 95 | text = text.gsub(/''([^\s])/, '_*\1')
|
| 96 | text = text.gsub(/([^\s])''/, '\1*_')
|
| 97 |
|
| 98 | # code
|
| 99 | text = text.gsub(/((^ [^\n]*\n)+)/m) { |s| "<pre>\n#{$1}</pre>\n" }
|
| 100 | # text = text.gsub(/(^\n^ .*?$)/m) { |s| "<pre><code>#{$1}" }
|
| 101 | # text = text.gsub(/(^ .*?\n)\n/m) { |s| "#{$1}</pre></code>\n" }
|
| 102 |
|
| 103 | # Tables
|
| 104 | # Half-assed attempt
|
| 105 | # First strip off the table formatting
|
| 106 | text = text.gsub(/^\![^\|]*/, '')
|
| 107 | text = text.gsub(/^\{\|[^\|]*$/, '{|')
|
| 108 |
|
| 109 | # Now congeal the rows
|
| 110 | while( text.gsub!(/(\|-.*)\n(\|\w.*)$/m, '\1\2'))
|
| 111 | end
|
| 112 |
|
| 113 | # Now congeal the headers
|
| 114 | while( text.gsub!(/(\{\|.*)\n(\|\w.*)$/m, '\1\2'))
|
| 115 | end
|
| 116 |
|
| 117 | # format the headers properly
|
| 118 | while( text.gsub!(/(\{\|.*)\|([^_].*)$/, '\1|_. \2'))
|
| 119 | end
|
| 120 |
|
| 121 | # get rid of leading '{|'
|
| 122 | text = text.gsub(/^\{\|(.*)$/) { |s| "table(stdtbl)\n#{$1}|" }
|
| 123 |
|
| 124 | # get rid of leading '|-'
|
| 125 | text = text.gsub(/^\|-(.*)$/, '\1|')
|
| 126 |
|
| 127 | # get rid of trailing '|}'
|
| 128 | text = text.gsub(/^\|\}.*$/, '')
|
| 129 |
|
| 130 | # Internal Links
|
| 131 | text = text.gsub(/\[\[Image:([^\s]+)\]\]/) { |s| "!#{$1}!" }
|
| 132 |
|
| 133 | # Wiki page separator ':'
|
| 134 | while( text.gsub!(/(\[\[\s*\w+):(\w+)/, '\1_\2') )
|
| 135 | end
|
| 136 |
|
| 137 | text
|
| 138 | end
|
| 139 |
|
| 140 | def self.migrate
|
| 141 | establish_connection
|
| 142 |
|
| 143 | # Quick database test
|
| 144 | pages = MWPage.count
|
| 145 |
|
| 146 | migrated_wiki_edits = 0
|
| 147 |
|
| 148 | puts "No wiki defined" unless @target_project.wiki
|
| 149 | wiki = @target_project.wiki ||
|
| 150 | Wiki.new(:project => @target_project,
|
| 151 | :start_page => @target_project.name)
|
| 152 |
|
| 153 |
|
| 154 | # Wiki
|
| 155 | puts "Migrating #{mw_page_title}, 1 of #{pages} pages"
|
| 156 | pages = MWPage.find(:all,
|
| 157 | :conditions => ["page_title = ?", mw_page_title])
|
| 158 |
|
| 159 | if((pages.size > 0) && (@@mw_whole_namespace == "y" || @@mw_whole_namespace == "Y"))
|
| 160 | pages = MWPage.find(:all,
|
| 161 | :conditions => ["page_namespace = ?", pages[0].page_namespace])
|
| 162 | end
|
| 163 |
|
| 164 | pages.each do |page|
|
| 165 | print "Translate #{page.page_title} (y/N)? "
|
| 166 | next unless STDIN.gets.match(/^[yY]$/i)
|
| 167 |
|
| 168 | STDOUT.flush
|
| 169 | new_title = page.page_title.gsub(/:/, "_")
|
| 170 | p = wiki.find_or_new_page(new_title)
|
| 171 | p.content = WikiContent.new(:page => p) if p.new_record?
|
| 172 | p.content.text = convert_wiki_text(page.revisions[0].text.old_text)
|
| 173 | p.content.author = User.find_by_mail(@@mw_default_user)
|
| 174 | p.content.comments = page.revisions[0].rev_comment
|
| 175 | puts "Record: " + p.content.to_s
|
| 176 | puts " Text: " + p.content.text
|
| 177 | print "Save translated page (y/N)? "
|
| 178 | next unless STDIN.gets.match(/^[yY]$/i)
|
| 179 |
|
| 180 | p.new_record? ? p.save : p.content.save
|
| 181 | migrated_wiki_edits += 1 unless p.content.new_record?
|
| 182 | end
|
| 183 |
|
| 184 | puts
|
| 185 | puts "Wiki edits: #{migrated_wiki_edits}/#{MWPage.count}"
|
| 186 | end
|
| 187 |
|
| 188 | def self.limit_for(klass, attribute)
|
| 189 | klass.columns_hash[attribute.to_s].limit
|
| 190 | end
|
| 191 |
|
| 192 | def self.encoding(charset)
|
| 193 | @ic = Iconv.new('UTF-8', charset)
|
| 194 | rescue Iconv::InvalidEncoding
|
| 195 | puts "Invalid encoding!"
|
| 196 | return false
|
| 197 | end
|
| 198 |
|
| 199 | def self.set_mw_directory(path)
|
| 200 | @@bz_directory = path
|
| 201 | raise "This directory doesn't exist!" unless File.directory?(path)
|
| 202 | @@bz_directory
|
| 203 | rescue Exception => e
|
| 204 | puts e
|
| 205 | return false
|
| 206 | end
|
| 207 |
|
| 208 | def self.mw_directory
|
| 209 | @@mw_directory
|
| 210 | end
|
| 211 |
|
| 212 | def self.set_mw_adapter(adapter)
|
| 213 | return false if adapter.blank?
|
| 214 | raise "Unknown adapter: #{adapter}!" unless %w(sqlite sqlite3 mysql postgresql).include?(adapter)
|
| 215 | # If adapter is sqlite or sqlite3, make sure that mw.db exists
|
| 216 | raise "#{mw_db_path} doesn't exist!" if %w(sqlite sqlite3).include?(adapter) && !File.exist?(mw_db_path)
|
| 217 | @@mw_adapter = adapter
|
| 218 | rescue Exception => e
|
| 219 | puts e
|
| 220 | return false
|
| 221 | end
|
| 222 |
|
| 223 | def self.set_mw_db_host(host)
|
| 224 | return nil if host.blank?
|
| 225 | @@mw_db_host = host
|
| 226 | end
|
| 227 |
|
| 228 | def self.set_mw_db_port(port)
|
| 229 | return nil if port.to_i == 0
|
| 230 | @@mw_db_port = port.to_i
|
| 231 | end
|
| 232 |
|
| 233 | def self.set_mw_db_socket(sock)
|
| 234 | @@mw_db_socket = sock
|
| 235 | end
|
| 236 |
|
| 237 | def self.set_mw_db_name(name)
|
| 238 | return nil if name.blank?
|
| 239 | @@mw_db_name = name
|
| 240 | end
|
| 241 |
|
| 242 | def self.set_mw_db_username(username)
|
| 243 | @@mw_db_username = username
|
| 244 | end
|
| 245 |
|
| 246 | def self.set_mw_db_password(password)
|
| 247 | @@mw_db_password = password
|
| 248 | end
|
| 249 |
|
| 250 | def self.set_mw_default_user(username)
|
| 251 | @@mw_default_user = username
|
| 252 | end
|
| 253 |
|
| 254 | def self.set_mw_page_title(name)
|
| 255 | @@mw_page_title = name
|
| 256 | end
|
| 257 |
|
| 258 | def self.set_mw_whole_namespace(flag)
|
| 259 | @@mw_whole_namespace = flag
|
| 260 | end
|
| 261 |
|
| 262 | mattr_reader :mw_directory, :mw_adapter, :mw_db_host, :mw_db_port, :mw_db_name, :mw_db_username, :mw_db_password, :mw_db_socket, :mw_page_title, :mw_whole_namespace
|
| 263 |
|
| 264 |
|
| 265 | def self.mw_db_path; "#{mw_directory}/db/wiki.db" end
|
| 266 | def self.mw_attachments_directory; "#{mw_directory}/attachments" end
|
| 267 |
|
| 268 | def self.target_project_identifier(identifier)
|
| 269 | project = Project.find_by_identifier(identifier)
|
| 270 | if !project
|
| 271 | # create the target project
|
| 272 | project = Project.new :name => identifier.humanize,
|
| 273 | :description => identifier.humanize
|
| 274 | project.identifier = identifier
|
| 275 | puts "Created Project: "+ project.to_s
|
| 276 | puts "Unable to create a project with identifier '#{identifier}'!" unless project.save
|
| 277 | # enable issues and wiki for the created project
|
| 278 | project.enabled_module_names = ['issue_tracking', 'wiki']
|
| 279 | project.trackers << TRACKER_BUG
|
| 280 | project.trackers << TRACKER_FEATURE
|
| 281 | project.trackers << TRACKER_SUPPORT
|
| 282 | else
|
| 283 | puts "Found Project: " + project.to_yaml
|
| 284 | end
|
| 285 | @target_project = project.new_record? ? nil : project
|
| 286 | end
|
| 287 |
|
| 288 |
|
| 289 | def self.connection_params
|
| 290 | if %w(sqlite sqlite3).include?(mw_adapter)
|
| 291 | {:adapter => mw_adapter,
|
| 292 | :database => mw_db_path}
|
| 293 | else
|
| 294 | {:adapter => mw_adapter,
|
| 295 | :database => mw_db_name,
|
| 296 | :host => mw_db_host,
|
| 297 | :port => mw_db_port,
|
| 298 | :socket => mw_db_socket,
|
| 299 | :username => mw_db_username,
|
| 300 | :password => mw_db_password}
|
| 301 | end
|
| 302 | end
|
| 303 |
|
| 304 | def self.establish_connection
|
| 305 | constants.each do |const|
|
| 306 | klass = const_get(const)
|
| 307 | next unless klass.respond_to? 'establish_connection'
|
| 308 | klass.establish_connection connection_params
|
| 309 | end
|
| 310 | end
|
| 311 |
|
| 312 | private
|
| 313 | def self.encode(text)
|
| 314 | @ic.iconv text
|
| 315 | rescue
|
| 316 | text
|
| 317 | end
|
| 318 | end
|
| 319 |
|
| 320 | puts
|
| 321 | puts "WARNING: a new project will be added to Redmine during this process."
|
| 322 | print "Are you sure you want to continue ? [y/N] "
|
| 323 | break unless STDIN.gets.match(/^[yY]$/i)
|
| 324 | puts
|
| 325 |
|
| 326 | def prompt(text, options = {}, &block)
|
| 327 | default = options[:default] || ''
|
| 328 | while true
|
| 329 | print "#{text} [#{default}]: "
|
| 330 | value = STDIN.gets.chomp!
|
| 331 | value = default if value.blank?
|
| 332 | break if yield value
|
| 333 | end
|
| 334 | end
|
| 335 |
|
| 336 | DEFAULT_PORTS = {'mysql' => 3306, 'postgresl' => 5432}
|
| 337 | DEFAULT_SOCKETS = {'mysql' => '/var/lib/mysql/mysql.sock'}
|
| 338 |
|
| 339 | prompt('MW directory',:default => '/var/www/html/mediawiki-1.8.2') {|directory| MWMigrate.set_mw_directory directory}
|
| 340 | prompt('MW database adapter (sqlite, sqlite3, mysql, postgresql)', :default => 'mysql') {|adapter| MWMigrate.set_mw_adapter adapter}
|
| 341 | unless %w(sqlite sqlite3).include?(MWMigrate.mw_adapter)
|
| 342 | prompt('MW database host', :default => 'localhost') {|host| MWMigrate.set_mw_db_host host}
|
| 343 | prompt('MW database port', :default => DEFAULT_PORTS[MWMigrate.mw_adapter]) {|port| MWMigrate.set_mw_db_port port}
|
| 344 | prompt('MW database socket', :default => DEFAULT_SOCKETS[MWMigrate.mw_adapter]) {|sock| MWMigrate.set_mw_db_socket sock}
|
| 345 | prompt('MW database name', :default => 'wikidb') {|name| MWMigrate.set_mw_db_name name}
|
| 346 | prompt('MW database username', :default => 'wiki') {|username| MWMigrate.set_mw_db_username username}
|
| 347 | prompt('MW database password', :default => 'wikidb') {|password| MWMigrate.set_mw_db_password password}
|
| 348 | end
|
| 349 | prompt('MW database encoding', :default => 'UTF-8') {|encoding| MWMigrate.encoding encoding}
|
| 350 | prompt('Target project identifier', :default => 'CommCore') {|identifier| MWMigrate.target_project_identifier identifier}
|
| 351 | prompt('MW Page Title', :default => 'CommCore:Devel:XMLDB') {|identifier| MWMigrate.set_mw_page_title identifier}
|
| 352 | prompt('Page Author', :default => 'carlnygard@fortna.com') {|identifier| MWMigrate.set_mw_default_user identifier}
|
| 353 | prompt('Whole namespace (Y/n)?', :default => 'y') {|flag| MWMigrate.set_mw_whole_namespace flag}
|
| 354 | puts
|
| 355 |
|
| 356 | MWMigrate.migrate
|
| 357 | end
|
| 358 | end
|