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 < > and & 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, '&', '&') # must be done 1st 80 text = replace(text, '<', '<') 81 text = replace(text, '>', '>') 82 text = replace(text, '"', '"') 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