diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e83ae97..7510505 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,42 +2,79 @@ name: tests on: push: - branches: [ main ] + branches: [ master, python3 ] pull_request: # Allow to run this workflow manually from the Actions tab workflow_dispatch: jobs: build: + runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - config: - # [Python version, tox env] - - ["2.7", "plone52-py27"] - - ["3.6", "plone52-py36"] - - ["3.7", "plone52-py37"] - - ["3.8", "plone52-py38"] - - ["3.7", "plone60-py37"] - - ["3.8", "plone60-py38"] - - ["3.9", "plone60-py39"] - runs-on: ubuntu-latest - name: ${{ matrix.config[1] }} + plone-version: + # - '5.2' + - '6.0' + python-version: + # - '2.7' + # - '3.7' + # - '3.8' + - '3.9' + # exclude: + # - plone-version: '5.2' + # python-version: '3.9' + # - plone-version: '6.0' + # python-version: '2.7' steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.config[0] }} - - name: Pip cache - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ matrix.config[0] }}-${{ hashFiles('setup.*', 'tox.ini') }} - restore-keys: | - ${{ runner.os }}-pip-${{ matrix.config[0] }}- - ${{ runner.os }}-pip- - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox - - name: Test - run: tox -e ${{ matrix.config[1] }} + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + sudo apt-get install -y libxml2-dev libxslt-dev python-dev xvfb + pip install virtualenv wheel + - uses: nanasess/setup-chromedriver@master + - name: Cache multiple paths + uses: actions/cache@v2 + with: + path: | + ~/buildout-cache + ~/extends + ~/.cache/pip + key: ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.plone-version }}-${{ hashFiles('**/*.cfg') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.plone-version }}- + ${{ runner.os }}-${{ matrix.python-version }}- + ${{ runner.os }}- + - name: setup buildout cache + run: | + mkdir -p ~/buildout-cache/{eggs,downloads} + mkdir ~/.buildout + echo "[buildout]" > $HOME/.buildout/default.cfg + echo "download-cache = $HOME/buildout-cache/downloads" >> $HOME/.buildout/default.cfg + echo "eggs-directory = $HOME/buildout-cache/eggs" >> $HOME/.buildout/default.cfg + - name: bootstrap buildout py2 + if: ${{ matrix.python-version == '2.7'}} + run: | + virtualenv . + bin/pip install -r requirements.txt + - name: bootstrap buildout py3 + if: ${{ matrix.python-version != '2.7'}} + run: | + python -m venv . + bin/pip install -r requirements.txt + - name: buildout + run: | + bin/buildout -t 10 -Nc test-${{ matrix.plone-version }}.x.cfg + bin/pip install zest.pocompile + bin/pocompile src + - name: test + run: | + export DISPLAY=:99 + chromedriver --url-base=/wd/hub & + sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & + sleep 2 + PATH=$PATH:$PWD/chromedriver ROBOT_BROWSER=chrome ROBOT_HTTP_PORT=55001 bin/test + cat /home/runner/work/Products.PasswordStrength/Products.PasswordStrength/parts/test/password/Test_register_form/output.xml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4e9997c..0000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: - python -python: - 2.7 -env: - - PLONE_VERSION=4.3 - - PLONE_VERSION=5.0 -matrix: - include: - - python: 2.6 - env: PLONE_VERSION=4.2 - - python: 2.6 - env: PLONE_VERSION=4.1 - -install: - - sed -ie "s#travis-4.x.cfg#travis-$PLONE_VERSION.x.cfg#" travis.cfg - - sed -ie "s#buildout.cfg#buildout-$PLONE_VERSION.cfg#" travis.cfg - - mkdir -p buildout-cache/{downloads,eggs} - - python bootstrap.py -c travis.cfg - - bin/buildout -c travis.cfg -N -before_script: - - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" -script: - bin/test -after_success: - - test -v QA && bin/coverage.sh && pip install -q coveralls && coveralls || true diff --git a/CHANGES.rst b/CHANGES.rst index bc5b3a5..60926d4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,9 +1,12 @@ Changes ======= -0.4.1 (unreleased) +0.5.0 (unreleased) ------------------ +- Add support for Python 3, Plone 5.2 and Plone 6. Drop support for Plone 5.1 and older. + [pbauer, djay] + - Fix can not change weak password [ivanteoh] diff --git a/MANIFEST.in b/MANIFEST.in index cfba368..6a5d6cc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include *.txt *.rst recursive-include Products *.zcml *.xml *.txt -recursive-include Products *.cpt *.metadata *.zpt +recursive-include Products *.cpt *.metadata *.zpt *.pt recursive-include Products *.po *.pot recursive-include Products *.robot exclude bootstrap.py *.cfg *.yml diff --git a/Products/PasswordStrength/__init__.py b/Products/PasswordStrength/__init__.py index d357fa1..130e9f4 100644 --- a/Products/PasswordStrength/__init__.py +++ b/Products/PasswordStrength/__init__.py @@ -8,7 +8,7 @@ from Products.PluggableAuthService import registerMultiPlugin from zope.i18nmessageid import MessageFactory _ = MessageFactory("Products.PasswordStrength") -from plugin import PasswordStrength, manage_addPasswordStrength, manage_addPasswordStrengthForm +from Products.PasswordStrength.plugin import PasswordStrength, manage_addPasswordStrength, manage_addPasswordStrengthForm def initialize(context): diff --git a/Products/PasswordStrength/backward.py b/Products/PasswordStrength/backward.py deleted file mode 100644 index bc2eb54..0000000 --- a/Products/PasswordStrength/backward.py +++ /dev/null @@ -1,69 +0,0 @@ -from Products.PluggableAuthService.interfaces.plugins import IValidationPlugin -from Products.CMFCore.utils import getToolByName -from Products.PasswordStrength import _ -from plone.app.users.browser.register import BaseRegistrationForm -try: - from plone.app.users.browser.personalpreferences import PasswordAccountPanel as PasswordPanel -except: - try: - from plone.app.users.browser.passwordpanel import PasswordPanel - except: - PasswordPanel = None - -# monkey patch to be used for Plone < 4.3 -def testPasswordValidity(self, password, confirm=None): - - """ Verify that the password satisfies the portal's requirements. - - o If the password is valid, return None. - o If not, return a string explaining why. - """ - if confirm is not None and confirm != password: - return _('Your password and confirmation did not match. Please try again.') - - if not password: - err = [_('You must enter a password')] - else: - err = [] - - # We escape the test if it looks like a generated password - if password and password.startswith('G-') and len(password) == len(self.origGeneratePassword()) + 2: - return None - - # Use PAS to test validity - pas_instance = self.acl_users - plugins = pas_instance._getOb('plugins') - validators = plugins.listPlugins(IValidationPlugin) - for validator_id, validator in validators: - user = None - set_id = '' - set_info = {'password': password} - errors = validator.validateUserInfo(user, set_id, set_info) - err += [info['error'] for info in errors if info['id'] == 'password'] - if err: - return '. '.join(err) - else: - return None - - -# monkey patch to be used for Plone < 4.3 -def updateRegistrationForm(self): - # Change the password description based on PAS Plugin - # The user needs a list of instructions on what kind of password is required. - # We'll reuse password errors as instructions e.g. "Must contain a letter and a number". - # Assume PASPlugin errors are already translated - if self.form_fields and self.form_fields.get('password', None): - registration = getToolByName(self.context, 'portal_registration') - err_str = registration.testPasswordValidity('') - if err_str: - self.form_fields['password'].field.description = err_str - super(BaseRegistrationForm, self).update() - -# for 4.3 and 5.0 -def updatePasswordAccountPanel(self): - if self.form_fields and self.form_fields.get('new_password', None): - registration = getToolByName(self.context, 'portal_registration') - err_str = registration.testPasswordValidity('') - if err_str: - self.form_fields['new_password'].field.description = err_str - super(PasswordPanel, self).update() diff --git a/Products/PasswordStrength/configure.zcml b/Products/PasswordStrength/configure.zcml index 90b2dc5..0db2da3 100644 --- a/Products/PasswordStrength/configure.zcml +++ b/Products/PasswordStrength/configure.zcml @@ -1,61 +1,22 @@ - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - + - + diff --git a/Products/PasswordStrength/interfaces.py b/Products/PasswordStrength/interfaces.py new file mode 100644 index 0000000..877b18e --- /dev/null +++ b/Products/PasswordStrength/interfaces.py @@ -0,0 +1,7 @@ +"""Module where all interfaces, events and exceptions live.""" + +from zope.publisher.interfaces.browser import IDefaultBrowserLayer + + +class IPasswordStrengthLayer(IDefaultBrowserLayer): + """Marker interface that defines a browser layer.""" diff --git a/Products/PasswordStrength/locales/de/LC_MESSAGES/Products.PasswordStrength.po b/Products/PasswordStrength/locales/de/LC_MESSAGES/Products.PasswordStrength.po new file mode 100644 index 0000000..8cab054 --- /dev/null +++ b/Products/PasswordStrength/locales/de/LC_MESSAGES/Products.PasswordStrength.po @@ -0,0 +1,71 @@ +msgid "" +msgstr "" +"Project-Id-Version: Products.PasswordStrength\n" +"POT-Creation-Date: 2015-07-03 13:29+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" +"Last-Translator: Steffen Lindner \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0\n" +"Language-Code: de\n" +"Language-Name: Germany\n" +"Preferred-Encodings: utf-8 latin1\n" +"Domain: Products.PasswordStrength\n" + +#. Default: "Add" +#: ../www/passwordStrengthAdd.zpt:37 +msgid " Add " +msgstr " Hinzufügen " + +#: ../www/passwordStrengthAdd.zpt:6 +msgid "A plugin that rejects passwords that don't conform to a certain strength" +msgstr "Ein Plugin welches keine unsichere Passwörter zulässt." + +#: ../plugin.py:31 +msgid "Create your own rules for enforcing password strength" +msgstr "Erstelle deine eigenen Passwort regeln" + +#: ../plugin.py:79 +msgid "Minimum 1 capital letter" +msgstr "Mindestens ein Grossbuchstabe" + +#: ../plugin.py:80 +msgid "Minimum 1 lower case letter" +msgstr "Mindestens ein Kleinbuchstabe" + +#: ../plugin.py:82 +msgid "Minimum 1 non-alpha character" +msgstr "Mindestens ein Sonderzeichen" + +#: ../plugin.py:81 +msgid "Minimum 1 number" +msgstr "Mindestens eine Nummer" + +#: ../plugin.py:80 +msgid "Minimum 10 characters" +msgstr "Mindestens 10 Zeichen" + +#. Default: "PAS plugin that ensures strong passwords." +#: ../configure.zcml +msgid "PAS plugin that ensures strong passwords." +msgstr "Module PAS proposant une politique forte des mots de passes" + +#. Default: "Password Strength Plugin" +#: ../configure.zcml +msgid "Password Strength Plugin" +msgstr "Module Password Strength" + +#: ../plugin.py:207 +msgid "This password doesn't match requirements for passwords" +msgstr "Dieses Passwort entspricht nicht den Passwort Vorgaben" + +#: ./backward.py:20 +msgid "You must enter a password" +msgstr "Sie müssen ein Passwort eingeben" + +#: ./backward.py:17 +msgid "Your password and confirmation did not match. Please try again." +msgstr "Dein Passwort und deine Bestätigung stimmen nicht überein. Bitte versuch es nochmal." + diff --git a/Products/PasswordStrength/overrides/Products.CMFPlone.browser.login.templates.pwreset_form.pt b/Products/PasswordStrength/overrides/Products.CMFPlone.browser.login.templates.pwreset_form.pt new file mode 100644 index 0000000..1f9f55e --- /dev/null +++ b/Products/PasswordStrength/overrides/Products.CMFPlone.browser.login.templates.pwreset_form.pt @@ -0,0 +1,141 @@ + + + + + Set your password + + + + + Please fill out the form below to set your password. + + + + + + + + + New Password + + + + + + My user name is + + + + Validation error output + + + Enter your user name for verification. + + + + + My email address is + + + + Validation error output + + + Enter your email address for verification. + + + + + + + + New password + + + + Validation error output + + + Enter your new password. + + + + + + + Confirm password + + + + Validation error output + + + Re-enter the password. Make sure the passwords are identical. + + + + + + + + + + Set my password + + + + + + + + + + + diff --git a/Products/PasswordStrength/plugin.py b/Products/PasswordStrength/plugin.py index af4cd56..6edc7b6 100644 --- a/Products/PasswordStrength/plugin.py +++ b/Products/PasswordStrength/plugin.py @@ -4,23 +4,23 @@ __author__ = "Dylan Jay " -import logging - from AccessControl import ClassSecurityInfo -from Globals import InitializeClass +from AccessControl.class_init import InitializeClass from OFS.Cache import Cacheable - -from Products.CMFPlone.RegistrationTool import RegistrationTool +from plone.api import portal from Products.CMFPlone import PloneMessageFactory as _p -from . import _ +from Products.CMFPlone.utils import safe_unicode +from Products.CMFPlone.RegistrationTool import RegistrationTool +from Products.PageTemplates.PageTemplateFile import PageTemplateFile +from Products.PasswordStrength import _ +from Products.PluggableAuthService.interfaces.plugins import IValidationPlugin from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin from Products.PluggableAuthService.utils import classImplements -from Products.PluggableAuthService.interfaces.plugins import IValidationPlugin -import re from zope.i18n import translate -from plone.api import portal -from Products.PageTemplates.PageTemplateFile import PageTemplateFile +import logging +import re + try: from plone.app.users.browser.personalpreferences import IPasswordSchema except: @@ -47,7 +47,7 @@ def testPasswordValidity(self, password, confirm=None): # We escape the test if it looks like a generated password (with default length of 56 chars) if password is not None and password.startswith('G-') and len(password) == len(self.origGeneratePassword()) + 2: return None - session = self.REQUEST.get('SESSION', {}) + session = self.REQUEST.get('SESSION', {}) or {} # We also skip the test if a skip_password_check session variable is set if password is not None and session.get('skip_password_check'): return None @@ -189,7 +189,7 @@ def validateUserInfo(self, user, set_id, set_info): if not re.match(reg, password): err = getattr(self, 'p%i_err' % i, None) if err: - errors += [translate(err.decode('utf8'), domain='Products.PasswordStrength', + errors += [translate(safe_unicode(err), domain='Products.PasswordStrength', context=site.REQUEST)] i += 1 @@ -224,7 +224,8 @@ def validate(self, value): return skip = False - # Do not validate existing passwords + + # Do not validate old password when changing passwords as logged in user if getattr(self, '__name__', '') == 'current_password': skip = True diff --git a/Products/PasswordStrength/profiles.zcml b/Products/PasswordStrength/profiles.zcml index 6e2e260..0458a6c 100644 --- a/Products/PasswordStrength/profiles.zcml +++ b/Products/PasswordStrength/profiles.zcml @@ -9,6 +9,7 @@ directory="profiles/default" description="PAS plugin that ensures strong passwords." provides="Products.GenericSetup.interfaces.EXTENSION" + post_handler=".setuphandlers.setupPasswordStrength" /> @@ -19,6 +20,7 @@ description='Uninstall profile for the "PasswordStrength" Plone theme.' provides="Products.GenericSetup.interfaces.EXTENSION" for="Products.CMFCore.interfaces.ISiteRoot" + post_handler=".setuphandlers.removePasswordStrength" /> diff --git a/Products/PasswordStrength/profiles/default/browserlayer.xml b/Products/PasswordStrength/profiles/default/browserlayer.xml new file mode 100644 index 0000000..25c9f48 --- /dev/null +++ b/Products/PasswordStrength/profiles/default/browserlayer.xml @@ -0,0 +1,7 @@ + + + + diff --git a/Products/PasswordStrength/profiles/default/import_steps.xml b/Products/PasswordStrength/profiles/default/import_steps.xml deleted file mode 100644 index ae2e514..0000000 --- a/Products/PasswordStrength/profiles/default/import_steps.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - PasswordStrength installation step. - - diff --git a/Products/PasswordStrength/profiles/default/metadata.xml b/Products/PasswordStrength/profiles/default/metadata.xml index f341fc6..b22370d 100644 --- a/Products/PasswordStrength/profiles/default/metadata.xml +++ b/Products/PasswordStrength/profiles/default/metadata.xml @@ -1,4 +1,4 @@ - 0.4 + 1000 diff --git a/Products/PasswordStrength/profiles/default/passwordstrength.txt b/Products/PasswordStrength/profiles/reset/.gitkeep similarity index 100% rename from Products/PasswordStrength/profiles/default/passwordstrength.txt rename to Products/PasswordStrength/profiles/reset/.gitkeep diff --git a/Products/PasswordStrength/profiles/reset/import_steps.xml b/Products/PasswordStrength/profiles/reset/import_steps.xml deleted file mode 100644 index 5a8532b..0000000 --- a/Products/PasswordStrength/profiles/reset/import_steps.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - PasswordStrength: users password reset. - - diff --git a/Products/PasswordStrength/profiles/reset/passwordstrength_reset.txt b/Products/PasswordStrength/profiles/reset/passwordstrength_reset.txt deleted file mode 100644 index e69de29..0000000 diff --git a/Products/PasswordStrength/profiles/uninstall/browserlayer.xml b/Products/PasswordStrength/profiles/uninstall/browserlayer.xml new file mode 100644 index 0000000..46e23d6 --- /dev/null +++ b/Products/PasswordStrength/profiles/uninstall/browserlayer.xml @@ -0,0 +1,7 @@ + + + + diff --git a/Products/PasswordStrength/profiles/uninstall/import_steps.xml b/Products/PasswordStrength/profiles/uninstall/import_steps.xml deleted file mode 100644 index dbc02ee..0000000 --- a/Products/PasswordStrength/profiles/uninstall/import_steps.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - PasswordStrength uninstallation step. - - diff --git a/Products/PasswordStrength/profiles/uninstall/passwordstrength-uninstall.txt b/Products/PasswordStrength/profiles/uninstall/passwordstrength-uninstall.txt deleted file mode 100644 index e69de29..0000000 diff --git a/Products/PasswordStrength/setuphandlers.py b/Products/PasswordStrength/setuphandlers.py index fa95767..5400f74 100644 --- a/Products/PasswordStrength/setuphandlers.py +++ b/Products/PasswordStrength/setuphandlers.py @@ -1,23 +1,20 @@ import logging from smtplib import SMTPException -from StringIO import StringIO +from six import StringIO from Products.CMFCore.utils import getToolByName +from plone import api from Products.PasswordStrength.plugin import PROJECTNAME, PLUGIN_ID, PLUGIN_TITLE PLONE_POLICY = 'password_policy' logger = logging.getLogger('Products.PasswordStrength: setuphandlers') def setupPasswordStrength(context): - if context.readDataFile('passwordstrength.txt') is None: - return - site = context.getSite() + site = api.portal.get() install(site) def removePasswordStrength(context): - if context.readDataFile('passwordstrength-uninstall.txt') is None: - return - site = context.getSite() + site = api.portal.get() uninstall(site) @@ -28,7 +25,7 @@ def install(portal): Different interfaces need to be activated for either case. """ out = StringIO() - print >> out, "Installing %s:" % PROJECTNAME + print("Installing %s:" % PROJECTNAME, file=out) plone_pas = getToolByName(portal, 'acl_users') @@ -42,13 +39,13 @@ def install(portal): activatePluginSelectedInterfaces(plone_pas, PLONE_POLICY, out, 'IValidationPlugin', disable=['IValidationPlugin']) - print >> out, "Successfully installed %s." % PROJECTNAME + print("Successfully installed %s." % PROJECTNAME, file=out) return out.getvalue() def uninstall(portal): out = StringIO() - print >> out, "Uninstalling %s:" % PROJECTNAME + print("Uninstalling %s:" % PROJECTNAME, file=out) plone_pas = getToolByName(portal, 'acl_users') existing = plone_pas.objectIds() if PLUGIN_ID in existing: @@ -75,12 +72,12 @@ def activatePluginSelectedInterfaces(pas, plugin, out, selected_interfaces, disa interface_name in selected_interfaces: if interface_name in disable: disable.append(interface_name) - print >> out, " - Disabling: " + info['title'] + print(" - Disabling: " + info['title'], file=out) else: activatable.append(interface_name) - print >> out, " - Activating: " + info['title'] + print(" - Activating: " + info['title'], file=out) plugin_obj.manage_activateInterfaces(activatable) - print >> out, plugin + " activated." + print(plugin + " activated.", file=out) def list_append(lst, elem): @@ -95,9 +92,7 @@ def reset_passwords(context): """ Reset all users passwords """ - if context.readDataFile('passwordstrength_reset.txt') is None: - return - portal = context.getSite() + portal = api.porta.get() regtool = portal.portal_registration logs = [] logger.info(list_append(logs, "Resetting all users passwords")) diff --git a/Products/PasswordStrength/skins.zcml b/Products/PasswordStrength/skins.zcml deleted file mode 100644 index d6821db..0000000 --- a/Products/PasswordStrength/skins.zcml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/Products/PasswordStrength/skins/PasswordStrength_public/README.txt b/Products/PasswordStrength/skins/PasswordStrength_public/README.txt deleted file mode 100644 index cdc4c1b..0000000 --- a/Products/PasswordStrength/skins/PasswordStrength_public/README.txt +++ /dev/null @@ -1,7 +0,0 @@ -Overrides are as follows - -- - Enter your new password. Minimum 5 characters. - -+ - - - - Set your password - - - Please fill out the form below to set your password. - - - - - - New Password - - - - - - - My user name is - Validation error output - - - Enter your user name for verification. - - - - My email address is - Validation error output - - - Enter your email address for verification. - - - - - - - - - New password - Validation error output - - - Enter your new password. - - - - - - - - Confirm password - Validation error output - - - Re-enter the password. Make sure the passwords are identical. - - - - - - - - - - - - - - - - - - -