Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
Lucas SCHMIDT
sight
Commits
c5b7d575
Verified
Commit
c5b7d575
authored
Sep 03, 2021
by
Didier WECKMANN
Browse files
feat(io): add a "retry/cancel" dialog when the password is wrong
parent
0aae7ba1
Changes
13
Hide whitespace changes
Inline
Side-by-side
libs/io/session/PasswordKeeper.hpp
View file @
c5b7d575
...
...
@@ -43,7 +43,7 @@ public:
NEVER
=
0
,
/// Never use a password
ONCE
=
1
,
/// Ask for password once and reuse it later
ALWAYS
=
2
,
/// Always ask for a password
DEFAULT
=
NEVER
,
/// Default behavior i
s
nothing is set
DEFAULT
=
NEVER
,
/// Default behavior i
f
nothing is set
INVALID
=
255
/// Used for error management
};
...
...
@@ -53,7 +53,7 @@ public:
PASSWORD
=
0
,
/// Use the given password for encryption
SALTED
=
1
,
/// Use the given password with salt for encryption
FORCED
=
2
,
/// Force encryption with a pseudo random hidden password
DEFAULT
=
PASSWORD
,
/// Default behavior i
s
nothing is set
DEFAULT
=
PASSWORD
,
/// Default behavior i
f
nothing is set
INVALID
=
255
/// Used for error management
};
...
...
libs/io/session/SessionReader.cpp
View file @
c5b7d575
...
...
@@ -47,7 +47,7 @@ public:
inline
SessionReaderImpl
(
SessionReader
*
const
sessionReader
)
:
m_SessionReader
(
sessionReader
),
m_password
(
std
::
make_unique
<
PasswordKeeper
>
()),
m_
E
ncryptionPolicy
(
PasswordKeeper
::
EncryptionPolicy
::
DEFAULT
)
m_
e
ncryptionPolicy
(
PasswordKeeper
::
EncryptionPolicy
::
DEFAULT
)
{
}
...
...
@@ -59,7 +59,7 @@ public:
{
// Create the session and deserialize the root object
detail
::
SessionDeserializer
session
;
m_object
=
session
.
deserialize
(
m_SessionReader
->
getFile
(),
m_password
->
getPassword
(),
m_
E
ncryptionPolicy
);
m_object
=
session
.
deserialize
(
m_SessionReader
->
getFile
(),
m_password
->
getPassword
(),
m_
e
ncryptionPolicy
);
}
/// Use a shared_ptr to keep the object alive as it is the read() return value
...
...
@@ -72,7 +72,7 @@ public:
const
std
::
unique_ptr
<
PasswordKeeper
>
m_password
;
/// The encryption policy
PasswordKeeper
::
EncryptionPolicy
m_
E
ncryptionPolicy
;
PasswordKeeper
::
EncryptionPolicy
m_
e
ncryptionPolicy
;
};
SessionReader
::
SessionReader
(
base
::
reader
::
IObjectReader
::
Key
)
:
...
...
@@ -111,7 +111,7 @@ void SessionReader::setPassword(const core::crypto::secure_string& password)
void
SessionReader
::
setEncryptionPolicy
(
const
PasswordKeeper
::
EncryptionPolicy
policy
)
{
m_pimpl
->
m_
E
ncryptionPolicy
=
policy
;
m_pimpl
->
m_
e
ncryptionPolicy
=
policy
;
}
}
// namespace sight::io::session
libs/io/session/SessionWriter.cpp
View file @
c5b7d575
...
...
@@ -47,7 +47,7 @@ public:
inline
SessionWriterImpl
(
SessionWriter
*
const
sessionWriter
)
:
m_SessionWriter
(
sessionWriter
),
m_password
(
std
::
make_unique
<
PasswordKeeper
>
()),
m_
E
ncryptionPolicy
(
PasswordKeeper
::
EncryptionPolicy
::
DEFAULT
)
m_
e
ncryptionPolicy
(
PasswordKeeper
::
EncryptionPolicy
::
DEFAULT
)
{
}
...
...
@@ -67,7 +67,7 @@ public:
m_SessionWriter
->
getFile
(),
root_object
,
m_password
->
getPassword
(),
m_
E
ncryptionPolicy
m_
e
ncryptionPolicy
);
}
...
...
@@ -78,7 +78,7 @@ public:
const
std
::
unique_ptr
<
PasswordKeeper
>
m_password
;
/// The encryption policy
PasswordKeeper
::
EncryptionPolicy
m_
E
ncryptionPolicy
;
PasswordKeeper
::
EncryptionPolicy
m_
e
ncryptionPolicy
;
};
SessionWriter
::
SessionWriter
(
base
::
writer
::
IObjectWriter
::
Key
)
:
...
...
@@ -114,7 +114,7 @@ void SessionWriter::setPassword(const core::crypto::secure_string& password)
void
SessionWriter
::
setEncryptionPolicy
(
const
PasswordKeeper
::
EncryptionPolicy
policy
)
{
m_pimpl
->
m_
E
ncryptionPolicy
=
policy
;
m_pimpl
->
m_
e
ncryptionPolicy
=
policy
;
}
}
//namespace sight::io::session
libs/io/zip/ArchiveReader.cpp
View file @
c5b7d575
...
...
@@ -230,18 +230,29 @@ public:
{
// In case of error, unlock the file
m_attributes
.
m_archive
->
unlock
(
m_attributes
.
m_filePath
);
}
SIGHT_THROW_EXCEPTION_IF
(
exception
::
Read
(
"Cannot open file '"
+
m_attributes
.
m_filePath
.
string
()
+
"' in archive '"
+
m_attributes
.
m_archive
->
m_archivePath
.
string
()
+
"'."
),
result
!=
MZ_OK
);
SIGHT_THROW_EXCEPTION_IF
(
exception
::
BadPassword
(
"The password used to open file '"
+
m_attributes
.
m_filePath
.
string
()
+
"' in archive '"
+
m_attributes
.
m_archive
->
m_archivePath
.
string
()
+
"' is wrong."
),
result
==
MZ_PASSWORD_ERROR
);
SIGHT_THROW_EXCEPTION
(
exception
::
Read
(
"Cannot open file '"
+
m_attributes
.
m_filePath
.
string
()
+
"' in archive '"
+
m_attributes
.
m_archive
->
m_archivePath
.
string
()
+
"'.
\n\n
Error code: "
+
std
::
to_string
(
result
)
)
);
}
}
~
HandleKeeper
()
...
...
libs/io/zip/exception/Read.cpp
deleted
100644 → 0
View file @
0aae7ba1
/************************************************************************
*
* Copyright (C) 2009-2021 IRCAD France
* Copyright (C) 2012-2015 IHU Strasbourg
*
* This file is part of Sight.
*
* Sight is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Sight is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Sight. If not, see <https://www.gnu.org/licenses/>.
*
***********************************************************************/
#include
"io/zip/exception/Read.hpp"
#include
"io/zip/config.hpp"
#include
<core/Exception.hpp>
namespace
sight
::
io
::
zip
{
namespace
exception
{
Read
::
Read
(
const
std
::
string
&
err
)
:
core
::
Exception
(
err
)
{
}
}
// namespace exception
}
// namespace sight::io::zip
libs/io/zip/exception/Read.hpp
View file @
c5b7d575
...
...
@@ -35,7 +35,18 @@ namespace exception
/// Read exception.
struct
Read
:
core
::
Exception
{
IO_ZIP_API
Read
(
const
std
::
string
&
err
);
inline
Read
(
const
std
::
string
&
err
)
:
core
::
Exception
(
err
)
{
}
};
struct
BadPassword
:
Read
{
inline
BadPassword
(
const
std
::
string
&
err
)
:
Read
(
err
)
{
}
};
}
// namespace exception
...
...
libs/io/zip/exception/Write.cpp
deleted
100644 → 0
View file @
0aae7ba1
/************************************************************************
*
* Copyright (C) 2009-2021 IRCAD France
* Copyright (C) 2012-2015 IHU Strasbourg
*
* This file is part of Sight.
*
* Sight is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Sight is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Sight. If not, see <https://www.gnu.org/licenses/>.
*
***********************************************************************/
#include
"io/zip/exception/Write.hpp"
#include
"io/zip/config.hpp"
#include
<core/Exception.hpp>
namespace
sight
::
io
::
zip
{
namespace
exception
{
Write
::
Write
(
const
std
::
string
&
err
)
:
core
::
Exception
(
err
)
{
}
}
// namespace exception
}
// namespace sight::io::zip
libs/io/zip/exception/Write.hpp
View file @
c5b7d575
...
...
@@ -35,7 +35,10 @@ namespace exception
/// Write exception.
struct
Write
:
core
::
Exception
{
IO_ZIP_API
Write
(
const
std
::
string
&
err
);
inline
Write
(
const
std
::
string
&
err
)
:
core
::
Exception
(
err
)
{
}
};
}
// namespace exception
...
...
libs/ui/base/dialog/IMessageDialog.hpp
View file @
c5b7d575
...
...
@@ -61,6 +61,7 @@ public:
YES
=
1
<<
2
,
NO
=
1
<<
3
,
CANCEL
=
1
<<
4
,
RETRY
=
1
<<
5
,
YES_NO
=
YES
|
NO
}
Buttons
;
...
...
@@ -81,7 +82,7 @@ public:
/// Set the icon (CRITICAL, WARNING, INFO or QUESTION)
UI_BASE_API
virtual
void
setIcon
(
Icons
icon
)
=
0
;
/// Add a button (OK, YES_NO, YES, NO, CANCEL)
/// Add a button (OK, YES_NO, YES, NO, CANCEL
, RETRY
)
UI_BASE_API
virtual
void
addButton
(
Buttons
button
)
=
0
;
/// Set the default button
...
...
libs/ui/qml/dialog/MessageDialog.cpp
View file @
c5b7d575
...
...
@@ -61,7 +61,8 @@ MessageDialogQmlButtonType messageDialogQmlButton =
{
ui
::
base
::
dialog
::
IMessageDialog
::
OK
,
StandardButton
::
ButtonList
::
Ok
},
{
ui
::
base
::
dialog
::
IMessageDialog
::
CANCEL
,
StandardButton
::
ButtonList
::
Cancel
},
{
ui
::
base
::
dialog
::
IMessageDialog
::
YES
,
StandardButton
::
ButtonList
::
Yes
},
{
ui
::
base
::
dialog
::
IMessageDialog
::
NO
,
StandardButton
::
ButtonList
::
No
}
{
ui
::
base
::
dialog
::
IMessageDialog
::
NO
,
StandardButton
::
ButtonList
::
No
},
{
ui
::
base
::
dialog
::
IMessageDialog
::
RETRY
,
StandardButton
::
ButtonList
::
Retry
}
};
//------------------------------------------------------------------------------
...
...
libs/ui/qt/dialog/MessageDialog.cpp
View file @
c5b7d575
...
...
@@ -48,11 +48,12 @@ MessageDialogQtIconsType messageDialogQtIcons = {{ui::base::dialog::IMessageDial
typedef
const
std
::
map
<
ui
::
base
::
dialog
::
IMessageDialog
::
Buttons
,
QMessageBox
::
StandardButtons
>
MessageDialogQtButtonType
;
MessageDialogQtButtonType
messageDialogQtButton
=
{{
ui
::
base
::
dialog
::
IMessageDialog
::
OK
,
QMessageBox
::
Ok
},
MessageDialogQtButtonType
messageDialogQtButton
=
{
{
ui
::
base
::
dialog
::
IMessageDialog
::
OK
,
QMessageBox
::
Ok
},
{
ui
::
base
::
dialog
::
IMessageDialog
::
CANCEL
,
QMessageBox
::
Cancel
},
{
ui
::
base
::
dialog
::
IMessageDialog
::
YES
,
QMessageBox
::
Yes
},
{
ui
::
base
::
dialog
::
IMessageDialog
::
NO
,
QMessageBox
::
No
}
{
ui
::
base
::
dialog
::
IMessageDialog
::
NO
,
QMessageBox
::
No
},
{
ui
::
base
::
dialog
::
IMessageDialog
::
RETRY
,
QMessageBox
::
Retry
}
};
//------------------------------------------------------------------------------
...
...
modules/io/session/SReader.cpp
View file @
c5b7d575
...
...
@@ -31,6 +31,7 @@
#include
<io/session/PasswordKeeper.hpp>
#include
<io/session/SessionReader.hpp>
#include
<io/zip/exception/Read.hpp>
#include
<ui/base/Cursor.hpp>
#include
<ui/base/dialog/InputDialog.hpp>
...
...
@@ -74,13 +75,16 @@ public:
std
::
string
m_extensionDescription
{
"Sight session"
};
/// Password policy to use
PasswordKeeper
::
PasswordPolicy
m_
P
asswordPolicy
{
PasswordKeeper
::
PasswordPolicy
::
DEFAULT
};
PasswordKeeper
::
PasswordPolicy
m_
p
asswordPolicy
{
PasswordKeeper
::
PasswordPolicy
::
DEFAULT
};
/// Encryption policy to use
PasswordKeeper
::
EncryptionPolicy
m_
E
ncryptionPolicy
{
PasswordKeeper
::
EncryptionPolicy
::
DEFAULT
};
PasswordKeeper
::
EncryptionPolicy
m_
e
ncryptionPolicy
{
PasswordKeeper
::
EncryptionPolicy
::
DEFAULT
};
/// Signal emitted when job created.
JobCreatedSignal
::
sptr
m_jobCreatedSignal
;
/// Used in case of bad password
int
m_passwordRetry
{
0
};
};
SReader
::
SReader
()
noexcept
:
...
...
@@ -124,23 +128,23 @@ void SReader::configuring()
if
(
password
.
is_initialized
())
{
// Password policy
m_pimpl
->
m_
P
asswordPolicy
=
PasswordKeeper
::
stringToPasswordPolicy
(
m_pimpl
->
m_
p
asswordPolicy
=
PasswordKeeper
::
stringToPasswordPolicy
(
password
->
get
<
std
::
string
>
(
"policy"
)
);
SIGHT_THROW_IF
(
"Cannot read password policy."
,
m_pimpl
->
m_
P
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
INVALID
m_pimpl
->
m_
p
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
INVALID
);
// Encryption policy
m_pimpl
->
m_
E
ncryptionPolicy
=
PasswordKeeper
::
stringToEncryptionPolicy
(
m_pimpl
->
m_
e
ncryptionPolicy
=
PasswordKeeper
::
stringToEncryptionPolicy
(
password
->
get
<
std
::
string
>
(
"encryption"
)
);
SIGHT_THROW_IF
(
"Cannot read encryption policy."
,
m_pimpl
->
m_
E
ncryptionPolicy
==
PasswordKeeper
::
EncryptionPolicy
::
INVALID
m_pimpl
->
m_
e
ncryptionPolicy
==
PasswordKeeper
::
EncryptionPolicy
::
INVALID
);
}
}
...
...
@@ -171,7 +175,7 @@ void SReader::updating()
const
secure_string
&
password
=
[
&
]
{
if
(
m_pimpl
->
m_
P
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
NEVER
)
if
(
m_pimpl
->
m_
p
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
NEVER
)
{
// No password management
return
secure_string
();
...
...
@@ -180,8 +184,9 @@ void SReader::updating()
{
const
secure_string
&
globalPassword
=
PasswordKeeper
::
getGlobalPassword
();
if
((
m_pimpl
->
m_PasswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
ALWAYS
)
||
(
m_pimpl
->
m_PasswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
ONCE
if
(
m_pimpl
->
m_passwordRetry
>
0
||
(
m_pimpl
->
m_passwordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
ALWAYS
)
||
(
m_pimpl
->
m_passwordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
ONCE
&&
globalPassword
.
empty
()))
{
sight
::
ui
::
base
::
dialog
::
InputDialog
inputDialog
;
...
...
@@ -194,7 +199,7 @@ void SReader::updating()
)
);
if
(
m_pimpl
->
m_
P
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
ONCE
)
if
(
m_pimpl
->
m_
p
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
ONCE
)
{
PasswordKeeper
::
setGlobalPassword
(
newPassword
);
}
...
...
@@ -216,7 +221,7 @@ void SReader::updating()
auto
reader
=
sight
::
io
::
session
::
SessionReader
::
New
();
reader
->
setFile
(
filepath
);
reader
->
setPassword
(
password
);
reader
->
setEncryptionPolicy
(
m_pimpl
->
m_
E
ncryptionPolicy
);
reader
->
setEncryptionPolicy
(
m_pimpl
->
m_
e
ncryptionPolicy
);
// Set cursor to busy state. It will be reset to default even if exception occurs
const
sight
::
ui
::
base
::
BusyCursor
busyCursor
;
...
...
@@ -253,6 +258,28 @@ void SReader::updating()
jobs
->
run
().
get
();
m_readFailed
=
false
;
}
catch
(
sight
::
io
::
zip
::
exception
::
BadPassword
&
badPassword
)
{
// Ask if the user want to retry.
sight
::
ui
::
base
::
dialog
::
MessageDialog
messageBox
;
messageBox
.
setTitle
(
"Wrong password"
);
messageBox
.
setMessage
(
"The file is password protected and the provided password is wrong.
\n\n
Retry with a different password ?"
);
messageBox
.
setIcon
(
ui
::
base
::
dialog
::
IMessageDialog
::
QUESTION
);
messageBox
.
addButton
(
ui
::
base
::
dialog
::
IMessageDialog
::
RETRY
);
messageBox
.
addButton
(
ui
::
base
::
dialog
::
IMessageDialog
::
CANCEL
);
if
(
messageBox
.
show
()
==
sight
::
ui
::
base
::
dialog
::
IMessageDialog
::
RETRY
)
{
m_pimpl
->
m_passwordRetry
++
;
updating
();
}
else
{
m_pimpl
->
m_passwordRetry
=
0
;
}
}
catch
(
std
::
exception
&
_e
)
{
// Handle the error.
...
...
modules/io/session/SWriter.cpp
View file @
c5b7d575
...
...
@@ -74,10 +74,10 @@ public:
std
::
string
m_extensionDescription
{
"Sight session"
};
/// Password policy to use
PasswordKeeper
::
PasswordPolicy
m_
P
asswordPolicy
{
PasswordKeeper
::
PasswordPolicy
::
DEFAULT
};
PasswordKeeper
::
PasswordPolicy
m_
p
asswordPolicy
{
PasswordKeeper
::
PasswordPolicy
::
DEFAULT
};
/// Encryption policy to use
PasswordKeeper
::
EncryptionPolicy
m_
E
ncryptionPolicy
{
PasswordKeeper
::
EncryptionPolicy
::
DEFAULT
};
PasswordKeeper
::
EncryptionPolicy
m_
e
ncryptionPolicy
{
PasswordKeeper
::
EncryptionPolicy
::
DEFAULT
};
/// Signal emitted when job created.
JobCreatedSignal
::
sptr
m_jobCreatedSignal
;
...
...
@@ -124,20 +124,20 @@ void SWriter::configuring()
if
(
password
.
is_initialized
())
{
// Password policy
m_pimpl
->
m_
P
asswordPolicy
=
PasswordKeeper
::
stringToPasswordPolicy
(
password
->
get
<
std
::
string
>
(
"policy"
));
m_pimpl
->
m_
p
asswordPolicy
=
PasswordKeeper
::
stringToPasswordPolicy
(
password
->
get
<
std
::
string
>
(
"policy"
));
SIGHT_THROW_IF
(
"Cannot read password policy."
,
m_pimpl
->
m_
P
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
INVALID
m_pimpl
->
m_
p
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
INVALID
);
// Encryption policy
m_pimpl
->
m_
E
ncryptionPolicy
=
PasswordKeeper
::
stringToEncryptionPolicy
(
m_pimpl
->
m_
e
ncryptionPolicy
=
PasswordKeeper
::
stringToEncryptionPolicy
(
password
->
get
<
std
::
string
>
(
"encryption"
)
);
SIGHT_THROW_IF
(
"Cannot read encryption policy."
,
m_pimpl
->
m_
E
ncryptionPolicy
==
PasswordKeeper
::
EncryptionPolicy
::
INVALID
m_pimpl
->
m_
e
ncryptionPolicy
==
PasswordKeeper
::
EncryptionPolicy
::
INVALID
);
}
}
...
...
@@ -177,7 +177,7 @@ void SWriter::updating()
const
secure_string
&
password
=
[
&
]
{
if
(
m_pimpl
->
m_
P
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
NEVER
)
if
(
m_pimpl
->
m_
p
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
NEVER
)
{
// No password management
return
secure_string
();
...
...
@@ -186,8 +186,8 @@ void SWriter::updating()
{
const
secure_string
&
globalPassword
=
PasswordKeeper
::
getGlobalPassword
();
if
((
m_pimpl
->
m_
P
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
ALWAYS
)
||
(
m_pimpl
->
m_
P
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
ONCE
if
((
m_pimpl
->
m_
p
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
ALWAYS
)
||
(
m_pimpl
->
m_
p
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
ONCE
&&
globalPassword
.
empty
()))
{
sight
::
ui
::
base
::
dialog
::
InputDialog
inputDialog
;
...
...
@@ -200,7 +200,7 @@ void SWriter::updating()
)
);
if
(
m_pimpl
->
m_
P
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
ONCE
)
if
(
m_pimpl
->
m_
p
asswordPolicy
==
PasswordKeeper
::
PasswordPolicy
::
ONCE
)
{
PasswordKeeper
::
setGlobalPassword
(
newPassword
);
}
...
...
@@ -226,7 +226,7 @@ void SWriter::updating()
writer
->
setObject
(
data
.
get_shared
());
writer
->
setFile
(
temporaryFile
.
getTemporaryFilePath
());
writer
->
setPassword
(
password
);
writer
->
setEncryptionPolicy
(
m_pimpl
->
m_
E
ncryptionPolicy
);
writer
->
setEncryptionPolicy
(
m_pimpl
->
m_
e
ncryptionPolicy
);
}
// Set cursor to busy state. It will be reset to default even if exception occurs
...
...
@@ -247,7 +247,7 @@ void SWriter::updating()
runningJob
.
doneWork
(
80
);
// Robust rename
core
::
tools
::
System
::
robustRename
(
temporaryFile
.
getTemporaryFilePath
(),
filepath
);
core
::
tools
::
System
::
robustRename
(
temporaryFile
.
getTemporaryFilePath
(),
filepath
,
true
);
runningJob
.
done
();
},
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment