Back to index

plone3  3.1.7
zopeedit.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 ##############################################################################
00003 #
00004 # Copyright (c) 2001, 2002 Zope Corporation and Contributors.
00005 # All Rights Reserved.
00006 # 
00007 # This software is subject to the provisions of the Zope Public License,
00008 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
00009 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00010 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00011 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00012 # FOR A PARTICULAR PURPOSE.
00013 # 
00014 ##############################################################################
00015 """Zope External Editor Helper Application by Casey Duncan
00016 
00017 $Id: zopeedit.py 71558 2006-12-15 11:19:41Z wichert $"""
00018 
00019 __version__ = '0.9.3'
00020 
00021 import sys
00022 
00023 win32 = sys.platform == 'win32'
00024 
00025 if win32:
00026     # import pywin32 stuff first so it never looks into system32
00027     import pythoncom, pywintypes
00028     
00029     # prevent warnings from being turned into errors by py2exe
00030     import warnings
00031     warnings.filterwarnings('ignore')
00032 
00033 import os, re
00034 import rfc822
00035 import traceback
00036 import logging
00037 import urllib
00038 import shutil
00039 
00040 from tempfile import mktemp, NamedTemporaryFile
00041 from ConfigParser import ConfigParser
00042 from httplib import HTTPConnection, HTTPSConnection
00043 from urlparse import urlparse
00044 
00045 logger = logging.getLogger('zopeedit')
00046 log_file = None
00047 
00048 class Configuration:
00049     
00050     def __init__(self, path):
00051         # Create/read config file on instantiation
00052         self.path = path
00053         if not os.path.exists(path):
00054             f = open(path, 'w')
00055             f.write(default_configuration)
00056             f.close()
00057         self.changed = 0
00058         self.config = ConfigParser()
00059         self.config.readfp(open(path))
00060             
00061     def save(self):
00062         """Save config options to disk"""
00063         self.config.write(open(self.path, 'w'))
00064         self.changed = 0
00065             
00066     def set(self, section, option, value):
00067         self.config.set(section, option, value)
00068         self.changed = 1
00069     
00070     def __getattr__(self, name):
00071         # Delegate to the ConfigParser instance
00072         return getattr(self.config, name)
00073         
00074     def getAllOptions(self, meta_type, content_type, host_domain):
00075         """Return a dict of all applicable options for the
00076            given meta_type, content_type and host_domain
00077         """
00078         opt = {}
00079         sep = content_type.find('/')
00080         general_type = '%s/*' % content_type[:sep]
00081         
00082         # Divide up the domains segments and create a
00083         # list of domains from the bottom up
00084         host_domain = host_domain.split('.')
00085         domains = []
00086         for i in range(len(host_domain)):
00087             domains.append('domain:%s' % '.'.join(host_domain[i:]))
00088         domains.reverse()
00089 
00090         sections = ['general']
00091         sections.extend(domains)
00092         sections.append('meta-type:%s' % meta_type)
00093         sections.append('content-type:%s' % general_type)
00094         sections.append('content-type:%s' % content_type)
00095         
00096         for section in sections:
00097             if self.config.has_section(section):
00098                 for option in self.config.options(section):
00099                     opt[option] = self.config.get(section, option)
00100         return opt
00101         
00102 class ExternalEditor:
00103     
00104     did_lock = 0
00105     
00106     def __init__(self, input_file):
00107         global log_file
00108         log_file = NamedTemporaryFile(suffix='-zopeedit-log.txt')
00109 
00110         self.input_file = input_file
00111 
00112         # Setup logging.
00113         logging.basicConfig(stream=log_file,
00114                             level=logging.DEBUG)
00115         logger.info('Opening %r.', input_file)
00116     
00117         try:
00118             # Read the configuration file
00119             if win32:
00120                 # Check the home dir first and then the program dir
00121                 config_path = os.path.expanduser('~\\ZopeEdit.ini')
00122 
00123                 # sys.path[0] might be library.zip!!!!
00124                 app_dir = sys.path[0]
00125                 if app_dir.lower().endswith('library.zip'):
00126                     app_dir = os.path.dirname(app_dir)
00127                 global_config = os.path.join(app_dir or '', 'ZopeEdit.ini')
00128 
00129                 if not os.path.exists(config_path):
00130                     logger.debug('Config file %r does not exist. '
00131                                  'Using global configuration file: %r.',
00132                                  config_path, global_config)
00133 
00134                     # Don't check for the existence of the global
00135                     # config file. It will be created anyway.
00136                     config_path = global_config
00137                 else:
00138                     logger.debug('Using user configuration file: %r.',
00139                                  config_path)
00140                     
00141             else:
00142                 config_path = os.path.expanduser('~/.zope-external-edit')
00143                 
00144             self.config = Configuration(config_path)
00145 
00146             # Open the input file and read the metadata headers
00147             in_f = open(input_file, 'rb')
00148             m = rfc822.Message(in_f)
00149 
00150             self.metadata = metadata = m.dict.copy()
00151                                
00152             # parse the incoming url
00153             scheme, self.host, self.path = urlparse(metadata['url'])[:3]
00154             self.ssl = scheme == 'https'
00155             
00156             # Get all configuration options
00157             self.options = self.config.getAllOptions(
00158                                             metadata['meta_type'],
00159                                             metadata.get('content_type',''),
00160                                             self.host)
00161 
00162             # Should we keep the log file?
00163             self.keep_log = int(self.options.get('keep_log', 0))
00164 
00165             # Write the body of the input file to a separate file
00166             if int(self.options.get('long_file_name', 1)):
00167                 sep = self.options.get('file_name_separator', ',')
00168                 content_file = urllib.unquote('-%s%s' % (self.host, self.path))
00169                 content_file = content_file.replace(
00170                     '/', sep).replace(':',sep).replace(' ','_')
00171             else:
00172                 content_file = '-' + urllib.unquote(self.path.split('/')[-1])
00173                 
00174             extension = self.options.get('extension')
00175             if extension and not content_file.endswith(extension):
00176                 content_file = content_file + extension
00177             
00178             if self.options.has_key('temp_dir'):
00179                 while 1:
00180                     temp = os.path.expanduser(self.options['temp_dir'])
00181                     temp = os.tempnam(temp)
00182                     content_file = '%s%s' % (temp, content_file)
00183                     if not os.path.exists(content_file):
00184                         break
00185             else:
00186                 content_file = mktemp(content_file)
00187                 
00188             logger.debug('Destination filename will be: %r.', content_file)
00189             
00190             body_f = open(content_file, 'wb')
00191             shutil.copyfileobj(in_f, body_f)
00192             self.content_file = content_file
00193             self.saved = 1
00194             in_f.close()
00195             body_f.close()
00196             self.clean_up = int(self.options.get('cleanup_files', 1))
00197             if self.clean_up: 
00198                 try:
00199                     logger.debug('Cleaning up %r.', input_file)
00200                     os.remove(input_file)
00201                 except OSError:
00202                     logger.exception('Failed to clean up %r.', input_file)
00203                     pass # Sometimes we aren't allowed to delete it
00204             
00205             if self.ssl:
00206                 # See if ssl is available
00207                 try:
00208                     from socket import ssl
00209                 except ImportError:
00210                     fatalError('SSL support is not available on this system. '
00211                                'Make sure openssl is installed '
00212                                'and reinstall Python.')
00213             self.lock_token = None
00214             self.did_lock = 0
00215         except:
00216             # for security, always delete the input file even if
00217             # a fatal error occurs, unless explicitly stated otherwise
00218             # in the config file
00219             if getattr(self, 'clean_up', 1):
00220                 try:
00221                     exc, exc_data = sys.exc_info()[:2]
00222                     os.remove(input_file)
00223                 except OSError:
00224                     # Sometimes we aren't allowed to delete it
00225                     raise exc, exc_data
00226             raise
00227         
00228     def __del__(self):
00229         if getattr(self, 'clean_up', 1) and hasattr(self, 'content_file'):
00230             # for security we always delete the files by default
00231             try:
00232                 os.remove(self.content_file)
00233             except OSError:
00234                 logger.exception('Failed to clean up %r', self.content_file)
00235                 pass     
00236 
00237         if self.did_lock:
00238             # Try not to leave dangling locks on the server
00239             try:
00240                 self.unlock(interactive=0)
00241             except:
00242                 logger.exception('Failure during unlock.')
00243 
00244         if getattr(self, 'keep_log', 0):
00245             if log_file is not None:
00246                 base = getattr(self, 'content_file', '')
00247                 if not base:
00248                     base = getattr(self, 'input_file', 'noname')
00249                 base = os.path.basename(base)
00250                 fname = mktemp(suffix='-zopeedit-log.txt',
00251                                prefix='%s-' % base)
00252                 bkp_f = open(fname, 'wb')
00253 
00254                 # Copy the log file to a backup file.
00255                 log_file.seek(0)
00256                 shutil.copyfileobj(log_file, bkp_f)
00257             
00258     def getEditorCommand(self):
00259         """Return the editor command"""
00260         editor = self.options.get('editor')
00261         
00262         if win32 and editor is None:
00263             from _winreg import HKEY_CLASSES_ROOT, OpenKeyEx, \
00264                                 QueryValueEx, EnumKey
00265             from win32api import FindExecutable, ExpandEnvironmentStrings
00266 
00267             # Find editor application based on mime type and extension
00268             content_type = self.metadata.get('content_type')
00269             extension = self.options.get('extension')
00270 
00271             logger.debug('Have content type: %r, extension: %r',
00272                          content_type, extension)
00273             
00274             if content_type:
00275                 # Search registry for the extension by MIME type
00276                 try:
00277                     key = 'MIME\\Database\\Content Type\\%s' % content_type
00278                     key = OpenKeyEx(HKEY_CLASSES_ROOT, key)
00279                     extension, nil = QueryValueEx(key, 'Extension')
00280                     logger.debug('Registry has extension %r for '
00281                                  'content type %r',
00282                                  extension, content_type)
00283                 except EnvironmentError:
00284                     pass
00285             
00286             if extension is None:
00287                 url = self.metadata['url']
00288                 dot = url.rfind('.')
00289 
00290                 if dot != -1 and dot > url.rfind('/'):
00291                     extension = url[dot:]
00292 
00293                     logger.debug('Extracted extension from url: %r',
00294                                  extension)
00295 
00296             classname = editor = None
00297             if extension is not None:
00298                 try:
00299                     key = OpenKeyEx(HKEY_CLASSES_ROOT, extension)
00300                     classname, nil = QueryValueEx(key, None)
00301                     logger.debug('ClassName for extension %r is: %r',
00302                                  extension, classname)
00303                 except EnvironmentError:
00304                     classname = None
00305 
00306             if classname is not None:
00307                 try:
00308                     # Look for Edit action in registry
00309                     key = OpenKeyEx(HKEY_CLASSES_ROOT, 
00310                                     classname+'\\Shell\\Edit\\Command')
00311                     editor, nil = QueryValueEx(key, None)
00312                     logger.debug('Edit action for %r is: %r',
00313                                  classname, editor)
00314                 except EnvironmentError:
00315                     pass
00316 
00317             if classname is not None and editor is None:
00318                 logger.debug('Could not find Edit action for %r. '
00319                              'Brute-force enumeration.', classname)
00320                 # Enumerate the actions looking for one
00321                 # starting with 'Edit'
00322                 try:
00323                     key = OpenKeyEx(HKEY_CLASSES_ROOT, 
00324                                     classname+'\\Shell')
00325                     index = 0
00326                     while 1:
00327                         try:
00328                             subkey = EnumKey(key, index)
00329                             index += 1
00330                             if str(subkey).lower().startswith('edit'):
00331                                 subkey = OpenKeyEx(key, subkey + '\\Command')
00332                                 editor, nil = QueryValueEx(subkey, 
00333                                                            None)
00334                             if editor is None:
00335                                 continue
00336                             logger.debug('Found action %r for %r. '
00337                                          'Command will be: %r',
00338                                          subkey, classname, editor)
00339                         except EnvironmentError:
00340                             break
00341                 except EnvironmentError:
00342                     pass
00343 
00344             if classname is not None and editor is None:
00345                 try:
00346                     # Look for Open action in registry
00347                     key = OpenKeyEx(HKEY_CLASSES_ROOT, 
00348                                     classname+'\\Shell\\Open\\Command')
00349                     editor, nil = QueryValueEx(key, None)
00350                     logger.debug('Open action for %r has command: %r. ',
00351                                  classname, editor)
00352                 except EnvironmentError:
00353                     pass
00354 
00355             if editor is None:
00356                 try:
00357                     nil, editor = FindExecutable(self.content_file, '')
00358                     logger.debug('Executable for %r is: %r. ',
00359                                  self.content_file, editor)
00360                 except pywintypes.error:
00361                     pass
00362             
00363             # Don't use IE as an "editor"
00364             if editor is not None and editor.find('\\iexplore.exe') != -1:
00365                 logger.debug('Found iexplore.exe. Skipping.')
00366                 editor = None
00367 
00368             if editor is not None:            
00369                 return ExpandEnvironmentStrings(editor)
00370 
00371         if editor is None:
00372             fatalError('No editor was found for that object.\n'
00373                        'Specify an editor in the configuration file:\n'
00374                        '(%s)' % self.config.path)
00375 
00376         return editor
00377         
00378     def launch(self):
00379         """Launch external editor"""
00380         use_locks = int(self.options.get('use_locks', 0))
00381         if use_locks and self.metadata.get('lock-token'):
00382             # A lock token came down with the data, so the object is
00383             # already locked, see if we can borrow the lock
00384             if (int(self.options.get('always_borrow_locks', 0))
00385                 or self.metadata.get('borrow_lock')
00386                 or askYesNo('This object is already locked by you in another'
00387                             ' session.\n Do you want to borrow this lock'
00388                             ' and continue?')):
00389                 self.lock_token = 'opaquelocktoken:%s' \
00390                                   % self.metadata['lock-token']
00391             else:
00392                 sys.exit()            
00393         
00394         save_interval = float(self.options.get('save_interval'))
00395         last_mtime = os.path.getmtime(self.content_file)
00396         command = self.getEditorCommand()
00397 
00398         # Extract the executable name from the command
00399         if win32:
00400             if command.find('\\') != -1:
00401                 bin = re.search(r'\\([^\.\\]+)\.exe', command.lower())
00402                 if bin is not None:
00403                     bin = bin.group(1)
00404             else:
00405                 bin = command.lower().strip()
00406         else:
00407             bin = command
00408 
00409         logger.debug('Command %r, will use %r', command, bin)
00410 
00411         if bin is not None:
00412             # Try to load the plugin for this editor
00413             try:
00414                 module = 'Plugins.%s' % bin
00415                 Plugin = __import__(module, globals(), locals(), 
00416                                     ('EditorProcess',))
00417                 editor = Plugin.EditorProcess(self.content_file)
00418                 logger.debug('Launching Plugin %r with: %r',
00419                              Plugin, self.content_file)
00420             except (ImportError, AttributeError):
00421                 bin = None
00422 
00423         if bin is None: 
00424             # Use the standard EditorProcess class for this editor
00425             if win32:
00426                 file_insert = '%1'
00427             else:
00428                 file_insert = '$1'
00429                 
00430             if command.find(file_insert) > -1:
00431                 command = command.replace(file_insert, self.content_file)
00432             else:
00433                 command = '%s %s' % (command, self.content_file)
00434 
00435             logger.debug('Launching EditorProcess with: %r', command)
00436             editor = EditorProcess(command)
00437             
00438         launch_success = editor.isAlive()
00439         
00440         if use_locks:
00441             self.lock()
00442 
00443         final_loop = 0
00444            
00445         while 1:
00446             if not final_loop:
00447                 editor.wait(save_interval or 2)
00448 
00449             mtime = os.path.getmtime(self.content_file)
00450 
00451             if mtime != last_mtime:
00452                 if save_interval or final_loop:
00453                     launch_success = 1 # handle very short editing sessions
00454                     self.saved = self.putChanges()
00455                     last_mtime = mtime
00456 
00457             if not editor.isAlive():
00458                 if final_loop:
00459                     break
00460                 else:
00461                     # Go through the loop one final time for good measure.
00462                     # Our editor's isAlive method may itself *block* during a
00463                     # save operation (seen in COM calls, which seem to
00464                     # respond asynchronously until they don't) and subsequently
00465                     # return false, but the editor may have actually saved the
00466                     # file to disk while the call blocked.  We want to catch
00467                     # any changes that happened during a blocking isAlive call.
00468                     final_loop = 1
00469 
00470         if not launch_success:
00471             fatalError('Editor did not launch properly.\n'
00472                        'External editor lost connection '
00473                        'to editor process.\n'
00474                        '(%s)' % command)
00475         
00476         if use_locks:
00477             self.unlock()
00478         
00479         if not self.saved \
00480            and askYesNo('File not saved to Zope.\nReopen local copy?'):
00481             self.launch()
00482         
00483     def putChanges(self):
00484         """Save changes to the file back to Zope"""
00485         if int(self.options.get('use_locks', 0)) and self.lock_token is None:
00486             # We failed to get a lock initially, so try again before saving
00487             if not self.lock():
00488                 # Confirm save without lock
00489                 if not askYesNo('Could not acquire lock. '
00490                                 'Attempt to save to Zope anyway?'):
00491                     return 0
00492             
00493         f = open(self.content_file, 'rb')
00494         body = f.read()
00495         f.close()
00496         headers = {'Content-Type': 
00497                    self.metadata.get('content_type', 'text/plain')}
00498         
00499         if self.lock_token is not None:
00500             headers['If'] = '<%s> (<%s>)' % (self.path, self.lock_token)
00501         
00502         response = self.zopeRequest('PUT', headers, body)
00503         del body # Don't keep the body around longer then we need to
00504 
00505         if response.status / 100 != 2:
00506             # Something went wrong
00507             if self.askRetryAfterError(response, 
00508                                        'Could not save to Zope.\n'
00509                                        'Error occurred during HTTP put'):
00510                 return self.putChanges()
00511             else:
00512                 return 0
00513         return 1
00514     
00515     def lock(self):
00516         """Apply a webdav lock to the object in Zope"""
00517         if self.lock_token is not None:
00518             return 0 # Already have a lock token
00519         
00520         headers = {'Content-Type':'text/xml; charset="utf-8"',
00521                    'Timeout':'infinite',
00522                    'Depth':'infinity',
00523                   }
00524         body = ('<?xml version="1.0" encoding="utf-8"?>\n'
00525                 '<d:lockinfo xmlns:d="DAV:">\n'
00526                 '  <d:lockscope><d:exclusive/></d:lockscope>\n'
00527                 '  <d:locktype><d:write/></d:locktype>\n'
00528                 '  <d:depth>infinity</d:depth>\n'
00529                 '  <d:owner>\n' 
00530                 '  <d:href>Zope External Editor</d:href>\n'
00531                 '  </d:owner>\n'
00532                 '</d:lockinfo>'
00533                 )
00534         
00535         response = self.zopeRequest('LOCK', headers, body)
00536         
00537         if response.status / 100 == 2:
00538             # We got our lock, extract the lock token and return it
00539             reply = response.read()
00540             token_start = reply.find('>opaquelocktoken:')
00541             token_end = reply.find('<', token_start)
00542             if token_start > 0 and token_end > 0:
00543                 self.lock_token = reply[token_start+1:token_end]
00544                 self.did_lock = 1
00545         else:
00546             # We can't lock her sir!
00547             if response.status == 423:
00548                 message = '(object already locked)'
00549             else:
00550                 message = ''
00551                 
00552             if self.askRetryAfterError(response, 
00553                                        'Lock request failed', 
00554                                        message):
00555                 self.lock()
00556             else:
00557                 self.did_lock = 0
00558         return self.did_lock
00559                     
00560     def unlock(self, interactive=1):
00561         """Remove webdav lock from edited zope object"""
00562         if not self.did_lock or self.lock_token is None:
00563             return 0 # nothing to do
00564             
00565         headers = {'Lock-Token':self.lock_token}
00566         response = self.zopeRequest('UNLOCK', headers)
00567         
00568         if interactive and response.status / 100 != 2:
00569             # Captain, she's still locked!
00570             if self.askRetryAfterError(response, 'Unlock request failed'):
00571                 self.unlock()
00572             else:
00573                 self.did_lock = 0
00574         else:
00575             self.did_lock = 1
00576             self.lock_token = None
00577         return self.did_lock
00578         
00579     def zopeRequest(self, method, headers={}, body=''):
00580         """Send a request back to Zope"""
00581         try:
00582             if self.ssl:
00583                 h = HTTPSConnection(self.host)
00584             else:
00585                 h = HTTPConnection(self.host)
00586 
00587             h.putrequest(method, self.path)
00588             h.putheader('User-Agent', 'Zope External Editor/%s' % __version__)
00589             h.putheader('Connection', 'close')
00590 
00591             for header, value in headers.items():
00592                 h.putheader(header, value)
00593 
00594             h.putheader("Content-Length", str(len(body)))
00595 
00596             if self.metadata.get('auth','').lower().startswith('basic'):
00597                 h.putheader("Authorization", self.metadata['auth'])
00598 
00599             if self.metadata.get('cookie'):
00600                 h.putheader("Cookie", self.metadata['cookie'])
00601 
00602             h.endheaders()
00603             h.send(body)
00604             return h.getresponse()
00605         except:
00606             # On error return a null response with error info
00607             class NullResponse:                
00608                 def getheader(self, n, d=None):
00609                     return d
00610                     
00611                 def read(self): 
00612                     return '(No Response From Server)'
00613             
00614             response = NullResponse()
00615             response.reason = sys.exc_info()[1]
00616             
00617             try:
00618                 response.status, response.reason = response.reason
00619             except ValueError:
00620                 response.status = 0
00621             
00622             if response.reason == 'EOF occurred in violation of protocol':
00623                 # Ignore this protocol error as a workaround for
00624                 # broken ssl server implementations
00625                 response.status = 200
00626                 
00627             return response
00628             
00629     def askRetryAfterError(self, response, operation, message=''):
00630         """Dumps response data"""
00631         if not message \
00632            and response.getheader('Bobo-Exception-Type') is not None:
00633             message = '%s: %s' % (response.getheader('Bobo-Exception-Type'),
00634                                   response.getheader('Bobo-Exception-Value'))
00635         return askRetryCancel('%s:\n%d %s\n%s' % (operation, response.status, 
00636                                                response.reason, message))
00637 
00638 title = 'Zope External Editor'
00639 
00640 ## Platform specific declarations ##
00641 
00642 if win32:
00643     import Plugins # Assert dependancy
00644     from win32ui import MessageBox
00645     from win32process import CreateProcess, GetExitCodeProcess, STARTUPINFO
00646     from win32event import WaitForSingleObject
00647     from win32con import MB_OK, MB_OKCANCEL, MB_YESNO, MB_RETRYCANCEL, \
00648                          MB_SYSTEMMODAL, MB_ICONERROR, MB_ICONQUESTION, \
00649                          MB_ICONEXCLAMATION
00650 
00651     def errorDialog(message):
00652         MessageBox(message, title, MB_OK + MB_ICONERROR + MB_SYSTEMMODAL)
00653 
00654     def askRetryCancel(message):
00655         return MessageBox(message, title, 
00656                           MB_OK + MB_RETRYCANCEL + MB_ICONEXCLAMATION 
00657                           + MB_SYSTEMMODAL) == 4
00658 
00659     def askYesNo(message):
00660         return MessageBox(message, title, 
00661                           MB_OK + MB_YESNO + MB_ICONQUESTION +
00662                           MB_SYSTEMMODAL) == 6
00663 
00664     class EditorProcess:
00665         def __init__(self, command):
00666             """Launch editor process"""
00667             try:
00668                 logger.debug('CreateProcess: %r', command)
00669                 self.handle, nil, nil, nil = CreateProcess(None, command, None, 
00670                                                            None, 1, 0, None, 
00671                                                            None, STARTUPINFO())
00672             except pywintypes.error, e:
00673                 fatalError('Error launching editor process\n'
00674                            '(%s):\n%s' % (command, e[2]))
00675         def wait(self, timeout):
00676             """Wait for editor to exit or until timeout"""
00677             WaitForSingleObject(self.handle, int(timeout * 1000.0))
00678                 
00679         def isAlive(self):
00680             """Returns true if the editor process is still alive"""
00681             return GetExitCodeProcess(self.handle) == 259
00682 
00683 else: # Posix platform
00684     from time import sleep
00685     import re
00686 
00687     def has_tk():
00688         """Sets up a suitable tk root window if one has not
00689            already been setup. Returns true if tk is happy,
00690            false if tk throws an error (like its not available)"""
00691             # create a hidden root window to make Tk happy
00692         if not locals().has_key('tk_root'):
00693             try:
00694                 global tk_root
00695                 from Tkinter import Tk
00696                 tk_root = Tk()
00697                 tk_root.withdraw()
00698                 return 1
00699             except:
00700                 return 0
00701         return 1
00702 
00703     def errorDialog(message):
00704         """Error dialog box"""
00705         try:
00706             if has_tk():
00707                 from tkMessageBox import showerror
00708                 showerror(title, message)
00709                 has_tk()
00710         finally:
00711             print message
00712 
00713     def askRetryCancel(message):
00714         if has_tk():
00715             from tkMessageBox import askretrycancel
00716             r = askretrycancel(title, message)
00717             has_tk() # ugh, keeps tk happy
00718             return r
00719 
00720     def askYesNo(message):
00721         if has_tk():
00722             from tkMessageBox import askyesno
00723             r = askyesno(title, message)
00724             has_tk() # must...make...tk...happy
00725             return r
00726 
00727     class EditorProcess:
00728         def __init__(self, command):
00729             """Launch editor process"""
00730             # Prepare the command arguments, we use this regex to 
00731             # split on whitespace and properly handle quoting
00732             arg_re = r"""\s*([^'"]\S+)\s+|\s*"([^"]+)"\s*|\s*'([^']+)'\s*"""
00733             args = re.split(arg_re, command.strip())
00734             args = filter(None, args) # Remove empty elements
00735             self.pid = os.spawnvp(os.P_NOWAIT, args[0], args)
00736         
00737         def wait(self, timeout):
00738             """Wait for editor to exit or until timeout"""
00739             sleep(timeout)
00740                 
00741         def isAlive(self):
00742             """Returns true if the editor process is still alive"""
00743             try:
00744                 exit_pid, exit_status = os.waitpid(self.pid, os.WNOHANG)
00745             except OSError:
00746                 return 0
00747             else:
00748                 return exit_pid != self.pid
00749 
00750 def fatalError(message, exit=1):
00751     """Show error message and exit"""
00752     global log_file
00753     errorDialog('FATAL ERROR: %s' % message)
00754     # Write out debug info to a temp file
00755     debug_f = open(mktemp('-zopeedit-traceback.txt'), 'w')
00756     try:
00757         # Copy the log_file before it goes away on a fatalError.
00758         if log_file is not None:
00759             log_file.seek(0)
00760             shutil.copyfileobj(log_file, debug_f)
00761             print >> debug_f, '-' * 80
00762         traceback.print_exc(file=debug_f)
00763     finally:
00764         debug_f.close()
00765     if exit: 
00766         sys.exit(0)
00767 
00768 default_configuration = """\
00769 # Zope External Editor helper application configuration
00770 
00771 [general]
00772 # General configuration options
00773 
00774 # Uncomment and specify an editor value to override the editor
00775 # specified in the environment
00776 #editor = 
00777 
00778 # Automatic save interval, in seconds. Set to zero for
00779 # no auto save (save to Zope only on exit).
00780 save_interval = 1
00781 
00782 # Temporary file cleanup. Set to false for debugging or
00783 # to waste disk space. Note: setting this to false is a
00784 # security risk to the zope server
00785 cleanup_files = 1
00786 
00787 # Use WebDAV locking to prevent concurrent editing by
00788 # different users. Disable for single user use or for
00789 # better performance
00790 use_locks = 1
00791 
00792 # To suppress warnings about borrowing locks on objects
00793 # locked by you before you began editing you can
00794 # set this flag. This is useful for applications that
00795 # use server-side locking, like CMFStaging
00796 always_borrow_locks = 0
00797 
00798 # Specific settings by content-type or meta-type. Specific
00799 # settings override general options above. Content-type settings
00800 # override meta-type settings for the same option.
00801 
00802 [meta-type:DTML Document]
00803 extension=.dtml
00804 
00805 [meta-type:DTML Method]
00806 extension=.dtml
00807 
00808 [meta-type:Script (Python)]
00809 extension=.py
00810 
00811 [meta-type:Page Template]
00812 extension=.pt
00813 
00814 [meta-type:Z SQL Method]
00815 extension=.sql
00816 
00817 [content-type:text/plain]
00818 extension=.txt
00819 
00820 [content-type:text/html]
00821 extension=.html
00822 
00823 [content-type:text/xml]
00824 extension=.xml
00825 
00826 [content-type:text/css]
00827 extension=.css
00828 
00829 [content-type:text/javascript]
00830 extension=.js
00831 
00832 [content-type:image/*]
00833 editor=gimp
00834 
00835 [content-type:image/gif]
00836 extension=.gif
00837 
00838 [content-type:image/jpeg]
00839 extension=.jpg
00840 
00841 [content-type:image/png]
00842 extension=.png"""
00843 
00844 if __name__ == '__main__':
00845     try:
00846         args = sys.argv
00847         
00848         if '--version' in args or '-v' in args:
00849             credits = ('Zope External Editor %s\n'
00850                        'By Casey Duncan, Zope Corporation\n'
00851                        'http://www.zope.com/') % __version__
00852             if win32:
00853                 errorDialog(credits)
00854             else:
00855                 print credits
00856             sys.exit()
00857 
00858         input_file = sys.argv[1]
00859     except IndexError:
00860         fatalError('Input file name missing.\n'
00861                    'Usage: zopeedit inputfile')
00862     try:
00863         ExternalEditor(input_file).launch()
00864     except KeyboardInterrupt:
00865         pass
00866     except SystemExit:
00867         pass
00868     except:
00869         fatalError(sys.exc_info()[1])