Back to index

plone3  3.1.7
Public Member Functions | Public Attributes | Static Public Attributes
ExternalEditor.zopeedit.ExternalEditor Class Reference

List of all members.

Public Member Functions

def __init__
def __del__
def getEditorCommand
def launch
def putChanges
def lock
def unlock
def zopeRequest
def askRetryAfterError

Public Attributes

 input_file
 config
 metadata
 path
 ssl
 options
 keep_log
 content_file
 saved
 clean_up
 lock_token
 did_lock

Static Public Attributes

int did_lock = 0

Detailed Description

Definition at line 102 of file zopeedit.py.


Constructor & Destructor Documentation

def ExternalEditor.zopeedit.ExternalEditor.__init__ (   self,
  input_file 
)

Definition at line 106 of file zopeedit.py.

00106 
00107     def __init__(self, input_file):
00108         global log_file
00109         log_file = NamedTemporaryFile(suffix='-zopeedit-log.txt')
00110 
00111         self.input_file = input_file
00112 
00113         # Setup logging.
00114         logging.basicConfig(stream=log_file,
00115                             level=logging.DEBUG)
00116         logger.info('Opening %r.', input_file)
00117     
00118         try:
00119             # Read the configuration file
00120             if win32:
00121                 # Check the home dir first and then the program dir
00122                 config_path = os.path.expanduser('~\\ZopeEdit.ini')
00123 
00124                 # sys.path[0] might be library.zip!!!!
00125                 app_dir = sys.path[0]
00126                 if app_dir.lower().endswith('library.zip'):
00127                     app_dir = os.path.dirname(app_dir)
00128                 global_config = os.path.join(app_dir or '', 'ZopeEdit.ini')
00129 
00130                 if not os.path.exists(config_path):
00131                     logger.debug('Config file %r does not exist. '
00132                                  'Using global configuration file: %r.',
00133                                  config_path, global_config)
00134 
00135                     # Don't check for the existence of the global
00136                     # config file. It will be created anyway.
00137                     config_path = global_config
00138                 else:
00139                     logger.debug('Using user configuration file: %r.',
00140                                  config_path)
00141                     
00142             else:
00143                 config_path = os.path.expanduser('~/.zope-external-edit')
00144                 
00145             self.config = Configuration(config_path)
00146 
00147             # Open the input file and read the metadata headers
00148             in_f = open(input_file, 'rb')
00149             m = rfc822.Message(in_f)
00150 
00151             self.metadata = metadata = m.dict.copy()
00152                                
00153             # parse the incoming url
00154             scheme, self.host, self.path = urlparse(metadata['url'])[:3]
00155             self.ssl = scheme == 'https'
00156             
00157             # Get all configuration options
00158             self.options = self.config.getAllOptions(
00159                                             metadata['meta_type'],
00160                                             metadata.get('content_type',''),
00161                                             self.host)
00162 
00163             # Should we keep the log file?
00164             self.keep_log = int(self.options.get('keep_log', 0))
00165 
00166             # Write the body of the input file to a separate file
00167             if int(self.options.get('long_file_name', 1)):
00168                 sep = self.options.get('file_name_separator', ',')
00169                 content_file = urllib.unquote('-%s%s' % (self.host, self.path))
00170                 content_file = content_file.replace(
00171                     '/', sep).replace(':',sep).replace(' ','_')
00172             else:
00173                 content_file = '-' + urllib.unquote(self.path.split('/')[-1])
00174                 
00175             extension = self.options.get('extension')
00176             if extension and not content_file.endswith(extension):
00177                 content_file = content_file + extension
00178             
00179             if self.options.has_key('temp_dir'):
00180                 while 1:
00181                     temp = os.path.expanduser(self.options['temp_dir'])
00182                     temp = os.tempnam(temp)
00183                     content_file = '%s%s' % (temp, content_file)
00184                     if not os.path.exists(content_file):
00185                         break
00186             else:
00187                 content_file = mktemp(content_file)
00188                 
00189             logger.debug('Destination filename will be: %r.', content_file)
00190             
00191             body_f = open(content_file, 'wb')
00192             shutil.copyfileobj(in_f, body_f)
00193             self.content_file = content_file
00194             self.saved = 1
00195             in_f.close()
00196             body_f.close()
00197             self.clean_up = int(self.options.get('cleanup_files', 1))
00198             if self.clean_up: 
00199                 try:
00200                     logger.debug('Cleaning up %r.', input_file)
00201                     os.remove(input_file)
00202                 except OSError:
00203                     logger.exception('Failed to clean up %r.', input_file)
00204                     pass # Sometimes we aren't allowed to delete it
00205             
00206             if self.ssl:
00207                 # See if ssl is available
00208                 try:
00209                     from socket import ssl
00210                 except ImportError:
00211                     fatalError('SSL support is not available on this system. '
00212                                'Make sure openssl is installed '
00213                                'and reinstall Python.')
00214             self.lock_token = None
00215             self.did_lock = 0
00216         except:
00217             # for security, always delete the input file even if
00218             # a fatal error occurs, unless explicitly stated otherwise
00219             # in the config file
00220             if getattr(self, 'clean_up', 1):
00221                 try:
00222                     exc, exc_data = sys.exc_info()[:2]
00223                     os.remove(input_file)
00224                 except OSError:
00225                     # Sometimes we aren't allowed to delete it
00226                     raise exc, exc_data
00227             raise
        

Definition at line 228 of file zopeedit.py.

00228 
00229     def __del__(self):
00230         if getattr(self, 'clean_up', 1) and hasattr(self, 'content_file'):
00231             # for security we always delete the files by default
00232             try:
00233                 os.remove(self.content_file)
00234             except OSError:
00235                 logger.exception('Failed to clean up %r', self.content_file)
00236                 pass     
00237 
00238         if self.did_lock:
00239             # Try not to leave dangling locks on the server
00240             try:
00241                 self.unlock(interactive=0)
00242             except:
00243                 logger.exception('Failure during unlock.')
00244 
00245         if getattr(self, 'keep_log', 0):
00246             if log_file is not None:
00247                 base = getattr(self, 'content_file', '')
00248                 if not base:
00249                     base = getattr(self, 'input_file', 'noname')
00250                 base = os.path.basename(base)
00251                 fname = mktemp(suffix='-zopeedit-log.txt',
00252                                prefix='%s-' % base)
00253                 bkp_f = open(fname, 'wb')
00254 
00255                 # Copy the log file to a backup file.
00256                 log_file.seek(0)
00257                 shutil.copyfileobj(log_file, bkp_f)
            

Here is the call graph for this function:


Member Function Documentation

def ExternalEditor.zopeedit.ExternalEditor.askRetryAfterError (   self,
  response,
  operation,
  message = '' 
)
Dumps response data

Definition at line 629 of file zopeedit.py.

00629 
00630     def askRetryAfterError(self, response, operation, message=''):
00631         """Dumps response data"""
00632         if not message \
00633            and response.getheader('Bobo-Exception-Type') is not None:
00634             message = '%s: %s' % (response.getheader('Bobo-Exception-Type'),
00635                                   response.getheader('Bobo-Exception-Value'))
00636         return askRetryCancel('%s:\n%d %s\n%s' % (operation, response.status, 
00637                                                response.reason, message))

Here is the call graph for this function:

Here is the caller graph for this function:

Return the editor command

Definition at line 258 of file zopeedit.py.

00258 
00259     def getEditorCommand(self):
00260         """Return the editor command"""
00261         editor = self.options.get('editor')
00262         
00263         if win32 and editor is None:
00264             from _winreg import HKEY_CLASSES_ROOT, OpenKeyEx, \
00265                                 QueryValueEx, EnumKey
00266             from win32api import FindExecutable, ExpandEnvironmentStrings
00267 
00268             # Find editor application based on mime type and extension
00269             content_type = self.metadata.get('content_type')
00270             extension = self.options.get('extension')
00271 
00272             logger.debug('Have content type: %r, extension: %r',
00273                          content_type, extension)
00274             
00275             if content_type:
00276                 # Search registry for the extension by MIME type
00277                 try:
00278                     key = 'MIME\\Database\\Content Type\\%s' % content_type
00279                     key = OpenKeyEx(HKEY_CLASSES_ROOT, key)
00280                     extension, nil = QueryValueEx(key, 'Extension')
00281                     logger.debug('Registry has extension %r for '
00282                                  'content type %r',
00283                                  extension, content_type)
00284                 except EnvironmentError:
00285                     pass
00286             
00287             if extension is None:
00288                 url = self.metadata['url']
00289                 dot = url.rfind('.')
00290 
00291                 if dot != -1 and dot > url.rfind('/'):
00292                     extension = url[dot:]
00293 
00294                     logger.debug('Extracted extension from url: %r',
00295                                  extension)
00296 
00297             classname = editor = None
00298             if extension is not None:
00299                 try:
00300                     key = OpenKeyEx(HKEY_CLASSES_ROOT, extension)
00301                     classname, nil = QueryValueEx(key, None)
00302                     logger.debug('ClassName for extension %r is: %r',
00303                                  extension, classname)
00304                 except EnvironmentError:
00305                     classname = None
00306 
00307             if classname is not None:
00308                 try:
00309                     # Look for Edit action in registry
00310                     key = OpenKeyEx(HKEY_CLASSES_ROOT, 
00311                                     classname+'\\Shell\\Edit\\Command')
00312                     editor, nil = QueryValueEx(key, None)
00313                     logger.debug('Edit action for %r is: %r',
00314                                  classname, editor)
00315                 except EnvironmentError:
00316                     pass
00317 
00318             if classname is not None and editor is None:
00319                 logger.debug('Could not find Edit action for %r. '
00320                              'Brute-force enumeration.', classname)
00321                 # Enumerate the actions looking for one
00322                 # starting with 'Edit'
00323                 try:
00324                     key = OpenKeyEx(HKEY_CLASSES_ROOT, 
00325                                     classname+'\\Shell')
00326                     index = 0
00327                     while 1:
00328                         try:
00329                             subkey = EnumKey(key, index)
00330                             index += 1
00331                             if str(subkey).lower().startswith('edit'):
00332                                 subkey = OpenKeyEx(key, subkey + '\\Command')
00333                                 editor, nil = QueryValueEx(subkey, 
00334                                                            None)
00335                             if editor is None:
00336                                 continue
00337                             logger.debug('Found action %r for %r. '
00338                                          'Command will be: %r',
00339                                          subkey, classname, editor)
00340                         except EnvironmentError:
00341                             break
00342                 except EnvironmentError:
00343                     pass
00344 
00345             if classname is not None and editor is None:
00346                 try:
00347                     # Look for Open action in registry
00348                     key = OpenKeyEx(HKEY_CLASSES_ROOT, 
00349                                     classname+'\\Shell\\Open\\Command')
00350                     editor, nil = QueryValueEx(key, None)
00351                     logger.debug('Open action for %r has command: %r. ',
00352                                  classname, editor)
00353                 except EnvironmentError:
00354                     pass
00355 
00356             if editor is None:
00357                 try:
00358                     nil, editor = FindExecutable(self.content_file, '')
00359                     logger.debug('Executable for %r is: %r. ',
00360                                  self.content_file, editor)
00361                 except pywintypes.error:
00362                     pass
00363             
00364             # Don't use IE as an "editor"
00365             if editor is not None and editor.find('\\iexplore.exe') != -1:
00366                 logger.debug('Found iexplore.exe. Skipping.')
00367                 editor = None
00368 
00369             if editor is not None:            
00370                 return ExpandEnvironmentStrings(editor)
00371 
00372         if editor is None:
00373             fatalError('No editor was found for that object.\n'
00374                        'Specify an editor in the configuration file:\n'
00375                        '(%s)' % self.config.path)
00376 
00377         return editor
        

Here is the call graph for this function:

Here is the caller graph for this function:

Launch external editor

Definition at line 378 of file zopeedit.py.

00378 
00379     def launch(self):
00380         """Launch external editor"""
00381         use_locks = int(self.options.get('use_locks', 0))
00382         if use_locks and self.metadata.get('lock-token'):
00383             # A lock token came down with the data, so the object is
00384             # already locked, see if we can borrow the lock
00385             if (int(self.options.get('always_borrow_locks', 0))
00386                 or self.metadata.get('borrow_lock')
00387                 or askYesNo('This object is already locked by you in another'
00388                             ' session.\n Do you want to borrow this lock'
00389                             ' and continue?')):
00390                 self.lock_token = 'opaquelocktoken:%s' \
00391                                   % self.metadata['lock-token']
00392             else:
00393                 sys.exit()            
00394         
00395         save_interval = float(self.options.get('save_interval'))
00396         last_mtime = os.path.getmtime(self.content_file)
00397         command = self.getEditorCommand()
00398 
00399         # Extract the executable name from the command
00400         if win32:
00401             if command.find('\\') != -1:
00402                 bin = re.search(r'\\([^\.\\]+)\.exe', command.lower())
00403                 if bin is not None:
00404                     bin = bin.group(1)
00405             else:
00406                 bin = command.lower().strip()
00407         else:
00408             bin = command
00409 
00410         logger.debug('Command %r, will use %r', command, bin)
00411 
00412         if bin is not None:
00413             # Try to load the plugin for this editor
00414             try:
00415                 module = 'Plugins.%s' % bin
00416                 Plugin = __import__(module, globals(), locals(), 
00417                                     ('EditorProcess',))
00418                 editor = Plugin.EditorProcess(self.content_file)
00419                 logger.debug('Launching Plugin %r with: %r',
00420                              Plugin, self.content_file)
00421             except (ImportError, AttributeError):
00422                 bin = None
00423 
00424         if bin is None: 
00425             # Use the standard EditorProcess class for this editor
00426             if win32:
00427                 file_insert = '%1'
00428             else:
00429                 file_insert = '$1'
00430                 
00431             if command.find(file_insert) > -1:
00432                 command = command.replace(file_insert, self.content_file)
00433             else:
00434                 command = '%s %s' % (command, self.content_file)
00435 
00436             logger.debug('Launching EditorProcess with: %r', command)
00437             editor = EditorProcess(command)
00438             
00439         launch_success = editor.isAlive()
00440         
00441         if use_locks:
00442             self.lock()
00443 
00444         final_loop = 0
00445            
00446         while 1:
00447             if not final_loop:
00448                 editor.wait(save_interval or 2)
00449 
00450             mtime = os.path.getmtime(self.content_file)
00451 
00452             if mtime != last_mtime:
00453                 if save_interval or final_loop:
00454                     launch_success = 1 # handle very short editing sessions
00455                     self.saved = self.putChanges()
00456                     last_mtime = mtime
00457 
00458             if not editor.isAlive():
00459                 if final_loop:
00460                     break
00461                 else:
00462                     # Go through the loop one final time for good measure.
00463                     # Our editor's isAlive method may itself *block* during a
00464                     # save operation (seen in COM calls, which seem to
00465                     # respond asynchronously until they don't) and subsequently
00466                     # return false, but the editor may have actually saved the
00467                     # file to disk while the call blocked.  We want to catch
00468                     # any changes that happened during a blocking isAlive call.
00469                     final_loop = 1
00470 
00471         if not launch_success:
00472             fatalError('Editor did not launch properly.\n'
00473                        'External editor lost connection '
00474                        'to editor process.\n'
00475                        '(%s)' % command)
00476         
00477         if use_locks:
00478             self.unlock()
00479         
00480         if not self.saved \
00481            and askYesNo('File not saved to Zope.\nReopen local copy?'):
00482             self.launch()
        

Here is the call graph for this function:

Here is the caller graph for this function:

Apply a webdav lock to the object in Zope

Definition at line 515 of file zopeedit.py.

00515 
00516     def lock(self):
00517         """Apply a webdav lock to the object in Zope"""
00518         if self.lock_token is not None:
00519             return 0 # Already have a lock token
00520         
00521         headers = {'Content-Type':'text/xml; charset="utf-8"',
00522                    'Timeout':'infinite',
00523                    'Depth':'infinity',
00524                   }
00525         body = ('<?xml version="1.0" encoding="utf-8"?>\n'
00526                 '<d:lockinfo xmlns:d="DAV:">\n'
00527                 '  <d:lockscope><d:exclusive/></d:lockscope>\n'
00528                 '  <d:locktype><d:write/></d:locktype>\n'
00529                 '  <d:depth>infinity</d:depth>\n'
00530                 '  <d:owner>\n' 
00531                 '  <d:href>Zope External Editor</d:href>\n'
00532                 '  </d:owner>\n'
00533                 '</d:lockinfo>'
00534                 )
00535         
00536         response = self.zopeRequest('LOCK', headers, body)
00537         
00538         if response.status / 100 == 2:
00539             # We got our lock, extract the lock token and return it
00540             reply = response.read()
00541             token_start = reply.find('>opaquelocktoken:')
00542             token_end = reply.find('<', token_start)
00543             if token_start > 0 and token_end > 0:
00544                 self.lock_token = reply[token_start+1:token_end]
00545                 self.did_lock = 1
00546         else:
00547             # We can't lock her sir!
00548             if response.status == 423:
00549                 message = '(object already locked)'
00550             else:
00551                 message = ''
00552                 
00553             if self.askRetryAfterError(response, 
00554                                        'Lock request failed', 
00555                                        message):
00556                 self.lock()
00557             else:
00558                 self.did_lock = 0
00559         return self.did_lock
                    

Here is the call graph for this function:

Here is the caller graph for this function:

Save changes to the file back to Zope

Definition at line 483 of file zopeedit.py.

00483 
00484     def putChanges(self):
00485         """Save changes to the file back to Zope"""
00486         if int(self.options.get('use_locks', 0)) and self.lock_token is None:
00487             # We failed to get a lock initially, so try again before saving
00488             if not self.lock():
00489                 # Confirm save without lock
00490                 if not askYesNo('Could not acquire lock. '
00491                                 'Attempt to save to Zope anyway?'):
00492                     return 0
00493             
00494         f = open(self.content_file, 'rb')
00495         body = f.read()
00496         f.close()
00497         headers = {'Content-Type': 
00498                    self.metadata.get('content_type', 'text/plain')}
00499         
00500         if self.lock_token is not None:
00501             headers['If'] = '<%s> (<%s>)' % (self.path, self.lock_token)
00502         
00503         response = self.zopeRequest('PUT', headers, body)
00504         del body # Don't keep the body around longer then we need to
00505 
00506         if response.status / 100 != 2:
00507             # Something went wrong
00508             if self.askRetryAfterError(response, 
00509                                        'Could not save to Zope.\n'
00510                                        'Error occurred during HTTP put'):
00511                 return self.putChanges()
00512             else:
00513                 return 0
00514         return 1
    

Here is the call graph for this function:

Here is the caller graph for this function:

def ExternalEditor.zopeedit.ExternalEditor.unlock (   self,
  interactive = 1 
)
Remove webdav lock from edited zope object

Definition at line 560 of file zopeedit.py.

00560 
00561     def unlock(self, interactive=1):
00562         """Remove webdav lock from edited zope object"""
00563         if not self.did_lock or self.lock_token is None:
00564             return 0 # nothing to do
00565             
00566         headers = {'Lock-Token':self.lock_token}
00567         response = self.zopeRequest('UNLOCK', headers)
00568         
00569         if interactive and response.status / 100 != 2:
00570             # Captain, she's still locked!
00571             if self.askRetryAfterError(response, 'Unlock request failed'):
00572                 self.unlock()
00573             else:
00574                 self.did_lock = 0
00575         else:
00576             self.did_lock = 1
00577             self.lock_token = None
00578         return self.did_lock
        

Here is the call graph for this function:

Here is the caller graph for this function:

def ExternalEditor.zopeedit.ExternalEditor.zopeRequest (   self,
  method,
  headers = {},
  body = '' 
)
Send a request back to Zope

Definition at line 579 of file zopeedit.py.

00579 
00580     def zopeRequest(self, method, headers={}, body=''):
00581         """Send a request back to Zope"""
00582         try:
00583             if self.ssl:
00584                 h = HTTPSConnection(self.host)
00585             else:
00586                 h = HTTPConnection(self.host)
00587 
00588             h.putrequest(method, self.path)
00589             h.putheader('User-Agent', 'Zope External Editor/%s' % __version__)
00590             h.putheader('Connection', 'close')
00591 
00592             for header, value in headers.items():
00593                 h.putheader(header, value)
00594 
00595             h.putheader("Content-Length", str(len(body)))
00596 
00597             if self.metadata.get('auth','').lower().startswith('basic'):
00598                 h.putheader("Authorization", self.metadata['auth'])
00599 
00600             if self.metadata.get('cookie'):
00601                 h.putheader("Cookie", self.metadata['cookie'])
00602 
00603             h.endheaders()
00604             h.send(body)
00605             return h.getresponse()
00606         except:
00607             # On error return a null response with error info
00608             class NullResponse:                
00609                 def getheader(self, n, d=None):
00610                     return d
00611                     
00612                 def read(self): 
00613                     return '(No Response From Server)'
00614             
00615             response = NullResponse()
00616             response.reason = sys.exc_info()[1]
00617             
00618             try:
00619                 response.status, response.reason = response.reason
00620             except ValueError:
00621                 response.status = 0
00622             
00623             if response.reason == 'EOF occurred in violation of protocol':
00624                 # Ignore this protocol error as a workaround for
00625                 # broken ssl server implementations
00626                 response.status = 200
00627                 
00628             return response
            

Here is the caller graph for this function:


Member Data Documentation

Definition at line 196 of file zopeedit.py.

Definition at line 144 of file zopeedit.py.

Definition at line 192 of file zopeedit.py.

Definition at line 104 of file zopeedit.py.

Definition at line 214 of file zopeedit.py.

Definition at line 110 of file zopeedit.py.

Definition at line 163 of file zopeedit.py.

Definition at line 213 of file zopeedit.py.

Definition at line 150 of file zopeedit.py.

Definition at line 157 of file zopeedit.py.

Definition at line 153 of file zopeedit.py.

Definition at line 193 of file zopeedit.py.

Definition at line 154 of file zopeedit.py.


The documentation for this class was generated from the following file: