Index: quintagroup.plonecaptchas/tags/4.1/MANIFEST.in
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/MANIFEST.in	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/MANIFEST.in	(revision 3272)
@@ -0,0 +1,3 @@
+recursive-include docs *
+recursive-include quintagroup *
+global-exclude *pyc
Index: quintagroup.plonecaptchas/tags/4.1/README.txt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/README.txt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/README.txt	(revision 3272)
@@ -0,0 +1,92 @@
+Plone Captchas
+==============
+
+quintagroup.plonecaptchas is a simple captchas implementation for Plone, designed
+for validation of human input in insecure forms. This is a standalone 
+implementation which does not depend on captchas.net services.
+
+quintagroup.plonecaptchas adds captcha support to Plone, it works together with 
+quintagroup.captcha.core package (http://projects.quintagroup.com/products/wiki/quintagroup.captcha.core) and quintagroup.formlib.captcha (http://projects.quintagroup.com/products/wiki/quintagroup.formlib.captcha)
+
+With these products installed captchas will be added to Plone's 'Send this', 
+'Contact Us' (/contact-info) forms, and Plone's default discussion mechanism: 
+'Add Comment' and 'Reply' forms.
+
+quintagroup.plonecaptchas does not automatically plug to Plone's default registration
+(/@@register). You can make captcha plug to Plone's Register form via Zope Management Interface.
+Instructions here: http://projects.quintagroup.com/products/wiki/quintagroup.plonecaptchas#JoinForm
+
+Requirements
+------------
+
+* Plone 4.0 and above 
+
+Notes
+-----
+
+* For Plone 4 versions - use quintagroup.plonecaptchas 4.0 release and up http://plone.org/products/plone-captchas/releases/4.0. In your buildout.cfg file's egg section set product version::
+
+   [buildout]
+   ....
+   eggs =
+        ...
+        quintagroup.plonecaptchas >=4.0
+
+* For Plone 3 versions - use quintagroup.plonecaptcha 3.x releases http://plone.org/products/plone-captchas/releases/3.0. In your buildout.cfg file's egg section set product version::
+
+   [buildout]
+   ....
+   eggs =
+        ...
+        quintagroup.plonecaptchas >=3.0,<4.0
+
+* For Plone 2.x versions - use 1.3.4 version of qPloneCaptchas product for use on forms
+
+
+Dependencies
+------------
+
+* quintagroup.captcha.core (PIL with _imagingft C module for dynamic captcha)
+* quintagroup.formlib.captcha
+* PIL with Jpeg and FreeType support
+
+Plone Captchas on PloneFormGen forms 
+------------------------------------
+
+To make captchas work on forms created with PloneFormGen, please use 'quintagroup.pfg.captcha' product:
+http://projects.quintagroup.com/products/wiki/quintagroup.pfg.captcha
+
+Installation
+------------
+
+See docs/INSTALL.txt for instructions.
+
+Note: If Plone Captchas is expected to be used with Plone Comments 
+http://quintagroup.com/services/plone-development/products/plone-comments,
+for proper behavior you have to install Plone Captchas first, and then Plone Comments.
+
+Links
+-----
+
+* Plone Captchas home page - http://quintagroup.com/services/plone-development/products/plone-captchas
+* Plone Captchas Screencasts - http://quintagroup.com/cms/screencasts/qplonecaptchas
+* Documentation - http://projects.quintagroup.com/products/wiki/quintagroup.plonecaptchas
+* SVN Repository - http://svn.quintagroup.com/products/quintagroup.plonecaptchas
+
+Authors
+-------
+
+The product was developed by Quintagroup team:
+
+* Andriy Mylenkyi 
+* Volodymyr Cherepanyak
+* Mykola Kharechko
+* Vitaliy Stepanov
+* Bohdan Koval
+
+Contributors
+------------
+
+* Dorneles Tremea
+
+Copyright (c) "Quintagroup": http://quintagroup.com, 2004-2010
Index: quintagroup.plonecaptchas/tags/4.1/docs/HISTORY.txt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/docs/HISTORY.txt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/docs/HISTORY.txt	(revision 3272)
@@ -0,0 +1,35 @@
+Changelog
+=========
+
+4.1  - Sep 5, 2011
+------------------
+
+* compatibility with plone.app.discussion added
+
+4.0 - June 9, 2010
+------------------
+
+* compatibility with Plone 4 added
+
+3.0 - Apr 7, 2010
+-----------------
+
+* product was refactored: captcha support for the standard Plone forms remained
+  in quintagroup.plonecaptchas; the captchas core rendering and validation 
+  functionality were factored into quintagroup.captcha.core package
+
+2.0.0 - July 15, 2009
+---------------------
+
+* product was transformed to meet new standards: eggified package
+
+* compatibility with Plone 3.x added
+
+* support for earlier Plone versions 2.0.x -2.5.x removed
+
+* test install/uninstall skins & configlet added
+
+1.3.4 - March 2, 2009
+---------------------
+
+* Fixed bug in method parseKey [Maik Röder]
Index: quintagroup.plonecaptchas/tags/4.1/docs/INSTALL.txt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/docs/INSTALL.txt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/docs/INSTALL.txt	(revision 3272)
@@ -0,0 +1,90 @@
+Installation
+============
+
+Buildout
+--------
+
+To install quintagroup.plonecaptchas to your buildout-based project:
+
+* Add ``quintagroup.plonecaptchas`` to the list of eggs to install::
+
+    [buildout]
+    ...
+    eggs =
+        ...
+        quintagroup.plonecaptchas
+       
+* Tell the plone.recipe.zope2instance recipe to install a ZCML slug::
+
+    [instance]
+    recipe = plone.recipe.zope2instance
+    ...
+    zcml =
+        quintagroup.plonecaptchas
+      
+* Re-run buildout, e.g. with::
+
+    $ ./bin/buildout
+    
+* Restart the Zope server, e.g with the following command in the terminal::
+
+   $ ./bin/instance restart
+   
+* Install ``Plone Captchas`` with Quickinstaller in Plone (Site Setup -> Add/Remove Products). 
+``Quintagroup Captcha Core`` will automatically be installed as dependency.
+        
+	
+Traditional Zope 2 instance
+---------------------------
+
+To install quintagroup.plonecaptchas into the global Python environment (or a workingenv),
+using a traditional Zope 2 instance, you can do this:
+
+* Copy ``quintagroup`` folder to your instance's ``/lib/python`` directory.
+
+* Create a file called ``quintagroup.plonecaptchas-configure.zcml`` in your instance's ``/etc/package-includes`` directory. The file
+should only contain this::
+
+    <include package="quintagroup.plonecaptchas" />
+
+* Restart the Zope server, for example with the following command in the terminal::
+
+   $ ./bin/zopectl restart
+
+* Install ``Plone Captchas`` with Quickinstaller in Plone (Site Setup -> Add/Remove Products). 
+``Quintagroup Captcha Core`` will automatically be installed as dependency.
+
+
+Protecting Register form with captcha
+-------------------------------------
+
+quintagroup.plonecaptchas does not automatically plug to Plone's default registration
+(/@@register), to protect register form with captcha - you must follow the instructions here:
+http://projects.quintagroup.com/products/wiki/quintagroup.plonecaptchas#JoinForm
+
+
+Migration notes
+===============
+
+Following migration notes must be done only if you protect join (registration) form with captcha.
+All steps performed in ZMI (<portal>/manage_main).
+
+=== Migration from Plone-3.X to Plone-3.Y ===
+
+If you have Plone-3.X site with installed quintagroup.plonecaptchas v3 package, and want to migrate site to Plone-3.Y, you must perform following steps:
+  * migrate your plone site to Plone-3.Y
+  * update *Directory:* property of `<portal>/portal_skins/captchas_join_form` Filesystem Directory View' - go to `<portal>/portal_skins/captchas_join_form/manage_propertiesForm` and update value of the *Directory* property according to the following rooles:
+    *  for Plone-3.0: quintagroup.plonecaptchas:skins/captchas_join_form_30
+    *  for Plone-3.1 or Plone-3.2: quintagroup.plonecaptchas:skins/captchas_join_form_31_32
+    *  for Plone-3.3+: quintagroup.plonecaptchas:skins/captchas_join_form_33
+    
+
+=== Migration from Plone-3 to Plone-4 ===
+
+If you have Plone-3 site with installed quintagroup.plonecaptchas v3 package, and want to migrate site to Plone-4, you must perform following steps:
+  * update plone-4 buildout/instance - to use quintagroup.plonecaptchas v4 package instead of quintagroup.plonecaptchas v3
+  * migrate your plone site to Plone-4
+  * remove `<portal>/portal_skins/captchas_join_form` Filesystem Directory View'
+  * remove *captchas_join_form* layer from all skins in `<portal>/portal_skins/manage_propertiesForm`
+  * run all steps with "Plone Captchas Register Forms for Plone 4" upgrade profile - go to `<portal>/portal_setup/manage_importSteps`, select mentioned profile and push "Import all steps" button.
+
Index: quintagroup.plonecaptchas/tags/4.1/docs/LICENSE.GPL
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/docs/LICENSE.GPL	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/docs/LICENSE.GPL	(revision 3272)
@@ -0,0 +1,225 @@
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
Index: quintagroup.plonecaptchas/tags/4.1/docs/LICENSE.txt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/docs/LICENSE.txt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/docs/LICENSE.txt	(revision 3272)
@@ -0,0 +1,16 @@
+  quintagroup.plonecaptchas is copyright Quintagroup
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 59 Temple Place, Suite 330, Boston, 
+  MA 02111-1307 USA.
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/__init__.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/__init__.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/__init__.py	(revision 3272)
@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/Extensions/Install.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/Extensions/Install.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/Extensions/Install.py	(revision 3272)
@@ -0,0 +1,30 @@
+import transaction
+from Products.CMFCore.utils import getToolByName
+REQUIRED = 'quintagroup.captcha.core'
+from quintagroup.plonecaptchas.config import HAS_APP_DISCUSSION
+from quintagroup.plonecaptchas.setuphandlers import registerDiscussionLayer
+
+
+def install(self):
+    qi = getToolByName(self, 'portal_quickinstaller')
+    # install required quintagroup.captcha.core product
+    # BBB: Need to success installation in Plone<3.1
+    #      (with GenericSetup < v1.4.2, where dependency
+    #       support was not yet implemented)
+    if not REQUIRED in qi.listInstalledProducts():
+        qi.installProduct(REQUIRED)
+    # install plonecaptchas
+    gs = getToolByName(self, 'portal_setup')
+    profile = 'profile-quintagroup.plonecaptchas:default'
+    gs.runAllImportStepsFromProfile(profile)
+    if HAS_APP_DISCUSSION:
+        # register browser layer
+        registerDiscussionLayer(self)
+    transaction.savepoint()
+
+
+def uninstall(self):
+    portal_setup = getToolByName(self, 'portal_setup')
+    profile = 'profile-quintagroup.plonecaptchas:uninstall'
+    portal_setup.runAllImportStepsFromProfile(profile, purge_old=False)
+    transaction.savepoint()
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/Extensions/__init__.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/Extensions/__init__.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/Extensions/__init__.py	(revision 3272)
@@ -0,0 +1,1 @@
+#
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/__init__.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/__init__.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/__init__.py	(revision 3272)
@@ -0,0 +1,14 @@
+from zope.i18nmessageid import MessageFactory
+
+from AccessControl import ModuleSecurityInfo
+
+product = 'quintagroup.plonecaptchas'
+ProductMessageFactory = MessageFactory(product)
+ModuleSecurityInfo(product).declarePublic("ProductMessageFactory")
+
+#from quintagroup.plonecaptchas import config
+#allow_module('quintagroup.plonecaptchas.config')
+
+
+def initialize(context):
+    pass
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/__init__.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/__init__.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/__init__.py	(revision 3272)
@@ -0,0 +1,1 @@
+#
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/configure.zcml
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/configure.zcml	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/configure.zcml	(revision 3272)
@@ -0,0 +1,18 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser"
+    xmlns:five="http://namespaces.zope.org/five"
+    i18n_domain="plone">
+
+    <include package="Products.CMFCore" file="permissions.zcml" />
+
+    <!-- Join form definition -->
+    <browser:page
+      name="register"
+      for="plone.app.layout.navigation.interfaces.INavigationRoot"
+      class=".register.CaptchaRegistrationForm"
+      layer="..interfaces.IQGPloneCaptchas"
+      permission="cmf.AddPortalMember"
+      />
+
+</configure>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/newuser_form.pt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/newuser_form.pt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/newuser_form.pt	(revision 3272)
@@ -0,0 +1,46 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+      metal:use-macro="context/main_template/macros/master"
+      i18n:domain="plone">
+<head>
+    <metal:block fill-slot="top_slot"
+                 tal:define="dummy python:request.set('disable_border',1);
+                             disable_column_one python:request.set('disable_plone.leftcolumn',1);
+                             disable_column_two python:request.set('disable_plone.rightcolumn',1);" />
+</head>
+
+<body>
+    <div metal:fill-slot="body" id="content">
+
+      <div metal:use-macro="context/base-pageform.html/form" />
+        <!--tal:nomailhost tal:condition="not:view/showForm">
+            <div metal:use-macro="context/base-pageform.html/form" >
+                <div metal:fill-slot="viewspace">
+                    <h1 class="documentFirstHeading"
+                        i18n:translate=""
+                        tal:content="view/label" />
+
+                    <p class="documentDescription"
+                       i18n:translate="help_cannot_add_new_user">
+                      In the
+                      <a i18n:name="security_link" i18n:translate=""
+                         tal:attributes="href string:${context/portal_url}/@@security-controlpanel">
+                      Security settings</a>
+                      you have requested to validate emails of new
+                      users.  But this site doesn't have a valid email
+                      setup, so you cannot register users at this
+                      time.  You can fix this in the
+                      <a i18n:name="mail_link" i18n:translate=""
+                         tal:attributes="href string:${context/portal_url}/@@mail-controlpanel">
+                      Mail settings</a>.
+                    </p>
+                </div>
+            </div>
+        </tal:nomailhost-->
+
+
+    </div>
+</body>
+</html>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/register.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/register.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/register.py	(revision 3272)
@@ -0,0 +1,30 @@
+from zope.formlib import form
+from zope.interface import Interface
+from plone.app.users.browser.register import RegistrationForm
+
+from quintagroup.formlib.captcha import Captcha
+from quintagroup.formlib.captcha import CaptchaWidget
+
+from quintagroup.plonecaptchas import ProductMessageFactory as _
+
+
+class CaptchaSchema(Interface):
+    captcha = Captcha(
+        title=_(u'Type the code'),
+        description=_(u'Type the code from the picture shown below.'))
+
+
+class CaptchaRegistrationForm(RegistrationForm):
+    """Registration form with captacha."""
+
+    @property
+    def form_fields(self):
+        """Add captcha field to form_fields."""
+        ffields = super(CaptchaRegistrationForm, self).form_fields
+        if len(ffields):
+            ffields = ffields + form.Fields(CaptchaSchema)
+            # XXX: Because of raising ConfigurationConflictError when include
+            # configure.zcml slug of quintagroup.formlib.captcha, we must
+            # explicytly define custom CaptchaWidget widget for Captcha field.
+            ffields["captcha"].custom_widget = CaptchaWidget
+        return ffields
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/register_form.pt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/register_form.pt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/browser/register_form.pt	(revision 3272)
@@ -0,0 +1,38 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+      metal:use-macro="context/main_template/macros/master"
+      i18n:domain="plone">
+<head>
+    <metal:block fill-slot="top_slot"
+                 tal:define="dummy python:request.set('disable_border',1);
+                             disable_column_one python:request.set('disable_plone.leftcolumn',1);
+                             disable_column_two python:request.set('disable_plone.rightcolumn',1);" />
+</head>
+
+<body>
+    <metal:body fill-slot="body">
+        
+        <div id="content">    
+            <tal:mailhost tal:condition="view/showForm">
+                <div metal:use-macro="context/base-pageform.html/form" />
+            </tal:mailhost>
+
+            <tal:nomailhost tal:condition="not:view/showForm">
+            
+                <h1 class="documentFirstHeading"
+                    i18n:translate=""
+                    tal:content="view/label" />
+
+                <div class="documentDescription">
+                    This site doesn't have a valid email setup, so you
+                    cannot register at this time.
+                </div>
+                            
+            </tal:nomailhost>
+        </div>
+
+    </metal:body>
+</body>
+</html>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/config.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/config.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/config.py	(revision 3272)
@@ -0,0 +1,11 @@
+GLOBALS = globals()
+PRODUCT_NAME = 'quintagroup.plonecaptchas'
+CAPTCHA_NAME = 'plonecaptchas'
+
+HAS_APP_DISCUSSION = True
+try:
+    import plone.app.discussion
+except ImportError:
+    HAS_APP_DISCUSSION = False
+
+#TOOL_ICON = 'tool.gif'
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/configure.zcml
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/configure.zcml	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/configure.zcml	(revision 3272)
@@ -0,0 +1,62 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:five="http://namespaces.zope.org/five"
+    xmlns:i18n="http://namespaces.zope.org/i18n"
+    xmlns:cmf="http://namespaces.zope.org/cmf"
+    xmlns:gs="http://namespaces.zope.org/genericsetup"
+    xmlns:zcml="http://namespaces.zope.org/zcml"
+    i18n_domain="quintagroup.plonecaptchas">
+
+    <!-- include dependent packages -->
+    <include package="quintagroup.captcha.core" />
+    <!-- can't included quintagroup.formlib.captcha
+        because of ConfigurationConflictError. -->
+
+    <five:registerPackage package="." initialize=".initialize" />
+
+    <!-- include packages -->
+    <include package=".browser" />
+
+    <include file="meta.zcml"/>
+
+    <!-- File System Directory Views registration -->
+    <cmf:registerDirectory name="captchas_discussion" />
+    <cmf:registerDirectory name="captchas_sendto_form" />
+    <cmf:registerDirectory name="captchas_contact_info" />
+
+    <configure zcml:condition="installed plone.app.discussion">
+
+        <adapter
+            factory=".validate.QGDiscussionCaptchaValidator"
+            provides="z3c.form.interfaces.IValidator"
+            />
+    </configure>
+
+    <gs:registerProfile
+        name="default"
+        title="Plone Captchas"
+        directory="profiles/default"
+        description="This product adds captcha support to Plone."
+        for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+        provides="Products.GenericSetup.interfaces.EXTENSION"
+        />
+
+    <gs:registerProfile
+        name="register_form"
+        title="Plone Captchas Register Forms for Plone 4"
+        directory="profiles/register_form"
+        description="This profile adds captcha protection of Register Form in Plone 4."
+        for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+        provides="Products.GenericSetup.interfaces.EXTENSION"
+        />
+
+    <gs:registerProfile
+        name="uninstall"
+        title="Uninstall Plone Captchas"
+        directory="profiles/uninstall"
+        description="Uninstall Plone Captchas product."
+        for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+        provides="Products.GenericSetup.interfaces.EXTENSION"
+        />
+
+</configure>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/interfaces.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/interfaces.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/interfaces.py	(revision 3272)
@@ -0,0 +1,12 @@
+from plone.theme.interfaces import IDefaultPloneLayer
+from quintagroup.plonecaptchas.config import HAS_APP_DISCUSSION
+
+if HAS_APP_DISCUSSION:
+    from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+
+    class IQGDiscussionCaptchas(IDefaultBrowserLayer):
+        """quintagroup.plonecaptchas browser layer interface for plone.app.discussion"""
+
+
+class IQGPloneCaptchas(IDefaultPloneLayer):
+    """quintagroup.plonecaptchas browser layer interface"""
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/meta.zcml
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/meta.zcml	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/meta.zcml	(revision 3272)
@@ -0,0 +1,10 @@
+<configure
+    xmlns:meta="http://namespaces.zope.org/meta"
+    xmlns:zcml="http://namespaces.zope.org/zcml">
+
+    <!-- Declare that quintagroup.plonecaptchas provides a Captcha field that 
+         can be used by plone.app.discussion to add a Captcha field to comment 
+         forms. -->
+    <meta:provides feature="plone.app.discussion-captcha" />
+    
+</configure>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/default/metadata.xml
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/default/metadata.xml	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/default/metadata.xml	(revision 3272)
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<metadata>
+  <version>4.1</version>
+  <dependencies>
+    <dependency>profile-quintagroup.captcha.core:default</dependency>
+  </dependencies>
+</metadata>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/default/quintagroup.plonecaptchas_various.txt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/default/quintagroup.plonecaptchas_various.txt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/default/quintagroup.plonecaptchas_various.txt	(revision 3272)
@@ -0,0 +1,1 @@
+This file is used as a marker in setuphandlers.py.
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/default/skins.xml
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/default/skins.xml	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/default/skins.xml	(revision 3272)
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<object name="portal_skins">
+
+  <object name="captchas_discussion"
+          meta_type="Filesystem Directory View"
+          directory="quintagroup.plonecaptchas:skins/captchas_discussion" />
+  <object name="captchas_sendto_form"
+          meta_type="Filesystem Directory View"
+          directory="quintagroup.plonecaptchas:skins/captchas_sendto_form" />
+  <object name="captchas_contact_info"
+          meta_type="Filesystem Directory View"
+          directory="quintagroup.plonecaptchas:skins/captchas_contact_info" />
+
+  <skin-path name="*">
+    <layer name="captchas_discussion"
+           insert-after="custom" />
+    <layer name="captchas_sendto_form"
+           insert-after="custom" />
+    <layer name="captchas_contact_info"
+           insert-after="custom" />
+  </skin-path>
+
+</object>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/register_form/browserlayer.xml
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/register_form/browserlayer.xml	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/register_form/browserlayer.xml	(revision 3272)
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<!-- Register the package-specific browser layer, so that it will be activated
+when this product is installed. -->
+<layers>
+   <layer name="quintagroup.plonecaptchas"
+          interface="quintagroup.plonecaptchas.interfaces.IQGPloneCaptchas" />
+</layers>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/uninstall/import_steps.xml
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/uninstall/import_steps.xml	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/uninstall/import_steps.xml	(revision 3272)
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<import-steps>
+ <import-step id="qgplonecaptchas_uninstall" version="20100603-01"
+              handler="quintagroup.plonecaptchas.setuphandlers.uninstall"
+              title="Quintagroup Plone Captchas uninstallation">
+  Quintagroup Plone Captchas uninstallation
+ </import-step>
+</import-steps>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/uninstall/qgplonecaptchas_uninstall.txt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/uninstall/qgplonecaptchas_uninstall.txt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/uninstall/qgplonecaptchas_uninstall.txt	(revision 3272)
@@ -0,0 +1,1 @@
+#
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/uninstall/skins.xml
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/uninstall/skins.xml	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/profiles/uninstall/skins.xml	(revision 3272)
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<object name="portal_skins">
+
+  <skin-path name="*">
+    <layer name="captchas_discussion" remove="True" />
+    <layer name="captchas_sendto_form" remove="True" />
+    <layer name="captchas_contact_info" remove="True" />
+  </skin-path>
+
+</object>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/setuphandlers.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/setuphandlers.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/setuphandlers.py	(revision 3272)
@@ -0,0 +1,40 @@
+import logging
+from zope.component import getSiteManager
+
+logger = logging.getLogger('quintagroup.plonecaptchas')
+
+
+def registerDiscussionLayer(context):
+    """ Register browser layer for extending discussion
+        with quintagroup captcha 
+    """
+    from quintagroup.plonecaptchas.interfaces import IQGDiscussionCaptchas
+    from plone.browserlayer.utils import register_layer
+    name = "QGCaptchaDiscussionLayer"
+    site = getSiteManager(context)
+    register_layer(IQGDiscussionCaptchas, name, site_manager=site)
+
+
+def removeBrowserLayers(site):
+    """ Remove browser layers.
+    """
+    from plone.browserlayer.utils import unregister_layer
+    from plone.browserlayer.interfaces import ILocalBrowserLayerType
+
+    layers = ["quintagroup.plonecaptchas", "QGCapchaDiscussionLayer"]
+    site = getSiteManager(site)
+    registeredLayers = [r.name for r in site.registeredUtilities()
+                        if r.provided == ILocalBrowserLayerType]
+    for name in layers:
+        if name in registeredLayers:
+            unregister_layer(name, site_manager=site)
+            logger.log(logging.INFO, "Unregistered \"%s\" browser layer." % name)
+
+
+def uninstall(context):
+    """ Do customized uninstallation.
+    """
+    if context.readDataFile('qgplonecaptchas_uninstall.txt') is None:
+        return
+    site = context.getSite()
+    removeBrowserLayers(site)
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_contact_info/contact-info.cpt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_contact_info/contact-info.cpt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_contact_info/contact-info.cpt	(revision 3272)
@@ -0,0 +1,188 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+      lang="en"
+      metal:use-macro="context/main_template/macros/master"
+      i18n:domain="plone">
+
+<head>
+    <metal:noborder fill-slot="top_slot"
+                 tal:define="dummy python:request.set('disable_border',1)" />
+</head>
+
+<body>
+<metal:main fill-slot="main"
+     tal:define="portal context/@@plone_portal_state/portal;
+                 email_from_address portal/email_from_address;
+                 isAnon context/@@plone_portal_state/anonymous;">
+
+    <h1 class="documentFirstHeading"
+        i18n:translate="heading_contact_form">Contact form</h1>
+
+    <div class="documentDescription" 
+         tal:condition="portal/description"
+         tal:content="portal/description">Site Description</div>
+
+    <div tal:condition="not:email_from_address">
+
+        <div i18n:translate="text_no_email_setup">
+            This site doesn't have a valid email setup, so you cannot use
+            any contact forms. 
+        </div>
+
+    </div>
+
+    <div tal:condition="email_from_address">
+
+        <div i18n:translate="description_contact_site_owner">
+            Fill in this form to contact the site owners.
+        </div>
+        <div id="content-core">
+            <form name="feedback_form"
+                class="enableAutoFocus"
+                action=""
+                method="post"
+                tal:define="errors options/state/getErrors"
+                tal:attributes="action string:$portal_url/${template/getId}">
+
+            <tal:defs define="member context/@@plone_portal_state/member;
+                                  fullname python:request.get('fullname', not isAnon and member.getProperty('fullname', '') or None);
+                                  email python:request.get('email', not isAnon and member.getProperty('email','') or None);">
+
+                <input type="hidden" name="sender_fullname" value="fullname"
+                       tal:attributes="value fullname"
+                       tal:condition="fullname" />
+
+                <div class="field"
+                     tal:condition="not:fullname"
+                     tal:define="error errors/sender_fullname|nothing"
+                     tal:attributes="class python:test(error, 'field error', 'field')">
+                    <label for="sender_fullname" i18n:translate="label_sender_fullname">
+                        Name
+                    </label>
+
+                    <div class="formHelp" i18n:translate="help_sender_fullname">
+                        Please enter your full name
+                    </div>
+                    <div tal:condition="error"
+                       tal:content="error">Validation error output</div>
+                    <input type="text" 
+                           id="sender_fullname" 
+                           name="sender_fullname" 
+                           size="40" 
+                           value="" 
+                           tal:attributes="value request/sender_fullname|nothing"
+                           />
+                </div>
+
+                <input type="hidden" name="sender_from_address" value="email"
+                       tal:attributes="value email"
+                       tal:condition="email" />
+
+                <div class="field"
+                     tal:condition="not:email"
+                     tal:define="error errors/sender_from_address|nothing"
+                     tal:attributes="class python:test(error, 'field error', 'field')">
+
+                    <label for="sender_from_address" i18n:translate="label_sender_from_address">
+                        E-Mail
+                    </label>
+
+                    <span class="fieldRequired" title="Required"
+                          i18n:attributes="title title_required;"
+                          i18n:translate="label_required">(Required)</span>
+
+                    <div class="formHelp" i18n:translate="help_sender_from_address">
+                        Please enter your e-mail address
+                    </div>
+
+                    <div tal:condition="error"
+                         tal:content="error">Validation error output</div>
+
+                    <input type="text" 
+                           id="sender_from_address" 
+                           name="sender_from_address" 
+                           size="40" 
+                           value="" 
+                           tal:attributes="value request/sender_from_address|nothing"
+                           />
+                </div>
+
+                <div class="field"
+                     tal:define="error errors/subject|nothing"
+                     tal:attributes="class python:test(error, 'field error', 'field')">
+
+                    <label for="subject" i18n:translate="label_subject">
+                        Subject
+                    </label>
+
+                    <span class="fieldRequired" title="Required"
+                          i18n:attributes="title title_required;"
+                          i18n:translate="label_required">(Required)</span>
+
+                    <div class="formHelp">
+                    </div>
+
+                    <div tal:condition="error"
+                         tal:content="error">Validation error output</div>
+
+                    <input type="text" 
+                           id="subject" 
+                           name="subject" 
+                           size="40" 
+                           value="" 
+                           tal:attributes="value request/subject|nothing"
+                           />
+                </div>
+
+                <div class="field"
+                     tal:define="error errors/message|nothing"
+                     tal:attributes="class python:test(error, 'field error', 'field')">
+
+                    <label for="message" i18n:translate="label_message">
+                        Message
+                    </label>
+
+                    <span class="fieldRequired" title="Required"
+                            i18n:attributes="title title_required;"
+                            i18n:translate="label_required">(Required)</span>
+
+                    <div class="formHelp" i18n:translate="help_message">
+                        Please enter the message you want to send.
+                    </div>
+
+                    <div tal:condition="error"
+                         tal:content="error">Validation error output</div>
+
+                    <textarea name="message"
+                              id="message"
+                              rows="5"
+                              cols="25"
+                              tal:content="request/message|nothing"
+                              > </textarea>
+                </div>
+
+                <div metal:use-macro="here/captcha_widget/macros/captcha"></div>
+
+                <div class="formControls">
+
+                    <input class="context"
+                           type="submit"
+                           name="form.button.Send"
+                           value="Send"
+                           i18n:attributes="value label_send;"
+                           />
+
+                </div>
+
+            </tal:defs>
+
+            <input type="hidden" name="form.submitted" value="1" />
+
+            </form>
+        </div>
+    </div>
+</metal:main>
+</body>
+</html>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_contact_info/contact-info.cpt.metadata
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_contact_info/contact-info.cpt.metadata	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_contact_info/contact-info.cpt.metadata	(revision 3272)
@@ -0,0 +1,12 @@
+[default]
+title= Contact information
+
+[security]
+View=0:Anonymous
+
+[validators]
+validators=validate_site_feedback, captcha_validator
+
+[actions]
+action.success=traverse_to:string:send_feedback_site
+action.failure=traverse_to:string:contact-info
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_discussion/discussion_reply.cpy
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_discussion/discussion_reply.cpy	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_discussion/discussion_reply.cpy	(revision 3272)
@@ -0,0 +1,68 @@
+## Script (Python) "discussion_reply"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind state=state
+##bind subpath=traverse_subpath
+##parameters=subject,body_text,text_format='plain',username=None,password=None
+##title=Reply to content
+
+from Products.PythonScripts.standard import url_quote_plus
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone import PloneMessageFactory as _
+mtool = getToolByName(context, 'portal_membership')
+dtool = getToolByName(context, 'portal_discussion')
+req = context.REQUEST
+
+if username or password:
+    # The user username/password inputs on on the comment form were used,
+    # which might happen when anonymous commenting is enabled. If they typed
+    # something in to either of the inputs, we send them to 'logged_in'.
+    # 'logged_in' will redirect them back to this script if authentication
+    # succeeds with a query string which will post the message appropriately
+    # and show them the result.  if 'logged_in' fails, the user will be
+    # presented with the stock login failure page.  This all depends
+    # heavily on cookiecrumbler, but I believe that is a Plone requirement.
+    came_from = '%s?subject=%s&amp;body_text=%s' % (req['URL'], subject, body_text)
+    came_from = url_quote_plus(came_from)
+    portal_url = context.portal_url()
+
+    return req.RESPONSE.redirect(
+        '%s/logged_in?__ac_name=%s'
+        '&amp;__ac_password=%s'
+        '&amp;came_from=%s' % (portal_url,
+                               url_quote_plus(username),
+                               url_quote_plus(password),
+                               came_from,
+                               )
+        )
+
+# if (the user is already logged in) or (if anonymous commenting is enabled and
+# they posted without typing a username or password into the form), we do
+# the following
+
+creator = mtool.getAuthenticatedMember().getId()
+tb = dtool.getDiscussionFor(context)
+id = tb.createReply(title=subject, text=body_text, Creator=creator)
+reply = tb.getReply(id)
+
+# TODO THIS NEEDS TO GO AWAY!
+if hasattr(dtool.aq_explicit, 'cookReply'):
+    dtool.cookReply(reply, text_format='plain')
+
+parent = tb.aq_parent
+
+# return to the discussable object.
+redirect_target = context.plone_utils.getDiscussionThread(tb)[0]
+view = redirect_target.getTypeInfo().getActionInfo('object/view',
+                                                   redirect_target)['url']
+anchor = reply.getId()
+
+from Products.CMFPlone.utils import transaction_note
+transaction_note('Added comment to %s at %s' % (parent.title_or_id(),
+                                                reply.absolute_url()))
+
+context.plone_utils.addPortalMessage(_(u'Comment added.'))
+target = '%s#%s' % (view, anchor)
+return req.RESPONSE.redirect(target)
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_discussion/discussion_reply.cpy.metadata
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_discussion/discussion_reply.cpy.metadata	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_discussion/discussion_reply.cpy.metadata	(revision 3272)
@@ -0,0 +1,5 @@
+[validators]
+validators=captcha_validator
+
+[actions]
+action.failure=traverse_to:string:discussion_reply_form
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_discussion/discussion_reply_form.cpt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_discussion/discussion_reply_form.cpt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_discussion/discussion_reply_form.cpt	(revision 3272)
@@ -0,0 +1,191 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+      metal:use-macro="context/main_template/macros/master"
+      i18n:domain="plone">
+<body>
+
+<metal:block fill-slot="top_slot"
+             tal:define="dummy python:request.set('disable_border',1)" />
+
+<metal:main fill-slot="main"
+     tal:define="portal context/@@plone_portal_state/portal;
+                 errors options/state/getErrors;
+                 thread python:context.plone_utils.getDiscussionThread(here);
+                 discussable python:thread[0];
+                 replies python:thread[1:];
+                 toLocalizedTime nocall: context/@@plone/toLocalizedTime;">
+
+    <div id="content-core">
+        <tal:discussable define="object_title context/Title;
+                                 object_description context/Description;
+                                 view_template python:getattr(here, discussable.getLayout(), None);
+                                 view_macros view_template/macros | nothing;
+                                 view_macro view_macros/content-core | nothing;
+                                 showDiscussable python:object_title or object_description or view_macro;">
+             <div class="comment" 
+                  tal:condition="showDiscussable"
+                  tal:define="here python:discussable;
+                              context python:discussable">
+                 <h1 class="documentDescription" tal:content="object_title">Title</h1>
+                 <div class="documentDescription" tal:content="object_description">
+                     Description
+                 </div>
+                 <tal:block tal:condition="view_macro">
+                     <div metal:use-macro="view_macro"/>
+                 </tal:block>
+             </div>
+        </tal:discussable>
+
+        <tal:thread tal:repeat="reply replies">
+            <tal:block tal:define="level repeat/reply/index">
+                <div class="comment"
+                     tal:attributes="style python:'margin-left:'+str((int(level)+1)*2)+'em'">
+                   <h3>
+                       <tal:block replace="structure portal/discussionitem_icon.png"/>
+                       <span tal:replace="reply/pretty_title_or_id">Comment title</span>
+                   </h3>
+                   <div class="documentByLine"
+                        tal:define="anonymous_creator python:reply.Creator()=='Anonymous User'">
+                       <span i18n:translate="label_comment_by">Posted by</span>
+                       <span tal:content="reply/Creator"
+                             tal:condition="not:anonymous_creator">Poster Name</span>
+                       <span i18n:translate="label_anonymous_user"
+                             tal:condition="anonymous_creator">Anonymous User</span>
+                       <span i18n:translate="label_commented_at">at</span> 
+                       <span tal:replace="python:toLocalizedTime(reply.ModificationDate(),
+                                          long_format=1)">8/23/2001 12:40:44 PM</span>
+                   </div>
+                   <div class="commentBody"
+                        tal:content="structure reply/CookedBody">
+                        This is the body text of the comment.
+                   </div>
+               </div>    
+                 
+            </tal:block>
+        </tal:thread>
+
+        <form name="edit_form"
+              class="enableAutoFocus"
+              method="post"
+              action="talkback"
+              tal:attributes="action string:${context/@@plone_context_state/object_url}/${template/getId}"
+              tal:define="isAnon context/@@plone_portal_state/anonymous;">
+
+            <fieldset>
+
+                <legend i18n:translate="legend_add_comment">Add comment</legend>
+                <p i18n:translate="description_add_comment">
+                    You can add a comment by filling out the form below. Plain text
+                    formatting.
+                </p>
+
+                <input type="hidden" name="Creator" value=""
+                       tal:define="member context/@@plone_portal_state/member;"
+                       tal:attributes="value member/getUserName" />
+
+                <div tal:condition="isAnon">
+                    <dl class="portalMessage warning">
+                        <dt i18n:translate="">
+                            Info
+                        </dt>
+                        <dd i18n:translate="legend_note_reply_anonymous">
+                            You are not logged in. You may optionally enter your
+                            username and password below. If you don't enter anything,
+                            this comment will be posted as 'Anonymous User'.
+                        </dd>
+                    </dl>
+
+                    <div class="field">
+
+                        <label for="username" i18n:translate="label_name">Name</label>
+
+                        <input name="username"
+                               id="username"
+                               value="" alt="Username" title="Name"
+                               size="40"
+                               i18n:attributes="title label_name; alt label_username;" />
+
+                    </div>
+
+                    <div class="field">
+
+                        <label for="password" i18n:translate="label_password">Password</label>
+
+                        <input type="password"
+                               id="password"
+                               name="password"
+                               value="" alt="Password" title="Password"
+                               size="40"
+                               i18n:attributes="title label_password; alt label_password;" />
+
+                    </div>
+
+                </div>
+
+
+            <div class="field"
+            tal:define="error errors/subject|nothing;"
+            tal:attributes="class python:test(error, 'field error', 'field')">
+
+                    <label for="subject" i18n:translate="label_subject">Subject</label>
+
+            <span class="fieldRequired" title="Required"
+                  i18n:attributes="title title_required;"
+                  i18n:translate="label_required">(Required)</span>
+
+                <div tal:content="error">Validation error output</div>
+
+                    <input name="subject"
+                           id="subject"
+                           value=""
+                           size="40"
+                           tal:attributes="value request/subject|request/title_override|nothing;" />
+
+                </div>
+
+            <div class="field"
+            tal:define="error errors/body_text|nothing;"
+            tal:attributes="class python:test(error, 'field error', 'field')">
+
+              <label for="body_text" i18n:translate="label_comment">Comment</label>
+
+            <span class="fieldRequired" title="Required"
+                  i18n:attributes="title title_required;"
+                  i18n:translate="label_required">(Required)</span>
+
+                <div tal:content="error">Validation error output</div>
+
+                    <textarea name="body_text"
+                              id="body_text"
+                              cols="40"
+                              rows="8"
+                              tal:content="request/body_text|request/text_override | nothing"
+                            ></textarea>
+
+                </div>
+
+                <div metal:use-macro="here/captcha_widget/macros/captcha"></div>
+
+                <div class="formControls">
+
+                    <input class="context"
+                           type="submit"
+                           value="Save"
+                           name="form.button.Save"
+                           i18n:attributes="value label_save;"
+                           tal:attributes="name string:discussion_reply:method;" />
+
+                </div>
+              <input type="hidden" name="form.submitted" value="1" />
+
+            </fieldset>
+
+        </form>
+    </div>
+
+</metal:main>
+
+</body>
+</html>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_discussion/discussion_reply_form.cpt.metadata
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_discussion/discussion_reply_form.cpt.metadata	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_discussion/discussion_reply_form.cpt.metadata	(revision 3272)
@@ -0,0 +1,9 @@
+[default]
+title=Reply to Comment
+
+[validators]
+validators = validate_talkback
+
+[actions]
+action.success = traverse_to:string:discussion_reply
+action.failure = traverse_to:string:discussion_reply_form
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_sendto_form/sendto.cpy
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_sendto_form/sendto.cpy	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_sendto_form/sendto.cpy	(revision 3272)
@@ -0,0 +1,57 @@
+## Controller Python Script "sendto"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind state=state
+##bind subpath=traverse_subpath
+##parameters=
+##title=Send an URL to a friend
+##
+REQUEST=context.REQUEST
+
+from Products.CMFPlone.utils import transaction_note
+from Products.CMFPlone.PloneTool import AllowSendto
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone import PloneMessageFactory as _
+from ZODB.POSException import ConflictError
+
+plone_utils = getToolByName(context, 'plone_utils')
+mtool = getToolByName(context, 'portal_membership')
+site_properties = getToolByName(context, 'portal_properties').site_properties
+pretty_title_or_id = plone_utils.pretty_title_or_id
+
+if not mtool.checkPermission(AllowSendto, context):
+    context.plone_utils.addPortalMessage(_(u'You are not allowed to send this link.'), 'error')
+    return state.set(status='failure')
+
+# Find the view action.
+context_state = context.restrictedTraverse("@@plone_context_state")
+url = context_state.view_url()
+
+variables = {'send_from_address' : REQUEST.send_from_address,
+             'send_to_address'   : REQUEST.send_to_address,
+             'subject'           : pretty_title_or_id(context),
+             'url'               : url,
+             'title'             : pretty_title_or_id(context),
+             'description'       : context.Description(),
+             'comment'           : REQUEST.get('comment', None),
+             'envelope_from'     : site_properties.email_from_address
+             }
+
+try:
+    plone_utils.sendto( **variables )
+except ConflictError:
+    raise
+except: # TODO To many things could possibly go wrong. So we catch all.
+    exception = context.plone_utils.exceptionString()
+    message = _(u'Unable to send mail: ${exception}',
+                mapping={u'exception' : exception})
+    context.plone_utils.addPortalMessage(message, 'error')
+    return state.set(status='failure')
+
+tmsg='Sent page %s to %s' % (url, REQUEST.send_to_address)
+transaction_note(tmsg)
+
+context.plone_utils.addPortalMessage(_(u'Mail sent.'))
+return state
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_sendto_form/sendto.cpy.metadata
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_sendto_form/sendto.cpy.metadata	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_sendto_form/sendto.cpy.metadata	(revision 3272)
@@ -0,0 +1,6 @@
+[validators]
+validators=captcha_validator,validate_sendto
+
+[actions]
+action.success = redirect_to_action:string:view
+action.failure = redirect_to_action:string:view
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_sendto_form/sendto_form.cpt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_sendto_form/sendto_form.cpt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_sendto_form/sendto_form.cpt	(revision 3272)
@@ -0,0 +1,128 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+      metal:use-macro="context/main_template/macros/master"
+      i18n:domain="plone">
+
+<metal:block fill-slot="top_slot"
+             tal:define="dummy python:request.set('disable_border',1)" />
+
+  <metal:head fill-slot="head_slot">
+    <meta name="robots" content="noindex,follow" />
+  </metal:head>
+
+  <body>
+
+    <metal:main fill-slot="main"
+         tal:define="errors options/state/getErrors;">
+
+      <h1 class="documentFirstHeading"
+          i18n:translate="heading_send_page_to">Send this page to someone</h1>
+
+      <div class="documentDescription"
+         i18n:translate="description_send_page_url_to">
+        Fill in the email address of your friend, and we will send an email
+        that contains a link to this page.
+      </div>
+
+      <div id="content-core">
+          <form name="sendto_form"
+                class="enableAutoFocus"
+                action="sendto_form"
+                method="post"
+                enctype="multipart/form-data"
+                tal:attributes="action string:${context/@@plone_context_state/object_url}/${template/getId}">
+
+            <fieldset>
+
+              <legend i18n:translate="legend_address_info">Address info</legend>
+
+              <div class="field"
+                   tal:define="error errors/send_to_address|nothing;"
+                   tal:attributes="class python:test(error, 'field error', 'field')">
+
+                <label for="send_to_address" i18n:translate="label_send_to_mail">Send to</label>
+
+                <span class="fieldRequired" title="Required"
+                      i18n:attributes="title title_required;"
+                      i18n:translate="label_required">(Required)</span>
+
+                      <div class="formHelp" i18n:translate="help_send_to_mail">
+                        The e-mail address to send this link to.
+                      </div>
+
+                      <div tal:content="error">Validation error output</div>
+
+                      <input type="text"
+                             id="send_to_address"
+                             name="send_to_address"
+                             size="25"
+                             tal:attributes="value request/send_to_address | nothing;"
+                             />
+              </div>
+
+              <div class="field"
+                   tal:define="error errors/send_from_address|nothing;"
+                   tal:attributes="class python:test(error, 'field error', 'field')">
+
+                <label for="send_from_address" i18n:translate="label_send_from">From</label>
+
+                <span class="fieldRequired" title="Required"
+                      i18n:attributes="title title_required;"
+                      i18n:translate="label_required">(Required)</span>
+
+                      <div class="formHelp" i18n:translate="help_send_from">
+                        Your email address.
+                      </div>
+
+                      <div tal:content="error">Validation error output</div>
+
+                      <input type="text"
+                             id="send_from_address"
+                             name="send_from_address"
+                             size="25"
+                             tal:define="member context/@@plone_portal_state/member;"
+                             tal:attributes="value python: request.get('send_from_address', member.getProperty('email',''));"
+                             />
+              </div>
+
+              <div class="field">
+                <label for="comment" i18n:translate="label_comment">Comment</label>
+
+                <div class="formHelp" i18n:translate="help_comment_to_link">
+                  A comment about this link.
+                </div>
+
+                <textarea cols="80"
+                          rows="5"
+                          id="comment"
+                          name="comment"
+                          tal:content="request/comment | nothing"
+                          >
+                  Comment
+                </textarea>
+              </div>
+
+              <div metal:use-macro="here/captcha_widget/macros/captcha"></div>
+
+              <div class="formControls">
+                <input class="context"
+                       type="submit"
+                       name="form.button.Send"
+                       value="Send"
+                       i18n:attributes="value label_send;"
+                       />
+              </div>
+
+              <input type="hidden" name="form.submitted" value="1" />
+
+            </fieldset>
+
+          </form>
+      </div>
+      
+    </metal:main>
+
+  </body>
+</html>
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_sendto_form/sendto_form.cpt.metadata
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_sendto_form/sendto_form.cpt.metadata	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/skins/captchas_sendto_form/sendto_form.cpt.metadata	(revision 3272)
@@ -0,0 +1,9 @@
+[default]
+title=Send this page to somebody
+
+[validators]
+validators=validate_sendto, captcha_validator
+
+[actions]
+action.success=traverse_to:string:sendto
+action.failure=traverse_to:string:sendto_form
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/tests/base.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/tests/base.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/tests/base.py	(revision 3272)
@@ -0,0 +1,85 @@
+import transaction
+
+from Products.Five import zcml
+from Products.Five import fiveconfigure
+
+from Testing import ZopeTestCase as ztc
+
+from Products.PloneTestCase import setup as ptc_setup
+from Products.PloneTestCase import PloneTestCase as ptc
+from Products.PloneTestCase.layer import PloneSite
+
+from quintagroup.plonecaptchas.config import PRODUCT_NAME
+
+# TESTING CONSTANTS
+CAPTCHA_KEY = 'captcha_key'
+CAPTCHAS_COUNT = 165
+LAYERS = ['captchas_discussion', 'captchas_sendto_form',
+          'captchas_contact_info']
+
+TOOL_ICON = 'skins/plone_captchas/tool.gif'
+TOOL_ID = 'portal_captchas'
+CONFIGLET_ID = "qpc_tool"
+PROPERTY_SHEET = 'qPloneCaptchas'
+
+# register_form profile
+RF_PROFILE = 'profile-quintagroup.plonecaptchas:register_form'
+
+ptc.setupPloneSite()
+
+
+class NotInstalled(PloneSite):
+    """ Only package register, without installation into portal
+    """
+
+    @classmethod
+    def setUp(cls):
+        fiveconfigure.debug_mode = True
+        import quintagroup.captcha.core
+        import quintagroup.plonecaptchas
+        zcml.load_config('configure.zcml', quintagroup.captcha.core)
+        zcml.load_config('configure.zcml', quintagroup.plonecaptchas)
+        fiveconfigure.debug_mode = False
+        ztc.installPackage('quintagroup.captcha.core')
+        ztc.installPackage('quintagroup.plonecaptchas')
+
+
+class Installed(NotInstalled):
+    """ Install product into the portal
+    """
+    @classmethod
+    def setUp(cls):
+        app = ztc.app()
+        portal = app[ptc_setup.portal_name]
+
+        # Sets the local site/manager
+        ptc_setup._placefulSetUp(portal)
+        # Install PROJECT
+        qi = getattr(portal, 'portal_quickinstaller', None)
+        qi.installProduct(PRODUCT_NAME)
+
+        # Run register_form profile
+        gs = getattr(portal, 'portal_setup', None)
+        gs.runAllImportStepsFromProfile(RF_PROFILE)
+
+        transaction.commit()
+
+    @classmethod
+    def tearDown(cls):
+        ptc_setup._placefulTearDown()
+
+
+class TestCase(ptc.PloneTestCase):
+    layer = Installed
+
+
+class TestCaseNotInstalled(ptc.PloneTestCase):
+    layer = NotInstalled
+
+
+class FunctionalTestCase(ptc.FunctionalTestCase):
+    layer = Installed
+
+
+class FunctionalTestCaseNotInstalled(ptc.FunctionalTestCase):
+    layer = NotInstalled
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/tests/discussion.txt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/tests/discussion.txt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/tests/discussion.txt	(revision 3272)
@@ -0,0 +1,122 @@
+Introduction
+============
+
+This is a full-blown functional test. The emphasis here is on testing what
+the user may input and see, and the system is largely tested as a black box.
+We use PloneTestCase to set up this test as well, so we have a full Plone site
+to play with. We *can* inspect the state of the portal, e.g. using
+self.portal and self.folder, but it is often frowned upon since you are not
+treating the system as a black box. Also, if you, for example, log in or set
+roles using calls like self.setRoles(), these are not reflected in the test
+browser, which runs as a separate session.
+
+Being a doctest, we can tell a story here.
+
+First, we must perform some setup. We use the testbrowser that is shipped
+with Five, as this provides proper Zope 2 integration. Most of the
+documentation, though, is in the underlying zope.testbrower package.
+
+    >>> from Products.Five.testbrowser import Browser
+    >>> browser = Browser()
+    >>> portal_url = self.portal.absolute_url()
+
+The following is useful when writing and debugging testbrowser tests. It lets
+us see all error messages in the error_log.
+
+    >>> self.portal.error_log._ignored_exceptions = ()
+
+With that in place, we can go to the portal front page and log in. We will
+do this using the default user from PloneTestCase:
+
+    >>> from Products.PloneTestCase.setup import portal_owner, default_password
+    >>> browser.open(portal_url)
+
+We have the login portlet, so let's use that.
+
+    >>> browser.open('http://nohost/plone/login_form')
+    >>> browser.getControl('Login Name').value = portal_owner
+    >>> browser.getControl('Password').value = default_password
+    >>> browser.getControl('Log in').click()
+    >>> "You are now logged in" in browser.contents
+    True
+    >>> "Login failed" in browser.contents
+    False
+    >>> browser.url
+    'http://nohost/plone/login_form'
+
+
+
+-*- extra stuff goes here -*-
+Test 
+===============================
+
+Enable commenting.
+
+    >>> from zope.component import queryUtility
+    >>> from plone.registry.interfaces import IRegistry
+    >>> from plone.app.discussion.interfaces import IDiscussionSettings
+    >>> registry = queryUtility(IRegistry)
+    >>> settings = registry.forInterface(IDiscussionSettings, check=False)
+    >>> settings.globally_enabled = True
+    >>> settings.anonymous_comments = True
+
+Enable quintagroup.plonecaptchas
+
+    >>> from quintagroup.plonecaptchas.config import CAPTCHA_NAME 
+    >>> settings.captcha = CAPTCHA_NAME
+
+    >>> import transaction
+    >>> transaction.commit()
+
+Create a public page with comments allowed.
+
+    >>> browser.open(portal_url)
+    >>> browser.getLink(id='document').click()
+    >>> browser.getControl(name='title').value = "index_html"
+    >>> browser.getControl(name='allowDiscussion:boolean').value = True
+    >>> browser.getControl(name='form.button.save').click()
+    >>> browser.open('http://nohost/plone/index_html/content_status_modify?workflow_action=publish')
+    >>> page_url = browser.url
+
+Check that the form has been properly submitted
+
+    >>> browser.url
+    'http://nohost/plone/index_html'
+
+Test captcha image in comment form
+
+    >>> import re
+    >>> from quintagroup.captcha.core.tests.testWidget import IMAGE_PATT
+    >>> patt = re.compile(IMAGE_PATT % self.portal.absolute_url())
+
+    >>> browser.open(portal_url + '/logout')
+    >>> browser.open(page_url)
+    >>> match_obj = patt.search(browser.contents)
+    >>> img_url = portal_url + match_obj.group(1)
+    >>> browser.open(img_url)
+    >>> browser.headers.getheader("content-type")
+    'image/jpeg'
+
+Test submit right captcha
+
+    >>> from quintagroup.captcha.core.utils import getWord, decrypt, parseKey
+    >>> self.captcha_key = self.portal.captcha_key
+    >>> browser.open(page_url)
+    >>> captcha_path = patt.search(browser.contents).group(1)
+    >>> captcha_hashkey = captcha_path.lstrip('/getCaptchaImage/')
+    >>> key = getWord(int(parseKey(decrypt(self.captcha_key, captcha_hashkey))['key']) - 1)
+
+    >>> browser.getControl(name='form.widgets.text').value = 'comment'
+    >>> browser.getControl(name='form.widgets.captcha').value = key
+    >>> browser.getControl(name='form.buttons.comment').click()
+    >>> "Please re-enter validation code" in browser.contents
+    False
+
+Test submit wrong captcha
+
+    >>> browser.open(page_url)
+    >>> browser.getControl(name='form.widgets.text').value = 'comment'
+    >>> browser.getControl(name='form.widgets.captcha').value = 'wrong captcha'
+    >>> browser.getControl(name='form.buttons.comment').click()
+    >>> "Please re-enter validation code" in browser.contents
+    True
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/tests/testForms.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/tests/testForms.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/tests/testForms.py	(revision 3272)
@@ -0,0 +1,234 @@
+import unittest
+import doctest
+import re
+from urllib import urlencode
+from StringIO import StringIO
+
+from Products.PloneTestCase.PloneTestCase import portal_owner
+from Products.PloneTestCase.PloneTestCase import default_password
+
+from quintagroup.plonecaptchas.tests.base import FunctionalTestCase, ztc
+from quintagroup.plonecaptchas.config import PRODUCT_NAME, HAS_APP_DISCUSSION
+
+from quintagroup.captcha.core.tests.testWidget import IMAGE_PATT, NOT_VALID
+from quintagroup.captcha.core.tests.testWidget import addTestLayer
+from quintagroup.captcha.core.tests.base import testPatch
+from quintagroup.captcha.core.utils import getWord, decrypt, parseKey
+
+from plone.app.controlpanel.security import ISecuritySchema
+
+# BBB for plone v<3.1, where plone.protect not used yet
+PROTECT_SUPPORT = True
+try:
+    from plone import protect
+    # pyflakes fix (pyflakes message: 'protect' imported but unused)
+    protect
+except ImportError:
+    PROTECT_SUPPORT = False
+
+# USE PATCH FROM quintagroup.captcha.core
+# patch to use test images and dictionary
+testPatch()
+
+
+class TestFormMixin(FunctionalTestCase):
+
+    formkey_key = "key"
+    formkey_hashkey = "hashkey"
+
+    def afterSetUp(self):
+        self.loginAsPortalOwner()
+        self.addProduct(PRODUCT_NAME)
+        # Add test_captcha layer from quintagroup.captcah.core
+        addTestLayer(self)
+        # Prepare form data
+        self.basic_auth = ':'.join((portal_owner, default_password))
+        self.form_url = ''
+        self.form_method = "POST"
+        self.hasAuthenticator = False
+        self.form_data = self.getFormData()
+        # Prepare captcha related test data
+        self.captcha_key = self.portal.captcha_key
+        self.hashkey = self.portal.getCaptcha()
+        self.form_data[self.formkey_hashkey] = self.hashkey
+        self.form_data[self.formkey_key] = ''
+
+    def getFormData(self):
+        raise NotImplementedError(
+            "getFormData not implemented")
+
+    def publishForm(self):
+        stdin_data = None
+        form_url = self.portal.absolute_url(1) + self.form_url
+        # Prepare form data
+        if PROTECT_SUPPORT and self.hasAuthenticator:
+            self.form_data['_authenticator'] = self._getauth()
+        form_data = urlencode(self.form_data)
+        if self.form_method.upper() == 'GET':
+            form_url += "?%s" % form_data
+        else:
+            stdin_data = StringIO(form_data)
+        # Publish form and get response
+        response = self.publish(form_url, self.basic_auth,
+            request_method=self.form_method, stdin=stdin_data)
+        return response
+
+    def _getauth(self):
+        # Fix authenticator for the form
+        authenticator = self.portal.restrictedTraverse("@@authenticator")
+        html = authenticator.authenticator()
+        handle = re.search('value="(.*)"', html).groups()[0]
+        return handle
+
+    def elog(self, name="", response=""):
+        open("/tmp/test.%s.html" % name, "w").write(response)
+        logs = self.portal.error_log.getLogEntries()
+        if len(logs) > 0:
+            i = 0
+            while logs:
+                l = logs.pop()
+                i += 1
+                open("/tmp/test.%s.error.%d.html" % (l["id"], i),
+                                                     "w").write(l["tb_html"])
+
+    def testImage(self):
+        self.form_data = {}
+        self.form_method = "GET"
+        response = self.publishForm().getBody()
+        patt = re.compile(IMAGE_PATT % self.portal.absolute_url())
+        match_obj = patt.search(response)
+        self.elog("image", response)
+        img_url = match_obj.group(1)
+
+        content_type = self.publish(
+                            '/plone' + img_url).getHeader('content-type')
+        self.assertTrue(content_type.startswith('image'),
+            "Wrong captcha image content type")
+
+    def testSubmitRightCaptcha(self):
+        key = getWord(int(parseKey(decrypt(self.captcha_key,
+                                           self.hashkey))['key']) - 1)
+        self.form_data[self.formkey_key] = key
+
+        response = self.publishForm().getBody()
+        self.elog("right", response)
+        self.assertFalse(NOT_VALID.search(response))
+
+    def testSubmitWrongCaptcha(self):
+        self.form_data[self.formkey_key] = 'wrong word'
+        response = self.publishForm().getBody()
+        self.elog("wrong", response)
+        self.assertTrue(NOT_VALID.search(response))
+
+    def testSubmitRightCaptchaTwice(self):
+        key = getWord(int(parseKey(decrypt(self.captcha_key,
+                                           self.hashkey))['key']) - 1)
+        self.form_data[self.formkey_key] = key
+
+        response1 = self.publishForm().getBody()
+        self.elog("right1", response1)
+        response2 = self.publishForm().getBody()
+        self.elog("right2", response2)
+        self.assertTrue(NOT_VALID.search(response2))
+
+
+class TestDiscussionForm(TestFormMixin):
+
+    def afterSetUp(self):
+        TestFormMixin.afterSetUp(self)
+        self.portal.invokeFactory('Document', 'index_html')
+        self.portal['index_html'].allowDiscussion(True)
+        self.form_url = '/index_html/discussion_reply_form'
+
+    def getFormData(self):
+        return {'form.submitted': '1',
+                'subject': 'testing',
+                'Creator': portal_owner,
+                'body_text': 'Text in Comment',
+                'discussion_reply:method': 'Save',
+                'form.button.form_submit': 'Save'}
+
+
+class TestRegisterForm(TestFormMixin):
+
+    formkey_key = "form.captcha"
+    formkey_hashkey = "form..hashkey"
+
+    def afterSetUp(self):
+        TestFormMixin.afterSetUp(self)
+        ISecuritySchema(self.portal).enable_self_reg = True
+        self.portal._updateProperty('validate_email', False)
+        self.hasAuthenticator = True
+        self.form_url = '/@@register'
+        self.basic_auth = ":"
+        self.logout()
+
+    def getFormData(self):
+        return {"form.fullname": "Tester",
+                "form.username": "tester",
+                "form.email": "tester@test.com",
+                "form.password": "123456",
+                "form.password_ctl": "123456",
+                'form.actions.register': 'Register'}
+
+
+class TestSendtoForm(TestFormMixin):
+
+    def afterSetUp(self):
+        TestFormMixin.afterSetUp(self)
+        self.portal.invokeFactory('Document', 'index_html')
+        self.portal['index_html'].allowDiscussion(True)
+        self.form_url = '/index_html/sendto_form'
+
+    def getFormData(self):
+        return {'form.submitted': '1',
+                "send_to_address": "recipient@test.com",
+                "send_from_address": "sender@test.com",
+                'comment': 'Text in Comment',
+                'form.button.Send': 'Save'}
+
+
+def send_patch(self, *args, **kwargs):
+    """This patch prevent breakage on sending."""
+
+
+class TestContactInfo(TestFormMixin):
+
+    def afterSetUp(self):
+        TestFormMixin.afterSetUp(self)
+        # preparation to form correct working
+        self.portal._updateProperty('email_from_address', 'manager@test.com')
+        self.logout()
+        self.form_url = '/contact-info'
+        self.orig_mh_send = self.portal.MailHost.send
+        self.portal.MailHost.send = send_patch
+
+    def beforeTearDown(self):
+        self.portal.MailHost.send = self.orig_mh_send
+
+    def getFormData(self):
+        return {'form.submitted': '1',
+                "sender_fullname": "tester",
+                "sender_from_address": "sender@test.com",
+                'subject': 'Subject',
+                'message': 'Message',
+                'form.button.Send': 'Save'}
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    if HAS_APP_DISCUSSION:
+        suite.addTest(unittest.TestSuite([
+            ztc.FunctionalDocFileSuite(
+                'discussion.txt', package='quintagroup.plonecaptchas.tests',
+                test_class=FunctionalTestCase, globs=globals(),
+                optionflags=doctest.REPORT_ONLY_FIRST_FAILURE |
+                    doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS),
+            ]))
+
+    else:
+        suite.addTest(unittest.makeSuite(TestDiscussionForm))
+    suite.addTest(unittest.makeSuite(TestRegisterForm))
+    suite.addTest(unittest.makeSuite(TestSendtoForm))
+    suite.addTest(unittest.makeSuite(TestContactInfo))
+    return suite
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/tests/testInstallation.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/tests/testInstallation.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/tests/testInstallation.py	(revision 3272)
@@ -0,0 +1,92 @@
+import unittest
+from quintagroup.plonecaptchas.tests.base import TestCase, LAYERS
+
+from Products.CMFCore.utils import getToolByName
+
+from zope.interface import alsoProvides
+from zope.component import getSiteManager
+from zope.component import queryMultiAdapter
+from plone.browserlayer.interfaces import ILocalBrowserLayerType
+
+from quintagroup.plonecaptchas.interfaces import IQGPloneCaptchas
+from quintagroup.plonecaptchas.browser.register import CaptchaRegistrationForm
+from quintagroup.plonecaptchas.config import PRODUCT_NAME
+
+
+class TestInstallation(TestCase):
+
+    def afterSetUp(self):
+        self.loginAsPortalOwner()
+        self.skins = getToolByName(self.portal, 'portal_skins', None)
+
+    def testSkinInstall(self):
+        for skin in self.skins.getSkinSelections():
+            path = self.skins.getSkinPath(skin)
+            path = map(str.strip, path.split(','))
+            for layer in LAYERS:
+                self.assert_(layer.split('/')[0] in self.skins.objectIds(),
+                             '%s directory view not found in portal_skins '\
+                             'after installation' % layer)
+                self.assert_(layer in path,
+                             '%s layer not found in %s' % (PRODUCT_NAME, skin))
+
+    def testBrowserLayerRegistration(self):
+        # Test if IQGPloneCaptchas browser layer registered on installation
+        site = getSiteManager(self.portal)
+        registeredLayers = [r.name for r in site.registeredUtilities()
+                            if r.provided == ILocalBrowserLayerType]
+        self.assertEqual("quintagroup.plonecaptchas" in registeredLayers, True)
+
+    def testRegisterFormOverriden(self):
+        # Mark request with IQGPloneCaptchas browser layer interface
+        alsoProvides(self.portal.REQUEST, IQGPloneCaptchas)
+        register = queryMultiAdapter((self.portal, self.portal.REQUEST),
+                                     name="register")
+        self.assertEqual(isinstance(register, CaptchaRegistrationForm), True)
+
+
+class TestUninstallation(TestCase):
+
+    def afterSetUp(self):
+        self.loginAsPortalOwner()
+        self.skins = getToolByName(self.portal, 'portal_skins', None)
+        self.qi = getToolByName(self.portal, 'portal_quickinstaller', None)
+        self.qi.uninstallProducts([PRODUCT_NAME])
+
+    def testProductUninstalled(self):
+        self.assertNotEqual(self.qi.isProductInstalled(PRODUCT_NAME), True)
+
+    def testSkinUninstall(self):
+        for skin in self.skins.getSkinSelections():
+            path = self.skins.getSkinPath(skin)
+            path = map(str.strip, path.split(','))
+            for layer in LAYERS:
+                self.assertTrue(
+                        not layer.split('/')[0] in self.skins.objectIds(),
+                        '%s directory view found in portal_skins '\
+                        'after uninstallation' % layer)
+                self.assert_(not layer in path,
+                    '%s layer found in %s skin after uninstallation' % (layer,
+                                                                        skin))
+
+    def testBrowserLayerUnregistration(self):
+        # Test if IQGPloneCaptchas browser layer registered on installation
+        site = getSiteManager(self.portal)
+        registeredLayers = [r.name for r in site.registeredUtilities()
+                            if r.provided == ILocalBrowserLayerType]
+        self.assertNotEqual("quintagroup.plonecaptchas" in registeredLayers,
+                            True)
+
+    def testRegisterFormOverriden(self):
+        # Mark request with IQGPloneCaptchas browser layer interface
+        register = queryMultiAdapter((self.portal, self.portal.REQUEST),
+                                     name="register")
+        self.assertNotEqual(isinstance(register, CaptchaRegistrationForm),
+                            True)
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TestInstallation))
+    suite.addTest(unittest.makeSuite(TestUninstallation))
+    return suite
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/validate.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/validate.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/validate.py	(revision 3272)
@@ -0,0 +1,29 @@
+from plone.app.discussion.interfaces import IDiscussionSettings
+from plone.app.discussion.interfaces import ICaptcha
+from quintagroup.z3cform.captcha.validator import CaptchaValidator
+from quintagroup.z3cform.captcha.widget import CaptchaWidget
+from quintagroup.plonecaptchas.config import CAPTCHA_NAME 
+from quintagroup.plonecaptchas.interfaces import IQGDiscussionCaptchas
+
+from zope.component import queryUtility
+from zope.component import adapts
+from zope.interface import Interface
+from zope.schema.interfaces import IField
+from plone.registry.interfaces import IRegistry
+from z3c.form import validator
+
+
+class QGDiscussionCaptchaValidator(CaptchaValidator):
+    adapts(Interface, IQGDiscussionCaptchas, Interface, IField, Interface)
+
+    def validate(self, value):
+
+        registry = queryUtility(IRegistry)
+        settings = registry.forInterface(IDiscussionSettings, check=False)
+        if settings.captcha == CAPTCHA_NAME:
+            super(QGDiscussionCaptchaValidator, self).validate(value)
+
+
+validator.WidgetValidatorDiscriminators(QGDiscussionCaptchaValidator,
+                                        widget=CaptchaWidget,
+                                        field=ICaptcha['captcha'])
Index: quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/version.txt
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/version.txt	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/quintagroup/plonecaptchas/version.txt	(revision 3272)
@@ -0,0 +1,1 @@
+4.0
Index: quintagroup.plonecaptchas/tags/4.1/setup.cfg
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/setup.cfg	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/setup.cfg	(revision 3272)
@@ -0,0 +1,5 @@
+[zopeskel]
+template = plone
+
+[egg_info]
+
Index: quintagroup.plonecaptchas/tags/4.1/setup.py
===================================================================
--- quintagroup.plonecaptchas/tags/4.1/setup.py	(revision 3272)
+++ quintagroup.plonecaptchas/tags/4.1/setup.py	(revision 3272)
@@ -0,0 +1,40 @@
+from setuptools import setup, find_packages
+import os
+
+version = '4.1'
+
+setup(name='quintagroup.plonecaptchas',
+      version=version,
+      description="quintagroup.plonecaptchas is simple captchas "
+                  "implementation for Plone, designed for validating "
+                  "human input in insecure forms.",
+      long_description=open("README.txt").read() + "\n" +
+                       open(os.path.join("docs", "HISTORY.txt")).read(),
+      classifiers=[
+        "Framework :: Plone",
+        "Programming Language :: Python",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+        ],
+      keywords='plone captcha',
+      author='Quintagroup',
+      author_email='support@quintagroup.com',
+      url='http://svn.quintagroup.com/products/quintagroup.plonecaptchas',
+      license='GPL',
+      packages=find_packages(exclude=['ez_setup']),
+      namespace_packages=['quintagroup'],
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          'setuptools',
+          'Plone>=4.0dev',
+          'quintagroup.captcha.core>=0.2',
+          'quintagroup.formlib.captcha',
+          'quintagroup.z3cform.captcha',
+          # -*- Extra requirements: -*-
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      [z3c.autoinclude.plugin]
+      target = plone
+      """,
+      )
