From 70a2a722fe89159724af4029db23e0265a05a52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johann=20Kl=C3=A4hn?= Date: Mon, 25 Feb 2013 10:49:17 +0100 Subject: [PATCH 1/3] Add GetBoolValue helper in vimsupport --- python/completers/all/identifier_completer.py | 4 ++-- python/vimsupport.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/python/completers/all/identifier_completer.py b/python/completers/all/identifier_completer.py index cd181fda..8aa013a2 100644 --- a/python/completers/all/identifier_completer.py +++ b/python/completers/all/identifier_completer.py @@ -87,8 +87,8 @@ class IdentifierCompleter( Completer ): def AddBufferIdentifiers( self ): filetype = vim.eval( "&filetype" ) filepath = vim.eval( "expand('%:p')" ) - collect_from_comments_and_strings = bool( int( vimsupport.GetVariableValue( - "g:ycm_collect_identifiers_from_comments_and_strings" ) ) ) + collect_from_comments_and_strings = vimsupport.GetBoolValue( + "g:ycm_collect_identifiers_from_comments_and_strings" ) if not filetype or not filepath: return diff --git a/python/vimsupport.py b/python/vimsupport.py index 5399d0eb..6760755f 100644 --- a/python/vimsupport.py +++ b/python/vimsupport.py @@ -44,7 +44,7 @@ def CurrentColumn(): def GetUnsavedBuffers(): def BufferModified( buffer_number ): to_eval = 'getbufvar({0}, "&mod")'.format( buffer_number ) - return bool( int( vim.eval( to_eval ) ) ) + return GetBoolValue( to_eval ) return ( x for x in vim.buffers if BufferModified( x.number ) ) @@ -78,3 +78,7 @@ def CurrentFiletypes(): def GetVariableValue( variable ): return vim.eval( variable ) + + +def GetBoolValue( variable ): + return bool( int( vim.eval( variable ) ) ) From 4b3e0a189528c284eeee519c4104b682ef80c464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johann=20Kl=C3=A4hn?= Date: Mon, 25 Feb 2013 10:49:51 +0100 Subject: [PATCH 2/3] Add code to ask user for confirmation in vimsupport --- python/vimsupport.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/python/vimsupport.py b/python/vimsupport.py index 6760755f..75e15218 100644 --- a/python/vimsupport.py +++ b/python/vimsupport.py @@ -59,6 +59,34 @@ def PostVimMessage( message ): .format( EscapeForVim( message ) ) ) +def PresentDialog( message, choices, default_choice_index = 0 ): + """Presents the user with a dialog where a choice can be made. + This will be a dialog for gvim users or a question in the message buffer + for vim users or if `set guioptions+=c` was used. + + choices is list of alternatives. + default_choice_index is the 0-based index of the default element + that will get choosen if the user hits . Use -1 for no default. + + PresentDialog will return a 0-based index into the list + or -1 if the dialog was dismissed by using , Ctrl-C, etc. + + See also: + :help confirm() in vim (Note that vim uses 1-based indexes) + + Example call: + PresentDialog("Is this a nice example?", ["Yes", "No", "May&be"]) + Is this a nice example? + [Y]es, (N)o, May(b)e:""" + to_eval = "confirm('{0}', '{1}', {2})".format( EscapeForVim( message ), + EscapeForVim( "\n" .join( choices ) ), default_choice_index + 1 ) + return int( vim.eval( to_eval ) ) - 1 + + +def Confirm( message ): + return bool( PresentDialog( message, [ "Ok", "Cancel" ] ) == 0 ) + + def EchoText( text ): def EchoLine( text ): vim.command( "echom '{0}'".format( EscapeForVim( text ) ) ) From e9cce297615f1632ce890146bfaa7cac0f27848f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johann=20Kl=C3=A4hn?= Date: Mon, 25 Feb 2013 10:58:04 +0100 Subject: [PATCH 3/3] Ask before loading .ycm_extra_conf.py files To prevent the execution of malicious code the new default is to ask the user before a `.ycm_extra_conf.py` file is loaded. This can be disabled using the option `g:ycm_confirm_extra_conf`. This commit introduces a helper class `FlagsModules` that keeps track of and caches the currently loaded modules. To introduce further criteria for a module look at `FlagsModules.ShouldLoad`. Also `:YcmDebugInfo` now lists the file that was used to determine the current set of flags. `Flags.ModuleForFile` could be used in a user-facing command that opens the `.ycm_extra_conf.py` corresponding to the current file. A second command could then force a reloding of this module via `Flags.ReloadModule`. --- README.md | 14 +++ plugin/youcompleteme.vim | 3 + python/completers/cpp/clang_completer.py | 3 +- python/completers/cpp/flags.py | 152 +++++++++++++++-------- 4 files changed, 122 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 8905074c..26924322 100644 --- a/README.md +++ b/README.md @@ -316,6 +316,10 @@ method in that module which should provide it with the information necessary to compile the current file. (You can also provide a path to a global `.ycm_extra_conf.py` file, which will be used as a fallback. See the Options section for more details.) +To prevent the execution of malicious code from a file you didn't write +YCM will ask once per module if it is safe to be loaded. +(This can be disabled. See the Options section.) + This system was designed this way so that the user can perform any arbitrary sequence of operations to produce a list of compilation flags YCM should hand @@ -651,6 +655,16 @@ Default: `''` let g:ycm_global_ycm_extra_conf = '' +### The `g:ycm_confirm_extra_conf` option + +When this option is set to `1` YCM will ask once per '.ycm_extra_conf.py' file +if it is safe to be loaded. This is to prevent execution of malicious code +from a '.ycm_extra_conf.py' file you didn't write. + +Default: `1` + + let g:ycm_confirm_extra_conf = 1 + ### The `g:ycm_semantic_triggers` option This option controls the character-based triggers for the various semantic diff --git a/plugin/youcompleteme.vim b/plugin/youcompleteme.vim index fd4c5bfa..1624aabd 100644 --- a/plugin/youcompleteme.vim +++ b/plugin/youcompleteme.vim @@ -103,6 +103,9 @@ let g:ycm_key_detailed_diagnostics = let g:ycm_global_ycm_extra_conf = \ get( g:, 'ycm_global_ycm_extra_conf', '' ) +let g:ycm_confirm_extra_conf = + \ get( g:, 'ycm_confirm_extra_conf', 1 ) + let g:ycm_semantic_triggers = \ get( g:, 'ycm_semantic_triggers', { \ 'c' : ['->', '.'], diff --git a/python/completers/cpp/clang_completer.py b/python/completers/cpp/clang_completer.py index 6f3a37ae..456f80e3 100644 --- a/python/completers/cpp/clang_completer.py +++ b/python/completers/cpp/clang_completer.py @@ -206,7 +206,8 @@ class ClangCompleter( Completer ): def DebugInfo( self ): filename = vim.current.buffer.name flags = self.flags.FlagsForFile( filename ) or [] - return 'Flags for {0}:\n{1}'.format( filename, list( flags ) ) + source = self.flags.ModuleForFile( filename ) + return 'Flags for {0} loaded from {1}:\n{2}'.format( filename, source, list( flags ) ) # TODO: make these functions module-local diff --git a/python/completers/cpp/flags.py b/python/completers/cpp/flags.py index 646995d9..7bec7ee3 100644 --- a/python/completers/cpp/flags.py +++ b/python/completers/cpp/flags.py @@ -29,32 +29,49 @@ YCM_EXTRA_CONF_FILENAME = '.ycm_extra_conf.py' NO_EXTRA_CONF_FILENAME_MESSAGE = ('No {0} file detected, so no compile flags ' 'are available. Thus no semantic support for C/C++/ObjC/ObjC++. See the ' 'docs for details.').format( YCM_EXTRA_CONF_FILENAME ) +CONFIRM_CONF_FILE_MESSAGE = 'Found {0}. Load?' GLOBAL_YCM_EXTRA_CONF_FILE = os.path.expanduser( vimsupport.GetVariableValue( "g:ycm_global_ycm_extra_conf" ) ) class Flags( object ): + """Keeps track of the flags necessary to compile a file. + The flags are loaded from user-created python files + (hereafter referred to as 'modules') that contain + a method FlagsForFile( filename ).""" def __init__( self ): # It's caches all the way down... self.flags_for_file = {} - self.flags_module_for_file = {} - self.flags_module_for_flags_module_file = {} + self.module_for_file = {} + self.modules = FlagsModules() self.special_clang_flags = _SpecialClangIncludes() self.no_extra_conf_file_warning_posted = False + def ModuleForFile( self, filename ): + """This will try all files returned by _FlagsModuleSourceFilesForFile in + order and return the filename of the first module that was allowed to load. + If no module was found or allowed to load, None is returned.""" + + if not self.module_for_file.has_key( filename ): + for flags_module_file in _FlagsModuleSourceFilesForFile( filename ): + if self.modules.Load( flags_module_file ): + self.module_for_file[ filename ] = flags_module_file + break + + return self.module_for_file.setdefault( filename ) def FlagsForFile( self, filename ): try: return self.flags_for_file[ filename ] except KeyError: - flags_module = self._FlagsModuleForFile( filename ) - if not flags_module: + module_file = self.ModuleForFile( filename ) + if not module_file: if not self.no_extra_conf_file_warning_posted: vimsupport.PostVimMessage( NO_EXTRA_CONF_FILENAME_MESSAGE ) self.no_extra_conf_file_warning_posted = True return None - results = flags_module.FlagsForFile( filename ) + results = self.modules[ module_file ].FlagsForFile( filename ) if not results.get( 'flags_ready', True ): return None @@ -66,58 +83,95 @@ class Flags( object ): self.flags_for_file[ filename ] = sanitized_flags return sanitized_flags - - def _FlagsModuleForFile( self, filename ): - try: - return self.flags_module_for_file[ filename ] - except KeyError: - flags_module_file = _FlagsModuleSourceFileForFile( filename ) - if not flags_module_file: - return None - - try: - flags_module = self.flags_module_for_flags_module_file[ - flags_module_file ] - except KeyError: - sys.path.insert( 0, _DirectoryOfThisScript() ) - flags_module = imp.load_source( _RandomName(), flags_module_file ) - del sys.path[ 0 ] - - self.flags_module_for_flags_module_file[ - flags_module_file ] = flags_module - - self.flags_module_for_file[ filename ] = flags_module - return flags_module + def ReloadModule( self, module_file ): + """Reloads a module file cleaning the flags cache for all files + associated with that module. Returns False if reloading failed + (for example due to the model not being loaded in the first place).""" + module_file = os.path.abspath(module_file) + if self.modules.Reload( module_file ): + for filename, module in self.module_for_file.iteritems(): + if module == module_file: + del self.flags_for_file[ filename ] + return True + return False +class FlagsModules( object ): + """Keeps track of modules. + Modules are loaded on-demand and cached in self.modules for quick access.""" + def __init__( self ): + self.modules = {} -def _FlagsModuleSourceFileForFile( filename ): - """For a given filename, finds its nearest YCM_EXTRA_CONF_FILENAME file that - will compute the flags necessary to compile the file. If no - YCM_EXTRA_CONF_FILENAME file could be found, try to use - GLOBAL_YCM_EXTRA_CONF_FILE instead. If that also fails, return None. - Uses the global ycm_extra_conf file if one is set.""" + def Disable( self, module_file ): + """Disables the loading of a module for the current session.""" + self.modules[ module_file ] = None - ycm_conf_file = None - parent_folder = os.path.dirname( filename ) - old_parent_folder = '' + @staticmethod + def ShouldLoad( module_file ): + """Checks if a module is safe to be loaded. + By default this will ask the user for confirmation.""" + if module_file == GLOBAL_YCM_EXTRA_CONF_FILE: + return True + if ( vimsupport.GetBoolValue( 'g:ycm_confirm_extra_conf' ) and + not vimsupport.Confirm( + CONFIRM_CONF_FILE_MESSAGE.format( module_file ) ) ): + return False + return True - while True: - current_file = os.path.join( parent_folder, YCM_EXTRA_CONF_FILENAME ) - if os.path.exists( current_file ): - ycm_conf_file = current_file - break + def Load( self, module_file, force = False ): + """Load and return the module contained in a file. + Using force = True the module will be loaded regardless + of the criteria in ShouldLoad. + This will return None if the module was not allowed to be loaded.""" + if not force: + if self.modules.has_key( module_file ): + return self.modules[ module_file ] - old_parent_folder = parent_folder - parent_folder = os.path.dirname( parent_folder ) - if parent_folder is old_parent_folder: - break + if not self.ShouldLoad( module_file ): + return self.Disable( module_file ) - if ( not ycm_conf_file and GLOBAL_YCM_EXTRA_CONF_FILE and - os.path.exists( GLOBAL_YCM_EXTRA_CONF_FILE ) ): - ycm_conf_file = GLOBAL_YCM_EXTRA_CONF_FILE + sys.path.insert( 0, _DirectoryOfThisScript() ) + module = imp.load_source( _RandomName(), module_file ) + del sys.path[ 0 ] - return ycm_conf_file + self.modules[ module_file ] = module + return module + + def Reload( self, module_file ): + """Reloads the given module. If it has not been loaded yet does nothing. + Note that the module will not be subject to the loading criteria again.""" + if self.modules.get( module_file ): + return self.Load( module_file, force = True ) + + def __getitem__( self, key ): + return self.Load( key ) + + +def _FlagsModuleSourceFilesForFile( filename ): + """For a given filename, search all parent folders for YCM_EXTRA_CONF_FILENAME + files that will compute the flags necessary to compile the file. + If GLOBAL_YCM_EXTRA_CONF_FILE exists it is returned as a fallback.""" + for folder in _PathsToAllParentFolders( filename ): + candidate = os.path.join( folder, YCM_EXTRA_CONF_FILENAME ) + if os.path.exists( candidate ): + yield candidate + if ( GLOBAL_YCM_EXTRA_CONF_FILE + and os.path.exists( GLOBAL_YCM_EXTRA_CONF_FILE ) ): + yield GLOBAL_YCM_EXTRA_CONF_FILE + + +def _PathsToAllParentFolders( filename ): + """Build a list of all parent folders of a file. + The neares files will be returned first. + Example: _PathsToAllParentFolders( '/home/user/projects/test.c' ) + [ '/home/user/projects', '/home/user', '/home', '/' ]""" + parent_folders = os.path.abspath( + os.path.dirname( filename ) ).split( os.path.sep ) + if not parent_folders[0]: + parent_folders[0] = os.path.sep + parent_folders = [ os.path.join( *parent_folders[:i + 1] ) + for i in xrange( len( parent_folders ) ) ] + return reversed( parent_folders ) def _RandomName():