#@+leo-ver=4
#@+node:@file mod_http.py
#@@language python

"""
A minimal http plugin for LEO, based on AsyncHttpServer.py.

Based on the asyncore / asynchat framework.
"""

# From http://home.pacbell.net/bwmulder/python/Leo/HttpPlugin.leo
# See also, the related script from the Python Cookbook:
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/259148

__version__ = "0.9"

#@<< how to use this plugin >>
#@+node:<< how to use this plugin >>
#@+at
# Use this plugin is as follows:
# 
# 1. Start Leo with the plugin enabled.  You will see a purple message that 
# says something like:
# 
# "http serving enabled on port 8080, version 0.9"
# 
# 2. Start a web browser, and enter the following url: http://localhost:8080/
# 
# You will see a a "top" level page containing one link for every open .leo 
# file.  Start clicking :-)
# 
# You can use the browser's refresh button to update the top-level view in the 
# browser after you have opened or closed files.
#@-at
#@nonl
#@-node:<< how to use this plugin >>
#@nl
#@<< imports >>
#@+node:<< imports >>
import asynchat,asyncore,socket,SimpleHTTPServer, select, urllib, urlparse
import posixpath,sys, cgi,cStringIO, os

import exceptions
import select
import socket
import sys
import time

import shutil
from StringIO import StringIO

from  leoPlugins import registerHandler
from leoGlobals import es, app

import ConfigParser
#@-node:<< imports >>
#@nl

sockets_to_close = []

#@+others
#@+node:class delayedSocketStream
class delayedSocketStream(asyncore.dispatcher_with_send):
	#@	@+others
	#@+node:__init__
	def __init__(self,sock):
		self.socket=sock
		self.socket.setblocking(0)
		self.closed=1   # compatibility with SocketServer
		self.buffer = []
	#@-node:__init__
	#@+node:write
	def write(self,data):
		self.buffer.append(data)
	#@-node:write
	#@+node:initiate_sending
	def initiate_sending(self):
		self.out_buffer = ''.join(self.buffer)
		del self.buffer
		self.set_socket(self.socket, None)
		self.socket.setblocking(0)
		self.connected = 1
		try:
			self.addr = self.socket.getpeername()
		except socket.error:
			# The addr isn't crucial
			pass
	#@-node:initiate_sending
	#@+node:handle_read
	def handle_read(self):
		pass
	#@nonl
	#@-node:handle_read
	#@+node:writable
	def writable(self):
		result = (not self.connected) or len(self.out_buffer)
		if not result:
			sockets_to_close.append(self)
		return result
	#@-node:writable
	#@-others
#@-node:class delayedSocketStream
#@+node:class nodeNotFound
class nodeNotFound(Exception):
	pass
#@nonl
#@-node:class nodeNotFound
#@+node:class escaped_StringIO
class escaped_StringIO(StringIO):
	#@	@+others
	#@+node:write_escaped
	def write_escaped(self, s):
		s = s.replace('&', "&amp;")
		s = s.replace('<', "&lt;")
		s = s.replace('>', "&gt;")
		
		# is there a more elegant way to do this?
		# Replaces blanks with &nbsp; id they are in
		# the beginning of the line.
		lines = s.split('\n')
		result = []
		blank = chr(32)
		for line in lines:
			if line.startswith(blank):
				resultchars = []
				startline = True
				for char in line:
					if char == blank:
						if startline:
							resultchars.append('&nbsp;')
						else:
							resultchars.append(' ')
					else:
						startline = False
						resultchars.append(char)
				result.append(''.join(resultchars))
			else:
				result.append(line)
		s = '\n'.join(result)
	
		s = s.replace('\n', '<br>')
		s = s.replace(chr(9), '&nbsp;&nbsp;&nbsp;&nbsp;')
		StringIO.write(self, s)
		
	#@nonl
	#@-node:write_escaped
	#@-others
#@nonl
#@-node:class escaped_StringIO
#@+node:class RequestHandler
class RequestHandler(
	asynchat.async_chat,
	SimpleHTTPServer.SimpleHTTPRequestHandler):
	#@	@+others
	#@+node:__init__
	def __init__(self,conn,addr,server):
		asynchat.async_chat.__init__(self,conn)
		self.client_address=addr
		self.connection=conn
		self.server=server
		self.wfile = delayedSocketStream(self.socket)
		# sets the terminator : when it is received, this means that the
		# http request is complete ; control will be passed to
		# self.found_terminator
		self.set_terminator ('\r\n\r\n')
		self.buffer=cStringIO.StringIO()
		self.found_terminator=self.handle_request_line
	#@-node:__init__
	#@+node:copyfile
	def copyfile(self, source, outputfile):
		"""Copy all data between two file objects.
	
		The SOURCE argument is a file object open for reading
		(or anything with a read() method) and the DESTINATION
		argument is a file object open for writing (or
		anything with a write() method).
	
		The only reason for overriding this would be to change
		the block size or perhaps to replace newlines by CRLF
		-- note however that this the default server uses this
		to copy binary data as well.
		 """
		shutil.copyfileobj(source, outputfile, length=255)
	#@-node:copyfile
	#@+node:log_message
	def log_message(self, format, *args):
		"""Log an arbitrary message.
	
		 This is used by all other logging functions.  Override
		 it if you have specific logging wishes.
	
		 The first argument, FORMAT, is a format string for the
		 message to be logged.  If the format string contains
		 any % escapes requiring parameters, they should be
		 specified as subsequent arguments (it's just like
		 printf!).
	
		 The client host and current date/time are prefixed to
		 every message.
	
		 """
		message = "%s - - [%s] %s\n" % (
			self.address_string(),
			self.log_date_time_string(),
			format%args)
		es(message)
	#@-node:log_message
	#@+node:collect_incoming_data
	def collect_incoming_data(self,data):
		"""Collects the data arriving on the connexion"""
		self.buffer.write(data)
	#@-node:collect_incoming_data
	#@+node:prepare_POST
	def prepare_POST(self):
		"""Prepare to read the request body"""
		bytesToRead = int(self.headers.getheader('content-length'))
		# set terminator to length (will read bytesToRead bytes)
		self.set_terminator(bytesToRead)
		self.buffer=cStringIO.StringIO()
		# control will be passed to a new found_terminator
		self.found_terminator=self.handle_post_data
	#@-node:prepare_POST
	#@+node:handle_post_data
	def handle_post_data(self):
		"""Called when a POST request body has been read"""
		self.rfile=cStringIO.StringIO(self.buffer.getvalue())
		self.do_POST()
		self.finish()
	#@-node:handle_post_data
	#@+node:add_leo_links
	def add_leo_links(self, window, node, f):
		"""
		Given a node 'node', add links to:
			The next sibling, if any.
			the next node.
			the parent.
			The children, if any.
		"""
	
		# Collecting the navigational links.
		if node is None:
			# top level
			child = window.c.rootVnode()
			children = [child]
			next = child.next()
			while next:
				child = next
				children.append(child)
				next = child.next()
			nodename = window.shortFileName()
		else:
			nodename = node.headString()
			threadNext = node.threadNext()
			sibling = node.next()
			parent = node.parent()
	
			children = []
			firstChild = node.firstChild()
			if firstChild:
				child = firstChild
				while child:
					children.append(child)
					child = child.next()
	
			if threadNext is not None:
				self.create_leo_reference(window, threadNext,  "next", f)
			f.write("<br>")
			if sibling is not None:
				self.create_leo_reference(window, sibling, "next Sibling", f)
			f.write("<br>")
			if parent is None:
				self.create_href("/", "Top level", f)
			else:
				self.create_leo_reference(window, parent, "Up", f)
			f.write("<br>")
			
		if children:
			f.write("<h2>")
			f.write("Children of ")
			f.write_escaped(nodename)
			f.write("</h2>\n")
			f.write("<ol>\n")
			for child in children:
				f.write("<li>\n")
				self.create_leo_reference(window, child, child.headString(), f)
			f.write("</ol>\n")
	#@nonl
	#@-node:add_leo_links
	#@+node:create_href
	def create_href(self, href, text, f):
		f.write('<a href="%s">' % href)
		f.write_escaped(text)
		f.write("</a>\n")
	
	#@-node:create_href
	#@+node:create_leo_reference
	def create_leo_reference(self, window, node, text, f):
		"""
		Create a reference to 'node' in 'window', displaying 'text'
		"""
		parts = [window.shortFileName()] + self.get_leo_nameparts(node)
		href = '_'.join(parts)
		self.create_href(href, text, f)
	#@-node:create_leo_reference
	#@+node:format_leo_node
	def format_leo_node(self, window, node):
		"""
		Given a node 'node', return the contents of that node as html text.
		
		Include some navigational references too
		"""
	
		if node is not None:
			headString = node.headString()
			bodyString = node.bodyString()
		else:
			headString, bodyString = "Top level", ""
		f = escaped_StringIO()
		write, write_escaped = f.write, f.write_escaped
		write("<title>")
		write_escaped(window.shortFileName() + ":" + headString)
		write("</title>\n")
		# write navigation
		self.add_leo_links(window, node, f)
		# write path
		self.write_path(node, f)
		write("<hr>\n") # horizontal rule
		# f.write('<span style="font-family: monospace;">')
		write_escaped(bodyString)
		# f.write("</span>\n")
		return f
	#@nonl
	#@-node:format_leo_node
	#@+node:get_leo_nameparts
	def get_leo_nameparts(self, node):
		"""
		Given a 'node', construct a list of sibling numbers to get to that node.
		"""
		result = []
		cnode = node
		parent = cnode.parent()
		while parent:
			i = 0
			child = parent.firstChild()
			while child != cnode:
				child = child.next()
				i += 1
			result.append(str(i))
			cnode = parent
			parent = cnode.parent()
		i = 0
		previous = cnode.back()
		while previous:
			i += 1
			previous = previous.back()
		result.append(str(i))
		result.reverse()
		return result
	#@-node:get_leo_nameparts
	#@+node:get_leo_node
	def get_leo_node(self, path):
		"""
		given a path of the form:
			<short filename>_<number1>_<number2>...<numbern>
			identify the leo node which is in that file, and,
			from top to bottom, is the <number1> child of the topmost
			node, the <number2> child of that node, and so on.
			
			Return None if that node can not be identified that way.
		"""
		# Identify the window
		for w in app().windowList:
			if w.shortFileName() == path[0]:
				break
		else:
			return None, None
			
		node = w.c.rootVnode()
		
		if len(path) >= 2:
			for i in range(int(path[1])):
				node = node.next()
				if node is None:
					raise nodeNotFound
			# go to the i'th child for each path element.
			for i in path[2:]:
				node = node.nthChild(int(i))
				if node is None:
					raise nodeNotFound
		else:
			node = None
		return w, node
	#@nonl
	#@-node:get_leo_node
	#@+node:get_leo_windowlist
	def get_leo_windowlist(self):
		"""
	 
		"""
		f = escaped_StringIO()
		write, write_escaped = f.write, f.write_escaped
		write("<title>ROOT for LEO HTTP plugin</title>\n")
		write("<h2>Windowlist</h2>\n")
		write("<hr>\n") # horizontal rule
		write("<ul>\n")
		a = app() # get the singleton application instance.
		windows = a.windowList # get the list of all open frames.
		for w in windows:
			write("<li>")
			shortfilename = w.shortFileName()
			write('<a href="%s">' % shortfilename)
			write("file name: %s" % shortfilename)
			write("</a>\n")
		write("</ul>\n")
		write("<hr>\n")
		return f
	#@-node:get_leo_windowlist
	#@+node:write_path
	def write_path(self, node, f):
		result = []
		while node:
			result.append(node.headString())
			node = node.parent()
		result.reverse()
		if result:
			result2 = result[:-1]
			if result2:
				result2 = ' / '.join(result2)
				f.write("<br>\n")
				f.write_escaped(result2)
				f.write("<br>\n")
			f.write("<h2>")
			f.write_escaped(result[-1])
			f.write("</h2>\n")
	#@nonl
	#@-node:write_path
	#@+node:send_head
	def send_head(self):
		"""Common code for GET and HEAD commands.
	
		 This sends the response code and MIME headers.
	
		 Return value is either a file object (which has to be copied
		 to the outputfile by the caller unless the command was HEAD,
		 and must be closed by the caller under all circumstances), or
		 None, in which case the caller has nothing further to do.
	
		 """
		try:
			path = self.split_leo_path(self.path)
			if path == '/':
				 f = self.get_leo_windowlist()
			else:
				try:
					window, node = self.get_leo_node(path)
					if window is None:
						self.send_error(404, "File not found")
						return None
					f = self.format_leo_node(window, node)
				except nodeNotFound:
					self.send_error(404, "Node not found")
					return None
			length = f.tell()
			f.seek(0)
			self.send_response(200)
			self.send_header("Content-type", "text/html")
			self.send_header("Content-Length", str(length))
			self.end_headers()
			return f
		except:
			import traceback
			traceback.print_exc()
			raise
	
	#@-node:send_head
	#@+node:split_leo_path
	def split_leo_path(self, path):
		"""
		A leo node is represented by a string of the form:
			<number1>_<number2>...<numbern>,
		where <number> is the number of sibling of the node.
		"""
		if path == '/':
			return '/'
		if path.startswith("/"):
			path = path[1:]
		return path.split('_')
	#@nonl
	#@-node:split_leo_path
	#@+node:do_GET
	def do_GET(self):
		"""Begins serving a GET request"""
		# nothing more to do before handle_data()
		self.handle_data()
	#@-node:do_GET
	#@+node:do_POST
	def do_POST(self):
		"""Begins serving a POST request. The request data must be readable
		 on a file-like object called self.rfile"""
		ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
		length = int(self.headers.getheader('content-length'))
		if ctype == 'multipart/form-data':
			query=cgi.parse_multipart(self.rfile, pdict)
		elif ctype == 'application/x-www-form-urlencoded':
			qs=self.rfile.read(length)
			query=cgi.parse_qs(qs, keep_blank_values=1)
		else:
			query = ''                   # Unknown content-type
		# some browsers send 2 more bytes...
		[ready_to_read,x,y]=select.select([self.connection],[],[],0)
		if ready_to_read:
			self.rfile.read(2)
	
		self.QUERY.update(self.query(query))
		self.handle_data()
	#@-node:do_POST
	#@+node:query
	def query(self,parsedQuery):
		"""Returns the QUERY dictionary, similar to the result of cgi.parse_qs
		 except that :
		 - if the key ends with [], returns the value (a Python list)
		 - if not, returns a string, empty if the list is empty, or with the
		 first value in the list"""
		res={}
		for item in parsedQuery.keys():
			value=parsedQuery[item] # a Python list
			if item.endswith("[]"):
					res[item[:-2]]=value
			else:
					if len(value)==0:
						res[item]=''
					else:
						res[item]=value[0]
		return res
	#@-node:query
	#@+node:handle_data
	def handle_data(self):
		"""Class to override"""
		f = self.send_head()
		if f:
			self.copyfile(f, self.wfile)
	#@-node:handle_data
	#@+node:handle_request_line
	def handle_request_line(self):
		"""Called when the http request line and headers have been received"""
	
		# prepare attributes needed in parse_request()
		self.rfile=cStringIO.StringIO(self.buffer.getvalue())
		self.raw_requestline=self.rfile.readline()
		self.parse_request()
	
		# if there is a Query String, decodes it in a QUERY dictionary
		self.path_without_qs,self.qs=self.path,''
		if self.path.find('?')>=0:
			self.qs=self.path[self.path.find('?')+1:]
			self.path_without_qs=self.path[:self.path.find('?')]
		self.QUERY=self.query(cgi.parse_qs(self.qs,1))
	
		if self.command in ['GET','HEAD']:
			# if method is GET or HEAD, call do_GET or do_HEAD and finish
			method="do_"+self.command
			if hasattr(self,method):
					getattr(self,method)()
					self.finish()
		elif self.command=="POST":
			# if method is POST, call prepare_POST, don't finish before
			self.prepare_POST()
		else:
			self.send_error(501, "Unsupported method (%s)" %self.command)
	#@-node:handle_request_line
	#@+node:finish
	def finish(self):
		"""Reset terminator (required after POST method), then close"""
		self.set_terminator ('\r\n\r\n')
		self.wfile.initiate_sending()
		# self.close()
	#@nonl
	#@-node:finish
	#@-others
#@-node:class RequestHandler
#@+node:class Server
class Server(asyncore.dispatcher):
	"""Copied from http_server in medusa"""
	#@	@+others
	#@+node:__init__
	def __init__ (self, ip, port,handler):
		self.ip = ip
		self.port = port
		self.handler=handler
		asyncore.dispatcher.__init__ (self)
		self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
	
		self.set_reuse_addr()
		self.bind ((ip, port))
	
		# lower this to 5 if your OS complains
		self.listen (1024)
	#@-node:__init__
	#@+node:handle_accept
	def handle_accept (self):
		try:
			conn, addr = self.accept()
		except socket.error:
			self.log_info ('warning: server accept() threw an exception', 'warning')
			return
		except TypeError:
			self.log_info ('warning: server accept() threw EWOULDBLOCK', 'warning')
			return
		# creates an instance of the handler class to handle the request/response
		# on the incoming connexion
		self.handler(conn,addr,self)
	#@-node:handle_accept
	#@-others
#@-node:class Server
#@+node:poll
def poll(timeout=0.0):
	global sockets_to_close
	map = asyncore.socket_map
	if not map:
		return False
	while 1:
		r = []; w = []; e = []
		for fd, obj in map.items():
			if obj.readable():
				r.append(fd)
			if obj.writable():
				w.append(fd)
		if not sockets_to_close: # Set by writeable()
			break
		for s in sockets_to_close:
			s.close()
		sockets_to_close = []
	if [] == r == w == e:
		time.sleep(timeout)
	else:
		#@		<< try r, w, e = select.select >>
		#@+node:<< try r, w, e = select.select >>
		try:
			r, w, e = select.select(r, w, e, timeout)
		except select.error, err:
			if err[0] != EINTR:
				raise
			else:
				return False # EKR: added return value.
		#@nonl
		#@-node:<< try r, w, e = select.select >>
		#@nl
	for fd in r:
		#@		<< asyncore.read(map.get(fd)) >>
		#@+node:<< asyncore.read(map.get(fd)) >>
		obj = map.get(fd)
		if obj is not None:
			asyncore.read(obj)
		#@nonl
		#@-node:<< asyncore.read(map.get(fd)) >>
		#@nl
	for fd in w:
		#@		<< asyncore.write(map.get(fd)) >>
		#@+node:<< asyncore.write(map.get(fd)) >>
		 obj = map.get(fd)
		 if obj is not None:
			asyncore.write(obj)
		#@-node:<< asyncore.write(map.get(fd)) >>
		#@nl
	return len(r) > 0 or len(w) > 0
#@-node:poll
#@+node:loop
def loop(timeout=5.0, use_poll=0, map=None):
	"""
	Override the loop function of asynchore.
	We poll only until there is not read or
	write request pending.
	"""
	return poll(timeout)
#@nonl
#@-node:loop
#@+node:plugin_wrapper
def plugin_wrapper(tag, keywords):
	first = True
	while loop(timeout):
		pass
#@nonl
#@-node:plugin_wrapper
#@+node:a_read
def a_read(obj):
	try:
		obj.handle_read_event()
	except asyncore.ExitNow:
		raise
	except:
		obj.handle_error()


#@-node:a_read
#@+node:applyConfiguration
def applyConfiguration(config=None):

	"""Called when the user presses the "Apply" button on the Properties form"""

	global timeout, port, active

	if config is None:
		fileName = os.path.join(app().loadDir,"../","plugins","mod_http.ini")
		config = ConfigParser.ConfigParser()
		config.read(fileName)

	timeout = config.getint("Main", "timeout") / 1000.0
	port = config.getint("Main", "port")
	active = config.getboolean("Main", "active")
#@nonl
#@-node:applyConfiguration
#@-others

applyConfiguration()

if active:
	s=Server('',port,RequestHandler)
	asyncore.read = a_read
	registerHandler("idle", plugin_wrapper)
	
	es("http serving enabled on port %s, version %s" % (port, __version__), color="purple")
#@-node:@file mod_http.py
#@-leo
