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
|