#!/usr/bin/python 
# 
# PyUp - Python CGI Upload Script 
#   By Isam M. http://biodegradablegeek.com/ 
#   September 21st 2004 
#
# This hideous arrangement of bytes was a 
# direct port from a CGI script I wrote in C 
# 
# This script actually gained a lot of momentum 
# back when ImageShack and RapidShare didn't exist. 
# I had to make it private because the bandwidth usage 
# was lagging me in Counter-Strike. 
# 
# TODO Filter results
# TODO can not have upload list on the same page..
# TODO Arrow to show desc/asc sort display
# TODO ability to browse more than one file at a time 
# TODO Read settings from config file 
# 

import os
import re
import glob
import time
import sys
import string
import cgi
import cgitb; cgitb.enable();
import urllib


###############################################################################
# Modify these to fit your needs. 'True' and 'False' are case sensitive.
###############################################################################

# Used to link to the local files
HOST='http://do.main.me/'

# Uploaded files will go here. Server needs write permission.
UPLOADDIR='../upload/files'

# After a successful upload, a link to the file will be displayed.
# LINK/filename_of_the_uploaded_file
LINK=HOST+'upload/files/'

# Ban file, has a list of the IPS to ban, ascii file, ip on each line
BANFILE='../upload/bans'

# Log files (Server needs write permission)
LOG_ACCESS='../upload/logs/access.log'
LOG_ERROR='../uploadlogs/error.log'

# Templates 
TEMPLATE_BANNED='../upload/templates/banned.html'
TEMPLATE_MAIN='../upload/templates/main.html'
TEMPLATE_FORM='../upload/templates/form.html'
TEMPLATE_FORM_UPFIELD='../upload/templates/uploadfield.html'

# Turning logging off is not r_tttmended.
LOG=True

# Upload file size limit in bytes.
MAXSZ=1024*1024*15

# Chunk of file read and written to disk at a time.
CHUNKSZ=1024*1024

# If False, uploaded files are not publicly displayed.
DISPLAYLOCALFILES=True

# The field name in the HTML code, you need a %d in it.
FIELDFMT='file_%d_'
RENAMEFMT='as_%d'


def StripPath(filename):
	"""Strip the path from file and return filename alone."""
	# Some OSs do not use / in their file system, Windows
	# for example uses \, so let's convert all to /
	if not filename:
		return ''
	tmp=re.sub(r'(%(2f|2F|5c|5C|3a|3A))|\\|:', '/', filename)
	x=string.split(tmp, '/')
	return x[len(x)-1]


def FileExtension(filename):
	dot=filename.rfind('.')
	if (-1==dot):
		return None
	# The user can embed HTML as the file extension.
	return cgi.escape(filename[dot+1:].lower())


def FormatFileSize(sz):
	"""Return filesize in readable human form."""
	# Bytes 
	if 1024>sz:
		ret=str(sz)
		return ret
	# Kilobytes
	if ((1024*1024)>sz):
		kb=float(sz/1024.0)
		ret=str(round(kb,1))+'K'
		return ret
	# Megabytes
	if ((1024*1024*1024)>sz):
		mb=float((sz/1024.0)/1024.0)
		ret=str(round(mb,1))+'M'
	else:
		# Gigabytes
		gb=float(((sz/1024.0)/1024.0)/1024)
		ret=str(round(gb,1))+'G'
	return ret


class Time:
	def __init__(self):
		self.seconds=0
		self.minutes=0
		self.hours=0
	def __str__(self):
		# Omit seconds 
		m=str(self.minutes)
		if len(m)==1: m = '0' + m
		return str(self.hours) + ':' \
				+ m


def TimeToSeconds(t):
	minutes = t.hours * 60 + t.minutes
	seconds = minutes * 60 + t.seconds
	return seconds 


def SecondsToTime(s):
	time = Time()
	time.hours = s/3600
	s = s- time.hours * 3600
	time.minutes = s/60
	s = s- time.minutes * 60
	time.s = s
	return time


def LogLine(logfile, line):
	try:
		fp=file(logfile, 'a')
		entry=time.strftime('[%m/%d/%y %I:%M:%S %p] ')
		entry+=GetVisitorIP()+': '+line
		fp.write(entry+'\n')
		fp.close()
	except:
		# Try to log to error file, if we can not (bad) just leave
		try:
			fp=file(LOG_ERROR, 'a')
			entry=time.strftime('[%m/%d/%y %I:%M:%S %p] ')
			entry+=': Log entry "' + line + '" to file "' + logfile + '"'
			fp.write(entry+'\n')
			fp.close()
		except:
			return


def LogAccess(line):
	LogLine(LOG_ACCESS, line)


def LogUpload(remotefile, uploadedfile):
	x='Uploaded ' + remotefile
	if remotefile != uploadedfile: x += ' as ' + uploadedfile
	LogLine(LOG_ACCESS, x)


def LogError(error):
	x='Error: ' + error
	LogLine(LOG_ERROR, x)


def LinkToLocalFile(file, short=True, maxlen=-1, quote=False):
	"""Return html link code for file.
	
	short, if true will return the link in a short form, 
	<a href="http://asdf/file">file</a>
	as opposed to long form, which is:
	<a href="http://asdf/file">http://asdf/file</a>

	maxlen is used only when short is True, maxlen is the
	of chars to display for file, if the file is too
	long, it returns the link with 3 dots appended to the end
	to show that it continues on.
	<a href="http://asdf/longfileblablablablabla.exe">longfilebl...</a>
	-1 means turn that off and show all of it
	"""
	if quote:
		file=urllib.quote(file)

	if short:
		if -1==maxlen:
			a=file
		else:
			a=file[0:maxlen]
			if (len(file)-len(a)) >= 3:
				a+='...'
	else:
		a=LINK+file

	link='<a href="' + HOST + '/' \
	+ UPLOADDIR + '/' \
	+ file + '">' 
	return link + cgi.escape(a) + '</a>'


def ColumnSortLink(col, order, name=None):
	# <a href="scriptname.cgi?sort=col&order=asc">Col</a>
	if not name: name=col.lower()
	script = os.path.basename(sys.argv[0])
	return '<a href="%s?sort=%s&order=%s">%s</a>' \
		% (script, name, order, col)


def SortDictionary(lidic, column, order):
	""" This is very bad.. bubble sort to sort a list of dictionaries
		by a specific key. """
	for a in range(0, len(lidic)-1):
		for b in range(a, len(lidic)):
			if order=='asc': 
				if lidic[a][column] > lidic[b][column]:
					lidic[a], lidic[b] = lidic[b], lidic[a]
			else:
				if lidic[a][column] < lidic[b][column]:
					lidic[a], lidic[b] = lidic[b], lidic[a]
	return lidic


def ColColor(column, x):
	if column==x:
		return '#cccccc'
	return '#ffffff'


def GetLocalFiles(form):
	"""Print a table of the files in the upload directory."""

	# Let us see how the files are to be sorted 
	accepted_columns = ['name', 'size', 'dt', 'mime', 'ext', 'ip']
	if form.has_key('sort') and \
		form['sort'].value and \
		form['sort'].value.lower() in accepted_columns:
		column=form['sort'].value.lower()
	else:
		column='dt'

	if form.has_key('order') and \
		form['order'].value and \
		form['order'].value.lower() in ['asc', 'desc']: 
		order=form['order'].value.lower()
	else:
		order='asc'

	# We have a 'database' file that lists information
	# about each uploaded file. 
	# filename, type, date uploaded, IP of uploader
	# We want to open up this file now, if it doesn't
	# exist, either something is broken or more likely
	# this is the first time this script is being run.

	try:
		db=file('../upload/files.db', 'r')
		# File is delimeted by \
		fd={}
		files=[]
		f=db.readline()
		while f:
			# Ignore lines that begin with a # 
			if f != '\n' and f.lstrip()[0]!='#':
				d=string.split(f, ' / ')
				fd['name']=d[0]
				fd['size']=int(d[1])
				fd['dt']=d[2]
				fd['mime']=d[3]
				fd['ip']=d[4][:-1] # Ends with \n
				fd['ext']=FileExtension(d[0])
				files.append(fd.copy())
			f=db.readline()
		db.close()
		if files==[]:
			return ''
	except IOError:
		# File doesn't exist? Let's create an empty one
		try:
			# Try to create the file.
			db=file('../upload/files.db', 'w')
			db.close()
			return ''
		except IOError:
			LogError('Could not open/create local list of uploaded files! Check permissions.')
			return ''
	res = ''
	res += '<table class="files">\n<tr>'
	if order=='asc':
		order='desc'
	else:
		order='asc'
	res += '<td class="filerow" bgcolor="' + ColColor('name', column) + '"><b><p>%s</p></b></td>' % ColumnSortLink('Name', order)
	res += '<td class="filerow" bgcolor="' + ColColor('size', column)  + '"><b><p>%s</p></b></td>' % ColumnSortLink('Size', order)
	res += '<td class="filerow" bgcolor="' + ColColor('dt', column)  + '"><b><p>%s</p></b></td>' % ColumnSortLink('Date/Time', order, name='dt')
	res += '<td class="filerow" bgcolor="' + ColColor('ext', column)  + '"><b><p>%s</p></b></td>' % ColumnSortLink('Ext', order)
	res += '<td class="filerow" bgcolor="' + ColColor('mime', column)  + '"><b><p>%s</p></b></td>' % ColumnSortLink('MIME', order)
	res += '<td class="filerow" bgcolor="' + ColColor('ip', column)  + '"><b><p>%s</p></b></td>' % ColumnSortLink('IP', order)
	res += '</tr>'
	sorted=SortDictionary(files, column, order)

	gray = True
	for i in range(len(sorted)):
		lnk=LinkToLocalFile(files[i]['name'], maxlen=35) 
		sz=FormatFileSize(files[i]['size']) 
		dt=files[i]['dt']
		if not files[i]['ext']: files[i]['ext']='N/A'
		if gray:
			res += '<tr bgcolor="#eeeeee">'
			gray=False;
		else:
			res += '<tr bgcolor="#ffffff">'
			gray=True
#		res += '<td><p>' + str(i+1) + '</p></td>'
		res += '<td class="filerow"><p>'       + lnk + '</p></td>\n'
		res += '<td class="filerow"><p>' + sz + '</p></td>\n'
		res += '<td class="filerow"><p>' + dt + '</p></td>\n'
		res += '<td class="filerow"><p>' + files[i]['ext'] + '</p></td>\n'
		res += '<td class="filerow"><p>' + files[i]['mime'] + '</p></td>\n'
		res += '<td class="filerow"><p>' + files[i]['ip'] + '</p></td>\n'
		res += '</tr>'
	res += '</table>'
	return res


def GetVisitorIP():
	return os.getenv('REMOTE_ADDR', 'get.ip.failed')


def HandleUpload(form, NumOfUploads):	
	"""Save from form to file dir."""
	# Let us see if the user DID click the upload button.
	if not form.has_key('upform'): return ''
	
	res=''
	for i in range(NumOfUploads):
		field=FIELDFMT % (i)
		if not form.has_key(field): 
			# This is probably the user's first time on the page.
			continue
		else:
			fs=form[field]
			if not fs.value or not fs.filename:
				continue

			# Check if user wants to save this locally with a different name. 
			renameto=RENAMEFMT % (i)
			if form.has_key(renameto) and ''!=form[renameto].value:
				fn=form[renameto].value
			else:
				fn=StripPath(fs.filename)

			# Check if the name is allowed. 
			if	'/' in fn or \
				'\\' in fn or \
				'.htaccess'==fn or \
				'robots.txt'==fn or \
				fn.startswith('index.'):
				res += '<p class="result_warning">Name "<b>'+fn+'</b>" is not allowed.<br />'
				res += 'Please choose another name for the file.</p>'
				continue

			# At this point, we have the absolute path to the local file.
			pfn=os.path.join(UPLOADDIR, fn)

			# Check if file exists already.
			try:
				tmp=file(pfn, 'rb')
				tmp.close()
				res += '<p class="result_warning">A file named <b>'+fn+\
						'</b> already exists.<br />'\
						'Please choose another name and try again.</p>'
				if LOG: LogAccess('Tried uploaded file "' + fn + '" but it already exists')
			except IOError:
				# We probably caught a file not found exception.
				# Meaning we are good to go.
				try:
					f=file(pfn, 'wb')
				except IOError:
					res += '<p class="result_warning"><b>'+cgi.escape(fn)+'</b> could not be' \
							'uploaded.</p>'
					evidence.append('Upload of ' + fn + 'failed')
					continue

				# Write file to local disk.
				tb=0
				while 1:
					if tb>MAXSZ:
						res += '<p class="result_warning">Max file size limit exceeded (' \
									+FormatFileSize(MAXSZ) + ')</p>'
						evidence.append('Exceeded file limit with file: ' + fn)
						os.remove(pfn)
						return res
					br=fs.file.read(CHUNKSZ)
					if not br:
						break
					tb+=len(br)
					f.write(br)
				f.close()
				if LOG: LogUpload(fs.filename, fn)
				res += '<p class="result_success">Uploaded <b>' + cgi.escape(fn) + \
						'</b> successfully.</p>'
#				res += '<p>ID of file:</p> %d<br />' % (fileid)
				res += '<p>'+LinkToLocalFile(fn, short=False, quote=True)
				res += '</p>'

				# Upload was successful, let's add this file to the db now.
				try:
					f=file('../upload/files.db', 'a')
					dt=time.strftime('%D %I:%M %p')
					tp=fs.type
					# " / " is our delimeter in the file, we can't
					# have it in the type else it will ruin EVERYTHING.
					tp=tp.replace(' / ', '/')
					if len(tp) > 25: 
						tp = tp[0:25] + '...'
					line=fn + ' / ' \
							+ str(tb) + ' / ' \
							+ dt + ' / ' \
							+ tp + ' / ' \
							+ GetVisitorIP()
					f.write(line + '\n')
					f.close()
				except:
					res += 'File could not be added to file list'
					res += 'You will not be able to<br /> see the file'
					res += 'on the file list but you can still access it'
					res += '<br /> using the URL but tell the admin '
					res += 'what happened.'
					return res
	return res


def GetNumFields(form):
	"""Return # of upload fields we should display."""
		# Did user click 'update' to update the number of fields?
	n=0
	if form.has_key('numfields'): 
		n=form['numfields'].value
	else:
		# Guess not, let's see if we have a previous number.
		if form.has_key('numfieldshidden'): 
			n=form['numfieldshidden'].value
	try:
		n = int(n)
		if 0 < n < 100: return n
	except:
		pass
	return 3


def GetTemplate(template):
	"""Open and return a template file"""
	try:
		f = file(template)
		tmp = f.read()
		f.close()
		return tmp
	except:
		return None


def GetUploadForm(NumOfUpFields):
	"""Generate the html upload form and return."""
	raw = GetTemplate(TEMPLATE_FORM)
	if not raw: 
		LogError('Could not open template file "' + TEMPLATE_FORM + '"')
		PrintErrorPage('Could not open form template, check permissions')

	raw = raw.replace('%FORM_SCRIPT_NAME%', os.path.basename(sys.argv[0]))
	raw = raw.replace('%FORM_NUMUP%', str(NumOfUpFields))
	
	upfieldtemp = GetTemplate(TEMPLATE_FORM_UPFIELD)
	if not upfieldtemp: 
		LogError('Could not open template file "' + TEMPLATE_FORM + '"')
		PrintErrorPage('Could not open form upfield template, check permissions')
	
	upfieldtemp = upfieldtemp.replace('%FORM_UPFIELDNAME%', 'file_%d_')
	upfieldtemp = upfieldtemp.replace('%FORM_UPLOADAS%', 'as_%d')
	upfieldtemp = upfieldtemp.replace('%FORM_ARROW%', 'id_%d')
	upfieldtemp = upfieldtemp.replace('%FORM_OPTIONAL_TABLE%', 'optional_' + 'id_%d')

	for n in range(NumOfUpFields):
		tmp = upfieldtemp % (n, n, n, n)
		raw = raw.replace('%FORM_UPFIELDS%', tmp + '%FORM_UPFIELDS%')

	raw = raw.replace('%FORM_UPFIELDS%', '')

	return raw


def ParseTemplate(raw, Replace):
	"""Take the raw template, replace some variables in it and return."""

	if Replace.has_key('%MAIN_RESULT%'): 
		raw = raw.replace('%MAIN_RESULT%', Replace['%MAIN_RESULT%'])

	if Replace.has_key('%LOCAL_FILES%'):
		raw = raw.replace('%LOCAL_FILES%', Replace['%LOCAL_FILES%'])
		
	if Replace.has_key('%UPLOAD_FORM%'):
		raw = raw.replace('%UPLOAD_FORM%', Replace['%UPLOAD_FORM%'])

	if Replace.has_key('%MAX_SIZE%'):
		raw = raw.replace('%MAX_SIZE%', Replace['%MAX_SIZE%'])

	return raw


def PrintErrorPage(error, MIME='Content-type: text/plain\n'):
	print MIME
	print error
	sys.exit(0)


def ParsePrintTemplate(template, NumOfUpFields=None, 
							MainResult='', 
							LocalFiles='',
							MIME='Content-type: text/html\n'):
	"""Parse and print a template file."""
	res = GetTemplate(template)
	# TODO LOG ERROR
	if not res: PrintErrorPage('Could not open main template, check permissions')

	replace = {}
	replace['%MAIN_RESULT%'] = MainResult
	replace['%LOCAL_FILES%'] = LocalFiles
	replace['%UPLOAD_FORM%'] = GetUploadForm(NumOfUpFields)
	replace['%MAX_SIZE%'] = FormatFileSize(MAXSZ)
	res = ParseTemplate(res, replace)
	print MIME
	print res
	sys.exit(0)


def main():
	# ------------------------------------------------
#	print 'Content-type: text/html\n'
#	print '<img src="../under-construction.jpg" />'
#	sys.exit(0)
	# ------------------------------------------------
	form=cgi.FieldStorage()
	numupfields = GetNumFields(form)
	result = HandleUpload(form, numupfields)
	if DISPLAYLOCALFILES: 
		ParsePrintTemplate(TEMPLATE_MAIN, numupfields, result, GetLocalFiles(form))
	else:
		ParsePrintTemplate(TEMPLATE_MAIN, numupfields, result)


def PrintHtmlFile(HtmlFile, MIME='Content-type: text/html\n'):
	"""Print passed file to screen."""
	res = GetTemplate(HtmlFile)
	if res:
		print MIME
		print res
	else:
		LogError('Could not open template file "' + TEMPLATE_FORM + '"')
		PrintErrorPage('You are banned.')


def CheckIP():
	"""Checks if this ip is on the ban list, if so - do something accordingly."""
	try:
		user=GetVisitorIP()
		bf=file(BANFILE, 'r')
		for ip in bf.xreadlines():
			if ip[:-1]==user:
				LogAccess('Banned user tried to access page')
				sys.exit(0)
		bf.close()
	except:
		if sys.exc_type == SystemExit:
			PrintHtmlFile(TEMPLATE_BANNED) # Send this user the banned template 
			sys.exit(0)
		# Something happened while reading bans file.. 
		# Let us log it and move on.
		LogError('Error checking bans files "' + BANFILE + '"')


if __name__=='__main__':
	CheckIP()
	main()


