Skip to content
Snippets Groups Projects
Commit fcc13c56 authored by Guilherme Gazzo's avatar Guilherme Gazzo
Browse files

removed files

parent 181a20b4
No related branches found
No related tags found
No related merge requests found
Showing
with 19 additions and 611 deletions
Template.adminImport.helpers
isAdmin: ->
return RocketChat.authz.hasRole(Meteor.userId(), 'admin')
isImporters: ->
return Object.keys(Importer.Importers).length > 0
getDescription: (importer) ->
return TAPi18n.__('Importer_From_Description', { from: importer.name })
importers: ->
importers = []
_.each Importer.Importers, (importer, key) ->
importer.key = key
importers.push importer
return importers
Template.adminImport.events
'click .start-import': (event) ->
importer = @
Meteor.call 'setupImporter', importer.key, (error, data) ->
if error
console.log t('importer_setup_error'), importer.key, error
return handleError(error)
else
FlowRouter.go '/admin/import/prepare/' + importer.key
import toastr from 'toastr'
Template.adminImportPrepare.helpers
isAdmin: ->
return RocketChat.authz.hasRole(Meteor.userId(), 'admin')
importer: ->
importerKey = FlowRouter.getParam('importer')
importer = undefined
_.each Importer.Importers, (i, key) ->
i.key = key
if key == importerKey
importer = i
return importer
isLoaded: ->
return Template.instance().loaded.get()
isPreparing: ->
return Template.instance().preparing.get()
users: ->
return Template.instance().users.get()
channels: ->
return Template.instance().channels.get()
Template.adminImportPrepare.events
'change .import-file-input': (event, template) ->
importer = @
return if not importer.key
e = event.originalEvent or event
files = e.target.files
if not files or files.length is 0
files = e.dataTransfer?.files or []
for blob in files
template.preparing.set true
reader = new FileReader()
reader.readAsDataURL(blob)
reader.onloadend = ->
Meteor.call 'prepareImport', importer.key, reader.result, blob.type, blob.name, (error, data) ->
if error
toastr.error t('Invalid_Import_File_Type')
template.preparing.set false
return
if !data
console.warn 'The importer ' + importer.key + ' is not set up correctly, as it did not return any data.'
toastr.error t('Importer_not_setup')
template.preparing.set false
return
if data.step
console.warn 'Invalid file, contains `data.step`.', data
toastr.error t('Invalid_Export_File', importer.key)
template.preparing.set false
return
template.users.set data.users
template.channels.set data.channels
template.loaded.set true
template.preparing.set false
'click .button.start': (event, template) ->
btn = this
$(btn).prop "disabled", true
importer = @
for user in template.users.get()
user.do_import = $("[name=#{user.user_id}]").is(':checked')
for channel in template.channels.get()
channel.do_import = $("[name=#{channel.channel_id}]").is(':checked')
Meteor.call 'startImport', FlowRouter.getParam('importer'), { users: template.users.get(), channels: template.channels.get() }, (error, data) ->
if error
console.warn 'Error on starting the import:', error
return handleError(error)
else
FlowRouter.go '/admin/import/progress/' + FlowRouter.getParam('importer')
'click .button.restart': (event, template) ->
Meteor.call 'restartImport', FlowRouter.getParam('importer'), (error, data) ->
if error
console.warn 'Error while restarting the import:', error
handleError(error)
return
template.users.set []
template.channels.set []
template.loaded.set false
'click .button.uncheck-deleted-users': (event, template) ->
for user in template.users.get() when user.is_deleted
$("[name=#{user.user_id}]").attr('checked', false);
'click .button.uncheck-archived-channels': (event, template) ->
for channel in template.channels.get() when channel.is_archived
$("[name=#{channel.channel_id}]").attr('checked', false);
Template.adminImportPrepare.onCreated ->
instance = @
@preparing = new ReactiveVar true
@loaded = new ReactiveVar false
@users = new ReactiveVar []
@channels = new ReactiveVar []
loadSelection = (progress) ->
if progress?.step
switch progress.step
#When the import is running, take the user to the progress page
when 'importer_importing_started', 'importer_importing_users', 'importer_importing_channels', 'importer_importing_messages', 'importer_finishing'
FlowRouter.go '/admin/import/progress/' + FlowRouter.getParam('importer')
# when the import is done, restart it (new instance)
when 'importer_user_selection'
Meteor.call 'getSelectionData', FlowRouter.getParam('importer'), (error, data) ->
if error
handleError error
instance.users.set data.users
instance.channels.set data.channels
instance.loaded.set true
instance.preparing.set false
when 'importer_new'
instance.preparing.set false
else
Meteor.call 'restartImport', FlowRouter.getParam('importer'), (error, progress) ->
if error
handleError(error)
loadSelection(progress)
else
console.warn 'Invalid progress information.', progress
# Load the initial progress to determine what we need to do
if FlowRouter.getParam('importer')
Meteor.call 'getImportProgress', FlowRouter.getParam('importer'), (error, progress) ->
if error
console.warn 'Error while getting the import progress:', error
handleError error
return
# if the progress isnt defined, that means there currently isn't an instance
# of the importer, so we need to create it
if progress is undefined
Meteor.call 'setupImporter', FlowRouter.getParam('importer'), (err, data) ->
if err
handleError(err)
instance.preparing.set false
loadSelection(data)
else
# Otherwise, we might need to do something based upon the current step
# of the import
loadSelection(progress)
else
FlowRouter.go '/admin/import'
import toastr from 'toastr'
Template.adminImportProgress.helpers
step: ->
return Template.instance().step.get()
completed: ->
return Template.instance().completed.get()
total: ->
return Template.instance().total.get()
Template.adminImportProgress.onCreated ->
instance = @
@step = new ReactiveVar t('Loading...')
@completed = new ReactiveVar 0
@total = new ReactiveVar 0
@updateProgress = ->
if FlowRouter.getParam('importer') isnt ''
Meteor.call 'getImportProgress', FlowRouter.getParam('importer'), (error, progress) ->
if error
console.warn 'Error on getting the import progress:', error
handleError error
return
if progress
if progress.step is 'importer_done'
toastr.success t(progress.step[0].toUpperCase() + progress.step.slice(1))
FlowRouter.go '/admin/import'
else if progress.step is 'importer_import_failed'
toastr.error t(progress.step[0].toUpperCase() + progress.step.slice(1))
FlowRouter.go '/admin/import/prepare/' + FlowRouter.getParam('importer')
else
instance.step.set t(progress.step[0].toUpperCase() + progress.step.slice(1))
instance.completed.set progress.count.completed
instance.total.set progress.count.total
setTimeout(() ->
instance.updateProgress()
, 100)
else
toastr.warning t('Importer_not_in_progress')
FlowRouter.go '/admin/import/prepare/' + FlowRouter.getParam('importer')
instance.updateProgress()
Importer = {}
Importer.Importers = {}
Importer.addImporter = (name, importer, options) ->
if not Importer.Importers[name]?
Importer.Importers[name] =
name: options.name
importer: importer
mimeType: options.mimeType
warnings: options.warnings
...@@ -9,7 +9,6 @@ Package.onUse(function(api) { ...@@ -9,7 +9,6 @@ Package.onUse(function(api) {
api.use([ api.use([
'ecmascript', 'ecmascript',
'templating', 'templating',
'coffeescript',
'check', 'check',
'rocketchat:lib' 'rocketchat:lib'
]); ]);
...@@ -18,37 +17,37 @@ Package.onUse(function(api) { ...@@ -18,37 +17,37 @@ Package.onUse(function(api) {
api.use('templating', 'client'); api.use('templating', 'client');
//Import Framework //Import Framework
api.addFiles('lib/_importer.coffee'); api.addFiles('lib/_importer.js');
api.addFiles('lib/importTool.coffee'); api.addFiles('lib/importTool.js');
api.addFiles('server/classes/ImporterBase.coffee', 'server'); api.addFiles('server/classes/ImporterBase.js', 'server');
api.addFiles('server/classes/ImporterProgress.coffee', 'server'); api.addFiles('server/classes/ImporterProgress.js', 'server');
api.addFiles('server/classes/ImporterProgressStep.coffee', 'server'); api.addFiles('server/classes/ImporterProgressStep.js', 'server');
api.addFiles('server/classes/ImporterSelection.coffee', 'server'); api.addFiles('server/classes/ImporterSelection.js', 'server');
api.addFiles('server/classes/ImporterSelectionChannel.coffee', 'server'); api.addFiles('server/classes/ImporterSelectionChannel.js', 'server');
api.addFiles('server/classes/ImporterSelectionUser.coffee', 'server'); api.addFiles('server/classes/ImporterSelectionUser.js', 'server');
//Database models //Database models
api.addFiles('server/models/Imports.coffee', 'server'); api.addFiles('server/models/Imports.js', 'server');
api.addFiles('server/models/RawImports.coffee', 'server'); api.addFiles('server/models/RawImports.js', 'server');
//Server methods //Server methods
api.addFiles('server/methods/getImportProgress.coffee', 'server'); api.addFiles('server/methods/getImportProgress.js', 'server');
api.addFiles('server/methods/getSelectionData.coffee', 'server'); api.addFiles('server/methods/getSelectionData.js', 'server');
api.addFiles('server/methods/prepareImport.js', 'server'); api.addFiles('server/methods/prepareImport.js', 'server');
api.addFiles('server/methods/restartImport.coffee', 'server'); api.addFiles('server/methods/restartImport.js', 'server');
api.addFiles('server/methods/setupImporter.coffee', 'server'); api.addFiles('server/methods/setupImporter.js', 'server');
api.addFiles('server/methods/startImport.coffee', 'server'); api.addFiles('server/methods/startImport.js', 'server');
//Client //Client
api.addFiles('client/admin/adminImport.html', 'client'); api.addFiles('client/admin/adminImport.html', 'client');
api.addFiles('client/admin/adminImport.coffee', 'client'); api.addFiles('client/admin/adminImport.js', 'client');
api.addFiles('client/admin/adminImportPrepare.html', 'client'); api.addFiles('client/admin/adminImportPrepare.html', 'client');
api.addFiles('client/admin/adminImportPrepare.coffee', 'client'); api.addFiles('client/admin/adminImportPrepare.js', 'client');
api.addFiles('client/admin/adminImportProgress.html', 'client'); api.addFiles('client/admin/adminImportProgress.html', 'client');
api.addFiles('client/admin/adminImportProgress.coffee', 'client'); api.addFiles('client/admin/adminImportProgress.js', 'client');
//Imports database records cleanup, mark all as not valid. //Imports database records cleanup, mark all as not valid.
api.addFiles('server/startup/setImportsToInvalid.coffee', 'server'); api.addFiles('server/startup/setImportsToInvalid.js', 'server');
api.export('Importer'); api.export('Importer');
}); });
......
# Base class for all Importers.
#
# @example How to subclass an importer
# class ExampleImporter extends RocketChat.importTool._baseImporter
# constructor: ->
# super('Name of Importer', 'Description of the importer, use i18n string.', new RegExp('application\/.*?zip'))
# prepare: (uploadedFileData, uploadedFileContentType, uploadedFileName) =>
# super
# startImport: (selectedUsersAndChannels) =>
# super
# getProgress: =>
# #return the progress report, tbd what is expected
# @version 1.0.0
Importer.Base = class Importer.Base
@MaxBSONSize = 8000000
@http = Npm.require 'http'
@https = Npm.require 'https'
@getBSONSize: (object) ->
# The max BSON object size we can store in MongoDB is 16777216 bytes
# but for some reason the mongo instanace which comes with meteor
# errors out for anything close to that size. So, we are rounding it
# down to 8000000 bytes.
BSON = require('bson').native().BSON
bson = new BSON()
bson.calculateObjectSize object
@getBSONSafeArraysFromAnArray: (theArray) ->
BSONSize = Importer.Base.getBSONSize theArray
maxSize = Math.floor(theArray.length / (Math.ceil(BSONSize / Importer.Base.MaxBSONSize)))
safeArrays = []
i = 0
while i < theArray.length
safeArrays.push(theArray.slice(i, i += maxSize))
return safeArrays
# Constructs a new importer, adding an empty collection, AdmZip property, and empty users & channels
#
# @param [String] name the name of the Importer
# @param [String] description the i18n string which describes the importer
# @param [String] mimeType the of the expected file type
#
constructor: (@name, @description, @mimeType) ->
@logger = new Logger("#{@name} Importer", {});
@progress = new Importer.Progress @name
@collection = Importer.RawImports
@AdmZip = Npm.require 'adm-zip'
@getFileType = Npm.require 'file-type'
importId = Importer.Imports.insert { 'type': @name, 'ts': Date.now(), 'status': @progress.step, 'valid': true, 'user': Meteor.user()._id }
@importRecord = Importer.Imports.findOne importId
@users = {}
@channels = {}
@messages = {}
# Takes the uploaded file and extracts the users, channels, and messages from it.
#
# @param [String] dataURI a base64 string of the uploaded file
# @param [String] sentContentType the file type
# @param [String] fileName the name of the uploaded file
#
# @return [Importer.Selection] Contains two properties which are arrays of objects, `channels` and `users`.
#
prepare: (dataURI, sentContentType, fileName) =>
fileType = @getFileType(new Buffer(dataURI.split(',')[1], 'base64'))
@logger.debug 'Uploaded file information is:', fileType
@logger.debug 'Expected file type is:', @mimeType
if not fileType or fileType.mime isnt @mimeType
@logger.warn "Invalid file uploaded for the #{@name} importer."
throw new Meteor.Error('error-invalid-file-uploaded', "Invalid file uploaded to import #{@name} data from.", { step: 'prepare' })
@updateProgress Importer.ProgressStep.PREPARING_STARTED
@updateRecord { 'file': fileName }
# Starts the import process. The implementing method should defer as soon as the selection is set, so the user who started the process
# doesn't end up with a "locked" ui while meteor waits for a response. The returned object should be the progress.
#
# @param [Importer.Selection] selectedUsersAndChannels an object with `channels` and `users` which contains information about which users and channels to import
#
# @return [Importer.Progress] the progress of the import
#
startImport: (importSelection) =>
if importSelection is undefined
throw new Error "No selected users and channel data provided to the #{@name} importer." #TODO: Make translatable
else if importSelection.users is undefined
throw new Error "Users in the selected data wasn't found, it must but at least an empty array for the #{@name} importer." #TODO: Make translatable
else if importSelection.channels is undefined
throw new Error "Channels in the selected data wasn't found, it must but at least an empty array for the #{@name} importer." #TODO: Make translatable
@updateProgress Importer.ProgressStep.IMPORTING_STARTED
# Gets the Importer.Selection object for the import.
#
# @return [Importer.Selection] the users and channels selection
getSelection: () =>
throw new Error "Invalid 'getSelection' called on #{@name}, it must be overridden and super can not be called."
# Gets the progress of this importer.
#
# @return [Importer.Progress] the progress of the import
#
getProgress: =>
return @progress
# Updates the progress step of this importer.
#
# @return [Importer.Progress] the progress of the import
#
updateProgress: (step) =>
@progress.step = step
@logger.debug "#{@name} is now at #{step}."
@updateRecord { 'status': @progress.step }
return @progress
# Adds the passed in value to the total amount of items needed to complete.
#
# @return [Importer.Progress] the progress of the import
#
addCountToTotal: (count) =>
@progress.count.total = @progress.count.total + count
@updateRecord { 'count.total': @progress.count.total }
return @progress
# Adds the passed in value to the total amount of items completed.
#
# @return [Importer.Progress] the progress of the import
#
addCountCompleted: (count) =>
@progress.count.completed = @progress.count.completed + count
#Only update the database every 500 records
#Or the completed is greater than or equal to the total amount
if (@progress.count.completed % 500 == 0) or @progress.count.completed >= @progress.count.total
@updateRecord { 'count.completed': @progress.count.completed }
return @progress
# Updates the import record with the given fields being `set`
#
# @return [Importer.Imports] the import record object
#
updateRecord: (fields) =>
Importer.Imports.update { _id: @importRecord._id }, { $set: fields }
@importRecord = Importer.Imports.findOne @importRecord._id
return @importRecord
# Uploads the file to the storage.
#
# @param [Object] details an object with details about the upload. name, size, type, and rid
# @param [String] fileUrl url of the file to download/import
# @param [Object] user the Rocket.Chat user
# @param [Object] room the Rocket.Chat room
# @param [Date] timeStamp the timestamp the file was uploaded
#
uploadFile: (details, fileUrl, user, room, timeStamp) =>
@logger.debug "Uploading the file #{details.name} from #{fileUrl}."
requestModule = if /https/i.test(fileUrl) then Importer.Base.https else Importer.Base.http
requestModule.get fileUrl, Meteor.bindEnvironment((stream) ->
fileId = Meteor.fileStore.create details
if fileId
Meteor.fileStore.write stream, fileId, (err, file) ->
if err
throw new Error(err)
else
url = file.url.replace(Meteor.absoluteUrl(), '/')
attachment =
title: "File Uploaded: #{file.name}"
title_link: url
if /^image\/.+/.test file.type
attachment.image_url = url
attachment.image_type = file.type
attachment.image_size = file.size
attachment.image_dimensions = file.identify?.size
if /^audio\/.+/.test file.type
attachment.audio_url = url
attachment.audio_type = file.type
attachment.audio_size = file.size
if /^video\/.+/.test file.type
attachment.video_url = url
attachment.video_type = file.type
attachment.video_size = file.size
msg =
rid: details.rid
ts: timeStamp
msg: ''
file:
_id: file._id
groupable: false
attachments: [attachment]
if details.message_id? and (typeof details.message_id is 'string')
msg['_id'] = details.message_id
RocketChat.sendMessage user, msg, room, true
else
@logger.error "Failed to create the store for #{fileUrl}!!!"
)
# Class for all the progress of the importers to use.
Importer.Progress = class Importer.Progress
# Constructs a new progress object.
#
# @param [String] name the name of the Importer
#
constructor: (@name) ->
@step = Importer.ProgressStep.NEW
@count = { completed: 0, total: 0 }
# "ENUM" of the import step, the value is the translation string
Importer.ProgressStep = Object.freeze
NEW: 'importer_new'
PREPARING_STARTED: 'importer_preparing_started'
PREPARING_USERS: 'importer_preparing_users'
PREPARING_CHANNELS: 'importer_preparing_channels'
PREPARING_MESSAGES: 'importer_preparing_messages'
USER_SELECTION: 'importer_user_selection'
IMPORTING_STARTED: 'importer_importing_started'
IMPORTING_USERS: 'importer_importing_users'
IMPORTING_CHANNELS: 'importer_importing_channels'
IMPORTING_MESSAGES: 'importer_importing_messages'
FINISHING: 'importer_finishing'
DONE: 'importer_done'
ERROR: 'importer_import_failed'
CANCELLED: 'importer_import_cancelled'
# Class for all the selection of users and channels for the importers
Importer.Selection = class Importer.Selection
# Constructs a new importer selection object.
#
# @param [String] name the name of the Importer
# @param [Array<Importer.User>] users the array of users
# @param [Array<Importer.Channel>] channels the array of channels
#
constructor: (@name, @users, @channels) ->
# Class for the selection channels for ImporterSelection
Importer.SelectionChannel = class Importer.SelectionChannel
# Constructs a new selection channel.
#
# @param [String] channel_id the unique identifier of the channel
# @param [String] name the name of the channel
# @param [Boolean] is_archived whether the channel was archived or not
# @param [Boolean] do_import whether we will be importing the channel or not
# @param [Boolean] is_private whether the channel is private or public
#
constructor: (@channel_id, @name, @is_archived, @do_import, @is_private) ->
#TODO: Add some verification?
# Class for the selection users for ImporterSelection
Importer.SelectionUser = class Importer.SelectionUser
# Constructs a new selection user.
#
# @param [String] user_id the unique user identifier
# @param [String] username the user's username
# @param [String] email the user's email
# @param [Boolean] is_deleted whether the user was deleted or not
# @param [Boolean] is_bot whether the user is a bot or not
# @param [Boolean] do_import whether we are going to import this user or not
#
constructor: (@user_id, @username, @email, @is_deleted, @is_bot, @do_import) ->
#TODO: Add some verification?
Meteor.methods
getImportProgress: (name) ->
if not Meteor.userId()
throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'getImportProgress' }
if not RocketChat.authz.hasPermission(Meteor.userId(), 'run-import')
throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'});
if Importer.Importers[name]?
return Importer.Importers[name].importerInstance?.getProgress()
else
throw new Meteor.Error 'error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'getImportProgress' }
Meteor.methods
getSelectionData: (name) ->
if not Meteor.userId()
throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'getSelectionData' }
if not RocketChat.authz.hasPermission(Meteor.userId(), 'run-import')
throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'});
if Importer.Importers[name]?.importerInstance?
progress = Importer.Importers[name].importerInstance.getProgress()
switch progress.step
when Importer.ProgressStep.USER_SELECTION
return Importer.Importers[name].importerInstance.getSelection()
else
return false
else
throw new Meteor.Error 'error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'getSelectionData' }
Meteor.methods
restartImport: (name) ->
if not Meteor.userId()
throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'restartImport' }
if not RocketChat.authz.hasPermission(Meteor.userId(), 'run-import')
throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'});
if Importer.Importers[name]?
importer = Importer.Importers[name]
importer.importerInstance.updateProgress Importer.ProgressStep.CANCELLED
importer.importerInstance.updateRecord { valid: false }
importer.importerInstance = undefined
importer.importerInstance = new importer.importer importer.name, importer.description, importer.mimeType
return importer.importerInstance.getProgress()
else
throw new Meteor.Error 'error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'restartImport' }
Meteor.methods
setupImporter: (name) ->
if not Meteor.userId()
throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'setupImporter' }
if not RocketChat.authz.hasPermission(Meteor.userId(), 'run-import')
throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'});
if Importer.Importers[name]?.importer?
importer = Importer.Importers[name]
# If they currently have progress, get it and return the progress.
if importer.importerInstance
return importer.importerInstance.getProgress()
else
importer.importerInstance = new importer.importer importer.name, importer.description, importer.mimeType
return importer.importerInstance.getProgress()
else
console.warn "Tried to setup #{name} as an importer."
throw new Meteor.Error 'error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'setupImporter' }
Meteor.methods
startImport: (name, input) ->
# Takes name and object with users / channels selected to import
if not Meteor.userId()
throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'startImport' }
if not RocketChat.authz.hasPermission(Meteor.userId(), 'run-import')
throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter'});
if Importer.Importers[name]?.importerInstance?
usersSelection = input.users.map (user) ->
return new Importer.SelectionUser user.user_id, user.username, user.email, user.is_deleted, user.is_bot, user.do_import
channelsSelection = input.channels.map (channel) ->
return new Importer.SelectionChannel channel.channel_id, channel.name, channel.is_archived, channel.do_import
selection = new Importer.Selection name, usersSelection, channelsSelection
Importer.Importers[name].importerInstance.startImport selection
else
throw new Meteor.Error 'error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'startImport' }
Importer.Imports = new class Importer.Imports extends RocketChat.models._Base
constructor: ->
super('import')
Importer.RawImports = new class Importer.RawImports extends RocketChat.models._Base
constructor: ->
super('raw_imports')
Meteor.startup ->
# Make sure all imports are marked as invalid, data clean up since you can't
# restart an import at the moment.
Importer.Imports.update { valid: { $ne: false } }, { $set: { valid: false } }, { multi: true }
# Clean up all the raw import data, since you can't restart an import at the moment
Importer.Imports.find({ valid: { $ne: true }}).forEach (item) ->
Importer.RawImports.remove { 'import': item._id, 'importer': item.type }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment