Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/__init__.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/__init__.py	(revision 386)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/__init__.py	(revision 387)
@@ -0,0 +1,4 @@
+from AccessControl import allow_module
+
+allow_module('Products.qSEOptimizer.util')
+qSEO_globals = globals()
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/adapters.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/adapters.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/adapters.py	(revision 387)
@@ -0,0 +1,37 @@
+import re, commands
+from zope.interface import implements
+from Products.CMFCore.utils import getToolByName
+from quintagroup.seoptimizer.interfaces import IKeywords
+
+
+class AdditionalKeywords(object):
+    implements(IKeywords)
+
+    def __init__(self, context):
+        self.context = context
+
+    def listKeywords(self):
+        portal_props = getToolByName(self.context, 'portal_properties')
+        seo_props = getToolByName(portal_props, 'seo_properties')
+
+        # now set type is not using because of it unordered behaviour
+        #original = set(self.context.qSEO_Keywords())
+        #additional = set(seo_props.additional_keywords)
+        #text = set(self.context.SearchableText().split())
+        #keywords = list(additional.intersection(text).union(original))
+
+        keywords = list(self.context.qSEO_Keywords())
+        lower_keywords = map(lambda x: x.lower(), keywords)
+        additional = seo_props.additional_keywords
+
+        is_test = self.context.REQUEST.get('qseo_without_additional_keywords', None)
+
+        if additional and is_test is None:
+            # extract words from url page using lynx browser
+            text = commands.getoutput('lynx --dump --nolist %s?qseo_without_additional_keywords=1' % self.context.absolute_url()).lower()
+            if text and text != 'sh: lynx: command not found':
+                for keyword in additional:
+                    if keyword.lower() not in lower_keywords and re.compile(r'\b%s\b' % keyword, re.I).search(text):
+                        keywords.append(keyword)
+
+        return ', '.join(keywords)
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/__init__.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/__init__.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/__init__.py	(revision 387)
@@ -0,0 +1,1 @@
+#
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/configure.zcml
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/configure.zcml	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/configure.zcml	(revision 387)
@@ -0,0 +1,43 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser">
+
+    <!-- The customized title -->
+    <browser:viewlet
+        name="plone.htmlhead.title"
+        manager="plone.app.layout.viewlets.interfaces.IHtmlHead"
+        class=".viewlets.TitleCommentViewlet"
+        permission="zope2.View"
+        />
+       
+    <browser:viewlet
+        name="quintagroup.seoptimizer.http_equiv"
+        manager="plone.app.layout.viewlets.interfaces.IHtmlHead"
+        template="templates/http_equiv.pt"
+        class=".viewlets.HTTPEquiv"
+        permission="zope2.View"
+        />
+
+    <browser:viewlet
+        name="quintagroup.seoptimizer.base"
+        manager="plone.app.layout.viewlets.interfaces.IHtmlHead"
+        class=".viewlets.BaseUrlViewlet"
+        permission="zope2.View"
+        />
+
+    <browser:viewlet
+        name="quintagroup.seoptimizer.generator"
+        manager="plone.app.layout.viewlets.interfaces.IHtmlHead"
+        template="templates/generator.pt"
+        permission="zope2.View"
+        />
+
+    <browser:viewlet
+        name="quintagroup.seoptimizer.metategs"
+        manager="plone.app.layout.viewlets.interfaces.IHtmlHead"
+        class=".viewlets.MetaTagsViewlet"
+        template="templates/metatags.pt"
+        permission="zope2.View"
+        />
+
+</configure>
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/interfaces.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/interfaces.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/interfaces.py	(revision 387)
@@ -0,0 +1,7 @@
+from zope.interface import Interface
+
+class IValidateSEOKeywordsView(Interface):
+    """ View for validating keywords on qSEO_properties_edit_form """
+
+    def validateKeywords(text):
+        """ Parse text and validate each keyword (extracted from text) for appearing on the context page """
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/keywords.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/keywords.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/keywords.py	(revision 387)
@@ -0,0 +1,111 @@
+import urllib, re, os, commands
+from xml.dom import minidom, Node
+
+from zope.interface import implements
+from Products.Five.browser import BrowserView
+
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone import PloneMessageFactory as _
+
+from interfaces import IValidateSEOKeywordsView
+
+class ValidateSEOKeywordsView(BrowserView):
+
+    implements(IValidateSEOKeywordsView)
+
+    def validateKeywords(self, text):
+        """ see interface """
+
+        # extract keywords from text
+        if not text.strip():
+            return _(u'Keywords list is empty!')
+
+        keywords = map(lambda x: x.strip(), text.lower().split('\n'))
+        if not keywords:
+            return _(u'Keywords list is empty!')
+
+        # request html page of context object
+        url = '%s?qseo_without_additional_keywords=1' % self.context.absolute_url()
+        #try:
+            #page = urllib.urlopen(url)
+        #except IOError:
+            #return _('Could not find requested page')
+
+        #page_html = page.read()
+        #if not page_html:
+            #return _('Page is empty')
+
+        # extract words from body from html page
+
+        # this block work only with valid html
+        #doc = minidom.parseString(page_html)
+        #rootNode = doc.documentElement
+        #bodies = rootNode.getElementsByTagName('body')
+        #if len(bodies) > 0:
+            #body = bodies[0]
+        #else:
+            #return _(u'Invalid page html')
+        #page_words = []
+        #self.walkTextNodes(body, page_words)
+
+        # this block work even with invalid html
+        #pattern = re.compile('<\s*body[^>]*>(.*?)<\s*/\s*body\s*>', re.S|re.M|re.I)
+        #search = pattern.search(page_html)
+        #if search:
+            #body_html = search.group(1)
+        #else:
+            #return _('Invalid html code on page')
+
+        #page_text = self.strip_tags(body_html)
+        #page_words = page_text.lower().split()
+
+        # extract words from url page using lynx browser
+        page_text = commands.getoutput('lynx --dump --nolist %s' % url).lower()
+        if page_text and page_text != 'sh: lynx: command not found':
+            #page_words = page_text.lower().split()
+            page_text = page_text
+        else:
+            return _(u'Could not find lynx browser!')
+
+        # check every keyword on appearing in body of html page
+        missing = []
+        added = {}
+        for keyword in keywords:
+            if keyword not in added.keys() and not re.compile(r'\b%s\b' % keyword, re.I).search(page_text):
+                missing.append(keyword)
+                added[keyword] = 1
+
+        # return list of missing keywords
+        if missing:
+            msg = u"""Next keywords did not appear on the page:\n%s""" % '\n'.join(missing)
+        else:
+            msg = u"""All keywords found on the page!"""
+        return _(msg)
+
+    def walkTextNodes(self, parent, page_words=[]):
+        for node in parent.childNodes:
+            if node.nodeType == Node.ELEMENT_NODE:
+                self.walkTextNodes(node, page_words)
+            elif node.nodeType == Node.TEXT_NODE:
+                value = node.nodeValue
+                if value is not None:
+                    page_words.extend(map(lambda x: x.lower(), value.split()))
+
+    def strip_tags(self, in_text):
+        s_list = list(in_text)
+        i,j = 0,0
+
+        while i < len(s_list):
+            if s_list[i] == '<':
+                while s_list[i] != '>':
+                    # pop everything from the the left-angle bracket until the right-angle bracket
+                    s_list.pop(i)
+                    
+                # pops the right-angle bracket, too
+                s_list.pop(i)
+            else:
+                i=i+1
+                
+        # convert the list back into text
+        join_char=''
+        return join_char.join(s_list)
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/templates/generator.pt
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/templates/generator.pt	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/templates/generator.pt	(revision 387)
@@ -0,0 +1,1 @@
+<meta name="generator" content="Plone - http://plone.org" />
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/templates/http_equiv.pt
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/templates/http_equiv.pt	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/templates/http_equiv.pt	(revision 387)
@@ -0,0 +1,3 @@
+<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"
+      tal:define="charset view/charset"
+      tal:attributes="content string:text/html;;charset=${charset}" />
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/templates/metatags.pt
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/templates/metatags.pt	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/templates/metatags.pt	(revision 387)
@@ -0,0 +1,5 @@
+<meta tal:define="metatags view/listMetaTags"
+      tal:condition="metatags"
+      tal:repeat="keyval metatags"
+      tal:attributes="name python:keyval[0];
+                      content python:keyval[1];" />
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/viewlets.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/viewlets.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/browser/viewlets.py	(revision 387)
@@ -0,0 +1,127 @@
+from zope.component import getMultiAdapter
+from zope.viewlet.interfaces import IViewlet
+from Products.CMFPlone.utils import safe_unicode
+from plone.app.layout.viewlets.common import ViewletBase
+from Products.CMFCore.utils import getToolByName
+from AccessControl import Unauthorized
+from quintagroup.seoptimizer.util import SortedDict
+from quintagroup.seoptimizer.interfaces import IKeywords 
+
+class TitleCommentViewlet(ViewletBase):
+
+    def update(self):
+        self.portal_state = getMultiAdapter((self.context, self.request),
+                                            name=u'plone_portal_state')
+        self.context_state = getMultiAdapter((self.context, self.request),
+                                             name=u'plone_context_state')
+        self.page_title = self.context_state.object_title
+        self.portal_title = self.portal_state.portal_title
+
+        self.override_title = self.context.hasProperty('qSEO_title')
+        self.override_comments = self.context.hasProperty('qSEO_html_comment')
+
+    def render(self):
+        std_title = u"<title>%s &mdash; %s</title>" % ( safe_unicode(self.page_title()),
+                                                        safe_unicode(self.portal_title())
+                                                      )
+        if not self.override_title:
+            if not self.override_comments:
+                return std_title
+            else:
+                qseo_comments = u"<!--%s-->"%safe_unicode(self.context.qSEO_HTML_Comment())
+                return u"%s\n%s"%(std_title, qseo_comments)
+        else:
+            qseo_title = u"<title>%s</title>" % safe_unicode(self.context.qSEO_Title())
+            if not self.override_comments:
+                return qseo_title
+            else:
+                qseo_comments = u"<!--%s-->"%safe_unicode(self.context.qSEO_HTML_Comment())
+                return u"%s\n%s"%(qseo_title, qseo_comments)
+
+
+class HTTPEquiv(ViewletBase):
+    
+    def charset( self ):
+        context = self.context.aq_inner
+        site_properties = getToolByName( context, 'portal_properties').site_properties
+        return site_properties.getProperty('default_charset', 'utf-8')
+         
+class BaseUrlViewlet( ViewletBase ):
+    """
+       simpel viewlet for base href rendering
+    """
+    def renderBase( self ):
+        # returns correct base href
+        context = self.context.aq_inner
+        isFolder = getattr(context.aq_explicit, 'isPrincipiaFolderish', 0)
+        base_url = context.absolute_url()
+
+        # when accessing via WEBDAV you're not allowed to access aq_explicit
+        try:
+            return '%s/'%base_url and isFolder or base_url
+        except (Unauthorized, 'Unauthorized'):
+            pass
+
+    def render( self ):
+        return """<base href="%s" /><!--[if lt IE 7]></base><![endif]-->"""% self.renderBase()
+
+
+class MetaTagsViewlet( ViewletBase ):
+
+    def listMetaTags( self ):
+        context = self.context.aq_inner
+        portal_props = getToolByName(context, 'portal_properties')
+        pu = getToolByName(context, 'plone_utils')
+        seo_props = getToolByName(portal_props, 'seo_properties', None)
+        if seo_props is None:
+            return pu.listMetaTags(context)
+
+        site_props = getToolByName(portal_props, 'site_properties')
+        exposeDCMetaTags = site_props.exposeDCMetaTags
+
+        metaTags = SortedDict()
+        metaTags.update(pu.listMetaTags(context))
+        metadataList = [
+            ('qSEO_Description', 'description'),
+            ('qSEO_Keywords',    'keywords'),
+            ('qSEO_Robots',      'robots'),
+            ('qSEO_Distribution','distribution')]
+
+        if exposeDCMetaTags:
+            metadataList.append(('qSEO_Distribution', 'DC.distribution'))
+
+        for accessor, key in metadataList:
+            method = getattr(context, accessor, None)
+            if not callable(method):
+                # ups
+                continue
+            # Catch AttributeErrors raised by some AT applications
+            try:
+                value = method()
+            except AttributeError:
+                value = None
+
+            if not value:
+                continue
+            if isinstance(value, (tuple, list)):
+                value = ', '.join(value)
+
+            metaTags[key] = value
+
+        # add custom meta tags (added from qseo tab by user) for given context
+        property_prefix = 'qSEO_custom_'
+        for property, value in context.propertyItems():
+            idx = property.find(property_prefix)
+            if idx == 0 and len(property) > len(property_prefix):
+                metaTags[property[len(property_prefix):]] = value
+
+        # Set the additional matching keywords, if any
+        adapter = IKeywords(context, None)
+        if adapter is not None:
+            keywords = adapter.listKeywords()
+            if keywords:
+                metaTags['keywords'] = keywords
+
+        return metaTags.items()
+        
+
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/config.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/config.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/config.py	(revision 387)
@@ -0,0 +1,16 @@
+PROJECT_NAME = 'qSEOptimizer'
+SKINS_DIR = 'skins'
+
+GLOBALS = globals()
+
+PROPERTY_SHEET = 'seo_properties'
+SHEET_TITLE = 'SEO Properties'
+
+STOP_WORDS = ['a', 'an', 'amp', 'and', 'are', 'arial', 'as', 'at', 'be', 'but', 'by', 'can', 'com', 'do', 'font',
+              'for', 'from', 'gif', 'had', 'has', 'have', 'he', 'helvetica', 'her', 'his', 'how', 'href', 'i',
+              'if', 'in', 'is', 'it', 'javascript', 'jpg', 'made', 'net', 'of', 'on', 'or', 'org', 'our', 'sans',
+              'see', 'serif', 'she', 'that', 'the', 'this', 'to', 'us', 'we', 'with', 'you', 'your']
+
+FIELDS = ['seo_title', 'seo_description', 'seo_keywords']
+
+DEFAULT_CUSTOM_METATAGS = []
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/configure.zcml
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/configure.zcml	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/configure.zcml	(revision 387)
@@ -0,0 +1,32 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:five="http://namespaces.zope.org/five"
+    xmlns:browser="http://namespaces.zope.org/browser"
+    xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
+    xmlns:cmf="http://namespaces.zope.org/cmf"
+    >
+
+  <genericsetup:registerProfile
+      name="default"
+      title="quintagroup.seoptimizer"
+      directory="profiles/default"
+      description=""
+      provides="Products.GenericSetup.interfaces.EXTENSION" />
+
+    <browser:page
+        for="Products.Archetypes.interfaces.IBaseObject"
+        attribute="validateKeywords"
+        class=".browser.keywords.ValidateSEOKeywordsView"
+        name="checkSEOKeywords"
+        permission="cmf.ModifyPortalContent"
+        />
+
+  <adapter
+    for="Products.Archetypes.public.BaseObject"
+    provides=".interfaces.IKeywords"
+    factory=".adapters.AdditionalKeywords"
+    />
+
+  <cmf:registerDirectory name="quintagroup.seoptimizer" />
+
+</configure>
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/interfaces.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/interfaces.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/interfaces.py	(revision 387)
@@ -0,0 +1,8 @@
+from zope.interface import Interface
+
+class IKeywords(Interface):
+    """Handle the available keywords.
+    """
+    def listKeywords():
+        """Returns all the existing keywords for the current content type.
+        """
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/overrides.zcml
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/overrides.zcml	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/overrides.zcml	(revision 387)
@@ -0,0 +1,7 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:zcml="http://namespaces.zope.org/zcml">
+    <include zcml:condition="installed plone.app.viewletmanager"
+             package=".browser" />
+
+</configure>
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/profiles/default/actions.xml
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/profiles/default/actions.xml	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/profiles/default/actions.xml	(revision 387)
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<object name="portal_actions" meta_type="Plone Actions Tool"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <object name="object" meta_type="CMF Action Category">
+  <object name="seo_properties" meta_type="CMF Action" i18n:domain="plone">
+   <property name="title" i18n:translate="">SEO Properties</property>
+   <property name="description" i18n:translate=""></property>
+   <property
+      name="url_expr">string:${object_url}/qseo_properties_edit_form</property>
+   <property name="icon_expr"></property>
+   <property
+      name="available_expr"></property>
+   <property name="permissions">
+    <element value="Modify portal content"/>
+   </property>
+   <property name="visible">True</property>
+  </object>
+ </object>
+</object>
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/profiles/default/skins.xml
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/profiles/default/skins.xml	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/profiles/default/skins.xml	(revision 387)
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<object name="portal_skins">
+ <object name="quintagroup.seoptimizer" meta_type="Filesystem Directory View"
+    directory="quintagroup.seoptimizer:skins/quintagroup.seoptimizer"/>
+  <skin-path name="*">
+  <layer name="quintagroup.seoptimizer" insert-after="custom"/>
+ </skin-path>
+</object>
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/profiles/default/viewlets.xml
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/profiles/default/viewlets.xml	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/profiles/default/viewlets.xml	(revision 387)
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<object>
+  <order manager="plone.htmlhead" skinname="Plone Default">
+    <viewlet name="plone.htmlhead.title" />
+    <viewlet name="quintagroup.seoptimizer.base" />
+    <viewlet name="quintagroup.seoptimizer.http_equiv" />
+    <viewlet name="quintagroup.seoptimizer.generator" />
+    <viewlet name="quintagroup.seoptimizer.metatags" />
+    <viewlet name="plone.resourceregistries" />
+  </order>
+</object>
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/checkkeywords.js.dtml
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/checkkeywords.js.dtml	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/checkkeywords.js.dtml	(revision 387)
@@ -0,0 +1,118 @@
+/******************************************************************/
+
+var seo_keywords_url = '&dtml-absolute_url;/checkSEOKeywords';
+var KEYWORDS_IDS = ['seo_keywords',];
+// var KEYWORDS_REPORT = {};
+
+// function extractWords(data) {
+//     data = data // replace all non-word character with space
+//                 .replace(/[^a-zA-Z0-9\-\'\u2019\"\`]+/g, ' ')
+//                 // replace "-" and "'" symbols if it create groups event inside of token
+//                 .replace(/[\-\'\u2019\"\`]{2,}/g, ' ')
+//                 // replace all non-word characters and "-", "'" if it stay at word edge
+//                 .replace(/(?:^|\s+)[^a-zA-Z0-9]+|[^a-zA-Z0-9]+(?:\s+|$)/g, ' ')
+//                 // strip whitespaces
+//                 .replace(/^\s*(.*?)\s*$/, '$1');
+//     return data.split(/[^a-zA-Z0-9\-\'\u2019\"\`]+/);
+// }
+// 
+// function countTerm(node, word, id) {
+//     var contents = extractWords(node.nodeValue.toLowerCase());
+//     var term = word.toLowerCase();
+//     for (var i = 0, w; w = contents[i]; i++) {
+//         if (w == term) {
+//             KEYWORDS_REPORT[id][word] = 1;
+//             return 'found';
+//         }
+//     }
+//     return false;
+// }
+// 
+// function keywordsWalkTextNodes(node, func, data, id) {
+//     if (!node) return false;
+//     if (KEYWORDS_REPORT[id][data] == 1) return 'found';
+//     if (node.hasChildNodes) {
+//         if (node.nodeType == 3) {
+//             if (func(node, data, id) == 'found') {
+//                 return 'found';
+//             }
+//         }
+//         for (var i = 0; i < node.childNodes.length; i++) {
+//             if (keywordsWalkTextNodes(node.childNodes[i], func, data, id) == 'found') {
+//                 return 'found';
+//             }
+//         }
+//     }
+// }
+
+// function checkPageKeywords(event) {
+//     var event = event ? event:window.event;
+//     var target = event.target ? event.target : event.srcElement;
+//     if (!target) {return false;};
+//     var id = target.id.replace('_check_keywords', '')
+//     KEYWORDS_REPORT[id] = {};
+//     var area = document.getElementById(id);
+//     if (area && typeof(area.value) != 'undefined') {
+//         var terms = extractWords(area.value);
+//         for (var i = 0, term; term = terms[i]; i++) {
+//             if (KEYWORDS_REPORT[id][term] == 1) continue;
+//             KEYWORDS_REPORT[id][term] = 0;
+//             keywordsWalkTextNodes(document.body, countTerm, term, id);
+//         }
+//     }
+//     var report = '';
+//     for (var term in KEYWORDS_REPORT[id]) {
+//         if (KEYWORDS_REPORT[id][term] != 1)
+//             report += term + ' ';
+//     }
+//     KEYWORDS_REPORT[id] = {};
+//     if (report != '') {
+//         report = 'Next keywords did not appear on the page:\n' + report;
+//     } else {
+//         report = 'All keywords found on the page!';
+//     }
+//     alert(report);
+//     return false;
+// }
+
+function checkPageKeywords(event) {
+    var event = event ? event:window.event;
+    var target = event.target ? event.target : event.srcElement;
+    if (!target) {return false;};
+    var id = target.id.replace('_check_keywords', '')
+    var area = document.getElementById(id);
+    if (area && typeof(area.value) != 'undefined') {
+        var req = new XMLHttpRequest();
+        req.open("POST", seo_keywords_url, true);
+        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+        req.onreadystatechange = function(request) {
+            if (req.readyState == 4) {
+                if (req.status == '200') {
+                    alert(req.responseText);
+                } else {
+                    alert('Error on server!');
+                }
+            }
+        };
+        req.send("text="+area.value);
+    }
+    return false;
+}
+
+function addKeywordsButton(event) {
+    for (var i = 0, id; id = KEYWORDS_IDS[i]; i++) {
+        var area = document.getElementById(id);
+        if (!area || (typeof(area.value) == 'undefined')) continue;
+        var button = document.createElement('INPUT');
+        button.type = 'button';
+        button.value = 'Check Keywords';
+        button.name = id+'_check_keywords';
+        button.id = id+'_check_keywords';
+        button.className = 'check-keywords-button';
+        area.parentNode.insertBefore(button, area.nextSibling);
+        registerEventListener(button, 'click', checkPageKeywords);
+//         KEYWORDS_REPORT[id] = {};
+    }
+}
+
+registerPloneFunction(addKeywordsButton);
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/custommetatags.js
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/custommetatags.js	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/custommetatags.js	(revision 387)
@@ -0,0 +1,352 @@
+customMetaTagsFunctions = new Object()
+
+customMetaTagsFunctions.getInputOrSelect = function(node) {
+    /* Get the (first) input or select form element under the given node */
+    
+    var inputs = node.getElementsByTagName("input");
+    if(inputs.length > 0) {
+        return inputs[0];
+    }
+    
+    var selects = node.getElementsByTagName("select");
+    if(selects.length > 0) {
+        return selects[0];
+    }
+
+    return null;
+}
+
+customMetaTagsFunctions.getWidgetRows = function(currnode) {
+	/* Return primary <tr>s of current node's parent DGW */
+	tbody = this.getParentElementById(currnode, "datagridwidget-tbody");
+	return this.getRows(tbody);
+}
+
+customMetaTagsFunctions.getRows = function(tbody) {
+	/* Return <tr> rows of <table> element */
+    
+	var rows = new Array()
+	
+	child = tbody.firstChild;
+	while(child != null) {
+		if(child.tagName != null) {
+			if(child.tagName.toLowerCase() == "tr") {
+				rows = rows.concat(child);
+			}
+		}
+		child = child.nextSibling;
+	}
+	              
+	return rows;   
+} 
+
+customMetaTagsFunctions.autoInsertRow = function(e) {
+    /* Add a new row when changing the last row 
+       (i.e. the infamous auto insert feature)
+    
+       Check that if this onchange event handler was
+       called from the last row. In this case,
+       add a new row for DGF.
+       
+    */
+
+    var currnode = window.event ? window.event.srcElement : e.currentTarget;
+    
+	// fetch required data structure   
+    var tbody = this.getParentElement(currnode, "TBODY");
+    var rows = this.getRows(tbody);        
+    var lastRow = rows[rows.length-1]; 
+    
+    var thisRow = this.getParentElementById(currnode, "datagridwidget-row");      
+    
+    /* Skip the very last row which is a hidden template row */
+    if(rows.length-1 ==(thisRow.rowIndex)) {
+	    // Create a new row
+	    var newtr = this.createNewRow(lastRow);
+	                                                        
+	    // Put new row to DOM tree before template row        
+		lastRow.parentNode.insertBefore(newtr, lastRow);
+		
+		// update orderindex hidden fields
+		this.updateOrderIndex(tbody);	        	    
+    }    
+}
+
+customMetaTagsFunctions.addRowAfter = function(currnode) {
+	/*
+		Creates a new row before the clicked row
+	*/
+	
+	// fetch required data structure
+    var tbody = this.getParentElementById(currnode, "datagridwidget-tbody"); 
+    var thisRow = this.getParentElementById(currnode, "datagridwidget-row"); 
+
+    var newtr = this.createNewRow(thisRow);
+        
+	thisRow.parentNode.insertBefore(newtr, thisRow);
+	
+	// update orderindex hidden fields
+	this.updateOrderIndex(tbody);	
+  
+}
+
+customMetaTagsFunctions.addRow = function(id) {
+	/* Explitcly add row for given DataGridField 
+	
+		@param id Archetypes field id for the widget	
+	*/
+	
+	// fetch required data structure
+    var tbody = document.getElementById("datagridwidget-tbody-" + id);    
+    var rows = this.getRows(tbody);    
+    var lastRow = rows[rows.length-1];
+        
+    var oldRows = rows.length;
+                  
+    // Create a new row
+    var newtr = this.createNewRow(lastRow);
+    
+    // Put new row to DOM tree before template row        
+	newNode = lastRow.parentNode.insertBefore(newtr, lastRow);
+	
+	// update orderindex hidden fields
+	this.updateOrderIndex(tbody);		
+      
+}
+
+customMetaTagsFunctions.createNewRow = function(tr) { 
+	/* Creates a new row 
+		   
+	   @param tr A row in a table where we'll be adding the new row
+	*/
+	
+    var tbody = this.getParentElementById(tr, "datagridwidget-tbody"); 
+    var rows = this.getRows(tbody);   
+    
+    // hidden template row 
+    var lastRow = rows[rows.length-1]; 
+	
+	var newtr = document.createElement("tr");
+    newtr.setAttribute("id", "datagridwidget-row");
+    newtr.setAttribute("class", "datagridwidget-row");
+    	
+	// clone template contents from the last row to the newly created row
+	// HOX HOX HOX
+	// If f****ng IE clones lastRow directly it doesn't work.
+	// lastRow is in hidden state and no matter what you do it remains hidden.
+	// i.e. overriding class doesn't bring it visible.
+	// In Firefox everything worked like a charm.
+	// So the code below is really a hack to satisfy Microsoft codeborgs.
+	// keywords: IE javascript clone clonenode hidden element render visibility visual
+	child = lastRow.firstChild;
+	while(child != null) {
+		newchild = child.cloneNode(true);
+		newtr.appendChild(newchild);
+		child = child.nextSibling;
+	}		
+	    
+    return newtr;	 
+}
+
+
+customMetaTagsFunctions.removeFieldRow = function(node) {
+    /* Remove the row in which the given node is found */
+    
+    var row = this.getParentElementById(node, 'datagridwidget-row');
+    var tbody = this.getParentElementById(node, 'datagridwidget-tbody');
+    tbody.removeChild(row);
+}
+
+customMetaTagsFunctions.moveRowDown = function(currnode){
+    /* Move the given row down one */
+           
+    var tbody = this.getParentElementById(currnode, "datagridwidget-tbody");    
+    
+    var rows = this.getWidgetRows(currnode);
+    
+    var row = this.getParentElementById(currnode, "datagridwidget-row");      
+    if(row == null) {
+    	alert("Couldn't find DataGridWidget row");
+    	return;
+    }
+    
+    var idx = null
+    
+    // We can't use nextSibling because of blank text nodes in some browsers
+    // Need to find the index of the row
+    for(var t = 0; t < rows.length; t++) {
+        if(rows[t] == row) {
+            idx = t;
+            break;
+        }
+    }
+
+    // Abort if the current row wasn't found
+    if(idx == null)
+        return;     
+        
+    // If this was the last row (before the blank row at the end used to create
+    // new rows), move to the top, else move down one.
+    if(idx + 2 == rows.length) {
+        var nextRow = rows.item[0]
+        this.shiftRow(row, nextRow)
+    } else {
+        var nextRow = rows[idx+1]
+        this.shiftRow(nextRow, row)
+    }
+    
+    this.updateOrderIndex(tbody)
+
+}
+
+customMetaTagsFunctions.moveRowUp = function(currnode){
+    /* Move the given row up one */
+    
+    var tbody = this.getParentElementById(currnode, "datagridwidget-tbody");    
+    var rows = this.getWidgetRows(currnode);
+    
+    var row = this.getParentElementById(currnode, "datagridwidget-row");      
+    if(row == null) {
+    	alert("Couldn't find DataGridWidget row");
+    	return;
+    }
+
+    var idx = null
+    
+    // We can't use nextSibling because of blank text nodes in some browsers
+    // Need to find the index of the row
+    for(var t = 0; t < rows.length; t++) {
+        if(rows[t] == row) {
+            idx = t;
+            break;
+        }
+    }
+    
+    // Abort if the current row wasn't found
+    if(idx == null)
+        return;
+        
+    // If this was the first row, move to the end (i.e. before the blank row
+    // at the end used to create new rows), else move up one
+    if(idx == 0) {
+        var previousRow = rows[rows.length - 1]
+        this.shiftRow(row, previousRow);
+    } else {
+        var previousRow = rows[idx-1];
+        this.shiftRow(row, previousRow);
+    }
+    
+    this.updateOrderIndex(tbody);
+}
+
+customMetaTagsFunctions.shiftRow = function(bottom, top){
+    /* Put node top before node bottom */
+    
+    bottom.parentNode.insertBefore(bottom, top)   
+}
+
+customMetaTagsFunctions.updateOrderIndex = function (tbody) {
+
+    /* Update the hidden orderindex fields to be in the right order */
+    
+    var xre = new RegExp(/^orderindex__/)
+    var idx = 0;
+    var cell;
+    
+    var rows = this.getRows(tbody); 
+    
+    /* Make sure that updateOrderIndex doesn't touch 
+       the template (last) row */
+    for(var i=0; i<rows.length-1; i++) {
+    
+	    for (var c = 0; (cell = rows[i].getElementsByTagName('INPUT').item(c)); c++) {
+	            
+	        if (cell.getAttribute('id')) {
+	            if (xre.exec(cell.id)) {
+	                cell.value = idx;
+	            }
+	        }           
+	        this.updateRadioButtonGroupName(this.getParentElement(cell, "TR"), idx);        
+	        idx++;
+	    }      
+	}
+}
+
+
+customMetaTagsFunctions.updateRadioButtonGroupName = function (row, newIndex) {
+	/* Adjust radio button group names after reordering 
+	
+	   Why we do this, see RadioColumn class comments
+	   
+	   TODO: If chain onchange -> updateOrderIndex -> updaterRadioButtonGroupName
+	   is triggered on Firefox, the value of checked radio button is put to the
+	   newly generated row instead of clicked row.
+	*/
+
+   var cell;
+   var xre = new RegExp(/^radio/)
+   var xre2 = new RegExp(/^checkbox/)
+   
+    for (var c = 0; (cell = row.getElementsByTagName('INPUT').item(c)); c++) {
+   	    	    
+   	   	if(cell.getAttribute('type')) {
+   	   		var type = cell.getAttribute('type');
+             if (xre.exec(type) || xre2.exec(type)) {          
+            	
+				var name = cell.getAttribute("NAME")
+				if(name == null) continue;
+
+				// save fieldId + columnId part
+				var baseLabel = name.substring(0, name.lastIndexOf("."));				
+				// update per row running id
+				cell.setAttribute("NAME", baseLabel + "." + newIndex);
+			}
+   	    }               
+	}
+}
+
+customMetaTagsFunctions.getParentElement = function(currnode, tagname) {
+    /* Find the first parent node with the given tag name */
+
+    tagname = tagname.toUpperCase();
+    var parent = currnode.parentNode;
+
+    while(parent.tagName.toUpperCase() != tagname) {
+        parent = parent.parentNode;
+        // Next line is a safety belt
+        if(parent.tagName.toUpperCase() == "BODY") 
+            return null;
+    }
+
+    return parent;
+}
+
+customMetaTagsFunctions.getParentElementById = function(currnode, id) {
+    /* Find the first parent node with the given id 
+    
+    	Id is partially matched: the beginning of
+    	an element id matches parameter id string.
+    
+    	Currnode: Node where ascending in DOM tree beings
+    	Id: Id string to look for. 
+    	    	
+    */
+    
+    id = id.toLowerCase();
+    var parent = currnode.parentNode;
+
+    while(true) {
+       
+    	var parentId = parent.getAttribute("id");
+    	if(parentId != null) {    	
+    		 if(parentId.toLowerCase().substring(0, id.length) == id) break;
+    	}
+    	    
+        parent = parent.parentNode;
+        // Next line is a safety belt
+        if(parent.tagName.toUpperCase() == "BODY") 
+            return null;
+    }
+
+    return parent;
+}
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/main_template.pt
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/main_template.pt	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/main_template.pt	(revision 387)
@@ -0,0 +1,172 @@
+<metal:page define-macro="master"><metal:doctype define-slot="doctype"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"></metal:doctype>
+<metal:block define-slot="top_slot" />
+<metal:block use-macro="here/global_defines/macros/defines" />
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xml:lang="en"
+      lang="en"
+      tal:define="lang language"
+      tal:attributes="lang lang;
+                      xml:lang lang">
+
+  <tal:cache tal:define="charset site_properties/default_charset|string:utf-8">
+    <metal:cache use-macro="here/global_cache_settings/macros/cacheheaders">
+      Get the global cache headers located in global_cache_settings.
+    </metal:cache>
+  </tal:cache>
+
+  <head>
+    <div tal:replace="structure provider:plone.htmlhead" />
+
+    <!-- Internet Explorer CSS Fixes -->
+    <tal:iefixstart replace="structure string:&lt;!--[if IE]&gt;" />
+        <style type="text/css" media="all" tal:condition="exists: portal/IEFixes.css"
+               tal:content="string:@import url($portal_url/IEFixes.css);">
+        </style>
+    <tal:iefixend replace="structure string:&lt;![endif]--&gt;" />
+
+    <link tal:replace="structure provider:plone.htmlhead.links" />
+
+    <!-- Disable IE6 image toolbar -->
+    <meta http-equiv="imagetoolbar" content="no" />
+    
+    <tal:comment replace="nothing"> A slot where you can insert elements in the header from a template </tal:comment>
+    <metal:headslot define-slot="head_slot" />
+
+    <tal:comment replace="nothing"> A slot where you can insert CSS in the header from a template </tal:comment>
+    <metal:styleslot define-slot="style_slot" />
+
+    <tal:comment replace="nothing"> This is deprecated, please use style_slot instead. </tal:comment>
+    <metal:cssslot define-slot="css_slot" />
+
+    <tal:comment replace="nothing"> A slot where you can insert javascript in the header from a template </tal:comment>
+    <metal:javascriptslot define-slot="javascript_head_slot" />
+
+  </head>
+
+  <body tal:attributes="class string:${here/getSectionFromURL} template-${template/id};
+                        dir python:test(isRTL, 'rtl', 'ltr')">
+    <div id="visual-portal-wrapper">
+
+      <div id="portal-top" i18n:domain="plone">
+        <div tal:replace="structure provider:plone.portaltop" />
+      </div>
+
+      <div class="visualClear" id="clear-space-before-wrapper-table"><!-- --></div>
+
+      <table id="portal-columns">
+        <tbody>
+          <tr>
+            <tal:comment replace="nothing"> Start of the left column </tal:comment>
+            <td id="portal-column-one"
+                metal:define-slot="column_one_slot"
+                tal:condition="sl">
+              <div class="visualPadding">
+                <metal:portlets define-slot="portlets_one_slot">
+                  <tal:block replace="structure provider:plone.leftcolumn" />
+                </metal:portlets>
+                &nbsp;
+              </div>
+            </td>
+            <tal:comment replace="nothing"> End of the left column </tal:comment>
+
+            <tal:comment replace="nothing"> Start of main content block </tal:comment>
+            <td id="portal-column-content"
+                tal:define="tabindex python:Iterator(mainSlot=False)">
+
+              <metal:block define-slot="content">
+                <div metal:define-macro="content"
+                     tal:define="show_border context/@@plone/showEditableBorder"
+                     tal:attributes="class python:test(show_border,'documentEditable','')">
+
+                  <div tal:replace="structure provider:plone.contentviews" />
+
+                  <div id="region-content"
+                       class="documentContent">
+
+                    <span id="contentTopLeft"></span>
+                    <span id="contentTopRight"></span>
+                    
+                    <a name="documentContent"></a>
+
+                    <div metal:use-macro="here/global_statusmessage/macros/portal_message">
+                      Portal status message
+                    </div>
+                    
+                    <div id="viewlet-above-content" tal:content="structure provider:plone.abovecontent" />
+
+                    <metal:slot metal:define-slot="body">
+                    <tal:comment replace="nothing">
+                        The div with ID #content will only show up if we're actually on a content
+                        view, never on edit forms, control panels etc. It's meant to only wrap the
+                        actual content that gets rendered on a page, not the other UI elements.
+                    </tal:comment>
+                    <div id="content"
+                         tal:omit-tag="not:context/@@plone_context_state/is_view_template">
+                      <metal:header metal:define-slot="header" tal:content="nothing">
+                        Visual Header
+                      </metal:header>
+                      <metal:bodytext metal:define-slot="main" tal:content="nothing">
+                        Page body text
+                      </metal:bodytext>
+                    </div>
+                    </metal:slot>
+
+                    <metal:sub metal:define-slot="sub">
+                    
+                      <div tal:replace="structure provider:plone.belowcontent" />
+                    
+                    </metal:sub>
+
+                    <span id="contentBottomLeft"></span>
+                    <span id="contentBottomRight"></span>
+
+                  </div>
+
+                </div>
+
+              </metal:block>
+            </td>
+            <tal:comment replace="nothing"> End of main content block </tal:comment>
+
+            <tal:comment replace="nothing"> Start of right column </tal:comment>
+            <td id="portal-column-two"
+                metal:define-slot="column_two_slot"
+                tal:condition="sr">
+              <div class="visualPadding">
+                <metal:portlets define-slot="portlets_two_slot">
+                  <tal:block replace="structure provider:plone.rightcolumn" />
+                </metal:portlets>
+                &nbsp;
+              </div>
+            </td>
+            <tal:comment replace="nothing"> End of the right column </tal:comment>
+          </tr>
+        </tbody>
+      </table>
+      <tal:comment replace="nothing"> end column wrapper </tal:comment>
+
+      <div class="visualClear" id="clear-space-before-footer"><!-- --></div>
+      
+      <div tal:define="context_state context/@@plone_context_state;
+                        portlet_assignable context_state/portlet_assignable"
+            tal:condition="python:not sl and not sr and portlet_assignable and checkPermission('Portlets: Manage portlets', context)">
+          <a class="managePortletsFallback"
+             tal:attributes="href string:${context_state/canonical_object_url}/@@manage-portlets"
+             i18n:translate="manage_portlets_fallback">
+             Manage portlets
+          </a>
+      </div>
+
+      <metal:block i18n:domain="plone">
+
+        <div tal:replace="structure provider:plone.portalfooter" />
+
+      </metal:block>
+
+      <div class="visualClear"><!-- --></div>
+    </div>
+<div id="kss-spinner"><img tal:attributes="src string:${portal_url}/spinner.gif" alt="" /></div>
+</body>
+</html>
+</metal:page>
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/prefs_qseo_setup.cpy
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/prefs_qseo_setup.cpy	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/prefs_qseo_setup.cpy	(revision 387)
@@ -0,0 +1,48 @@
+## Script (Python) "prefs_seo_config"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters= portalTypes=[], exposeDCMetaTags=None, additionalKeywords=[], default_custom_metatags=[]
+##title=add action tab for selected portal types
+##
+from Products.CMFCore.utils import getToolByName
+
+portal_props = getToolByName(context, 'portal_properties')
+site_props = getToolByName(portal_props, 'site_properties')
+seo_props = getToolByName(portal_props, 'seo_properties')
+site_props.manage_changeProperties(exposeDCMetaTags=exposeDCMetaTags)
+seo_props.manage_changeProperties(additional_keywords=additionalKeywords)
+seo_props.manage_changeProperties(default_custom_metatags=default_custom_metatags)
+
+pt = getToolByName(context, 'portal_types')
+for ptype in pt.objectValues():
+    try:
+        #for Plone-2.5 and higher
+        acts = filter(lambda x: x.id == 'seo_properties', ptype.listActions())
+        action = acts and acts[0] or None
+    except AttributeError:
+        action = ptype.getActionById('seo_properties', default=None )
+
+    if ptype.getId() in portalTypes:
+        if action is None:
+            ptype.addAction('seo_properties',
+                            'SEO Properties',
+                            'string:${object_url}/qseo_properties_edit_form',
+                            '',
+                            'Modify portal content',
+                            'object',
+                            visible=1)
+    else:
+        if action !=None:
+            actions = list(ptype.listActions())
+            ptype.deleteActions([actions.index(a) for a in actions if a.getId()=='seo_properties'])
+
+
+msg = "Search Engine Optimizer configuration updated."
+try:
+    context.plone_utils.addPortalMessage(msg)
+    return state
+except AttributeError:
+    return state.set(context=context, portal_status_message=msg)
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/prefs_qseo_setup.cpy.metadata
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/prefs_qseo_setup.cpy.metadata	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/prefs_qseo_setup.cpy.metadata	(revision 387)
@@ -0,0 +1,14 @@
+#
+# $$
+#
+[default]
+title= Save Propertiest
+
+[security]
+View=0:Authenticated
+
+[validators]
+
+[actions]
+action.success=traverse_to:string:prefs_qseo_setup_form
+action.failure=traverse_to:string:prefs_qseo_setup_form
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/prefs_qseo_setup_form.cpt
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/prefs_qseo_setup_form.cpt	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/prefs_qseo_setup_form.cpt	(revision 387)
@@ -0,0 +1,119 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      lang="en-US"
+      metal:use-macro="here/prefs_main_template/macros/master"
+      i18n:domain="qPloneGoogleSitemaps">
+<body>
+    <div metal:fill-slot="prefs_configlet_main"
+        tal:define="errors python:request.get('errors', {})">
+
+                <h1>Search Engine Optimizer configuration</h1>
+                <a href=""
+                class="link-parent"
+                tal:attributes="href string: $portal_url/plone_control_panel"
+                i18n:translate="label_up_to_plone_setup">
+                Up to Plone Setup
+                </a>
+                <div class="documentDescription">
+                   You can select what content types are
+                   qSEOptimizer-enabled, and control if Dublin Core metatags
+                   are exposed in the header of content pages.
+                </div>
+
+                <form name="edit_form"
+                    action="."
+                    method="post"
+                    enctype="multipart/form-data"
+                    tal:attributes="action string:${here_url}/${template/getId}" >
+
+                    <div class="field">
+                        <input type="checkbox"
+                               id="exposeDCMetaTags"
+                               name="exposeDCMetaTags"
+                               tal:define="exposeDCMetaTags portal/portal_properties/site_properties/exposeDCMetaTags"
+                               tal:attributes="checked exposeDCMetaTags"/>
+                        <label for="exposeDCMetaTags">Expose <abbr title="Dublin Core">DC</abbr> meta tags</label>
+                        <div class="formHelp" i18n:translate="description_seo_dc_metatags">
+                           Controls if <abbr title="Dublin Core">DC</abbr> metatags are exposed to page
+                           header. They include DC.description, DC.type,
+                           DC.format, DC.creator and others.
+                        </div>
+                    </div>
+
+                    <div class="field"
+                         tal:define="default_custom_metatags portal/portal_properties/seo_properties/default_custom_metatags|python:[]">
+                        <label for="default_custom_metatags" i18n:translate="label_default_custom_metatags">
+                            Default custom metatags.
+                        </label>
+                        <div class="formHelp" i18n:translate="help_default_custom_metatags">
+                            Fill in custom metatag names (one per line) which will appear on qseo_properties
+                            edit tab.
+                        </div>
+                        <div class="widget">
+                            <textarea cols="60" id="default_custom_metatags" name="default_custom_metatags" rows="15"
+                                      tal:content="python:'\n'.join(default_custom_metatags)"></textarea>
+                        </div>
+                    </div>
+
+                    <div class="field"
+                         tal:define="additionalKeywords portal/portal_properties/seo_properties/additional_keywords">
+                        <label for="additionalKeywords" i18n:translate="label_additional_keywords">
+                            Additional keywords that should be added to the content types.
+                        </label>
+                        <div class="formHelp" i18n:translate="help_additional_keywords">
+                            Use this field when you want that your content types receive
+                            additional keywords from the ones you manually specify. Enter
+                            one keyword per line.
+                        </div>
+                        <div class="widget">
+                            <textarea cols="60" id="additionalKeywords" name="additionalKeywords" rows="15"
+                                      tal:content="python:'\n'.join(additionalKeywords)"></textarea>
+                        </div>
+                    </div>
+
+                    <fieldset class="portalTypes"
+                              tal:define="portal_types    portal/portal_types;
+                                          allTypes        portal_types/listContentTypes;
+                                          typeLists       python:modules['Products.qSEOptimizer.util'].createMultiColumnList(context, allTypes, numCols=3, sort_on='title_or_id')">
+                        <legend i18n:translate="label_content_type_title">Content Types</legend>
+
+                        <div class="formHelp" i18n:translate="description_seo_content_types">
+                            Select content types that will have SEO properties
+                            enabled.
+                        </div>
+                        <div style="float:left; margin-right: 2em;" tal:repeat="sublist typeLists">
+                            <ul class="configlets">
+                                <li tal:repeat="ptypeInfo sublist">
+                                    <tal:type define="type python:portal_types.getTypeInfo(ptypeInfo);
+                                                      name type/getId;">
+
+                                        <input  type="checkbox"
+                                                name="portalTypes:list"
+                                                tal:attributes="id name;
+                                                                value name;
+                                                                checked python:test(filter(lambda x:x.id == 'seo_properties', type.listActions()), True, None)"/>
+                                        <label tal:attributes="for python:type.getId()"
+                                               tal:content="type/Title"/>
+                                    </tal:type>
+                                </li>
+                            </ul>
+                        </div>
+                    </fieldset>
+
+                    <div class="formControls"
+                         tal:define="process_creation request/process_creation|nothing;"
+                         i18n:domain="plone">
+                         <input class="context"
+                             tabindex=""
+                             type="submit"
+                             name="form.button.Save"
+                             value="Save"
+                             i18n:attributes="value"
+                             tal:attributes="tabindex tabindex/next;" />
+                    </div>
+                    <input type="hidden" name="form.submitted" value="1" />
+                </form>
+    </div>
+ </body>
+</html>
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/prefs_qseo_setup_form.cpt.metadata
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/prefs_qseo_setup_form.cpt.metadata	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/prefs_qseo_setup_form.cpt.metadata	(revision 387)
@@ -0,0 +1,14 @@
+#
+# $$
+#
+[default]
+title= Setup actions
+
+[security]
+View=0:Authenticated
+
+[validators]
+
+[actions]
+action.success=traverse_to:string:prefs_qseo_setup
+action.failure=traverse_to:string:prefs_qseo_setup_form
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_CustomMetaTags.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_CustomMetaTags.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_CustomMetaTags.py	(revision 387)
@@ -0,0 +1,30 @@
+## Script (Python) "qSEO_CustomMetaTags"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters=
+##title=Return context's properties prefixed with qSEO_custom_
+##
+
+result = []
+added = []
+property_prefix = 'qSEO_custom_'
+for property, value in context.propertyItems():
+    idx = property.find(property_prefix)
+    if idx == 0 and len(property) > len(property_prefix):
+        added.append(property[len(property_prefix):])
+        result.append({'meta_name'    : property[len(property_prefix):],
+                       'meta_content' : value})
+
+from Products.CMFCore.utils import getToolByName
+site_properties = getToolByName(context, 'portal_properties')
+if hasattr(site_properties, 'seo_properties'):
+    names = getattr(site_properties.seo_properties, 'default_custom_metatags', [])
+    for name in names:
+        if name not in added:
+            result.append({'meta_name'    : name,
+                           'meta_content' : ''})
+
+return result
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Description.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Description.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Description.py	(revision 387)
@@ -0,0 +1,29 @@
+## Script (Python) "qSEO_Description"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters=
+##title=Generate Description from SEO properties
+##
+
+prop_name = 'qSEO_description'
+
+if context.hasProperty(prop_name):
+    return context.getProperty(prop_name)
+
+accessor = 'Description'
+
+method = getattr(context, accessor, None)
+if not callable(method):
+    # ups
+    return None
+
+# Catch AttributeErrors raised by some AT applications
+try:
+    value = method()
+except AttributeError:
+    value = None
+
+return value
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Distribution.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Distribution.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Distribution.py	(revision 387)
@@ -0,0 +1,16 @@
+## Script (Python) "qSEO_Description"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters=
+##title=Generate Description from SEO properties
+##
+
+prop_name = 'qSEO_distribution'
+
+if context.hasProperty(prop_name):
+    return context.getProperty(prop_name)
+
+return 'Global'
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_HTML_Comment.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_HTML_Comment.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_HTML_Comment.py	(revision 387)
@@ -0,0 +1,17 @@
+## Script (Python) "qSEO_HTML_Comment"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters=
+##title=Generate Description from SEO properties
+##
+
+prop_name = 'qSEO_html_comment'
+
+if context.hasProperty(prop_name):
+    return context.getProperty(prop_name)
+
+# No comment by default
+return ''
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Keywords.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Keywords.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Keywords.py	(revision 387)
@@ -0,0 +1,39 @@
+## Script (Python) "listMetaTags"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters=
+##title=Generate Keywords from SEO properties
+##
+
+prop_name = 'qSEO_keywords'
+add_keywords = 'additional_keywords'
+
+keywords = []
+if context.hasProperty(prop_name):
+    keywords = context.getProperty(prop_name)
+
+pprops = context.portal_properties
+sheet = getattr(pprops, 'seo_properties', None)
+if sheet and sheet.hasProperty(add_keywords):
+    keywords += sheet.getProperty(add_keywords)
+
+if keywords:
+    return keywords
+
+accessor = 'Subject'
+
+method = getattr(context, accessor, None)
+if not callable(method):
+    # ups
+    return None
+
+# Catch AttributeErrors raised by some AT applications
+try:
+    value = method()
+except AttributeError:
+    value = None
+
+return value
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Robots.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Robots.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Robots.py	(revision 387)
@@ -0,0 +1,17 @@
+## Script (Python) "qSEO_Description"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters=
+##title=Generate Description from SEO properties
+##
+
+prop_name = 'qSEO_robots'
+
+if context.hasProperty(prop_name):
+    return context.getProperty(prop_name)
+
+# by default make robots crawl website
+return 'ALL'
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Title.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Title.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qSEO_Title.py	(revision 387)
@@ -0,0 +1,29 @@
+## Script (Python) "qSEO_Description"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind subpath=traverse_subpath
+##parameters=
+##title=Generate Description from SEO properties
+##
+
+prop_name = 'qSEO_title'
+
+if context.hasProperty(prop_name):
+    return context.getProperty(prop_name)
+
+accessor = 'Title'
+
+method = getattr(context, accessor, None)
+if not callable(method):
+    # ups
+    return None
+
+# Catch AttributeErrors raised by some AT applications
+try:
+    value = method()
+except AttributeError:
+    value = None
+
+return value
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qseo_properties_edit.cpy
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qseo_properties_edit.cpy	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qseo_properties_edit.cpy	(revision 387)
@@ -0,0 +1,59 @@
+## Controller Python Script "qseo_properties_edit"
+##bind container=container
+##bind context=context
+##bind namespace=
+##bind script=script
+##bind state=state
+##bind subpath=traverse_subpath
+##title=Update SEO Properties
+##parameters=title=None,description=None,keywords=None,html_comment=None,robots=None,distribution=None,title_override=0,description_override=0,keywords_override=0,html_comment_override=0,robots_override=0,distribution_override=0,custommetatags=[]
+
+def setProperty(context, property, value, type='string'):
+    if context.hasProperty(property):
+        context.manage_changeProperties({property: value})
+    else:
+        context.manage_addProperty(property, value, type)
+
+delete_list = []
+
+# update custom meta tags
+property_prefix = 'qSEO_custom_'
+custom_existing = []
+for property, value in context.propertyItems():
+    if property.find(property_prefix) == 0 and len(property) > len(property_prefix):
+        custom_existing.append(property)
+
+custom_updated = []
+for tag in custommetatags:
+    meta_name, meta_content = tag['meta_name'], tag['meta_content']
+    if meta_name and meta_content:
+        setProperty(context, '%s%s' % (property_prefix, meta_name), meta_content)
+        custom_updated.append('%s%s' % (property_prefix, meta_name))
+
+#add not updated custom metatags to delete list
+for tag in custom_existing:
+    if tag not in custom_updated:
+        delete_list.append(tag)
+
+setProperty(context, 'qSEO_title', title)
+setProperty(context, 'qSEO_description', description)
+setProperty(context, 'qSEO_keywords', keywords, 'lines')
+setProperty(context, 'qSEO_html_comment', html_comment)
+setProperty(context, 'qSEO_robots', robots)
+setProperty(context, 'qSEO_distribution', distribution)
+
+if not title_override:        delete_list.append('qSEO_title')
+if not description_override:  delete_list.append('qSEO_description')
+if not keywords_override:     delete_list.append('qSEO_keywords')
+if not html_comment_override: delete_list.append('qSEO_html_comment')
+if not robots_override:       delete_list.append('qSEO_robots')
+if not distribution_override: delete_list.append('qSEO_distribution')
+
+if delete_list: context.manage_delProperties(delete_list)
+
+msg ='Content SEO properties have been saved.'
+try:
+    context.plone_utils.addPortalMessage(msg)
+    return state
+except AttributeError:
+    return state.set(context=context, portal_status_message=msg)
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qseo_properties_edit.cpy.metadata
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qseo_properties_edit.cpy.metadata	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qseo_properties_edit.cpy.metadata	(revision 387)
@@ -0,0 +1,6 @@
+[validators]
+validators = 
+
+[actions]
+action.failure=redirect_to:string:qseo_properties_edit_form
+action.success=redirect_to_action:string:view
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qseo_properties_edit_form.cpt
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qseo_properties_edit_form.cpt	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qseo_properties_edit_form.cpt	(revision 387)
@@ -0,0 +1,393 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
+      lang="en"
+      metal:use-macro="here/main_template/macros/master"
+      i18n:domain="qseoptimizer">
+
+  <metal:border fill-slot="top_slot">
+    <tal:border define="dummy python:request.set('enable_border',1)" />
+  </metal:border>
+  <metal:style_head fill-slot="style_slot">
+      <style type="text/css">
+          tr.datagridwidget-empty-row {
+              display:none;
+          }
+      </style>
+  </metal:style_head>
+  <metal:javascript_head fill-slot="javascript_head_slot">
+      <script type="text/javascript"
+              tal:condition="python:exists('portal/statistics.js')"
+              tal:attributes="src string:${here/absolute_url}/statistics.js">
+      </script>
+      <script type="text/javascript"
+              tal:condition="python:exists('portal/checkkeywords.js')"
+              tal:attributes="src string:${here/absolute_url}/checkkeywords.js">
+      </script>
+      <script type="text/javascript"
+              tal:condition="python:exists('portal/custommetatags.js')"
+              tal:attributes="src string:${here/absolute_url}/custommetatags.js">
+      </script>
+  </metal:javascript_head>
+  <body>
+
+    <div metal:fill-slot="main"
+         tal:define="errors options/state/getErrors;">
+
+      <h1 i18n:translate="heading_edit_seo_properties">Edit SEO Properties</h1>
+
+      <p i18n:translate="description_edit_seo_properties">
+        This form allows you to edit html header meta tags that ape produced by 
+        Plone CMS to incrase Search Engine visibility. The values generated by
+        Plone are present as default values in input fields. To override default
+        value, check corresponding checkbox and input value itself in 
+        corresponding field.
+      </p>
+
+
+      <form name="edit_form"
+            action=""
+            method="post"
+            tal:attributes="action string:${here/getId}/${template/getId}">
+
+        <fieldset>
+
+          <legend i18n:domain="plone" i18n:translate="legend_item_properties">
+            Item Properties
+          </legend>
+
+          <div class="field">
+            <label i18n:translate="label_page_title_tag" for="title">
+              Page Title Tag
+            </label>
+
+            <div class="formHelp" i18n:translate="help_seo_title">
+              Text to be present into &lt;TITLE&gt; tag. It is displayed in 
+              browser title bar. Search engines display it as a title of the 
+              document.
+            </div>
+
+            <input type="checkbox"
+                   id="title_override"
+                   name="title_override"
+                   tabindex=""
+                   tal:attributes="checked python:test(here.hasProperty('qSEO_title'), 'on', None);
+                                   tabindex tabindex/next;"
+                   />
+
+            <label i18n:translate="label_override" for="title_override">
+              Override
+            </label>
+
+            <input type="text"
+                   id="seo_title"
+                   name="title"
+                   size="40"
+                   value=""
+                   tabindex=""
+                   tal:attributes="value request/title | here/qSEO_Title;
+                                   tabindex tabindex/next;"
+                   />
+          </div>
+
+
+          <div class="field">
+            <label i18n:translate="label_meta_description_tag" for="description">
+              Meta Description Tag
+            </label>
+
+            <div class="formHelp" i18n:translate="help_seo_description">
+              Description of the document to be displayed right below title
+              in search engine results. It is indexed and considered to be
+              more important then the page content by several SE.
+            </div>
+
+            <input type="checkbox"
+                   id="description_override"
+                   name="description_override"
+                   tabindex=""
+                   tal:attributes="checked python:test(here.hasProperty('qSEO_description'), 'on', None);
+                                   tabindex tabindex/next;"
+                   />
+
+            <label i18n:translate="label_override" for="description_override">
+              Override
+            </label>
+
+            <textarea 
+                      type="text"
+                      id="seo_description"
+                      name="description"
+                      rows="5"
+                      cols="15"
+                      tabindex=""
+                      tal:attributes="tabindex tabindex/next;"
+                      tal:content="request/description | here/qSEO_Description"
+                      ></textarea>
+
+          </div>
+            
+          <div class="field">
+            <label i18n:translate="label_meta_keywords_tag" for="keywords">
+              Meta Keywords Tag
+            </label>
+
+            <div class="formHelp" i18n:translate="help_seo_keywords">
+              Keywords of the page to be indexed with. 
+              <strong>Enter each keyword in separate line, please.</strong>
+              Though the relevance of listing meta keywords is of 
+              questionable value now it is useful to document what
+              meta keywords you are focusing on for this page - for
+              furture reference.
+            </div>
+
+            <input type="checkbox"
+                   id="keywords_override"
+                   name="keywords_override"
+                   tabindex=""
+                   tal:attributes="checked python:test(here.hasProperty('qSEO_keywords'), 'on', None);
+                                   tabindex tabindex/next;"
+                   />
+
+            <label i18n:translate="label_override" for="keywords_override">
+              Override
+            </label>
+
+            <textarea 
+                      id="seo_keywords"
+                      name="keywords:lines"
+                      rows="11"
+                      cols="15"
+                      wrap="off"
+                      tabindex=""
+                      tal:attributes="tabindex tabindex/next;"
+                      tal:define="keywords request/keywords | here/qSEO_Keywords"
+                      tal:content="python:'\n'.join(keywords)"
+                      ></textarea>
+          </div>
+
+          <div class="field">
+            <label i18n:translate="label_html_comment" for="html_commment">
+              HTML Comment
+            </label>
+
+            <div class="formHelp" i18n:translate="help_seo_html_comment">
+              Comments embedded into HTML header.
+            </div>
+
+            <input type="checkbox"
+                   id="html_comment_override"
+                   name="html_comment_override"
+                   tabindex=""
+                   tal:attributes="checked python:test(here.hasProperty('qSEO_html_comment'), 'on', None);
+                                   tabindex tabindex/next;"
+                   />
+
+            <label i18n:translate="label_override" for="html_comment_override">
+              Override
+            </label>
+
+            <textarea 
+                      id="seo_html_comment"
+                      name="html_comment"
+                      rows="5"
+                      cols="15"
+                      tabindex=""
+                      tal:attributes="tabindex tabindex/next;"
+                      tal:content="request/html_comment | here/qSEO_HTML_Comment"
+                      ></textarea>
+          </div>
+
+          <div class="field">
+            <label i18n:translate="label_robot_tag" for="robots">
+              Robot Tag
+            </label>
+
+            <div class="formHelp" i18n:translate="help_seo_robots">
+              Robots meta tag value. Allows or denies indefing, following 
+              links, etc. for spiders indexing your content.
+            </div>
+
+            <input type="checkbox"
+                   id="robots_override"
+                   name="robots_override"
+                   tabindex=""
+                   tal:attributes="checked python:test(here.hasProperty('qSEO_robots'), 'on', None);
+                                   tabindex tabindex/next;"
+                   />
+
+            <label i18n:translate="label_override" for="robots_override">
+              Override
+            </label>
+
+            <input type="text"
+                   id="seo_robots"
+                   name="robots"
+                   size="25"
+                   value=""
+                   tabindex=""
+                   tal:attributes="value request/robots | here/qSEO_Robots;
+                                   tabindex tabindex/next;"
+                   />
+          </div>
+
+          <div class="field">
+            <label i18n:translate="label_distribution_tag" for="distribution">
+              Distribution Tag
+            </label>
+
+            <div class="formHelp" i18n:translate="help_seo_distribution">
+              Distribution meta tag to designate time and area disribution
+              of the document.
+            </div>
+
+            <input type="checkbox"
+                   id="distribution_override"
+                   name="distribution_override"
+                   tabindex=""
+                   tal:attributes="checked python:test(here.hasProperty('qSEO_distribution'), 'on', None);
+                                   tabindex tabindex/next;"
+                   />
+
+            <label i18n:translate="label_override" for="distribution_override">
+              Override
+            </label>
+
+            <input type="text"
+                   id="seo_distribution"
+                   name="distribution"
+                   size="25"
+                   value=""
+                   tabindex=""
+                   tal:attributes="value request/distribution | here/qSEO_Distribution;
+                                   tabindex tabindex/next;"
+                   />
+          </div>
+
+          <div class="field"
+               tal:define="fieldName string:custommetatags;">
+            <label i18n:translate="label_custom_metatags" for="custom_metatags">
+              Custom Meta Tags
+            </label>
+
+            <div class="formHelp" i18n:translate="help_seo_custommetatags">
+              Here you can manage your custom meta tags.
+            </div>
+
+            <table class="custom-metatags-table"
+                   id="datagridwidget-table-custommetatags"
+                   style="width: 100%"
+                   tal:define="columnNames python:['meta name', 'meta content'];
+                               columnIds   python:['meta_name', 'meta_content'];
+                               fielddata context/qSEO_CustomMetaTags|python:[];">
+                <thead>
+                    <tr>
+                        <th class="discreet" style="text-align: left"
+                            tal:repeat="columnName columnNames"
+                            tal:content="columnName"
+                            i18n:translate="">Column name</th>
+                        <th />
+                        <th />
+                    </tr>
+                </thead>
+                <tbody id="datagridwidget-tbody-custommetatags">
+                    <tal:row-loop tal:repeat="row fielddata">
+                        <tr id="datagridwidget-row">
+                            <tal:vars define="fieldId string:${fieldName}_${repeat/row/number}">
+                                <td class="datagridwidget-cell"
+                                    tal:repeat="column columnIds">
+                                    <tal:cell tal:define="cell_value python:row.get(column);">
+                                        <input type="text"
+                                               id="columnId_fieldId"
+                                               value=""
+                                               name="fieldId.columnId:records"
+                                               style="width: 100%;"
+                                               tal:attributes="name string:${fieldName}.${column}:records;
+                                                               id string:${column}_${fieldId};
+                                                               value cell_value"/>
+                                    </tal:cell>
+                                </td>
+                                <td  class="datagridwidget-manipulator">
+                                    <img alt="Delete row"
+                                         onclick="customMetaTagsFunctions.removeFieldRow(this);return false"
+                                         tal:attributes="src string:${portal_url}/delete_icon.gif;" />
+                                </td>
+                                <td class="datagridwidget-hidden-data">
+                                    <input type="hidden"
+                                           tal:attributes="name string:${fieldName}.orderindex_:records;
+                                                           id string:orderindex__${fieldId};
+                                                           value repeat/row/number;" />
+                                </td>
+                            </tal:vars>
+                        </tr>
+                    </tal:row-loop>
+                    <tr id="datagridwidget-empty-row"
+                        class="datagridwidget-empty-row"
+                        tal:define="fieldId string:${fieldName}_new">
+                        <td class="datagridwidget-cell"
+                            tal:repeat="column columnIds">
+                            <span>
+                                <input type="text"
+                                       id="columnId_fieldName_new"
+                                       value=""
+                                       name="fieldName.columnId:records"
+                                       style="width: 100%;"
+                                       tal:attributes="id string:${column}_${fieldName}_new;
+                                                       name string:${fieldName}.${column}:records;" />
+                           </span>
+                        </td>
+                        <td  class="datagridwidget-manipulator">
+                            <img alt="Delete row"
+                                 onclick="customMetaTagsFunctions.removeFieldRow(this);return false"
+                                 tal:attributes="src string:${portal_url}/delete_icon.gif;" />
+                        </td>
+                        <td class="datagridwidget-hidden-data">
+                            <input type="hidden"
+                                   value="template_row_marker"
+                                   tal:attributes="name string:${fieldName}.orderindex_:records;
+                                                  id string:orderindex__${fieldId};" />
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
+
+            <input type="button"
+                   class="datagridwidget-add-button"
+                   id="datagridwidget-add-button"
+                   value="Add new row"
+                   tal:attributes="onclick string:customMetaTagsFunctions.addRow('${fieldName}')" />
+
+          </div>
+
+          <div class="formControls"
+               tal:define="process_creation request/process_creation|nothing;"
+               i18n:domain="plone">
+            <input class="context"
+                   tabindex=""
+                   type="submit"
+                   name="form.button.Save"
+                   value="Save"
+                   i18n:attributes="value"
+                   tal:attributes="tabindex tabindex/next;" />
+
+            <input class="standalone"
+                   tabindex=""
+                   type="submit"
+                   name="form.button.Cancel"
+                   value="Cancel"
+                   i18n:attributes="value"
+                   tal:attributes="tabindex tabindex/next;" />
+          </div>
+        </fieldset>
+
+        <input type="hidden" name="form.submitted" value="1" />
+
+      </form>
+
+      <div metal:use-macro="here/document_byline/macros/byline">
+        Get the byline - contains details about author and modification date.
+      </div>
+
+    </div>
+
+  </body>
+</html>
+
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qseo_properties_edit_form.cpt.metadata
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qseo_properties_edit_form.cpt.metadata	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/qseo_properties_edit_form.cpt.metadata	(revision 387)
@@ -0,0 +1,14 @@
+[default]
+title=Edit SEO Properties
+
+[security]
+View=0:Authenticated
+
+[validators]
+validators..Save=
+validators..Cancel=
+
+[actions]
+action.success..Save=traverse_to:string:qseo_properties_edit
+action.success..Cancel=redirect_to_action:string:view
+action.failure=traverse_to:string:qseo_properties_edit_form
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/statistics.js.dtml
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/statistics.js.dtml	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/skins/quintagroup.seoptimizer/statistics.js.dtml	(revision 387)
@@ -0,0 +1,105 @@
+/******************************************************************
+<dtml-with portal_properties>
+
+Word Wrapping Rules:
+
+    1. multiple spaces == one space 1
+    2. word can include "-" and "'" // [\-\']
+    3. token that not contain a-zA-Z0-9 symbols isn't a word
+    4. non-word characters inside (or on the edge of) word divide it in smaller words
+
+HTML code for text statistics: // for else fields than title in html code title will change accordingly to the field's id
+
+  <div id="title-statistics" class="statistics">
+      <span class="total-words">Total Words: <span class="value">5</span></span>
+      <span class="stop-words">Stop Words: <span class="value">2</span></span>
+      <span class="used-words">Used Words: <span class="value">3</span></span>
+      <span class="length-words">Length: <span class="value">24</span></span>
+  </div>
+
+*******************************************************************/
+
+<dtml-try>
+var stop_words = <dtml-var expr="list(seo_properties.stop_words)">;
+<dtml-except>
+var stop_words = [];
+</dtml-try>
+
+<dtml-try>
+var ids = <dtml-var expr="list(seo_properties.fields)">;
+<dtml-except>
+var ids = [];
+</dtml-try>
+
+var template = '<span class="total-words">Total Words: <span class="value">total_row</span></span> <span class="stop-words">Stop Words: <span class="value">stop_row</span></span> <span class="used-words">Used Words: <span class="value">used_row</span></span> <span class="length-words">Length: <span class="value">length_row</span></span>';
+
+var stop_dict = {};
+
+for (var j=0; word=stop_words[j]; j++) {stop_dict[word] = 1;};
+
+function countWords(data) {
+    var total = 0;
+    var stop  = 0;
+    var used  = 0;
+    var len = data.length || 0;
+    if (len != 0) {
+        data = data // replace all non-word character with space
+                   .replace(/[^a-zA-Z0-9\-\'\u2019\"\`]+/g, ' ')
+                    // replace "-" and "'" symbols if it create groups event inside of token
+                   .replace(/[\-\'\u2019\"\`]{2,}/g, ' ')
+                    // replace all non-word characters and "-", "'" if it stay at word edge
+                   .replace(/(?:^|\s+)[^a-zA-Z0-9]+|[^a-zA-Z0-9]+(?:\s+|$)/g, ' ')
+                   // strip whitespaces
+                   .replace(/^\s*(.*?)\s*$/, '$1');
+        var data_list = data.split(/[^a-zA-Z0-9\-\'\u2019\"\`]+/);
+        for (var i=0; word=data_list[i]; i++) {
+            stop += stop_dict[word.toLowerCase()] ? 1:0;
+        };
+        total = data_list.length;
+        used = total - stop;
+    };
+    return {'total': total, 'stop': stop, 'used': used, 'length': len};
+};
+
+function getHTML(source) {
+    var stats = countWords(source);
+    var html = template;
+    for (var p in stats) {
+        html = html.replace(p+'_row', stats[p]);
+    };
+    return html;
+};
+
+function listenField(event) {
+    var event = event ? event:window.event;
+    var target = null;
+    if (event.target) {
+        target = event.target;
+    } else if (event.srcElement) {
+        target = event.srcElement;
+    };
+    if (!target) {return false;};
+    var update = document.getElementById(target.id+'-statistics');
+    if (update) {
+        update.innerHTML = getHTML(target.value||'');
+    } else {
+        window.status = "Couldn\'t find element with id = \'"+target.id+"-statistics\' on this page";
+    };
+};
+
+function loadStatistics(event){
+    for (var i=0; id=ids[i]; i++) {
+        var el = document.getElementById(id);
+        if (el && (typeof(el.value) != 'undefined')) {
+            var div = document.createElement('DIV');
+            div.id = id+'-statistics';
+            div.className = 'statistics';
+            div.innerHTML = getHTML(el.value||'');
+            el.parentNode.insertBefore(div, el);
+            registerEventListener(el, 'keyup', listenField);
+        };
+    };
+};
+
+registerPloneFunction(loadStatistics);
+/*</dtml-with> Register onload function */
Index: /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/util.py
===================================================================
--- /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/util.py	(revision 387)
+++ /quintagroup.seoptimizer/trunk/quintagroup/seoptimizer/util.py	(revision 387)
@@ -0,0 +1,25 @@
+from Globals import InitializeClass
+from AccessControl import ClassSecurityInfo
+
+def createMultiColumnList(self,slist, numCols, sort_on='title_or_id'):
+    try:
+        mcl = self.createMultiColumnList(slist, numCols, sort_on=sort_on)
+        return mcl
+    except AttributeError:
+        return [slist]
+
+class SortedDict(dict):
+    security = ClassSecurityInfo()
+    security.declarePublic('items')    
+    def items(self):
+        primary_metatags = ['description', 'keywords']
+        lst = [(name,self[name]) for name in primary_metatags                    \
+                                                 if name in self.keys()] +       \
+              [(name, self[name]) for name in self.keys()                        \
+                                                 if name not in primary_metatags]
+        return lst
+
+try:
+    InitializeClass(SortedDict)
+except:
+    pass
