1#!/usr/bin/env python
2
3"""
4FCKeditor - The text editor for Internet - http://www.fckeditor.net
5Copyright (C) 2003-2007 Frederico Caldeira Knabben
6
7== BEGIN LICENSE ==
8
9Licensed under the terms of any of the following licenses at your
10choice:
11
12 - GNU General Public License Version 2 or later (the "GPL")
13   http://www.gnu.org/licenses/gpl.html
14
15 - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
16   http://www.gnu.org/licenses/lgpl.html
17
18 - Mozilla Public License Version 1.1 or later (the "MPL")
19   http://www.mozilla.org/MPL/MPL-1.1.html
20
21== END LICENSE ==
22
23Connector for Python.
24
25Tested With:
26Standard:
27	Python 2.3.3
28Zope:
29	Zope Version: (Zope 2.8.1-final, python 2.3.5, linux2)
30	Python Version: 2.3.5 (#4, Mar 10 2005, 01:40:25)
31		[GCC 3.3.3 20040412 (Red Hat Linux 3.3.3-7)]
32	System Platform: linux2
33"""
34
35"""
36Author Notes (04 December 2005):
37This module has gone through quite a few phases of change.  Obviously,
38I am only supporting that part of the code that I use.  Initially
39I had the upload directory as a part of zope (ie. uploading files
40directly into Zope), before realising that there were too many
41complex intricacies within Zope to deal with.  Zope is one ugly piece
42of code.  So I decided to complement Zope by an Apache server (which
43I had running anyway, and doing nothing).  So I mapped all uploads
44from an arbitrary server directory to an arbitrary web directory.
45All the FCKeditor uploading occurred this way, and I didn't have to
46stuff around with fiddling with Zope objects and the like (which are
47terribly complex and something you don't want to do - trust me).
48
49Maybe a Zope expert can touch up the Zope components.  In the end,
50I had FCKeditor loaded in Zope (probably a bad idea as well), and
51I replaced the connector.py with an alias to a server module.
52Right now, all Zope components will simple remain as is because
53I've had enough of Zope.
54
55See notes right at the end of this file for how I aliased out of Zope.
56
57Anyway, most of you probably wont use Zope, so things are pretty
58simple in that regard.
59
60Typically, SERVER_DIR is the root of WEB_DIR (not necessarily).
61Most definitely, SERVER_USERFILES_DIR points to WEB_USERFILES_DIR.
62"""
63
64import cgi
65import re
66import os
67import string
68
69"""
70escape
71
72Converts the special characters '<', '>', and '&'.
73
74RFC 1866 specifies that these characters be represented
75in HTML as &lt; &gt; and &amp; respectively. In Python
761.5 we use the new string.replace() function for speed.
77"""
78def escape(text, replace=string.replace):
79    text = replace(text, '&', '&amp;') # must be done 1st
80    text = replace(text, '<', '&lt;')
81    text = replace(text, '>', '&gt;')
82    text = replace(text, '"', '&quot;')
83    return text
84
85"""
86getFCKeditorConnector
87
88Creates a new instance of an FCKeditorConnector, and runs it
89"""
90def getFCKeditorConnector(context=None):
91	# Called from Zope.  Passes the context through
92	connector = FCKeditorConnector(context=context)
93	return connector.run()
94
95
96"""
97FCKeditorRequest
98
99A wrapper around the request object
100Can handle normal CGI request, or a Zope request
101Extend as required
102"""
103class FCKeditorRequest(object):
104	def __init__(self, context=None):
105		if (context is not None):
106			r = context.REQUEST
107		else:
108			r = cgi.FieldStorage()
109		self.context = context
110		self.request = r
111
112	def isZope(self):
113		if (self.context is not None):
114			return True
115		return False
116
117	def has_key(self, key):
118		return self.request.has_key(key)
119
120	def get(self, key, default=None):
121		value = None
122		if (self.isZope()):
123			value = self.request.get(key, default)
124		else:
125			if key in self.request.keys():
126				value = self.request[key].value
127			else:
128				value = default
129		return value
130
131"""
132FCKeditorConnector
133
134The connector class
135"""
136class FCKeditorConnector(object):
137	# Configuration for FCKEditor
138	# can point to another server here, if linked correctly
139	#WEB_HOST = "http://127.0.0.1/"
140	WEB_HOST = ""
141	SERVER_DIR = "/var/www/html/"
142
143	WEB_USERFILES_FOLDER = WEB_HOST + "upload/"
144	SERVER_USERFILES_FOLDER = SERVER_DIR + "upload/"
145
146	# Allow access (Zope)
147	__allow_access_to_unprotected_subobjects__ = 1
148	# Class Attributes
149	parentFolderRe = re.compile("[\/][^\/]+[\/]?$")
150
151	"""
152	Constructor
153	"""
154	def __init__(self, context=None):
155		# The given root path will NOT be shown to the user
156		# Only the userFilesPath will be shown
157
158		# Instance Attributes
159		self.context = context
160		self.request = FCKeditorRequest(context=context)
161		self.rootPath = self.SERVER_DIR
162		self.userFilesFolder = self.SERVER_USERFILES_FOLDER
163		self.webUserFilesFolder = self.WEB_USERFILES_FOLDER
164
165		# Enables / Disables the connector
166		self.enabled = False # Set to True to enable this connector
167
168		# These are instance variables
169		self.zopeRootContext = None
170		self.zopeUploadContext = None
171
172		# Copied from php module =)
173		self.allowedExtensions = {
174				"File": None,
175				"Image": None,
176				"Flash": None,
177				"Media": None
178				}
179		self.deniedExtensions = {
180				"File": [ "html","htm","php","php2","php3","php4","php5","phtml","pwml","inc","asp","aspx","ascx","jsp","cfm","cfc","pl","bat","exe","com","dll","vbs","js","reg","cgi","htaccess","asis" ],
181				"Image": [ "html","htm","php","php2","php3","php4","php5","phtml","pwml","inc","asp","aspx","ascx","jsp","cfm","cfc","pl","bat","exe","com","dll","vbs","js","reg","cgi","htaccess","asis" ],
182				"Flash": [ "html","htm","php","php2","php3","php4","php5","phtml","pwml","inc","asp","aspx","ascx","jsp","cfm","cfc","pl","bat","exe","com","dll","vbs","js","reg","cgi","htaccess","asis" ],
183				"Media": [ "html","htm","php","php2","php3","php4","php5","phtml","pwml","inc","asp","aspx","ascx","jsp","cfm","cfc","pl","bat","exe","com","dll","vbs","js","reg","cgi","htaccess","asis" ]
184				}
185
186	"""
187	Zope specific functions
188	"""
189	def isZope(self):
190		# The context object is the zope object
191		if (self.context is not None):
192			return True
193		return False
194
195	def getZopeRootContext(self):
196		if self.zopeRootContext is None:
197			self.zopeRootContext = self.context.getPhysicalRoot()
198		return self.zopeRootContext
199
200	def getZopeUploadContext(self):
201		if self.zopeUploadContext is None:
202			folderNames = self.userFilesFolder.split("/")
203			c = self.getZopeRootContext()
204			for folderName in folderNames:
205				if (folderName <> ""):
206					c = c[folderName]
207			self.zopeUploadContext = c
208		return self.zopeUploadContext
209
210	"""
211	Generic manipulation functions
212	"""
213	def getUserFilesFolder(self):
214		return self.userFilesFolder
215
216	def getWebUserFilesFolder(self):
217		return self.webUserFilesFolder
218
219	def getAllowedExtensions(self, resourceType):
220		return self.allowedExtensions[resourceType]
221
222	def getDeniedExtensions(self, resourceType):
223		return self.deniedExtensions[resourceType]
224
225	def removeFromStart(self, string, char):
226		return string.lstrip(char)
227
228	def removeFromEnd(self, string, char):
229		return string.rstrip(char)
230
231	def convertToXmlAttribute(self, value):
232		if (value is None):
233			value = ""
234		return escape(value)
235
236	def convertToPath(self, path):
237		if (path[-1] <> "/"):
238			return path + "/"
239		else:
240			return path
241
242	def getUrlFromPath(self, resourceType, path):
243		if (resourceType is None) or (resourceType == ''):
244			url = "%s%s" % (
245					self.removeFromEnd(self.getUserFilesFolder(), '/'),
246					path
247					)
248		else:
249			url = "%s%s%s" % (
250					self.getUserFilesFolder(),
251					resourceType,
252					path
253					)
254		return url
255
256	def getWebUrlFromPath(self, resourceType, path):
257		if (resourceType is None) or (resourceType == ''):
258			url = "%s%s" % (
259					self.removeFromEnd(self.getWebUserFilesFolder(), '/'),
260					path
261					)
262		else:
263			url = "%s%s%s" % (
264					self.getWebUserFilesFolder(),
265					resourceType,
266					path
267					)
268		return url
269
270	def removeExtension(self, fileName):
271		index = fileName.rindex(".")
272		newFileName = fileName[0:index]
273		return newFileName
274
275	def getExtension(self, fileName):
276		index = fileName.rindex(".") + 1
277		fileExtension = fileName[index:]
278		return fileExtension
279
280	def getParentFolder(self, folderPath):
281		parentFolderPath = self.parentFolderRe.sub('', folderPath)
282		return parentFolderPath
283
284	"""
285	serverMapFolder
286
287	Purpose: works out the folder map on the server
288	"""
289	def serverMapFolder(self, resourceType, folderPath):
290		# Get the resource type directory
291		resourceTypeFolder = "%s%s/" % (
292				self.getUserFilesFolder(),
293				resourceType
294				)
295		# Ensure that the directory exists
296		self.createServerFolder(resourceTypeFolder)
297
298		# Return the resource type directory combined with the
299		# required path
300		return "%s%s" % (
301				resourceTypeFolder,
302				self.removeFromStart(folderPath, '/')
303				)
304
305	"""
306	createServerFolder
307
308	Purpose: physically creates a folder on the server
309	"""
310	def createServerFolder(self, folderPath):
311		# Check if the parent exists
312		parentFolderPath = self.getParentFolder(folderPath)
313		if not(os.path.exists(parentFolderPath)):
314			errorMsg = self.createServerFolder(parentFolderPath)
315			if errorMsg is not None:
316				return errorMsg
317		# Check if this exists
318		if not(os.path.exists(folderPath)):
319			os.mkdir(folderPath)
320			os.chmod(folderPath, 0755)
321			errorMsg = None
322		else:
323			if os.path.isdir(folderPath):
324				errorMsg = None
325			else:
326				raise "createServerFolder: Non-folder of same name already exists"
327		return errorMsg
328
329
330	"""
331	getRootPath
332
333	Purpose: returns the root path on the server
334	"""
335	def getRootPath(self):
336		return self.rootPath
337
338	"""
339	setXmlHeaders
340
341	Purpose: to prepare the headers for the xml to return
342	"""
343	def setXmlHeaders(self):
344		#now = self.context.BS_get_now()
345		#yesterday = now - 1
346		self.setHeader("Content-Type", "text/xml")
347		#self.setHeader("Expires", yesterday)
348		#self.setHeader("Last-Modified", now)
349		#self.setHeader("Cache-Control", "no-store, no-cache, must-revalidate")
350		self.printHeaders()
351		return
352
353	def setHeader(self, key, value):
354		if (self.isZope()):
355			self.context.REQUEST.RESPONSE.setHeader(key, value)
356		else:
357			print "%s: %s" % (key, value)
358		return
359
360	def printHeaders(self):
361		# For non-Zope requests, we need to print an empty line
362		# to denote the end of headers
363		if (not(self.isZope())):
364			print ""
365
366	"""
367	createXmlFooter
368
369	Purpose: returns the xml header
370	"""
371	def createXmlHeader(self, command, resourceType, currentFolder):
372		self.setXmlHeaders()
373		s = ""
374		# Create the XML document header
375		s += """<?xml version="1.0" encoding="utf-8" ?>"""
376		# Create the main connector node
377		s += """<Connector command="%s" resourceType="%s">""" % (
378				command,
379				resourceType
380				)
381		# Add the current folder node
382		s += """<CurrentFolder path="%s" url="%s" />""" % (
383				self.convertToXmlAttribute(currentFolder),
384				self.convertToXmlAttribute(
385					self.getWebUrlFromPath(
386						resourceType,
387						currentFolder
388						)
389					),
390				)
391		return s
392
393	"""
394	createXmlFooter
395
396	Purpose: returns the xml footer
397	"""
398	def createXmlFooter(self):
399		s = """</Connector>"""
400		return s
401
402	"""
403	sendError
404
405	Purpose: in the event of an error, return an xml based error
406	"""
407	def sendError(self, number, text):
408		self.setXmlHeaders()
409		s = ""
410		# Create the XML document header
411		s += """<?xml version="1.0" encoding="utf-8" ?>"""
412		s += """<Connector>"""
413		s += """<Error number="%s" text="%s" />""" % (number, text)
414		s += """</Connector>"""
415		return s
416
417	"""
418	getFolders
419
420	Purpose: command to recieve a list of folders
421	"""
422	def getFolders(self, resourceType, currentFolder):
423		if (self.isZope()):
424			return self.getZopeFolders(resourceType, currentFolder)
425		else:
426			return self.getNonZopeFolders(resourceType, currentFolder)
427
428	def getZopeFolders(self, resourceType, currentFolder):
429		# Open the folders node
430		s = ""
431		s += """<Folders>"""
432		zopeFolder = self.findZopeFolder(resourceType, currentFolder)
433		for (name, o) in zopeFolder.objectItems(["Folder"]):
434			s += """<Folder name="%s" />""" % (
435					self.convertToXmlAttribute(name)
436					)
437		# Close the folders node
438		s += """</Folders>"""
439		return s
440
441	def getNonZopeFolders(self, resourceType, currentFolder):
442		# Map the virtual path to our local server
443		serverPath = self.serverMapFolder(resourceType, currentFolder)
444		# Open the folders node
445		s = ""
446		s += """<Folders>"""
447		for someObject in os.listdir(serverPath):
448			someObjectPath = os.path.join(serverPath, someObject)
449			if os.path.isdir(someObjectPath):
450				s += """<Folder name="%s" />""" % (
451						self.convertToXmlAttribute(someObject)
452						)
453		# Close the folders node
454		s += """</Folders>"""
455		return s
456
457	"""
458	getFoldersAndFiles
459
460	Purpose: command to recieve a list of folders and files
461	"""
462	def getFoldersAndFiles(self, resourceType, currentFolder):
463		if (self.isZope()):
464			return self.getZopeFoldersAndFiles(resourceType, currentFolder)
465		else:
466			return self.getNonZopeFoldersAndFiles(resourceType, currentFolder)
467
468	def getNonZopeFoldersAndFiles(self, resourceType, currentFolder):
469		# Map the virtual path to our local server
470		serverPath = self.serverMapFolder(resourceType, currentFolder)
471		# Open the folders / files node
472		folders = """<Folders>"""
473		files = """<Files>"""
474		for someObject in os.listdir(serverPath):
475			someObjectPath = os.path.join(serverPath, someObject)
476			if os.path.isdir(someObjectPath):
477				folders += """<Folder name="%s" />""" % (
478						self.convertToXmlAttribute(someObject)
479						)
480			elif os.path.isfile(someObjectPath):
481				size = os.path.getsize(someObjectPath)
482				files += """<File name="%s" size="%s" />""" % (
483						self.convertToXmlAttribute(someObject),
484						os.path.getsize(someObjectPath)
485						)
486		# Close the folders / files node
487		folders += """</Folders>"""
488		files += """</Files>"""
489		# Return it
490		s = folders + files
491		return s
492
493	def getZopeFoldersAndFiles(self, resourceType, currentFolder):
494		folders = self.getZopeFolders(resourceType, currentFolder)
495		files = self.getZopeFiles(resourceType, currentFolder)
496		s = folders + files
497		return s
498
499	def getZopeFiles(self, resourceType, currentFolder):
500		# Open the files node
501		s = ""
502		s += """<Files>"""
503		zopeFolder = self.findZopeFolder(resourceType, currentFolder)
504		for (name, o) in zopeFolder.objectItems(["File","Image"]):
505			s += """<File name="%s" size="%s" />""" % (
506					self.convertToXmlAttribute(name),
507					((o.get_size() / 1024) + 1)
508					)
509		# Close the files node
510		s += """</Files>"""
511		return s
512
513	def findZopeFolder(self, resourceType, folderName):
514		# returns the context of the resource / folder
515		zopeFolder = self.getZopeUploadContext()
516		folderName = self.removeFromStart(folderName, "/")
517		folderName = self.removeFromEnd(folderName, "/")
518		if (resourceType <> ""):
519			try:
520				zopeFolder = zopeFolder[resourceType]
521			except:
522				zopeFolder.manage_addProduct["OFSP"].manage_addFolder(id=resourceType, title=resourceType)
523				zopeFolder = zopeFolder[resourceType]
524		if (folderName <> ""):
525			folderNames = folderName.split("/")
526			for folderName in folderNames:
527				zopeFolder = zopeFolder[folderName]
528		return zopeFolder
529
530	"""
531	createFolder
532
533	Purpose: command to create a new folder
534	"""
535	def createFolder(self, resourceType, currentFolder):
536		if (self.isZope()):
537			return self.createZopeFolder(resourceType, currentFolder)
538		else:
539			return self.createNonZopeFolder(resourceType, currentFolder)
540
541	def createZopeFolder(self, resourceType, currentFolder):
542		# Find out where we are
543		zopeFolder = self.findZopeFolder(resourceType, currentFolder)
544		errorNo = 0
545		errorMsg = ""
546		if self.request.has_key("NewFolderName"):
547			newFolder = self.request.get("NewFolderName", None)
548			zopeFolder.manage_addProduct["OFSP"].manage_addFolder(id=newFolder, title=newFolder)
549		else:
550			errorNo = 102
551		error = """<Error number="%s" originalDescription="%s" />""" % (
552				errorNo,
553				self.convertToXmlAttribute(errorMsg)
554				)
555		return error
556
557	def createNonZopeFolder(self, resourceType, currentFolder):
558		errorNo = 0
559		errorMsg = ""
560		if self.request.has_key("NewFolderName"):
561			newFolder = self.request.get("NewFolderName", None)
562			currentFolderPath = self.serverMapFolder(
563					resourceType,
564					currentFolder
565					)
566			try:
567				newFolderPath = currentFolderPath + newFolder
568				errorMsg = self.createServerFolder(newFolderPath)
569				if (errorMsg is not None):
570					errorNo = 110
571			except:
572				errorNo = 103
573		else:
574			errorNo = 102
575		error = """<Error number="%s" originalDescription="%s" />""" % (
576				errorNo,
577				self.convertToXmlAttribute(errorMsg)
578				)
579		return error
580
581	"""
582	getFileName
583
584	Purpose: helper function to extrapolate the filename
585	"""
586	def getFileName(self, filename):
587		for splitChar in ["/", "\\"]:
588			array = filename.split(splitChar)
589			if (len(array) > 1):
590				filename = array[-1]
591		return filename
592
593	"""
594	fileUpload
595
596	Purpose: command to upload files to server
597	"""
598	def fileUpload(self, resourceType, currentFolder):
599		if (self.isZope()):
600			return self.zopeFileUpload(resourceType, currentFolder)
601		else:
602			return self.nonZopeFileUpload(resourceType, currentFolder)
603
604	def zopeFileUpload(self, resourceType, currentFolder, count=None):
605		zopeFolder = self.findZopeFolder(resourceType, currentFolder)
606		file = self.request.get("NewFile", None)
607		fileName = self.getFileName(file.filename)
608		fileNameOnly = self.removeExtension(fileName)
609		fileExtension = self.getExtension(fileName).lower()
610		if (count):
611			nid = "%s.%s.%s" % (fileNameOnly, count, fileExtension)
612		else:
613			nid = fileName
614		title = nid
615		try:
616			zopeFolder.manage_addProduct['OFSP'].manage_addFile(
617					id=nid,
618					title=title,
619					file=file.read()
620					)
621		except:
622			if (count):
623				count += 1
624			else:
625				count = 1
626			self.zopeFileUpload(resourceType, currentFolder, count)
627		return
628
629	def nonZopeFileUpload(self, resourceType, currentFolder):
630		errorNo = 0
631		errorMsg = ""
632		if self.request.has_key("NewFile"):
633			# newFile has all the contents we need
634			newFile = self.request.get("NewFile", "")
635			# Get the file name
636			newFileName = newFile.filename
637			newFileNameOnly = self.removeExtension(newFileName)
638			newFileExtension = self.getExtension(newFileName).lower()
639			allowedExtensions = self.getAllowedExtensions(resourceType)
640			deniedExtensions = self.getDeniedExtensions(resourceType)
641			if (allowedExtensions is not None):
642				# Check for allowed
643				isAllowed = False
644				if (newFileExtension in allowedExtensions):
645					isAllowed = True
646			elif (deniedExtensions is not None):
647				# Check for denied
648				isAllowed = True
649				if (newFileExtension in deniedExtensions):
650					isAllowed = False
651			else:
652				# No extension limitations
653				isAllowed = True
654
655			if (isAllowed):
656				if (self.isZope()):
657					# Upload into zope
658					self.zopeFileUpload(resourceType, currentFolder)
659				else:
660					# Upload to operating system
661					# Map the virtual path to the local server path
662					currentFolderPath = self.serverMapFolder(
663							resourceType,
664							currentFolder
665							)
666					i = 0
667					while (True):
668						newFilePath = "%s%s" % (
669								currentFolderPath,
670								newFileName
671								)
672						if os.path.exists(newFilePath):
673							i += 1
674							newFilePath = "%s%s(%s).%s" % (
675									currentFolderPath,
676									newFileNameOnly,
677									i,
678									newFileExtension
679									)
680							errorNo = 201
681							break
682						else:
683							fileHandle = open(newFilePath,'w')
684							linecount = 0
685							while (1):
686								#line = newFile.file.readline()
687								line = newFile.readline()
688								if not line: break
689								fileHandle.write("%s" % line)
690								linecount += 1
691							os.chmod(newFilePath, 0777)
692							break
693			else:
694				newFileName = "Extension not allowed"
695				errorNo = 203
696		else:
697			newFileName = "No File"
698			errorNo = 202
699
700		string = """
701<script type="text/javascript">
702window.parent.frames["frmUpload"].OnUploadCompleted(%s,"%s");
703</script>
704				""" % (
705						errorNo,
706						newFileName.replace('"',"'")
707						)
708		return string
709
710	def run(self):
711		s = ""
712		try:
713			# Check if this is disabled
714			if not(self.enabled):
715				return self.sendError(1, "This connector is disabled.  Please check the connector configurations and try again")
716			# Make sure we have valid inputs
717			if not(
718					(self.request.has_key("Command")) and
719					(self.request.has_key("Type")) and
720					(self.request.has_key("CurrentFolder"))
721					):
722				return
723			# Get command
724			command = self.request.get("Command", None)
725			# Get resource type
726			resourceType = self.request.get("Type", None)
727			# folder syntax must start and end with "/"
728			currentFolder = self.request.get("CurrentFolder", None)
729			if (currentFolder[-1] <> "/"):
730				currentFolder += "/"
731			if (currentFolder[0] <> "/"):
732				currentFolder = "/" + currentFolder
733			# Check for invalid paths
734			if (".." in currentFolder):
735				return self.sendError(102, "")
736			# File upload doesn't have to return XML, so intercept
737			# her:e
738			if (command == "FileUpload"):
739				return self.fileUpload(resourceType, currentFolder)
740			# Begin XML
741			s += self.createXmlHeader(command, resourceType, currentFolder)
742			# Execute the command
743			if (command == "GetFolders"):
744				f = self.getFolders
745			elif (command == "GetFoldersAndFiles"):
746				f = self.getFoldersAndFiles
747			elif (command == "CreateFolder"):
748				f = self.createFolder
749			else:
750				f = None
751			if (f is not None):
752				s += f(resourceType, currentFolder)
753			s += self.createXmlFooter()
754		except Exception, e:
755			s = "ERROR: %s" % e
756		return s
757
758# Running from command line
759if __name__ == '__main__':
760	# To test the output, uncomment the standard headers
761	#print "Content-Type: text/html"
762	#print ""
763	print getFCKeditorConnector()
764
765"""
766Running from zope, you will need to modify this connector.
767If you have uploaded the FCKeditor into Zope (like me), you need to
768move this connector out of Zope, and replace the "connector" with an
769alias as below.  The key to it is to pass the Zope context in, as
770we then have a like to the Zope context.
771
772## Script (Python) "connector.py"
773##bind container=container
774##bind context=context
775##bind namespace=
776##bind script=script
777##bind subpath=traverse_subpath
778##parameters=*args, **kws
779##title=ALIAS
780##
781import Products.connector as connector
782return connector.getFCKeditorConnector(context=context).run()
783"""
784
785
786