• Login
Community
  • Login

Way to disallow copying text?

Scheduled Pinned Locked Moved Help wanted · · · – – – · · ·
7 Posts 3 Posters 1.5k Views
Loading More Posts
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • A
    Alan Kilborn
    last edited by May 13, 2021, 1:09 PM

    Title says it all. :-)

    So what I’m wondering about is, can I tell Notepad++ that I don’t want a user to be able to copy text data out of its editing window (i.e., view1 and view2)?

    So they’d make a selection and press Ctrl+c (or Ctrl+x) and nothing would happen. Or Edit menu Copy (or Cut) would be disabled. Or drag-and-drop text would be disabled and not work. Basically all methods of getting the data out of a document would fail.

    This would be similar to how some PDF (and maybe Word?) documents are “secured” and, while you can select text, you then are not allowed to copy that selection to the clipboard?

    Ideally, I’d want this capability (enable it/disable it) via script, but I’m just wondering if there’s a way to achieve it at all. I’m trying to protect some users of a script I’m developing from doing bad things with the data the script produces.

    (And sure, I realize that opening the file in notepad.exe, for example, would totally bypass my imagined “security” method, but I’m mainly trying to protect my users from themselves).

    P 1 Reply Last reply May 13, 2021, 1:24 PM Reply Quote 1
    • P
      PeterJones @Alan Kilborn
      last edited by May 13, 2021, 1:24 PM

      @Alan-Kilborn said in Way to disallow copying text?:

      this would be similar to how some PDF (and maybe Word?) documents are “secured”

      My guess is that PDF viewers are coded in such a way that they have direct control over what happens with various menu and keystroke activations, so inside their “copy” handler, they can check “if file protected, do not actually perform the copy”. In Notepad++, you would need access to the underlying “copy” code, which is whatever function cmd-id 42002 calls. And it would probably be harder in Notepad++, because the actually copying from the buffer to the clipboard is probably done in the Scintilla codebase, not in the Notepad++ codebase. (Sorry, I haven’t checked the source.)

      Ideally, I’d want this capability (enable it/disable it) via script, but I’m just wondering if there’s a way to achieve it at all.

      I cannot imagine it could be done any way other than a script or plugin… and that is likely hard, if at all possible.

      You could easily remap Ctrl+C/Ctrl+X, and remove copy/cut from the contextMenu… but that wouldn’t prevent users from clicking the toolbar or menu versions of the copy/cut commands.

      I’ve never tried, but maybe there are appropriate Scintilla notifications to intercept the copy/cut commands – a quick glance through didn’t show any, but maybe I didn’t read closely enough.

      Hmm, maybe there’s a win32 API notification of the clipboard contents being modified, or maybe you could poll the windows clipboard every 0.1s or something and see if it’s been changed (and, if Notepad++ has focus, clear the changes… you wouldn’t want to just arbitrarily clear the changes, because what if the user was in the browser window and copied text there, while Notepad++ just happened to be open in the background).

      Really, my guess is that, effectively, no, you cannot disallow copying text without fundamental changes to the codebase.

      A 2 Replies Last reply May 13, 2021, 3:06 PM Reply Quote 1
      • A
        Alan Kilborn @PeterJones
        last edited by May 13, 2021, 3:06 PM

        @PeterJones

        Thanks for your thoughts.
        Largely, I was looking for something less extreme than some of the ideas you put forth. Obviously not your fault if there aren’t any easier ways.

        Anyway, it was just a thought I had (about protecting my users from doing dumb things). If it isn’t realistically achievable, I’ll just warn them to be careful.

        BTW the background on this is that I have created some tooling that enhances the format of logs that we have to deal with. However, when reporting problems outside of our organization, we have to present the data original format and not my improved format. Using the new format can only get us in trouble. :-( But internally the users are getting addicted to the productivity boost by the new format, and my fear is that they are going to start using it when sharing outside our org.

        1 Reply Last reply Reply Quote 3
        • A
          Alan Kilborn @PeterJones
          last edited by Alan Kilborn May 13, 2021, 3:14 PM May 13, 2021, 3:13 PM

          @PeterJones said in Way to disallow copying text?:

          I’ve never tried, but maybe there are appropriate Scintilla notifications to intercept the copy/cut commands

          Actually, some further thought on this…
          Yes, I believe I can intercept WM_COPY and WM_CUT before they get to Notepad++/Scintilla.
          I just wasn’t thinking along these lines until Peter’s post prompted that.
          It might not be as “drastic”/“extreme” as I thought.
          I’ll look into it and post back again if it pans out reasonably well.

          1 Reply Last reply Reply Quote 2
          • C
            carypt
            last edited by May 16, 2021, 8:48 AM

            surely known : password encryption .

            A 1 Reply Last reply May 16, 2021, 10:31 AM Reply Quote 0
            • A
              Alan Kilborn @carypt
              last edited by May 16, 2021, 10:31 AM

              @carypt said in Way to disallow copying text?:

              surely known : password encryption

              Sorry to say this to you, but that’s one of the DUMBEST replies to any posting I’ve ever read here.

              1 Reply Last reply Reply Quote 0
              • A
                Alan Kilborn
                last edited by May 25, 2021, 2:49 PM

                So I “solved” this issue.
                Disclaimer: I didn’t do it all by myself; I enlisted the aid of a former contributor on this forum to get it done with a PythonScript – and he said that even he consulted some others about certain aspects…so it is not a trivial endeavor!

                And solving this problem is not an absolute. It only prevents the novice user from copying from a Notepad++ window – it can totally be circumvented if a user renames a file to have a file extension that isn’t enforced, or one that knows how to disable the script, or one that does a Find All in Current Document search and then copies results from “THAT” window, etc., etc., etc… That’s why I said “solved” – with the quotes.

                Ok, so let’s go with some details before presenting the script…

                The general approach is to “hook” the Windows message loops for the Notepad++ window, and the two editor windows used for Notepad++'s two views. “Hooking” allows one to pre-process messages, and when you do this, you can “filter out” messages selectively – and not allow Notepad++ to process them. This is perfect for when you want to disallow a copy – you just intercept messages relating to copying of text and don’t allow Notepad++ to get them and do their default duty.

                However, we can’t get at certain things by hooking the message loops. Meaning, we can’t get at a WM_COPY or an SCI_COPY message directly. This would be ideal: Any time we receive something that looks like a user request to copy data, it should look like a WM_COPY message which we could throw away – and nothing will be copied. Reality, however, is a bit more complicated. Here are the general rules:

                • A Notepad++ command that has no keycombo assigned in Shortcut Mapper is easily intercepted; one merely has to “hook” into WM_COMMAND messages and check to see if wParam has a value corresponding to the command of interest. The command values are found by looking at the Notepad++ source code file called menuCmdID.h, see HERE.

                • A Notepad++ command that has a keycombo assigned to it in Shortcut Mapper – and one that is NOT a “Scintilla command” – gets translated into something else BEFORE the “hook” function can obtain the keycombo. These commands get translated into a WM_COMMAND message with a wParam parameter (as described above for an command with no keycombo assigned). But it does mean that you can’t decide, for example, “I’m going to intercept anytime the user presses F5!” because it just won’t work; your “hook” function won’t see the WM_KEYDOWN messages for F5 (by default, this is the “Run” command on N++'s Run menu).

                • A keycombo that has a “Scintilla command” function tied to it can be directly intercepted as a WM_KEYDOWN message. While you can intercept at the keycombo level, you can’t – for key-assigned “Scintilla commands” – intercept at the functional (or command) level. As an example, you can intercept “Ctrl+c” but you can’t intercept the “copy” functionality itself – thus if your intent is to intercept “copy” and user assigns it to a non-standard keycombo, you can’t get at it that way, you have to go directly after the specific keycombo. And how would you know what that non-standard assignment is? – you wouldn’t.

                So how do we apply the “rules” above to what I originally wanted to do, specifically, prevent a user from copying data out of Notepad++?

                Well, we need to:

                • prevent menu commands (which possibly have keycombos assigned to them) that have to do with copying (or cutting) data from running – examples include Edit menu’s Copy and Cut commands, the right-click context menu’s Copy and Cut commands, the Edit menu’s Paste Special commands for Copy Binary Content and Cut Binary Content

                • prevent Scintilla cut/copy-related keycombos from executing their functions – examples include Ctrl+c, Ctrl+x, Ctrl+Ins, Shift+Delete

                • we also need to decide what file types (extensions) the copy functionality should be disabled for

                The following script does all that. I call the script PreventCopyFromCertainFileTypes.py and here’s its listing:

                # -*- coding: utf-8 -*-
                from __future__ import print_function
                
                from Npp import editor, notepad
                import ctypes
                from ctypes.wintypes import HWND, INT, UINT, WPARAM, LPARAM, BOOL
                import platform
                import os
                
                user32 = ctypes.WinDLL('user32', use_last_error=True)
                
                LRESULT = LPARAM
                WndProcType = ctypes.WINFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM)
                
                user32.CallWindowProcW.restype = LRESULT
                user32.CallWindowProcW.argtypes = [WndProcType, HWND, UINT, WPARAM, LPARAM]
                
                if platform.architecture()[0] == '32bit':
                    user32.SetWindowLongW.restype = WndProcType
                    user32.SetWindowLongW.argtypes = [HWND, INT, WndProcType]
                    SetWindowLong = user32.SetWindowLongW
                else:
                    user32.SetWindowLongPtrW.restype = WndProcType
                    user32.SetWindowLongPtrW.argtypes = [HWND, INT, WndProcType]
                    SetWindowLong = user32.SetWindowLongPtrW
                
                GWL_WNDPROC = -4
                WM_KEYDOWN = 0x100; WM_COMMAND = 0x111; WM_CHAR = 0x102
                VK_SHIFT = 0x10; VK_CONTROL = 0x11; VK_MENU = VK_ALT = 0x12
                VK_C = 67; VK_X = 88; VK_INSERT = 45; VK_DELETE = 46
                CTRL_C_CHAR = 3; CTRL_X_CHAR = 24
                IDM_EDIT_COPY = 42002; IDM_EDIT_CUT = 42001; IDM_EDIT_COPY_BINARY = 42048; IDM_EDIT_CUT_BINARY = 42049
                
                class PCFCFT(object):
                
                    def __init__(self, debug=False):
                        self.debug = debug
                        self.registered = False
                        self.npp_hwnd = user32.FindWindowW(u'Notepad++', None)
                        assert self.npp_hwnd
                        self.enum_scintilla(self.npp_hwnd)  # get hwnds for editor1 and editor2
                        assert all([ self.scintilla1_hwnd, self.scintilla2_hwnd ])
                        self.STOP_PROCESSING_MSG = False     # consume the msg; don't let npp/scintilla process it when we return
                        self.CONTINUE_PROCESSING_MSG = True  # let npp/scintilla process the message when we return
                        self.register()
                
                    def enum_scintilla(self, npp_hwnd):
                        self.scintilla1_hwnd = self.scintilla2_hwnd = None
                        EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
                        def foreach_window(hwnd, lParam):
                            curr_class = ctypes.create_unicode_buffer(256)
                            user32.GetClassNameW(hwnd, curr_class, 256)
                            if curr_class.value in [u'Scintilla']:
                                if user32.GetParent(hwnd) == npp_hwnd:
                                    if self.scintilla1_hwnd is None:
                                        self.scintilla1_hwnd = hwnd
                                    elif self.scintilla2_hwnd is None:
                                        self.scintilla2_hwnd = hwnd
                                        return False  # stop enumeration
                            return True  # continue enumeration
                        user32.EnumChildWindows(npp_hwnd, EnumWindowsProc(foreach_window), 0)
                
                    def register(self):
                        if not self.registered:
                            self.new_npp_wnd_proc = WndProcType(self.new_npp_hook_proc)
                            self.old_npp_wnd_proc = SetWindowLong(self.npp_hwnd, GWL_WNDPROC, self.new_npp_wnd_proc)
                            self.new_editor1_wnd_proc = WndProcType(self.new_editor1_hook_proc)
                            self.old_editor1_wnd_proc = SetWindowLong(self.scintilla1_hwnd, GWL_WNDPROC, self.new_editor1_wnd_proc)
                            self.new_editor2_wnd_proc = WndProcType(self.new_editor2_hook_proc)
                            self.old_editor2_wnd_proc = SetWindowLong(self.scintilla2_hwnd, GWL_WNDPROC, self.new_editor2_wnd_proc)
                            self.registered = True
                
                    def unregister(self):
                        if self.registered:
                            _ = SetWindowLong(self.npp_hwnd, GWL_WNDPROC, self.old_npp_wnd_proc)
                            _ = SetWindowLong(self.scintilla1_hwnd, GWL_WNDPROC, self.old_editor1_wnd_proc)
                            _ = SetWindowLong(self.scintilla2_hwnd, GWL_WNDPROC, self.old_editor2_wnd_proc)
                            self.registered = False
                
                    def modifier_key_pressed(self, the_key):  # the_key can be VK_SHIFT, VK_CONTROL, VK_ALT
                        return (user32.GetAsyncKeyState(the_key) & 0x8000) == 0x8000
                
                    def new_npp_hook_proc(self, hWnd, msg, wParam, lParam):
                        retval = self.common_msg_processing_function(hWnd, msg, wParam, lParam)
                        if retval: retval = user32.CallWindowProcW(self.old_npp_wnd_proc, hWnd, msg, wParam, lParam)
                        return retval
                
                    def new_editor1_hook_proc(self, hWnd, msg, wParam, lParam):
                        retval = self.common_msg_processing_function(hWnd, msg, wParam, lParam)
                        if retval: retval = user32.CallWindowProcW(self.old_editor1_wnd_proc, hWnd, msg, wParam, lParam)
                        return retval
                
                    def new_editor2_hook_proc(self, hWnd, msg, wParam, lParam):
                        retval = self.common_msg_processing_function(hWnd, msg, wParam, lParam)
                        if retval: retval = user32.CallWindowProcW(self.old_editor2_wnd_proc, hWnd, msg, wParam, lParam)
                        return retval
                
                    def common_msg_processing_function(self, hWnd, msg, wParam, lParam):
                
                        if msg in [ WM_KEYDOWN, WM_COMMAND, WM_CHAR ]:
                
                            # maybe the active file is NOT one that we are looking for...
                            filename = notepad.getCurrentFilename().rsplit(os.sep, 1)[-1]
                            if '.' in filename:
                                (file, ext) = filename.rsplit('.', 1)
                                if ext.lower() not in [ 'txt', 'py' ]:
                                    if self.debug: print('received a msg we handle, but not in a file with an excluded extension')
                                    return self.CONTINUE_PROCESSING_MSG
                
                            if self.debug: origin = 'npp' if hWnd == self.npp_hwnd else 'editor1' if hWnd == self.scintilla1_hwnd else 'editor2'
                
                            if msg == WM_KEYDOWN:
                                if wParam not in [ VK_SHIFT, VK_CONTROL, VK_ALT ]:  # these keys come thru when they are pressed by themselves (we don't care about that case)
                                    shift = self.modifier_key_pressed(VK_SHIFT)
                                    control = self.modifier_key_pressed(VK_CONTROL)
                                    alt = self.modifier_key_pressed(VK_ALT)
                                    if self.debug: print('origin:{o}, msg:WM_KEYDOWN, wParam:{w}, lParam:{L}, Shift:{s}, Ctrl:{c}, Alt:{a}'.format(
                                        o=origin, w=wParam, L=lParam,
                                        s=shift, c=control, a=alt))
                                    if wParam == VK_C and control and not shift and not alt:
                                        if self.debug: print('Ctrl+c keydown consumed!')
                                        return self.STOP_PROCESSING_MSG
                                    if wParam == VK_X and control and not shift and not alt:
                                        if self.debug: print('Ctrl+x keydown consumed!')
                                        return self.STOP_PROCESSING_MSG
                                    if wParam == VK_INSERT and control and not shift and not alt:
                                        if self.debug: print('Ctrl+Insert keydown consumed!')
                                        return self.STOP_PROCESSING_MSG
                                    if wParam == VK_DELETE and not control and shift and not alt:
                                        if self.debug: print('Shift+Delete keydown consumed!')
                                        return self.STOP_PROCESSING_MSG
                
                            elif msg == WM_COMMAND:
                                if wParam not in [ VK_SHIFT, VK_CONTROL, VK_ALT ]:  # for some reason these happen with WM_COMMAND...filter them out
                                    if self.debug: print('origin:{o}, msg:WM_COMMAND, wParam:{w}, lParam:{L}'.format(o=origin, w=wParam, L=lParam))
                                    if wParam in [ IDM_EDIT_COPY, IDM_EDIT_CUT, IDM_EDIT_COPY_BINARY, IDM_EDIT_CUT_BINARY ]:
                                        if self.debug: print('copy/cut related menu item consumed!')
                                        return self.STOP_PROCESSING_MSG
                
                            elif msg == WM_CHAR:
                                if self.debug: print('origin:{o}, msg:WM_CHAR, wParam:{w}, lParam:{L}'.format(o=origin, w=wParam, L=lParam))
                                if wParam == CTRL_C_CHAR:
                                    if self.debug: print('Ctrl+c char consumed!')
                                    return self.STOP_PROCESSING_MSG
                                if wParam == CTRL_X_CHAR:
                                    if self.debug: print('Ctrl+x char consumed!')
                                    return self.STOP_PROCESSING_MSG
                
                        return self.CONTINUE_PROCESSING_MSG
                
                if __name__ == '__main__':
                
                    try:
                        pcfcft
                    except NameError:
                        debug = True if 1 else False
                        pcfcft = PCFCFT(debug)
                        if pcfcft.debug: print('installed')
                    else:
                        if pcfcft.registered:
                            pcfcft.unregister()
                            if pcfcft.debug: print('deactivated')
                        else:
                            pcfcft.register()
                            if pcfcft.debug: print('reactivated')
                

                Some supplmental info:

                An interesting thing about “hook” functions is that you can use them to either avoid original functionality (like our example here shows) or you can “add on” functionality. To “add on” you simply call the original window function in addition to doing your special functionality. So maybe some add on functionality is just to print to the console “got message” or something like that. You do that print in your hook function, and then call the original function – blammo, add-on functionality! If you’re interested in seeing how that is accomplished in the script, look for usage of CONTINUE_PROCESSING_MSG and STOP_PROCESSING_MSG. This is also what lets the script be selective about which filetypes copies/cuts are disallowed from (the demo script chooses .txt and .py files).

                In the case of the non-keycombo commands, looking up the IDs for the commands in menuCmdID.h in Notepad++ source code yields this information (C++ code):

                #define IDM                  40000
                #define IDM_EDIT             (IDM + 2000)
                #define IDM_EDIT_CUT         (IDM_EDIT + 1)
                #define IDM_EDIT_COPY        (IDM_EDIT + 2)
                #define IDM_EDIT_COPY_BINARY (IDM_EDIT + 48)
                #define IDM_EDIT_CUT_BINARY  (IDM_EDIT + 49)
                

                leading to these "equivalent"s in the PythonScript code:

                IDM_EDIT_COPY = 42002; IDM_EDIT_CUT = 42001; IDM_EDIT_COPY_BINARY = 42048; IDM_EDIT_CUT_BINARY = 42049

                Another interesting thing with the script’s execution is that if you don’t allow Notepad++ to process the Ctrl+c keycombo as a WM_KEYDOWN event, you’ll get an “ETX” (hex code = 0x03) character in your document at the caret position. This occurs because, with Notepad++ ignoring the Ctrl+c as a command, it thinks that you want to embed a control character with value “3” into the doc (it gets a WM_CHAR message to that effect, which it would normally filter out on its own). So… in the case of preventing copying data, we have to remove any Ctrl+c or Ctrl+x characters that might get inserted; we do this by processing the WM_CHAR message and filtering those as well.

                1 Reply Last reply Reply Quote 4
                • First post
                  Last post
                The Community of users of the Notepad++ text editor.
                Powered by NodeBB | Contributors