@@ -262,15 +262,15 @@ def preview_changes(self):
262262                # Auto-resize rows to content 
263263                self .preview_table .resizeRowsToContents ()
264264
265-                 self .emit_log (f"Preview completed: { len (changes )}   datasource (s) with  credentials found" )
265+                 self .emit_log (f"Preview completed: { len (changes )}   type (s) of  credentials found" )
266266            else :
267267                # Hide table and show info message 
268268                self .preview_table .setVisible (False )
269269                self .preview_info_label .setVisible (True )
270-                 self .preview_info_label .setText ("No datasources with  credentials found in this file." )
270+                 self .preview_info_label .setText ("No credentials found in this file." )
271271                self .preview_info_label .setStyleSheet ("color: #4CAF50; font-style: italic; padding: 10px;" )
272272
273-                 self .emit_log ("Preview completed: No datasources with  credentials found" )
273+                 self .emit_log ("Preview completed: No credentials found" )
274274
275275        except  Exception  as  e :
276276            self .show_error (f"Error previewing file: { str (e )}  " )
@@ -315,7 +315,7 @@ def clean_file(self):
315315            self .file_cleaned .emit (cleaned_path )
316316
317317            success_msg  =  (f"✓ File cleaned successfully!\n " 
318-                           f"• Cleaned  { changes_count }   datasource (s)\n " 
318+                           f"• Removed  { changes_count }   credential (s)\n " 
319319                          f"• Original file preserved\n " 
320320                          f"• Saved to: { os .path .basename (cleaned_path )}  " )
321321
@@ -372,46 +372,84 @@ def _write_qgs_file(self, output_path, content, is_qgz=False):
372372    def  _find_datasource_changes (self , content ):
373373        """Find datasources that would be changed and return before/after pairs.""" 
374374        changes  =  []
375-         
376-         # Pattern to match datasource tags with PostgreSQL connections 
377-         datasource_pattern  =  r'<datasource>(.*?)</datasource>' 
378-         
379-         for  match  in  re .finditer (datasource_pattern , content , re .DOTALL ):
380-             original_datasource  =  match .group (1 ).strip ()
381-             
382-             # Check if it's a PostgreSQL connection with credentials 
383-             if  self ._has_postgres_credentials (original_datasource ):
384-                 cleaned_datasource  =  self ._clean_single_datasource (original_datasource )
385-                 if  cleaned_datasource  !=  original_datasource :
386-                     changes .append ((original_datasource , cleaned_datasource ))
375+         found_connections  =  set ()  # To avoid duplicates 
376+         
377+         # Pattern to find all PostgreSQL connection strings (containing dbname=) 
378+         # This covers quoted strings, attribute values, and various formats 
379+         patterns  =  [
380+             r'"[^"]*dbname=[^"]*"' ,     # Double-quoted strings 
381+             r"'[^']*dbname=[^']*'" ,     # Single-quoted strings   
382+             r'(?:value|source|dataSource|destinationLayerSource)="([^"]*dbname=[^"]*)"' ,  # Attribute values 
383+             r"(?:value|source|dataSource|destinationLayerSource)='([^']*dbname=[^']*)'" ,  # Single-quoted attribute values 
384+         ]
385+         
386+         for  pattern  in  patterns :
387+             for  match  in  re .finditer (pattern , content ):
388+                 # Get the connection string (either full match or group 1 if it's an attribute) 
389+                 if  match .groups ():
390+                     connection_string  =  match .group (1 )
391+                 else :
392+                     connection_string  =  match .group (0 )
393+                 
394+                 # Skip if we've already found this connection string 
395+                 if  connection_string  in  found_connections :
396+                     continue 
397+                 
398+                 # Check if this connection string has credentials we want to remove 
399+                 if  self ._has_postgres_credentials (connection_string ):
400+                     cleaned  =  self ._clean_single_datasource (connection_string )
401+                     if  cleaned  !=  connection_string :
402+                         changes .append ((connection_string , cleaned ))
403+                         found_connections .add (connection_string )
387404
388405        return  changes 
389406
390407    def  _clean_datasources (self , content ):
391408        """Clean all datasources in the content and return cleaned content and count.""" 
392409        changes_count  =  0 
410+         cleaned_content  =  content 
393411
394-         def  replace_datasource (match ):
395-             nonlocal  changes_count 
396-             original_datasource  =  match .group (1 ).strip ()
412+         # Global removal of user credentials 
413+         if  self .remove_user_checkbox .isChecked ():
414+             # Count user credentials before removing them 
415+             user_matches  =  re .findall (r'user=[\'"][^\'\"]*[\'"]|user=[^\s]+' , cleaned_content )
416+             changes_count  +=  len (user_matches )
397417
398-             if  self ._has_postgres_credentials (original_datasource ):
399-                 cleaned_datasource  =  self ._clean_single_datasource (original_datasource )
400-                 if  cleaned_datasource  !=  original_datasource :
401-                     changes_count  +=  1 
402-                     return  f'<datasource>{ cleaned_datasource }  </datasource>' 
418+             # Remove user credentials (being very careful about spaces) 
419+             # Handle space + user= (most common case) 
420+             cleaned_content  =  re .sub (r'\s+user=[\'"][^\'\"]*[\'"]' , '' , cleaned_content )
421+             cleaned_content  =  re .sub (r'\s+user=[^\s]+' , '' , cleaned_content )
422+             # Handle user= + space (when user is first parameter)   
423+             cleaned_content  =  re .sub (r'user=[\'"][^\'\"]*[\'"]\s+' , '' , cleaned_content )
424+             cleaned_content  =  re .sub (r'user=[^\s]+\s+' , '' , cleaned_content )
425+             # Handle isolated user= (no surrounding spaces) 
426+             cleaned_content  =  re .sub (r'user=[\'"][^\'\"]*[\'"]' , '' , cleaned_content )
427+             cleaned_content  =  re .sub (r'user=[^\s]+' , '' , cleaned_content )
428+         
429+         # Global removal of password credentials 
430+         if  self .remove_password_checkbox .isChecked ():
431+             # Count password credentials before removing them 
432+             password_matches  =  re .findall (r'password=[\'"][^\'\"]*[\'"]|password=[^\s]+' , cleaned_content )
433+             changes_count  +=  len (password_matches )
403434
404-             return  match .group (0 )
405-         
406-         # Pattern to match datasource tags 
407-         datasource_pattern  =  r'<datasource>(.*?)</datasource>' 
408-         cleaned_content  =  re .sub (datasource_pattern , replace_datasource , content , flags = re .DOTALL )
435+             # Remove password credentials (being very careful about spaces) 
436+             # Handle space + password= (most common case) 
437+             cleaned_content  =  re .sub (r'\s+password=[\'"][^\'\"]*[\'"]' , '' , cleaned_content )
438+             cleaned_content  =  re .sub (r'\s+password=[^\s]+' , '' , cleaned_content )
439+             # Handle password= + space (when password is first parameter) 
440+             cleaned_content  =  re .sub (r'password=[\'"][^\'\"]*[\'"]\s+' , '' , cleaned_content )
441+             cleaned_content  =  re .sub (r'password=[^\s]+\s+' , '' , cleaned_content )
442+             # Handle isolated password= (no surrounding spaces) 
443+             cleaned_content  =  re .sub (r'password=[\'"][^\'\"]*[\'"]' , '' , cleaned_content )
444+             cleaned_content  =  re .sub (r'password=[^\s]+' , '' , cleaned_content )
445+         
446+         # NO general whitespace cleanup - preserve all XML formatting exactly as is 
409447
410448        return  cleaned_content , changes_count 
411449
412450    def  _has_postgres_credentials (self , datasource ):
413451        """Check if datasource has PostgreSQL credentials.""" 
414-         # Check for  dbname parameter  (indicates PostgreSQL) and credentials 
452+         # Must have  dbname=  (indicates PostgreSQL) and credentials we want to remove  
415453        has_dbname  =  'dbname='  in  datasource 
416454        has_user  =  'user='  in  datasource  and  self .remove_user_checkbox .isChecked ()
417455        has_password  =  'password='  in  datasource  and  self .remove_password_checkbox .isChecked ()
@@ -420,15 +458,16 @@ def _has_postgres_credentials(self, datasource):
420458
421459    def  _clean_single_datasource (self , datasource ):
422460        """Clean a single datasource string.""" 
461+         # This method is less important now, but kept for compatibility 
423462        cleaned  =  datasource 
424463
425464        if  self .remove_user_checkbox .isChecked ():
426-             # Remove user='...' or  user="..."  
427-             cleaned  =  re .sub (r" \s*user=['\"][^'\"]*['\"]"  ,  "" , cleaned )
465+             cleaned   =   re . sub ( r'\s* user=[\'"][^\'\"]*[\'"]' ,  '' ,  cleaned ) 
466+             cleaned  =  re .sub (r' \s*user=[^\s]+'  ,  '' , cleaned )
428467
429468        if  self .remove_password_checkbox .isChecked ():
430-             # Remove password='...' or  password="..." 
431-             cleaned  =  re .sub (r" \s*password=['\"][^'\"]*['\"]"  ,  "" , cleaned )
469+             cleaned   =   re . sub ( r'\s* password=[\'"][^\'\"]*[\'"]' ,  '' ,  cleaned ) 
470+             cleaned  =  re .sub (r' \s*password=[^\s]+'  ,  '' , cleaned )
432471
433472        # Clean up any double spaces 
434473        cleaned  =  re .sub (r'\s+' , ' ' , cleaned ).strip ()
0 commit comments