Copyright © 2004 本文遵从GNU 的自由文档许可证(Free Document License)的条款,欢迎转载、修改、散布。
发布时间:2004年9月17日
最近更新:2005年06月14日
Abstract
ZopeBook是Zope系统的官方资料,是我们学习Zope系统最重要的在线资料。本笔记基于zopebook2.7版本,英文版位于http://www.plope.com/Books/2_7Edition/bt_sections_view。
Table of Contents
Table of Contents
当你用zope建立一个基于Web的应用程序时,对象就是你这个应用程序的基础。这章主要介绍一些基础的zope对象。
Zope对象能帮我们处理不同的任务,不同的对象可处理应用程序中的不同部份。一些对象可保存内容(文本、电子表格和图象等);一些对象可处理逻辑操作,如接收Web表单的输入或执行脚本;一些对象可控制内容的显示,如显示网页、邮件等。
一般来说,基础的Zope对象有以下三类:
内容对象
内容对象可保存各种文本二进制数据,另外,内容对象还可存取外部数据,如系统文件和关系数据库。
显示对象
Zope提供两种对象来控制网站的显示,一种是DTML,一种是ZPT。两者间的不同点是,DTML同时处理显示和逻辑关系,ZPT则可与表现层分离。
逻辑处理对象
Zope提供了一些工具帮助我们处理商业逻辑。有三种对象可处理进行逻辑处理:DTML,python script,perl script。通过这些对象我们可以改变对象属性、发送信息和邮件、进行条件测试和对某些事进行动态反馈等。
DTML对象的分类有些不清,它即可作为显示对象,也可作为逻辑处理对象。我们还可安装第三方的Zope对象来扩展zope的功能。我们通常称这些对象为“产品”
Folders
Zope的Folder对象主要用于包含其它对象,包括Folder。文件夹的结构对于安全和显示很重要,建立时需考虑清楚。
Files
Zope的Files对象包含raw数据,象音频、视频和文档。它还可保存一些Files对象不支持的内容,如Flash文件、Java Applets等。Files对象保存内容时不考虑内容的类型,所以可以用Files对象来保存任意的数据。
每 个File对象都有一个叫“content type”的属性,该属性符合MIME标准。如:“text/plain(纯文本)”、“text/html(html格式)”、 “application/pdf(pdf格式)”。当你上传一个文件时,zope会根据文件名自动分配一种MIME类型给“content type”属性。
建立和编辑文件
从zmi 右上角的对象添加列表中选择“File”,按“Add”。并填上“id”(必填)、“title”(可选)。如果想建立一个空白文件,直接按“Add”即 可,如果想上传一个文件,可按“Browse”按钮,从本地选择一个文件上传。文件一旦建立,就可打开进行内容编辑或修改属性。也可通过“upload” 功能通过本地文件更新它。
浏览文件
编辑和上传完文件后,我们可通过“view”标签来浏览文件内容。当然 前提是要zope能识别该文件的类型,否则zope会弹出一个下载窗口,要求你下载该文件。你也可直接通过浏览器显示文件内容,例如有一个放在zope的 根目录下的test.pdf文件,你只要在浏览器输入Http://localhost:8080/test.pdf即可,
Images
Image对象包含如GIF,JPEG,PNG格式的文件,和文件对象很类似,但Image对象包含一些专有的属性,如图像的宽、高等属性。
表 现层和逻辑层应该分开。表现层负责显示静态和动态内容。一般是html文档。ZPT采用XML名称空间元素,能有效分离逻辑层和表现层。DTML采用 “tags”元素,所以表现层和逻辑层分离得不好。ZPT和DTML都是“server-side”技术,就象SSI,PHP,JSP一样。代码都由服务 器来执行。
“tag -based”的脚本语言不能很好地分离表现层和逻辑层。如DTML、SSI、PHP和JSP。这样使程序设计师和网页设计师不能很好地分工合作。为了有 效分离表现层和逻辑层,就产生了ZPT这种“attribute-based”的语言。这两种语言在zope中支持,而且为一直共存。ZPT和DTML的 功能有些重复,致使一些人感到困惑,不知怎么选择。下面有几点提示希望能帮到大家更好地使用这两种语言:
如果你的团队包含程序设计师和网页设计师的团队,就应该选择ZPT,以使团队能更好地分工合作。
ZPT建立的页面需是XHTML、XML兼容,而DTML不用。ZPT不能动态改变CSS样式表、SQL语句等,但DTML可很容易实现。
DTML能提供很好的条件控制语句,在这方面,DTML更像PHP、ASP这样的脚本语言。可以用DTML来替代它们。
表现层与逻辑层分离也不是适用于所有情况,所以有时DTML能比ZPT工作地更好。
DTML对象包括DTML文档和DTML方法两种。DTML对象受到zope安全机制的约束,所以能安全地显示内容。
DTML 文档和DTML方法有什么不同呢?表面上,两者都包含DTML代码和数据,都有相同的用户接口和API。DTML方法主要用于显示其它对象,DTML文档 用于显示本身的内容。DTML文档支持属性功能,DTML方法不支持属性。一般来说,你应该用DTML方法来存放DTML内容,除非你有一个好的理由需使 用DTML文档,如你需要使用对象属性。详细介绍请参考13、14章。
逻 辑对象返回数据给显示对象进行显示。在zope中内置的逻辑对象有python script和外部方法,这两种对象都是使用Python脚本语言来开发的。现在也可以使用perl script这个附加的逻辑对象了。好象也有一些产品可以在zope中使用php和jsp,如PHParser,PHPObject和ZopeJSP,但 我没试过。
Script (Python)对象是基于的Web的,受zope安全约束的python代码,并不是所有的python代码都可以在zope中运行。也就是说你不能导 入受限的Python模块和直接访问本地文件系统。这些特性能帮管理员更好地管理网站。Script(Python)对象的建立、编辑和更新同File对 象类似,这里不细讲了。详细内容中参考第9、16章。
外部方法和Script(Python)对象差不多,都是采用python脚本语言编写,作用也一样,但它们一些不同:
外部方法不能用ZMI编辑,它放到本地文件系统中的zope实例目录的Extensions目录下。
由于外部方法不在ZMI中编辑,所以它不受zope的安全机制约束,可以直接访问本地文件系统,能导入和执行任意的python模块和代码。
外部方法不支持“bindings”的概念。
首先,在zope实例目录的Extensions目录下建一个文本文件,并命名为“SalesEM.py”。在文件中写入以下内容:
def SalesEM(self, name="Chris"): id = self.id return 'Hello, %s from the %s external method' % (name, id)
接着在ZMI中建立一个External Method Object。“id”和“title”可随便起,这里填“SalesEM”。“Module Name”要填脚本名,这里是SalesEM.py。“Function Name”要填函数名,这里是SalesEM。填完按“Add”即可。
添加完成后就可进行测试,当前刚建立的外部方法,按“test”标签就可执行外部方法中定义的函数“SalesEM”了。
我们还可通过http请求把参数传递给外部方法。如:
http://localhost:8080/Sales/SalesEM?name=Fred 显示: Hello, Fred from the Sales external method
该应用很简单,只是计算所有债务利息的总计。
首先,建立一个页面模板表单“interestRateForm”,用于收集数据。代码如下:
<html> <body> <form action="interestRateDisplay" method="POST"> <p>Please enter the following information:</p> Your current balance (or debt): <input name="principal:float"><br> Your annual interest rate: <input name="interest_rate:float"><br> Number of periods in a year: <input name="periods:int"><br> Number of years: <input name="years:int"><br> <input type="submit" value=" Calculate "><br> </form> </body> </html>
提交该表单将会打开“interesRateDisplay”页面模板。
接着,建立一个脚本“calculateCompoundingInterest”,用于计算利息。它有四个参数,分别是:principal, interest_rate, periods,years。脚本的代码如下:
""" Calculate compounding interest. """ i = interest_rate / periods n = periods * years return ((1 + i) ** n) * principal
在parameters list栏填上参数,多个参数以逗号分隔。
最后建立一个页面模板“interestRateDisplay”以显示结果。代码如下:
<html> <body> Your total balance (or debt) including compounded interest over <span tal:define="years request/years; principal request/principal; interest_rate request/interest_rate; periods request/periods"> <span tal:content="years">2</span> years is:<br><br> <b>$ <span tal:content="python: here.calculateCompoundingInterest(principal, interest_rate, periods, years)" >1.00</span> </b> </span> </body> </html>
该页面由“interestRateForm”所调用,该页面里面又调用了“calculateCompoundingInterest”脚本,并把表单收集的四个参数传递给该脚本。最后把脚本计算出的结果显示出来。
有关错误处理,在上例中,“interestRateDisplay”页面模板不能按“test”标签进行测试,因为,它需要四个参数。所以如果按“test”标签,将会出现出错信息。如下所示:
Site Error An error was encountered while publishing this resource. Error Type: KeyError Error Value: years
它提示找不到yesrs变量,在zope根目录下的error_log文件中有更详细的出错日志可供查询。如:
* Module Products.PageTemplates.TALES, line 217, in evaluate URL: /Interest/interestRateDisplay Line 4, Column 8 Expression: standard:'request/years'
执行该应用只需打开“interestRateForm”页面,按“test”标签即可。
Zope自带有一个在线教程,你可以通过它来学习Zope。该教程可在对象添加列表中找到,添加后就可使用了。我已整理了一份The Zope Tutorial的学习笔记以供参考。
获取(Acquisition)是一种通过包含关系动态共享Zope对象的技术,在Zope中,该技术无处不在,DTML、ZPT、scrip甚至zope urls都有用到获取技术。所以在zope中理解它是很重要的。
继承是面向对象理论中的一个重要概念。使用继承,一个对象可以继承特定类的行为,在有需要时,还可以重载或增加一个类的行为。类的行为用方法来定义,属性也可通过继承获得。
python、java都是典型的面向对象的编程语言。通过类的继承能方便我们在子类中继承父类的各种方法和属性,加快我们的开发速度和增加软件架构的稳定性。具体有关面向对象编程方法这里就不讲了,请参考相应的书籍。下面是一个用python写的有关类继承的例子:
>>> class SuperA: ... def amethod(self): ... print "I am the 'amethod' method of the SuperA class" ... def anothermethod(self): ... print "I am the 'anothermethod' method of the SuperA class" ... >>> class SuperB: ... def amethod(self): ... print "I am the 'amethod' method of the SuperB class" ... def anothermethod(self): ... print "I am the 'anothermethod' method of the SuperB class" ... def athirdmethod(self): ... print "I am the 'athirdmethod' method of the SuperB class" ... >>> class Sub(SuperA, SuperB): ... def amethod(self): ... print "I am the 'amethod' method of the Sub class" ...
这里定义了三个类,Sub类型继承自SuperA类和SuperB类。下面是使用这些类的例子:
>>> instance = Sub() >>> instance.amethod() I am the 'amethod' method of the Sub class >>> instance.anothermethod() I am the 'anothermethod' method of the SuperA class >>> instance.athirdmethod() I am the 'athirdmethod' method of the SuperB class
当调用Sub类的amethod()方法时,由于子类已定义了该方法,所以系统就直接调用。但当调用Sub类的 anothermethod()方法和athirdmethod()方法时,在Sub类是没有这些方法的,但由于Sub类继承了SuperA类和 SuperB类。所以系统会到这两个类中查询该方法。从而打印出以上结果,这就是继承。在非Zope系统中通常是用以上的方法进行方法的继承的,但在 Zope中,对象是用获取(Acquisition)这种工具来搜索方法的。
获取的概念很简单:
一个对象是位于另一个对象内的。对象就象一个容器,包含其它对象。如:DTML_Example folder文件夹对象内有一个DTML Method对象叫“amethod”,则DTML_Example文件夹就是包含“amethod”对象的容器。
对象能获得所在容器的方法。
在Zope中,对象的继承机制先于获取机制。
我们现在用一个简单的例子来说明如何“获取”。在根目录中建一个叫“acquisition_test”的DTML Method。内容如下:
<html> <body> <p> I am being called from within the <dtml-var title> Folder! </p> </body> </html>
保存并使用“view”功能可看到以下输出:
I am being called from within the Zope Folder!
Zope根目录文件夹的标题名是“Zope”,所以这样显示。现在我们在根目录下建立一个文件夹叫“ AcquisitionTestFolder”,标题为“TheAcquisitionTest”。现在我们在 “AcquisitionTestFolder”文件夹下执行“acquisition_test”这个DTML Method,在浏览器上打入以下地址:http://localhost: 8080/AcquisitionTestFolder/acquisition_test。你将看到以下内容:
I am being called from within the TheAcquisitionTest Folder!
注意到了吗?在“ AcquisitionTestFolder”文件夹内并没有“acquisition_test”,但zope照样可找到该方法,并把“title”换 成“AcquisitionTestFolder”的标题。这就是获取的概念,很简单。如果对象名在当前文件夹内找不找,zope就会到包含它的上一层文 件夹内去找,一层层地找,直到找到对象为止。用这种方法,获取可为对象添加方法,如“ AcquisitionTestFolder”文件夹就通过获取增加了一个“acquisition\ _test”方法。
对 象可以通过获取而获得相应的服务,同时也对其它对象提供服务。这使得我们重用服务变得很简单和容易。例如我们在一个叫AFolder的文件夹内建立了一个 “Mail Host”对象,则在该文件夹的对象就获得了发邮件的功能,同时也为“AFolder”文件夹的中的所有子文件夹提供了相同功能。
有时我们可能需要从多层目录结构中进行获取,如:我们有一个DTML Method,叫“HappySong”,它位于根目录中。根目录下有一个多层目录分别是“Users”、“Barney”、“Songs”。现在我们执行以下语句:
/Users/Barney/Songs/HappySong
由于“HappySong”位于根目录,所以这三个目录都可以调用该方法。如上例这样调用时,Zope的搜索顺序是这样的,它先在 “Songs”目录中搜索,如果找不到该方法,则到上一层目录“Barney”中搜索,如果还是找不到,则再上一级到“Users”目录搜索;以此类推, 最后,在根目录找到。这就是搜索的路径。
获取不仅能搜索一个包含结构,也可进行上下文结构搜索。但上下文搜索很复杂。接上例,如果在root目录找到“HappySong”。则有两种有趣的结构:
因为“HappySong”在位于root目录,所以包含结构由root和它自已组成。
因为“HappySong”通过三个文件夹“Users”、“Barney”、“Songs”。所以上下文结构将会包含这三个对象。
获取功能只在在包含结构中搜索不到对象时才会通过上下文结构进行搜索。 |
如果大家想更深一层了解获取的工作原理,可以参考以下网址:http://zope.org/Members/jim/Info/IPC8/AcquisitionAlgebra/index.html。下面一节使用Zope调试器来演示两种获取结构。
在你的zope实例目录运行bin/zopectl debug,按以下步骤输入:
$ bin/zopectl debug Starting debugger (the name "app" is bound to the top-level Zope object) >>> app.manage_addFolder('Users') >>> users = app.Users >>> users.manage_addFolder('Barney') >>> barney = users.Barney >>> barney.manage_addFolder('Songs') >>> songs = barney.Songs >>> songs.aq_chain # show the whole chain [<Folder instance at f651b290>, <Folder instance at f651b260>, <Folder instance at f651b230>, <Application instance at f65b0290>] >>> songs.aq_inner.aq_chain # show only containment; here its the same [<Folder instance at f651b290>, <Folder instance at f651b260>, <Folder instance at f651b230>, <Application instance at f65b0290>]
在上面,我们可以看到“Songs”文件夹的包含结构,且包含结构和上下文结构一样。现在让我们建立一个“HappySongs”页面模板,并测试它们的结构,在本例中,它们还是一样的。
>>> app.manage_addDTMLMethod('HappySong', file="""\ ... <dtml-if favorite_color> ... My favorite color is &dtml-favorite_color;. ... <dtml-else> ... I don't have a favorite color. ... </dtml-if> ... """) '' >>> happy = app.HappySong >>> happy.aq_chain [<DTMLMethod instance at f649d050>, <Application instance at f65b0290>] >>> happy.aq_inner.aq_chain [<DTMLMethod instance at f649d050>, <Application instance at f65b0290>]
下面我们模拟用“/Users/Barney/Songs/HappySong”来访问。
>>> happy2 = app.unrestrictedTraverse('/Users/Barney/Songs/HappySong') >>> happy2.aq_chain [<extension class OFS.DTMLMethod.DTMLMethod at f6b0d980>, <extension class OFS.Application.Application at f6a234d0>] >>> happy2.aq_inner.aq_chain [<DTMLMethod instance at f651b320>, <Application instance at f65b0290>]
我们注意到,包含结构(通过happy2.aq_inner.aq_chain访问)和上面一样,但上下文结构改变了,包含了所有经过的文件夹。
现在,让我们尝试通过获取的两种结构来查找属性,首先,我们显示通过包含获取没有得到“favorite_color”:
>>> happy2(songs, {}, None) " I don't have a favorite color.\n"
我们通过上下文结构设置“favorite_color”属性,这样我们就可以使用它了。
>>> barney.favorite_color = 'purple' >>> happy2(songs, {}, None) ' My favorite color is purple.\n'
获 取使得对象方法行为可分布在系统各处,当你添加一个新对象时,不用为这个对象建立所有对象方法,你可只建立只用于该对象的方法行为,其余的可依靠其它对象 获取。这样你就可通过修改一个对象的方法行为而改变其它与该对象有获取关系对象的行为。这可大大增中Zope应用程序的灵活性和适应性。下来的一些章节都 会用到获取技术,有关获取技术的基础原理介绍可参考以下网址:http://www.zope.org/Documentation/Books/ZDG/current/Acquisition.stx
Table of Contents
可通过ZMI来管理Zope对象,也可通过脚本来管理,本章就是介绍如何用编程的方法来控制和管理Zope对象。
由 于Zope是一个Web应用服务器,所以可直接通过浏览器和它打交道,任何URL都被映射为Zope中的对象和方法,如“http: //www.ringkee.com/forum/”这个链接,会被zope映射为根目录下的forum文件夹,并会默认打开forum目录下的 index_html文档(如果有的话,否则会打开上一级目录下的index_html文档);如链接是“http: //www.ringkee.com/forum/test”,则会被zope映射为forum文件夹下的一个test方法,test方法操作对象并返回 结果。正如我们所知,打开“http://localhost:8080/”可进入“Zope Quick Start”页面,其实就是打开了根目录下一个index_html文档。我们可修改该文档,替换成自已设计的首页,这样就可通过“http: //www.ringkee.com”来访问我们的首页了。(关于如何把Zope默认的8080端口改为80端口请参考我整理的Zope how-to文档)
可通过“http://localhost:8080/mamage_main”直接打开zope根目录。如果配置虚拟目录出错,进不了ZMI,可通过这种方法进入zope的根目录,从而可以重新配置虚拟目录。
可通过“http://localhost:8080/manage_main?skey=meta-type”打开根目录,同时,把skey=meta-type这个参数传递给manage_main方法,这里的skey参数指定排序关键字为meta-type。
记住,在zope中,默认的页面是index_html,相当于apache中的index.html或index.htm。
通 过“acquire获取机制”,一个文件夹会自动获得父文件夹的index_html文件。所以当你在根目录下建立了一个空的文件夹test,当用 “http://localhost/test/”来访问时会自动打开根目录的index_html文档。如果不想显示父目录的index_html,而 要显示当前目录的index_html,只需在当前目录建立一个index_html文档即可。这种机制可使我们能方便地设计一个缺省页面,把它放到目录 的最顶层,当链接出错,找不到对象时,可借助acquire机制自动地打开该页面。
index_html可以是一个zpt、dtml、python script或任意可被浏览器访问和识别的Zope对象。
在Zope中,可以使用python编程语言,以操作对象。
从ZMI的右上角的对象列表中选中“Script(Python)”,按“Add”添加,接着输入id和title,最后按“Add”添加即可。
脚本对象的“parameter list”栏用于定义脚本需接收的参数。如在该栏内定义一个参数“name”,则可在代码栏中这样使用:
return "Hello %s." % name
编写完脚本后可按“test”标签进行测试,如果脚本需接收参数则会出现接收参数的画面,按要求输入完参数后按“runscrip”按钮即可。
在script中,你可用context变量来访问当前目录的Zope对象。如下代码可获得当前目录下的zope对象的id列表,并打印出列表的长度,也就是对象的个数。:
list=context.objectIds() print len(list) return printed
objectIDs()是文件夹类的一个方法,所以context对象应该是可包含在文件夹内的对象。
我们可以通过context来访问zope对象的属性,如下:
## Script (Python) "objectsForStatus" ##parameters=status ## """ Returns all sub-objects that have a given status property. """ results=[] for object in context.objectValues(): if object.getProperty('status') == status: results.append(object) return results
该代码可通过context和对象的getProperty()方法来获得当前目录下所有对象的“status”属性(status属 性需自已在对象的Properties标签内增加),并根据对象的“status”属性的值与参数“status”的值是否相等来操作results列 表。
我们可通过REQUEST对象来获得给HTTP传递的参数。如以下提交表单的请求:http://localhost:8080/test?forum_id=1。我们可通过context.REQUEST.forum_id来访问forum_id的值。
另 外一种方法是通过脚本的parameters来传递。如果我们定义REQUEST为脚本的参数,也就是把“REQUEST”填到parameters list栏内,则zope会自动地把http请求传递给这个参数,在脚本中我们就可能通过REQUEST.forum_id来访问forum_id变量。
在zope中由于安全原因不能在python script中使用正则表达式,但可使用python中的字符处理模块的功能。如果要在zope中使用正则表式可用外部方法来实现。
一个处理字符串替换的例子:
## Script (Python) "replaceWord" ##parameters=word, replacement ## """ Replaces all the occurrences of a word with a replacement word in the source text of a text file. Call this script on a text file to use it. Note: you will need permission to edit the file in order to call this script on the *File* object. This script assumes that the context is a *File* object, which provides 'data', 'title', 'content_type' and the manage_edit() method. """ text = context.data text = text.replace(word, replacement) context.manage_edit(context.title, context.content_type, filedata=text)
replace()函数用于替换字符串,该函数的详细参考请查阅我整理的Python参考篇。 你可以在Web中执行该脚本,如把URL指向Spam/replaceWord?word=Alligator&replacement= Crocodile,则会把Spam文件中所有的“Alligator”替换成“Crocodile”。不过有一个前提是要确保Spam是一个文件对象 (File)。manage_edit()是一个文件对象方法。
我们可用字符串处理程序进行内容搜索,但在zope中提供了一个更好的选择---Catalog。Catalog的用法请参考“内容的搜索和分类”这一章。
在script中另一个常用功能是进行数学计算,这在ZPT和DTML中是较难实现的。math和random两个模块提供大量数学计算的函数,可使我们在zope中进行各种数学计算。
下面一个利用random模块随机显示图片的例子,它利用了random模块中一个很有趣的函数choice(),它可随机地从一个对象列表中返回对象。
## Script (Python) "randomImage" ## """ When called on a Folder that contains Image objects this script returns a random image. """ from random import choice return choice( context.objectValues(['Image']) )
如果你有一个文件夹,里面包含很多图象文件,你可以用zpt调用上面的script来随机显示文件夹中的图象。只要把以下语句加到 zpt中即可:<span tal:replace="structure context/Images/randomImage" />,randomImage脚本应该在Images目录可它的父目录中。
在zope中可通过printed变量显示print的输出。如:
## Script (Python) "printExample" ## for word in ('Zope', 'on', 'a', 'rope'): print word return printed
返回:
Zope on a rope
该变量可用于调试和返回信息给zpt和dtml。
zope中的python script的内建功能和python的内建功能有一些不同,这些不同主要是基于安全考虑,如open这个函数在zope的python script是没有的,因为该函数可直接访问文件系统,存在安全问题。
大 部份python的内建功能在zope中都是可用的,如:None, abs, apply, callable, chr, cmp, complex, delattr, divmod, filter, float, getattr, hash, hex, int, isinstance, issubclass, list, len, long, map, max, min, oct, ord, repr, round, setattr, str, tuple。
range和pow在zope中也是可用的,但限制了可生成的最大数值和最大序列以防止DOS攻击。
same_type()可比较两个对象的类型,相同则返回真,否则返回假。
脚本有利于计算和表示逻辑关系,而zpt有利于显示界面,所以我们可利用这个特点把逻辑层和表现层分开。脚本负责逻辑层,zpt负责表现层。下面以一个例子说明一下这种关系,首先建立一个名为hello_world_pt的zpt模板文件,用于显示内容:
<p>Hello <span tal:replace="options/name | default">World</span>!</p>
建立一个script处理逻辑状态
return context.hello_world_pt(name="John Doe")
最终显示为:
<p>Hello John Doe!</p>
脚本的name变量的值传递给了options/name,从面显示以上内容。
在python script中使用的对象id名不能用“.”分隔,因为在python script中,点用于分隔对象,所以在zope的默认页面要写成index_html,而不是通常使用的index.html。 hello_world_pt不能写成hello_world.pt。但如果一定要在ids中用“.”,则可以用getattr()函数来获得zpt。如
return getattr(context, 'hello_world.pt')(name="John Doe")
和zpt一样,我们也可在脚本中调用dtml,例如在脚本中调用一个a_dtml_method:
# grab the method and the REQUEST from the context dtml_method = context.a_dtml_method REQUEST = context.REQUEST # call the dtml method, for parameters see below s = dtml_method(client=context, REQUEST=REQUEST, foo='bar') # s now holds the rendered html return s
在zope中脚本就像一个函数或方法,可返回值。如果返回的是多值,则zope把它们放到列表或字典中。下面是一个返回一个字典的例子:
## Script (Python) "compute_diets" d = {'fat': 10, 'protein': 20, 'carbohydrate': 40, } return d
你可在下面的zpt中显示这些值:
<p tal:repeat="diet context/compute_diets"> This animal needs <span tal:replace="diet/fat" />kg fat, <span tal:replace="diet/protein" />kg protein, and <span tal:replace="diet/carbohydrate" />kg carbohydrates.</p>
编写脚本的一个重要资源是访问Zope API,它描述了Zope对象的内建动作,熟悉它方便我们使用Zope对象。Zope API文档可在zope系统的在线帮助里查到,也可到zope的官方网站上去查询最新的版本。下面举几个使用Zope API的例子。
objs = context.objectValues(),获得当前文件夹中的所以对象列表。
id = context.getId(),获得当前文件夹id。
root = context.getPhysicalRoot(),获得zope的根路径名。
restrictedTraverse()是getPhysicalRoot()的补充,它使用相对路径。
path = "/Zoo/LargeAnimals/hippo" hippo_obj = context.restrictedTraverse(path)
使用manage_edit()可动态改变dtml和文档的内容。
# context has to be a DTML method or document! context.manage_edit('new content', 'new title')
getProperty()可返回对象的属性。
pattern = context.getProperty('pattern') return pattern
manage_changeProperties()可改变对象的属性,需改变对象的属性一定要存在。
values = {'pattern' : 'spotted'} context.manage_changeProperties(values)
改变对象的属性。
path = "/Zoo/LargeAnimals/hippo" hippo_obj = context.restrictedTraverse(path) hippo_obj.manage_addProperty('weight', 500, 'int')
在上下文中增加一个对象
context.manage_addProduct['PackageName'].manage_addProductName(id)
内建zope类型的PackageName为“OFSP”
一个例子,增加一个dtml方法:
add_method = context.manage_addProduct['OFSP'].manage_addDTMLMethod add_method('object_id', file="Initial contents of the DTML Method")
对于其它类型,我们只要改变方法因子和参数即可。
如果要添加Folders,则用manage_addFolder。
如果要添加 UserFolders,则用manage_addUserFolder。
如果要添加 Images,则用manage_addImage。
如果要添加 Files,则用manage_addFile。
如果要添加 DTML Methods,则用manage_addDTMLMethod。
如果要添加 DTML Documents,则用manage_addDTMLDocument。
如果要添加 Version,则要用manage_addVersion。
如果你要在另外安装的产品目录下添加对象,则需用产品的安装目录替找“OFSP”即可,例如你安装了一个产品叫Boring,则添加对象的写法如下:
dd_method = context.manage_addProduct['Boring'].manage_addBoring add_method(id='test_boring')
删除对象使用以下方法
folder_object.manage_delObjects(id)_
更新对象内容使用以下方法
object.manage_upload(content)
Table of Contents
Zope页面模板是一种网页生成工具,它能帮助程序员与网页设计师协同开发zope应用程序。本章只介绍页面模板的基础功能,高级内容在高级页面模板这章介绍。
页面模板设计的目的是使程序员和网页设计师能够很好地一起协同工作。网页设计师可以用WYSIWYG的编辑器来建立模板,接着,程序员可编辑模板,增加逻辑处理功能,把模板组合到应用程序中。如果需要,网页设计师还可以把模板文件调出来修改而不会破坏程序员所做的工作。
页 面模板使用的是TAL(Template Attribute Language),DTML Methods、DTML Documents和SQL Methods使用的是DTML(Document Template Markup Language)。在zope中存在两种模板语言的原因主要有两个:
历史的原因,页面模板是一种新的技术,在zope2.5时才第一次发布,而DTML已经存在很长时间,有大量的基于DTML的产品和应用,很多人都还在使用DTML进行开发。
页 面模板和DTML都有各自的优缺点,页面模板能实现表现层、逻辑层和数据层分离,而DTML一旦嵌入html,就很难修改。还有页面模板能使你更好地控制 名字空间,而不象DTML,太灵活了,难于控制。但DTML在动态生邮件信息和SQL查询方面有优势,所以DTML还是应该学习一下的。
页面模板有两种模式:HTML Mode和XML Mode。多数使用HTML Mode,对于HTML Mode,Content-Type须设为text/html。
可以使用HTML编辑器来编写页面模板而不会显示TAL标记。
一个页面模板的例子:
<h1 tal:content="context/title">Sample Page Title</h1>
tal:content属性就是一个TAL语句,它有一个有效的XML名字空间(tal),所以有大多数编辑器都可以支持它。 content代表h1中包含的是文本,context/title是一个代表式,它提取所在文件夹的标题以替代“Sample Page Title”。
所有的TAL语句都以“tal:”开头。
建立一个页面模板只需从右上角 的对象添加列表选择“Page Template”即可。在页面模板中,“title”可填写模板的标题,“context-type”填模板的模式,默认是“text/html”。按 “Browse HTML source”可显示HTML的效果,但不解释tal标记。“Expand macros when editing”可控制是否在显示HTML时显示宏。
“template/title”是一个TAL的路径表达式,这种表达式在TALS中是很常用的,它获得模板的标题。
“context/objectValues”列出模板文件所在文件夹中所有对象。
“request/URL”获得当前请求的web地址。
“user/getUserName”获得已验证用户名。
路径表达式是默认的表式法,所以不用加前缀,如果要用python表达式,需加“python:”前缀。
以上面的例子为例,模板页面的写法如下:
<h1 tal:content="context/title">Sample Page Title</h1>
用python表达式可重写为:
<h1 tal:content="python: context.getProperty('title')">Sample Page Title</h1>
“python:variable1 == variable2”可用于进行比较运算。
“python:context.objectValues(['Folder'])”可把Folder传递参数给objectValues方法。
模板中的内容是一个模式,可通过TAL指令来控制如何动态替换模板内容。TAL的属性就是这样一组指令,它控制如何动态地替换、删除和重复模板内容。
上面介绍过tal:content可替换内容,替换完成后可保持原来的html元素属性。下面介绍的tal:replace也用于替换内容,但替换后不保持原来的html元素属性。如下:
<p>The URL is <span tal:replace="request/URL"> http://www.example.com</span>.</p>
当你预览网页时,你只会看到“The URL is http://www.example.com.”,但当你访问该网页时它才会自动用当前路径替换它,你会看到“The URL is http://localhost:8080/template_test/simple_page”这样的实际路径。
tal:repeat用于重复内容。
<p tal:repeat="number python: range(4)" tal:content="number"> 999 </p>
range(4)会生成一个包含[0,1,2,3]的列表。repeat会遍历这个列表,并打印出它的值。
<table border="1" width="100%"> <tr> <th>Id</th> <th>Meta-Type</th> <th>Title</th> </tr> <tr tal:repeat="item context/objectValues"> <td tal:content="item/getId">Id</td> <td tal:content="item/meta_type">Meta-Type</td> <td tal:content="item/title">Title</td> </tr> </table>
repeat还可遍历对象列表,如上所示,getId方法可获得的id,myta_type属性是元数据类型,title属性是标题。
在页面模板中,你可以根据条件动态地查询信息和可选地替换内容。这就要用到tal:condition这个模板属性了。
在zope中,零值、空串、空列表和内建变量nothing代表假,其它的为真。下面一个例子说明condition的用法:
<table tal:condition="python: context.objectValues(['Folder'])" border="1" width="100%"> <tr> <th>Id</th> <th>Meta-Type</th> <th>Title</th> </tr> <tr tal:repeat="item python: context.objectValues(['Folder'])"> <td tal:content="item/getId">Id</td> <td tal:content="item/meta_type">Meta-Type</td> <td tal:content="item/title">Title</td> </tr> </table>
上例将列出当前文件夹下所以子文件夹的id,meta-type和title。如果不存在文件夹,则不会显示表格。
tal:attributes可替换元素的属性。如下例所示,img标签的src属性被item/icon属性替换了。
<table tal:condition="python: context.objectValues(['Folder'])" border="1" width="100%"> <tr> <th>Id</th> <th>Meta-Type</th> <th>Title</th> </tr> <tr tal:repeat="item python: context.objectValues(['Folder'])"> <td tal:content="item/getId">Id</td> <td><img src="/misc_/OFSP/File_icon.gif" tal:attributes="src item/icon" /> <span tal:replace="item/meta_type">Meta-Type</span></td> <td tal:content="item/title">Title</td> </tr> </table>
当你保存和按“test”标签进行测试,zope能帮你发现和定位页面模板的错误。保存时主要检测页面模板的语法错误,测试时主要检测逻辑错误。根目录下的error_log文档是一个日志,记录有系统出错的信息,还可通过配置控制日志记录的内容。
你可把需重复使用的表现形式和页面风格定义为宏,从而使它可在其它页面重用,使网站保持统一的页头、页脚和导航等。
定义宏时,用一种叫Macro Expansion Tag Attribute Language(METAL)的指令。这里有一个例子:
<p metal:define-macro="copyright"> Copyright 2001, <em>Foo, Bar, and Associates</em> Inc. </p>
这里定义了一个叫“copyright”的宏,它包含一些版权信息。
现在可在其它页面模板中使用该宏了,假设宏保存在名为“standard_template.pt”的页面模板。在其它页面模板中调用宏的方法如下:
<hr /> <b metal:use-macro="container/standard_template.pt/macros/copyright"> Macro goes here </b>
这样,当宏改变时,所有引用宏的页面都自动跟着更新。这样就可确保网站的表现统一。
在定义宏的页面模板中,宏的名字必须唯一。
在metal:use-macro语句中可以使用路径表达式,只要它能返回一个宏,如:
<p metal:use-macro="python:context.getMacro()"> Replaced with a dynamically determined macro, which is located by the getMacro script. </p>
在上例中,宏由getMacro()脚本动态生成。
在metal:use-macro中你可以使用“default”变量,表示没有使用宏。如:
<p metal:use-macro="default"> This content remains - no macro is used </p>
该变量可用于根据条件选择使用或不使用宏。
如果在页面模板编辑界面选中“Expand macros when editing”,则在页面中使用的宏就会在页面模板中展开。
我们可利用slots来在Macro中定制可选内容,如一个宏如下:
<div metal:define-macro="sidebar"> Links <ul> <li><a href="/">Home</a></li> <li><a href="/products">Products</a></li> <li><a href="/support">Support</a></li> <li><a href="/contact">Contact Us</a></li> </ul> </div>
这个宏已可正常使用,但有时在个别页面中会需要有一些附加的内容。这样我们就可以在宏中定义一个solts。如下所示:
<div metal:define-macro="sidebar"> Links <ul> <li><a href="/">Home</a></li> <li><a href="/products">Products</a></li> <li><a href="/support">Support</a></li> <li><a href="/contact">Contact Us</a></li> </ul> <span metal:define-slot="additional_info"></span> </div>
这样,我们在页面模板中使用宏的时候就可以可选地使用这个slot以插入附加内容。如下所示:
<p metal:use-macro="container/master.html/macros/sidebar"> <b metal:fill-slot="additional_info"> Make sure to check out our <a href="/specials">specials</a>. </b> </p>
这样,就可在这个页面中增加了一个叫specials的链接。
我们知道,利用slots能够可选地增加内容,所以我们可利用这种特性,建立一个完全可定制的默认页面。如下所示:
<div metal:define-macro="sidebar"> <div metal:define-slot="links"> Links <ul> <li><a href="/">Home</a></li> <li><a href="/products">Products</a></li> <li><a href="/support">Support</a></li> <li><a href="/contact">Contact Us</a></li> </ul> </div> <span metal:define-slot="additional_info"></span> </div>
这个Macro是完全可定制的,你可重新定义links的内容以适应新的要求,如果你不重新定义links的内容,则利用上面已经定义的默认内容。
Slot可嵌套,可以在一个slot里面再定义一个slot。这为我们定制界面提供了最大的灵活性,下面是一个改进版的例子:
<div metal:define-macro="sidebar"> <div metal:define-slot="links"> Links <ul> <li><a href="/">Home</a></li> <li><a href="/products">Products</a></li> <li><a href="/support">Support</a></li> <li><a href="/contact">Contact Us</a></li> <span metal:define-slot="additional_links"></span> </ul> </div> <span metal:define-slot="additional_info"></span> </div>
如果你想完全改变links的内容,就可以以新的内容填充links即可,如果你不想全部改变,只是想增加一些新的内容,就可用嵌套的additional_links宏。在zope中,这种嵌套的层次可不受限制。
你可在同一上标签中使用METAL和TAL,例如:
<ul metal:define-macro="links" tal:repeat="link context/getLinks"> <li> <a href="link url" tal:attributes="href link/url" tal:content="link/name">link name</a> </li> </ul>
在这个例子中,getLinks是一个脚本,它返回链接对象的列表。METAL指令先于TAL指令执行,指令间不会存在冲突问题。我们可利用这种方式替代slot以定制宏,宏利用getLinks脚本可动态返回links,这样就可动态地改变链接的内容。
一般来说,我们利用slot来定制我们界面,用脚本来提供动态的内容。
利用slots,我们可用宏定义整个页面,从而提供一个统一的网站表现形式。例如:
<html metal:define-macro="page"> <head> <title tal:content="context/title">The title</title> </head> <body> <h1 metal:define-slot="headline" tal:content="context/title">title</h1> <p metal:define-slot="body"> This is the body. </p> <span metal:define-slot="footer"> <p>Copyright 2001 Fluffy Enterprises</p> </span> </body> </html>
宏“page”定义了三个slots,分别是“headline”、“body”和“footer”。我们可在其它页面使用它:
<html metal:use-macro="container/master.html/macros/page"> <h1 metal:fill-slot="headline"> Press Release: <span tal:replace="context/getHeadline">Headline</span> </h1> <p metal:fill-slot="body" tal:content="context/getBody"> News item body goes here </p> </html>
这里重新定义了“headline”和“body”。其实该功能和样式表的原理差不多,目的都是为了在整个网站内保持风格的一致。在zope中,已预设了一个“standard_template.pt”文件,里面定义了head和body两个slot的宏“page”。
有 一点需指出的是,我们如何定位宏的路径,有三种选择,一个是root,直接使用根目录下的宏;一个是container;一个是context。 container和context之间的差主要是在如何继承关系方面。container以文件夹为起点,context以对象为起点。
zope支持内容组件、表现组件和逻辑组件,页面模板是一种表现组件,但它也可作为显示内容的组件。
zope中的内容组件有ZSQL Methods,Files和Images。DTML文档和方法因为可以在里面执行代码,所以不算纯内容组件。你可以在Files中编辑和存储小于64K的文本文件。但Files对象很简单,不支持元数据和很多其它特性。
zope 的CMF通过提供一个“rich”内容组件有效解决了这些问题。CMF是zope的一个内容管理插件,它扩展了zope有关内容管理方面的功能,提供如工 作流、skins和内容对象等功能。听说Zope3里已包含了所有CMF的功能,这样Zope3在内容管理方面的功能就比Zope2强大很多了。
Table of Contents
Zope是一个多用户系统,但它的安全机制不依靠操作系统的用户帐号,而是有自已的一个用户数据库。通过该用户数据库的授权和验证就可访问Zope应用程序和管理Zope系统。最重要的一点是zope的用户和操作系统的用户是完全没关系的。
在Zope中,通过Zope安全策略设置用户的权限,管理员有权设置相应的权限以适应你的商业要求。而且,使用安全策略还可以在Zope站点的不同地方设置“代理管理员”,代理管理员只在被授权管理的区域有管理员的权限。这样可实现分级管理,有效减轻管理员的工作量。
当一个用户访问受保护的资源时(如一个DTML方法),Zope将会弹出一个验证窗口,用户一旦填完提交,Zope就用这个信息来查找用户,默认,Zope使用Http基本验证方式。
基于cookie-based的验证可把cookie信息存在站点的文件夹中。
Zope通过验证窗口提供的用户名和密码进行标识用户。当在用户数据库找到相同的用户名时,则这个用户就可被Zope标识了。
用 户一旦被标识,验证可能会进行,也可能不需做验证。如果用户不访问受限资源,Zope会信任这个用户,而不做密码验证。匿名用户访问公共网站就是这种情 况。一旦用户需访问受限资源,验证就会发生,只要前面输入的密码和数据库该用户的密码一样,则验证成功。ZMI就是一种受限资源,所以我们每次访问时都需 输入用户名和密码以进行标识和验证。
一旦用户通过验证,Zope就会确认该用户可以访问什么受限资源,这个过程称为“授权”。记住,Zope唯一需要验证信息的原因是因为用户需访问不被匿名用户访问的资源。
授权的过程包含有两个媒介,角色和许可。角色表示用户可以做什么,如管理员、作者,编辑等,这些角色由管理员管理。用户可能会有一个或多个不同的角色;对象许可表示可以对对象做些什么操作,如查询、删除和管理属性等。这些许可可在Zope系统内或Zope产品中定义。
Zope 中的上下文可表示为Zope 对象结构中的一个“位置”。由于安全性要求,上下文对象存储在zope对象数据库中。我们可以这样描述一个上下文,Examples文件夹位于Zope根 文件夹内;或者也可这样说,一个名为show_css的DTML方法对象位于Zope根文件夹内。实质上,上下文可以看成对象在Zope对象数据库中的位 置,可用路径来描述。在对象数据库中的每一个对象都有一个和Web管理接口关联的安全策略。为了减轻建立安全策略的负担,所有对象都可从包含对象中获取安 全策略。这样也能大大减轻安全策略的维护量。只当有个别例外情况下,才为单独的对象指定不同的安全策略。
实质上,安全策略是通过角色来控制 用户对上下文可以进行什么操作,换句话说,也就是“谁”在“哪里”可以做“什么”。例如:一个文件夹(上下文)的安全策略允许“Manager”角色对该 文件夹有“删除对象”的许可。这个安全策略允许管理员在这个文件夹内删除对象。如果一个DTML Method在这个文件夹被建立,而且我们没有对该对象指定独立的安全策略,而是获取自文件夹,则管理员就有权删除它。在该文件夹下建立的子文件夹同样也 获取了该安全策略,除非你在子文件夹中重新指定新的安全策略。
Zope提供一个默认的用户,用户名由建立实例时由用户命名。该用户拥用管理员角色,允许我们管理实例的对象。为了使其它用户可以登录入Zope,我们可以建立一个新的用户。
用户帐号由用户对象定义,一个Zope用户有一个名称、一个密码、一个或多个角色和几个属性。指定角色是为了方便控制用户在Zope中的行为。
一 个Zope用户必须在用户文件夹内定义,用户文件夹内包含了所有的Zope用户帐号。用户文件夹通常有一个叫“acl_users”的id号。在一个 Zope实例中可以存在多个用户文件夹,但在一个文件夹内只可包含一个用户文件夹。建立用户的方法很简单,我们只要进行用户文件夹,按“Add”按钮即可 进入增加用户的窗口,在增加用户窗口填上用户名和密码,如果现在要指定角色就可在下面的Roles栏中选择相应的角色,默认有两个,一个是 Manager,另一个是Owner。Manager角色使你拥有管理zope系统的权限,Owner角色已在多数情况已不用了,它的存在只是为了系统向 后兼容。因为如果你把Owner角色指定给用户,则该用户就会变成用户文件夹所在的文件中所有对象的拥有者,包含该文件夹的子文件夹。这显然是不合适的。 所以一般都不用该角色了。我们也可自定义角色,这在下面的内容中会讲到。Domains是用来控制用户只能由该域登录,该选项可增加系统的安全性。例如我 只想我的用户只能从“myjob.org”登录,就可把它填到Dominos栏内,该可填多条项目,也支持用IP,如“192.168.1.*”。最后按 “Add”完成用户添加,这时你可在用户文件夹内看到刚建的用户了。基本的用户帐号不支持邮件地址、电话号码等附加的属性。不过我们可通过使用扩展用户文 件夹来获得这些功能,如exUserFolder等。还有一点,用户帐号不能在两个用户文件夹间进行复制和粘贴操作。
通过单击用户文件夹中的用户名就可进行编辑,内容和新增用户时的差不多。在基本用户文件夹中,除用户名外,其它的内容都可修改。如果要修改用户名,就只有删除再重建了。
我们不能通过管理界面来找到用户的密码,即使是管理员也不行,所以如果用户丢失了密码,只有通过管理员从新设置一个新的密码,而旧的密码就永久丢失了。
和 其它的Zope管理功能一样,编辑用户功能也是受安全策略保护的,用户只有在所属用户文件夹内拥有“Manage Users”许可才能更改密码。我们都希望用户可以自由地修改自已的密码,但如果开放了用户的“Manage Users”许可,那该用户也就获得了修改同一用户文件夹内其它用户密码的许可,这可不是我们想要的。为了把修改用户密码的功能下放给用户,而不影响其它 用户。我们可以通过设置“Proxy Roles”来执行一段修改代码来实现。有关代码和代理角色下面会详细介绍。网上也有一篇关于如何通过代理角色修改自已密码的文章,网址是http://www.zope.org/Members/msx/ChangeOwnPassword-mini-howto
用户文件夹和一般文件夹差不多,你可以建立,编辑和删除文件夹内的对象,但你不能在用户文件夹内进行复制和粘贴。
在Zope的对象数据库结构中,允许在不同的位置建立多个用户文件夹。一个用户文件夹内的用户只能访问该用户文件夹所在文件夹的受限资源。所以用户文件夹的位置确定了用户可访问资源的区域。
如 果你在根目录的用户文件夹下定义了一个用户,则该用户就可访问根目录下的受限资源。你也可在Zope中的任意文件夹内建立用户文件夹。当你在一个子文件夹 内建立一个用户文件夹时,该用户文件夹内的用户只可访问这个子文件夹内的受限资源。一个例子:考虑一个位于 /BeautySchool/Hair/acl_users的用户文件夹。test是该用户文件夹下的一个用户。test不能访问 /BeautySchool/Hair文件夹的上一级文件夹的受限资源,他只能访问/BeautySchool/Hair文件内和该文件夹下其它子文件夹 的受限资源。如果test被赋予“Manager”角色,则他可通过/BeautyScholl/Hair/manage路径来管理该文件夹下的资源,但 不能用/BeautySchool/manage路径访问。把路径指向有管理员角色的用户文件夹所在的目录,可打开ZMI,通过验证后就可进行管理工作。 当然了,用户权限区域只在访问受限资源时有意义,如果我们访问不受限资源,Zope就不会进行验证和授权的工作。
zope中这种权限区域划分管理的概念可方便网站管理员把管理权细化,并下放到下一级管理员,大大减轻管理员的工作量。
有时,你不想用Zope自带的用户文件夹来管理你的用户,因为你可能已有一个用户数据库,或者你想用一些更好的工具来管理用户数据。Zope允许我们使用其它的存储方式来存储用户信息。你可到Zope网站的产品区找到很多相关的产品,如:
可插式用户文件夹。它提供一套插件,可根据请求来生成用户。
可扩展式用户文件夹。可通过不同的数据源来验证用户信息,如数据库、RADIUS、SMB或者ZODB。
这个用户文件夹用Unix/etc/passwd风格的文件来验证用户信息
这个用户文件夹允许我们通过一个LDAP服务器来进行用户验证。
通过NT的用户帐号来验证,Zope服务器必须运行在Windows平台下。
该用户文件夹把验证信息存储在MySQL数据中。
以上的用户文件夹给我们提供了多种选择,但登录和验证的过程都是一样的。在Zope中,有几个特殊帐号是不能通过这些用户文件夹来管理的。下一节就会讲到了。
Zope 有三个特殊帐号,它们不通过用户文件夹来定义。这三个帐号分别是:anonymous user(匿名用户)、emergency user(紧急用户)、initial manager(初始管理员用户)。匿名用户使用得最多,紧急用户帐号和初始管理员帐号较少使用,但他们很重要。
匿 名用户不需验证,如果你没有用户帐号,就默认为匿名用户。匿名用户拥有Anonymous角色,可访问公共信息。你还可通过设置安全策略使它拥有更多的权 限。即使你拥有用户帐号,但当你访问公共信息时,Zope会一直把你当成匿名用户,直到你访问受限资源时,通过输入用户名和密码成功验证后,才成为特定的 用户。
紧 急用户不受一般安全策略的约束,但它除用户对象外,不能建立任何其它新的对象。当你用紧急用户帐号建立“content”,“logic”、 “presentation”对象时,系统将提示“ Error caused by trying to create a new object when logged in as the emergency user.”出错信息。紧急用户的主要作用有两个:
修正错误的许可。如果你设置了错误许可而被系统拒于门外时,就可用紧急用户帐号登录入管理界面并修改错误。
建立和修改用户帐号。紧急用户帐号一个典型应用就是用它来修改管理员帐号,当你丢失管理员用户名或密码时,就不会被系统拒于门外了。
和Zope中其它用户帐号的建立方法不同,建立紧急用户帐号要在文件系统中通过命令来建立。这个命令叫zpasswd.py,位于ZOPE_HOME的utilities目录下。具体操用如下:
$ cd (... where your ZOPE_HOME is... ) $ python zpasswd.py access Username: superuser Password: Verify password: Please choose a format from: SHA - SHA-1 hashed password CRYPT - UNIX-style crypt password CLEARTEXT - no protection. Encoding: SHA Domain restrictions:
设置了紧急用户帐号了,需重启服务器帐号才会生效。
初始管理员帐号由Zope安装程序建立,用于第一次登录系统。我们也可用zpasswd.py程序来管理,如:
$ cd ( ... were your ZOPE_HOME is ... ) $ python zpasswd.py inituser Username: bob Password: Verify password: Please choose a format from: SHA - SHA-1 hashed password CRYPT - UNIX-style crypt password CLEARTEXT - no protection. Encoding: SHA Domain restrictions:
当你启动zope服务器时,如果root下面的用户文件夹内没有用户,而inituser文件又存在话,那么存在inituser里的 用户就会在root下的用户文件夹内创建。如果root下面的用户文件夹已存在用户,则inituser就不会使用。这个帐号文件很少使用。当你用一个新 的数据库启动服务器时(旧的var/Data.fs被删除了),你才需要它。
zope 的用户文件夹为了兼容所有浏览器,使用了基本的HTTP验证协议来验证用户,信息经过线路时很容易被截获并解码。所以如果你想建立一个高安全性的zope 服务器。你需用SSL连接你的服务器。一种实现方法是用apache等支持SSL的Web服务器作为Zope的前端。这样位于后端的Zope服务器就会受 到保护,这里有一篇apache+zope+ssl的集成文档http://zope.org/Members/unfo/apache_zserver_ssl。大家可参考一下。本书的“虚拟主机”一章会介绍一些相关的知识。
Zope 安全策略控制授权,它定义谁在哪里可以做什么。安全策略可以描述为如何用角色和许可来配置上下文中的对象。角色可把用户分类,许可用以保护对象。所以安全 策略也就是定义一类用户(角色)可以在网站中做什么(许可)。比指定一个用户对一个对象可以做什么更好的方法是指定一类用户在网站的一个区域可以做些什 么。这可使我们的安全策略简单而有力。当然,Zope两种方式都能很好地支持。
zope 用户可赋予定义了许可的角色,如Manager(管理员)、Anouymous(匿名用户)和Authenticated(验证用户)。zope中的角色 类似于unix系统中的组,每个zope用户都可以有一种或多种的角色。这可方便管理员进行安全策略管理。管理员可以把一组许可赋予一个角色,再把一组用 户定义为该角色,这样,这一组用户就会有相同许可,可大简化管理员的管理工作。Zope内置有四种角色:
Manager,这是zope的管理员角色,执行管理任务。
Anonymous,这是匿名用户的角色。这个角色可以访问公共资源。该角色一般不能修改对象。
Owner,所有者角色在你建立对象时自动被赋予。也就是说,如果这个对象是由你建立的,你在该对象上就被赋予了所有者的角色。
Authenticated,当你通过验证,登录入Zope系统时,自动被分配该角色。这个角色表示Zope知道这个用户的身份。
全 局角色是在对象的“Security”标签内“roles”列显示的角色。建立一个角色可在“Security”标签页,拉到页面底部,就会有一个 “User defined roles”栏,在该栏填上角色名,按Add Role按钮即可。角色名最好和角色的功能相关联,不要取没意义的名字。
角色建立后,我们可在“roles”列中看到新建的角色,在页面底也出现了“delete role”按钮。刚建的角色也出现在这里。选中一个角色按“delete role”按钮就可把角色删除。
角色的使用范围也是按对象结构的获取来定义的,只能用于定义该角色的文件夹内和下层文件夹,上层文件夹不能使用该角色。所以如果你想定义一个在整个系统中可用的角色,就可在root文件夹内定义。
一般来说,角色应该应用于网站的大区域,如果你想建立一个角色来限制用户只可部份地访问你网站,我们可用另外一种方法来完成,而不必须要使用角色。如:通过在你想保护的文件夹中设置已有的角色的许可,或在该文件夹中设置用户文件夹,定义一组用户来控制他们的访问。
本 地角色是Zope中的一种高级安全特性。特定用户可通过本地角色获得操作特定对象的额外权限。例如,我通过ZMI新建一个对象,并把对象通过本地角色功能 把Owner角色指定给了一个普通用户。本来这个普通用户是不具有编辑对象许可的,但它可通过额外的Owner本地角色来编辑新建立的这个对象。一般我们 应该尽量避免使用这种一对一的安全设置,因为这样会增加管理的复杂程序和维护的难度。
许 可是定义在一个Zope对象可做的动作。就象角色是一组用户的抽象一样,许可是对象行为的抽象。例如,很多对象都可以被浏览,则这个浏览行为就受到 “View”许可的约束。许可由zope产品开发者和内核定义。每个Zope产品都会建立一套与产品对象相关的许可。有些许可只对应一种对象,如 “Change DTML Methods”许可只是保护DTML Methods。一些许可对应多种对象,如“FTP access”和“WebDAV access”许可就可保护所有经由FTP和WebDAV访问的所有对象。你可通过“Security”管理标签来查看对象的许可。所有核心的许可在以下 网址查到,http://www.zope.org/Documentation/Books/ZDG/current/AppendixA.stx
安 全策略是通过配置角色和许可来设置的,它定义在网站中“谁”在“哪里”可以“做什么”。要设置一个对象的安全策略,只要进入该对象的“Security” 标签设置即可。“Security”页面中间有很多选择框,竖列是角色栏,水平行是许可项。通过横坚交叉方式确定角色的许可。
很多Zope产品会增加自已的许可,所以这个表格的长度会随着你安装产品的数量不断增加。所以我们建立产品开发者尽量要用回系统自带的许可。
当 你打开根目录的“Security”页时,你会注意到系统已默认为我们配置了很多许可,而且该许可已可很好地工作。它赋予了管理员角色最大的许可,以进行 系统管理工作;而匿名用户角色就只配置了较少的许可,适用访问公共信息。我们可在这个基础上进行配置,以适合我们自已的安全需求。例如:我们可以把匿名用 户的许可全部取消,这样你的网站就变成私有的了,只有管理员可以访问。
如果你在根目录上作以上设置,则该设置会应用到整个站点。如果你想在个别目录进行设置就需进入相应文件夹再进行配置。 |
Zope 中不同的安全策略是如果相互作用的呢?我们可以在不同的对象中建立安全策略,但哪个策略才是最终控制对象的呢?答案是如果对象本身设置了一个安全策略,则 使用这个安全策略。否则,对象会通过获取机制获取上一层对象的安全策略。Zope在安全策略中广泛使用获取机制。获取中Zope中在文件夹或子文件夹内共 享对象信息的一种结构。Zope安全系统通过获取来共享安全策略,所以可通过上层文件夹来控制下层对象和子文件夹。
你可通过 “Security”标签来设置安全策略获取功能。进入该标签后,屏幕左边有一列叫“Acquire permission settings”的选择框,每一个选择框代表一个或多个许可。默认所有框都是选中的,也就是说该对象获取了上一层对象的许可,再加上在这里指定给角色的 许可共同组成该对象的安全策略。
根文件夹的左边是没有这些选择框的,因为它已是最高级的对象的,不存在获取。 |
假 设你需要一个私有的文件夹,只有认证用户可以浏览。正如前面所说,我们可以把这个对象的上下文中Anonymous角色的“View”许可去掉即可。但这 还不够,因为如果“Acquire permission settins”选择框是选中的话,它还是会获取上层对象中Anonymous角色的“View”许可的。所以,要设置私有文件夹,一定要取消 “Acquire permission settings”选项。这样才可确保只有在当前显式设置的许可才有效。
如果你不想获取安全策略,请把“Acquire permission settings”的选择框清空。 |
zope 安全的基本概念很简单:角色和许可是建立安全策略的两大要素。角色(全局或本地)分配给用户,用户的行为受到对象中角色所拥有许可的控制。这些简单的工具 以许多不同的方法组合在一起,使安全管理复杂化。以下有几个基本的安全管理模板的例子,可以帮助我们有效地和容易地管理我们的安全架构。
这里有几点安全管理的原则,这些原则不是特效药和秘方,但会在你面对一个未知领域时给你一些指导性的建议。
在你需控制范围的最高层设置用户,而不仅仅是上一层。
把需由相同人员管理的一组对象放到同一个文件夹内。
保持简单。
第一和第二差不多,是Zope安全结构的基础要求。一般你需把互相关系密切的资源和对象放在一起,这不是必须的,但可帮助你合理安排文件夹和子文件夹。
不 管你网站结构怎么样,请尽量保持简单。复杂的安全设置会使你较难理解、管理和确保它的有效性。尽量减少新角色的建立,同时多一些用安全策略获取机制来代替 显式的安全策略设置。当你的安全策略、用户和角色越来越多,越来越复杂时,你应该重新考虑一下你的配置,可有会有一种更简单的方法可以帮助你实现相同的功 能。
一 个常用的安全规则是在根文件夹定义一个全局的安全策略,其它文件夹通过获取机制获得这个安全策略。在需要时,也可在子文件夹内附加新的策略以扩充全局策 略。但要限制这种情况,当你发现你要在多个文件夹使用附加策略时,应考虑把这些对象集中到一个文件夹内来,只在这个文件夹内设置附加策略。
如果你的子策略中某个许可的约束性大于全局策略时,你应该取消该许可的获取功能,在本地重新设置本地许可。
简单的规则能使你的系统更安全、更强健、更容易管理和维护。这一点对于任何的安全架构是都很重要的。
委 托的概念在zope中很重要。Zope鼓励你把相同的资源放在一个文件夹内,并在该文件夹内建立一个用户来管理这些资源。这个用户将被分配管理员角色,对 该文件夹及该文件夹下面的对象有管理员的权限,它不能以管理员身份登录进任何除该文件夹外的文件夹。这个规则可递归使用,也就是说,该文件夹的管理员也有 权定义子文件夹的管理员,把一些工作再细分下去。
本地管理员规则是强大而稳定的,但控制的程度较粗,只能控制有权或没权访问。有时我们需要一些较细致的访问控制,例如我们可能要具体控制到这个资源可以由谁来访问,这就要用到角色。角色可以让我们定义一类用户,针对这类用户设置相应的安全策略。
角 色可以帮我们解决本地管理员委托的中存在的一个问题,那就是本地管理员规则需要一个固定的从属结构。但当两个不同组的人需访问同一资源,而一个组又不是另 一个组的管理员时,委托就做不到很好地管理了。换句话说,我们不能在网站的这一个文件夹中定义一个管理员来管理网站另外一个文件夹中的资源。但角色可以帮 我们做到控制网站中不同地位置的资源访问。
让我们用一个例子来说明本地管理委托的第二个缺陷。假设你维护一个大型的医药公司的网站,有两类 用户,一类是科研人员,一类是销售人员。一般来说,这两类人员都各自管理自已的网站资源。但是,假设有一些资源两类人员都需要,比如一些广告信息。如果我 们科研人员在“Science”文件夹里定义,销售人员在“Sales”文件夹里定义。那广告信息文件夹应该放到哪个文件夹里呢?因为这两个文件不是嵌套 的,所以放在哪个文件夹都不能使两个文件夹的管理员同时管理广告信息。那我们应该怎么做呢?答案就是使用角色。我们可以在这两个文件夹的上一层文件夹内建 立两个角色,一个叫“Scientist”,一个叫“SalesPerson”。在这个上层文件夹里给这两个角色指定合适的许可,而不是管理员角色。在 “Science”文件夹把“Scientist”角色设为管理员,同样,在“Sales”文件夹,把“Salesperson”设为管理员。最后,在广 告文件夹,我们授予“Scientist”和“SalesPerson”适当的许可。这样,利用角色就实现了不同位置的访问控制了。
经过上面有关安全规则的讨论,我们应该知道怎么使用用户文件夹、角色和安全策略来制定应用程序的安全结构。下面我们讨论两个有关安全的高级主题,一个是怎样进行安全检测,另一个是如果安全地执行可执行内容。
在开发一个Zope应用程序时,开发人员一般不用编写程序进行安全检测,Zope会做很多工作。如果你访问一个受限资源时,Zope会自动弹出登录框,进行安全验证。如果你没有适当的访问许可,Zope会禁止你对资源的访问。
但是,我们希望可进行手动安全检测工作。主要原因不是为了阻止非法用户的访问,而是对合法用户访问受限资源的条件进行约束,以保证用户的正常访问,而不会进行一些无效的操作。
最 通常的安全检测是检查当前用户是否有一个许可。我们可以用“checkPermission”API来完成这项工作。假设你的应用程序允许用户上传文件, 这个操作受“Add Documents,Images,and Files”许可保护。我们可以用一个DTML里测试一个用户是否有这个许可。代码如下:
<dtml-if expr="_.SecurityCheckPermission( 'Add Documents, Images, and Files', this())"> <form action="upload"> ... </form> </dtml-if>
“SecurityCheckPermisson”函数有两个参数,一个是许可名,另一个是对象名。在DTML中可用this()传递当前的对象名。对于ZPT,语法不些不同,但操作是一样的。如:
<form action="upload" tal:condition="python: modules['AccessControl'].getSecurityManager().checkPermission('Add / Documents, Images, and Files', here)"> ... </form>
python script也可以用来完成相同的任务,通过ZPT来调用安全检测的python script,下面一个调用check_security安全检测脚本的页面模板:
<form action="upload" tal:condition="python: here.check_security('Add Documents, Images and Files', here)">
check_security脚本如下:
## Script (Python) "check_security" ##bind container=container ##bind context=context ##bind namespace= ##bind script=script ##bind subpath=traverse_subpath ##parameters=permission, object ##title=Checks security on behalf of a caller from AccessControl import getSecurityManager sec_mgr = getSecurityManager() return sec_mgr.checkPermission(permission, object)
在上例我们可以看到可通过手工的方式进行许可检测,Zope API中还有一些其它的函数为完成该工作,但“checkPermission”是最有用一个函数。通过“checkPermission”函数,我们可 以确认当前用户是否有适当的许可。你可通过访问用户对象获得当前用户名,用户对象和其它Zope对象一样,你可通过在API里定义的方法来操作对象。假设 我们想显示当前用户名,可在DTML里这样写:
<dtml-var expr="_.SecurityGetUser().getUserName()">
在DTML中用SecurityGetUser的getUserName()方法可获当前登录的用户名。ZPT中用 getUserName的user方法也可获得当前登录用户名。如果用户没有登录,则获得一个匿名用户名叫“Anonymous User”。在Zpt中的写法如下:
<p tal:content="user/getUserName">username</p>
Zope的安全API在本书的附录中有详细介绍。在Zope的ZMI通过中“help”也可查询到相关的内容。
我们已了解了有关Zope安全的基础内容,所有权和可执行内容的概念是什么呢?Zope用所有权来关联对象和对象的创建者,可执行内容包含脚本、DTML方法和DTML文档等执行用户代码的对象。
在针对可信用户的小型网站,这些高级主题可以忽略,但在一个允许不可信用户创建和管理Zope对象的大型网站,明白所有权和可执行内容这些内容就显得很重要了。
木马攻击是我们需理解所有权和可执行内容控制的一个原因,木马会欺骗用户进行有害的操作,一个典型的木马是它会假装一个没问题程序,但当你不自觉地运行它时,就会对你的系统造成损害。
所有操作系统对这类型攻击的防护能力都较弱。对于Web平台,进行这种攻击所要做的工作是欺骗一个授权用户访问一个存在有害程序的网址。这种攻击很难预防。你可以很容易欺骗其他人点击一个友好的链接,或用javascript技术来把链接重定向到一个有害的网址。
Zope 提供了对这类木马的保护措施。Zope在服务器端通过限制以作者为基础的Web资源的能力来保护网站。如果一个不受信任的用户创建了一个页面,则这个页面 通过可信用的浏览户来执行有害操作的能力就会受到限制。例如,假设一个不受信用户创建了一个DTML页面或Python script来删除你网站上的所有页面。如果有一个用户访问该页面,将会出错,不会执行有害代码,因为该页面没有足够的许可来执行删除操作。如果是管理员 浏览该网页,也会出错,即使管理员有足够的许可来执行这些有害操作。Zope是通过所有权信息和可执行内容控制来提供这些限制保护的。
当 一个用户创建一个Zope对象,这个用户就拥有该对象。如果一个对象没有拥有者则被称为“unowned”。所有权信息保存在对象内部,就象Unix系统 中文件所有权的概念。你可通过对象的“Ownership”管理标签来查看所有权相关信息。该标签可显示该对象是否被拥用和被谁拥有。如果你有“Take ownership”的许可,你可以把其它有所拥用的对象变成自已的。这项功能在拥有对象的帐号被删除后很有用,或者可通过该功能接管理其它用户的对象。
在前面的章节中我们提到,因为可以设置本地Owner角色,所以所有权会影响安全策略的设置。同时也因为所有权可控制角色的可执行内容。
对象的“owner”角色和所有权没有什么关系。“owner”角色和可执行内容的所有权是不同的。因为一个可执行对象有“Owner”本地角色并不意味着它是这个对象的拥有者。
DTML 文档、DTML方法、SQL方法、python脚本都是可执行内容,因为它们可动态生成内容。对象内容可通过Web来编辑。当你访问它的URL时, Zope就会执行对象的可执行内容。对象的操作会受到对象拥用者的角色和访问者的角色的限制。也就是说,一个可执行对象只能执行拥用者和访问者都经授权允 许的操作。这可阻止一个无特权的用户写一个有害的脚本来欺骗一个特权用户来执行。你不能欺骗其他人执行一个你没有权执行的操作。这就是zope为什么能利 用所有权来保护服务器端免受木马攻击的原因。
一个“unowned”对象是不能执行的。如果你要运行可执行对象,请确保设置了正确的所有权。 |
有时,Zope限制访问可执行对象的约束不能满足你的需求,有时你想允许一个非特权用户执行一个受保护的操作,代理角色可提供一个定制执行可执行对象的方法。
假 设你想建立一个邮件表格,允许匿名用户通过它给网站管理员发信。发邮件是受到“User mailhost services”许可约束的。匿名用户没权访问。问题是,这样也使你的DTML方法不能由匿名用户来发邮件。解决方法是在DTML方法上设置 “Manager”代理角色,以至匿名用户能以管理员的身份执行DTML脚本,这样就可正常发送邮件了。
代理角色可在可执行对象中定义一个可修改的许可。因此,你也可用它来约束安全。例如,如果你在一个可执行对象设置一个“Anonymous”角色,则该对象就不能被其它对象执行,即使有所有者角色和访问者的角色。
设置代理角色时必须小心,因为这会影响默认的安全策略。 |
Table of Contents
本章介绍zpt的高级功能和新的表达式类型
这 章将深入讲述所有的TAL语法和它们的选项,在附录C中有更全面的参考资料。在这节中,“标签(tag)”和“元素(element)”的定义遵从 “XHTML spec”。“<p>”表示一个标签,“<p>stuff<p>”表示一个元素。
在前面的章节中我们已学过怎么使用tal:content和tal:replace,在这章,你将学到更多有关内容插入的技巧。
通常,tal:replace和tal:content只能显示纯文本数据,不会解释HTML文档,也就是说它不会把<br>解释成回车。为了处理好象HTML这样的结构化数据,需要用到structure关键字。例如:
<p tal:repeat="newsItem here/topNews" tal:content="structure newsItem"> A news item with<code>HTML</code> markup. </p>
内建变量here指向当前的文件夹。
你可以用内建变量nothing来隐藏页面信息。如:
<tr tal:replace="nothing"> <td>10213</td><td>Example Item</td><td>$15.34</td> </tr>
上面语句将不会显示任何内容。
这节介绍循环指令的高级特性。
循环变量很重要,需详细介绍。提供了当前循环的信息,以下是有效的循环变量:
index---循环索引值,从0开始。
number---循环计数值,从1开始。
even---偶数循环索引值。(0,2,4,...)
odd---奇数循环索引值。(1,3,5,...)
start---循环的初始值。(index 0)
end---循环的最终值。
length---循环序列的长度,它将会是循环的总次数。
letter---用小写字母来循环计数,“a”-“z”,“aa”-“az”,“ba”-“bz”以此类推。
Letter---同上,只是使用大写字母。
你可以通过路径表达式或python表达式来访问这些循环变量。如:“repeat/item/start”可访问start变量。在python表达式中要用字典表示法来访问循环变量,如:“python:repeat['item'].start”。
这里提供一些有用的技巧。
tal:repeat指令可嵌套,每个指令必须有一个不同的循环变量。如:
<table border="1"> <tr tal:repeat="x python:range(1, 13)"> <td tal:repeat="y python:range(1, 13)" tal:content="python:'%d x %d = %d' % (x, y, x*y)"> X x Y = Z </td> </tr> </table>
分批功能可把一个大的列表分成一些小列表,方便显示和管理。tal:repeat指令本身不支持分批功能,但可通过 zope的分批工具(batching)来实现。具体例子本章后面会讲到。还有一个有用的sequence.sort工具,它可实现排序功能,下面是一个 同时利用了排序工具的例子:
<table tal:define="objects here/objectValues; sort_on python:(('title', 'nocase', 'asc'), ('bobobase_modification_time', 'cmp', 'desc')); sorted_objects python:sequence.sort(objects, sort_on)"> <tr tal:repeat="item sorted_objects"> <td tal:content="item/title">title</td> <td tal:content="item/bobobase_modification_time"> modification date</td> </tr> </table>
sequence.sort函数需要两个参数,一个是排序的对象(例子中的objects),一个是排序的方式(例子中的sort_on)。
tal:attributes指令可帮我们动态替换标签的属性。如:
<a href="link" tal:attributes="href here/getLink; class here/getClass">link</a>
href和class属性都被替换了。在XML中也可替换名字空间的属性,如:
<Description dc:Creator="creator name" tal:attributes="dc:Creator here/owner/getUserName"> Description</Description>
用tal:define可定义自已的变量。定义变量的原因主要有几个,一是为避免重复写一个长的表达式,一个是为了避免重复调用一个方法。你可在一个元素的标签里定义一个变量,则可在元素的这个标签内多次调用。这里有一个例子:
<ul tal:define="items container/objectIds" tal:condition="items"> <li tal:repeat="item items"> <p tal:content="item">id</p> </li> </ul>
上例定义了一个items变量,它在整个ul元素内可用。我们注意到,在一个ul标签里有两个TAL指令,这在下面的“TAL指令的交 互操作”章节中会具体讲到。在这里,第一个指令指定一个items变量,第二个指令判断items是否为空,如果为空,则不会显示ul元素。
现在,假设你想显示一些信息,而不是简单地不显示列表,你可用以下指令放到列表前面。
<h4 tal:condition="not:container/objectIds">There Are No Items</h4>
当container/objectIds为空时,not:container/objectIds为真,则会显示提示信息“There Are No Items”。
如 果你在h4元素里定义items变量,那你就不能在ul元素里使用它。因为items是h4元素的本地变量。要可在h4和ul元素里都可使用items, 我们可在一个能包含h4和ul元素的元素中来定义。另一个简单的办法是在变量前面加一个“global”关键字,使它成为一个全局变量,这样就不受元素包 含关系限制,如下例所示:
<span tal:define="global items container/objectIds"></span> <h4 tal:condition="not:items">There Are No Items</h4>
你可用tal:define定义超过一个的变量,变量间用分号分隔,如:
<p tal:define="ids container/objectIds; title container/title">
你可定义很多的变量,每个变量都有它自已的全局和本地范围。你也在后面的定义中引用前面定义的变量。如:
<p tal:define="title template/title; global untitled not:title; tlen python:len(title);">
在上例中,title和tlen都是本地变量,unitiled是全局变量。灵活运用tal:define可使你的页面模板高效并可读性强。
你可用tal:omit-tag指令移除标签。该指令较少使用,但有时会很有用。omit-tag会移除开始和结束标签,但它不会影响元素的内容,如:
<b tal:omit-tag=""><i>this</i> stays</b>
相当于:
<i>this</i> stays
在这里,tal:omi-tag的动作类似于tal:replace="default"。但tal:omit-gat可用于真假表达式。在这个例子中,当表达式为真是,就把标签移走。所以只有是“best firend's”时会才显示粗体字,如:
Friends: <span tal:repeat="friend friends"> <b tal:omit-tag="not:friend/best" tal:content="friend/name">Fred</b> </span>
当你的页面模板出现错误时,你可以捕捉这个错误,并把出错信息反馈给你的用户,如:
... <span tal:define="global prefs here/scriptToGetPreferences" tal:omit-tag="" tal:on-error="string:An error occurred"> ...
当一个错误发生时,Zope会查找tal:on-error指令来处理错误。它首先会搜索当前元素,再到该元素的上层元素,直到最高层的元素。当发现一个错误处理器,它就会用错误处理表达式代替当前内容。
你可在一对嵌套的元素间定义一个错误处理,例如表格。当错误发生时,预定义的错误信息就会代替表格。为了提高错误处理的灵活性,你也可在错误发生时调用一个错误处理脚本,如:
<div tal:on-error="structure here/handleError"> ... </div>
当在div元素里发生错误时就会调用handeError脚本。“structure”属性能使返回的信息可以用HTML显示出来。脚本通过“error”变量接收传递到脚本的错误信息。如:
## Script (Python) "handleError" ##bind namespace=_ ## error=_['error'] if error.type==ZeroDivisionError: return "<p>Can't divide by zero.</p>" else: return """<p>An error occurred.</p> <p>Error type: %s</p> <p>Error value: %s</p>""" % (error.type, error.value)
当我们接收到错误时,脚本就可帮我们进行相应的处理,例如可以把错误信息以邮件的形式发送出去。
tal:on-error指令不是用来处理异常的一般方法,例如,你不应该用它来校验表单的输入。你应该使用脚本来做这个工作,因为脚本处理异常的能力强大很多。tal:on-error只用来在页面模板中处理一些不常见的问题。
如 果每个元素中只有一个TAL指令,则执行顺序就很简单。从根元素开始,每个元素中的指令都被执行,如果有子元素的,则按元素间的嵌套关系顺序执行。然而, 有可能在一元素内使用多个TAL指令。在一个元素包含多种TAL指令集,但tal:content和tal:replace两个指令不能用于同一元素。当 在一个元素中包含多条指令时,它们的执行顺序如下:
define
condition
repeat
content or replace
attributes
omit-tag
由于tal:on-error只在发生错误时才执行,所以该指令不包含在上面的列表中。 |
为 什么是按这样的顺序呢?它是这样推理出来的:我们经常要设置一些变量以供其它指令使用,所以变量定义指令需首先执行;接着我们要依靠变量值来确定这个元素 的行为,所以条件语句是下一步;重复可用不同的内容替换当前元素的内容,所以重复需在content、replace和attributes之前执行。 omit-tag放在最后是因为其它指令都和它没有什么依赖关系。下面是一个在一个元素中包含多条TAL指令的例子:
<p tal:define="x /root/a/long/path/x | nothing" tal:condition="x" tal:content="x/txt" tal:attributes="class x/class">Ex Text</p>
我们注意到,tal:define首先被执行,其它指令都使用它定义的变量。在元素中使用多指令这种情况,有三个约束,分别是:
在一个元素内每一类指令只能使用一次,这是因为HTML不允许同名属性。例如,你不能在一个元素内使用两个tal:define指令。
在同一个元素内不能同时使用tal:content和tal:replace。因为它们的功能有冲突。
你写TAL的顺序和它们执行的顺序无关。不管你怎么排列,它们都是按照前面讲的执行顺序执行。
如果你想改变这种执行顺序,你必须使用内嵌元素。一个例子,假设你想循环一串值,但又想跳过其中的一些值。下面是一段错误的代码:
<!-- broken template --> <ul> <li tal:repeat="n python:range(10)" tal:condition="python:n != 3" tal:content="n"> 1 </li> </ul>
上例将不能正常工作,因为condition早于repeat执行,这时变量n还没定义,所以系统会报找不到变量n 的错误。下面提供一个正确的写法:
<ul> <div tal:repeat="n python:range(10)" tal:omit-tag=""> <li tal:condition="python:n != 3" tal:content="n"> 1 </li> </div> </ul>
上例用div内嵌元素定义了变量n,并通过tal:omit-tag指令忽略div元素。
虽然span和div是HTML的当然选择,但在XML中就没有类似的元素。我们可使用TAL's名称空间来在XML中处理以上问题。TAL没有定义任何标签,但也没禁止任何标签,我们可以在TAL名称空间里用自已喜欢的名字定义一个标签,并用该标签组成一个元素,如:
<tal:series define="items here/getItems"> <tal:items repeat="item items"> <tal:parts repeat="part item"> <part tal:content="part">Part</part> </tal:parts> </tal:items> <noparts tal:condition="not:items" /> </tal:series>
本例中的tal:series,tal:items和tal:parts可被XML和HTML工具所识别。该方法比用div有两个附加 的优势:首先,这些标签能象TAL属性一样被忽略,所以不用使用omit-tag指令;第二,标签的TAL属性不需要它们自已的tal:后缀,因为它们继 承了标签的名称空间。METAL名称空间也可这样用。
在DTML中我们可通过“表单/处理”模式来处理表单,该模式包括两个DTML方法或者两个DTML文档:一个作为表单用来收集用户的输入信息,另一个定义一些操作方法用来处理输入信息和反馈信息。DTML的具体操作请参考相关的章节。
在ZPT 中,我们不用“表单/处理”模式,而是用“表单/处理/反馈”模式。表单和反馈用ZPT来做,处理这部分通过脚本来完成。ZPT收集信息并调用处理脚本, 处理脚本处理用户输入的信息并返回一个反馈模板。该模式比“表单/处理”更灵活,因为它允许脚本返回各种反馈对象。下面有一个表单:
... <form action="action"> <input type="text" name="name"> <input type="text" name="age:int"> <input type="submit"> </form> ...
该表单调用的脚本如下:
## Script (Python) "action" ##parameters=name, age ## container.addPerson(name, age) return container.responseTemplate()
脚本调用一个方法处理输入信息并返回另一个页面模板。处理脚本可做很多事情,如校验输入、处理错误、发邮件等。下面有一个处理输入信息校检的例子:
## Script (Python) "action" ## if not context.validateData(request): # if there's a problem return the form page template # along with an error message return context.formTemplate(error_message='Invalid data') # otherwise return the thanks page return context.responseTemplate()
这个脚本会校验表单的输入信息,如果有问题,则会把出错误信息反馈给页面模板。脚本的context变量和TALES中的here一 样。我们还可通过关健字参数把一些反馈信息传递给页面模板,如上例中的error_message="Invalid data"。在页面模板中接收反馈信息需用到模板的内建变量“options”。因此接收上例中出错误信息的页面模板应该包含以下内容:
<span tal:condition="options/error_message | nothing"> Error: <b tal:content="options/error_message"> Error message goes here. </b></span>
上例演示了如何通过关关健字参数把出信息传递给页面模板。| nothing是处理当没有出错信息传递过来时就什么都不显示。
ZPT中没有与dtml-call类似的调用语法,但它可用很多其它方法调用对象,而不会在页面中显示值,如:
<span tal:define="unused here/processInputs" tal:omit-tag=""/>
这个例子调用了processInputs对象,并把结果存入unused变量。
我 们已经接触过很多页面模板表达式,它为模板指令提供值。如这样一个TAL指令<td tal:content="request/form/age">Age</td>,request/form/age就是一个表达 式,一个路径表达式。路径表达式描述对象的路径,如request/form/age、user/getUserName。表达式只用于TAL指令,它不 能用于页面模板中的HTML代码。这一节将介绍各种不同的表达式和变量。
我们可以在表达式中使用变量,其实我们在前面已使用过一些内建的变量,如:template,user,repeat和request。这里将全面地介绍其它的内建变量,注意,这些变量和Python script中的内建变量不同,它们只用于页面模板。
一个假值,类似于空串,可用于tal:replace和tal:content,以删除元素和元素中的内容。设置了该属性的元素将不会把内容显示出来,就好象被删除或屏蔽掉了。
当在tal:replace,tal:content和tal:attributes中使用了该属性后,将不会对显示内容产生任何的改变。
指 定关键字参数,可由脚本通过该参数把值传递给页面模板。当页面模板由Web静态生成时,是不会用到options。只有从脚本或其它类似工具动态生成页面 模板时才会用到options。例如:当一个名为t的页面模板在python脚本中用t(foo=1)调用时,则options/foo路径表达式就等于 1。
模板中当前标签属性的字典。键是属性名,值是模板中的属性的原始值。这个变量很少使用。
Zope的根对象,始终指向Zope服务器的“/”路径。如:tal:content="root/title"可获得根目录的title属性。
调用页面模板对象的地方,与container类似。通过它我们可在不同的地方访问同一个Zope对象。在Python-base脚本中,here变量和context变量类似。
container(一般是文件夹)是存放页面模板的地方,使用它可获得Zope对象。
在页面模板中可以用到Python中的各种模块。
字符串表达式允许我们以文本的方式组合路径表达式。跟在string:后面的文本就是我们组合的路径表达式。每个变量必须以“$”号开头。如:
"string:Just text. There's no path here." 这里没有用到变量 "string:copyright $year by Fred Flintstone." 这里使用了YEAR这个变量
如果路径表达式由多部份组成(以“/”分隔),或者需与文本串组合,就必须使用“{}”号把变量括起来,如:
"string:Three ${vegetable}s, please." 这里需与s字符串组合成一个单词 "string:Your name is ${user/getUserName}!" 这里需用反斜杠
如果要在字符串里显示双引号等特殊符号,就要用该符号的实体形式(")表示。如果要显示以“$”号开头的变量,就必须用三个美元符号来表示,如:
"string:Please pay $$$dollars_owed" "string:She said, "Hello world.""
一些复杂的字符串格式化操用并不适宜用字符串表达式来做,还是用python表达式和python脚本来实现较好。 |
路径表达式是用类似URL路径的形式来表示一个对象。一个路径会遍历一个一个的对象,直达你所需要的最终对象。所有的路径都以一个已知的对象(内建变量、重复变量或一个用户定义变量)开始,并以你想获得的结果对象为终点。如:
template/title 获得当前页面模板的title属性值 container/files/objectValues 获得当前目录中files文件夹下所有对象值 user/getUserName 获得user对象的getuserName方法的值 container/master.html/macros/header 获得当前目录下master.html中的header宏 request/form/address 获得URL请求中的address值 root/standard_look_and_feel.html 获得Zope根目录下的standard_look_and_feel.html文件
通过路径表达式,你可访问对象及子对象的属性和方法。在路径表达式中我们也可使用获取机制,在高级Zope脚本一章会有详细介绍。
Zope使用与约束通过URLs访问对象相同的方法约束路径表达式遍历对象。你必须有足够的权限才能访问对象。
如果路径表达式指向一个不存在的对象,则Zope会出错,我们可用“|”符号进行选择,相当于逻辑表达式中的或操作。如:
<h4 tal:content="request/form/x | here/x">Header</h4> <p tal:content="request/form/age|python:18">age</p>
上例的意思是,如果request/form/x不存在,则调用here/x。第二句的意思是如果request/form/age不存在,则显示18。
NOT表达式可进行取反操作,如:
<p tal:condition="not:here/objectIds"> There are no contained objects. </p>
如果here/objectIds为假,经not操作后,就变为真,所以将显示下面的内容。在Zope中,零值,空字符串,空序列, nothing关键字和None关键字都为假,其它的都为真。不存在的路径既不是真,也不是假,把not:表达式应用于不存在路径会出错。在python 表达式中,我们可用Python的not关键字替代not:表达式。
一 个普通的路径表达式会调用对象,也就是说,当对象是一个函数、脚本、方法或其它任意的可执行对象时,则表达式会返回对象结果。这是我们经常使用的方法,但 有时我们可能不想直接返回结果。例如,我们想把一个DTML方档定义为一个对象,通过该对象,我们就可方便地访问它的各种属性。如果我们用普通的路径表达 式,该DTML文档会自动执行,生成显示文档。为了该目的,我们可使用nocall表达式,如:
<span tal:define="doc nocall:here/aDoc" tal:content="string:${doc/getId}: ${doc/title}"> Id: Title</span>
nocall也可用于函数,而不仅仅是对象,如:
<p tal:define="join nocall:modules/string/join">
这样,我们就定义了一个join函数(string.join),而不调用join函数。
如果路径存在,则exists表达式为真,否则为假。这里有一个显示出错信息的例子,先用普通方式实现:
<h4 tal:define="err request/form/errmsg | nothing" tal:condition="err" tal:content="err">Error!</h4>
如果使用exists表达式,可简化成:
<h4 tal:condition="exists:request/form/errmsg" tal:content="request/form/errmsg">Error!</h4>
exits表达式和not表达式可联合起来使用:
<p tal:condition="not:exists:request/form/number">Please enter a number between 0 and 5</p>
注意,表达式不能写成exists:not:request/form/number,否则number变量存在和为零时,表达式为真,不符合要求。
Python是一种简单化而又强大的脚本语言,Zope的主要开发语言就是python,所以在zope中可以使用python的功能是理所当然的了。在ZPT中,我们可以使用Python语言中表达式,但不能使用if和while等流程控制语句。
在tal: condition语句中,我们常常会用到python的比较表达式,用于比较两个字符串或数值。如果没有python表达式,TAL本身不支持这种比较 操作。在Python表达式中,我们可以使用各种比较操作符,如<、>、==、!=等,我们还可以使用and,not,和or布尔操作符。例 如:
<p tal:repeat="widget widgets"> <span tal:condition="python:widget.type == 'gear'"> Gear #<span tal:replace="repeat/widget/number>1</span>: <span tal:replace="widget/name">Name</span> </span> </p>
该脚本会遍历对象集,并把类型为gear的信息显示出来。
有时,你想在一条语句中根据一个或多个条件选择不同的值,这样就要用到test函数。如:
You <span tal:define="name user/getUserName" tal:replace="python:test(name=='Anonymous User', 'need to log in', default)"> are logged in as <span tal:replace="name">Name</span> </span>
如果用户是Anonymous,则显示need to log in,否则,使用默认值,显示are logged in as。test功能的工作方式类似于if/then/else语句。下面是另外一个例子,可根据oddrow的值设置tr的class属性,从而实现不同 的输出格式:
<tr tal:define="oddrow repeat/item/odd" tal:attributes="class python:test(oddrow, 'oddclass', 'evenclass')">
在Python表达式内部可以嵌入其它类型的表达式,如path(),string(),exists()和nocall()。这些表达式类型对应前面介绍的路径,字符,存在测试和nocall表达式。我们可把表达式写:
"python:path('here/%s/thing' % foldername)" "python:path(string('here/$foldername/thing'))" "python:path('request/form/x') or default"
最后一个例子和路径表达式“request/form/x | default”稍有不同,在“request/form/x”不存在或它为假时都会显示默认文本。
Zope的强大功能之一是它可集成各种对象。你的页面模板可以使用脚本,SQL方法,搜索目录和可定制的内容对象。为了使用这些对象,我们必须知道如何调用它们。
每 个对象都有一些属性,我们可以用object.attribute的方式访问它,如我们想获得页面模块的标题,我们可用“template.title” 表达式。很多Zope对象都支持获取(继承)功能,这样我们可获得父对象的属性。如“here.Control_Panel”可获得root文件夹中的 Control Panel对象。对象方法也是属性,可用同样的方式调用,如“here.objectIds”和“request.set”。通常,位于文件夹内的对象都 可通过文件夹的属性来访问,但有时对象id包含有点号时就会出错,如:
python:here.hello.html
这时我们要写成
python:getattr(here,'hello.html')
一些对象,如request,modules和Zope文件夹支持索引方式访问,如:
request['URL'] modules['math'] here['thing']
当用索引方式访问文件夹对象时,它不具有获取功能,也就是说它只能访问本文件夹内的对象,不能访问父文件夹中的对象。
通过前几章学习,我们知道,路径表达式可使我们能忽略具体的实现细节灵活方便地访问对象,如:
"here/images/penguin.gif" 可替代 "python:getattr(here.images, 'penguin.gif')" "request/form/x" 可替代 "python:request.form['x']"
在python表达式内,我们可使用path()函数使用路径表达式。
脚本对象经常用于处理商业逻辑和复杂的数据。如果你在页面模板中需写大量的语句和表达式,则可考虑使用脚本。尽量保护页面模板简单,让复杂的工作给脚本来做。
每个脚本都有一个参数列表,在调用脚本时用于传递参数。如果该列表为空,也就是说脚本不需要任何参数,则我们直接用路径表达式调用它即可。否则,我们需用python表达式来调用它,把参数传递给脚本。如:
"python:here.myscript(1, 2)" "python:here.myscript('arg', foo=request.form['x'])"
如果我们需从一个脚本中返回多值到页面模板。则我们需使用字典变量,把多值保存在这个变量中,通过路径表达式获取每个项。假如一个getPerson脚本返回一个包含name和age键值的字典变量,则我们可用以下语句取出返回值:
<span tal:define="person here/getPerson" tal:replace="string:${person/name} is ${person/age}"> Name is 30</span> years old.
当然,上面语句同样也可用于返回Zope对象和Python列表的情况。
DTML方法和文档没有参数列表,但它可通过客户,映射和关键字来构建参数列表,当我们在ZPT中调用一个DTML对象时,会把对象上下文作为客户(here),REQUEST作为映射表传递给DTML,这样DTML就可使用ZPT中的变量,如:
<head tal:define="items here/getItems.sql"> <title tal:content="template/title">Title</title> <script tal:content="structure here/jsItems"></script> </head> ...etc...
jsItems为DTML方法,内容如下:
<dtml-let prefix="template.id"> <dtml-in items> &dtml-prefix;_&dtml-name; = &dtml-value; ; </dtml-in> </dtml-let>
template的id和items变量可以在DTML中使用。
Python语言包含有大量的组件,为Python编程提供了强大的功能。每个组件集合包含了完成类似功能的Python函数,数据和类,如数值计算、正则表达式等。
有几个组件包括math和string,在python表达式中已可使用。使用python:math.pi就可获得pi值。用路径表达式中使用组件要用modules变量,如modules/math/pi。sting组件的调用方法如下:
tal:define="global mstring modules/string" tal:replace="python:mstring.join(slist, ':')"
事实上,我们很少这样使用string组件,我们多数使用string方法。
组件可组成包,用来组织和管理同类型组件。 Zope的Python-base脚本由一组组件组成,包含在Zope的Products包的PythonScripts子包中。该包中的 standard组件包含了很多有用的格式化功能。全名是Products.PythonScripts.standard。我们可以用下方法访问该包。
tal:define="global pps modules/Products.PythonScripts.standard" tal:define="global pps python:modules['Products.PythonScripts.standard']"
由于安全的原因,一些组件不能在DTML、ZPT和脚本中直接调用,除非添加了Zope安全许可。有关组件安全许可的问题,请参考Zope开发人员手册安全章节中使用“ModuleSecurityInfo”的内容。
我们可通过FTP和WebDAV远程编辑页面模板,这样我们就可在一些可视化编辑工具中进行页面模板的开发,如Macromedia Dreamweaver。操作步骤如下:
配置好服务器和编辑器,可参考本书中使用扩展工具一章的内容。
在 编辑器中以.pt为后缀新建一个文件,当上传到zope中时就会自动建立一个同名的页面模板对象。如果要建立一个以.html和.zpt等后缀或 index_html这样没有后缀的文件的话,就要在编辑器中以.pt上传到服务器后再通过ZMI界面进行手动修改或直接在ZMI中创建该文件。
修改PUT factory可以更改页面模板的默认.pt后缀,详情主参考本书的使用扩展工具一章的内容。
页面模板的生成速度是很快的,但当页面需频繁访问或页面的生成时间很长时,我们可用缓冲来动态调整服务器性能。有关缓冲的内容在本书的Zope服务章节中的缓冲管理器小节中有详细的介绍。
使 用缓冲管理缓冲页面模板和缓冲其它对象的操作一样。为了缓冲页面模板,我们必须把它与缓冲管理器关联起来。首先在Zope服务器中添加RAM Cache Manager对象。然后打开该对象,在Associate标签页中按locate,选择需要缓冲的Page Template,另一种方法是通过在需缓冲的页面模板中的Cache标签页中选择相应的缓冲管理来设置(设置前要先创建缓冲管理器)。下面我们一个示例 演示一下缓冲的作用。
## Script (Python) "long.py" ## for i in range(500): for j in range(500): for k in range(5): pass return 'Done'
上面是一个循环脚本,下面用一个页面模板来调用它,观察它的运行时间。
<html> <body> <p tal:content="here/long.py">results</p> </body> </html>
按上面二种方法之一设置面页缓冲后,浏览该页面,第一次将会花较长的时间,当我们第二次浏览该页面时速度快了很多,因为第二次是直接从 缓存中读取数据。当我们修改过页面模板后,页面需重新缓冲,所以修改后的第一次浏览会变回正常速度。缓冲是一个简单而强大的性能优化技术,我们需好好利 用。但缓冲也会有问题,因为zope只缓冲页面模板,所以如果脚本程序发生改变,它不能马上反映出来,需重新保存一次页面模板,使zope重新缓冲才能反 映最新变化,所以在使用时要好好规划你的缓冲机制。
有关缓冲的进一步信息可在在本书的zope服务一章中找到。
Zope的页机模板强大而简单。和DTML不同,页面模板没有一些方便的功能,如分组功能,生成树型结构和排序等功能。页面模板的开发者们为了保持它的简单,所以如DTML一样的内置功能都取消了。取而代之的是一个叫ZTUtils的实用工具。
当 用户查询数据库并返回大量的数据时,最好用分页的方法把数据以每页20条记录的形式显示。把一个大的列表分成小列表的方法叫分组(batching)。在 页面模板中,使用一个ZTUtils工具组件的Batch对象来实现。附录的API 参考里有ZTUtils工具组件的详细介绍。下面是利用Batch对象的例子。
<ul tal:define="lots python:range(100); batch python:modules['ZTUtils'].Batch(lots, size=10, start=0)"> <li tal:repeat="num batch" tal:content="num">0 </li> </ul>
上例把一100个数据的大组分成10组,每组10个数据。这样分组后就方便页面模板进行分页显示。用不同的开始值就可以10个为一组随机抽取数据,下例可抽取13到22共10个数据。
<ul tal:define="lots python:range(100); batch python:modules['ZTUtils'].Batch(lots, size=10, start=13)">
这里的start变量代表第一个数据项的索引,该索引可从0开始,所以13指向序列的第十四个数据项。分组后,我们还需要有导航元素,以便我们在分组的数据段间移动,以显示不同的数据。下面是一个包含导航功能的完整示例。
<html> <head> <title tal:content="template/title">The title</title> </head> <body tal:define="employees here/getEmployees; start python:int(path('request/start | nothing') or 0); batch python:modules['ZTUtils'].Batch(employees, size=3, start=start); previous python:batch.previous; next python:batch.next"> <p> <a tal:condition="previous" tal:attributes="href string:${request/URL0}?start:int=${previous/first}" href="previous_url">previous</a> <a tal:condition="next" tal:attributes="href string:${request/URL0}?start:int=${next/first}" href="next_url">next</a> </p> <ul tal:repeat="employee batch" > <li> <span tal:replace="employee/name">Bob Jones</span> makes $<span tal:replace="employee/salary">100,000</span> a year. </li> </ul> </body> </html>
接着在同一个文件夹下创建一个getEmployees的脚本。该脚本直接返回一个字典列表,页面模板以3为基数对该列表进行分组显示。
return [ {'name': 'Chris McDonough', 'salary':'5'}, {'name': 'Guido van Rossum', 'salary': '10'}, {'name': 'Casey Duncan', 'salary':'20' }, {'name': 'Andrew Sawyers', 'salary':'30' }, {'name': 'Evan Simpson', 'salary':'35' }, {'name': 'Stephanie Hand', 'salary':'40' }, ]
本例从getEmployees方法中获取数据分页显示,并能显示previous和next链接。使我们能进行翻页操作。
body 元素的tal:define语句定义了一个变量employees,接收getEmployees脚本返回的数据集。第二个变量start设置为 request/start或者0(当没有start变量时),start变量会随着显示数据段的不同而改变。batch变量保存以start为起点分组 后的3个数据。previous和next变量指向前一个和下一个分组。因为所有变量在body元素里定义,所以这些变量作用于整个body元素内。
接 着,让我们来看一看导航链接是如何生成的。tal:condition语句会测试是否有previous和next变量,如果有previous或 next变量,则会生成相关的链接。否则,不生成任何链接。tal:attributes语句具体生成指向previous和next的链接。我们可拷贝 该示例,做适当的修改以满足自已的需求。
Table of Contents
在 前面的Zope脚本基础一章中,你已知道如何通过编程来管理Zope对象。在这章中,我们将深入该主题。讨论的主题包括其它的脚本对象,脚本安全问题和如 何从ZPT或DTML表现层里调用脚本。就象前面我们所提及的,表现层和逻辑层分离是实现可维护Web应用的一个关键因素。
什么是逻辑层和它与表现层有何不同呢?一般来说,逻辑层是主要处理这些事务,如改变对象状态、发送信息、条件测试和对特定事件进行反馈等。而表示层主要格式化和显示信息或报表。在Zope中,DTML和ZPT是处理表现层内容的,而Zope脚本是用来处理逻辑层的。
Zope 脚本对象是用编程语言编写的一小块代码。脚本对象的概念首先出现在Zope2.3,现已成为在Zope中处理逻辑事务最好的方法。现在,Zope提供 Python-base脚本,可使用Python语言来编写Zope脚本。通过扩展,我们还可使用Perl语言脚本。在Zope脚本基础一章中,我们已讨 论过Python-base脚本的一些内容。在这里,我们还会讨论Python-base脚本的另一种形式--“外部方法(Extemal Methods)”,它位于文件系统中,不受Zope系统的安全约束。它允许你做很多在Zope中的Python-base脚本中不能做的事。通过外部方 法,Zope可利用Python编程语言扩展功能,基本上用Python语言可实现的功能,在Zope中也可实现。
在Zope 中,调用脚本的方式是很灵活的,我们可在ZPT或DTML中调用脚本,也可在脚本中调用另外一个脚本。调用时,脚本被当作一个对象来看待,Zope只关心 返回值,不理会具体实现,也就是说,不会理会脚本是用Python语言,还是Perl语言来写的。只要把脚本需要的参数正确传递给脚本对象即可。
和调用其它对象一样,只要使用标准的TALES路径表达式就可调用脚本,如:
<div tal:replace="here/hippo/feed"> Output of feed() </div>
上面的示例调用了当前位置下hippo目录中的feed脚本。脚本的返回值会插入到当前面页中。如果不想显示内容,可使用TALES的define标记,如:
<div tal:define="dummy here/hippo/feed" />
这里的here指向当前位置,与python-base脚本中的context变量类似。
如果你调用的脚本需要传递参数,你可使用TALES的python表达式来实现,如:
<div tal:replace="python:here.hippo.feed(food='spam')"> Output of feed(food='spam') </div>
这里的Python表达式中的here也是代表当前路径,它与Python Script中的context一样。名称不同,但代表的意思是一样的。在Zope 3中,ZPT中的here计划变成context,ZPT和Python Script统一叫法。但还是有一点不同,在ZPT中使用反斜杠分隔对象,在Python Script中使用点号分隔对象。
在DTML中调用脚本要使用call标记,如:
<dtml-call updateInfo>
上面会调用updateInfo脚本,这种调用方式同时也可用于调用页面模板或其它对象。如果调用的脚本需要传递参数,则用以下语法:
<dtml-call expr="updateInfo(color='brown', pattern='spotted')">
如果你在DTML中用dtml-let定义了变量,也可把该变量传递给脚本,如,我定义了两个参数newColor和newPattern,则可用以下语句把这两个变量作为参数传递给脚本:
<dtml-call expr="updateInfo(color=newColor, pattern=newPattern)">
同理,也可利用dtml-in标记自动定义的变量作为传递的参数,如:
<dtml-in all_animals prefix="seq"> <dtml-call expr="feed(animal=seq_item)"> </dtml-in>
从python script中调用另一个脚本的方法类似于在DTML中调用脚本,如:
new_color='brown' context.updateInfo(color=new_color, pattern="spotted")
context变量告诉Zope基于当前路径,并通过获取机制来搜索updateInfo脚本。
这里总结了各种调用脚本的方法
从Web中请求脚本
http://my-zope-server.com:8080/foo/updateInfo?amount=lots
在python script中调用
context.foo.updateInfo(amount="lots")
从页面模块调用
<span tal:content="here/foo/updateInfo" />
从页面模块带参数调用
<span tal:content="python:here.foo.updateInfo(amount='lots')" />
从DTML中调用
<dtml-with foo > <dtml-var updateInfo> </dtml-with>
从DTML中带参数调用
<dtml-with foo> <dtml-var expr="updateInfo(amount='lots')"> </dtml-with>
DTML的一种变种
<dtml-var expr="_['foo'].updateInfo()">
如果在调用脚本时找不到调用脚本,Zope会引发一个KeyError异常。我们可通过Python脚本来进行判断,如:
updateInfo = getattr(context, "updateInfo", None) if updateInfo is not None: updateInfo(color="brown", pattern="spotted") else: # complain about missing script return "error: updateInfo() not found"
getattr函数是Python的内建函数。它的第一个参数指定一个对象,第二个参数指定对象的名称,如果对象存在,则 getattr函数会返回对象名称,如果对象不存在,则返回第三个参数。所以可通过判断updateInfo是否为None来确定是否找到 updateInfo对象。
外部方法可使我们突破Zope的安全约束操作系统文件或网络。在使用时需确保它的访问权限是正确。外部方法建立在Zope服务器上Zope程序目录的Extensions目录中。
现在让我们来测试一下外部方法。在Extensions目录下建立一个文件叫example.py。内容如下:
def hello(name="World"): return "Hello %s." % name
接着在Zope服务器添加一个外部方法对象。在id栏填上“hello",在Function name栏填上”hello“,在Module name栏上填上”example“,最后按”Add“添加。添加完成后,变可按”test“标签测试,或用hello?name=mytest访问。
使用外部方法最主要的原因就是访问系统文件、网络和使用受约束的Python包和模块。如使用下面的方法访问本地主机系统的信息:
def instance_home(): import os return os.environ.get('INSTANCE_HOME')
re模块就一个很有用的工具,但在Zope中是不能使用的,只能通过外部方法实现:
import re pattern = r"<\s*body.*?>(.*?)</body>" regexp = re.compile(pattern, re.IGNORECASE + re.DOTALL) def extract_body(htmlstring): """ If htmlstring is a complete HTML page, return the string between (the first) <body> ... </body> tags """ matched = regexp.search(htmlpage) if matched is None: return "No match found" body = matched.group(1) return body
regexp对象在Zope第一次调用extract_body()方法被编译,而不是每次调用都编译一次,这可有效减少通信量。我们 把这段代码叫做my_extensions.py,在Zope中添加一个外部方法对象,id栏填”body_external_m“,Module Name栏填”my_extensions“,Function Name栏填”extract_body“。
现在我们可在Python Script中调用该 外部方法,如:
## Script (Python) "store_html" ## # code to get 'htmlpage' goes here... htmlpage = "some string, perhaps from an uploaded file" # now extract the body body = context.body_external_m(htmlpage) # now do something with 'body' ...
下面这个外部方法利用PIL建立文件夹中图像的缩略图。
def makeThumbnail(self, original_id, size=200): """ Makes a thumbnail image given an image Id when called on a Zope folder. The thumbnail is a Zope image object that is a small JPG representation of the original image. The thumbnail has an 'original_id' property set to the id of the full size image object. """ import PIL from StringIO import StringIO import os.path # none of the above imports would be allowed in Script (Python)! # Note that PIL.Image objects expect to get and save data # from the filesystem; so do Zope Images. We can get around # this and do everything in memory by using StringIO. # Get the original image data in memory. original_image=getattr(self, original_id) original_file=StringIO(str(original_image.data)) # create the thumbnail data in a new PIL Image. image=PIL.Image.open(original_file) image=image.convert('RGB') image.thumbnail((size,size)) # get the thumbnail data in memory. thumbnail_file=StringIO() image.save(thumbnail_file, "JPEG") thumbnail_file.seek(0) # create an id for the thumbnail path, ext=os.path.splitext(original_id) thumbnail_id=path + '.thumb.jpg' # if there's an old thumbnail, delete it if thumbnail_id in self.objectIds(): self.manage_delObjects([thumbnail_id]) # create the Zope image object for the new thumbnail self.manage_addProduct['OFSP'].manage_addImage(thumbnail_id, thumbnail_file, 'thumbnail image') # now find the new zope object so we can modify # its properties. thumbnail_image=getattr(self, thumbnail_id) thumbnail_image.manage_addProperty('original_id', original_id, 'string')
在Zope中添加一个makeThumbnail的外部方法,Function名为makeThumbnail,Module名为 Thumbnail。使用ImageFolder/makeThumbnail?original_id=Horse.gif就可调用该方法,建立一个名 为Horse.thumb.jpg缩略图。用一个循环就可把一个文件夹下的所有图像都创建一个缩略图,代码如下:
## Script (Python) "makeThumbnails" ## for image_id in context.objectIds('Image'): context.makeThumbnail(image_id)
再一次调该代码,它会为新创建的缩略图创建缩略图,这可不是我们想要的,我们把代码修改一下,因为所有的缩略图都有一个original_id属性,我们可通过这个属性判断普通图片和缩略图的区别。
## Script (Python) "makeThumbnails" ## for image in context.objectValues('Image'): if not image.hasProperty('original_id'): context.makeThumbnail(image.getId())
好了,现在我们可利用DTML把Python Script和外部方法粘合起来,利用DTML调用外部方法来显示缩略图。
<dtml-var standard_html_header> <dtml-if updateThumbnails> <dtml-call makeThumbnails> </dtml-if> <h2>Thumbnails</h2> <table><tr valign="top"> <dtml-in expr="objectValues('Image')"> <dtml-if original_id> <td> <a href="&dtml-original_id;"><dtml-var sequence-item></a> <br /> <dtml-var original_id> </td> </dtml-if> </dtml-in> </tr></table> <form> <input type="submit" name="updateThumbnails" value="Update Thumbnails" /> </form> <dtml-var standard_html_footer>
这个例子很好地展示了如何利用Script、External Methods和DTML一起工作。Python负责逻辑处理,DTML负责显示,外部方法负责调用扩展的功能包(PIL)。另外,我们也可很容易地用ZPT代替DTML的工作。
下面是一个利用外部方法处理XML信息程序。
# Uses Python 2.x standard xml processing packages. See # http://www.python.org/doc/current/lib/module-xml.sax.html for # information about Python's SAX (Simple API for XML) support If # you are using Python 1.5.2 you can get the PyXML package. See # http://pyxml.sourceforge.net for more information about PyXML. from xml.sax import parseString from xml.sax.handler import ContentHandler class MessageHandler(ContentHandler): """ SAX message handler class Extracts a message's to, from, and body """ inbody=0 body="" def startElement(self, name, attrs): if name=="message": self.recipient=attrs['to'] self.sender=attrs['from'] elif name=="body": self.inbody=1 def endElement(self, name): if name=="body": self.inbody=0 def characters(self, content): if self.inbody: self.body=self.body + content def receiveMessage(self, message): """ Called by a Jabber server """ handler=MessageHandler() parseString(message, handler) # call a script that returns a response string # given a message body string response_body=self.getResponse(handler.body) # create a response XML message response_message=""" <message to="%s" from="%s"> <body>%s</body> </message>""" % (handler.sender, handler.recipient, response_body) # return it to the server return response_message
外部方法可使我们做很多事情,但有一些功能还是很难实现的。我们还要遵循Zope架构的规则。你不能在外部方法中定义类和不能把该实例当成Zope对象的属性。如果你想创建一个新的持久对象,应该学习如何写Zope产品。相关的知识查阅Zope Developer's Guide。
我们可把参数传递给脚本,使脚本能灵活处理各种事务。当我们从Web中调用脚本时,Zope就会尝试从Web请求中查找参数并传递参数给脚本。例如有一个表单:
<form action="form_action"> Name of Hippo <input type="text" name="name" /><br /> Age of Hippo <input type="text" name="age" /><br /> <input type="submit" /> </form>
在Form中调用了form_action脚本,name和age参数就通过Web请求传递给form_action脚本。在form_action脚本中只要在parameter list中定义name和age两个参数即可接收这两个参数的值,如:
## Script (Python) "form_action" ##parameters=name, age ## "Process form" age=int(age) message= 'This hippo is called %s and is %d years old' % (name, age) if age < 18: message += '\n %s is not old enough to drive!' % name return message
通过在parameter list中定义参数列表,我们可在脚本中接收任意的参数。有关Web请求变量的介绍可参考Appendix B
在上面的脚本中,有一个小问题,我们需要传递的age参数是数值型而不是字符型的。但所有Form变量都是字符型的。我们必须手工用Python内建函数int()把它转成数值型。
age = int(age)
zope还提供一种方法,使我们可在form中进行类型转换:
Age <input type="text" name="age:int" />
:int后缀告诉Zope在接收Form参数时自动把它转换成int型。Zope可处理多种的类型转换,下面是一个Zope的基本类型转换表。
boolean
把一个变量转换成true或false。0、None、空字符串或空序列转换为false,其它的都转换为true。
int
转换成整型
long
转换成长整型
float
转换成浮点型
string
转换成字符型,变量基本都是字符型的,这种转换很少用到。
tuple
转换成Python元组
tokens
以空白符为分隔,把字符串转换成一个列表。
lines
以换行符为分隔,把字符串转换成一个列表。
date
把字符串转换成一个Date Time对象。
required
如果变量不存在,则引一个异常。
ignore_enpty
如果变量是一个空串,则从Web请求中获取变量
列表和元组转换器可与其它的转换器组合使用,这可把转换器应用到列表和元组的每一个元素。如:
<form action="processTimes"> <p>I would prefer not to be disturbed at the following times:</p> <input type="checkbox" name="disturb_times:list:date" value="12:00 AM" /> Midnight<br /> <input type="checkbox" name="disturb_times:list:date" value="01:00 AM" /> 1:00 AM<br /> <input type="checkbox" name="disturb_times:list:date" value="02:00 AM" /> 2:00 AM<br /> <input type="checkbox" name="disturb_times:list:date" value="03:00 AM" /> 3:00 AM<br /> <input type="checkbox" name="disturb_times:list:date" value="04:00 AM" /> 4:00 AM<br /> <input type="submit" /> </form>
上面的示例可把每个时间转换成date型,并把它们添加到disturb_times列表中。
一个更高级的转换器叫records,它是一种有属性的结构对象,可把一系列的参数组合成一个变量传递给脚本,脚本通过点标记法访问结构对象中的每个属性值。record转换器有以下几种:
record
把变量转换成一个结构对象的属性。
records
把变量转换成一个结构对象列表的属性。
default
如果变量为空,则提供一个默认值组结构对象属性。
ignore_empty
如果变量为空,忽略一个结构对象属性。
这里有一个例子:
<form action="processPerson"> First Name <input type="text" name="person.fname:record" /><br /> Last Name <input type="text" name="person.lname:record" /><br /> Age <input type="text" name="person.age:record:int" /><br /> <input type="submit" /> </form>
该表单会调用processPerson脚本,并传递一个person参数给脚本,这个参数有三个属性,分别是fname、lname和age。在脚本中使用person参数的方法如下:
## Script (Python) "processPerson" ##parameters=person ## "Process a person record" full_name="%s %s" % (person.fname, person.lname) if person.age < 21: return "Sorry, %s. You are not old enough to adopt an aardvark." % full_name return "Thanks, %s. Your aardvark is on its way." % full_name
records与record类似,但它会生成一个结构对象列表,如:
<form action="processPeople"> <p>Please, enter information about one or more of your next of kin.</p> <p> First Name <input type="text" name="people.fname:records" /> Last Name <input type="text" name="people.lname:records" /> </p> <p> First Name <input type="text" name="people.fname:records" /> Last Name <input type="text" name="people.lname:records" /> </p> <p> First Name <input type="text" name="people.fname:records" /> Last Name <input type="text" name="people.lname:records" /> </p> <input type="submit" /> </form>
变量people是一个records列表,每个record都有一个fname和lname属性。
list:int和int:list是一样的,与位置无关。
另一个有用的转换器是action,它可从新定义form的行为。这样我们可在form中根据不同条件选择不同的脚本。action转换器的描述如下:
action
为原始的form添加新的选择功能,在有两个提交按钮的情况下,我们可根据不同的提交按钮选择不同的脚本。action的我们也理解为method。
default_action
当没有action转换器时的默认选择。
这里是一个使用action的例子:
<form action="employeeHandlers"> <p>Select one or more employees</p> <input type="checkbox" name="employees:list" value="Larry" /> Larry<br /> <input type="checkbox" name="employees:list" value="Simon" /> Simon<br /> <input type="checkbox" name="employees:list" value="Rene" /> Rene<br /> <input type="submit" name="fireEmployees:action" value="Fire!" /><br /> <input type="submit" name="promoteEmployees:action" value="Promote!" /> </form>
我们假设一个名为employeeHandlers的文件夹下有两个脚本,分别fireEmployees和promoteEmployees。表单将根据你选择的提交按钮执行其中的一个脚本。
Form转换器在一发Zope应用时是很有用的,我们要好好学会使用它。
在 符合Zope安全策略的前提下,所有的脚本都能通过Web来编辑。Zope考虑到本身系统的安全性,Python Script的一些功能是受到约束的。例如,如果脚本要直接访问文件系统,则只能通过外部方法来实现。本章将介绍在Zope的安全架构如何确保脚本安全运 行。
如 果没有约束,Python Script可访问Zope的私有对象,修改Zope对象和影响Zope进程,直接访问运行Zope的服务器等。这些都会使Zope服务器运行出错,造成 Down机。约束Python Script的目的是防止它危害到Zope系统的正常运行。这些约束分别有:
循环限制
Script不能创建一个无限循环。如果Script运行一个大的循环,Zope会引发一个异常。这些循环包括for和while循环。
导入限制
Script不能随意地导入模块和包,你只能导入Products.PythonScript.standard工具模块、AccessControl模块。如果你要导入其它包和模块,请用外部方法来实现。
访问限制
当 你访问Zope对象时会受到Zope标准的安全策略的限制。换句话说,当用户用Script去访问对象时,是要被授权的。在调用脚本时,可以使用代理角 色,它可改变一个角色的授权。另外,你不能访问用下横线到头的对象,因为Zope把这个对象当成私有对象。在脚本中不要定义类,因为在zope中,你被限 制访问这些类的属性,因为你不能访问类的__init__方法。如果你要定义类,你应该使用外部方法或Zope产品。
写入限制
一般地,你不能直接改变Zope对象的属性,你应该调用Zope API提供的方法。
尽管有这些约束,但用户使用python Script还是有机会耗用大量的CPU和Memory资源。所以一些恶意的脚本会造成Dos攻击,耗用大量的系统资源。这是一个很难解决的问题,DTML同样也有这样的问题,所以我们应控制好可执行脚本人员的权限。
Zope为我们提供了多种开发的工具,编写短小的代码可使用python script脚本实现。DTML和ZPT可用以设计表现层。大型的逻辑处理可用python script或外部方法。下面用一个例子演示一下三种工具的不同实现过程。
使用DTML
<dtml-in objectValues> <dtml-var getId>: <dtml-var sequence-item> </dtml-in> done
使用ZPT
<div tal:repeat="item here/objectValues" tal:replace="python:'%s: %s\n' % (item.getId(), str(item))" />
使用python
for item in context.objectValues(): print "%s: %s" % (item.getId(), item) print "done" return printed
使用XML-RPC
import xmlrpclib server = xmlrpclib.Server('http://www.zopezoo.org/') for employee in server.JanitorialDepartment.personnel(): server.fireEmployee(employee)
通过HTTP客户端,可以远程调用Zope的脚本,下面以wget这个程序为例介绍一下。假设Zope中的Lions目录下有一个叫feeds的脚本,如果我们想每天早上运行:
$ wget --spider http://www.zopezope.org/Lions/feed
--spider选项告诉wget,不保存文件,直接执行。如果要该脚本需授权访问,则可这样写:
$ wget --spider --http-user=ZooKeeper \ --http-passwd=SecretPhrase \ http://www.zopezope.org/Lions/feed
设置cron,每天早上8点运行该命令。
$ crontab -e 0 8 * * * wget -nv --spider --http_user=ZooKeeper \ --http_pass=SecretPhrase http://www.zopezoo.org/Lions/feed
Table of Contents
Zope有两个对象可帮助你实现虚拟主机,分别是“SiteRoot”和“Virtual Host Monster”。通过虚拟主机服务,我们可在一个Zope服务器中建立多个网站。
“SiteRoot”对象是一个旧的在Zope中实现虚拟主机的对象,现在已多数不使用了,它还存在新版zope的对象列表中是为了向后兼容。在本章内,我们就不介绍它了。
如果你不正确配置“SiteRoot”,可能会被锁在Zope实例之外。 |
很 幸运,我们有了“Virtual Host Monsters”。“SiteRoot”有的功能它都有,而且没有“SiteRoot”那样的危险性。所以,如果你要建立虚拟主机,一定要用 “Virtual Host Monster”,如果你之前设置了“SiteRoot”,你应该在建立“Virtual Host Monster”之前,先把它删除掉,以免造成混乱。
Zope 对象都有自已的URLs,如,当一个Zope对象调用“absolute_url”方法时,它会返回这个对象的URL。这个URL包含一个主机名,一个端 口号和一个路径。对于Zope服务器的默认安装,你可指定主机名、端口号和路径以满足你的要求。但有时你要在一个zope服务器建立多个网站,而且每个网 站都有它自已的独立域名。或者要用apache或其它Web服务器和zope中的一个文件夹集成时,则我们就要配置Zope,使它生成的URL满足这些要 求。
“Virtual Host Monster”的唯一的工作就是改变zope对象的URLs。它允许你定制显示的URLs,当你通过不同方式访问对象时有不同的URL。如,我想发布一 个Zope文件夹,/foofolder,但我不想以文件夹的形式,而是以http://www.foofolder.com/形式来发布。 “Virtual Host Monster”的工作就是把http://www.foofolder.com/翻译成zope服务器中的/foofolder文件夹。如果该文件夹不 存在,则“Virtual Host Monster”就什么都不做,如果存在,则“Virtual Host Monster”会通过路径和目标信息来生成与http://www.foofolder.com/不同的URL,从而能正确访问到/foofolder 文件夹。
以URL或BASE开始的REQUEST变量值和对象的absolute_url()方法的值都对“Virtual Host Monster”的处理有影响。
“Virtual Host Monster”的配置可能会比较复杂,因为它要重写指向zope对象的URLs。为了能对URLs进行重写,必须使用一个“重写”的工具。 “Virtual Host Monster”本身包含了一个简单的重写工具,我们可在“mappings”标签里找到。另外还可使用Apache或其它的Web服务器来重写指向 Zope服务器的URLs。
Virtual Host Monster是Zope内置产品“SiteAccess”中的一个功能,你可在Zope中的对象添加列表中选择“Virtual Host Monster”来进行添加。添加时只要指定id号就可以了。(id号不能和同文件夹内的其它对象同名)
在Zope根目录下的一个Virtual Host Monster就可处理所有的虚拟主机需求,id号不能和同文件夹其它对象名相同就可以了。
默认配置模式可以什么都不用配置,而是通过外部Webserver来修改URL,而指向真正的zope发布对象。(参考下面的“Apache重写规则”)
如果你选择配置VHM,一种最简单的方式是通过VHM的ZMI介面,在“Mappings”标签内进行配置,具体的配置方式在下面会讲到。
我们也可用zope debugger的命令行界面进行配VHM。但底层API的相关资料不多,所以如果不是Zope开发人员,最好不要用这种方式。
VHM在URL中发现以下两个元素后才会进行截取和转换处理:
如果VHM在URL中搜索到这个名字,它会使Zope对象用另外的协议、主机名和端口号来生成URLs。
如果VHM在URL搜索到这个名字,它会使Zope对象用不同的“根目录”来生成URLs
VirtualHostBase 宣告在新URL的开始位置进行,VHM会截取这个宣告后的两个元素,一个是新的协议,一个是新的主机名和端口号。格式如下: VirtualHostBase/protocol/hostname:portnumber,第二个元素的端口号是可选的,如果没有指定端口号,则 VHM就不会改变原始端口号。下面是一些例子:
如果VHM位于根目录,而且请求URL是: “http://zopeserver:8080/VirtualHostBase/http/www.buystuff.com” 则会生成如下的URL: “http://buystuff.com:8080” 如果VHM位于根目录,而且请求URL是: “http://zopeserver:8080/VirtualHostBase/http/www.buystuff.com:80” 则会生成如下的URL: “http://buystuff.com” (由于端口号80是默认的http端口,所以这里不用写) 如果VHM位于根目录,而且请求URL是: “http://zopeserver:8080/VirtualHostBase/https/www.buystuff.com:443” 则会生成如下URL: “https://buystuff.com/” (由于443端口是https默认的端口,所以这里也不用写)
如 果你的Zope是运行在8080端口,而你想重新生成的URL用标准的80端口代替8080端口(这样URL就不用加8080这样的端口后缀),你就要在 VirtualHostBase宣告中指明80端口,如:/VirtualHostBase/http/www.buystuff.com:80。如果没 有指定:80,则会用回Zope默认的http端口(8080),这可能不是你想要的。 |
VirtualHostRoot 宣告在新URL的结尾附近进行。VHM会把VirtualHostRoot宣告前后的路径元素联接起来,这些路径元素组成Zope对象结构,需发布的对象 就被重写成跟在VirtualHostRoot后的元素。举个例子就很容易明白:对于一个/a/b/c/VirtualHostRoot/d的URL, VHM将会形成“a/b/c/d”的对象结构,/d就是新的URL路径。下面是几个实例:
如果VHM位于根目录,而且请求URL是: “http://zopeserver:8080/Folder/VirtualHostRoot/” 则Folder对象会被发布,生成的URL是: “http://zopeserver:8080/” 当你用上面的URL访问时,就是访问Folder对象。 如果VHM位于根目录,而且请求URL是: “http://zopeserver:8080/HomeFolder/VirtualHostRoot/Chris” 则/HomeFolder/Chris对象就被发布,生成的URL是: “http://zopeserver:8080/Chris” 当你用上面的URL访问时,就是访问 “/HomeFolder/Chris”
我 们可在Zope的根目录中建立一个VHM,为不同的域提供服务,如:http://www.buystuff.com指向Zope根目录下的 /buystuff文件夹;http://www.mycause.org指向Zope根目录下的/mycause文件夹。要这样配置,我们需同时用到 VirtualHostBase和VirtualHostRoot。如:
/VirtualHostBase/http/www.mycause.org:80/mycause/VirtualHostRoot/ /VirtualHostBase/http/www.buystuff.com:80/buystuff/VirtualHostRoot/
设 置一台在8080端口监听http连接的zope服务器。在服务器的根目录下添加一个VHM对象,id名用“VHM”。接着在根目录下建立一个名为 vhm_test的文件夹。最后在这个新建立的文件夹内建立一个名为index_html的DTML Method,内容为:
<html> <body> <table border="1"> <tr> <td>Absolute URL</td> <td><dtml-var absolute_url></td> </tr> <tr> <td>URL0</td> <td><dtml-var URL0></td> </tr> <tr> <td>URL1</td> <td><dtml-var URL1></td> </tr> </table> </body> </html>
单击“view”标签,可看到如下内容:
Absolute URL http://localhost:8080/vhm_test URL0 http://localhost:8080/vhm_test/index_html URL1 http://localhost:8080/vhm_test
现在我们用http://localhost:8080/vhm_test来浏览,你将得到我上面一模一样的结果。接着再用 http://localhost:8080/VirtualHostBase/http/zope.com:80/vhm_test来浏览,你会得到如 下的结果:
Absolute URL http://zope.com/vhm_test URL0 http://zope.com/vhm_test/index_html URL1 http://zope.com/vhm_test
我们注意到,Zope生成的URLs改变了,用主机名和路径代替了localhost:8080。我们通过上面的指令告诉zope,直接使用zope.com作为主机名,不用写端口号是因为我们已告诉Zope生成的URLs是使用默认的80端口。
现在再浏览以下URL:http://localhost:8080/VirtualHostBase/http/zope.com:80/vhm_test/VirtualHostRoot/。显示结果如下:
Absolute URL http://zope.com URL0 http://zope.com/index_html URL1 http://zope.com
我们可以注意到,现在把vhm_test文件夹当作了zope.com的根目录。这是通过添加VirtualHostRoot来指定vhm_test为网站的根目录的。
到 这里为止,你可会奇怪,VHM是如何帮助我们的呢?我们肯定不会叫用户输入像这样的URL:http: //yourserver.com//VirtualHostBase/http/zope.com/vhm_test/VirtualHostRoot/ 。虽然Zope可正确生成URL,但对用户来说是不直观,难明的。所以我们可以叫电脑来做这个工作,通常有两种方式来完成该项工作,一种是通过VHM的 “Mappings”标签,另外一种是通过Apache的“重写规则(rewrite rules)”,或者其它Webserver的类似工具来完成。
你只可使用其中的一种,不能两种方式都用,这样会出现奇怪的情况。 |
使用VHM的Mappings Tab标签可设置URL的重写规则。但前提是要满足以下两个条件:
需单独运行Zope服务器,没有和其它Web服务器集成,如Apache。
需设置发布文件夹,以便可通过http://some.hostname.com代替http://hostname.com/a/folder。
下 面以例子来说明如何使用Mappings tab。假设我们有一个Zope服务器,它运行在localhost:8080上,VHM对象已添加到根目录上。在演示前,我们还需设置一下网络环境,在 hosts文件内添加一个别名(unix系统的hosts文件位于/etc/hosts,windows系统的hosts文件位于c:\winnt\ system32\drivers\etc\hosts),内容如下:
... 127.0.0.1 www.example.com ...
这样设置后,我们就可用www.example.com这样的域名来访问本机服务了。 |
接着进入根目录下VHM,点击“Mappings tab”标签,输入以下内容:
www.example.com:8080/vhm_test
这样,vhm_test文件夹就会以http://www.example.com:8080的形式发布。我们访问http://www.example.com:8080其实就是访问vhm_test文件夹。
你也可以用该功能来匹配多子域,只要在规则前加一个“.”就可以了,如:“.buystuff.com”可匹配“my.buystuff.com”、“zoom.buystuff.com”。
在这里不能更改8080端口号,如果需要更改端口号,你必须在使用Zope的配置文件设置或用Apache重写规则。
如 果你使用Apache做为Zope的前台服务器,那么你就要用Apache的重写规则来代替Zope的Mappings tab。Apache的重写规则是很直观,很容易理解:Apache服务器监听普通端口(80),Zope服务器监听另一个端口(8080)。 Apache在80端口接收请求,之后通过Apache配置文件中的虚拟主机指令,把请求重新定向到Zope服务器的发布目录。
要使用 Apache的重写功能,你需要mod_rewrite和mod_proxy两个Apache模块。安装这两个模块的方法是在编译Apache时,用-- enable-modules="rewrite proxy"标记编译就可以了。如果你使用Apache2.0系列版本,你需要mod_proxy_http模块,详细介绍请参考Apache mod_rewrite documentation。在Apache配置文件http.conf的LoadModule节可查看Apache可加载的模块。
配置好Apache重写模块后,我们就可以配置重写规则了。下面我们以一个例子详细介绍如何配置。和上例配置环境一样,我们在hosts文件中设置主机名:
... 127.0.0.1 www.example.com ...
Apache在80运行在端口,Zope运行在本地的8080端口。我们要把www.example.com映射到根目录下的 vhm_test文件夹。也就是以vhm_test作为www.example.com网站的根目录。我们可在Apache的http.conf配置文件 的虚拟主机部份设置如下内容:
NameVirtualHost *:80 <VirtualHost *:80> ServerName www.example.com RewriteEngine On RewriteRule ^/(.*) http://127.0.0.1:8080/VirtualHostBase/http/www.example.com:80/vhm_test/VirtualHostRoot/$1 [L,P] </VirtualHost>
如果你需要使用SSL连接Zope服务器,则可这样设置:
NameVirtualHost *:443 <VirtualHost *:443> ServerName www.example.com SSLProxyEngine on RewriteEngine On RewriteRule ^/(.*) http://127.0.0.1:8080/VirtualHostBase/https/www.example.com:443/vhm_test/VirtualHostRoot/$1 [L,P] </VirtualHost>
重写规则必须在一行中写完。 |
这 样设置后,当我们访问www.example.com时,重写规则就把URL重定向到http://127.0.0.1: 8080/VirtualHostBase/http/www.example.com:80/vhm_test/VirtualHostRoot/, Zope服务器中的VMH接受该URL并进行处理,指向vhm_test文件夹。Apache负责显示页面,内容则来自Zope服务器的vhm_test 文件夹。
有关Apache重写规则的写法请参考Apache Documentation。
虚 拟主机还有一个作用,它可使Zope服务器看起来象其它主控服务器的一部份。例如,Zope可能只提供http: //www.mycause.org/dynamic_stuff的内容,而Apache或其它Webserver提供http: //www.mycause.org/的服务。为了做到这个功能,我们需把dynamic_stuff加到zope生成URLs的开始位置。
如果你使用了VirtualHostRoot,紧接着以_vh_开头加上路径元素,形成新的规则,如:
VHM在根目录,请求URL是: http://127.0.0.1:8080/VirtualHostBase/http/www.example.com:80/travel/VirtualHostRoot/ Zope生成的URL是: http://www.example.com/index_html VHM在根目录,请求URL是: http://127.0.0.1:8080/VirtualHostBase/http/www.example.com:80/travel/VirtualHostRoot/_vh_mytest Zope生成的URL是: http://www.example.com/mytest/index_html