dradis-projects 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/config/routes.rb ADDED
@@ -0,0 +1,4 @@
1
+ Dradis::Plugins::Projects::Engine.routes.draw do
2
+ resource :package, only: [:show]
3
+ resource :template, only: [:show]
4
+ end
@@ -0,0 +1,30 @@
1
+ $:.push File.expand_path('../lib', __FILE__)
2
+
3
+ require 'dradis/plugins/projects/version'
4
+
5
+ # Describe your gem and declare its dependencies:
6
+ Gem::Specification.new do |spec|
7
+ spec.platform = Gem::Platform::RUBY
8
+ spec.name = 'dradis-projects'
9
+ spec.version = Dradis::Plugins::Projects::VERSION::STRING
10
+ spec.summary = 'Project export/upload for the Dradis Framework.'
11
+ spec.description = 'This plugin allows you to dump the contents of the repo into a zip archive and restore the state from one of them.'
12
+
13
+ spec.license = 'GPL-2'
14
+
15
+ spec.authors = ['Daniel Martin']
16
+ spec.email = ['etd@nomejortu.com']
17
+ spec.homepage = 'http://dradisframework.org'
18
+
19
+ spec.files = `git ls-files`.split($\)
20
+ spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.6'
24
+ spec.add_development_dependency 'combustion'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'rspec'
27
+
28
+ spec.add_dependency 'dradis-plugins', '~> 3.5'
29
+ spec.add_dependency 'rubyzip', '~> 1.1.0'
30
+ end
@@ -0,0 +1,5 @@
1
+ require 'zip'
2
+
3
+ require 'dradis-plugins'
4
+
5
+ require 'dradis/plugins/projects'
@@ -0,0 +1,15 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Projects
4
+ module Export; end
5
+ module Upload; end
6
+ end
7
+ end
8
+ end
9
+
10
+ require 'dradis/plugins/projects/engine'
11
+ require 'dradis/plugins/projects/export/package'
12
+ require 'dradis/plugins/projects/export/template'
13
+ require 'dradis/plugins/projects/upload/package'
14
+ require 'dradis/plugins/projects/upload/template'
15
+ require 'dradis/plugins/projects/version'
@@ -0,0 +1,31 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Projects
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Dradis::Plugins::Projects
6
+
7
+ include ::Dradis::Plugins::Base
8
+ description 'Save and restore project information'
9
+ provides :export, :upload
10
+
11
+ initializer 'dradis-projects.mount_engine' do
12
+ Rails.application.routes.append do
13
+ mount Dradis::Plugins::Projects::Engine => '/export/projects'
14
+ end
15
+ end
16
+
17
+ # Because this plugin provides two export modules, we have to overwrite
18
+ # the default .uploaders() method.
19
+ #
20
+ # See:
21
+ # Dradis::Plugins::Upload::Base in dradis-plugins
22
+ def self.uploaders
23
+ [
24
+ Dradis::Plugins::Projects::Upload::Package,
25
+ Dradis::Plugins::Projects::Upload::Template
26
+ ]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ module Dradis::Plugins::Projects::Export
2
+ class Package < Dradis::Plugins::Export::Base
3
+
4
+ # Create a new project export bundle. It will include an XML file with the
5
+ # contents of the repository (see db_only) and all the attachments that
6
+ # have been uploaded into the system.
7
+ def export(params={})
8
+ raise ":filename not provided" unless params.key?(:filename)
9
+
10
+ filename = params[:filename]
11
+ logger = params.fetch(:logger, Rails.logger)
12
+
13
+ File.delete(filename) if File.exists?(filename)
14
+
15
+ logger.debug{ "Creating a new Zip file in #{filename}..." }
16
+ Zip::File.open(filename, Zip::File::CREATE) do |zipfile|
17
+ Node.all.each do |node|
18
+ node_path = Attachment.pwd.join(node.id.to_s)
19
+
20
+ Dir["#{node_path}/**/**"].each do |file|
21
+ logger.debug{ "\tAdding attachment for '#{node.label}': #{file}" }
22
+ zipfile.add(file.sub("#{Attachment.pwd.to_s}/", ''), file)
23
+ end
24
+ end
25
+
26
+ logger.debug{ "\tAdding XML repository dump" }
27
+ template_exporter = Template.new(content_service: content_service)
28
+ template = template_exporter.export(params)
29
+ zipfile.get_output_stream('dradis-repository.xml') { |out|
30
+ out << template
31
+ }
32
+ end
33
+ logger.debug{ 'Done.' }
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,173 @@
1
+ module Dradis::Plugins::Projects::Export
2
+ class Template < Dradis::Plugins::Export::Base
3
+ # This method returns an XML representation of current repository which
4
+ # includes Categories, Nodes and Notes
5
+ def export(params={})
6
+ builder = Builder::XmlMarkup.new
7
+ builder.instruct!
8
+ result = builder.tag!('dradis-template') do |template_builder|
9
+ build_nodes(template_builder)
10
+ build_issues(template_builder)
11
+ build_methodologies(template_builder)
12
+ build_categories(template_builder)
13
+ build_tags(template_builder)
14
+ end
15
+ return result
16
+ end
17
+
18
+ private
19
+
20
+ def build_activities_for(builder, trackable)
21
+ builder.activities do |activities_builder|
22
+ trackable.activities.each do |activity|
23
+ activities_builder.activity do |activity_builder|
24
+ activity_builder.action(activity.action)
25
+ activity_builder.user_email(user_email_for_activity(activity))
26
+ activity_builder.created_at(activity.created_at.to_i)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def build_categories(builder)
33
+ categories = []
34
+ categories << Category.issue if @issues.any?
35
+ categories += @nodes.map do |node|
36
+ node.notes.map { |note| note.category }.uniq
37
+ end.flatten.uniq
38
+
39
+ builder.categories do |categories_builder|
40
+ categories.each do |category|
41
+ categories_builder.category do |category_builder|
42
+ category_builder.id(category.id)
43
+ category_builder.name(category.name)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def build_evidence_for_node(builder, node)
50
+ builder.evidence do |evidences_builder|
51
+ node.evidence.each do |evidence|
52
+ evidences_builder.evidence do |evidence_builder|
53
+ evidence_builder.id(evidence.id)
54
+ evidence_builder.author(evidence.author)
55
+ evidence_builder.tag!('issue-id', evidence.issue_id)
56
+ evidence_builder.content do
57
+ evidence_builder.cdata!(evidence.content)
58
+ end
59
+ build_activities_for(evidence_builder, evidence)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def build_issues(builder)
66
+ @issues = Issue.where(node_id: Node.issue_library).includes(:activities)
67
+
68
+ builder.issues do |issues_builder|
69
+ @issues.each do |issue|
70
+ issues_builder.issue do |issue_builder|
71
+ issue_builder.id(issue.id)
72
+ issue_builder.author(issue.author)
73
+ issue_builder.text do
74
+ issue_builder.cdata!(issue.text)
75
+ end
76
+ build_activities_for(issue_builder, issue)
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def build_methodologies(builder)
83
+ methodologies = Node.methodology_library.notes
84
+ builder.methodologies do |methodologies_builder|
85
+ methodologies.each do |methodology|
86
+ methodologies_builder.methodology do |methodology_builder|
87
+ methodology_builder.text do
88
+ methodology_builder.cdata!(methodology.text)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ def build_nodes(builder)
96
+ @nodes = Node.includes(:activities, :evidence, :notes, evidence: [:activities], notes: [:activities, :category]).all.reject do |node|
97
+ [Node::Types::METHODOLOGY,
98
+ Node::Types::ISSUELIB].include?(node.type_id)
99
+ end
100
+
101
+ builder.nodes do |nodes_builder|
102
+ @nodes.each do |node|
103
+ nodes_builder.node do |node_builder|
104
+ node_builder.id(node.id)
105
+ node_builder.label(node.label)
106
+ node_builder.tag!('parent-id', node.parent_id)
107
+ node_builder.position(node.position)
108
+ node_builder.properties do
109
+ node_builder.cdata!(node.raw_properties)
110
+ end
111
+ node_builder.tag!('type-id', node.type_id)
112
+ # Notes
113
+ build_notes_for_node(node_builder, node)
114
+ # Evidence
115
+ build_evidence_for_node(node_builder, node)
116
+ build_activities_for(node_builder, node)
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ def build_notes_for_node(builder, node)
123
+ builder.notes do |notes_builder|
124
+ node.notes.each do |note|
125
+ notes_builder.note do |note_builder|
126
+ note_builder.id(note.id)
127
+ note_builder.author(note.author)
128
+ note_builder.tag!('category-id', note.category_id)
129
+ note_builder.text do
130
+ note_builder.cdata!(note.text)
131
+ end
132
+ build_activities_for(note_builder, note)
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ def build_tags(builder)
139
+ tags = Tag.all
140
+ builder.tags do |tags_builder|
141
+ tags.each do |tag|
142
+ tags_builder.tag do |tag_builder|
143
+ tag_builder.id(tag.id)
144
+ tag_builder.name(tag.name)
145
+ tag_builder.taggings do |taggings_builder|
146
+ tag.taggings.each do |tagging|
147
+ taggings_builder.tagging do |tagging_builder|
148
+ tagging_builder.tag!('taggable-id', tagging.taggable_id)
149
+ tagging_builder.tag!('taggable-type', tagging.taggable_type)
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+
159
+ # Cache user emails so we don't have to make an extra SQL request
160
+ # for every activity
161
+ def user_email_for_activity(activity)
162
+ return activity.user if activity.user.is_a?(String)
163
+
164
+ @user_emails ||= begin
165
+ User.select([:id, :email]).all.each_with_object({}) do |user, hash|
166
+ hash[user.id] = user.email
167
+ end
168
+ end
169
+ @user_emails[activity.user_id]
170
+ end
171
+
172
+ end
173
+ end
@@ -0,0 +1,19 @@
1
+ module Dradis
2
+ module Plugins
3
+ module Projects
4
+ # Returns the version of the currently loaded Projects as a <tt>Gem::Version</tt>
5
+ def self.gem_version
6
+ Gem::Version.new VERSION::STRING
7
+ end
8
+
9
+ module VERSION
10
+ MAJOR = 3
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = nil
14
+
15
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,71 @@
1
+ module Dradis::Plugins::Projects::Upload
2
+ module Package
3
+ def self.meta
4
+ package = Dradis::Plugins::Projects
5
+ {
6
+ name: package::Engine::plugin_name,
7
+ description: 'Upload Project package file (.zip)',
8
+ version: package.version
9
+ }
10
+ end
11
+
12
+ # In this module you will find the implementation details that enable you to
13
+ # upload a project archive (generated using ProjectExport::Processor::full_project)
14
+ class Importer < Dradis::Plugins::Upload::Importer
15
+
16
+ def import(params={})
17
+ package = params[:file]
18
+ success = false
19
+
20
+ # Unpack the archive in a temporary location
21
+ FileUtils.mkdir Rails.root.join('tmp', 'zip')
22
+
23
+ begin
24
+ logger.info { 'Uncompressing the file' }
25
+ #TODO: this could be improved by only uncompressing the XML, then parsing
26
+ # it to get the node_lookup table and then uncompressing each entry to its
27
+ # final destination
28
+ Zip::File.foreach(package) do |entry|
29
+ path = Rails.root.join('tmp', 'zip', entry.name)
30
+ FileUtils.mkdir_p(File.dirname(path))
31
+ entry.extract(path)
32
+ logger.info { "\t#{entry.name}" }
33
+ end
34
+ logger.info { 'Done.' }
35
+
36
+ logger.info { 'Loading XML state file' }
37
+ importer = Template::Importer.new(
38
+ content_service: content_service,
39
+ logger: logger,
40
+ template_service: template_service
41
+ )
42
+ node_lookup = importer.import(
43
+ file: Rails.root.
44
+ join('tmp', 'zip', 'dradis-repository.xml')
45
+ )
46
+
47
+ logger.info { 'Moving attachments to their final destinations' }
48
+ node_lookup.each do |oldid,newid|
49
+ if File.directory? Rails.root.join('tmp', 'zip', oldid)
50
+ FileUtils.mkdir_p Attachment.pwd.join(newid.to_s)
51
+
52
+ Dir.glob(Rails.root.join('tmp', 'zip', oldid, '*')).each do |attachment|
53
+ FileUtils.mv(attachment, Attachment.pwd.join(newid.to_s))
54
+ end
55
+ end
56
+ end
57
+
58
+ success = true
59
+ rescue Exception => e
60
+ logger.error { e.message }
61
+ success = false
62
+ ensure
63
+ # clean up the temporary files
64
+ FileUtils.rm_rf(Rails.root.join('tmp', 'zip'))
65
+ end
66
+
67
+ return success
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,364 @@
1
+ module Dradis::Plugins::Projects::Upload
2
+ module Template
3
+ def self.meta
4
+ package = Dradis::Plugins::Projects
5
+ {
6
+ name: package::Engine::plugin_name,
7
+ description: 'Upload Project template file (.xml)',
8
+ version: package.version
9
+ }
10
+ end
11
+
12
+ class Importer < Dradis::Plugins::Upload::Importer
13
+
14
+ # The import method is invoked by the framework to process a template file
15
+ # that has just been uploaded using the 'Import from file...' dialog.
16
+ #
17
+ # This module will take the XMl export file created with the ProjectExport
18
+ # module and dump the contents into the current database.
19
+ #
20
+ # Since we cannot ensure that the original node and category IDs as specified
21
+ # in the XML are free in this database, we need to keep a few lookup tables
22
+ # to maintain the original structure of Nodes and the Notes pointing to the
23
+ # right nodes and categories.
24
+ #
25
+ # This method also returns the Node lookup table so callers can understand
26
+ # what changes to the original IDs have been applied. This is mainly for the
27
+ # benefit of the ProjectPackageUpload module that would use the translation
28
+ # table to re-associate the attachments in the project archive with the new
29
+ # node IDs in the current project.
30
+ def import(params={})
31
+
32
+ # load the template
33
+ logger.info { "Loading template file from: #{params[:file]}" }
34
+ template = Nokogiri::XML(File.read(params[:file]))
35
+ logger.info { "Done." }
36
+
37
+ unless template.errors.empty?
38
+ logger.error { "Invalid project template format." }
39
+ return false
40
+ end
41
+
42
+ # we need this to be able to convert from old category_id to the new
43
+ # category_id once the categories are added to the DB (the ID may have
44
+ # changed)
45
+ category_lookup = {}
46
+ # the same applies to Nodes (think parent_id)
47
+ node_lookup = {}
48
+ # and to issues
49
+ issue_lookup = {}
50
+
51
+ # evidence is parsed when nodes are parsed, but cannot be saved until issues
52
+ # have been created. Therefore, parse evidence into arrays until time for
53
+ # creation
54
+ evidence_array = []
55
+
56
+ # likewise we also need to hold on to the XML about evidence activities
57
+ # until after the evidence has been saved
58
+ evidence_activity_xml_array = []
59
+
60
+ # all children nodes, we will need to find the new ID of their parents
61
+ orphan_nodes = []
62
+
63
+ # if the note has an attachment screenshot (i.e. !.../nodes/i/attachments/...!)
64
+ # we will fix the URL to point to the new Node ID.
65
+ #
66
+ # WARNING: we need a lookup table because one note may be referencing a
67
+ # different (yet unprocessed) node's attachments.
68
+ attachment_notes = []
69
+
70
+ # go through the categories, keep a translation table between the old
71
+ # category id and the new ones so we know to which category we should
72
+ # assign our notes
73
+ template.xpath('dradis-template/categories/category').each do |xml_category|
74
+ old_id = xml_category.at_xpath('id').text.strip
75
+ name = xml_category.at_xpath('name').text.strip
76
+ category = nil
77
+
78
+ # Prevent creating duplicate categories
79
+ logger.info { "Looking for category: #{name}" }
80
+ category = Category.find_or_create_by!(name: name)
81
+ category_lookup[old_id] = category.id
82
+ end
83
+
84
+
85
+ # ------------------------------------------------------------------- Nodes
86
+ # Re generate the Node tree structure
87
+ template.xpath('dradis-template/nodes/node').each do |xml_node|
88
+ element = xml_node.at_xpath('type-id')
89
+ type_id = element.text.nil? ? nil : element.text.strip
90
+ label = xml_node.at_xpath('label').text.strip
91
+ element = xml_node.at_xpath('parent-id')
92
+ parent_id = element.text.nil? ? nil : element.text.strip
93
+
94
+ # Node positions
95
+ element = xml_node.at_xpath('position')
96
+ position = (element && !element.text.nil?) ? element.text.strip : nil
97
+
98
+ # Node properties
99
+ element = xml_node.at_xpath('properties')
100
+ properties = (element && !element.text.blank?) ? element.text.strip : nil
101
+
102
+ created_at = xml_node.at_xpath('created-at')
103
+ updated_at = xml_node.at_xpath('updated-at')
104
+
105
+ logger.info { "New node detected: #{label}, parent_id: #{parent_id}, type_id: #{type_id}" }
106
+
107
+ # There is one exception to the rule, the Configuration.uploadsNode node,
108
+ # it does not make sense to have more than one of this nodes, in any
109
+ # given tree
110
+ node = nil
111
+ note = nil
112
+ evidence = nil
113
+ if (label == Configuration.plugin_uploads_node)
114
+ node = Node.create_with(type_id: type_id, parent_id: parent_id).
115
+ find_or_create_by!(label: label)
116
+ else
117
+ node = Node.create!(
118
+ type_id: type_id,
119
+ label: label,
120
+ parent_id: parent_id,
121
+ position: position
122
+ )
123
+ end
124
+
125
+ if properties
126
+ node.raw_properties = properties
127
+ end
128
+
129
+ node.update_attribute(:created_at, created_at.text.strip) if created_at
130
+ node.update_attribute(:updated_at, updated_at.text.strip) if updated_at
131
+
132
+ return false unless validate_and_save(node)
133
+ return false unless create_activities(node, xml_node)
134
+
135
+ xml_node.xpath('notes/note').each do |xml_note|
136
+
137
+ if xml_note.at_xpath('author') != nil
138
+ old_id = xml_note.at_xpath('category-id').text.strip
139
+ new_id = category_lookup[old_id]
140
+
141
+ created_at = xml_note.at_xpath('created-at')
142
+ updated_at = xml_note.at_xpath('updated-at')
143
+
144
+ logger.info { "Note category rewrite, used to be #{old_id}, now is #{new_id}" }
145
+ note = Note.create!(
146
+ author: xml_note.at_xpath('author').text.strip,
147
+ node_id: node.id,
148
+ category_id: new_id,
149
+ text: xml_note.at_xpath('text').text
150
+ )
151
+
152
+ note.update_attribute(:created_at, created_at.text.strip) if created_at
153
+ note.update_attribute(:updated_at, updated_at.text.strip) if updated_at
154
+
155
+ return false unless validate_and_save(note)
156
+
157
+ if note.text =~ %r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}
158
+ attachment_notes << note
159
+ end
160
+
161
+ return false unless create_activities(note, xml_note)
162
+
163
+ logger.info { "\tNew note added detected." }
164
+ end
165
+ end
166
+
167
+ # Create array of evidence from xml input. Cannot store in DB until we
168
+ # have a new issue id
169
+ xml_node.xpath('evidence/evidence').each do |xml_evidence|
170
+ if xml_evidence.at_xpath('author') != nil
171
+ created_at = xml_evidence.at_xpath('created-at')
172
+ updated_at = xml_evidence.at_xpath('updated-at')
173
+
174
+ evidence = Evidence.new(
175
+ author: xml_evidence.at_xpath('author').text.strip,
176
+ node_id: node.id,
177
+ content: xml_evidence.at_xpath('content').text,
178
+ issue_id: xml_evidence.at_xpath('issue-id').text.strip
179
+ )
180
+
181
+ evidence.update_attribute(:created_at, created_at.text.strip) if created_at
182
+ evidence.update_attribute(:updated_at, updated_at.text.strip) if updated_at
183
+ evidence_array << evidence
184
+
185
+ evidence_activity_xml_array << xml_evidence.xpath("activities/activity")
186
+
187
+ logger.info { "\tNew evidence added." }
188
+ end
189
+ end
190
+
191
+ # keep track of reassigned ids
192
+ node_lookup[xml_node.at_xpath('id').text.strip] = node.id
193
+
194
+ if node.parent_id != nil
195
+ # keep track of orphaned nodes
196
+ orphan_nodes << node
197
+ end
198
+ end
199
+
200
+
201
+ # ------------------------------------------------------------------- Issues
202
+ issue = nil
203
+ issue_category = Category.issue
204
+ issue_library = Node.issue_library
205
+ # go through the issues, keep a translation table between the old
206
+ # issue id and the new ones. This is important for importing evidence
207
+ # Will need to adjust node ID after generating node structure
208
+ template.xpath('dradis-template/issues/issue').each do |xml_issue|
209
+ old_id = xml_issue.at_xpath('id').text.strip
210
+
211
+ # TODO: Need to find some way of checking for dups
212
+ # May be combination of text, category_id and created_at
213
+ issue = Issue.new
214
+ issue.author = xml_issue.at_xpath('author').text.strip
215
+ issue.text = xml_issue.at_xpath('text').text
216
+ issue.node = issue_library
217
+ issue.category = issue_category
218
+
219
+ return false unless validate_and_save(issue)
220
+
221
+ return false unless create_activities(issue, xml_issue)
222
+
223
+ if issue.text =~ %r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}
224
+ attachment_notes << issue
225
+ end
226
+
227
+ issue_lookup[old_id] = issue.id
228
+ logger.info{ "New issue detected: #{issue.title}" }
229
+ end
230
+
231
+ # ----------------------------------------------------------- Methodologies
232
+ methodology_category = Category.default
233
+ methodology_library = Node.methodology_library
234
+ template.xpath('dradis-template/methodologies/methodology').each do |xml_methodology|
235
+ # FIXME: this is wrong in a few levels, we should be able to save a
236
+ # Methodology instance calling .save() but the current implementation
237
+ # of the model would consider this a 'methodology template' and not an
238
+ # instance.
239
+ #
240
+ # Also, methodology notes don't have a valid author, see
241
+ # MethodologiesController#create action (i.e. 'methodology builder' is
242
+ # used).
243
+ Note.create!(
244
+ author: 'methodology importer',
245
+ node_id: methodology_library.id,
246
+ category_id: methodology_category.id,
247
+ text: xml_methodology.at_xpath('text').text
248
+ )
249
+ end
250
+
251
+ # -------------------------------------------------------------------- Tags
252
+ template.xpath('dradis-template/tags/tag').each do |xml_tag|
253
+ name = xml_tag.at_xpath('name').text()
254
+ tag = Tag.find_or_create_by!(name: name)
255
+ @logger.info { "New tag detected: #{name}" }
256
+
257
+ xml_tag.xpath('./taggings/tagging').each do |xml_tagging|
258
+ old_taggable_id = xml_tagging.at_xpath('taggable-id').text()
259
+ taggable_type = xml_tagging.at_xpath('taggable-type').text()
260
+
261
+ new_taggable_id = case taggable_type
262
+ when 'Note'
263
+ issue_lookup[old_taggable_id]
264
+ end
265
+
266
+ Tagging.create! tag: tag, taggable_id: new_taggable_id, taggable_type: taggable_type
267
+ end
268
+ end
269
+
270
+ # ----------------------------------------------------------------- Wrap up
271
+
272
+ logger.info { "Wrapping up..." }
273
+
274
+ # Save the Evidence instance to the DB now that we have populated Issues
275
+ # the original issues
276
+ evidence_array.each_with_index do |evidence, i|
277
+ logger.info { "Setting issue_id for evidence" }
278
+ evidence.issue_id = issue_lookup[evidence.issue_id.to_s]
279
+
280
+ new_content = evidence.content.gsub(%r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}) do |_|
281
+ "!%s/nodes/%d/attachments/%s!" % [$1, node_lookup[$2], $3]
282
+ end
283
+ evidence.content = new_content
284
+
285
+ return false unless validate_and_save(evidence)
286
+
287
+ evidence_activity_xml_array[i].each do |xml_activity|
288
+ return false unless create_activity(evidence, xml_activity)
289
+ end
290
+ end
291
+
292
+ # Fix relationships between nodes to ensure parents and childrens match
293
+ # with the new assigned :ids
294
+ orphan_nodes.each do |node|
295
+ logger.info { "Finding parent for orphaned node: #{node.label}. Former parent was #{node.parent_id}" }
296
+ node.parent_id = node_lookup[node.parent_id.to_s]
297
+ return false unless validate_and_save(node)
298
+ end
299
+
300
+ # Adjust attachment URLs for new Node IDs
301
+ attachment_notes.each do |note|
302
+ @logger.info{ "Adjusting screenshot URLs: Note ##{note.id}" }
303
+ new_text = note.text.gsub(%r{^!(.*)/nodes/(\d+)/attachments/(.+)!$}) do |_|
304
+ "!%s/nodes/%d/attachments/%s!" % [$1, node_lookup[$2], $3]
305
+ end
306
+ note.text = new_text
307
+ return false unless validate_and_save(note)
308
+ end
309
+
310
+ return node_lookup
311
+ end
312
+
313
+ private
314
+
315
+ def create_activities(trackable, xml_trackable)
316
+ xml_trackable.xpath('activities/activity').each do |xml_activity|
317
+ # if 'validate_and_save(activity)' returns false, it needs
318
+ # to bubble up to the 'import' method so we can stop execution
319
+ return false unless create_activity(trackable, xml_activity)
320
+ end
321
+ end
322
+
323
+ def create_activity(trackable, xml_activity)
324
+ activity = trackable.activities.new(
325
+ action: xml_activity.at_xpath("action").text,
326
+ created_at: Time.at(xml_activity.at_xpath("created_at").text.to_i)
327
+ )
328
+
329
+ set_activity_user(activity, xml_activity.at_xpath("user_email").text)
330
+
331
+ validate_and_save(activity)
332
+ end
333
+
334
+ def set_activity_user(activity, email)
335
+ if Activity.column_names.include?('user')
336
+ activity.user = email
337
+ else
338
+ activity.user_id = user_id_for_email(email)
339
+ end
340
+ end
341
+
342
+ # Cache users to cut down on excess SQL requests
343
+ def user_id_for_email(email)
344
+ return -1 if email.blank?
345
+ @users ||= begin
346
+ User.select([:id, :email]).all.each_with_object({}) do |user, hash|
347
+ hash[user.email] = user.id
348
+ end
349
+ end
350
+ @users[email] || -1
351
+ end
352
+
353
+ def validate_and_save(instance)
354
+ if instance.save
355
+ return true
356
+ else
357
+ @logger.info{ "Malformed #{ instance.class.name } detected: #{ instance.errors.full_messages }" }
358
+ return false
359
+ end
360
+ end
361
+
362
+ end
363
+ end
364
+ end