diff --git a/.codecov.yml b/.codecov.yml
index e0d835df0..40663c7f5 100644
--- a/.codecov.yml
+++ b/.codecov.yml
@@ -9,6 +9,5 @@ ignore:
- "roundup/cgi/TAL/talgettext.py" # utility command
- "roundup/cgi/TAL/DummyEngine.py" # test harness
- "roundup/cgi/apache.py" # mod_python is deprecated
- - "roundup/install_util.py" # another build utlity
- "roundup/dist" # more build utils
- "roundup/test" # code used for testing
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index fc75e2ae2..79414b360 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -18,6 +18,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
+ cooldown:
+ default-days: 7
# - package-ecosystem: "docker"
# directory: "/scripts/Docker"
# target-branch: "master"
diff --git a/.github/workflows/anchore.yml b/.github/workflows/anchore.yml
index 7eff99f17..7f62ff8d8 100644
--- a/.github/workflows/anchore.yml
+++ b/.github/workflows/anchore.yml
@@ -37,22 +37,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the code
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ persist-credentials: false
- name: Build the Docker image
run: docker pull python:3-alpine; docker build . --file scripts/Docker/Dockerfile --tag localbuild/testimage:latest
- name: List the Docker image
run: docker image ls
- name: Run the Anchore scan action itself with GitHub Advanced Security code scanning integration enabled
- uses: anchore/scan-action@1638637db639e0ade3258b51db49a9a137574c3e # 6.5.1
+ uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0
id: scan
with:
image: "localbuild/testimage:latest"
fail-build: true
- name: Upload Anchore Scan Report
if: always()
- uses: github/codeql-action/upload-sarif@b1e4dc3db58c9601794e22a9f6d28d45461b9dbf # v2.22.0
+ uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
sarif_file: ${{ steps.scan.outputs.sarif }}
- name: Inspect action SARIF report
if: always()
- run: cat ${{ steps.scan.outputs.sarif }}
+ run: cat ${STEPS_SCAN_OUTPUTS_SARIF}
+ env:
+ STEPS_SCAN_OUTPUTS_SARIF: ${{ steps.scan.outputs.sarif }}
diff --git a/.github/workflows/build-xapian.yml b/.github/workflows/build-xapian.yml
index 4d6edf6dc..318683529 100644
--- a/.github/workflows/build-xapian.yml
+++ b/.github/workflows/build-xapian.yml
@@ -42,11 +42,13 @@ jobs:
# if: {{ false }}
# continue running if step fails
# continue-on-error: true
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ persist-credentials: false
# Setup version of Python to use
- name: Set Up Python 3.13
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: 3.13
allow-prereleases: true
@@ -69,7 +71,7 @@ jobs:
# allow remote ssh into the CI container. I need this to debug
# some xfail cases
- name: Setup tmate session
- uses: mxschmitt/action-tmate@v3
+ uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3.24
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
timeout-minutes: 60
with:
diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml
index 8e88da76c..bb2962126 100644
--- a/.github/workflows/ci-test.yml
+++ b/.github/workflows/ci-test.yml
@@ -45,6 +45,12 @@ jobs:
if: "!contains(github.event.head_commit.message, 'no-github-ci')"
+ # run the finalizer for coveralls even if one or more
+ # experimental matrix runs fail.
+ # moving it above strategy produces unexpected value false
+ # moving it below (here) produces unexpected value ''.
+ continue-on-error: ${{ matrix.experimental }}
+
strategy:
fail-fast: false
max-parallel: 4
@@ -52,18 +58,14 @@ jobs:
# Run in all these versions of Python
python-version:
# - "2.7"
+ - "3.14"
+ - "3.14t"
- "3.13"
- # - 3.6 run via include on ubuntu 20.04
- # - "3.7"
- # - "3.8" run via include for ubuntu-22.04
- # - "3.9"
- - "3.10"
- # - "3.11"
- "3.12"
+ - "3.10"
# use for multiple os or ubuntu versions
#os: [ubuntu-latest, macos-latest, windows-latest]
- # ubuntu latest 22.04 12/2022
# ubuntu latest 24.04 12/2024
os: [ubuntu-latest]
@@ -74,34 +76,22 @@ jobs:
# example: if this version fails the jobs still succeeds
# allow-prereleases in setup-python allows alpha/beta
# releases to run. Also allow free threaded python testing
- - python-version: 3.13t
+ - python-version: 3.15-dev
os: ubuntu-24.04
experimental: true
-
- # 3.7 not available on new 22.04 runners, so run on 22.04 ubuntu
- - python-version: 3.7
- os: ubuntu-22.04
-
- # 3.8 not available on new 24.04 runners, so run on 22.04 ubuntu
- - python-version: 3.8
- os: ubuntu-22.04
+ - python-version: 3.15t-dev
+ os: ubuntu-24.04
+ experimental: true
#exclude:
# skip all python versions on explicit 20.04/24.04 unless
# explicitly included
- #- os: ubuntu-20.04
#- os: ubuntu-24.04
# disable when testing ubuntu-24.04 to speed up
# runs.
# - os: ubuntu-latest
- # run the finalizer for coveralls even if one or more
- # experimental matrix runs fail.
- # moving it above strategy produces unexpected value false
- # moving it below (here) produces unexpected value ''.
- # continue-on-error: ${{ matrix.experimental }}
-
env:
# get colorized pytest output even without a controlling tty
PYTEST_ADDOPTS: "--color=yes"
@@ -116,11 +106,13 @@ jobs:
# if: {{ false }}
# continue running if step fails
# continue-on-error: true
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ persist-credentials: false
# Setup version of Python to use
- name: Set Up Python ${{ matrix.python-version }}
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
@@ -146,16 +138,18 @@ jobs:
- name: Update pip
run: python -m pip install --upgrade pip
- # note pytest-env is not needed for redis password as there is
- # no password on CI's redis.
+ # note pytest-env is not required for redis password as there is
+ # no password on CI's redis. But pytest logs a warning if it's
+ # missing. pytest-subtests isn't needed with
+ # pytest 9.0.0+, but 9.0 only supports 3.10+ so include for 3.7+.
- name: Install pytest and other packages needed for running tests
- run: pip install flake8 hypothesis mock pytest pytest-cov requests sphinx-tabs
+ run: pip install flake8 hypothesis mock pytest pytest-cov pytest-env pytest-subtests requests sphinx-tabs
# https://github.com/mxschmitt/action-tmate
# allow remote ssh into the CI container. I need this to debug
# some xfail cases
- name: Setup tmate session
- uses: mxschmitt/action-tmate@v3
+ uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3.24
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
timeout-minutes: 10
with:
@@ -228,7 +222,7 @@ jobs:
# allow remote ssh into the CI container. I need this to debug
# some xfail cases
- name: Setup tmate session
- uses: mxschmitt/action-tmate@v3
+ uses: mxschmitt/action-tmate@35b54afac29c97fb54faba5b513f8fbd1882f113 # v3.24
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
timeout-minutes: 10
with:
@@ -240,23 +234,19 @@ jobs:
# pygments for markdown2 to highlight code blocks
pip install markdown2 pygments
# docutils for ReStructuredText
- pip install beautifulsoup4 brotli docutils jinja2 \
+ pip install beautifulsoup4 brotli docutils gpg jinja2 \
mistune==0.8.4 pyjwt pytz whoosh
- # gpg on PyPi is currently broken with newer OS platform
- # ubuntu 24.04
- # used for newer Python versions. Temporarily use the
- # testing index, which contains a newer version of the
- # bindings on 24.04 or released version for other OS
- # versions. See issue2551368. 'pip install gpg' should work
- # at some point when things are released to the production repo.
- pip install --index-url https://test.pypi.org/simple/ \
- --extra-index-url https://pypi.org/simple gpg;
- name: Install aux packages that need versions differences
# if zstd fails install, keep going with test, don't abort
run: |
set -xv
pip install zstd || true
+ # justhtml supports python 3.10 or newer according to pypi
+ # page, but will install on 3.8 (and maybe 3.9) but formats
+ # differently. So skip install if before 3.10.
+ if echo $PYTHON_VERSION | grep '^3\...'; then
+ pip install justhtml || true; fi
if [[ "$PYTHON_VERSION" != "2."* ]]; then
pip install Markdown; fi
@@ -311,7 +301,7 @@ jobs:
-W "ignore:'U' mode::docutils.io" \
-W "ignore:unclosed:ResourceWarning:roundup.roundup.demo" \
-W "ignore:unclosed file:ResourceWarning:enum" \
- -v test/ --cov=roundup
+ -v test/ --cov=roundup
coverage lcov
else
# python2 case
@@ -323,7 +313,7 @@ jobs:
- name: Upload coverage to Codecov
# see: https://github.com/codecov/codecov-action#usage
- uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
@@ -331,7 +321,7 @@ jobs:
- name: Upload coverage to Coveralls
# python 2.7 and 3.6 versions of coverage can't produce lcov files.
if: matrix.python-version != '2.7' && matrix.python-version != '3.6'
- uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6
+ uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # v2.3.7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: coverage.lcov
@@ -367,7 +357,7 @@ jobs:
steps:
- name: Coveralls Finished
- uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6
+ uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # v2.3.7
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 0b214cc78..918184dc3 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -49,11 +49,13 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@b1e4dc3db58c9601794e22a9f6d28d45461b9dbf # v2.22.0
+ uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -64,7 +66,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@b1e4dc3db58c9601794e22a9f6d28d45461b9dbf # v2.22.0
+ uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -78,4 +80,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@b1e4dc3db58c9601794e22a9f6d28d45461b9dbf # v2.22.0
+ uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml
index c6a5547e3..fcd4b7926 100644
--- a/.github/workflows/ossf-scorecard.yml
+++ b/.github/workflows/ossf-scorecard.yml
@@ -35,12 +35,12 @@ jobs:
steps:
- name: "Checkout code"
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: "Run analysis"
- uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v5.2.1
+ uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
@@ -62,7 +62,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SARIF file
path: results.sarif
@@ -70,6 +70,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@b1e4dc3db58c9601794e22a9f6d28d45461b9dbf # v2.22.0
+ uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
sarif_file: results.sarif
diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml
new file mode 100644
index 000000000..0c05bf6b0
--- /dev/null
+++ b/.github/workflows/zizmor.yml
@@ -0,0 +1,23 @@
+name: GitHub Actions Security Analysis with zizmor
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+ branches: ["**"]
+
+permissions: {}
+
+jobs:
+ zizmor:
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ persist-credentials: false
+
+ - name: Run zizmor
+ uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
diff --git a/.hgtags b/.hgtags
index e0d4f7130..4a5f11fac 100644
--- a/.hgtags
+++ b/.hgtags
@@ -156,3 +156,5 @@ c10df5dfed95dc37e8092861886bf66614b77062 2.4.0b1
4dfc07ee489ad000c8ffe7ee97b506c94fae6774 2.4.0
3bf6ad421347c87ae3a6bebe288f8f1dfa7bd6ba 2.5.0b1
c7a2e01793cdb5228d44df085e3b2f8a73e51c28 2.5.0
+174e6163e178598edd2c33c8c051aa03bd3dbf04 2.6.0b1
+d7abd2cf2be584e4869770dfc616c31d249d8b9e 2.6.0
diff --git a/CHANGES.txt b/CHANGES.txt
index 122e83527..833178217 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -6,14 +6,10 @@ and template changes not listed here.
Each entry has the developer who committed the change in brackets.
Many entries without name were done by Richard Jones.
-**IMPORTANT** The v1.5.x releases of Roundup were the last to support
-Python v2.5 and v2.6. Starting with the v1.6 releases of Roundup
-v2.7.2 or later are required to run newer releases of Roundup.
-Roundup 2.0 supports Python 3.4 and later. Roundup 2.1.0 supports
-python 3.6 or newer (3.4/3.5 might work, but they are not tested).
-Roundup 2.4.0 is the last release to support Python 2.
+**IMPORTANT** Roundup 2.6 supports Python 3.10 or newer.
+Roundup 2.4.0 was the last release to support Python 2.
-2026-XX-XX 2.6.0
+2026-07-13 2.6.0
Fixed:
@@ -23,12 +19,161 @@ Fixed:
roundup.cgi.exceptions. Also it now inherits from HTTPException
rather than Exception since it is an HTTP exception. (John
Rouillard)
+- cleaned up repo. Close obsolete branches and close a split head due
+ to an identical merge in two different working copies. (John
+ Rouillard)
+- in roundup-admin, using 'pragma history_length interactively now
+ sets readline history length. Using -P history_length=10 on the
+ command line always worked. (John Rouillard)
+- enhanced error reporting for errors in ini style logging
+ configuration. (John Rouillard)
+- fix bogus javascript emitted by user_src_input macro. (John
+ Rouillard)
+- replaced hostname localhost with 127.0.0.1 in docker healthcheck
+ script. Found/patch by Norbert Schlemmer. (John Rouillard)
+- change some internal classes to use __slots__ for hopefully a small
+ performance improvement. (John Rouillard)
+- issue2551413 - Broken MultiLink columns in CSV export. CSV export of
+ a multilink link "messages" that does not have a 'name' property
+ causes a crash. (found/fix by cmeerw; commit and better handling of
+ non-labeled multilink by John Rouillard)
+- in cgi/client.py, set self.language attribute when translator passed
+ into Client(). (John Rouillard)
+- issue2551393 - Named searches lose their name in title when next
+ page is selected. (John Rouillard)
+- code cleanup replace bare except: with except Exception:. (patch by
+ Sense_wang (haosenwang1018) applied by John Rouillard)
+- handle "null" values in json objects sent to a rest endpoint by
+ filtering them from the object before processing. A "null" value
+ will not unset an attribute. The 'remove' action using the
+ PATCH verb can unset the value. Before this change "null" values
+ retrieved from the REST interface would cause errors when sent
+ using POST or PUT verbs. Also guard against password being set to
+ None. (John Rouillard)
+- change the html templates so that the password is not required if
+ the ``login_empty_passwords`` setting is enabled in ``config.ini``.
+ Directions for your tracker are in upgrading.txt. (John Rouillard)
+- fix traceback displayed when roundup-admin install was used with
+ invalid settings for config.ini. It now reports the error and
+ provides the usage output. Also usage output starts with a newline
+ to provide a blank line between the command and the output to
+ improve readability. (John Rouillard)
+- fix bug in 2.5.0 where roundup-admin import (or importtable) fails
+ to properly set the next available id for the class. (John Rouillard
+ broke it and fixed it 8-))
+- refactor mime detection/handling in the rest interface. Better
+ supports some mime types, ads default mime type for files without a
+ mime type (e.g. message contents). Cleaner code. (John Rouillard)
+- run hexora and mitigate/fix some medium sev and above. (John Rouillard)
+- Return 400 if environment or header variables
+ have values with characters outside of the printable ascii range
+ (32-127). Applies to roundup-server and roundup-cgi. wsgi and Zope
+ depend on the hosting server for sanitizing. Se waitress wsgi server
+ bug:
+ https://github.com/Pylons/waitress/security/advisories/GHSA-m5ff-3wj3-8ph4
+ (John Rouillard)
+- Cleanups/minor enhancements to roundup.cgi script. (John Rouillard)
+- fix back_anydm::Class::get() method to properly return default value
+ if requested property is set to None. This should fix missing text
+ indexing for the anydbm backend. (John Rouillard)
+- ruff driven cleanups/refactor of indexer* files. (John Rouillard)
+- remove ``roundup.anypy.scandir_`` which was needed for python 2 support.
+ (John Rouillard)
+- roundup-server no longer logs connection aborted when a client
+ disconnects and is using HTTP 1.1 or newer. (John Rouillard)
+- replace isinstance(..., type(X)) where X is an instance of a type
+ (1, {}, [], '', u"") with the type itself (int, dict, list, str,
+ str). This is a leftover construct from early Python 2. Replacing
+ them speeds up code (e.g. type({}) vs dict). (John Rouillard)
+- issue2551405 - Missing locale .mo files create 500 error on uwsgi
+ without further info. Added more specific ConnectionAbortedError
+ and changed IOError handling. Case in issue now logs the exception
+ for diagnosis. (Reported by Ralf Schlatterbeck; John Rouillard)
+- Make queries selected from query edit screen include the query
+ name/display name. This makes it work the same as invoking a query
+ from the main "Your Queries" menu. (John Rouillard)
+- Fix SearchAction to not clobber a saved search when a new search of
+ the same name is created. Assume a search named "asearch"
+ exists. Create a new search (not editing the existing
+ "asearch"). Set new query params and name it "asearch". Roundup will
+ warn you that "asearch" already exists and you need to choose a new
+ name. In the warning form, you rename it to "bsearch" and
+ save/execute. Before this would overwrite "asearch" as it set the
+ "@old-queryname" of the warning form to "asearch". So it looked like
+ you were trying to edit the existing query. This fix modifies the
+ queryname before generating the error form. So when submitted it
+ doesn't overwrite "asearch". (John Rouillard)
+- update the classic tracker's page.html to remove table based
+ layout. It now uses html5 landmarks like header, main, search,
+ nav. It also uses flexbox and grid layouts and includes a viewport
+ meta tag for use on mobile devices. It can be navigated with the
+ keyboard. Parts of the page that are too wide for the viewport can
+ be scrolled horizontally with the keyboard or touch. Some other CSS
+ changes make it easier to use. This is a first step to making the
+ classic template more responsive. (John Rouillard)
+- jinja2 template dependencies updated: bootstrap 4.4.1 -> 4.6.2.
+ (John Rouillard)
+- issue2551377 - Disabled submit button UI issues. When the submit
+ button is clicked (triggering submit_once) but client side
+ validation blocks the submission, the submit button no longer
+ submits the form. Clicking it says the form has been submitted
+ please wait. This change allows the submit button to submit the form
+ again after 5 seconds. (John Rouillard)
+- Fix developers.txt doc bug (discovered by Ross Boylan, change by
+ John Rouillard)
+- Handle ConfigurationErrors in demo.py cleanly. Used to dump a full
+ traceback. Now prints error and exits. (John Rouillard)
+- Do not display journal/history fields when user does not have access
+ to the property being changed. (John Rouillard)
+- Fix missing CSRF protection for PATCH method (John Rouillard)
Features:
- add support for authorized changes. User can be prompted to enter
their password to authorize a change. If the user's password is
properly entered, the change is committed. (John Rouillard)
+- add support for dictConfig style logging configuration. Ini/File
+ style configs will still be supported. (John Rouillard)
+- add 'q' as alias for quit in roundup-admin interactive mode. (John
+ Rouillard)
+- add readline command to roundup-admin to list history, control input
+ mode etc. Also support bang (!) commands to rerun commands in history
+ or put them in the input buffer for editing. (John Rouillard)
+- add format to logging section in config.ini. Used to set default
+ logging format. (John Rouillard)
+- the default logging format template includes an identifier unique
+ for a request. This identifier (trace_id) can be use to identify
+ logs for a specific transaction. Will use nanoid if installed, uses
+ uuid.uuid4 otherwise. Logging also supports a trace_reason log token
+ with the url for a web request. The logging format can be changed in
+ config.ini. (John Rouillard)
+- issue2551152 - added basic PGP setup/use info to admin_guide. (John
+ Rouillard)
+- add support for the 'justhtml' html 5 parser library for python >=
+ 3.10. It is written in pure Python. Used to convert html emails into
+ plain text. Faster then beautifulsoup4 and it passes the html 5
+ standard browser test suite. Beautifulsoup is still supported. (John
+ Rouillard)
+- add a new detector: immutable_file_contents.py that prevents
+ changes to file contents. By default, the permissions assigned
+ to FileClass based classes (files, msgs) do not prevent editing
+ of file contents. While the usual HTML templates don't provide
+ a way to modify files, the REST interface allows changing file
+ contents without an audit trail. Manually driving the HTML
+ interface (via curl for example) also allows content
+ changes. The new detector prevents changes to file contents via
+ Roundup even by a user with admin rights. (John Rouillard)
+- Added documentation on doing an in place database migration. This is
+ faster for large databases. (John Rouillard)
+- Added installation.txt docs on setting up SCGI support with the
+ althttpd web server. (John Rouillard)
+- add support for tokenless/nonceless CSRF prevention following
+ https://words.filippo.io/csrf/. (John Rouillard)
+- standardize use of ``roundup.anypy.urllib_``. Replace urlparse() with
+ urlsplit() (recommended) for some speedup. (John Rouillard)
+- issue2551368 - version 2.0.0 of the pgp module was published by
+ Bernhard Reiter. Updated documentation and tests. (Bernhard Reiter,
+ John Rouillard)
2025-07-13 2.5.0
diff --git a/COPYING.txt b/COPYING.txt
index 18d9f1c3b..5df83e10a 100644
--- a/COPYING.txt
+++ b/COPYING.txt
@@ -1,11 +1,12 @@
-Roundup, exclusive of the Zope page templates, is `MIT licensed`_.
+Roundup, exclusive of the Zope page templates and vendored cgi.py, is
+`MIT licensed`_.
.. _MIT licensed: https://opensource.org/license/mit
Roundup Licensing
-----------------
-| Copyright (c) 2009-2025 Roundup-Team (https://opensource.org/license/mit)
+| Copyright (c) 2009-2026 Roundup-Team (https://opensource.org/license/mit)
| Copyright (c) 2003-2009 Richard Jones (richard@mechanicalcat.net)
| Copyright (c) 2002 eKit.com Inc
| Copyright (c) 2001 Bizar Software Pty Ltd
@@ -117,8 +118,12 @@ Note link for http://www.zope.com/Marks is dead.
Vendored cgi.py module
----------------------
-This module is licensed under the Python Software Foundation License
-Version 2 as it was extracted from the 3.12 Python distribution.
+This module is licensed under the `Python Software Foundation License
+Version 2`_ as it was extracted from the 3.12 Python distribution.
+
+.. _Python Software Foundation License Version 2:
+ https://opensource.org/license/Python-2.0
+
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
diff --git a/RELEASE.txt b/RELEASE.txt
index 9410a721b..4cfedf93f 100644
--- a/RELEASE.txt
+++ b/RELEASE.txt
@@ -24,10 +24,9 @@ Roundup release checklist:
2. Run unit tests! They should pass successfully. Install pytest
using pip2/pip3 for python2 and python3. Then invoke pytest
- using both python versions from the top of the roundup tree:
+ using both python versions from the top of the roundup tree
python3 -m pytest test/
- python2 -m pytest test/
3. Update version in:
CHANGES.txt (set date for version as well)
@@ -39,7 +38,7 @@ Roundup release checklist:
org.opencontainers.image.version
3a. Update license end date in COPYING.txt
3b. Update doc/acknowledgements.txt (add section for
- release, churn contributers etc.). (Use hg churn -c -r ####..####)
+ release, churn contributers etc.). (Use hg churn -c -r 2.5.0..default)
4. Update documentation
doc/announcement.txt
@@ -130,7 +129,7 @@ Roundup release checklist:
added and removed files. Last release e.g. 1.5.1 where tip is what would
become 1.6) E.G.
- hg status --rev 2.2.0:tip | sed -ne 's/^A //p' | while read i ; \
+ hg status --rev 2.5.0:default | sed -ne 's/^A //p' | while read i ; \
do echo $i; grep "$i" roundup.egg-info/SOURCES.txt; done | \
uniq -c | sort -rn
@@ -141,7 +140,7 @@ Roundup release checklist:
(Note: files under website/ shouldn't be in the manifest.)
10a: Check for removed files still in manifest:
- hg status --rev 2.2.0:tip | sed -ne 's/^R //p' | while read i ; \
+ hg status --rev 2.5.0:default | sed -ne 's/^R //p' | while read i ; \
do echo $i; grep "$i" roundup.egg-info/SOURCES.txt; done | \
uniq -c | sort -n
@@ -250,10 +249,10 @@ Roundup release checklist:
the latest -N release. Also roundup:latest points to the
newest -N for the newest roundup version.)
- docker build -t rounduptracker/roundup:2.4.0 \
+ docker build -t rounduptracker/roundup:2.6.0 \
--build-arg="source=pypi" -f scripts/Docker/Dockerfile .
- to create the docker image. *Change 2.4.0 to current version*
+ to create the docker image. *Change 2.6.0 to current version*
Always use the exact release tag.
17c. vulnerability scan local image using:
@@ -298,8 +297,8 @@ Roundup release checklist:
first and user must be member of rounduptracker org with ability
to publish). Replace -N with the release number (e.g. -1, -2, -3...)
- docker tag rounduptracker/roundup:2.4.0 roundup-tracker/roundup:latest
- docker tag rounduptracker/roundup:2.4.0 roundup-tracker/roundup:2.4.0-N
+ docker tag rounduptracker/roundup:2.4.0 rounduptracker/roundup:latest
+ docker tag rounduptracker/roundup:2.4.0 rounduptracker/roundup:2.4.0-N
docker push rounduptracker/roundup:2.4.0
docker push rounduptracker/roundup:2.4.0-N
docker push rounduptracker/roundup # update roundup:latest
diff --git a/demo.py b/demo.py
index 861b504e3..13aba73e2 100755
--- a/demo.py
+++ b/demo.py
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import sys
-import roundup
from roundup.demo import main
diff --git a/detectors/README.txt b/detectors/README.txt
index fde325dd6..508299ce4 100644
--- a/detectors/README.txt
+++ b/detectors/README.txt
@@ -19,6 +19,10 @@ creator_resolution.py - only allow the creator of the issue to resolve it
emailauditor.py - Rename .eml files (from email multi-part bodies) to
.mht so they can be downloaded/viewed in Internet Explorer.
+immutable_file_contents.py - prevent changes to the contents property
+ of file and msg classes including by
+ people with the admin role.
+
irker.py - communicate with irkerd to allow roundtup to send announcements
to an IRC channel.
diff --git a/detectors/immutable_file_contents.py b/detectors/immutable_file_contents.py
new file mode 100644
index 000000000..d5a8dc391
--- /dev/null
+++ b/detectors/immutable_file_contents.py
@@ -0,0 +1,22 @@
+# HTML pages don't provide a way to change the contents of a file.
+# However REST does allow setting content and the HTML interface can
+# be directed to update the content as well. This detector
+# prevents changes to file content.
+
+from roundup.exceptions import UsageError
+
+def immutable_file_contents(db, cl, nodeid, newvalues):
+ ''' Prevent content changes to a file
+ '''
+ if 'content' in newvalues:
+ raise UsageError("File contents are immutable. "
+ "Rejecting change to contents.")
+
+
+def init(db):
+ """If you have other FileClass based classes add them here."""
+
+ # fire before changes are made
+ db.file.audit('set', immutable_file_contents)
+ db.msg.audit('set', immutable_file_contents)
+
diff --git a/detectors/irker.py b/detectors/irker.py
index 57ce2bfb1..1d5b45673 100644
--- a/detectors/irker.py
+++ b/detectors/irker.py
@@ -1,6 +1,6 @@
# This detector sends notification on IRC through an irker daemon
-# (http://www.catb.org/esr/irker/) when issues are created or messages
-# are added.
+# (https://gitlab.com/esr/irker formerly http://www.catb.org/esr/irker/)
+# when issues are created or messages are added.
#
# Written by Ezio Melotti
#
@@ -14,7 +14,6 @@
# channels = irc://chat.freenode.net/channelname
#
-from __future__ import print_function
import re
import json
import socket
diff --git a/doc/CVE.txt b/doc/CVE.txt
index ef4a9abae..f7dc08fcd 100644
--- a/doc/CVE.txt
+++ b/doc/CVE.txt
@@ -1,29 +1,34 @@
-.. comments:
+.. comment:
This file is a temporary way to post CVE notifications before
a release.
- Document the CVE fix info in upgrading.txt. We extract the sections
+ Document the CVE fix info in upgrading.txt. Extract the sections
from upgrading.txt that deal with the CVE into a separate CVE.html.
An updated docs/security.html and docs/CVE.html provide the details
- on a between release CVE announcment.
+ on a between release CVE announcment. These file get installed on
+ the website.
- Publishing upgrading.txt would include info on the to be released
- roundup software and wouldn't match the rest of the release docs.
+ We can't publish upgrading.txt as it includes info on the to be
+ released roundup software and wouldn't match the rest of the
+ release docs.
- To extract the info from upgrading.txt to use in CVE.html, add a
- commented out a reference anchor in upgrading.txt. Then in CVE.txt
- we use an include directive with start-after and end-before options
- to exract the sections from upgrading.txt into CVE.html.
+ Extract the info from upgrading.txt into CVE.html, by adding a
+ commented out a reference anchor in upgrading.txt. In CVE.txt we
+ use an include directive with start-after and end-before options to
+ extract the section from upgrading.txt into CVE.html. Note a
+ subheading should be placed right after start-after reference
+ anchor so that there is an entry in the CVE table of contents.
The extracted section in CVE.txt gets the same anchor that is in
- upgrading.txt, but is is not commented out. This allows us to swap
- out CVE.txt and uncomment the reference in upgrading.txt. Then
- rerunning sphinx-build will make security.html point to the sections
- in upgrading.html.
+ upgrading.txt, but is is not commented out. This anchor is used in
+ security.txt when listing all the CVE's. We can swap out CVE.txt
+ and uncomment the reference in upgrading.txt when the new release
+ comes out. Running sphinx-build will make security.html point to
+ the sections in upgrading.html.
For example, in upgrading.txt add a
- .. comment: _CVE-2024-39124:
+ .. comment: _CVE-2024-39124L:
before the section for the CVE (use the real CVE number). At the
end of the CVE section add an end comment:
@@ -34,18 +39,19 @@
security.txt references look like:
* `CVE-2024-39124`_ - :ref:`classhelpers (_generic.help.html) are
- vulnerable to an XSS attack. ` Requires fixing
+ vulnerable to an XSS attack. ` Requires fixing
tracker homes.
- where is the reference. The same reference anchor
- is present (commented out) in upgrading.txt. In CVE.txt you
- replicate the existing anchor and include to extract the content
- section from upgrading.txt. E.G.
+ where is the reference to local documentation
+ while ``CVE-2024-39124_`` is the reference to the CVE page. The
+ same reference anchor is present (commented out) in
+ upgrading.txt. In CVE.txt you replicate the existing anchor and
+ include to extract the content section from upgrading.txt. E.G.
- .. _CVE-2024-39124:
+ .. _CVE-2024-39124L:
.. include:: upgrading.txt
- :start-after: .. comment: _CVE-2024-39124:
+ :start-after: .. comment: _CVE-2024-39124L:
:end-before: .. comment: end of CVE
After building the docs, install docs/security.html and
@@ -55,18 +61,24 @@
in the CVE announcement from Mitre.
+ If Mitre is taking too long to give a number, use something like:
+
+ .. _CVE-2024-perm_brokenL
+
+ or similar until Mitre gives a number.
+
When the release is ready, replace 'comment: _CVE' with '_CVE' in
upgrading.txt. This makes the anchors in upgrading.txt live.
- Then disable CVE.txt by removing CVE.txt from contents.txt in the
- toctree hidden section. Also add docs/CVE.txt to exclude_patterns in
- conf.py.
+ Then comment out the '_CVE' directives in CVE.txt. This will
+ generate an empty placeholder CVE.txt, but that's ok.
No change needs to happen to security.txt as it's using a :ref: and
we just changed the location for the ref so sphinx will get the
links correct.
- Now build the docs and publish to the web site.
+ Now build the docs and publish (including the rebuilt
+ security.html) to the web site.
===========
Roundup CVE
@@ -80,10 +92,36 @@ upgrading doc `_ for these details.
:local:
:depth: 2
-.. _CVE-2024-39124:
+.. comment: _CVE-2026-csrfL:
.. note::
+ Releases of Roundup from 2.0.0 to 2.5.0 do not check for cross site
+ request forgeries (CSRF) when using the ``PATCH`` method. Release
+ 2.6 fixes this issue. If you are running 2.6.0 you don't have to do
+ anything.
+
+.. comment: include:: upgrading.txt
+ :start-after: .. comment: _CVE-2026-csrfL:
+ :end-before: .. comment:
+
+.. comment: _CVE-2026-history_propL:
+
+.. note::
+
+ If you are running versions 1.6.0 to version 2.5 the filtering of
+ history/journal items does not work correctly if the property is
+ protected by a check function. Release 2.6.0 fixes this. If you are
+ running 2.6.0, you don't have to do anything.
+
+.. comment: include:: upgrading.txt
+ :start-after: .. comment: _CVE-2026-history_prop:
+ :end-before: .. comment:
+
+.. comment _CVE-2024-39124L::
+
+.. comment note::
+
Prior to the release of Roundup 2.4.0, you can access updated
tracker templates that address CVE-2024-39124 from
`CVE-2024-39124-templates.zip
@@ -91,18 +129,18 @@ upgrading doc `_ for these details.
file to generate a templates subdirectory containing the classic,
minimal and other tracker templates.
-.. include:: upgrading.txt
- :start-after: .. comment: _CVE-2024-39124:
+.. comment include:: upgrading.txt
+ :start-after: .. _CVE-2024-39124L:
:end-before: .. comment:
-.. _CVE-2024-39125:
+.. comment CVE-2024-39125L:
-.. include:: upgrading.txt
- :start-after: .. comment: _CVE-2024-39125:
+.. comment include:: upgrading.txt
+ :start-after: .. _CVE-2024-39125L:
:end-before: .. comment:
-.. _CVE-2024-39126:
-
-.. include:: upgrading.txt
- :start-after: .. comment: _CVE-2024-39126:
+.. comment CVE-2024-39126L:
+
+.. comment include:: upgrading.txt
+ :start-after: .. _CVE-2024-39126L:
:end-before: .. comment: end of CVE include marker
diff --git a/doc/Makefile b/doc/Makefile
index c9db7af88..c7b87e9d8 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -1,6 +1,8 @@
+SPHINX := sphinx-build
+
##@ Default target
all: man_html tracker_config.txt admin_help.html ## make all docs under share
- cd ..; ./setup.py build_doc
+ cd ..; $(SPHINX) -n -W -b html -d ../build/temp_dir/doc_dir doc share/doc/roundup/html
##@ build doc parts
tracker_config.txt: ../roundup/configuration.py format_config.awk ## generate a current config file
@@ -20,21 +22,25 @@ MAN_HTML=$(patsubst ../share/man/man1/%.1,html_extra/man_pages/%.1.html,$(MAN_RO
man_html: $(MAN_HTML) ## generate html versions of man pages for docs
html_extra/man_pages/%.1.html: ../share/man/man1/%.1
- man --html=cat $< > $@
- # set max width for paragraphs
- sed -i '/
-ROUNDUP-ADMIN
+roundup-admin manual page
@@ -40,7 +41,7 @@
NAME
-
roundup-admin -
+
roundup-admin -
administrate roundup trackers
SYNOPSIS
@@ -49,7 +50,7 @@
SYNOPSIS
-
roundup-admin
+
roundup-admin
[options] <command>
<arguments>
@@ -58,116 +59,125 @@
OPTIONS
-
-i
+
-i
instance home
-
Specify the issue tracker
+
Specify the issue tracker
"home directory" to administer
-
-u
+
-uuser[:password]
-
The user and password to use
+
The user and password to use
for commands (partial implemention, see Security Notes
below).
-
+
-h
-
-
+
+
-
Print help text.
+
Print help text.
+
+
-
+
-
-d
-
-
+
-d
+
+
-
Print full designators (e.g. issue1) not just id numbers
-(1).
+
Print full designators (e.g.
+issue1) not just id numbers (1).
+
+
-
+
-
-c
-
-
+
-c
+
+
-
When outputting lists of data, comma-separate them. Same
-as -S ",".
+
When outputting lists of data,
+comma-separate them. Same as -S ",".
+
+
-
-Sstring
+
-Sstring
-
When outputting lists of data,
+
When outputting lists of data,
separate items with given string.
-
+
-s
-
-
+
+
When outputting lists of data, space-separate them. Same
-as -S " ".
+as -S " ".
+
+
-
-P pragma=value
+
-P pragma=value
-
Set a pragma on the command
+
Set a pragma on the command
line. Multiple -P options can be specified to set
multiple pragmas.
-
+
-V
-
-
+
+
Be verbose when importing data.
-
+
-
+
-
-v
-
-
+
-v
+
+
-
Report Roundup and Python versions and quit.
-
+
Report Roundup and Python
+versions and quit.
+
-
Only one of
+
Only one of
-s, -c or -S can be specified.
COMMANDS
@@ -175,209 +185,200 @@
COMMANDS
-
If no command
-is provided on the command line, roundup-admin will enter an
+
If no command is
+provided on the command line, roundup-admin will enter an
interactive mode. More details about these commands are
available using the help command.
-
A designator is
+
A designator is
a classname and a nodeid concatenated, eg. bug1, user10,
....
-
Roundup-admin
+
Roundup-admin
takes a number of commands:
-
-
+
+
help
-
-
+
+
prints help (same as
-h)
-
+
-
help
+
help<subcommand>
-
Print command-specific help
+
Print command-specific help
-
help all
+
help all
-
Print available help for all
+
Print available help for all
commands.
-
-
-
-
-
-
-
commit
-
-
+
commit
+
Commit changes made to the
+database during an interactive session.
-
Commit changes made to the database during an
-interactive session.
-
-
-
The changes
+
The changes
made during an interactive session are not automatically
written to the database - they must be committed using this
command.
-
One-off
+
One-off
commands on the command-line are automatically committed if
they are successful. See also rollback.
-
createclassname
+
createclassname
property=value ...
-
This creates a new entry of the
+
This creates a new entry of the
given class using the property name=value arguments provided
on the command line after the "create"
command.
-
display
+
displaydesignator[,designator]*
-
This lists the properties and
+
This lists the properties and
their associated values for the given node. The pragma
display_header can be used to add a header between
designators that includes the active/retired status of the
item.
-
export
+
export[[-]class[,class]] export_dir
-
Export the database to
+
Export the database to
colon-separated-value files. To exclude the files (e.g. for
the msg or file class), use the exporttables command.
-
Optionally
+
Optionally
limit the export to just the named classes or exclude the
named classes, if the 1st argument starts with
’-’.
-
This action
+
This action
exports the current data from the database into
colon-separated-value files that are placed in the nominated
destination directory.
-
exporttables
+
exporttables[[-]class[,class]] export_dir
-
Export the database to
+
Export the database to
colon-separated-value files, excluding the files below
$TRACKER_HOME/db/files/ (which can be archived separately).
To include the files, use the export command.
-
filterclassname
+
filterclassname
propname=value ...
-
Find the nodes of the given
+
Find the nodes of the given
class with a given property value. Multiple values can be
specified by separating them with commas. If property is a
-string, all values must match. I.E. it’s an matches.
-I.E. an ’or’ operation.
+string, all values must match. I.E. it’s an
+’and’ operation. If the property is a
+link/multilink any value matches. I.E. an ’or’
+operation.
-
findclassname
+
findclassname
propname=value ...
-
Find the nodes of the given
+
Find the nodes of the given
class with a given link property value.
-
genconfig
+
genconfig<filename>
-
Generate a new tracker config
+
Generate a new tracker config
file (ini style) with default values in
<filename>. Note that this does not preserve
any settings from the current tracker. Use updateconfig for
that.
-
getproperty
+
getproperty
designator[,designator]*
-
Get the given property of one
+
Get the given property of one
or more designator(s).
-
Retrieves the
+
Retrieves the
property value of the nodes specified by the
designators.
-
historydesignator
+
historydesignator
[skipquiet] [raw]
-
Lists the journal entries
+
Lists the journal entries
viewable by the user for the node identified by the
designator. If skipquiet is added, journal entries for quiet
properties are not shown. Without the raw option a more
human readable output format is used.
-
import
+
importimport_dir
-
Import a database from the
+
Import a database from the
directory containing CSV files, two per class to import.
-
The imported
+
The imported
nodes will have the same nodeid as defined in the import
file, thus replacing any existing content.
-
The new nodes
+
The new nodes
are added to the existing database - if you want to create a
new database using the imported data, then create a new
database (or, tediously, retire all the old data). See also
export.
-
importtables
+
importtablesimport_dir
-
Import a database from the
+
Import a database from the
directory containing CSV files, two per class to import.
-
The imported
+
The imported
nodes will have the same nodeid as defined in the import
file, thus replacing any existing content.
-
This only
+
This only
imports database tables. It does not import files/message
content. It is used to import data exported by exporttables.
See also exporttables.
-
initialise
+
initialise[adminpw]
-
Initialise a new Roundup
+
Initialise a new Roundup
tracker.
-
The
+
The
administrator details will be set at this step.
-
install[template
+
install[template
[backend [key=val[,key=val]]]]
-
Install a new roundup
+
Install a new roundup
tracker.
-
The command
+
The command
will prompt for the tracker home directory (if not supplied
through TRACKER_HOME or the -i option). The template and
backend may be specified on the command-line as arguments,
in that order.
-
Command line
+
Command line
arguments following the backend allows you to pass initial
values for config options. For example, passing
"web_http_auth=no,rdbms_user=dinsdale" will
@@ -386,189 +387,189 @@
COMMANDS
in this argument! (Enclose whole argument in quotes if you
need spaces in option value).
-
The initialise
+
The initialise
command must be called after this command in order to
initialise the tracker’s database. You may edit the
tracker’s initial database contents before running
that command by editing the tracker’s dbinit.py module
init() function.
-
See also
+
See also
initopts help.
-
listclassname
+
listclassname
[property]
-
Lists all instances of the
+
Lists all instances of the
given class. If the property is not specified, the
"label" property is used. The label property is
tried in order: the key, "name", "title"
and then the first property, alphabetically.
-
With -c,
+
With -c,
-S or -s print a list of item id’s if no
property specified. If property specified, print list of
that property for every class instance.
-
The pragma
+
The pragma
show_retired can be used to print only retired items
or to print retired and active items. The default is to
print only active items.
-
migrate
+
migrate
-
Update a tracker’s
+
Update a tracker’s
database to be compatible with the Roundup codebase.
-
You should run
+
You should run
the "migrate" command for your tracker once
you’ve installed the latest codebase.
-
Do this before
+
Do this before
you use the web, command-line or mail interface and before
any users access the tracker.
-
This command
+
This command
will respond with either "Tracker updated" (if
you’ve not previously run it on an RDBMS backend) or
"No migration action required" (if you have run
it, or have used another interface to the tracker, or
possibly because you are using anydbm).
-
It’s safe
+
It’s safe
to run this even if it’s not required, so just get
into the habit.
-
packperiod |
+
packperiod |
date
-
Remove journal entries older
+
Remove journal entries older
than a period of time specified or before a certain
date.
-
perftest[mode]
+
perftest[mode]
[arguments]*
-
Run performance test. For
+
Run performance test. For
example test the effect of changing password hashing
parameters.
will show all
settings and their current values. If verbose is enabled
hidden settings and descriptions will be shown.
-
reindex
+
reindex[classname|classname:#-#|designator]* This will
-
re-generate the search indexes
+
re-generate the search indexes
for a tracker. You can specify a specific item (or items)
(e.g. issue23), range(s) of items (e.g. issue:1-1000),
class(es) (e.g. issue) or reindex all items in the database
if no arguments are supplied.
-
restore
+
restoredesignator[,designator]*
-
Restore the retired node
+
Restore the retired node
specified by designator.
-
The given nodes
+
The given nodes
will become available for users again.
-
retire
+
retiredesignator[,designator]*
-
This action indicates that a
+
This action indicates that a
particular node is not to be retrieved by the list or find
commands, and its key value may be re-used. See also
restore.
-
rollback
+
rollback
-
Undo all changes that are
+
Undo all changes that are
pending commit to the database.
-
The changes
+
The changes
made during an interactive session are not automatically
written to the database - they must be committed manually.
This command undoes all those changes, so a commit
immediately after would make no changes to the database.
-
security[Role
+
security[Role
name]
-
Display the Permissions
+
Display the Permissions
available to one or all Roles.
-
setitems
+
setitems
property=value property=value ...
-
Set the given properties of one
+
Set the given properties of one
or more items(s).
-
The items are
+
The items are
specified as a class or as a comma-separated list of item
designators (ie
"designator[,designator,...]").
-
This command
+
This command
sets the properties to the values for all designators given.
If the value is missing (ie. "property=") then the
property is un-set. If the property is a multilink, you
specify the linked ids for the multilink as comma-separated
numbers (ie "1,2,3").
-
specification
+
specificationclassname
-
Show the properties for a
+
Show the properties for a
classname.
-
templates
+
templates[trace_search]]
-
Lists the names, location and
+
Lists the names, location and
description of all known templates.
-
tableclassname
+
tableclassname
[property[,property]*]
-
Lists all instances of the
+
Lists all instances of the
given class. If the properties are not specified, all
properties are displayed. By default, the column widths are
the width of the largest value.
-
The pragma
+
The pragma
show_retired can be used to print only retired items
or to print retired and active items. The default is to
print only active items.
-
updateconfig
+
updateconfig<filename>
-
This is used when updating
+
This is used when updating
software. It merges the config.ini from the tracker
with new settings from the new software. The merged/updated
config file is written to <filename>.
-
Commands may be
+
Commands may be
abbreviated as long as the abbreviation matches only one
command, e.g. l == li == lis == list.
@@ -577,15 +578,15 @@
READLINE SUPPORT
-
You can edit
-the command line and interact with history in
+
You can edit the
+command line and interact with history in
roundup-admin’s interactive mode. On Linux like
systems, GNU Readline is required. On Mac’s libedit
can be used as well. On Windows pyreadline3 is required.
See: https://docs.python.org/3/library/readline.html for
details on readline.
-
Roundup-admin
+
Roundup-admin
will save the history between roundup-admin sessions. The
commands are saved to the .roundup_admin_history file
in the user’s home directory. You can set the pragma
@@ -593,17 +594,17 @@
READLINE SUPPORT
change the amount of available and stored history for the
session.
-
If you are
-using GNU readline, you can persistently change the history
-size by adding: set history-size 100 to the init file
+
If you are using
+GNU readline, you can persistently change the history size
+by adding: set history-size 100 to the init file
.roundup_admin_rlrc located in the user’s home
directory.
-
Mac users using
+
Mac users using
libedit should be able to persistently save history and set
the history size by adding history size 100.
-
Users of
+
Users of
pyreadline3 can use: history_length(100).
SECURITY NOTES
@@ -611,14 +612,14 @@
SECURITY NOTES
-
The -u
+
The -u
user setting does not currently operate like a user
logging in via the web. The user running roundup-admin must
have read access to the tracker home directory. As a result
the user has access to the files and the database info
contained in config.ini.
-
Using -u
+
Using -u
user sets the actor/user parameter in the journal.
Changes that are made are attributed to that user. The
password is ignored if provided. Any existing username has
@@ -633,9 +634,9 @@
ENVIRONMENT VARIABLES
-
ROUNDUP_LOGIN
+
ROUNDUP_LOGIN
-
Provides an alternate way to
+
Provides an alternate way to
set the user.
FURTHER HELP
@@ -643,8 +644,8 @@
FURTHER HELP
-
roundup-admin
--h
+
roundup-admin -h
+
roundup-admin help -- this help
roundup-admin help <command> -- command-specific help
@@ -656,7 +657,7 @@
Create a fresh
+demo tracker (deleting the existing one if any). E.G.
+roundup-demo -b sqlite -t classic ./mytracker nuke
will remove an existing tracker (if present) from the
directory ./mytracker. Then it will create and serve
a new empty classic tracker using the sqlite backend.
-
-h, --help
+
-h, --help
-
Show the help
+
Show the help
message and exit
-
-BBIND_ADDRESS,
+
-BBIND_ADDRESS,--bind_addressBIND_ADDRESS
-
Choose address
+
Choose address
for server to listen at. Use 0.0.0.0 to bind to all
addreses. Default: 127.0.0.1.
Set the
hostname used in the URL. This does not make the server
listen at the hostname. Use the hostname with -B to
listen on that IP address.
-
-pPORT,
+
-pPORT,
--portPORT
-
Listen at this
+
Listen at this
port. Default: search for open port starting at 8917.
-
-PURLPORT,
+
-PURLPORT,
--urlportURLPORT
-
When using
+
When using
docker this option passes the docker external port to the
demo instance. If using docker ... -p 9090:8917 ...
this should be set to -P 9090. Default: as selected
by --port.
-
-V, --version
+
-V, --version
-
Show
+
Show
program’s version number and exit
-
If items marked
+
If items marked
with (*) are missing, they will be asked for interactively
when setting up the tracker.
@@ -159,7 +158,7 @@
DESCRIPTION
-
This command
+
This command
creates a fresh demo tracker for you to experiment with. The
email features of Roundup will be turned off (so the nosy
feature won’t send email). It does this by removing
@@ -170,12 +169,12 @@
DESCRIPTION
templates directory. The templates command from
roundup-admin(1) can help you locate a replacement.
-
If you wish,
-you may modify the demo tracker by editing its configuration
+
If you wish, you
+may modify the demo tracker by editing its configuration
files and HTML templates. See the customisation
manual for information about how to do that.
-
Once
+
Once
you’ve fiddled with the demo tracker, you may use it
as a template for creating your real, live tracker. Simply
run the roundup-admin command to install the tracker
@@ -189,7 +188,7 @@
specify the values to set on
the class specified as prefix using the same format as the
Subject line property manipulations. If the class prefix is
left out it defaults to msg. Can also be specified
with --set-value (and abbreviated to
--set).
-
-O --oauth-directory
+
-O --oauth-directorydirectory
-
Only with OAuth authentication
+
Only with OAuth authentication
specify the directory containing additional OAuth
credentials.
-
-T
---oauth-token-endpointurl
+
-T --oauth-token-endpoint
+url
-
Token endpoint URL of your
+
Token endpoint URL of your
cloud provider, only used with OAuth.
DESCRIPTION
@@ -100,67 +101,67 @@
DESCRIPTION
-
The roundup
-mail gateway may be called in one of four ways:
+
The roundup mail
+gateway may be called in one of four ways:
-
-
+
+
•
-
-
+
+
without arguments which will use the env var
ROUNDUP_INSTANCE,
-
-
+
+
-
•
-
-
+
•
+
+
-
with an instance_home as the only argument,
+
with an instance_home as
+the only argument,
-
-
+
+
-
•
-
-
+
•
+
+
-
with both an instance_home and a mail spool file,
-or
+
with both an
+instance_home and a mail spool file, or
-
-
+
+
-
•
-
-
+
•
+
+
-
with both an instance home and a mail source type and
-its specification.
+
with both an instance home and a
+mail source type and its specification.
-
PIPE
-
+
PIPE
If there is no mail source specified, the mail gateway reads
a single message from the standard input and submits the
message to the roundup.mailgw module.
-
UNIX
+
UNIX
mailbox
In this case, the gateway reads all messages from the UNIX
mail spool file and submits each in turn to the
@@ -169,16 +170,16 @@
DESCRIPTION
mailbox/path/to/mailbox
-
In all of the
+
In all of the
following mail source types, the username and password can
-be stored in a ~/.netrc file. If done so, only the server
+be stored in a ˜/.netrc file. If done so, only the server
name needs to be specified on the command-line.
-
The username
+
The username
and/or password will be prompted for if not supplied on the
-command-line or in ~/.netrc.
+command-line or in ˜/.netrc.
-
POP
+
POP
For the mail source "pop", the gateway reads all
messages from the POP server specified and submits each in
turn to the roundup.mailgw module. The server is specified
@@ -191,19 +192,16 @@
DESCRIPTION
pop server
are both valid.
-
POPS
-
+
POPS
Connect to the POP server over ssl/tls. This supports the
same notation as POP.
-
APOP
-
+
APOP
Same as POP, but using Authenticated POP:
apopusername:password@server
-
IMAP
-
+
IMAP
Connect to an IMAP server. This supports the same notation
as that of POP mail.
imap
@@ -215,7 +213,7 @@
DESCRIPTION
username:password@server
mailbox
-
IMAPS
+
IMAPS
Connect to an IMAP server over ssl/tls. This supports the
same notation as IMAP.
@@ -224,7 +222,7 @@
DESCRIPTION
[mailbox]
-
IMAPS_CRAM
+
IMAPS_CRAM
Connect to an IMAP server over ssl/tls using CRAM-MD5
authentication. This supports the same notation as IMAP.
@@ -234,7 +232,7 @@
DESCRIPTION
[mailbox]
-
IMAPS_OAUTH
+
IMAPS_OAUTH
Connect to an IMAP server over ssl/tls using OAUTH
authentication. Note that this does not support a password
@@ -262,7 +260,7 @@
ENVIRONMENT VARIABLES
-
ROUNDUP_INSTANCE
+
ROUNDUP_INSTANCE
This can be used to set the instance directory. If it is
set, the instance_home argument is optional.
@@ -272,17 +270,17 @@
EXAMPLE
-
If your msg
+
If your msg
class has a messsagetype property you can set up aliases to
receive the different types of messages. Setting up an alias
in your MTA Using:
-
tracker:
+
tracker:
|/tools/roundup/bin/roundup-mailgw
-S "msg.messagetype=reply - to all"
/var/roundup/sysadmin
-
(the lines are
+
(the lines are
split for readability. In the alias file they will be on the
same line). Replace /tools/roundup/bin/roundup-mailgw by
your path to the roundup-mailgw. This creates the email
@@ -291,39 +289,39 @@
EXAMPLE
all". The roundup tracker instance is located at
/var/roundup/sysadmin.
-
A comment alias
+
A comment alias
would receive emails at tracker_comment with:
which would be
-a technical message. These messages can trigger different
+
which would be a
+technical message. These messages can trigger different
workflows based on the messagetype.
-
Both examples
+
Both examples
use the default class specified in the tracker’s
config.ini (usually the issue class).
-
You can also
-set properties directly on the issue class. If your
+
You can also set
+properties directly on the issue class. If your
tracker’s issue has a "private" property
that limits who can view an issue and its messages:
Use options read from the
configuration file (see below).
-
-nhostname
+
-nhostname
-
Sets the host name or IP
+
Sets the host name or IP
address to listen on. Default is localhost (127.0.0.1). Use
0.0.0.0 to listen on all interfaces on the system.
-
-pport
+
-pport
-
Sets the port to listen on
+
Sets the port to listen on
(default: 8080).
-
-dPIDfile
+
-dPIDfile
-
Run the server in the
+
Run the server in the
background and write the server’s PID to the file
indicated by PIDfile. The -l (or -L) option must be
specified if -d is used.
@@ -81,49 +82,51 @@
OPTIONS
-
+
-D
-
-
+
+
-
Run the server in the foreground even if -d is used.
+
Run the server in the foreground even if -d is used.
+
+
-
-t
+
-t
fork|thread|debug|none
-
Control multi-process mode.
+
Control multi-process mode.
debug and none are always available. If an
invalid mode is specified the server starts in none
(also called single) mode. Default fork.
-
-m
+
-mmax_children
-
Set the maximum number of child
+
Set the maximum number of child
processes (servers). This value may need to be increased on
a heavily loaded site. Default 40.
-
-lfile
+
-lfile
-
Sets a filename to log to
+
Sets a filename to log to
(instead of stdout). This is required if the -d option is
used.
-
+
-P
-
-
+
+
If a reverse proxy is used in front of the
@@ -139,22 +142,22 @@
OPTIONS
for details and warnings about using the X-Forwarded-For
header.
-
+
-
-L
-
-
+
-L
+
+
-
Have the server log using the Python logger with key
-roundup.http.
+
Have the server log using the
+Python logger with key roundup.http.
-
-ifile
+
-ifile
-
Sets a filename to use as a
+
Sets a filename to use as a
template for generating the tracker index page. The variable
"trackers" is available to the template and is a
dict of all configured trackers. If you connect to the root
@@ -162,10 +165,10 @@
OPTIONS
page is shown. Using -i allows you to customize the
index page.
-
-I
+
-Iheader1[,header2,...]
-
Pass the header(s) and their
+
Pass the header(s) and their
values to the backend. This allow-list of header variables
can be used by custom code in the tracker or with a
tracker’s http_auth_header configuration option
@@ -175,13 +178,13 @@
OPTIONS
-
+
-s
-
-
+
+
Enables use of SSL. In most cases, you will want to run
@@ -190,91 +193,73 @@
OPTIONS
filter/rate limit/firewall requests to roundup-server.
-
-efile
+
-efile
-
Sets a filename containing the
+
Sets a filename containing the
PEM file to use for SSL. The PEM file must include both the
private key and certificate with appropriate header/trailer
markers:
If no file is
specified, a temporary self-signed certificate will be
used.
-
+
-N
-
-
+
+
Log client machine names instead of IP addresses (much
-slower).
+slower).
+
+
-
-VHTTPVER
+
-VHTTPVER
-
By default roundup-server uses
+
By default roundup-server uses
HTTP/1.1 to enable keepalives for faster response. HTTPVER
can be set to HTTP/1.0 to disable keepalives.
-
-
-
-
-
-
-
-uUID
-
-
+
-uUID
+
Runs the Roundup web server as
+this UID.
-
Runs the Roundup web server as this UID.
-
-
-
-
-
-
-
-
-gGID
-
-
+
-gGID
+
Runs the Roundup web server as
+this GID.
-
Runs the Roundup web server as this GID.
-
-
-
-
-
-v or
+
-v or
--version
-
Print version and exit.
+
Print version and exit.
-
-h or --help
+
-h or --help
-
Print help and exit.
+
Print help and exit.
-
--save-config
+
--save-config
-
Create configuration file and
+
Create configuration file and
exit. The old config.ini will be saved to config.bak. Note
that this command doesn’t attempt to load or verify an
existing config.ini. Running this in a tracker home
@@ -283,10 +268,10 @@
OPTIONS
This will make the tracker in the directory fail to start
until the original config.ini is restored.
-
name=tracker
+
name=tracker
home
-
Sets the tracker home(s) to
+
Sets the tracker home(s) to
use. The name variable is how the tracker is
identified in the URL (it’s the first part of the URL
path). The tracker home variable is the directory
@@ -298,7 +283,7 @@
OPTIONS
characters like spaces, as these confuse the cookie handling
in browsers like IE.
-
If you connect
+
If you connect
to the root directory (I.E. you do not include the
name component in the URL, the server will display an
index page of trackers served by roundup-server if more than
@@ -311,14 +296,14 @@
This manpage
-was written by Bastian Kleineidam <calvin@debian.org>
-for the Debian distribution of roundup.
+
This manpage was
+written by Bastian Kleineidam <calvin@debian.org> for
+the Debian distribution of roundup.
-
The main author
+
The main author
of roundup is Richard Jones
<richard@users.sourceforge.net>.
diff --git a/doc/installation.txt b/doc/installation.txt
index 14d5b0087..a5cd56b41 100644
--- a/doc/installation.txt
+++ b/doc/installation.txt
@@ -103,12 +103,13 @@ emails.
Once you install Roundup, you can use the ``roundup-demo`` command to
install new demo trackers.
-.. [1] Demo tracker is set up to be accessed by localhost browser.
- If you run demo on a server host, please stop the demo (using
+.. [1] Demo tracker is set up to be accessed by localhost browser. If
+ you run demo on a server host, please stop the demo (using
Control-C) after it has shown the startup notice, open file
- ``demo/config.ini`` with your editor, change host name in
- the ``web`` option in section ``[tracker]``, save the file,
- then re-run the demo.py program.
+ ``demo/config.ini`` with your editor, change host name in the
+ ``web`` option in section ``[tracker]`` from ``localhost`` to
+ your servers DNS name, save the file, then re-run the demo.py
+ program.
.. _demo mode using docker:
@@ -178,7 +179,7 @@ can remove the tracker using ``rm -f`` on the ``demo`` directory.
Prerequisites
=============
-Roundup requires Python 3.7 or newer with a functioning anydbm
+Roundup requires Python 3.10 or newer with a functioning anydbm
or sqlite module. The version installed by most vendors should work if
it meets the version requirements. If necessary, you can download the
latest version from https://www.python.org/. It is highly recommended
@@ -248,11 +249,12 @@ gpg
If gpg_ is installed you can configure the mail gateway to perform
verification or decryption of incoming OpenPGP MIME messages. When
configured, you can require email to be cryptographically signed
- before roundup will allow it to make modifications to issues. (Note
- with python 3.13 or newer on some platforms, you may need to install
- version 2.0 of gpg from test.pypi.org. See the `gpg install
- directions in the upgrading document`_.
+ before Roundup will allow it to make modifications to issues.
+nanoid
+ If nanoid_ is installed, it is used to generate short unique
+ ids to link all logging to a single request. If not installed,
+ uuid4's from the standard library are used.
jinja2
To use the jinja2 template (may still be experimental, check out
@@ -307,6 +309,14 @@ polib
roundup-gettext, you must install polib_. See the `developer's
guide`_ for details on translating your tracker.
+beautifulsoup, justhtml
+ When HTML only email is received, Roundup can convert it into
+ plain text using the native dehtml parser. To convert HTML
+ email into plain text, beautifulsoup4_ or justhtml_ can also be
+ used. You can choose the converter in the tracker's
+ config. Note that justhtml is pure Python, fast and conforms to
+ HTML 5 standards but works only for Python 3.10 or newer.
+
pywin32 - Windows Service
You can run Roundup as a Windows service if pywin32_ is installed.
Otherwise it must be started manually.
@@ -579,10 +589,7 @@ responsive
This issue tracker uses the same schema as devel. The difference
between devel and responsive templates is the use of Twitter
bootstrap (https://github.com/twbs/bootstrap) in templates and
- HTML5 markup. Make sure the "static_files" setting in your
- config.ini of your instance is set to the directory where the
- static files live (the subdirectory "static" in the default of the
- template).
+ HTML5 markup.
jinja2
This is a generic issue tracker based on classic schema. It uses
@@ -599,11 +606,14 @@ https://wiki.roundup-tracker.org/TrackerTemplates.
Choosing Your Backend
---------------------
-The actual storage of Roundup tracker information is handled by backends.
+The actual storage of Roundup tracker information is handled by
+database backends. Roundup has a process to allow you to change
+backends and preserve your tracker's data. So you can upgrade from an
+embedded database to MySQL, or PostgreSQL.
Roundup supports basic full text search (FTS) for all backends. Some
-backends support a faster native FTS. Also Roundup supports using
-whoosh or xapian for FTS.
+database backends support a faster native FTS. Roundup also supports
+using whoosh or xapian for FTS.
There are several backends to choose from, each with benefits and
limitations:
@@ -626,16 +636,11 @@ mysql Fast Many Needs install/admin (MySQLdb_)
(e.g. you are interested in adding support for MongoDB or other
NoSQL persistence layer).
**sqlite**
- This uses the embedded database engine SQLite and the PySQLite_
- driver to provide a very fast
- backend. This is not suitable for trackers which will have many
- simultaneous users, but requires much less installation and maintenance
- effort than more scalable postgresql and mysql backends.
-
- SQLite is supported via PySQLite version 2.1.0 and sqlite3 (the last
- being bundled with Python 2.5+)
-
- Installed SQLite should be the latest version available (3.9.0 or newer).
+ This uses the SQLite embedded database engine with the sqlite3
+ library bundled with Python. This provides a very fast backend. This
+ is not suitable for trackers which will have many (50+) users
+ simultaneous updating data. It requires no installation and less
+ maintenance effort than more scalable postgresql and mysql backends.
.. _fts5-testing:
@@ -704,8 +709,8 @@ tracker will be heavily used and accessible from the internet, we
suggest using Apache or Nginx in WSGI mode or as a reverse proxy
to the stand alone web server or WSGI server like Gunicorn.
-A FastCGI deployment with an alternate web server is suitable for
-lower traffic sites.
+A FastCGI or SCGI deployment with an alternate web server is suitable
+for lower traffic sites.
If you already run Zope, Roundup should deploy nicely in that
framework.
@@ -717,10 +722,10 @@ Using a true HTTP server provide tools including: DOS prevention,
throttling, web application firewalls etc. that are worth having
in an internet facing application.
-If you are running on an internal intranet, you can use the
-stand alone server: roundup-server, but even in this environment,
-using a real web server to serve static files and other resources
-will perform better.
+If you are running on an internal intranet, you can use the stand
+alone server: roundup-server, but even in this environment, using a
+real web server or proxy (e.g. pound) to serve static files and other
+resources will perform better.
.. contents::
:depth: 1
@@ -1283,7 +1288,7 @@ FastCGI (Cherokee, Hiawatha, lighttpd)
The Hiawatha and lighttpd web servers can run Roundup using FastCGI.
Cherokee can run FastCGI but it also supports wsgi directly using a
-wsgi server like uWSGI, Gnuicorn etc.
+wsgi server like Waitress, Gnuicorn etc.
To run Roundup using FastCGI, the flup_ package can be used under
Python 2 and Python 3. We don't have a detailed config for this, but
@@ -1296,6 +1301,81 @@ configuration files and instructions.
https://web.archive.org/web/20241010170738/https://flask.palletsprojects.com/en/2.0.x/deploying/fastcgi/
.. _flup: https://pypi.org/project/flup/
+SCGI (althttpd, Cherokee, lighttpd)
+-----------------------------------
+
+The `althttpd (written by the author of SQLite)
+`_ web server is a minimal server that
+can run Roundup using SCGI or CGI.
+
+To run Roundup using SCGI, the flup_ package provides a translator
+between SCGI and WSGI.
+
+The configuration below assumes the web server is running on one host
+and the roundup scgi server is running on another host. If they are
+both running on the same computer, change ``0.0.0.0`` to ``127.0.0.1``
+and ``hostname`` to ``localhost`` or ``127.0.0.1`` to prevent direct
+connection to the roundup scgi server from external hosts.
+
+The following ``roundup-scgi.py`` can be run using Python to start the
+server:
+
+.. code:: python
+
+ #!/usr/bin/env python
+ # Obtain the WSGI request dispatcher
+ from roundup.cgi.wsgi_handler import RequestDispatcher
+ from flup.server.scgi import WSGIServer
+
+ # Set the path to tracker home.
+ tracker_home = '/path/to/tracker/home'
+
+ try:
+ application = RequestDispatcher(tracker_home)
+ except Exception as e:
+ print(e)
+ sys.exit(1)
+
+ ret = WSGIServer(application,
+ # the tuple:
+ # hostname/ip address as a string (0.0.0.0 bind
+ # all ip address)
+ # port as an integer
+ bindAddress=("0.0.0.0", 8197),
+ # Unlike config.ini, the scriptName should
+ # not end with a /.
+ scriptName="/roundup.scgi").run()
+ sys.exit(ret and 42 or 0)
+
+which is started using ``python roundup-scgi.py``. Note this scgi
+configuration serves only one tracker at port 8197. So you run one
+scgi server process for each of your trackers. This is different from
+cgi, roundup-server or some wsgi deployments where you can access
+different trackers through the same entry point.
+
+Now you have to configure althttpd so it connects to the scgi
+server. To do this, althttpd requires a file with a ``.scgi``
+extension. Create the file ``roundup.scgi`` and put it in the root of
+your althttpd directory. It should contain:
+
+.. code::
+
+ SCGI hostname 8197
+
+where hostname is a valid name for the host running the SCGI
+server. You can also put in other directives including a fallback web
+page if the SCGI server is down, or a command to run to restart the
+SCGI server. See the `althttpd.c file
+`_ on
+the main web site for details. If you have already have althttpd or
+althttpsd installed you can just use the ``--help`` argument.
+
+Assuming you run:
+``althttpd --root /var/www/htdocs -port 443 --cert certs.pem`` and you
+installed ``/var/www/htdocs/roundup.scgi``, you will be able to access
+your tracker at: ``https://web_hostname/roundup.scgi/`` which will
+also be the value of your ``web`` setting in the tracker's config.ini.
+
WSGI Variations
---------------
@@ -1499,23 +1579,6 @@ including putting it behind a proxy, IPV6 support etc.
.. _`See the Waitress docs`:
https://docs.pylonsproject.org/projects/waitress/en/stable/
-.. index:: pair: web interface; uWSGI
- single: wsgi; uWSGI
-
-uWSGI Installation
-~~~~~~~~~~~~~~~~~~
-
-For a basic roundup install using uWSGI behind a front end server,
-install uwsgi and the python3 (or python) plugin. Then run::
-
- uwsgi --http-socket 127.0.0.1:8917 \
- --plugin python3 --mount=/tracker=wsgi.py \
- --manage-script-name --callable app
-
-using the same wsgi.py as was used for Gunicorn. If you get path not
-found errors, check the mount option. The /tracker entry must match
-the path used for the [tracker] web value in the tracker's config.ini.
-
Configure an Email Interface
============================
@@ -2375,32 +2438,29 @@ Make sure you read the instructions under `UNIX environment steps`_.
Problems? Testing your Python...
================================
-.. note::
- The ``run_tests.py`` script is not packaged in Roundup's source
- distribution anymore. You should install:
-
- * pytest,
- * requests, and
- * mock
-
- using your distributions package manger or using pip/pip2/pip3 to
- install pytest etc. for your Python version. See the
- `administration guide`_ for details.
-
- Remember to have a database user 'rounduptest' prepared (with
- password 'rounduptest'). This user
- must have at least the rights to create and drop databases.
- Documentation: details on `adding MySQL users`_,
- for PostgreSQL you want to call the ``createuser`` command with the
- ``-d`` option to allow database creation.
-
-This can only be done if you `install the source`_ distribution
-(steps 1-3) or from a `mercurial checkout
-<../code.html#get-sources>`_. It will not work if you used `pip
-install` as the test suite is not installed. Once you've unpacked
-roundup's source, if you have pytest installed, run ``python -m
-pytest test`` in the Roundup source directory and make sure there
-are no errors. If there are errors, please let us know!
+This can only be done if you `install the source`_ distribution (steps
+1-3) or from a `mercurial checkout <../code.html#get-sources>`_. It
+will not work if you used `pip install` as the test suite is not
+installed. Once you've unpacked roundup's source, you need to install
+the testing requirements. You probably want to do this in an activated
+virtual environment. This can be done with::
+
+ python -m pip install -r test/requirements.txt
+
+or you can install all the packages listed in requirements.txt using
+your distribution's package manager.
+
+If you are testing postgresql or mysql/mariadb you must have a
+database user 'rounduptest' (with password 'rounduptest'). This user
+must have at least the rights to create and drop databases.
+Documentation: details on `adding MySQL users`_, for PostgreSQL you
+want to call the ``createuser`` command with the ``-d`` option to
+allow database creation. The test suite will skip tests where the
+database driver software is missing.
+
+Then run ``python -m pytest test`` in the Roundup source directory and
+make sure there are no errors. If there are errors, please let us
+know!
Note that redis tests uses database 15 of the redis server running on
localhost.The tests verify that the database is empty before the redis
@@ -2436,6 +2496,7 @@ the test.
.. _`adding MySQL users`:
https://dev.mysql.com/doc/refman/8.0/en/creating-accounts.html
.. _apache: https://httpd.apache.org/
+.. _beautifulsoup4: https://pypi.org/project/beautifulsoup4/
.. _brotli: https://pypi.org/project/Brotli/
.. _`developer's guide`: developers.html
.. _defusedxml: https://pypi.org/project/defusedxml/
@@ -2443,12 +2504,14 @@ the test.
.. _flup: https://pypi.org/project/flup/
.. _gpg: https://www.gnupg.org/software/gpgme/index.html
.. _jinja2: https://palletsprojects.com/projects/jinja/
+.. _justhtml: https://pypi.org/project/justhtml/
.. _markdown: https://python-markdown.github.io/
.. _markdown2: https://github.com/trentm/python-markdown2
.. _mistune: https://pypi.org/project/mistune/
.. _mod_python: https://github.com/grisha/mod_python
.. _mod_wsgi: https://pypi.org/project/mod-wsgi/
.. _MySQLdb: https://pypi.org/project/mysqlclient/
+.. _nanoid: https://pypi.org/project/nanoid/
.. _Olson tz database: https://www.iana.org/time-zones
.. _polib: https://polib.readthedocs.io
.. _Psycopg2: https://www.psycopg.org/
diff --git a/doc/pydoc.txt b/doc/pydoc.txt
index ef7bf849b..36df7250f 100644
--- a/doc/pydoc.txt
+++ b/doc/pydoc.txt
@@ -1,10 +1,14 @@
-================
-Pydocs from code
-================
+================================
+Embedded documentation from code
+================================
.. contents::
:local:
+The following are embedded documentation selected from the Roundup
+code base. You can see the same information using the ``help``
+function after importing the modules.
+
Client class
============
@@ -40,3 +44,11 @@ Templating Utils class
.. autoclass:: roundup.cgi.templating::TemplatingUtils
:members:
+
+Logcontext Module
+=================
+.. _logcontext_pydoc:
+
+.. automodule:: roundup.logcontext
+ :members:
+ :exclude-members: SimpleSentinel
diff --git a/doc/reference.txt b/doc/reference.txt
index 9713ae283..3dea2518a 100644
--- a/doc/reference.txt
+++ b/doc/reference.txt
@@ -1307,7 +1307,7 @@ is added to the db object. As a result, the ``hasattr`` call shown
above will return True and the Reauth exception is not raised. (Note
that the value of the ``reauth_done`` attribute is True, so
``getattr(db, "reauth_done", False)`` will return True when reauth is
-done and the defaul value of False if the attribute is missing. If the
+done and the default value of False if the attribute is missing. If the
default is not set, `getattr` raises an ``AttributeError`` which might
be useful for flow control.)
@@ -1321,6 +1321,12 @@ user to submit each sensitive property separately. For example::
'at the same time is not allowed. Please '
'submit two changes.')
+ if db.tx_Source not in ("web"):
+ # the user is using rest, xmlrpc, command line,
+ # email (unlikely) which don't support interactive
+ # verification
+ return
+
if 'password' in newvalues and not hasattr(db, 'reauth_done'):
raise Reauth()
@@ -1362,10 +1368,8 @@ from this directory, at which point it calls ``init(instance)`` from
each file supplying itself as a first argument.
Note that at this point web interface is not loaded, but
-extensions still can register actions for it in the tracker
-instance. This may be fixed in Roundup 1.6 by introducing
-``init_web(client)`` callback or a more flexible extension point
-mechanism.
+extensions can register actions for it in the tracker
+instance.
* ``instance.registerUtil`` is used for adding `templating utilities`_
(see `adding a time log to your issues
@@ -1378,8 +1382,9 @@ mechanism.
* ``instance.registerAction`` is used to add more actions to the
instance and to web interface. See `Defining new web actions`_
- for details. Generic action can be added by inheriting from
- ``action.Action`` instead of ``cgi.action.Action``.
+ for details. Generic action (used by xmlrpc or rest interfaces) can
+ be added by inheriting from ``action.Action`` instead of
+ ``cgi.action.Action``.
.. _interfaces.py:
.. _modifying the core of Roundup:
@@ -1815,15 +1820,15 @@ The ``addPermission`` method takes a four optional parameters:
``issue`` class and the ``user`` class include a reference to an
``organization`` class. Users are permitted to view only those
issues that are associated with their respective organizations. A
- check function or this could look like::
+ check function for this could look like::
def view_issue(db, userid, itemid):
user = db.user.getnode(userid)
if not user.organisation:
return False
issue = db.issue.getnode(itemid)
- if user.organisation == issue.organisation:
- return True
+ return user.organisation == issue.organisation
+
The corresponding ``filter`` function::
@@ -2226,15 +2231,48 @@ for each action are:
**search**
Determine whether the user has permission to view this class.
+.. _`nonceless CSRF`:
+
Protecting users from web application attacks
---------------------------------------------
-There is a class of attacks known as Cross Site Request Forgeries
-(CSRF). Malicious code running in the browser can making a
-request to Roundup while you are logged into Roundup. The
-malicious code piggy backs on your existing Roundup session to
-make changes without your knowledge. Roundup 1.6 has support for
-defending against this by analyzing the
+There is a type of attack known as Cross Site Request Forgeries
+(CSRF). Malicious code running in the browser can send a request
+to Roundup while you are logged in. The malicious code takes
+advantage of your active Roundup session to make changes without
+your knowledge.
+
+Roundup 2.6 added support for tokenless (nonceless) protection.
+This is based on `Cross-Site Request Forgery by Filippo Valsorda
+`_. Currently you must manually
+enable it. At some point in the future, it will be the default
+CSRF protection method.
+
+You can enable it for each tracker using ``config.ini``. In the
+``[web]`` section of ``config.ini`` setting
+``use_tokenless_csrf_protection`` to ``yes`` enables it. Since
+this is a replacement for the prior anti-CSRF protections, it
+turns off the older nonce and header-based CSRF settings.
+
+The Origin header is checked against the value of the
+``allowed_api_origins`` option. You can use it to allow access to
+Roundup from other web sites. For example your homepage at
+www.example.com displays the 10 most recently updated issues from
+Roundup running at issues.example.com. To allow this, you would
+set the ``allowed_api_origins`` to ``https://www.example.com``.
+
+If your users have older browsers (pre 2023) you may need to
+continue using the older token/nonce based CSRF protection method.
+See Valsorda's page for details.
+
+With this mode enabled, the ``utils.anti_csrf_nonce()`` function
+always returns ``"0"``. It does not generate a nonce nor store it to
+the otks database which should improve response times.
+
+Token/nonce based CSRF protection
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Roundup 1.6 has support for defending against this by analyzing the
* Referer,
* Origin, and
diff --git a/doc/rest.txt b/doc/rest.txt
index b9163e0a7..afde5005f 100644
--- a/doc/rest.txt
+++ b/doc/rest.txt
@@ -945,12 +945,13 @@ with ``Content-Type: application/json`` and a (4x larger) json encoded
representation of the binary data.
If you want it returned with a ``Content-Type: image/png`` header,
-you can use ``image/png`` or ``*/*`` in the Accept header.
+you can use ``image/png``, ``image/*``, or ``*/*`` in the Accept header.
For message files, you can use
``https://.../demo/rest/data/msg/23/binary_content`` with ``Accept:
application/octet-stream; q=0.5, application/json; q=0.4, image/png;
-q=0.495, text/*``. It will return the plain text of the message.
+q=0.495, text/*``. It will return the plain text (text/plain mime
+type) of the message.
Most message files are not stored with a mime type. Getting
``https://.../demo/rest/data/msg/23/type`` returns::
@@ -972,10 +973,16 @@ Messages are meant to be human readable, so the mime type ``text/*``
can be used to access any text style mime type (``text/plain``,
``text/x-rst``, ``text/markdown``, ``text/html``, ...) or an empty
mime type. If the item's type is not empty, it will be used as the
-Content-Type (similar to ``*/*``). Otherwise ``text/*`` will be the
-Content-Type. If your tracker supports markup languages
-(e.g. markdown), you should set the mime type (e.g. ``text/markdown``)
-when storing your message.
+Content-Type (similar to ``*/*``). Otherwise the default mime type
+``text/plain`` will be the Content-Type. The default mime type can be
+changed using ``interfaces.py`` in your tracker. So you can change the
+default ``text/plain`` to ``text/markdown`` using::
+
+ from roundup.rest import RestfulInstance
+ RestfulInstance.default_text_file_mimetype = "text/markdown"
+
+If your tracker supports markup languages (e.g. markdown), you should
+set the mime type (e.g. ``text/markdown``) when storing your message.
Note that the header ``X-Content-Type-Options: nosniff`` is returned
with a non javascript or xml binary_content response to prevent the
@@ -1877,6 +1884,8 @@ You can extend the rest api for a tracker. This describes how to add
new rest end points. At some point it will also describe the rest.py
structure and implementation.
+.. _adding_new_rest_endpoints:
+
Adding new rest endpoints
-------------------------
@@ -1892,7 +1901,7 @@ In that file add::
@Routing.route("/summary2")
@_data_decorator
- def summary2(self, input):
+ def summary2(self, input_payload):
result = { "hello": "world" }
return 200, result
@@ -1909,7 +1918,7 @@ Similarly appending this to interfaces.py after summary2::
# handle more endpoints
@Routing.route("/data/<:class_name>/@schema", 'GET')
- def get_element_schema(self, class_name, input):
+ def get_element_schema(self, class_name, input_payload):
result = { "schema": {} }
uid = self.db.getuid ()
if not self.db.security.hasPermission('View', uid, class_name) :
@@ -1959,7 +1968,7 @@ endpoints. Adding this as described above::
@Routing.route("/summary")
@_data_decorator
- def summary2(self, input):
+ def summary2(self, input_payload):
result = { "hello": "world" }
return 200, result
@@ -1976,7 +1985,7 @@ In addition to overriding existing endpoints, you can move existing
endpoints to new locations. Adding::
@Routing.route("/data2/<:classname>")
- def get_collection2(self, classname, input):
+ def get_collection2(self, classname, input_payload):
""" Remap existing function in rest.py to a new endpoint
Existing function is decorated with:
@@ -1987,7 +1996,7 @@ endpoints to new locations. Adding::
so we need to drop @_data_decorator from this function since
we can't apply @_data_decorator twice.
"""
- return self.get_collection(classname, input)
+ return self.get_collection(classname, input_payload)
will make the response at /rest/data2/ be the same as what is
normally at /rest/data/.
@@ -2024,7 +2033,7 @@ that returns a list of users with the developer role::
class RestfulInstance(object):
@Routing.route("/data/@permission/Developer")
- def get_role_Developer(self, input):
+ def get_role_Developer(self, input_payload):
'''An endpoint to return a list of users with Developer
role who can be assigned to an issue.
@@ -2065,14 +2074,14 @@ that returns a list of users with the developer role::
# deny all other url parmeters
return False
- # Cleanup input.list to prevent user from probing roles
+ # Cleanup input_payload.list to prevent user from probing roles
# or viewing things the user should not be able to view.
- input.list[:] = [ fs for fs in input.list
- if allowed_field(fs) ]
+ input_payload.list[:] = [ fs for fs in input_payload.list
+ if allowed_field(fs) ]
# Add the role filter required to implement the permission
# search
- input.list.append(MiniFieldStorage("roles", "Developer"))
+ input_payload.list.append(MiniFieldStorage("roles", "Developer"))
# change user to acquire permission to search roles
self.db.setCurrentUser('admin')
@@ -2080,7 +2089,7 @@ that returns a list of users with the developer role::
# Once we have cleaned up the request, pass it to
# get_collection as though /rest/data/users?... has been called
# to get @verbose and other args supported.
- return self.get_collection('user', input)
+ return self.get_collection('user', input_payload)
Calling this with::
@@ -2203,7 +2212,7 @@ only been tested with python3)::
class RestfulInstance(object):
@Routing.route("/jwt/issue", 'POST')
@_data_decorator
- def generate_jwt(self, input):
+ def generate_jwt(self, input_payload):
"""Create a JSON Web Token (jwt)
"""
import datetime
@@ -2230,7 +2239,7 @@ only been tested with python3)::
raise Unauthorised(denialmsg)
# verify we have input data.
- if not input:
+ if not input_payload:
raise UsageError("Missing data payload. "
"Verify Content-Type is sent")
@@ -2250,13 +2259,13 @@ only been tested with python3)::
}
lifetime = 0
- if 'lifetime' in input:
- if input['lifetime'].value != 'unlimited':
+ if 'lifetime' in input_payload:
+ if input_payload['lifetime'].value != 'unlimited':
try:
- lifetime = datetime.timedelta(seconds=int(input['lifetime'].value))
+ lifetime = datetime.timedelta(seconds=int(input_payload['lifetime'].value))
except ValueError:
raise UsageError("Value 'lifetime' must be 'unlimited' or an integer to specify" +
- " lifetime in seconds. Got %s."%input['lifetime'].value)
+ " lifetime in seconds. Got %s."%input_payload['lifetime'].value)
else:
lifetime = datetime.timedelta(seconds=86400) # 1 day by default
@@ -2264,8 +2273,8 @@ only been tested with python3)::
claim['exp'] = utcnow() + lifetime
newroles = []
- if 'roles' in input:
- for role in [ r.lower() for r in input['roles'].value ]:
+ if 'roles' in input_payload:
+ for role in [ r.lower() for r in input_payload['roles'].value ]:
if role not in rolenames:
raise UsageError("Role %s is not valid."%role)
if role in user_roles:
@@ -2295,12 +2304,12 @@ only been tested with python3)::
@Routing.route("/jwt/validate", 'GET')
@_data_decorator
- def validate_jwt(self,input):
+ def validate_jwt(self,input_payload):
import jwt
- if not 'jwt' in input:
+ if not 'jwt' in input_payload:
raise UsageError("jwt key must be specified")
- myjwt = input['jwt'].value
+ myjwt = input_payload['jwt'].value
secret = self.db.config.WEB_JWT_SECRET[0]
diff --git a/doc/security.txt b/doc/security.txt
index 11aaaf558..678ca3b38 100644
--- a/doc/security.txt
+++ b/doc/security.txt
@@ -28,21 +28,36 @@ source release tarballs.
CVE Announcements
-----------------
+ * `CVE-2026 pending `_ - :ref:`PATCH method bypasses
+ CSRF checks `. Fixed in release 2.6.0, directions
+ available for fixing issue in releases 2.0.0 - 2.5.0.
+
+ * `CVE-2026 pending `_ - :ref:`history doesn't
+ use check function to determine access to properties
+ `. Fixed in release 2.6.0, directions
+ available for fixing issue in prior releases.
+
* `CVE-2025-53865`_ - :ref:`XSS security issue with devel or
- responsive templates `. Fixed in release 2.5.0,
+ responsive templates `. Fixed in release 2.5.0,
directions available for fixing trackers based on these templates.
* `CVE-2024-39124`_ - :ref:`classhelpers (_generic.help.html) are
- vulnerable to an XSS attack. ` Requires fixing
+ vulnerable to an XSS attack. ` Requires fixing
tracker homes.
+
* `CVE-2024-39125`_ - :ref:`if Referer header is set to a script tag,
- it will be executed. ` Fixed in release 2.4.0,
+ it will be executed. ` Fixed in release 2.4.0,
directions available for fixing in prior versions.
+
* `CVE-2024-39126`_ - :ref:`PDF, XML and SVG files downloaded from an
issue can contain embedded JavaScript which is
- executed. ` Fixed in release 2.4.0, directions
+ executed. ` Fixed in release 2.4.0, directions
available for fixing in prior versions.
+.. _CVE-2026-csrf:
+ https://cve.mitre.org/cgi-bin/cvename.cgi?name=pending
+.. _CVE-2026-history-prop:
+ https://cve.mitre.org/cgi-bin/cvename.cgi?name=pending
.. _CVE-2025-53865:
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2025-53865
.. _CVE-2024-39124:
@@ -139,6 +154,7 @@ with 1.6.0 have been moved and are linked below:
.. rst-class:: multicol
+* `2.6.0 <../signatures/roundup-2.6.0.tar.gz.asc>`_
* `2.5.0 <../signatures/roundup-2.5.0.tar.gz.asc>`_
* `2.4.0 <../signatures/roundup-2.4.0.tar.gz.asc>`_
* `2.4.0b2 <../signatures/roundup-2.4.0b2.tar.gz.asc>`_
diff --git a/doc/tracker_config.txt b/doc/tracker_config.txt
index 1d132660f..8856cb8d2 100644
--- a/doc/tracker_config.txt
+++ b/doc/tracker_config.txt
@@ -421,6 +421,39 @@
# Default: 600
api_failed_login_interval_in_sec = 600
+ # Use a more modern CSRF protection method. If enabled, the
+ # other CSRF settings below are disabled and a CSRF token is
+ # not needed when creating your forms.
+ #
+ # Allowed values: yes, no
+ # Default: no
+ use_tokenless_csrf_protection = no
+
+ # A comma separated list of additonal valid Origin header
+ # values used when enforcing the origin header. If token style CSRF
+ # protection is enabled, it is used only for the api URLs (/rest and
+ # /xmlrpc). It is not used for the standard html URL's. If tokenless
+ # style csrf protection is enabled, it is used for URLs.
+ #
+ # These strings must precisely match the value of the Origin header. For
+ # example, 'https://bar.edu' and 'https://Bar.edu' are two different
+ # Origin values. Note origin values consist of scheme://host without any
+ # path component. Thus 'https://bar.edu/' is not valid. The value '*'
+ # can be used to match any origin when token style CSRF is enabled. It
+ # must be placed first in the list if used. Note that the '*' value
+ # allows any web page on the internet to make anonymous requests against
+ # your Roundup tracker.
+ #
+ # You must configure this if you have a web application on a different
+ # origin that accesses your Roundup instance.
+ #
+ # (The origin from the tracker.web setting in config.ini is
+ # always valid and does not need to be specified.)
+ # A list of space separated case sensitive
+ # origin headers 'scheme://host'.
+ # Default:
+ allowed_api_origins =
+
# How do we deal with @csrf fields in posted forms.
# Set this to 'required' to block the post and notify
# the user if the field is missing or invalid.
@@ -485,29 +518,6 @@
# Default: yes
csrf_enforce_header_origin = yes
- # A comma separated list of additonal valid Origin header
- # values used when enforcing the header origin. They are used
- # only for the api URL's (/rest and /xmlrpc). They are not
- # used for the usual html URL's. These strings must match the
- # value of the Origin header exactly. So 'https://bar.edu' and
- # 'https://Bar.edu' are two different Origin values. Note that
- # the origin value is scheme://host. There is no path
- # component. So 'https://bar.edu/' would never be valid.
- # The value '*' can be used to match any origin. It must be
- # first in the list if used. Note that this value allows
- # any web page on the internet to make anonymous requests
- # against your Roundup tracker.
- #
- # You need to set these if you have a web application on a
- # different origin accessing your Roundup instance.
- #
- # (The origin from the tracker.web setting in config.ini is
- # always valid and does not need to be specified.)
- # A list of space separated case sensitive
- # origin headers 'scheme://host'.
- # Default:
- allowed_api_origins =
-
# Verify that the X-Forwarded-Host http header matches
# the host part of the tracker.web setting in config.ini.
# Set this to 'required' to block the post and notify
@@ -867,6 +877,14 @@
# Default: ERROR
level = ERROR
+ # Format of the logging messages with all '%' signs
+ # doubled so they are not interpreted by the config file.
+ # Allowed value: Python LogRecord attribute named formats with % *sign doubled*.
+ # Also you can include the following attributes:
+ # %%(trace_id)s %%(trace_reason)s and %%(pct_char)s
+ # Default: %%(asctime)s %%(trace_id)s %%(levelname)s %%(message)s
+ format = %%(asctime)s %%(trace_id)s %%(levelname)s %%(message)s
+
# If set to yes, only the loggers configured in this section will
# be used. Yes will disable gunicorn's --access-logfile.
#
@@ -1112,12 +1130,12 @@
# If an email has only text/html parts, use this module
# to convert the html to text. Choose from beautifulsoup 4,
- # dehtml - (internal code), or none to disable conversion.
- # If 'none' is selected, email without a text/plain part
- # will be returned to the user with a message. If
- # beautifulsoup is selected but not installed dehtml will
- # be used instead.
- # Allowed values: beautifulsoup, dehtml, none
+ # justhtml, dehtml - (internal code), or none to disable
+ # conversion. If 'none' is selected, email without a text/plain
+ # part will be returned to the user with a message. If
+ # beautifulsoup or justhtml is selected but not installed
+ # dehtml will be used instead.
+ # Allowed values: beautifulsoup, justhtml, dehtml, none
# Default: none
convert_htmltotext = none
diff --git a/doc/upgrading-history.txt b/doc/upgrading-history.txt
index 20188a597..5e460a288 100644
--- a/doc/upgrading-history.txt
+++ b/doc/upgrading-history.txt
@@ -45,6 +45,57 @@ Contents:
.. contents::
:local:
+.. index:: Upgrading; 1.5.0 to 1.5.1
+
+Migrating from 1.5.0 to 1.5.1
+=============================
+
+User data visibility
+--------------------
+
+For security reasons you should change the permissions on the user
+class. We previously shipped a configuration that allowed users to see
+too many of other users details, including hashed passwords under
+certain circumstances. In schema.py in your tracker, replace the line::
+
+ db.security.addPermissionToRole('User', 'View', 'user')
+
+with::
+
+ p = db.security.addPermission(name='View', klass='user',
+ properties=('id', 'organisation', 'phone', 'realname',
+ 'timezone', 'username'))
+ db.security.addPermissionToRole('User', p)
+
+Note that this removes visibility of user emails, if you want emails to
+be visible you can add 'address' and 'alternate_addresses' to the list
+above.
+
+XSS protection for custom actions
+---------------------------------
+
+If you have defined your own cgi actions in your tracker instance
+(e.g. in a custom ``extensions/spambayes.py`` file) you need to modify
+all cases where client.error_message or client.ok_message are modified
+directly. Instead of::
+
+ self.client.ok_message.append(...)
+
+you need to call::
+
+ self.client.add_ok_message(...)
+
+and the same for::
+
+ self.client.error_message.append(...)
+
+vs.::
+
+ self.client.add_error_message(...)
+
+The new calls escape the passed string by default and avoid XSS security
+issues.
+
.. index:: Upgrading; 1.4.20 to 1.4.21
Migrating from 1.4.20 to 1.4.21
diff --git a/doc/upgrading.txt b/doc/upgrading.txt
index 66faca566..a5ed78c82 100644
--- a/doc/upgrading.txt
+++ b/doc/upgrading.txt
@@ -42,13 +42,13 @@ General steps:
Repeat for each tracker instance.
-.. note::
- The v1.5.x releases of Roundup were the last to support
+.. note:: The v1.5.x releases of Roundup were the last to support
Python v2.5 and v2.6. Starting with the v1.6 releases of Roundup
Python version 2.7 that is newer than 2.7.2 is required to run
- Roundup. Starting with Roundup version 2.0.0 we also support Python 3
- versions newer than 3.6. Roundup version 2.5 supports Python
- 3.7 and newer.
+ Roundup. Starting with Roundup version 2.0.0 we also support
+ Python 3 versions newer than 3.6. Roundup version 2.5 supports
+ Python 3.7 and newer. Roundup version 2.6 supports Python version
+ 3.10 and newer.
Recent release notes have the following labels:
@@ -58,6 +58,7 @@ Recent release notes have the following labels:
* **optional** - new features or changes to existing features you might
want to use
* **info** - important possibly visible changes in how things operate
+* **obsolete** - ignore these entries. They are no longer required.
If you use virtual environments for your installation, you
can run trackers with different versions of Roundup. So you
@@ -108,6 +109,222 @@ your database.
Migrating from 2.5.0 to 2.6.0
=============================
+Incomplete CSRF protection fixed (required)
+--------------------------------------------
+
+This release fixes cross site request forgery (CSRF) protection for
+the PATCH method. You do not have to change anything if you are
+running release 2.6.0 or newer. This bug was introduced in version
+2.0.0 when the REST interface that implements the PATCH method was
+added.
+
+The ``PATCH`` method is used with the rest interface and allows
+partial modification of a Roundup object/document. If you have REST
+turned off for your tracker, you are not vulnerable.
+
+This release also returns a 405 Method not allowed error for all
+methods except GET, HEAD, and POST when querying the main (HTML)
+interface. Previously, other methods acted like GET was specified.
+
+.. _CVE-2026-csrfL:
+
+Remediating missing PATCH CSRF protections
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you are using the REST API, there are two ways to fix this issue
+with releases 2.0.0 through 2.5.0:
+
+ 1. if you are not using PATCH in your workflow, disable the PATCH
+ method in your tracker,
+
+ 2. patch the cgi/client.py file to include the PATCH method in csrf
+ protections.
+
+Disable PATCH
+^^^^^^^^^^^^^
+
+To disable the PATCH method for your tracker add the following code to
+the file ``interfaces.py`` in your tracker's home directory::
+
+ from roundup.cgi.client import UsageError
+ from roundup.cgi.client import Client
+
+ original_handle_csrf = Client.handle_csrf
+
+ def csrf_wrapper_deny_patch(self, api=False):
+ method = self.env['REQUEST_METHOD']
+ if method == 'PATCH':
+ raise UsageError("Blocked Request: %s" % method)
+ return original_handle_csrf(self, api)
+
+ Client.handle_csrf = csrf_wrapper_deny_patch
+
+then restart your tracker. You can test it using::
+
+ curl -u user:password -X PATCH \
+ -H "Origin: " /rest/data/issue/1
+
+and you should receive::
+
+ { "error": { "status": 400, "msg": "Blocked Request: PATCH"}}
+
+This will disable the use of PATCH in all web requests. When you
+upgrade to release 2.6, remember to re-enable ``PATCH`` you can
+re-enable it by deleting the code above from ``interfaces.py``.
+
+Apply CSRF Protections
+^^^^^^^^^^^^^^^^^^^^^^
+
+However, if you need to use PATCH request, you can change the
+installed Roundup source and apply CSRF protections to the PATCH
+command.
+
+Make a backup copy of the file before changing it. Find the lines in
+cgi/client.py that look like::
+
+ # Assume: never allow changes via GET
+ if method not in ['POST', 'PUT', 'DELETE']:
+ if (self.form.list is not None) and ("@csrf" in self.form):
+
+Change the middle line so it looks like::
+
+ if method not in ['POST', 'PUT', 'DELETE', 'PATCH']:
+
+.. comment: end of CVE include marker
+
+
+Correct property permission check in history display (recommended)
+------------------------------------------------------------------
+
+If you are upgrading to 2.6, you don't have to do anything and you can
+skip this section.
+
+If you are running versions 1.6.0 to version 2.5 the filtering of
+history/journal items does not work correctly if the property is
+protected by a check function.
+
+.. _CVE-2026-history_propL:
+
+Remediating incomplete property permission check
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To fix this, you need to find the ``View`` and ``Edit`` checks in the
+history method in the ``roundup/hyperdb.py`` file. Make a backup copy
+of the file before making a change. (Note that the 1.6 releases only
+checked for ``View`` permissions.) In the 1.6 release it looks
+something like::
+
+ if enforceperm and not perm("View",
+ self.db.getuid(),
+ self.classname,
+ property=k ):
+ logger.debug("skipping unViewable property %s in >
+ k, j_repr)
+ del j[4][k]
+ continue
+
+Add::
+
+ itemid=nodeid
+
+to the View (and Edit check if present) so it looks like::
+
+ if enforceperm and not perm("View",
+ self.db.getuid(),
+ self.classname,
+ itemid=nodeid,
+ property=k )
+
+In the 2.5 and newer releases this code block includes a check for
+Edit permissions and when fixed looks like::
+
+ if enforceperm and not (perm("View",
+ uid,
+ self.classname,
+ itemid=nodeid,
+ property=key) or
+ perm("Edit",
+ uid,
+ self.classname,
+ itemid=nodeid,
+ property=key)):
+ logger.debug("skipping unaccessible property "
+ "%s::%s seen by user%s in %s",
+ self.classname, key, uid, j_repr)
+ del args[key]
+ continue
+
+.. comment: end of CVE include marker
+
+Default Logs Include Unique Request Identifier (info)
+-----------------------------------------------------
+
+The default logging format has been changed from::
+
+ %(asctime)s %(levelname)s %(message)s
+
+to::
+
+ %(asctime)s %(trace_id)s %(levelname)s %(message)s
+
+So logs now look like::
+
+ 2025-08-20 03:25:00,308 f6RPbT2s70vvJ2jFb9BQNF DEBUG get user1 cached
+
+which in the previous format would look like::
+
+ 2025-08-20 03:25:00,308 DEBUG get user1 cached
+
+The new format includes ``trace_id`` which is a thread and process
+unique identifier for a single request. So you can link together all
+of the log lines and determine where a slow down or other
+problem occurred.
+
+The logging format is now a ``config.ini`` parameter in the
+``logging`` section with the name ``format``. You can change it if you
+would like the old logging format without having to create a logging
+configuration file. See :ref:`rounduplogging` for details.
+
+Make Pagination Links Keep Search Name (optional)
+-------------------------------------------------
+
+When displaying a named search, index templates don't preserve
+the name when using the pagination (Next/Prev) links. This is
+fixed in the 2.6.0 templates for issues/bugs/tasks. To make the
+change to your templates, look for the pagination links (look for
+prev or previous case insensitive) in your tracker's html
+subdirectory and change::
+
+ request.indexargs_url(request.classname,
+ {'@startwith':prev.first, '@pagesize':prev.size})"
+
+to read::
+
+ request.indexargs_url(request.classname,
+ dict({'@dispname': request.dispname}
+ if request.dispname
+ else {},
+ **{'@startwith':prev.first, '@pagesize':prev.size}))"
+
+This code will be embedded in templating markup that is not shown
+above. The change above is for your previous/prev link. The
+change for the next pagination link is similar with::
+
+ {'@startwith':next.first, '@pagesize':next.size}
+
+replacing::
+
+ {'@startwith':prev.first, '@pagesize':prev.size}
+
+in the example.
+
+This moves the existing dictionary used to override the URL
+arguments to the second argument inside a ``dict()`` call. It
+also adds ``**`` before it. This change creates a new override
+dictionary that includes an ``@dispname`` parameter if it is set
+in the request. If ``@dispname`` is not set, the existing
+dictionary contents are used.
+
Support authorized changes in your tracker (optional)
-----------------------------------------------------
@@ -133,12 +350,296 @@ date, text etc.) do not need JavaScript to work.
See :ref:`Confirming the User` in the reference manual for details.
+Support for dictConfig Logging Configuration (optional)
+-------------------------------------------------------
+
+Roundup's basic log configuration via config.ini has always had the
+ability to use an ini style logging configuration to set levels per
+log channel, control output file rotation etc.
+
+With Roundup 2.6 you can use a JSON like file to configure logging
+using `dictConfig
+`_. The
+JSON file format as been enhanced to support comments that are
+stripped before being processed by the logging system.
+
+You can read about the details in the :ref:`admin manual `.
+
+Fix user.item.html template producing invalid Javascript (optional)
+-------------------------------------------------------------------
+
+The html template ``page.html`` in the classic, devel, minimal, and
+responsive tracker templates define a ``user_src_input`` macro. This
+macro produces invalid javascript for the ``onblur`` event when used
+by ``user.item.html``. The only effect from this bug is a javascript
+error reported in the user's browser when the user does not have edit
+permissions on the page. It doesn't have any user visible impact.
+
+If you want to fix this, replace::
+
+ tal:attributes="onblur python:edit_ok and 'split_name(this)';
+
+with::
+
+ tal:attributes="onblur python:'split_name(this)' if edit_ok else '';
+
+in the ``html/page.html`` file in your tracker.
+
+Allow users without a password to log in (optional)
+---------------------------------------------------
+
+You can configure a tracker to allow a login without a password.
+However the default html templates require the password field to
+be filled in. This prevents a login with an empty password.
+
+If you don't want to allow a login without a password, you can
+skip this section.
+
+This change automatically removes the required attribute if the
+``config.ini`` ``login_empty_passwords`` setting is enabled
+(true). The default is disabled with the value ``no``).
+
+This change is the default for the tracker templates in 2.6 and
+newer.
+
+To add this to your tracker, change the ``page.html`` (for TAL
+based trackers) or ``layout/navigation.html`` (for jinja2 trackers).
+
+For TAL trackers, replace the ``required`` parameter by finding
+the following password input in the tracker's ``html/page.html``
+file::
+
+
+
+and modifying it to look like::
+
+
+
+The equivalent change for jinja2's
+``html/layout/navigation.html`` based template starts with::
+
+
+
+and changes to::
+
+
+
+REST interface changes (info)
+-----------------------------
+
+The default type of a message file (the msg class) is now
+``text/plain``. This can be changed to a different mime type using
+`the interfaces.py file`_. See `the rest documentation on getting file
+content for details `_.
+
+Roundup supports a wildcard mime type. So ``text/*`` would match any
+text resource. The response to this request should have a full mime
+type like ``text/plain`` or ``text/csv`` etc. However Roundup used to
+return ``text/*`` which makes it difficult for the client to interpret
+the data. This release should do a better job of returning a proper
+subtype replacing the ``*``.
+
+The mime type of ``application/*`` resolves to ``application/json`` by
+default. Using it before resulted in an error.
+
+Malformed mime types assigned to files/messages could cause a crash,
+now they are just ignored.
+
+There have been some internal refactorings and improvements in the
+REST code that will make it a bit faster.
+
+Make File Contents Immutable for Everybody (optional)
+-----------------------------------------------------
+
+The HTML based interface for files and messages doesn't provide a way
+to change file content. However it is possible for privileged people
+to modify the content via the web. In most cases, this change will not
+be recorded in the audit log. It can be detected by looking at the
+change time of the file. Then compare it to the change time of files
+before and after it. Since files are created in order, the file
+``msg50`` should have a change timestamp after ``msg49`` and before
+``msg51``.
+
+The 2.6.0 release includes an immutable_file_contents.py
+detector. If you copy the detector into your tracker's detector
+directory nobody, including users with admin rights, can change
+file/msg contents via Roundup. Changes to files would have to be
+done by logging into the Roundup server and editing the files
+locally.
+
+For non-admin user's the following edit permission for FileClass based
+classes will prevent regular users from changing file content via
+Roundup. Remove the existing ``Edit`` permission from your FileClass
+based classes. Then add your classname to the loop. The permission
+strips ``content`` from the list of editable properties and permits
+editing of the other properties::
+
+ for cl in 'file', 'msg':
+ properties = list(x for x in
+ db.getclass(cl).getprops(protected=False).keys()
+ if x != 'content')
+
+ file_edit_perm = db.security.addPermission(
+ name='Edit', klass=cl,
+ properties=properties,
+ description="User is allowed to edit all %s props except content" % cl)
+
+ db.security.addPermissionToRole('User', file_edit_perm)
+
+Enable tokenless/nonceless CSRF protection (optional)
+-----------------------------------------------------
+
+This version implements CSRF protection using `Cross-Site Request
+Forgery by Filippo Valsorda `_.
+This is an effective method for CSRF protection and is much
+simpler as well. There are no configurable options unlike the 8
+options for the older method.
+
+It is off by default and must be manually enabled. You can enable
+it on a per-tracker basis using ``config.ini``. In the ``[web]``
+section of ``config.ini`` set ``use_tokenless_csrf_protection`` to
+``yes`` to enable it. This also disable validation using the older
+nonce and header based CSRF settings.
+
+When enabled, calls to ``utils.anti_csrf_nonce()`` will always
+return ``"0"``. As a result, you do not have to change your
+templates. Performance should be better since the random nonce is
+not generated nor saved to the otks database.
+
+Additional details can be found in the :ref:`nonceless CSRF` section of
+the reference guide.
+
+Make Query Edit Page Queries Display Query Name (optional)
+----------------------------------------------------------
+
+When queries are started from the main ``Your Queries`` menu section,
+the window shows the query name for TAL based trackers. Users can
+pick which queries/searches appear in the main menu's ``Your Queries``
+section by clicking the ``edit`` link to go to the edit screen. The
+``edit`` screen also lets users run the queries that aren't in the
+main section.
+
+Before this update, the query name wasn't shown when running queries
+from the ``edit`` screen. This update fixes that for all the included
+templates. It also makes the jinja2 issue index template show the
+query name.
+
+You can add this to your tracker. If you are not using a jinja2 based
+tracker, open your tracker's ``html/query.edit.html`` file. Find and
+change the line that includes::
+
+ {{ query.name.plain() }}
+
+with::
+
+ {{ query.name.plain() }}
+
+Then in the ``html/issue.index.html`` you will add::
+
+ {% if request.form['@dispname'] %}
+ - {{ request.form['@dispname'].value|u }}
+ {% endif %}
+
+to the ``head_title`` and ``page_header`` blocks so they look like::
+
+ {% block head_title %}
+ {% trans %}List of issues{% endtrans %}
+ {% if request.form['@dispname'] %}
+ - {{ request.form['@dispname'].value|u }}
+ {% endif %}
+ {% endblock %}
+
+ {% block page_header %}
+ {% trans %}List of issues{% endtrans %}
+ {% if request.form['@dispname'] %}
+ - {{ request.form['@dispname'].value|u }}
+ {% endif %}
+ {% endblock %}
+
+
+Modernizing Classic Tracker HTML (optional)
+-------------------------------------------
+
+This update improves the classic tracker by changing the page layout
+to use modern HTML and CSS instead of the old table layout from
+2001. All the updates are done by changing the ``page.html`` and the
+``style.css`` files.
+
+Among the fixes are:
+
+* HTML5 landmarks like header, main, search, and nav replace the
+ table that was used for layout.
+* a meta viewport tag was added for mobile devices.
+* the sidebar and main columns now use flexbox. They will stack and
+ turn into one column when the screen size is less than 600px.
+* The sidebar will change from 6 blocks in a column to 6 blocks in a
+ responsive grid when the screen size is under 600px.
+* Text in the nav/sidebar will have wrapped lines indented making it
+ easier to identify individual queries with long names.
+* The other templates in your tracker should not need any changes. If
+ they are too wide the updated ``page.html`` enables keyboard or
+ touch horizontal scrolling to see all of a table or other content.
+* A line now appears under a link when you hover over it.
+* The header uses flexbox to arrange the left side (logo/page
+ description) and right side (search) components.
+
+The most noticable change is that the page title now lines up with the
+edge of the screen instead of the edge of the main column. Other than
+that your tracker should look pretty much the same as before.
+
+Two files were changed to provide this basic responsiveness:
+``page.html`` and ``style.css``.
+
+Most local changes to ``page.html`` are done to add javascript. You
+should be able to copy the new page.html from the classic tracker
+template and put your ``script`` tags back in to benefit from this
+upgrade.
+
+You should also copy ``style.css`` into your tracker home's html
+directory and merge any local changes you made. At the top of the new
+``style.css`` are directions for two adjustments. Long query names
+that wrap in the sidebar will have their wrapped lines indented by
+0.5em. You can control this by uncommenting and adjusting the value of
+the css custom property ``--long-query-indent``. Additionally you can
+change the 600px viewport breakpoint by following the directions at
+the top of ``style.css``.
+
+Jinja Template Bootstrap Dependency Updated (optional)
+------------------------------------------------------
+
+A dependency for the jinja template has been updated. This is an
+upgrade to the latest 4.x stable release.
+
+bootstrap:
+ Upgraded from 4.4.1 to 4.6.2
+
+Copy the files: ``bootstrap.min.css`` and ``bootstrap.min.js`` from
+the jinja2 ``static`` :term:`template` directory (meaning 3) to your
+tracker's ``static`` directory.
+
+This change did not show any issues in testing, but you might want to
+hold onto your original files in case of an issue. If you experience
+and issue you should report it on the `roundup-users mailing list
+`_.
+
.. index:: Upgrading; 2.4.0 to 2.5.0
Migrating from 2.4.0 to 2.5.0
=============================
-.. _CVE-2025-53865:
+.. _CVE-2025-53865L:
XSS security issue with devel and responsive templates (recommended)
--------------------------------------------------------------------
@@ -376,6 +877,30 @@ references that use user supplied content
in the url. Also its not positional @sort=1 can appear anywhere in
the url.
+Modify Custom Rest Endpoints (required)
+---------------------------------------
+
+If there is no interfaces.py file in your tracker home, you can
+skip this section.
+
+In the course of code maintenance and cleanup, the variable
+``input`` used when defining new REST endpoints was renamed to
+``input_payload``. The ``input`` variable was renamed because it
+was shadowing the ``input`` built-in function.
+
+If you defined a custom REST endpoint in interfaces.py that looks
+like::
+
+ def summary2(self, input):
+
+you should change it to::
+
+ def summary2(self, input_payload):
+
+Then change any references to ``input`` inside the function to
+``input_payload``. The section :ref:`adding_new_rest_endpoints`
+has been updated if you need additional examples.
+
Deprecation Notices (required)
------------------------------
@@ -721,9 +1246,17 @@ request (tu.client.request), the translator for the current language
You can find an example in :ref:`dynamic_csp`.
-Directions for installing gpg (optional)
+.. _gpginstall:
+
+Directions for installing gpg (obsolete)
----------------------------------------
+.. note::
+ issue2551368 was closed in May 2026. Running ``pip install
+ gpg`` will install gpg 2.0.0 on all versions of Python supported by
+ Roundup. If you installed the test version use ``pip install -U
+ gpg`` to upgrade.
+
In this release a new version of the gpg module was needed for Ubuntu
24.04 and python 3.13. Paul Schwabauer produced a new version of the
gpg module. However it is only on the test instance of pypi. If you
@@ -763,7 +1296,7 @@ This will insert the bad API login rate limiting settings.
Also if you have ``html_version`` set to ``xhtml``, you will get
an error.
-.. _CVE-2024-39124:
+.. _CVE-2024-39124L:
Fix for CVE-2024-39124 in help/calendar popups (recommended)
------------------------------------------------------------
@@ -934,9 +1467,9 @@ bug. Splitting the large script into two parts:
allows use of ``structure`` on the script with no replaced
strings should it be required for your tracker.
-.. [#markdown-note] If you are using markdown formatting for your tracker's notes,
- the user will see the markdown label rather than the long
- (suspicious) URL. You may want to add something like::
+.. [#markdown-note] If you are using markdown formatting for your
+ tracker's notes, the user will see the markdown label rather than
+ the long (suspicious) URL. You may want to add something like::
a[href*=\@template]::after {
content: ' [' attr(href) ']';
@@ -955,7 +1488,7 @@ section. These fixes are already present in 2.4.0.
This section is for people who can not upgrade yet, and want
to fix the issues.
-.. _CVE-2024-39125:
+.. _CVE-2024-39125L:
Referer value not escaped CVE-2024-39125
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -983,7 +1516,7 @@ to::
This escapes the Referer value and prevents it from being
executed.
-.. _CVE-2024-39126:
+.. _CVE-2024-39126L:
Stop JavaScript execution from attached files CVE-2024-39126
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2173,7 +2706,7 @@ running::
roundup-admin -i table password,id,username
Look for lines starting with ``{CRYPT}``. You can reset the user's
-password using::
+password using [#history-pragma]_ ::
roundup-admin -i
roundup> set user16 password=somenewpassword
@@ -2184,6 +2717,14 @@ prompt). This prevents the new password from showing up in the output
of ps or shell history. The new password will be encrypted using the
default encryption method (usually pbkdf2).
+.. [#history-pragma] If your version of roundup-admin provides history
+ support, you should add ``-P history_features=2`` to the command
+ line or run ``pragma history_features=2`` at the ``roundup>``
+ prompt. This will prevent the command line (and password) from being
+ saved to your history file (usually ``.roundup_admin_history`` in
+ your user's home directory. You can use ``roundup-admin -i
+ pragma list`` to see if pragmas are supported.
+
Enable performance improvement for wsgi mode (optional)
-------------------------------------------------------
@@ -3575,56 +4116,6 @@ your tracker if you wish. It will create a new file in the
current working directory called 'httpd.log' and will rotate
the log file at 500K and keep two old copies of the file.
-.. index:: Upgrading; 1.5.0 to 1.5.1
-
-Migrating from 1.5.0 to 1.5.1
-=============================
-
-User data visibility
---------------------
-
-For security reasons you should change the permissions on the user
-class. We previously shipped a configuration that allowed users to see
-too many of other users details, including hashed passwords under
-certain circumstances. In schema.py in your tracker, replace the line::
-
- db.security.addPermissionToRole('User', 'View', 'user')
-
-with::
-
- p = db.security.addPermission(name='View', klass='user',
- properties=('id', 'organisation', 'phone', 'realname',
- 'timezone', 'username'))
- db.security.addPermissionToRole('User', p)
-
-Note that this removes visibility of user emails, if you want emails to
-be visible you can add 'address' and 'alternate_addresses' to the list
-above.
-
-XSS protection for custom actions
----------------------------------
-
-If you have defined your own cgi actions in your tracker instance
-(e.g. in a custom ``extensions/spambayes.py`` file) you need to modify
-all cases where client.error_message or client.ok_message are modified
-directly. Instead of::
-
- self.client.ok_message.append(...)
-
-you need to call::
-
- self.client.add_ok_message(...)
-
-and the same for::
-
- self.client.error_message.append(...)
-
-vs.::
-
- self.client.add_error_message(...)
-
-The new calls escape the passed string by default and avoid XSS security
-issues.
Migrating from older versions
diff --git a/frontends/ZRoundup/ZRoundup.py b/frontends/ZRoundup/ZRoundup.py
index b349e2469..6d90550e1 100644
--- a/frontends/ZRoundup/ZRoundup.py
+++ b/frontends/ZRoundup/ZRoundup.py
@@ -13,7 +13,7 @@
# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
-#
+#
''' ZRoundup module - exposes the roundup web interface to Zope
This frontend works by providing a thin layer that sits between Zope and the
@@ -26,15 +26,15 @@
advantages over regular CGI :)
'''
-from roundup.anypy import urllib_
-
-from Globals import InitializeClass, HTMLFile
-from OFS.SimpleItem import Item
+from AccessControl import ClassSecurityInfo, ModuleSecurityInfo
+from Acquisition import Implicit
+from Globals import HTMLFile, InitializeClass
from OFS.PropertyManager import PropertyManager
-from Acquisition import Explicit, Implicit
+from OFS.SimpleItem import Item
from Persistence import Persistent
-from AccessControl import ClassSecurityInfo
-from AccessControl import ModuleSecurityInfo
+
+from roundup.anypy import urllib_
+
modulesecurity = ModuleSecurityInfo()
import roundup.instance
@@ -45,6 +45,8 @@
manage_addZRoundupForm = HTMLFile('dtml/manage_addZRoundupForm', globals())
modulesecurity.declareProtected('Add Z Roundups', 'manage_addZRoundup')
+
+
def manage_addZRoundup(self, id, instance_home, REQUEST):
"""Add a ZRoundup product """
# validate the instance_home
@@ -52,25 +54,31 @@ def manage_addZRoundup(self, id, instance_home, REQUEST):
self._setObject(id, ZRoundup(id, instance_home))
return self.manage_main(self, REQUEST)
+
class RequestWrapper:
'''Make the Zope RESPONSE look like a BaseHTTPServer
'''
def __init__(self, RESPONSE):
self.RESPONSE = RESPONSE
self.wfile = self.RESPONSE
+
def send_response(self, status):
self.RESPONSE.setStatus(status)
+
def send_header(self, header, value):
self.RESPONSE.addHeader(header, value)
+
def end_headers(self):
# not needed - the RESPONSE object handles this internally on write()
pass
+
def start_response(self, headers, response):
self.send_response(response)
for key, value in headers:
self.send_header(key, value)
self.end_headers()
+
class FormItem:
'''Make a Zope form item look like a cgi.py one
'''
@@ -80,11 +88,13 @@ def __init__(self, value):
self.filename = self.value.filename
self.value = self.value.read()
+
class FormWrapper:
'''Make a Zope form dict look like a cgi.py one
'''
def __init__(self, form):
self.__form = form
+
def __getitem__(self, item):
entry = self.__form[item]
if isinstance(entry, type([])):
@@ -92,26 +102,30 @@ def __getitem__(self, item):
else:
entry = FormItem(entry)
return entry
+
def __iter__(self):
return iter(self.__form)
+
def getvalue(self, key, default=None):
if key in self.__form:
return self.__form[key]
- else:
- return default
+ return default
+
def has_key(self, item):
return item in self.__form
+
def keys(self):
return list(self.__form.keys())
def __repr__(self):
- return ''%self.__form
+ return '' % self.__form
+
class ZRoundup(Item, PropertyManager, Implicit, Persistent):
'''An instance of this class provides an interface between Zope and
roundup for one roundup instance
'''
- meta_type = 'Z Roundup'
+ meta_type = 'Z Roundup'
security = ClassSecurityInfo()
def __init__(self, id, instance_home):
@@ -120,19 +134,20 @@ def __init__(self, id, instance_home):
# define the properties that define this object
_properties = (
- {'id':'id', 'type': 'string', 'mode': 'w'},
- {'id':'instance_home', 'type': 'string', 'mode': 'w'},
+ {'id': 'id', 'type': 'string', 'mode': 'w'},
+ {'id': 'instance_home', 'type': 'string', 'mode': 'w'},
)
property_extensible_schema__ = 0
# define the tabs for the management interface
- manage_options= PropertyManager.manage_options + (
- {'label': 'View', 'action':'index_html'},
+ manage_options = PropertyManager.manage_options + (
+ {'label': 'View', 'action': 'index_html'},
) + Item.manage_options
icon = "misc_/ZRoundup/icon"
security.declarePrivate('roundup_opendb')
+
def roundup_opendb(self):
'''Open the roundup instance database for a transaction.
'''
@@ -141,17 +156,17 @@ def roundup_opendb(self):
env = self.REQUEST.environ
# figure out the path components to set
- url = urllib_.urlparse( self.absolute_url() )
+ url = urllib_.urlparse(self.absolute_url())
path = url[2]
- path_components = path.split( '/' )
+ path_components = path.split('/')
# special case when roundup is '/' in this virtual host,
- if path == "/" :
+ if path == "/":
env['SCRIPT_NAME'] = "/"
env['TRACKER_NAME'] = ''
- else :
+ else:
# all but the last element is the path
- env['SCRIPT_NAME'] = '/'.join( path_components[:-1] )
+ env['SCRIPT_NAME'] = '/'.join(path_components[:-1])
# the last element is the name
env['TRACKER_NAME'] = path_components[-1]
@@ -161,20 +176,21 @@ def roundup_opendb(self):
return client.Client(tracker, request, env, form)
security.declareProtected('View', 'index_html')
+
def index_html(self):
'''Alias index_html to roundup's index
'''
# Redirect misdirected requests -- bugs 558867 , 565992
# PATH_INFO, as defined by the CGI spec, has the *real* request path
orig_path = self.REQUEST.environ['PATH_INFO']
- if orig_path[-1] != '/' :
- url = urllib_.urlparse( self.absolute_url() )
- url = list( url ) # make mutable
- url[2] = url[2]+'/' # patch
- url = urllib_.urlunparse( url ) # reassemble
+ if orig_path[-1] != '/':
+ url = urllib_.urlparse(self.absolute_url())
+ url = list(url) # make mutable
+ url[2] = url[2] + '/' # patch
+ url = urllib_.urlunparse(url) # reassemble
RESPONSE = self.REQUEST.RESPONSE
- RESPONSE.setStatus( "MovedPermanently" ) # 301
- RESPONSE.setHeader( "Location" , url )
+ RESPONSE.setStatus("MovedPermanently") # 301
+ RESPONSE.setHeader("Location", url)
return RESPONSE
client = self.roundup_opendb()
@@ -187,6 +203,7 @@ def __getitem__(self, item):
'''
return PathElement(self, item).__of__(self)
+
class PathElement(Item, Implicit):
def __init__(self, zr, path):
self.zr = zr
@@ -204,18 +221,19 @@ def index_html(self, REQUEST=None):
client = self.zr.roundup_opendb()
# fake the path that roundup should use
client.path = self.path
- # and call roundup to do something
+ # and call roundup to do something
client.main()
return ''
except client.NotFound:
raise Exception('NotFound ' + REQUEST.URL)
pass
- except:
+ except Exception:
import traceback
traceback.print_exc()
# all other exceptions in roundup are valid
raise
+
InitializeClass(ZRoundup)
modulesecurity.apply(globals())
diff --git a/frontends/ZRoundup/__init__.py b/frontends/ZRoundup/__init__.py
index 29c64d411..c56b91c12 100644
--- a/frontends/ZRoundup/__init__.py
+++ b/frontends/ZRoundup/__init__.py
@@ -13,10 +13,11 @@
# FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
-#
-__version__='1.1'
+#
+__version__ = '1.1'
import os
+
# figure where ZRoundup is installed
here = None
if 'INSTANCE_HOME' in os.environ:
@@ -35,20 +36,24 @@
raise ValueError("Can't determine where ZRoundup is installed")
# product initialisation
-from .ZRoundup import ZRoundup, manage_addZRoundupForm, manage_addZRoundup
+from .ZRoundup import ZRoundup, manage_addZRoundup, manage_addZRoundupForm
+
+
def initialize(context):
context.registerClass(
ZRoundup,
- meta_type = 'Z Roundup',
- constructors = (
+ meta_type='Z Roundup',
+ constructors=(
manage_addZRoundupForm, manage_addZRoundup
)
)
+
# set up the icon
from ImageFile import ImageFile
+
misc_ = {
- 'icon': ImageFile('icons/tick_symbol.gif', path),
+ 'icon': ImageFile('icons/tick_symbol.gif', path),
}
diff --git a/frontends/roundup.cgi b/frontends/roundup.cgi
index 2d106740e..7655a7cdc 100755
--- a/frontends/roundup.cgi
+++ b/frontends/roundup.cgi
@@ -16,13 +16,17 @@
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
-# python version check
-from __future__ import print_function
-from roundup import version_check
-from roundup.i18n import _
+import os
+import sys
+import time
+
+# If roundup is not on your normal path, add it here.
+# sys.path.append('/home/username/develop/roundup_dev')
+from roundup import version_check # noqa: F401
+from roundup.anypy import http_
from roundup.anypy.html import html_escape
-from roundup.anypy.strings import s2b, StringIO
-import sys, time
+from roundup.anypy.strings import StringIO, s2b
+from roundup.i18n import _
#
## Configuration
@@ -31,17 +35,17 @@ import sys, time
# Configuration can also be provided through the OS environment (or via
# the Apache "SetEnv" configuration directive). If the variables
# documented below are set, they _override_ any configuation defaults
-# given in this file.
+# given in this file.
# TRACKER_HOMES is a list of trackers, in the form
# "NAME=DIRNAME2=DIR2...", where is the directory path
-# separator (";" on Windows, ":" on Unix).
+# separator (";" on Windows, ":" on Unix).
-# Make sure the NAME part doesn't include any url-unsafe characters like
+# Make sure the NAME part doesn't include any url-unsafe characters like
# spaces, as these confuse the cookie handling in browsers like IE.
# ROUNDUP_LOG is the name of the logfile; if it's empty or does not exist,
-# logging is turned off (unless you changed the default below).
+# logging is turned off (unless you changed the default below).
# DEBUG_TO_CLIENT specifies whether debugging goes to the HTTP server (via
# stderr) or to the web client (via cgitb).
@@ -52,15 +56,27 @@ TRACKER_HOMES = {
# 'example': '/path/to/example',
}
+# print timing at bottom of web page
+#TIMING = "INLINE"
+# print timing inside web page comments
+#TIMING = "COMMENT"
+# disable timing
+TIMING = ""
+
+
# Where to log debugging information to. Use an instance of DevNull if you
# don't want to log anywhere.
class DevNull:
def write(self, info):
pass
+
def close(self):
pass
+
def flush(self):
pass
+
+
#LOG = open('/var/log/roundup.cgi.log', 'a')
LOG = DevNull()
@@ -68,14 +84,16 @@ LOG = DevNull()
## end configuration
#
+DEFAULT_ERROR_MESSAGE = http_.server.DEFAULT_ERROR_MESSAGE
#
# Set up the error handler
-#
+#
try:
import traceback
+
from roundup.cgi import cgitb
-except:
+except ImportError:
print("Content-Type: text/plain\n")
print(_("Failed to import cgitb!\n\n"))
s = StringIO()
@@ -87,7 +105,6 @@ except:
# Check environment for config items
#
def checkconfig():
- import os
global TRACKER_HOMES, LOG
# see if there's an environment var. ROUNDUP_INSTANCE_HOMES is the
@@ -100,13 +117,13 @@ def checkconfig():
TRACKER_HOMES = {}
for home in homes.split(os.pathsep):
try:
- name, dir = home.split('=', 1)
+ name, dir_ = home.split('=', 1)
except ValueError:
# ignore invalid definitions
continue
- if name and dir:
- TRACKER_HOMES[name] = dir
-
+ if name and dir_:
+ TRACKER_HOMES[name] = dir_
+
logname = os.environ.get('ROUNDUP_LOG', '')
if logname:
LOG = open(logname, 'a')
@@ -123,29 +140,66 @@ class RequestWrapper:
def __init__(self, wfile):
self.rfile = sys.stdin
self.wfile = wfile
+ self.headers = {}
+
def write(self, data):
self.wfile.write(data)
+
def send_response(self, code):
- self.write(s2b('Status: %s\r\n'%code))
+ self.write(s2b('Status: %s\r\n' % code))
+
def send_header(self, keyword, value):
self.write(s2b("%s: %s\r\n" % (keyword, value)))
+
def end_headers(self):
self.write(b"\r\n")
+
def start_response(self, headers, response):
self.send_response(response)
for key, value in headers:
self.send_header(key, value)
self.end_headers()
+
#
# Main CGI handler
#
def main(out, err):
- import os
+ import roundup.cgi.client
import roundup.instance
- path = os.environ.get('PATH_INFO', '/').split('/')
+
+ if TIMING:
+ os.environ["CGI_SHOW_TIMING"] = TIMING
+
request = RequestWrapper(out)
+ problem_header = roundup.cgi.client.are_header_values_safe(os.environ)
+ if problem_header:
+ error_body = """
+
+
+
+ Error response
+
+
+
Error response
+
Error code: 400
+
Message: Invalid value: %s.
+
Error code explanation: 400 - The value for the header is unacceptable.
"
-#: ../roundup/cgi/cgitb.py:90
+#: ../roundup/cgi/cgitb.py:89
#, python-format
msgid "A problem occurred in your template \"%s\"."
msgstr "Ein Problem ist in der Vorlage \"%s\" aufgetreten."
-#: ../roundup/cgi/cgitb.py:98
+#: ../roundup/cgi/cgitb.py:97
#, python-format
msgid ""
"\n"
@@ -2071,17 +2174,17 @@ msgstr ""
" %(locals)s\n"
"\n"
-#: ../roundup/cgi/cgitb.py:117
+#: ../roundup/cgi/cgitb.py:116
msgid "Full traceback:"
msgstr "Vollständiger Traceback:"
-#: ../roundup/cgi/cgitb.py:133 ../roundup/cgi/cgitb.py:138
-#: ../roundup/cgi/cgitb.py:133:138
+#: ../roundup/cgi/cgitb.py:132 ../roundup/cgi/cgitb.py:137
+#: ../roundup/cgi/cgitb.py:132:137
#, python-format
msgid "%(exc_type)s: %(exc_value)s"
msgstr "%(exc_type)s: %(exc_value)s"
-#: ../roundup/cgi/cgitb.py:143
+#: ../roundup/cgi/cgitb.py:142
msgid ""
"
A problem occurred while running a Python script. Here is the sequence of "
"function calls leading up to the error, with the most recent (innermost) "
@@ -2091,24 +2194,24 @@ msgstr ""
"Sie die Aufrufe, welche zu dem Fehler führten. Der letzte (innerste) Aufruf "
"erscheint dabei zuoberst. Der Fehler hat folgende Attribute: "
-#: ../roundup/cgi/cgitb.py:152
+#: ../roundup/cgi/cgitb.py:151
msgid "<file is None - probably inside eval or exec>"
msgstr ""
"<file ist None - Wahrscheinlich in einem eval oder einem "
"exec>"
-#: ../roundup/cgi/cgitb.py:161
+#: ../roundup/cgi/cgitb.py:160
#, python-format
msgid "in %s"
msgstr "in %s"
# ../roundup/cgi/cgitb.py:145 :151
-#: ../roundup/cgi/cgitb.py:209 ../roundup/cgi/cgitb.py:215
-#: ../roundup/cgi/cgitb.py:209:215
+#: ../roundup/cgi/cgitb.py:208 ../roundup/cgi/cgitb.py:214
+#: ../roundup/cgi/cgitb.py:208:214
msgid "undefined"
msgstr "nicht definiert"
-#: ../roundup/cgi/client.py:111
+#: ../roundup/cgi/client.py:114
msgid ""
"
An error has occurred\n"
"
An error has occurred
\n"
@@ -2122,165 +2225,161 @@ msgstr ""
"Admistratoren wurden benachrichtigt.\n"
""
-#: ../roundup/cgi/client.py:679
+#: ../roundup/cgi/client.py:694
msgid ""
"XMLRPC endpoint is not using defusedxml. Improve security by installing "
"defusedxml."
msgstr ""
-#: ../roundup/cgi/client.py:749 ../roundup/cgi/client.py:771
-#: ../roundup/cgi/client.py:749:771
+#: ../roundup/cgi/client.py:764 ../roundup/cgi/client.py:786
+#: ../roundup/cgi/client.py:764:786
#, fuzzy
msgid "Client is not allowed to use Rest Interface."
msgstr "Gast-Benutzer sind nicht berechtigt, das Web-Interface zu benutzen."
-#: ../roundup/cgi/client.py:768 ../roundup/cgi/client.py:1682
-#: ../roundup/cgi/client.py:768:1682
+#: ../roundup/cgi/client.py:783 ../roundup/cgi/client.py:1903
+#: ../roundup/cgi/client.py:783:1903
msgid "Required Header Missing"
msgstr ""
-#: ../roundup/cgi/client.py:1021
+#: ../roundup/cgi/client.py:1071
msgid "Form Error: "
msgstr "Formular-Fehler: "
-#: ../roundup/cgi/client.py:1114
+#: ../roundup/cgi/client.py:1175
#, python-format
msgid "Unrecognized charset: %r"
msgstr "Zeichensatz nicht erkannt: %r"
-#: ../roundup/cgi/client.py:1381 ../roundup/cgi/client.py:2396
-#: ../roundup/cgi/client.py:1381:2396
+#: ../roundup/cgi/client.py:1442 ../roundup/cgi/client.py:2665
+#: ../roundup/cgi/client.py:1442:2665
msgid "broken form: multiple @action values submitted"
msgstr ""
-#: ../roundup/cgi/client.py:1398
+#: ../roundup/cgi/client.py:1460
msgid "Anonymous users are not allowed to use the web interface"
msgstr "Gast-Benutzer sind nicht berechtigt, das Web-Interface zu benutzen."
-#: ../roundup/cgi/client.py:1543
-msgid "Referer header not available."
+#: ../roundup/cgi/client.py:1562
+msgid "Request source headers not available."
msgstr ""
-#: ../roundup/cgi/client.py:1547
+#: ../roundup/cgi/client.py:1566
#, python-format
-msgid "csrf key used with wrong method from: %s"
+msgid "csrf key used with method %(method)s from: %(source)s"
+msgstr ""
+
+#: ../roundup/cgi/client.py:1683 ../roundup/cgi/client.py:1693
+#: ../roundup/cgi/client.py:1683:1693
+msgid "Unable to authorize request"
msgstr ""
-#: ../roundup/cgi/client.py:1576
+#: ../roundup/cgi/client.py:1797
#, python-format
msgid "csrf header %(header)s required but missing for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1580
+#: ../roundup/cgi/client.py:1801
#, python-format
msgid "Missing header: %s"
msgstr ""
-#: ../roundup/cgi/client.py:1588
+#: ../roundup/cgi/client.py:1809
#, python-format
msgid "csrf Referer header check failed for user%(userid)s. Value=%(referer)s"
msgstr ""
-#: ../roundup/cgi/client.py:1593
+#: ../roundup/cgi/client.py:1814
#, fuzzy, python-format
msgid "Invalid Referer: %s"
msgstr "Ungültiger Benutzername %s"
-#: ../roundup/cgi/client.py:1608
+#: ../roundup/cgi/client.py:1829
#, python-format
msgid "csrf Origin header check failed for user%(userid)s. Value=%(origin)s"
msgstr ""
-#: ../roundup/cgi/client.py:1613
+#: ../roundup/cgi/client.py:1834
#, fuzzy, python-format
msgid "Invalid Origin %s"
msgstr "Ungültiger Benutzername %s"
-#: ../roundup/cgi/client.py:1627
+#: ../roundup/cgi/client.py:1848
#, python-format
msgid ""
"csrf X-FORWARDED-HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1633
+#: ../roundup/cgi/client.py:1854
#, python-format
msgid "Invalid X-FORWARDED-HOST %s"
msgstr ""
-#: ../roundup/cgi/client.py:1652
+#: ../roundup/cgi/client.py:1873
#, python-format
msgid "csrf HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1657
+#: ../roundup/cgi/client.py:1878
#, fuzzy, python-format
msgid "Invalid HOST %s"
msgstr "Ungültige Anforderung %s"
-#: ../roundup/cgi/client.py:1665
+#: ../roundup/cgi/client.py:1886
msgid "Csrf: unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1666
+#: ../roundup/cgi/client.py:1887
msgid "Unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1679
+#: ../roundup/cgi/client.py:1900
#, python-format
msgid ""
"csrf X-REQUESTED-WITH xmlrpc required header check failed for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1720
+#: ../roundup/cgi/client.py:1941
#, python-format
msgid "Required csrf field missing for user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1723 ../roundup/cgi/client.py:1751
-#: ../roundup/cgi/client.py:1768 ../roundup/cgi/client.py:1723:1751:1768
+#: ../roundup/cgi/client.py:1944 ../roundup/cgi/client.py:1972
+#: ../roundup/cgi/client.py:1989 ../roundup/cgi/client.py:1944:1972:1989
msgid ""
"We can't validate your session (csrf failure). Re-enter any unsaved data and "
"try again."
msgstr ""
-#: ../roundup/cgi/client.py:1728
+#: ../roundup/cgi/client.py:1949
#, python-format
msgid "csrf field not supplied by user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1740
+#: ../roundup/cgi/client.py:1961
#, python-format
msgid ""
"Csrf mismatch user: current user %(user)s != stored user %(stored)s, current "
"session, stored session: %(cur_sess)s,%(stor_sess)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1758
+#: ../roundup/cgi/client.py:1979
#, python-format
msgid ""
"Csrf mismatch user: current session %(curr_sess)s != stored session "
"%(stor_sess)s, current user/stored user is: %(user)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1950
+#: ../roundup/cgi/client.py:2209
msgid "You are not allowed to view this file."
msgstr "Sie sind nicht berechtigt, diese Seite anzuzeigen."
-#: ../roundup/cgi/client.py:2274
+#: ../roundup/cgi/client.py:2544
#, python-format
msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
msgstr "%(starttag)sBenötigte Zeit: %(seconds)fs%(endtag)s\n"
-#: ../roundup/cgi/client.py:2278
-#, python-format
-msgid ""
-"%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading "
-"items: %(get_items)f secs. Filtering: %(filtering)f secs.%(endtag)s\n"
-msgstr ""
-"%(starttag)sCache benutzt: %(cache_hits)d, verfehlt: %(cache_misses)d. "
-"Einträge laden: %(get_items)fs; filtern: %(filtering)fs.%(endtag)s\n"
-
-#: ../roundup/cgi/client.py:2817
+#: ../roundup/cgi/client.py:3086
#, python-format
msgid ""
"Cache failure: compressed file %(compressed)s is older than its source file "
@@ -2346,72 +2445,72 @@ msgstr[1] ""
"Die Eigenschaften \"%(property)s\" müssen für die Klasse \"%(class)s\" "
"angegeben werden"
-#: ../roundup/cgi/templating.py:278
+#: ../roundup/cgi/templating.py:286
#, python-format
msgid "You are not allowed to %(action)s items of class %(class)s"
msgstr ""
"Sie sind nicht berechtigt, die Aktion \"%(action)s\" auf Einträge der "
"Klasse \"%(class)s\" anzuwenden"
-#: ../roundup/cgi/templating.py:892
+#: ../roundup/cgi/templating.py:900
msgid "(list)"
msgstr "(Liste)"
-#: ../roundup/cgi/templating.py:975
+#: ../roundup/cgi/templating.py:983
msgid "Submit New Entry"
msgstr "Eintrag speichern"
-#: ../roundup/cgi/templating.py:997 ../roundup/cgi/templating.py:1174
-#: ../roundup/cgi/templating.py:1809 ../roundup/cgi/templating.py:1840
-#: ../roundup/cgi/templating.py:1861 ../roundup/cgi/templating.py:1899
-#: ../roundup/cgi/templating.py:1952 ../roundup/cgi/templating.py:1976
-#: ../roundup/cgi/templating.py:1983 ../roundup/cgi/templating.py:2020
-#: ../roundup/cgi/templating.py:2077 ../roundup/cgi/templating.py:2115
-#: ../roundup/cgi/templating.py:2215 ../roundup/cgi/templating.py:2236
-#: ../roundup/cgi/templating.py:2357 ../roundup/cgi/templating.py:2377
-#: ../roundup/cgi/templating.py:2399 ../roundup/cgi/templating.py:2442
-#: ../roundup/cgi/templating.py:2452 ../roundup/cgi/templating.py:2517
-#: ../roundup/cgi/templating.py:2820 ../roundup/cgi/templating.py:997:1174
-#: :1809:1840 :1861:1899 :1952:1976 :1983:2020 :2077:2115 :2215:2236 :2357:2377
-#: :2399:2442 :2452:2517:2820
+#: ../roundup/cgi/templating.py:1005 ../roundup/cgi/templating.py:1182
+#: ../roundup/cgi/templating.py:1817 ../roundup/cgi/templating.py:1848
+#: ../roundup/cgi/templating.py:1869 ../roundup/cgi/templating.py:1907
+#: ../roundup/cgi/templating.py:1960 ../roundup/cgi/templating.py:1984
+#: ../roundup/cgi/templating.py:1991 ../roundup/cgi/templating.py:2028
+#: ../roundup/cgi/templating.py:2085 ../roundup/cgi/templating.py:2123
+#: ../roundup/cgi/templating.py:2223 ../roundup/cgi/templating.py:2244
+#: ../roundup/cgi/templating.py:2365 ../roundup/cgi/templating.py:2385
+#: ../roundup/cgi/templating.py:2407 ../roundup/cgi/templating.py:2450
+#: ../roundup/cgi/templating.py:2460 ../roundup/cgi/templating.py:2525
+#: ../roundup/cgi/templating.py:2828 ../roundup/cgi/templating.py:1005:1182
+#: :1817:1848 :1869:1907 :1960:1984 :1991:2028 :2085:2123 :2223:2244 :2365:2385
+#: :2407:2450 :2460:2525:2828
msgid "[hidden]"
msgstr "[verborgen]"
-#: ../roundup/cgi/templating.py:998
+#: ../roundup/cgi/templating.py:1006
msgid "New node - no history"
msgstr "Neuer Eintrag - Noch kein Verlauf"
-#: ../roundup/cgi/templating.py:1139
+#: ../roundup/cgi/templating.py:1147
msgid "Submit Changes"
msgstr "Speichern"
-#: ../roundup/cgi/templating.py:1217
+#: ../roundup/cgi/templating.py:1225
msgid "The indicated property no longer exists"
msgstr "Die gewählte Eigenschaft existiert nicht mehr"
-#: ../roundup/cgi/templating.py:1253
+#: ../roundup/cgi/templating.py:1261
#, python-format
msgid "The linked class %(classname)s no longer exists"
msgstr "Die verlinkte Klasse \"%(classname)s\" existiert nicht mehr"
-#: ../roundup/cgi/templating.py:1290 ../roundup/cgi/templating.py:1320
-#: ../roundup/cgi/templating.py:2526 ../roundup/cgi/templating.py:2829
-#: ../roundup/cgi/templating.py:1290:1320 :2526:2829
+#: ../roundup/cgi/templating.py:1298 ../roundup/cgi/templating.py:1328
+#: ../roundup/cgi/templating.py:2534 ../roundup/cgi/templating.py:2837
+#: ../roundup/cgi/templating.py:1298:1328 :2534:2837
msgid "[label is missing]"
msgstr ""
# ../roundup/cgi/templating.py:905 :926
-#: ../roundup/cgi/templating.py:1294 ../roundup/cgi/templating.py:1323
-#: ../roundup/cgi/templating.py:1294:1323
+#: ../roundup/cgi/templating.py:1302 ../roundup/cgi/templating.py:1331
+#: ../roundup/cgi/templating.py:1302:1331
msgid "The linked node no longer exists"
msgstr "Der verknüpfte Eintrag existiert nicht mehr"
-#: ../roundup/cgi/templating.py:1385
+#: ../roundup/cgi/templating.py:1393
#, python-format
msgid "%s: (no value)"
msgstr "%s: (kein Wert)"
-#: ../roundup/cgi/templating.py:1401
+#: ../roundup/cgi/templating.py:1409
#, fuzzy, python-format
msgid ""
"This event %s is not handled by the history display!"
@@ -2420,46 +2519,46 @@ msgstr ""
"Dieses Ereignis kann nicht im Verlauf angezeigt werden!"
"strong>"
-#: ../roundup/cgi/templating.py:1414
+#: ../roundup/cgi/templating.py:1422
msgid "
A problem occurred while running a Python script. Here is the sequence of "
"function calls leading up to the error, with the most recent (innermost) "
"call first. The exception attributes are:"
msgstr ""
-#: ../roundup/cgi/cgitb.py:152
+#: ../roundup/cgi/cgitb.py:151
msgid "<file is None - probably inside eval or exec>"
msgstr ""
-#: ../roundup/cgi/cgitb.py:161
+#: ../roundup/cgi/cgitb.py:160
#, python-format
msgid "in %s"
msgstr ""
-#: ../roundup/cgi/cgitb.py:209 ../roundup/cgi/cgitb.py:215
-#: ../roundup/cgi/cgitb.py:209:215
+#: ../roundup/cgi/cgitb.py:208 ../roundup/cgi/cgitb.py:214
+#: ../roundup/cgi/cgitb.py:208:214
msgid "undefined"
msgstr ""
-#: ../roundup/cgi/client.py:111
+#: ../roundup/cgi/client.py:114
msgid ""
"
"
-#: ../roundup/cgi/cgitb.py:90
+#: ../roundup/cgi/cgitb.py:89
#, python-format
msgid "A problem occurred in your template \"%s\"."
msgstr "Ha ocurrido un problema en su template \"%s\"."
-#: ../roundup/cgi/cgitb.py:98
+#: ../roundup/cgi/cgitb.py:97
#, python-format
msgid ""
"\n"
@@ -2088,17 +2191,17 @@ msgstr ""
" %(locals)s\n"
"\n"
-#: ../roundup/cgi/cgitb.py:117
+#: ../roundup/cgi/cgitb.py:116
msgid "Full traceback:"
msgstr "Traza completa"
-#: ../roundup/cgi/cgitb.py:133 ../roundup/cgi/cgitb.py:138
-#: ../roundup/cgi/cgitb.py:133:138
+#: ../roundup/cgi/cgitb.py:132 ../roundup/cgi/cgitb.py:137
+#: ../roundup/cgi/cgitb.py:132:137
#, python-format
msgid "%(exc_type)s: %(exc_value)s"
msgstr "%(exc_type)s: %(exc_value)s"
-#: ../roundup/cgi/cgitb.py:143
+#: ../roundup/cgi/cgitb.py:142
msgid ""
"
A problem occurred while running a Python script. Here is the sequence of "
"function calls leading up to the error, with the most recent (innermost) "
@@ -2108,23 +2211,23 @@ msgstr ""
"de llamadas a funciones que llevaron al error, con la llamada mas reciente "
"(la mas anidada) ubicada primera. Los atributos de la excepcin son:"
-#: ../roundup/cgi/cgitb.py:152
+#: ../roundup/cgi/cgitb.py:151
msgid "<file is None - probably inside eval or exec>"
msgstr ""
"<file es None - probablemente dentro de eval or exec>"
-#: ../roundup/cgi/cgitb.py:161
+#: ../roundup/cgi/cgitb.py:160
#, python-format
msgid "in %s"
msgstr "en %s"
# ../roundup/cgi/cgitb.py:172 :178
-#: ../roundup/cgi/cgitb.py:209 ../roundup/cgi/cgitb.py:215
-#: ../roundup/cgi/cgitb.py:209:215
+#: ../roundup/cgi/cgitb.py:208 ../roundup/cgi/cgitb.py:214
+#: ../roundup/cgi/cgitb.py:208:214
msgid "undefined"
msgstr "indefinido/a"
-#: ../roundup/cgi/client.py:111
+#: ../roundup/cgi/client.py:114
msgid ""
"
An error has occurred\n"
"
An error has occurred
\n"
@@ -2139,167 +2242,163 @@ msgstr ""
"p>\n"
""
-#: ../roundup/cgi/client.py:679
+#: ../roundup/cgi/client.py:694
msgid ""
"XMLRPC endpoint is not using defusedxml. Improve security by installing "
"defusedxml."
msgstr ""
-#: ../roundup/cgi/client.py:749 ../roundup/cgi/client.py:771
-#: ../roundup/cgi/client.py:749:771
+#: ../roundup/cgi/client.py:764 ../roundup/cgi/client.py:786
+#: ../roundup/cgi/client.py:764:786
#, fuzzy
msgid "Client is not allowed to use Rest Interface."
msgstr "Los usuarios anonimos no tienen permitido usar esta interfaz Web"
-#: ../roundup/cgi/client.py:768 ../roundup/cgi/client.py:1682
-#: ../roundup/cgi/client.py:768:1682
+#: ../roundup/cgi/client.py:783 ../roundup/cgi/client.py:1903
+#: ../roundup/cgi/client.py:783:1903
msgid "Required Header Missing"
msgstr ""
-#: ../roundup/cgi/client.py:1021
+#: ../roundup/cgi/client.py:1071
msgid "Form Error: "
msgstr "Error de formulario"
-#: ../roundup/cgi/client.py:1114
+#: ../roundup/cgi/client.py:1175
#, python-format
msgid "Unrecognized charset: %r"
msgstr "Conjunto de caracteres desconocido: %r"
-#: ../roundup/cgi/client.py:1381 ../roundup/cgi/client.py:2396
-#: ../roundup/cgi/client.py:1381:2396
+#: ../roundup/cgi/client.py:1442 ../roundup/cgi/client.py:2665
+#: ../roundup/cgi/client.py:1442:2665
msgid "broken form: multiple @action values submitted"
msgstr ""
-#: ../roundup/cgi/client.py:1398
+#: ../roundup/cgi/client.py:1460
msgid "Anonymous users are not allowed to use the web interface"
msgstr "Los usuarios anonimos no tienen permitido usar esta interfaz Web"
-#: ../roundup/cgi/client.py:1543
-msgid "Referer header not available."
+#: ../roundup/cgi/client.py:1562
+msgid "Request source headers not available."
msgstr ""
-#: ../roundup/cgi/client.py:1547
+#: ../roundup/cgi/client.py:1566
#, python-format
-msgid "csrf key used with wrong method from: %s"
+msgid "csrf key used with method %(method)s from: %(source)s"
+msgstr ""
+
+#: ../roundup/cgi/client.py:1683 ../roundup/cgi/client.py:1693
+#: ../roundup/cgi/client.py:1683:1693
+msgid "Unable to authorize request"
msgstr ""
-#: ../roundup/cgi/client.py:1576
+#: ../roundup/cgi/client.py:1797
#, python-format
msgid "csrf header %(header)s required but missing for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1580
+#: ../roundup/cgi/client.py:1801
#, python-format
msgid "Missing header: %s"
msgstr ""
-#: ../roundup/cgi/client.py:1588
+#: ../roundup/cgi/client.py:1809
#, python-format
msgid "csrf Referer header check failed for user%(userid)s. Value=%(referer)s"
msgstr ""
# ../roundup/cgi/actions.py:891 :895
-#: ../roundup/cgi/client.py:1593
+#: ../roundup/cgi/client.py:1814
#, fuzzy, python-format
msgid "Invalid Referer: %s"
msgstr "nombre de usuario contrasea invlidos"
-#: ../roundup/cgi/client.py:1608
+#: ../roundup/cgi/client.py:1829
#, python-format
msgid "csrf Origin header check failed for user%(userid)s. Value=%(origin)s"
msgstr ""
# ../roundup/cgi/actions.py:891 :895
-#: ../roundup/cgi/client.py:1613
+#: ../roundup/cgi/client.py:1834
#, fuzzy, python-format
msgid "Invalid Origin %s"
msgstr "nombre de usuario contrasea invlidos"
-#: ../roundup/cgi/client.py:1627
+#: ../roundup/cgi/client.py:1848
#, python-format
msgid ""
"csrf X-FORWARDED-HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1633
+#: ../roundup/cgi/client.py:1854
#, python-format
msgid "Invalid X-FORWARDED-HOST %s"
msgstr ""
-#: ../roundup/cgi/client.py:1652
+#: ../roundup/cgi/client.py:1873
#, python-format
msgid "csrf HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1657
+#: ../roundup/cgi/client.py:1878
#, python-format
msgid "Invalid HOST %s"
msgstr ""
-#: ../roundup/cgi/client.py:1665
+#: ../roundup/cgi/client.py:1886
msgid "Csrf: unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1666
+#: ../roundup/cgi/client.py:1887
msgid "Unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1679
+#: ../roundup/cgi/client.py:1900
#, python-format
msgid ""
"csrf X-REQUESTED-WITH xmlrpc required header check failed for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1720
+#: ../roundup/cgi/client.py:1941
#, python-format
msgid "Required csrf field missing for user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1723 ../roundup/cgi/client.py:1751
-#: ../roundup/cgi/client.py:1768 ../roundup/cgi/client.py:1723:1751:1768
+#: ../roundup/cgi/client.py:1944 ../roundup/cgi/client.py:1972
+#: ../roundup/cgi/client.py:1989 ../roundup/cgi/client.py:1944:1972:1989
msgid ""
"We can't validate your session (csrf failure). Re-enter any unsaved data and "
"try again."
msgstr ""
-#: ../roundup/cgi/client.py:1728
+#: ../roundup/cgi/client.py:1949
#, python-format
msgid "csrf field not supplied by user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1740
+#: ../roundup/cgi/client.py:1961
#, python-format
msgid ""
"Csrf mismatch user: current user %(user)s != stored user %(stored)s, current "
"session, stored session: %(cur_sess)s,%(stor_sess)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1758
+#: ../roundup/cgi/client.py:1979
#, python-format
msgid ""
"Csrf mismatch user: current session %(curr_sess)s != stored session "
"%(stor_sess)s, current user/stored user is: %(user)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1950
+#: ../roundup/cgi/client.py:2209
msgid "You are not allowed to view this file."
msgstr "Ud. no tiene permitido ver este fichero"
-#: ../roundup/cgi/client.py:2274
+#: ../roundup/cgi/client.py:2544
#, python-format
msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
msgstr "%(starttag)sTiempo transcurrido: %(seconds)fs%(endtag)s\n"
-#: ../roundup/cgi/client.py:2278
-#, python-format
-msgid ""
-"%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading "
-"items: %(get_items)f secs. Filtering: %(filtering)f secs.%(endtag)s\n"
-msgstr ""
-"%(starttag)sAciertos Cache: %(cache_hits)d, no aciertos %(cache_misses)d. "
-"Cargando items: %(get_items)f secs. Filtrado: %(filtering)f secs.%(endtag)s\n"
-
-#: ../roundup/cgi/client.py:2817
+#: ../roundup/cgi/client.py:3086
#, python-format
msgid ""
"Cache failure: compressed file %(compressed)s is older than its source file "
@@ -2322,8 +2421,8 @@ msgid ""
"The form action claims to require property \"%(property)s\" which doesn't "
"exist"
msgstr ""
-"La accion de formulario especifica que requiere la propiedad \"%(property)s"
-"\" la cual no existe"
+"La accion de formulario especifica que requiere la propiedad "
+"\"%(property)s\" la cual no existe"
#: ../roundup/cgi/form_parser.py:347
#, python-format
@@ -2369,72 +2468,72 @@ msgstr[1] ""
"Las propiedades %(property)s de la clase %(class)s son obligatorias y no se "
"han provisto"
-#: ../roundup/cgi/templating.py:278
+#: ../roundup/cgi/templating.py:286
#, python-format
msgid "You are not allowed to %(action)s items of class %(class)s"
msgstr "Ud. no tiene permitido %(action)s items de la clase %(class)s"
-#: ../roundup/cgi/templating.py:892
+#: ../roundup/cgi/templating.py:900
msgid "(list)"
msgstr "(lista)"
-#: ../roundup/cgi/templating.py:975
+#: ../roundup/cgi/templating.py:983
msgid "Submit New Entry"
msgstr "Crear nuevo elemento"
# ../roundup/cgi/templating.py:673 :792 :1166 :1187 :1231 :1253 :1287 :1326
# :1377 :1394 :1470 :1490 :1503 :1520 :1530 :1580 :1755
-#: ../roundup/cgi/templating.py:997 ../roundup/cgi/templating.py:1174
-#: ../roundup/cgi/templating.py:1809 ../roundup/cgi/templating.py:1840
-#: ../roundup/cgi/templating.py:1861 ../roundup/cgi/templating.py:1899
-#: ../roundup/cgi/templating.py:1952 ../roundup/cgi/templating.py:1976
-#: ../roundup/cgi/templating.py:1983 ../roundup/cgi/templating.py:2020
-#: ../roundup/cgi/templating.py:2077 ../roundup/cgi/templating.py:2115
-#: ../roundup/cgi/templating.py:2215 ../roundup/cgi/templating.py:2236
-#: ../roundup/cgi/templating.py:2357 ../roundup/cgi/templating.py:2377
-#: ../roundup/cgi/templating.py:2399 ../roundup/cgi/templating.py:2442
-#: ../roundup/cgi/templating.py:2452 ../roundup/cgi/templating.py:2517
-#: ../roundup/cgi/templating.py:2820 ../roundup/cgi/templating.py:997:1174
-#: :1809:1840 :1861:1899 :1952:1976 :1983:2020 :2077:2115 :2215:2236 :2357:2377
-#: :2399:2442 :2452:2517:2820
+#: ../roundup/cgi/templating.py:1005 ../roundup/cgi/templating.py:1182
+#: ../roundup/cgi/templating.py:1817 ../roundup/cgi/templating.py:1848
+#: ../roundup/cgi/templating.py:1869 ../roundup/cgi/templating.py:1907
+#: ../roundup/cgi/templating.py:1960 ../roundup/cgi/templating.py:1984
+#: ../roundup/cgi/templating.py:1991 ../roundup/cgi/templating.py:2028
+#: ../roundup/cgi/templating.py:2085 ../roundup/cgi/templating.py:2123
+#: ../roundup/cgi/templating.py:2223 ../roundup/cgi/templating.py:2244
+#: ../roundup/cgi/templating.py:2365 ../roundup/cgi/templating.py:2385
+#: ../roundup/cgi/templating.py:2407 ../roundup/cgi/templating.py:2450
+#: ../roundup/cgi/templating.py:2460 ../roundup/cgi/templating.py:2525
+#: ../roundup/cgi/templating.py:2828 ../roundup/cgi/templating.py:1005:1182
+#: :1817:1848 :1869:1907 :1960:1984 :1991:2028 :2085:2123 :2223:2244 :2365:2385
+#: :2407:2450 :2460:2525:2828
msgid "[hidden]"
msgstr "[oculto]"
-#: ../roundup/cgi/templating.py:998
+#: ../roundup/cgi/templating.py:1006
msgid "New node - no history"
msgstr "Nuevo nodo - sin historia"
-#: ../roundup/cgi/templating.py:1139
+#: ../roundup/cgi/templating.py:1147
msgid "Submit Changes"
msgstr "Enviar modificaciones"
-#: ../roundup/cgi/templating.py:1217
+#: ../roundup/cgi/templating.py:1225
msgid "The indicated property no longer exists"
msgstr "La propiedad indicada ya no existe"
-#: ../roundup/cgi/templating.py:1253
+#: ../roundup/cgi/templating.py:1261
#, python-format
msgid "The linked class %(classname)s no longer exists"
msgstr "La clase relacionada %(classname)s ya no existe"
-#: ../roundup/cgi/templating.py:1290 ../roundup/cgi/templating.py:1320
-#: ../roundup/cgi/templating.py:2526 ../roundup/cgi/templating.py:2829
-#: ../roundup/cgi/templating.py:1290:1320 :2526:2829
+#: ../roundup/cgi/templating.py:1298 ../roundup/cgi/templating.py:1328
+#: ../roundup/cgi/templating.py:2534 ../roundup/cgi/templating.py:2837
+#: ../roundup/cgi/templating.py:1298:1328 :2534:2837
msgid "[label is missing]"
msgstr ""
# ../roundup/cgi/templating.py:903 :924
-#: ../roundup/cgi/templating.py:1294 ../roundup/cgi/templating.py:1323
-#: ../roundup/cgi/templating.py:1294:1323
+#: ../roundup/cgi/templating.py:1302 ../roundup/cgi/templating.py:1331
+#: ../roundup/cgi/templating.py:1302:1331
msgid "The linked node no longer exists"
msgstr "El nodo relacionado ya no existe"
-#: ../roundup/cgi/templating.py:1385
+#: ../roundup/cgi/templating.py:1393
#, python-format
msgid "%s: (no value)"
msgstr "%s: (sin valor)"
-#: ../roundup/cgi/templating.py:1401
+#: ../roundup/cgi/templating.py:1409
#, fuzzy, python-format
msgid ""
"This event %s is not handled by the history display!"
@@ -2443,46 +2542,46 @@ msgstr ""
"Este evento no es soportado por la visualizacin de historia!"
"em>"
-#: ../roundup/cgi/templating.py:1414
+#: ../roundup/cgi/templating.py:1422
msgid "
A problem occurred while running a Python script. Here is the sequence of "
"function calls leading up to the error, with the most recent (innermost) "
@@ -2116,25 +2220,25 @@ msgstr ""
"suite d'appels de fonction menant l'erreur, avec l'appel le plus rcent "
"(le plus imbriqu) d'abord. Les attributs de l'exception sont:"
-#: ../roundup/cgi/cgitb.py:152
+#: ../roundup/cgi/cgitb.py:151
msgid "<file is None - probably inside eval or exec>"
msgstr ""
"<file est None - probablement dans un eval ou un "
"exec>"
-#: ../roundup/cgi/cgitb.py:161
+#: ../roundup/cgi/cgitb.py:160
#, python-format
msgid "in %s"
msgstr "dans %s"
# ../roundup/cgi/cgitb.py:172 :178
# ../roundup/cgi/cgitb.py:172:178
-#: ../roundup/cgi/cgitb.py:209 ../roundup/cgi/cgitb.py:215
-#: ../roundup/cgi/cgitb.py:209:215
+#: ../roundup/cgi/cgitb.py:208 ../roundup/cgi/cgitb.py:214
+#: ../roundup/cgi/cgitb.py:208:214
msgid "undefined"
msgstr "indfini"
-#: ../roundup/cgi/client.py:111
+#: ../roundup/cgi/client.py:114
msgid ""
"
An error has occurred\n"
"
An error has occurred
\n"
@@ -2148,172 +2252,167 @@ msgstr ""
"Les administrateurs du pisteur ont t notifis du problme.\n"
""
-#: ../roundup/cgi/client.py:679
+#: ../roundup/cgi/client.py:694
msgid ""
"XMLRPC endpoint is not using defusedxml. Improve security by installing "
"defusedxml."
msgstr ""
-#: ../roundup/cgi/client.py:749 ../roundup/cgi/client.py:771
-#: ../roundup/cgi/client.py:749:771
+#: ../roundup/cgi/client.py:764 ../roundup/cgi/client.py:786
+#: ../roundup/cgi/client.py:764:786
#, fuzzy
msgid "Client is not allowed to use Rest Interface."
msgstr ""
"Les utilisateurs anonymes ne sont pas autoriss utiliser l'interface Web"
-#: ../roundup/cgi/client.py:768 ../roundup/cgi/client.py:1682
-#: ../roundup/cgi/client.py:768:1682
+#: ../roundup/cgi/client.py:783 ../roundup/cgi/client.py:1903
+#: ../roundup/cgi/client.py:783:1903
msgid "Required Header Missing"
msgstr ""
-#: ../roundup/cgi/client.py:1021
+#: ../roundup/cgi/client.py:1071
msgid "Form Error: "
msgstr "Erreur de formulaire: "
-#: ../roundup/cgi/client.py:1114
+#: ../roundup/cgi/client.py:1175
#, python-format
msgid "Unrecognized charset: %r"
msgstr "Jeu de caractres non reconnu: %r"
-#: ../roundup/cgi/client.py:1381 ../roundup/cgi/client.py:2396
-#: ../roundup/cgi/client.py:1381:2396
+#: ../roundup/cgi/client.py:1442 ../roundup/cgi/client.py:2665
+#: ../roundup/cgi/client.py:1442:2665
msgid "broken form: multiple @action values submitted"
msgstr ""
-#: ../roundup/cgi/client.py:1398
+#: ../roundup/cgi/client.py:1460
msgid "Anonymous users are not allowed to use the web interface"
msgstr ""
"Les utilisateurs anonymes ne sont pas autoriss utiliser l'interface Web"
-#: ../roundup/cgi/client.py:1543
-msgid "Referer header not available."
+#: ../roundup/cgi/client.py:1562
+msgid "Request source headers not available."
msgstr ""
-#: ../roundup/cgi/client.py:1547
+#: ../roundup/cgi/client.py:1566
#, python-format
-msgid "csrf key used with wrong method from: %s"
+msgid "csrf key used with method %(method)s from: %(source)s"
msgstr ""
-#: ../roundup/cgi/client.py:1576
+#: ../roundup/cgi/client.py:1683 ../roundup/cgi/client.py:1693
+#: ../roundup/cgi/client.py:1683:1693
+msgid "Unable to authorize request"
+msgstr ""
+
+#: ../roundup/cgi/client.py:1797
#, python-format
msgid "csrf header %(header)s required but missing for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1580
+#: ../roundup/cgi/client.py:1801
#, python-format
msgid "Missing header: %s"
msgstr ""
-#: ../roundup/cgi/client.py:1588
+#: ../roundup/cgi/client.py:1809
#, python-format
msgid "csrf Referer header check failed for user%(userid)s. Value=%(referer)s"
msgstr ""
# ../roundup/cgi/actions.py:930 :934
# ../roundup/cgi/actions.py:930:934
-#: ../roundup/cgi/client.py:1593
+#: ../roundup/cgi/client.py:1814
#, fuzzy, python-format
msgid "Invalid Referer: %s"
msgstr "Tentative de connexion non valide"
-#: ../roundup/cgi/client.py:1608
+#: ../roundup/cgi/client.py:1829
#, python-format
msgid "csrf Origin header check failed for user%(userid)s. Value=%(origin)s"
msgstr ""
# ../roundup/cgi/actions.py:930 :934
# ../roundup/cgi/actions.py:930:934
-#: ../roundup/cgi/client.py:1613
+#: ../roundup/cgi/client.py:1834
#, fuzzy, python-format
msgid "Invalid Origin %s"
msgstr "Tentative de connexion non valide"
-#: ../roundup/cgi/client.py:1627
+#: ../roundup/cgi/client.py:1848
#, python-format
msgid ""
"csrf X-FORWARDED-HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1633
+#: ../roundup/cgi/client.py:1854
#, python-format
msgid "Invalid X-FORWARDED-HOST %s"
msgstr ""
-#: ../roundup/cgi/client.py:1652
+#: ../roundup/cgi/client.py:1873
#, python-format
msgid "csrf HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1657
+#: ../roundup/cgi/client.py:1878
#, python-format
msgid "Invalid HOST %s"
msgstr ""
-#: ../roundup/cgi/client.py:1665
+#: ../roundup/cgi/client.py:1886
msgid "Csrf: unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1666
+#: ../roundup/cgi/client.py:1887
msgid "Unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1679
+#: ../roundup/cgi/client.py:1900
#, python-format
msgid ""
"csrf X-REQUESTED-WITH xmlrpc required header check failed for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1720
+#: ../roundup/cgi/client.py:1941
#, python-format
msgid "Required csrf field missing for user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1723 ../roundup/cgi/client.py:1751
-#: ../roundup/cgi/client.py:1768 ../roundup/cgi/client.py:1723:1751:1768
+#: ../roundup/cgi/client.py:1944 ../roundup/cgi/client.py:1972
+#: ../roundup/cgi/client.py:1989 ../roundup/cgi/client.py:1944:1972:1989
msgid ""
"We can't validate your session (csrf failure). Re-enter any unsaved data and "
"try again."
msgstr ""
-#: ../roundup/cgi/client.py:1728
+#: ../roundup/cgi/client.py:1949
#, python-format
msgid "csrf field not supplied by user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1740
+#: ../roundup/cgi/client.py:1961
#, python-format
msgid ""
"Csrf mismatch user: current user %(user)s != stored user %(stored)s, current "
"session, stored session: %(cur_sess)s,%(stor_sess)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1758
+#: ../roundup/cgi/client.py:1979
#, python-format
msgid ""
"Csrf mismatch user: current session %(curr_sess)s != stored session "
"%(stor_sess)s, current user/stored user is: %(user)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1950
+#: ../roundup/cgi/client.py:2209
msgid "You are not allowed to view this file."
msgstr "Vous n'tes pas autoris voir ce fichier"
-#: ../roundup/cgi/client.py:2274
+#: ../roundup/cgi/client.py:2544
#, python-format
msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
msgstr "%(starttag)sTemps coul: %(seconds)fs%(endtag)s\n"
-#: ../roundup/cgi/client.py:2278
-#, python-format
-msgid ""
-"%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading "
-"items: %(get_items)f secs. Filtering: %(filtering)f secs.%(endtag)s\n"
-msgstr ""
-"%(starttag)sAccs au cache: %(cache_hits)d, manqus %(cache_misses)d. "
-"Chargement d'lments: %(get_items)f secondes. Filtrage: %(filtering)f "
-"secondes.%(endtag)s\n"
-
-#: ../roundup/cgi/client.py:2817
+#: ../roundup/cgi/client.py:3086
#, python-format
msgid ""
"Cache failure: compressed file %(compressed)s is older than its source file "
@@ -2382,16 +2481,16 @@ msgstr[0] "La propri
msgstr[1] ""
"Les proprits requises %(property)s de %(class)s n'ont pas t fournies"
-#: ../roundup/cgi/templating.py:278
+#: ../roundup/cgi/templating.py:286
#, python-format
msgid "You are not allowed to %(action)s items of class %(class)s"
msgstr "Vous ne pouvez pas %(action)s des lments de classe %(class)s"
-#: ../roundup/cgi/templating.py:892
+#: ../roundup/cgi/templating.py:900
msgid "(list)"
msgstr "(liste)"
-#: ../roundup/cgi/templating.py:975
+#: ../roundup/cgi/templating.py:983
msgid "Submit New Entry"
msgstr "Soumettre un nouvelle entre"
@@ -2400,58 +2499,58 @@ msgstr "Soumettre un nouvelle entr
# ../roundup/cgi/templating.py:1875 ../roundup/cgi/templating.py:710:829
# :1236:1257 :1304:1327 :1361:1400 :1453:1470 :1549:1569 :1587:1619
# :1629:1683 :1875
-#: ../roundup/cgi/templating.py:997 ../roundup/cgi/templating.py:1174
-#: ../roundup/cgi/templating.py:1809 ../roundup/cgi/templating.py:1840
-#: ../roundup/cgi/templating.py:1861 ../roundup/cgi/templating.py:1899
-#: ../roundup/cgi/templating.py:1952 ../roundup/cgi/templating.py:1976
-#: ../roundup/cgi/templating.py:1983 ../roundup/cgi/templating.py:2020
-#: ../roundup/cgi/templating.py:2077 ../roundup/cgi/templating.py:2115
-#: ../roundup/cgi/templating.py:2215 ../roundup/cgi/templating.py:2236
-#: ../roundup/cgi/templating.py:2357 ../roundup/cgi/templating.py:2377
-#: ../roundup/cgi/templating.py:2399 ../roundup/cgi/templating.py:2442
-#: ../roundup/cgi/templating.py:2452 ../roundup/cgi/templating.py:2517
-#: ../roundup/cgi/templating.py:2820 ../roundup/cgi/templating.py:997:1174
-#: :1809:1840 :1861:1899 :1952:1976 :1983:2020 :2077:2115 :2215:2236 :2357:2377
-#: :2399:2442 :2452:2517:2820
+#: ../roundup/cgi/templating.py:1005 ../roundup/cgi/templating.py:1182
+#: ../roundup/cgi/templating.py:1817 ../roundup/cgi/templating.py:1848
+#: ../roundup/cgi/templating.py:1869 ../roundup/cgi/templating.py:1907
+#: ../roundup/cgi/templating.py:1960 ../roundup/cgi/templating.py:1984
+#: ../roundup/cgi/templating.py:1991 ../roundup/cgi/templating.py:2028
+#: ../roundup/cgi/templating.py:2085 ../roundup/cgi/templating.py:2123
+#: ../roundup/cgi/templating.py:2223 ../roundup/cgi/templating.py:2244
+#: ../roundup/cgi/templating.py:2365 ../roundup/cgi/templating.py:2385
+#: ../roundup/cgi/templating.py:2407 ../roundup/cgi/templating.py:2450
+#: ../roundup/cgi/templating.py:2460 ../roundup/cgi/templating.py:2525
+#: ../roundup/cgi/templating.py:2828 ../roundup/cgi/templating.py:1005:1182
+#: :1817:1848 :1869:1907 :1960:1984 :1991:2028 :2085:2123 :2223:2244 :2365:2385
+#: :2407:2450 :2460:2525:2828
msgid "[hidden]"
msgstr "[masqu]"
-#: ../roundup/cgi/templating.py:998
+#: ../roundup/cgi/templating.py:1006
msgid "New node - no history"
msgstr "Nouveau n~ud - pas d'historique"
-#: ../roundup/cgi/templating.py:1139
+#: ../roundup/cgi/templating.py:1147
msgid "Submit Changes"
msgstr "Soumettre les changements"
-#: ../roundup/cgi/templating.py:1217
+#: ../roundup/cgi/templating.py:1225
msgid "The indicated property no longer exists"
msgstr "La proprit indique n'existe plus"
-#: ../roundup/cgi/templating.py:1253
+#: ../roundup/cgi/templating.py:1261
#, python-format
msgid "The linked class %(classname)s no longer exists"
msgstr "La classe lie %(classname)s n'existe plus"
-#: ../roundup/cgi/templating.py:1290 ../roundup/cgi/templating.py:1320
-#: ../roundup/cgi/templating.py:2526 ../roundup/cgi/templating.py:2829
-#: ../roundup/cgi/templating.py:1290:1320 :2526:2829
+#: ../roundup/cgi/templating.py:1298 ../roundup/cgi/templating.py:1328
+#: ../roundup/cgi/templating.py:2534 ../roundup/cgi/templating.py:2837
+#: ../roundup/cgi/templating.py:1298:1328 :2534:2837
msgid "[label is missing]"
msgstr ""
# ../roundup/cgi/templating.py:940 :964
# ../roundup/cgi/templating.py:940:964
-#: ../roundup/cgi/templating.py:1294 ../roundup/cgi/templating.py:1323
-#: ../roundup/cgi/templating.py:1294:1323
+#: ../roundup/cgi/templating.py:1302 ../roundup/cgi/templating.py:1331
+#: ../roundup/cgi/templating.py:1302:1331
msgid "The linked node no longer exists"
msgstr "Le noeud li n'existe plus"
-#: ../roundup/cgi/templating.py:1385
+#: ../roundup/cgi/templating.py:1393
#, python-format
msgid "%s: (no value)"
msgstr "%s: (pas de valeur)"
-#: ../roundup/cgi/templating.py:1401
+#: ../roundup/cgi/templating.py:1409
#, fuzzy, python-format
msgid ""
"This event %s is not handled by the history display!"
@@ -2460,31 +2559,31 @@ msgstr ""
"Cet vnement n'est pas gr par l'affichage de l'historique."
"em>"
-#: ../roundup/cgi/templating.py:1414
+#: ../roundup/cgi/templating.py:1422
msgid "
A problem occurred while running a Python script. Here is the sequence of "
"function calls leading up to the error, with the most recent (innermost) "
@@ -1771,24 +1873,24 @@ msgstr ""
"megtekinthető a hibához vezető függvényhívások sora, a legutóbbi (legbelső) "
"hívás látható legelőször. A kivétel tulajdonságai:"
-#: ../roundup/cgi/cgitb.py:152
+#: ../roundup/cgi/cgitb.py:151
msgid "<file is None - probably inside eval or exec>"
msgstr ""
"<A fájl None értékű - feltehetőleg eval vagy exec "
"utasításon belül>"
-#: ../roundup/cgi/cgitb.py:161
+#: ../roundup/cgi/cgitb.py:160
#, python-format
msgid "in %s"
msgstr "%s-ban"
# ../roundup/cgi/cgitb.py:172 :178
-#: ../roundup/cgi/cgitb.py:209 ../roundup/cgi/cgitb.py:215
-#: ../roundup/cgi/cgitb.py:209:215
+#: ../roundup/cgi/cgitb.py:208 ../roundup/cgi/cgitb.py:214
+#: ../roundup/cgi/cgitb.py:208:214
msgid "undefined"
msgstr "nem definiált"
-#: ../roundup/cgi/client.py:111
+#: ../roundup/cgi/client.py:114
msgid ""
"
An error has occurred\n"
"
An error has occurred
\n"
@@ -1802,167 +1904,163 @@ msgstr ""
"A hibakövető karbantartói értesítést kaptak a problémáról.\n"
""
-#: ../roundup/cgi/client.py:679
+#: ../roundup/cgi/client.py:694
msgid ""
"XMLRPC endpoint is not using defusedxml. Improve security by installing "
"defusedxml."
msgstr ""
-#: ../roundup/cgi/client.py:749 ../roundup/cgi/client.py:771
-#: ../roundup/cgi/client.py:749:771
+#: ../roundup/cgi/client.py:764 ../roundup/cgi/client.py:786
+#: ../roundup/cgi/client.py:764:786
#, fuzzy
msgid "Client is not allowed to use Rest Interface."
msgstr "Anonim felhasználók nem használhatják a webes felületet"
-#: ../roundup/cgi/client.py:768 ../roundup/cgi/client.py:1682
-#: ../roundup/cgi/client.py:768:1682
+#: ../roundup/cgi/client.py:783 ../roundup/cgi/client.py:1903
+#: ../roundup/cgi/client.py:783:1903
msgid "Required Header Missing"
msgstr ""
-#: ../roundup/cgi/client.py:1021
+#: ../roundup/cgi/client.py:1071
msgid "Form Error: "
msgstr "Űrlap hiba: "
-#: ../roundup/cgi/client.py:1114
+#: ../roundup/cgi/client.py:1175
#, python-format
msgid "Unrecognized charset: %r"
msgstr "Ismeretlen karakterkészlet: %r"
-#: ../roundup/cgi/client.py:1381 ../roundup/cgi/client.py:2396
-#: ../roundup/cgi/client.py:1381:2396
+#: ../roundup/cgi/client.py:1442 ../roundup/cgi/client.py:2665
+#: ../roundup/cgi/client.py:1442:2665
msgid "broken form: multiple @action values submitted"
msgstr ""
-#: ../roundup/cgi/client.py:1398
+#: ../roundup/cgi/client.py:1460
msgid "Anonymous users are not allowed to use the web interface"
msgstr "Anonim felhasználók nem használhatják a webes felületet"
-#: ../roundup/cgi/client.py:1543
-msgid "Referer header not available."
+#: ../roundup/cgi/client.py:1562
+msgid "Request source headers not available."
msgstr ""
-#: ../roundup/cgi/client.py:1547
+#: ../roundup/cgi/client.py:1566
#, python-format
-msgid "csrf key used with wrong method from: %s"
+msgid "csrf key used with method %(method)s from: %(source)s"
+msgstr ""
+
+#: ../roundup/cgi/client.py:1683 ../roundup/cgi/client.py:1693
+#: ../roundup/cgi/client.py:1683:1693
+msgid "Unable to authorize request"
msgstr ""
-#: ../roundup/cgi/client.py:1576
+#: ../roundup/cgi/client.py:1797
#, python-format
msgid "csrf header %(header)s required but missing for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1580
+#: ../roundup/cgi/client.py:1801
#, python-format
msgid "Missing header: %s"
msgstr ""
-#: ../roundup/cgi/client.py:1588
+#: ../roundup/cgi/client.py:1809
#, python-format
msgid "csrf Referer header check failed for user%(userid)s. Value=%(referer)s"
msgstr ""
# ../roundup/cgi/actions.py:930 :934
-#: ../roundup/cgi/client.py:1593
+#: ../roundup/cgi/client.py:1814
#, fuzzy, python-format
msgid "Invalid Referer: %s"
msgstr "Hibás bejelentkezés"
-#: ../roundup/cgi/client.py:1608
+#: ../roundup/cgi/client.py:1829
#, python-format
msgid "csrf Origin header check failed for user%(userid)s. Value=%(origin)s"
msgstr ""
# ../roundup/cgi/actions.py:930 :934
-#: ../roundup/cgi/client.py:1613
+#: ../roundup/cgi/client.py:1834
#, fuzzy, python-format
msgid "Invalid Origin %s"
msgstr "Hibás bejelentkezés"
-#: ../roundup/cgi/client.py:1627
+#: ../roundup/cgi/client.py:1848
#, python-format
msgid ""
"csrf X-FORWARDED-HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1633
+#: ../roundup/cgi/client.py:1854
#, python-format
msgid "Invalid X-FORWARDED-HOST %s"
msgstr ""
-#: ../roundup/cgi/client.py:1652
+#: ../roundup/cgi/client.py:1873
#, python-format
msgid "csrf HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1657
+#: ../roundup/cgi/client.py:1878
#, python-format
msgid "Invalid HOST %s"
msgstr ""
-#: ../roundup/cgi/client.py:1665
+#: ../roundup/cgi/client.py:1886
msgid "Csrf: unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1666
+#: ../roundup/cgi/client.py:1887
msgid "Unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1679
+#: ../roundup/cgi/client.py:1900
#, python-format
msgid ""
"csrf X-REQUESTED-WITH xmlrpc required header check failed for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1720
+#: ../roundup/cgi/client.py:1941
#, python-format
msgid "Required csrf field missing for user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1723 ../roundup/cgi/client.py:1751
-#: ../roundup/cgi/client.py:1768 ../roundup/cgi/client.py:1723:1751:1768
+#: ../roundup/cgi/client.py:1944 ../roundup/cgi/client.py:1972
+#: ../roundup/cgi/client.py:1989 ../roundup/cgi/client.py:1944:1972:1989
msgid ""
"We can't validate your session (csrf failure). Re-enter any unsaved data and "
"try again."
msgstr ""
-#: ../roundup/cgi/client.py:1728
+#: ../roundup/cgi/client.py:1949
#, python-format
msgid "csrf field not supplied by user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1740
+#: ../roundup/cgi/client.py:1961
#, python-format
msgid ""
"Csrf mismatch user: current user %(user)s != stored user %(stored)s, current "
"session, stored session: %(cur_sess)s,%(stor_sess)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1758
+#: ../roundup/cgi/client.py:1979
#, python-format
msgid ""
"Csrf mismatch user: current session %(curr_sess)s != stored session "
"%(stor_sess)s, current user/stored user is: %(user)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1950
+#: ../roundup/cgi/client.py:2209
msgid "You are not allowed to view this file."
msgstr "Nem nézheti meg ezt a fájlt."
-#: ../roundup/cgi/client.py:2274
+#: ../roundup/cgi/client.py:2544
#, python-format
msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
msgstr "%(starttag)sEltelt idő: %(seconds)fs%(endtag)s\n"
-#: ../roundup/cgi/client.py:2278
-#, python-format
-msgid ""
-"%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading "
-"items: %(get_items)f secs. Filtering: %(filtering)f secs.%(endtag)s\n"
-msgstr ""
-"%(starttag)sCache találatok: %(cache_hits)d, tévedés %(cache_misses)d. "
-"Elemek betöltése: %(get_items)f mp. Szűrés: %(filtering)f mp.%(endtag)s\n"
-
-#: ../roundup/cgi/client.py:2817
+#: ../roundup/cgi/client.py:3086
#, python-format
msgid ""
"Cache failure: compressed file %(compressed)s is older than its source file "
@@ -2027,74 +2125,74 @@ msgstr[0] "Nincs megadva a(z) %(class)s kötelező %(property)s tulajdonsága"
msgstr[1] ""
"Nincsenek megadva a(z) %(class)s kötelező %(property)s tulajdonságai"
-#: ../roundup/cgi/templating.py:278
+#: ../roundup/cgi/templating.py:286
#, python-format
msgid "You are not allowed to %(action)s items of class %(class)s"
msgstr ""
"Nincs jogosultsága a(z) %(class)s osztály elemein %(action)s műveletet "
"végrehajtani"
-#: ../roundup/cgi/templating.py:892
+#: ../roundup/cgi/templating.py:900
msgid "(list)"
msgstr "(lista)"
-#: ../roundup/cgi/templating.py:975
+#: ../roundup/cgi/templating.py:983
msgid "Submit New Entry"
msgstr "Létrehozás"
# ../roundup/cgi/templating.py:710 :829 :1236 :1257 :1304 :1327 :1361 :1400
# :1453 :1470 :1549 :1569 :1587 :1619 :1629 :1683 :1875
-#: ../roundup/cgi/templating.py:997 ../roundup/cgi/templating.py:1174
-#: ../roundup/cgi/templating.py:1809 ../roundup/cgi/templating.py:1840
-#: ../roundup/cgi/templating.py:1861 ../roundup/cgi/templating.py:1899
-#: ../roundup/cgi/templating.py:1952 ../roundup/cgi/templating.py:1976
-#: ../roundup/cgi/templating.py:1983 ../roundup/cgi/templating.py:2020
-#: ../roundup/cgi/templating.py:2077 ../roundup/cgi/templating.py:2115
-#: ../roundup/cgi/templating.py:2215 ../roundup/cgi/templating.py:2236
-#: ../roundup/cgi/templating.py:2357 ../roundup/cgi/templating.py:2377
-#: ../roundup/cgi/templating.py:2399 ../roundup/cgi/templating.py:2442
-#: ../roundup/cgi/templating.py:2452 ../roundup/cgi/templating.py:2517
-#: ../roundup/cgi/templating.py:2820 ../roundup/cgi/templating.py:997:1174
-#: :1809:1840 :1861:1899 :1952:1976 :1983:2020 :2077:2115 :2215:2236 :2357:2377
-#: :2399:2442 :2452:2517:2820
+#: ../roundup/cgi/templating.py:1005 ../roundup/cgi/templating.py:1182
+#: ../roundup/cgi/templating.py:1817 ../roundup/cgi/templating.py:1848
+#: ../roundup/cgi/templating.py:1869 ../roundup/cgi/templating.py:1907
+#: ../roundup/cgi/templating.py:1960 ../roundup/cgi/templating.py:1984
+#: ../roundup/cgi/templating.py:1991 ../roundup/cgi/templating.py:2028
+#: ../roundup/cgi/templating.py:2085 ../roundup/cgi/templating.py:2123
+#: ../roundup/cgi/templating.py:2223 ../roundup/cgi/templating.py:2244
+#: ../roundup/cgi/templating.py:2365 ../roundup/cgi/templating.py:2385
+#: ../roundup/cgi/templating.py:2407 ../roundup/cgi/templating.py:2450
+#: ../roundup/cgi/templating.py:2460 ../roundup/cgi/templating.py:2525
+#: ../roundup/cgi/templating.py:2828 ../roundup/cgi/templating.py:1005:1182
+#: :1817:1848 :1869:1907 :1960:1984 :1991:2028 :2085:2123 :2223:2244 :2365:2385
+#: :2407:2450 :2460:2525:2828
msgid "[hidden]"
msgstr "[rejtett]"
-#: ../roundup/cgi/templating.py:998
+#: ../roundup/cgi/templating.py:1006
msgid "New node - no history"
msgstr "Új bejegyzés - nincs történet"
-#: ../roundup/cgi/templating.py:1139
+#: ../roundup/cgi/templating.py:1147
msgid "Submit Changes"
msgstr "Változások mentése"
-#: ../roundup/cgi/templating.py:1217
+#: ../roundup/cgi/templating.py:1225
msgid "The indicated property no longer exists"
msgstr "A jelzett tulajdonság már nem létezik"
-#: ../roundup/cgi/templating.py:1253
+#: ../roundup/cgi/templating.py:1261
#, python-format
msgid "The linked class %(classname)s no longer exists"
msgstr "A csatolt %(classname)s osztály már nem létezik"
-#: ../roundup/cgi/templating.py:1290 ../roundup/cgi/templating.py:1320
-#: ../roundup/cgi/templating.py:2526 ../roundup/cgi/templating.py:2829
-#: ../roundup/cgi/templating.py:1290:1320 :2526:2829
+#: ../roundup/cgi/templating.py:1298 ../roundup/cgi/templating.py:1328
+#: ../roundup/cgi/templating.py:2534 ../roundup/cgi/templating.py:2837
+#: ../roundup/cgi/templating.py:1298:1328 :2534:2837
msgid "[label is missing]"
msgstr ""
# ../roundup/cgi/templating.py:940 :964
-#: ../roundup/cgi/templating.py:1294 ../roundup/cgi/templating.py:1323
-#: ../roundup/cgi/templating.py:1294:1323
+#: ../roundup/cgi/templating.py:1302 ../roundup/cgi/templating.py:1331
+#: ../roundup/cgi/templating.py:1302:1331
msgid "The linked node no longer exists"
msgstr "A csatolt bejegyzés már nem létezik"
-#: ../roundup/cgi/templating.py:1385
+#: ../roundup/cgi/templating.py:1393
#, python-format
msgid "%s: (no value)"
msgstr "%s: (nincs érték)"
-#: ../roundup/cgi/templating.py:1401
+#: ../roundup/cgi/templating.py:1409
#, fuzzy, python-format
msgid ""
"This event %s is not handled by the history display!"
@@ -2102,48 +2200,48 @@ msgid ""
msgstr ""
"Az előzmények képernyő nem kezeli ezt az eseményt!"
-#: ../roundup/cgi/templating.py:1414
+#: ../roundup/cgi/templating.py:1422
msgid "
A problem occurred while running a Python script. Here is the sequence of "
"function calls leading up to the error, with the most recent (innermost) "
@@ -1688,22 +1789,22 @@ msgstr ""
"la sequenza di chiamate di sistema che hanno condotto a questo errore, con "
"la più recente (la più interna) prima. Le eccezioni sono:"
-#: ../roundup/cgi/cgitb.py:152
+#: ../roundup/cgi/cgitb.py:151
msgid "<file is None - probably inside eval or exec>"
msgstr ""
-#: ../roundup/cgi/cgitb.py:161
+#: ../roundup/cgi/cgitb.py:160
#, python-format
msgid "in %s"
msgstr "in %s"
# ../roundup/cgi/cgitb.py:172:178
-#: ../roundup/cgi/cgitb.py:209 ../roundup/cgi/cgitb.py:215
-#: ../roundup/cgi/cgitb.py:209:215
+#: ../roundup/cgi/cgitb.py:208 ../roundup/cgi/cgitb.py:214
+#: ../roundup/cgi/cgitb.py:208:214
msgid "undefined"
msgstr "indefinito"
-#: ../roundup/cgi/client.py:111
+#: ../roundup/cgi/client.py:114
msgid ""
"
An error has occurred\n"
"
An error has occurred
\n"
@@ -1717,167 +1818,165 @@ msgstr ""
"La notifica del problema è stata notificata al manutentore del tracker.\n"
""
-#: ../roundup/cgi/client.py:679
+#: ../roundup/cgi/client.py:694
msgid ""
"XMLRPC endpoint is not using defusedxml. Improve security by installing "
"defusedxml."
msgstr ""
-#: ../roundup/cgi/client.py:749 ../roundup/cgi/client.py:771
-#: ../roundup/cgi/client.py:749:771
+#: ../roundup/cgi/client.py:764 ../roundup/cgi/client.py:786
+#: ../roundup/cgi/client.py:764:786
#, fuzzy
msgid "Client is not allowed to use Rest Interface."
msgstr ""
"Gli utenti anonimi non hanno il permesso di utilizzare l'interfaccia web"
-#: ../roundup/cgi/client.py:768 ../roundup/cgi/client.py:1682
-#: ../roundup/cgi/client.py:768:1682
+#: ../roundup/cgi/client.py:783 ../roundup/cgi/client.py:1903
+#: ../roundup/cgi/client.py:783:1903
msgid "Required Header Missing"
msgstr ""
-#: ../roundup/cgi/client.py:1021
+#: ../roundup/cgi/client.py:1071
msgid "Form Error: "
msgstr "Errore nella Form: "
-#: ../roundup/cgi/client.py:1114
+#: ../roundup/cgi/client.py:1175
#, python-format
msgid "Unrecognized charset: %r"
msgstr "Codice di carattere sconosciuto: %r"
-#: ../roundup/cgi/client.py:1381 ../roundup/cgi/client.py:2396
-#: ../roundup/cgi/client.py:1381:2396
+#: ../roundup/cgi/client.py:1442 ../roundup/cgi/client.py:2665
+#: ../roundup/cgi/client.py:1442:2665
msgid "broken form: multiple @action values submitted"
msgstr ""
-#: ../roundup/cgi/client.py:1398
+#: ../roundup/cgi/client.py:1460
msgid "Anonymous users are not allowed to use the web interface"
msgstr ""
"Gli utenti anonimi non hanno il permesso di utilizzare l'interfaccia web"
-#: ../roundup/cgi/client.py:1543
-msgid "Referer header not available."
+#: ../roundup/cgi/client.py:1562
+msgid "Request source headers not available."
msgstr ""
-#: ../roundup/cgi/client.py:1547
+#: ../roundup/cgi/client.py:1566
#, python-format
-msgid "csrf key used with wrong method from: %s"
+msgid "csrf key used with method %(method)s from: %(source)s"
msgstr ""
-#: ../roundup/cgi/client.py:1576
+#: ../roundup/cgi/client.py:1683 ../roundup/cgi/client.py:1693
+#: ../roundup/cgi/client.py:1683:1693
+msgid "Unable to authorize request"
+msgstr ""
+
+#: ../roundup/cgi/client.py:1797
#, python-format
msgid "csrf header %(header)s required but missing for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1580
+#: ../roundup/cgi/client.py:1801
#, python-format
msgid "Missing header: %s"
msgstr ""
-#: ../roundup/cgi/client.py:1588
+#: ../roundup/cgi/client.py:1809
#, python-format
msgid "csrf Referer header check failed for user%(userid)s. Value=%(referer)s"
msgstr ""
# ../roundup/cgi/actions.py:931:935
-#: ../roundup/cgi/client.py:1593
+#: ../roundup/cgi/client.py:1814
#, fuzzy, python-format
msgid "Invalid Referer: %s"
msgstr "Login invalida"
-#: ../roundup/cgi/client.py:1608
+#: ../roundup/cgi/client.py:1829
#, python-format
msgid "csrf Origin header check failed for user%(userid)s. Value=%(origin)s"
msgstr ""
# ../roundup/cgi/actions.py:931:935
-#: ../roundup/cgi/client.py:1613
+#: ../roundup/cgi/client.py:1834
#, fuzzy, python-format
msgid "Invalid Origin %s"
msgstr "Login invalida"
-#: ../roundup/cgi/client.py:1627
+#: ../roundup/cgi/client.py:1848
#, python-format
msgid ""
"csrf X-FORWARDED-HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1633
+#: ../roundup/cgi/client.py:1854
#, python-format
msgid "Invalid X-FORWARDED-HOST %s"
msgstr ""
-#: ../roundup/cgi/client.py:1652
+#: ../roundup/cgi/client.py:1873
#, python-format
msgid "csrf HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1657
+#: ../roundup/cgi/client.py:1878
#, python-format
msgid "Invalid HOST %s"
msgstr ""
-#: ../roundup/cgi/client.py:1665
+#: ../roundup/cgi/client.py:1886
msgid "Csrf: unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1666
+#: ../roundup/cgi/client.py:1887
msgid "Unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1679
+#: ../roundup/cgi/client.py:1900
#, python-format
msgid ""
"csrf X-REQUESTED-WITH xmlrpc required header check failed for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1720
+#: ../roundup/cgi/client.py:1941
#, python-format
msgid "Required csrf field missing for user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1723 ../roundup/cgi/client.py:1751
-#: ../roundup/cgi/client.py:1768 ../roundup/cgi/client.py:1723:1751:1768
+#: ../roundup/cgi/client.py:1944 ../roundup/cgi/client.py:1972
+#: ../roundup/cgi/client.py:1989 ../roundup/cgi/client.py:1944:1972:1989
msgid ""
"We can't validate your session (csrf failure). Re-enter any unsaved data and "
"try again."
msgstr ""
-#: ../roundup/cgi/client.py:1728
+#: ../roundup/cgi/client.py:1949
#, python-format
msgid "csrf field not supplied by user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1740
+#: ../roundup/cgi/client.py:1961
#, python-format
msgid ""
"Csrf mismatch user: current user %(user)s != stored user %(stored)s, current "
"session, stored session: %(cur_sess)s,%(stor_sess)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1758
+#: ../roundup/cgi/client.py:1979
#, python-format
msgid ""
"Csrf mismatch user: current session %(curr_sess)s != stored session "
"%(stor_sess)s, current user/stored user is: %(user)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1950
+#: ../roundup/cgi/client.py:2209
msgid "You are not allowed to view this file."
msgstr "Non si dispone dei permessi per visualizzare questo file."
-#: ../roundup/cgi/client.py:2274
+#: ../roundup/cgi/client.py:2544
#, python-format
msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
msgstr "%(starttag)sTempo trascorso: %(seconds)fs%(endtag)s\n"
-#: ../roundup/cgi/client.py:2278
-#, python-format
-msgid ""
-"%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading "
-"items: %(get_items)f secs. Filtering: %(filtering)f secs.%(endtag)s\n"
-msgstr ""
-
-#: ../roundup/cgi/client.py:2817
+#: ../roundup/cgi/client.py:3086
#, python-format
msgid ""
"Cache failure: compressed file %(compressed)s is older than its source file "
@@ -1944,72 +2043,72 @@ msgid_plural "Required %(class)s properties %(property)s not supplied"
msgstr[0] "La proprietà %(class)s %(property)s non è stata fornita"
msgstr[1] "Le proprietà %(class)s %(property)s non sono state fornite"
-#: ../roundup/cgi/templating.py:278
+#: ../roundup/cgi/templating.py:286
#, python-format
msgid "You are not allowed to %(action)s items of class %(class)s"
msgstr "Non si dispone dei permessi per %(action)s item della classe %(class)s"
-#: ../roundup/cgi/templating.py:892
+#: ../roundup/cgi/templating.py:900
msgid "(list)"
msgstr "(elenco)"
-#: ../roundup/cgi/templating.py:975
+#: ../roundup/cgi/templating.py:983
msgid "Submit New Entry"
msgstr "Crea Nuovo"
# ../roundup/cgi/templating.py:728:862 :1269:1298 :1318:1364 :1387:1423
# :1460:1513 :1530:1614 :1634:1652 :1684:1694 :1746:1935
-#: ../roundup/cgi/templating.py:997 ../roundup/cgi/templating.py:1174
-#: ../roundup/cgi/templating.py:1809 ../roundup/cgi/templating.py:1840
-#: ../roundup/cgi/templating.py:1861 ../roundup/cgi/templating.py:1899
-#: ../roundup/cgi/templating.py:1952 ../roundup/cgi/templating.py:1976
-#: ../roundup/cgi/templating.py:1983 ../roundup/cgi/templating.py:2020
-#: ../roundup/cgi/templating.py:2077 ../roundup/cgi/templating.py:2115
-#: ../roundup/cgi/templating.py:2215 ../roundup/cgi/templating.py:2236
-#: ../roundup/cgi/templating.py:2357 ../roundup/cgi/templating.py:2377
-#: ../roundup/cgi/templating.py:2399 ../roundup/cgi/templating.py:2442
-#: ../roundup/cgi/templating.py:2452 ../roundup/cgi/templating.py:2517
-#: ../roundup/cgi/templating.py:2820 ../roundup/cgi/templating.py:997:1174
-#: :1809:1840 :1861:1899 :1952:1976 :1983:2020 :2077:2115 :2215:2236 :2357:2377
-#: :2399:2442 :2452:2517:2820
+#: ../roundup/cgi/templating.py:1005 ../roundup/cgi/templating.py:1182
+#: ../roundup/cgi/templating.py:1817 ../roundup/cgi/templating.py:1848
+#: ../roundup/cgi/templating.py:1869 ../roundup/cgi/templating.py:1907
+#: ../roundup/cgi/templating.py:1960 ../roundup/cgi/templating.py:1984
+#: ../roundup/cgi/templating.py:1991 ../roundup/cgi/templating.py:2028
+#: ../roundup/cgi/templating.py:2085 ../roundup/cgi/templating.py:2123
+#: ../roundup/cgi/templating.py:2223 ../roundup/cgi/templating.py:2244
+#: ../roundup/cgi/templating.py:2365 ../roundup/cgi/templating.py:2385
+#: ../roundup/cgi/templating.py:2407 ../roundup/cgi/templating.py:2450
+#: ../roundup/cgi/templating.py:2460 ../roundup/cgi/templating.py:2525
+#: ../roundup/cgi/templating.py:2828 ../roundup/cgi/templating.py:1005:1182
+#: :1817:1848 :1869:1907 :1960:1984 :1991:2028 :2085:2123 :2223:2244 :2365:2385
+#: :2407:2450 :2460:2525:2828
msgid "[hidden]"
msgstr "[nascosto]"
-#: ../roundup/cgi/templating.py:998
+#: ../roundup/cgi/templating.py:1006
msgid "New node - no history"
msgstr "Nuovo nodo - nessuno storico"
-#: ../roundup/cgi/templating.py:1139
+#: ../roundup/cgi/templating.py:1147
msgid "Submit Changes"
msgstr "Inserisci Modifiche"
-#: ../roundup/cgi/templating.py:1217
+#: ../roundup/cgi/templating.py:1225
msgid "The indicated property no longer exists"
msgstr "La caratteristica indicata non esiste"
-#: ../roundup/cgi/templating.py:1253
+#: ../roundup/cgi/templating.py:1261
#, python-format
msgid "The linked class %(classname)s no longer exists"
msgstr "La classe collegata %(classname)s non esiste più"
-#: ../roundup/cgi/templating.py:1290 ../roundup/cgi/templating.py:1320
-#: ../roundup/cgi/templating.py:2526 ../roundup/cgi/templating.py:2829
-#: ../roundup/cgi/templating.py:1290:1320 :2526:2829
+#: ../roundup/cgi/templating.py:1298 ../roundup/cgi/templating.py:1328
+#: ../roundup/cgi/templating.py:2534 ../roundup/cgi/templating.py:2837
+#: ../roundup/cgi/templating.py:1298:1328 :2534:2837
msgid "[label is missing]"
msgstr ""
# ../roundup/cgi/templating.py:973:997
-#: ../roundup/cgi/templating.py:1294 ../roundup/cgi/templating.py:1323
-#: ../roundup/cgi/templating.py:1294:1323
+#: ../roundup/cgi/templating.py:1302 ../roundup/cgi/templating.py:1331
+#: ../roundup/cgi/templating.py:1302:1331
msgid "The linked node no longer exists"
msgstr "Il Nodo collegato non esiste più"
-#: ../roundup/cgi/templating.py:1385
+#: ../roundup/cgi/templating.py:1393
#, python-format
msgid "%s: (no value)"
msgstr "%s: (nessun valore)"
-#: ../roundup/cgi/templating.py:1401
+#: ../roundup/cgi/templating.py:1409
#, fuzzy, python-format
msgid ""
"This event %s is not handled by the history display!"
@@ -2018,50 +2117,50 @@ msgstr ""
"Questo evento non è gestito dal visualizzatore dello storico!"
"em>"
-#: ../roundup/cgi/templating.py:1414
+#: ../roundup/cgi/templating.py:1422
msgid "
A problem occurred while running a Python script. Here is the sequence of "
"function calls leading up to the error, with the most recent (innermost) "
@@ -1644,21 +1745,21 @@ msgstr ""
"
A problem occurred while running a Python script. Here is the sequence of "
"function calls leading up to the error, with the most recent (innermost) "
@@ -2063,23 +2166,23 @@ msgstr ""
"iškvietimų iki klaidos, kur naujausias (giliausias) iškvietimas yra pirmas. "
"Klaidos atributai yra:"
-#: ../roundup/cgi/cgitb.py:152
+#: ../roundup/cgi/cgitb.py:151
msgid "<file is None - probably inside eval or exec>"
msgstr ""
"<failas yra None - greičiausiai viduje eval ar exec>"
-#: ../roundup/cgi/cgitb.py:161
+#: ../roundup/cgi/cgitb.py:160
#, python-format
msgid "in %s"
msgstr "%s viduje"
# ../roundup/cgi/cgitb.py:172 :178
-#: ../roundup/cgi/cgitb.py:209 ../roundup/cgi/cgitb.py:215
-#: ../roundup/cgi/cgitb.py:209:215
+#: ../roundup/cgi/cgitb.py:208 ../roundup/cgi/cgitb.py:214
+#: ../roundup/cgi/cgitb.py:208:214
msgid "undefined"
msgstr "neapibrėžta"
-#: ../roundup/cgi/client.py:111
+#: ../roundup/cgi/client.py:114
msgid ""
"
An error has occurred\n"
"
An error has occurred
\n"
@@ -2093,168 +2196,163 @@ msgstr ""
"Apie klaidą pranešėme tracker'io administratoriui.\n"
""
-#: ../roundup/cgi/client.py:679
+#: ../roundup/cgi/client.py:694
msgid ""
"XMLRPC endpoint is not using defusedxml. Improve security by installing "
"defusedxml."
msgstr ""
-#: ../roundup/cgi/client.py:749 ../roundup/cgi/client.py:771
-#: ../roundup/cgi/client.py:749:771
+#: ../roundup/cgi/client.py:764 ../roundup/cgi/client.py:786
+#: ../roundup/cgi/client.py:764:786
#, fuzzy
msgid "Client is not allowed to use Rest Interface."
msgstr "Anoniminiai vartotojai neturi teisių naudoti web interfeisą"
-#: ../roundup/cgi/client.py:768 ../roundup/cgi/client.py:1682
-#: ../roundup/cgi/client.py:768:1682
+#: ../roundup/cgi/client.py:783 ../roundup/cgi/client.py:1903
+#: ../roundup/cgi/client.py:783:1903
msgid "Required Header Missing"
msgstr ""
-#: ../roundup/cgi/client.py:1021
+#: ../roundup/cgi/client.py:1071
msgid "Form Error: "
msgstr "Formos klaida: "
-#: ../roundup/cgi/client.py:1114
+#: ../roundup/cgi/client.py:1175
#, python-format
msgid "Unrecognized charset: %r"
msgstr "Neatpažinta koduotė: %r"
-#: ../roundup/cgi/client.py:1381 ../roundup/cgi/client.py:2396
-#: ../roundup/cgi/client.py:1381:2396
+#: ../roundup/cgi/client.py:1442 ../roundup/cgi/client.py:2665
+#: ../roundup/cgi/client.py:1442:2665
msgid "broken form: multiple @action values submitted"
msgstr ""
-#: ../roundup/cgi/client.py:1398
+#: ../roundup/cgi/client.py:1460
msgid "Anonymous users are not allowed to use the web interface"
msgstr "Anoniminiai vartotojai neturi teisių naudoti web interfeisą"
-#: ../roundup/cgi/client.py:1543
-msgid "Referer header not available."
+#: ../roundup/cgi/client.py:1562
+msgid "Request source headers not available."
msgstr ""
-#: ../roundup/cgi/client.py:1547
+#: ../roundup/cgi/client.py:1566
#, python-format
-msgid "csrf key used with wrong method from: %s"
+msgid "csrf key used with method %(method)s from: %(source)s"
+msgstr ""
+
+#: ../roundup/cgi/client.py:1683 ../roundup/cgi/client.py:1693
+#: ../roundup/cgi/client.py:1683:1693
+msgid "Unable to authorize request"
msgstr ""
-#: ../roundup/cgi/client.py:1576
+#: ../roundup/cgi/client.py:1797
#, python-format
msgid "csrf header %(header)s required but missing for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1580
+#: ../roundup/cgi/client.py:1801
#, python-format
msgid "Missing header: %s"
msgstr ""
-#: ../roundup/cgi/client.py:1588
+#: ../roundup/cgi/client.py:1809
#, python-format
msgid "csrf Referer header check failed for user%(userid)s. Value=%(referer)s"
msgstr ""
# ../roundup/cgi/actions.py:897 :901
-#: ../roundup/cgi/client.py:1593
+#: ../roundup/cgi/client.py:1814
#, fuzzy, python-format
msgid "Invalid Referer: %s"
msgstr "Neteisingas vartotojo vardas ar slaptažodis"
-#: ../roundup/cgi/client.py:1608
+#: ../roundup/cgi/client.py:1829
#, python-format
msgid "csrf Origin header check failed for user%(userid)s. Value=%(origin)s"
msgstr ""
# ../roundup/cgi/actions.py:897 :901
-#: ../roundup/cgi/client.py:1613
+#: ../roundup/cgi/client.py:1834
#, fuzzy, python-format
msgid "Invalid Origin %s"
msgstr "Neteisingas vartotojo vardas ar slaptažodis"
-#: ../roundup/cgi/client.py:1627
+#: ../roundup/cgi/client.py:1848
#, python-format
msgid ""
"csrf X-FORWARDED-HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1633
+#: ../roundup/cgi/client.py:1854
#, python-format
msgid "Invalid X-FORWARDED-HOST %s"
msgstr ""
-#: ../roundup/cgi/client.py:1652
+#: ../roundup/cgi/client.py:1873
#, python-format
msgid "csrf HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1657
+#: ../roundup/cgi/client.py:1878
#, python-format
msgid "Invalid HOST %s"
msgstr ""
-#: ../roundup/cgi/client.py:1665
+#: ../roundup/cgi/client.py:1886
msgid "Csrf: unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1666
+#: ../roundup/cgi/client.py:1887
msgid "Unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1679
+#: ../roundup/cgi/client.py:1900
#, python-format
msgid ""
"csrf X-REQUESTED-WITH xmlrpc required header check failed for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1720
+#: ../roundup/cgi/client.py:1941
#, python-format
msgid "Required csrf field missing for user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1723 ../roundup/cgi/client.py:1751
-#: ../roundup/cgi/client.py:1768 ../roundup/cgi/client.py:1723:1751:1768
+#: ../roundup/cgi/client.py:1944 ../roundup/cgi/client.py:1972
+#: ../roundup/cgi/client.py:1989 ../roundup/cgi/client.py:1944:1972:1989
msgid ""
"We can't validate your session (csrf failure). Re-enter any unsaved data and "
"try again."
msgstr ""
-#: ../roundup/cgi/client.py:1728
+#: ../roundup/cgi/client.py:1949
#, python-format
msgid "csrf field not supplied by user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1740
+#: ../roundup/cgi/client.py:1961
#, python-format
msgid ""
"Csrf mismatch user: current user %(user)s != stored user %(stored)s, current "
"session, stored session: %(cur_sess)s,%(stor_sess)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1758
+#: ../roundup/cgi/client.py:1979
#, python-format
msgid ""
"Csrf mismatch user: current session %(curr_sess)s != stored session "
"%(stor_sess)s, current user/stored user is: %(user)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1950
+#: ../roundup/cgi/client.py:2209
msgid "You are not allowed to view this file."
msgstr "Jūs neturite teisių žiūrėti šį failą."
-#: ../roundup/cgi/client.py:2274
+#: ../roundup/cgi/client.py:2544
#, python-format
msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
msgstr "%(starttag)sPraėjęs laikas: %(seconds)fs%(endtag)s\n"
-#: ../roundup/cgi/client.py:2278
-#, python-format
-msgid ""
-"%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading "
-"items: %(get_items)f secs. Filtering: %(filtering)f secs.%(endtag)s\n"
-msgstr ""
-"%(starttag)sAtmintinės atitikimai: %(cache_hits)d, neatitikimai "
-"%(cache_misses)d. Įkeliami elementai: %(get_items)f sek. Filtruojama: "
-"%(filtering)f sek.%(endtag)s\n"
-
-#: ../roundup/cgi/client.py:2817
+#: ../roundup/cgi/client.py:3086
#, python-format
msgid ""
"Cache failure: compressed file %(compressed)s is older than its source file "
@@ -2318,121 +2416,121 @@ msgstr[0] "Reikalinga %(class)s parinktis %(property)s nepateikta"
msgstr[1] "Reikalingos %(class)s parinktys %(property)s nepateiktos"
msgstr[2] "Reikalingos %(class)s parinktys %(property)s nepateiktos"
-#: ../roundup/cgi/templating.py:278
+#: ../roundup/cgi/templating.py:286
#, python-format
msgid "You are not allowed to %(action)s items of class %(class)s"
msgstr ""
"Jūs negalite atlikti komandos %(action)s su klasės %(class)s elementais"
-#: ../roundup/cgi/templating.py:892
+#: ../roundup/cgi/templating.py:900
msgid "(list)"
msgstr "(list)"
-#: ../roundup/cgi/templating.py:975
+#: ../roundup/cgi/templating.py:983
msgid "Submit New Entry"
msgstr "Įvesti naują įrašą"
# ../roundup/cgi/templating.py:700 :819 :1193 :1214 :1258 :1280 :1314 :1353
# :1404 :1421 :1497 :1517 :1530 :1547 :1557 :1607 :1794
-#: ../roundup/cgi/templating.py:997 ../roundup/cgi/templating.py:1174
-#: ../roundup/cgi/templating.py:1809 ../roundup/cgi/templating.py:1840
-#: ../roundup/cgi/templating.py:1861 ../roundup/cgi/templating.py:1899
-#: ../roundup/cgi/templating.py:1952 ../roundup/cgi/templating.py:1976
-#: ../roundup/cgi/templating.py:1983 ../roundup/cgi/templating.py:2020
-#: ../roundup/cgi/templating.py:2077 ../roundup/cgi/templating.py:2115
-#: ../roundup/cgi/templating.py:2215 ../roundup/cgi/templating.py:2236
-#: ../roundup/cgi/templating.py:2357 ../roundup/cgi/templating.py:2377
-#: ../roundup/cgi/templating.py:2399 ../roundup/cgi/templating.py:2442
-#: ../roundup/cgi/templating.py:2452 ../roundup/cgi/templating.py:2517
-#: ../roundup/cgi/templating.py:2820 ../roundup/cgi/templating.py:997:1174
-#: :1809:1840 :1861:1899 :1952:1976 :1983:2020 :2077:2115 :2215:2236 :2357:2377
-#: :2399:2442 :2452:2517:2820
+#: ../roundup/cgi/templating.py:1005 ../roundup/cgi/templating.py:1182
+#: ../roundup/cgi/templating.py:1817 ../roundup/cgi/templating.py:1848
+#: ../roundup/cgi/templating.py:1869 ../roundup/cgi/templating.py:1907
+#: ../roundup/cgi/templating.py:1960 ../roundup/cgi/templating.py:1984
+#: ../roundup/cgi/templating.py:1991 ../roundup/cgi/templating.py:2028
+#: ../roundup/cgi/templating.py:2085 ../roundup/cgi/templating.py:2123
+#: ../roundup/cgi/templating.py:2223 ../roundup/cgi/templating.py:2244
+#: ../roundup/cgi/templating.py:2365 ../roundup/cgi/templating.py:2385
+#: ../roundup/cgi/templating.py:2407 ../roundup/cgi/templating.py:2450
+#: ../roundup/cgi/templating.py:2460 ../roundup/cgi/templating.py:2525
+#: ../roundup/cgi/templating.py:2828 ../roundup/cgi/templating.py:1005:1182
+#: :1817:1848 :1869:1907 :1960:1984 :1991:2028 :2085:2123 :2223:2244 :2365:2385
+#: :2407:2450 :2460:2525:2828
msgid "[hidden]"
msgstr "[paslėpta]"
-#: ../roundup/cgi/templating.py:998
+#: ../roundup/cgi/templating.py:1006
msgid "New node - no history"
msgstr "Naujas elementas -- nėra istorijos"
-#: ../roundup/cgi/templating.py:1139
+#: ../roundup/cgi/templating.py:1147
msgid "Submit Changes"
msgstr "Išsaugoti pakeitimus"
-#: ../roundup/cgi/templating.py:1217
+#: ../roundup/cgi/templating.py:1225
msgid "The indicated property no longer exists"
msgstr "Nurodytos parinkties nėra"
-#: ../roundup/cgi/templating.py:1253
+#: ../roundup/cgi/templating.py:1261
#, python-format
msgid "The linked class %(classname)s no longer exists"
msgstr "Susietos klasės %(classname)s nebėra"
-#: ../roundup/cgi/templating.py:1290 ../roundup/cgi/templating.py:1320
-#: ../roundup/cgi/templating.py:2526 ../roundup/cgi/templating.py:2829
-#: ../roundup/cgi/templating.py:1290:1320 :2526:2829
+#: ../roundup/cgi/templating.py:1298 ../roundup/cgi/templating.py:1328
+#: ../roundup/cgi/templating.py:2534 ../roundup/cgi/templating.py:2837
+#: ../roundup/cgi/templating.py:1298:1328 :2534:2837
msgid "[label is missing]"
msgstr ""
# ../roundup/cgi/templating.py:930 :951
-#: ../roundup/cgi/templating.py:1294 ../roundup/cgi/templating.py:1323
-#: ../roundup/cgi/templating.py:1294:1323
+#: ../roundup/cgi/templating.py:1302 ../roundup/cgi/templating.py:1331
+#: ../roundup/cgi/templating.py:1302:1331
msgid "The linked node no longer exists"
msgstr "Susieto elemento nebėra"
-#: ../roundup/cgi/templating.py:1385
+#: ../roundup/cgi/templating.py:1393
#, python-format
msgid "%s: (no value)"
msgstr "%s: (no value)"
-#: ../roundup/cgi/templating.py:1401
+#: ../roundup/cgi/templating.py:1409
#, fuzzy, python-format
msgid ""
"This event %s is not handled by the history display!"
"strong>"
msgstr "Šis įvykis nėra rodomas archyve!"
-#: ../roundup/cgi/templating.py:1414
+#: ../roundup/cgi/templating.py:1422
msgid "
"
-#: ../roundup/cgi/cgitb.py:90
+#: ../roundup/cgi/cgitb.py:89
#, python-format
msgid "A problem occurred in your template \"%s\"."
msgstr "Det oppsto et problem i din mal \"%s\"."
-#: ../roundup/cgi/cgitb.py:98
+#: ../roundup/cgi/cgitb.py:97
#, python-format
msgid ""
"\n"
@@ -2037,17 +2139,17 @@ msgstr ""
" %(locals)s\n"
"\n"
-#: ../roundup/cgi/cgitb.py:117
+#: ../roundup/cgi/cgitb.py:116
msgid "Full traceback:"
msgstr "Full traceback:"
-#: ../roundup/cgi/cgitb.py:133 ../roundup/cgi/cgitb.py:138
-#: ../roundup/cgi/cgitb.py:133:138
+#: ../roundup/cgi/cgitb.py:132 ../roundup/cgi/cgitb.py:137
+#: ../roundup/cgi/cgitb.py:132:137
#, python-format
msgid "%(exc_type)s: %(exc_value)s"
msgstr "%(exc_type)s: %(exc_value)s"
-#: ../roundup/cgi/cgitb.py:143
+#: ../roundup/cgi/cgitb.py:142
msgid ""
"
A problem occurred while running a Python script. Here is the sequence of "
"function calls leading up to the error, with the most recent (innermost) "
@@ -2057,22 +2159,22 @@ msgstr ""
"av funksjonskall somledet til feilen, with siste kall først. "
"Feilattributtene er:"
-#: ../roundup/cgi/cgitb.py:152
+#: ../roundup/cgi/cgitb.py:151
msgid "<file is None - probably inside eval or exec>"
msgstr ""
"<filen er None - untagelig inne i eval or exec>"
-#: ../roundup/cgi/cgitb.py:161
+#: ../roundup/cgi/cgitb.py:160
#, python-format
msgid "in %s"
msgstr "i %s"
-#: ../roundup/cgi/cgitb.py:209 ../roundup/cgi/cgitb.py:215
-#: ../roundup/cgi/cgitb.py:209:215
+#: ../roundup/cgi/cgitb.py:208 ../roundup/cgi/cgitb.py:214
+#: ../roundup/cgi/cgitb.py:208:214
msgid "undefined"
msgstr "udefinert"
-#: ../roundup/cgi/client.py:111
+#: ../roundup/cgi/client.py:114
msgid ""
"
An error has occurred\n"
"
An error has occurred
\n"
@@ -2081,165 +2183,161 @@ msgid ""
""
msgstr ""
-#: ../roundup/cgi/client.py:679
+#: ../roundup/cgi/client.py:694
msgid ""
"XMLRPC endpoint is not using defusedxml. Improve security by installing "
"defusedxml."
msgstr ""
-#: ../roundup/cgi/client.py:749 ../roundup/cgi/client.py:771
-#: ../roundup/cgi/client.py:749:771
+#: ../roundup/cgi/client.py:764 ../roundup/cgi/client.py:786
+#: ../roundup/cgi/client.py:764:786
#, fuzzy
msgid "Client is not allowed to use Rest Interface."
msgstr "Anonyme brukere får ikke benytte web-grensesnittet"
-#: ../roundup/cgi/client.py:768 ../roundup/cgi/client.py:1682
-#: ../roundup/cgi/client.py:768:1682
+#: ../roundup/cgi/client.py:783 ../roundup/cgi/client.py:1903
+#: ../roundup/cgi/client.py:783:1903
msgid "Required Header Missing"
msgstr ""
-#: ../roundup/cgi/client.py:1021
+#: ../roundup/cgi/client.py:1071
msgid "Form Error: "
msgstr "Skjema feil:"
-#: ../roundup/cgi/client.py:1114
+#: ../roundup/cgi/client.py:1175
#, python-format
msgid "Unrecognized charset: %r"
msgstr "Ukjent tegnsett: %r"
-#: ../roundup/cgi/client.py:1381 ../roundup/cgi/client.py:2396
-#: ../roundup/cgi/client.py:1381:2396
+#: ../roundup/cgi/client.py:1442 ../roundup/cgi/client.py:2665
+#: ../roundup/cgi/client.py:1442:2665
msgid "broken form: multiple @action values submitted"
msgstr ""
-#: ../roundup/cgi/client.py:1398
+#: ../roundup/cgi/client.py:1460
msgid "Anonymous users are not allowed to use the web interface"
msgstr "Anonyme brukere får ikke benytte web-grensesnittet"
-#: ../roundup/cgi/client.py:1543
-msgid "Referer header not available."
+#: ../roundup/cgi/client.py:1562
+msgid "Request source headers not available."
msgstr ""
-#: ../roundup/cgi/client.py:1547
+#: ../roundup/cgi/client.py:1566
#, python-format
-msgid "csrf key used with wrong method from: %s"
+msgid "csrf key used with method %(method)s from: %(source)s"
+msgstr ""
+
+#: ../roundup/cgi/client.py:1683 ../roundup/cgi/client.py:1693
+#: ../roundup/cgi/client.py:1683:1693
+msgid "Unable to authorize request"
msgstr ""
-#: ../roundup/cgi/client.py:1576
+#: ../roundup/cgi/client.py:1797
#, python-format
msgid "csrf header %(header)s required but missing for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1580
+#: ../roundup/cgi/client.py:1801
#, python-format
msgid "Missing header: %s"
msgstr ""
-#: ../roundup/cgi/client.py:1588
+#: ../roundup/cgi/client.py:1809
#, python-format
msgid "csrf Referer header check failed for user%(userid)s. Value=%(referer)s"
msgstr ""
-#: ../roundup/cgi/client.py:1593
+#: ../roundup/cgi/client.py:1814
#, fuzzy, python-format
msgid "Invalid Referer: %s"
msgstr "Ugylig login"
-#: ../roundup/cgi/client.py:1608
+#: ../roundup/cgi/client.py:1829
#, python-format
msgid "csrf Origin header check failed for user%(userid)s. Value=%(origin)s"
msgstr ""
-#: ../roundup/cgi/client.py:1613
+#: ../roundup/cgi/client.py:1834
#, fuzzy, python-format
msgid "Invalid Origin %s"
msgstr "Ugylig login"
-#: ../roundup/cgi/client.py:1627
+#: ../roundup/cgi/client.py:1848
#, python-format
msgid ""
"csrf X-FORWARDED-HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1633
+#: ../roundup/cgi/client.py:1854
#, python-format
msgid "Invalid X-FORWARDED-HOST %s"
msgstr ""
-#: ../roundup/cgi/client.py:1652
+#: ../roundup/cgi/client.py:1873
#, python-format
msgid "csrf HOST header check failed for user%(userid)s. Value=%(host)s"
msgstr ""
-#: ../roundup/cgi/client.py:1657
+#: ../roundup/cgi/client.py:1878
#, fuzzy, python-format
msgid "Invalid HOST %s"
msgstr "Ugyldig forespørsel"
-#: ../roundup/cgi/client.py:1665
+#: ../roundup/cgi/client.py:1886
msgid "Csrf: unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1666
+#: ../roundup/cgi/client.py:1887
msgid "Unable to verify sufficient headers"
msgstr ""
-#: ../roundup/cgi/client.py:1679
+#: ../roundup/cgi/client.py:1900
#, python-format
msgid ""
"csrf X-REQUESTED-WITH xmlrpc required header check failed for user%(userid)s."
msgstr ""
-#: ../roundup/cgi/client.py:1720
+#: ../roundup/cgi/client.py:1941
#, python-format
msgid "Required csrf field missing for user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1723 ../roundup/cgi/client.py:1751
-#: ../roundup/cgi/client.py:1768 ../roundup/cgi/client.py:1723:1751:1768
+#: ../roundup/cgi/client.py:1944 ../roundup/cgi/client.py:1972
+#: ../roundup/cgi/client.py:1989 ../roundup/cgi/client.py:1944:1972:1989
msgid ""
"We can't validate your session (csrf failure). Re-enter any unsaved data and "
"try again."
msgstr ""
-#: ../roundup/cgi/client.py:1728
+#: ../roundup/cgi/client.py:1949
#, python-format
msgid "csrf field not supplied by user%(userid)s"
msgstr ""
-#: ../roundup/cgi/client.py:1740
+#: ../roundup/cgi/client.py:1961
#, python-format
msgid ""
"Csrf mismatch user: current user %(user)s != stored user %(stored)s, current "
"session, stored session: %(cur_sess)s,%(stor_sess)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1758
+#: ../roundup/cgi/client.py:1979
#, python-format
msgid ""
"Csrf mismatch user: current session %(curr_sess)s != stored session "
"%(stor_sess)s, current user/stored user is: %(user)s for key %(key)s."
msgstr ""
-#: ../roundup/cgi/client.py:1950
+#: ../roundup/cgi/client.py:2209
msgid "You are not allowed to view this file."
msgstr "Du har ikke lov å se denne filen."
-#: ../roundup/cgi/client.py:2274
+#: ../roundup/cgi/client.py:2544
#, python-format
msgid "%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n"
msgstr "%(starttag)sMedgått tid: %(seconds)fs%(endtag)s\n"
-#: ../roundup/cgi/client.py:2278
-#, python-format
-msgid ""
-"%(starttag)sCache hits: %(cache_hits)d, misses %(cache_misses)d. Loading "
-"items: %(get_items)f secs. Filtering: %(filtering)f secs.%(endtag)s\n"
-msgstr ""
-"%(starttag)sCache treff: %(cache_hits)d, ikke-treff %(cache_misses)d. Laster "
-"saker: %(get_items)f secs. Filtrerer: %(filtering)f secs.%(endtag)s\n"
-
-#: ../roundup/cgi/client.py:2817
+#: ../roundup/cgi/client.py:3086
#, python-format
msgid ""
"Cache failure: compressed file %(compressed)s is older than its source file "
@@ -2300,69 +2398,69 @@ msgid_plural "Required %(class)s properties %(property)s not supplied"
msgstr[0] "Krevet %(class)s egenskap %(property)s ikke gitt."
msgstr[1] "Krevet %(class)s egenskap %(property)s ikke gitt."
-#: ../roundup/cgi/templating.py:278
+#: ../roundup/cgi/templating.py:286
#, python-format
msgid "You are not allowed to %(action)s items of class %(class)s"
msgstr "Du har ikke rettigheter til å %(action)s instanser av klasse %(class)s"
-#: ../roundup/cgi/templating.py:892
+#: ../roundup/cgi/templating.py:900
msgid "(list)"
msgstr "(liste)"
-#: ../roundup/cgi/templating.py:975
+#: ../roundup/cgi/templating.py:983
msgid "Submit New Entry"
msgstr "Legg til ny oppføring"
-#: ../roundup/cgi/templating.py:997 ../roundup/cgi/templating.py:1174
-#: ../roundup/cgi/templating.py:1809 ../roundup/cgi/templating.py:1840
-#: ../roundup/cgi/templating.py:1861 ../roundup/cgi/templating.py:1899
-#: ../roundup/cgi/templating.py:1952 ../roundup/cgi/templating.py:1976
-#: ../roundup/cgi/templating.py:1983 ../roundup/cgi/templating.py:2020
-#: ../roundup/cgi/templating.py:2077 ../roundup/cgi/templating.py:2115
-#: ../roundup/cgi/templating.py:2215 ../roundup/cgi/templating.py:2236
-#: ../roundup/cgi/templating.py:2357 ../roundup/cgi/templating.py:2377
-#: ../roundup/cgi/templating.py:2399 ../roundup/cgi/templating.py:2442
-#: ../roundup/cgi/templating.py:2452 ../roundup/cgi/templating.py:2517
-#: ../roundup/cgi/templating.py:2820 ../roundup/cgi/templating.py:997:1174
-#: :1809:1840 :1861:1899 :1952:1976 :1983:2020 :2077:2115 :2215:2236 :2357:2377
-#: :2399:2442 :2452:2517:2820
+#: ../roundup/cgi/templating.py:1005 ../roundup/cgi/templating.py:1182
+#: ../roundup/cgi/templating.py:1817 ../roundup/cgi/templating.py:1848
+#: ../roundup/cgi/templating.py:1869 ../roundup/cgi/templating.py:1907
+#: ../roundup/cgi/templating.py:1960 ../roundup/cgi/templating.py:1984
+#: ../roundup/cgi/templating.py:1991 ../roundup/cgi/templating.py:2028
+#: ../roundup/cgi/templating.py:2085 ../roundup/cgi/templating.py:2123
+#: ../roundup/cgi/templating.py:2223 ../roundup/cgi/templating.py:2244
+#: ../roundup/cgi/templating.py:2365 ../roundup/cgi/templating.py:2385
+#: ../roundup/cgi/templating.py:2407 ../roundup/cgi/templating.py:2450
+#: ../roundup/cgi/templating.py:2460 ../roundup/cgi/templating.py:2525
+#: ../roundup/cgi/templating.py:2828 ../roundup/cgi/templating.py:1005:1182
+#: :1817:1848 :1869:1907 :1960:1984 :1991:2028 :2085:2123 :2223:2244 :2365:2385
+#: :2407:2450 :2460:2525:2828
msgid "[hidden]"
msgstr "[skjult]"
-#: ../roundup/cgi/templating.py:998
+#: ../roundup/cgi/templating.py:1006
msgid "New node - no history"
msgstr "Ny node - ingen historie"
-#: ../roundup/cgi/templating.py:1139
+#: ../roundup/cgi/templating.py:1147
msgid "Submit Changes"
msgstr "Lagre endringer"
-#: ../roundup/cgi/templating.py:1217
+#: ../roundup/cgi/templating.py:1225
msgid "The indicated property no longer exists"
msgstr "Valgt egenskap finnes ikke lenger"
-#: ../roundup/cgi/templating.py:1253
+#: ../roundup/cgi/templating.py:1261
#, python-format
msgid "The linked class %(classname)s no longer exists"
msgstr "Den lenkede klassen %(classname)s finnes ikke lenger"
-#: ../roundup/cgi/templating.py:1290 ../roundup/cgi/templating.py:1320
-#: ../roundup/cgi/templating.py:2526 ../roundup/cgi/templating.py:2829
-#: ../roundup/cgi/templating.py:1290:1320 :2526:2829
+#: ../roundup/cgi/templating.py:1298 ../roundup/cgi/templating.py:1328
+#: ../roundup/cgi/templating.py:2534 ../roundup/cgi/templating.py:2837
+#: ../roundup/cgi/templating.py:1298:1328 :2534:2837
msgid "[label is missing]"
msgstr ""
-#: ../roundup/cgi/templating.py:1294 ../roundup/cgi/templating.py:1323
-#: ../roundup/cgi/templating.py:1294:1323
+#: ../roundup/cgi/templating.py:1302 ../roundup/cgi/templating.py:1331
+#: ../roundup/cgi/templating.py:1302:1331
msgid "The linked node no longer exists"
msgstr "Den lenkede noden finnes ikke lenger"
-#: ../roundup/cgi/templating.py:1385
+#: ../roundup/cgi/templating.py:1393
#, python-format
msgid "%s: (no value)"
msgstr "%s: (ingen verdi)"
-#: ../roundup/cgi/templating.py:1401
+#: ../roundup/cgi/templating.py:1409
#, fuzzy, python-format
msgid ""
"This event %s is not handled by the history display!"
@@ -2371,46 +2469,46 @@ msgstr ""
"Denne hendelsen håndteres ikke av historievisningen!"
"strong>"
-#: ../roundup/cgi/templating.py:1414
+#: ../roundup/cgi/templating.py:1422
msgid "
A problem occurred while running a Python script. Here is the sequence of "
"function calls leading up to the error, with the most recent (innermost) call "
"first. The exception attributes are:"
msgstr ""
-#: ../roundup/cgi/cgitb.py:152
+#: ../roundup/cgi/cgitb.py:151
msgid "<file is None - probably inside eval or exec>"
msgstr ""
-#: ../roundup/cgi/cgitb.py:161
+#: ../roundup/cgi/cgitb.py:160
#, python-format
msgid "in %s"
msgstr ""
-#: ../roundup/cgi/cgitb.py:209 ../roundup/cgi/cgitb.py:215
-#: ../roundup/cgi/cgitb.py:209:215
+#: ../roundup/cgi/cgitb.py:208 ../roundup/cgi/cgitb.py:214
+#: ../roundup/cgi/cgitb.py:208:214
msgid "undefined"
msgstr ""
-#: ../roundup/cgi/client.py:111
+#: ../roundup/cgi/client.py:114
msgid ""
"
"
msgstr ""
-#: ../roundup/cgi/templating.py:1479
+#: ../roundup/cgi/templating.py:1487
#, python-format
msgid "Copy of %(class)s %(id)s"
msgstr ""
-#: ../roundup/cgi/templating.py:2119 ../roundup/cgi/templating.py:2154
-#: ../roundup/cgi/templating.py:1367:2119 :2154
+#: ../roundup/cgi/templating.py:2127 ../roundup/cgi/templating.py:2162
+#: ../roundup/cgi/templating.py:1375:2127 :2162
msgid "No"
msgstr ""
-#: ../roundup/cgi/templating.py:2119 ../roundup/cgi/templating.py:2148
-#: ../roundup/cgi/templating.py:1367:2119 :2148
+#: ../roundup/cgi/templating.py:2127 ../roundup/cgi/templating.py:2156
+#: ../roundup/cgi/templating.py:1375:2127 :2156
msgid "Yes"
msgstr ""
-#: ../roundup/cgi/templating.py:2315
+#: ../roundup/cgi/templating.py:2323
msgid ""
"default value for DateHTMLProperty must be either DateHTMLProperty or string "
"date representation."
msgstr ""
-#: ../roundup/cgi/templating.py:2497
+#: ../roundup/cgi/templating.py:2505
#, python-format
msgid "Attempt to look up %(attr)s on a missing value"
msgstr ""
-#: ../roundup/cgi/templating.py:2508
+#: ../roundup/cgi/templating.py:2516
#, python-format
msgid "Attempt to look up %(item)s on a missing value"
msgstr ""
-#: ../roundup/cgi/templating.py:2613
+#: ../roundup/cgi/templating.py:2621
#, python-format
msgid ""
msgstr ""
-#: ../roundup/cgi/templating.py:3777
+#: ../roundup/cgi/templating.py:3653
+msgid ""
+"The excluded_fields parameter is invalid.It must have a __contains__ method."
+msgstr ""
+
+#: ../roundup/cgi/templating.py:3903
#, python-format
msgid "Unable to read or expand file '%(name)s' in template '%(template)s'."
msgstr ""
-#: ../roundup/cgi/templating.py:3827
+#: ../roundup/cgi/templating.py:3953
#, python-format
msgid ""
"Found an incorrect token when expandfile applied string subsitution on "
@@ -2018,225 +2120,228 @@ msgstr ""
msgid "Responding to form too quickly."
msgstr ""
-#: ../roundup/configuration.py:268
+#: ../roundup/configuration.py:280
#, python-format
msgid ""
"Error in %(filepath)s with section [%(section)s] at option %(option)s: "
"%(message)s"
msgstr ""
-#: ../roundup/configuration.py:501
+#: ../roundup/configuration.py:513
msgid "Valid languages: "
msgstr ""
-#: ../roundup/configuration.py:511
+#: ../roundup/configuration.py:523
msgid "Expected languages: "
msgstr ""
-#: ../roundup/date.py:400
+#: ../roundup/configuration.py:2616
+msgid "Error found at end of file. Maybe missing a block closing '}'."
+msgstr ""
+
+#: ../roundup/date.py:402
#, python-format
msgid ""
"Not a date spec: %r (\"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" or "
"\"yyyy-mm-dd.HH:MM:SS.SSS\")"
msgstr ""
-#: ../roundup/date.py:423
+#: ../roundup/date.py:425
msgid "Could not determine granularity"
msgstr ""
-#: ../roundup/date.py:475
+#: ../roundup/date.py:477
#, python-format
msgid ""
"%r not a date / time spec \"yyyy-mm-dd\", \"mm-dd\", \"HH:MM\", \"HH:MM:SS\" "
"or \"yyyy-mm-dd.HH:MM:SS.SSS\""
msgstr ""
-#: ../roundup/date.py:847
+#: ../roundup/date.py:849
#, python-format
msgid ""
"Not an interval spec: \"%s\" ([+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS] [date "
"spec])"
msgstr ""
-#: ../roundup/date.py:870
+#: ../roundup/date.py:872
#, python-format
msgid "Not an interval spec: \"%s\" ([+-] [#y] [#m] [#w] [#d] [[[H]H:MM]:SS])"
msgstr ""
-#: ../roundup/date.py:1047
+#: ../roundup/date.py:1049
#, python-format
msgid "%(number)s year"
msgid_plural "%(number)s years"
msgstr[0] ""
msgstr[1] ""
-#: ../roundup/date.py:1051
+#: ../roundup/date.py:1053
#, python-format
msgid "%(number)s month"
msgid_plural "%(number)s months"
msgstr[0] ""
msgstr[1] ""
-#: ../roundup/date.py:1055
+#: ../roundup/date.py:1057
#, python-format
msgid "%(number)s week"
msgid_plural "%(number)s weeks"
msgstr[0] ""
msgstr[1] ""
-#: ../roundup/date.py:1059
+#: ../roundup/date.py:1061
#, python-format
msgid "%(number)s day"
msgid_plural "%(number)s days"
msgstr[0] ""
msgstr[1] ""
-#: ../roundup/date.py:1063
+#: ../roundup/date.py:1065
msgid "tomorrow"
msgstr ""
-#: ../roundup/date.py:1065
+#: ../roundup/date.py:1067
msgid "yesterday"
msgstr ""
-#: ../roundup/date.py:1068
+#: ../roundup/date.py:1070
#, python-format
msgid "%(number)s hour"
msgid_plural "%(number)s hours"
msgstr[0] ""
msgstr[1] ""
-#: ../roundup/date.py:1072
+#: ../roundup/date.py:1074
msgid "an hour"
msgstr ""
-#: ../roundup/date.py:1074
+#: ../roundup/date.py:1076
msgid "1 1/2 hours"
msgstr ""
-#: ../roundup/date.py:1076
+#: ../roundup/date.py:1078
#, python-format
msgid "1 %(number)s/4 hours"
msgid_plural "1 %(number)s/4 hours"
msgstr[0] ""
msgstr[1] ""
-#: ../roundup/date.py:1081
+#: ../roundup/date.py:1083
msgid "in a moment"
msgstr ""
-#: ../roundup/date.py:1083
+#: ../roundup/date.py:1085
msgid "just now"
msgstr ""
-#: ../roundup/date.py:1086
+#: ../roundup/date.py:1088
msgid "1 minute"
msgstr ""
-#: ../roundup/date.py:1089
+#: ../roundup/date.py:1091
#, python-format
msgid "%(number)s minute"
msgid_plural "%(number)s minutes"
msgstr[0] ""
msgstr[1] ""
-#: ../roundup/date.py:1092
+#: ../roundup/date.py:1094
msgid "1/2 an hour"
msgstr ""
-#: ../roundup/date.py:1094
+#: ../roundup/date.py:1096
#, python-format
msgid "%(number)s/4 hour"
msgid_plural "%(number)s/4 hours"
msgstr[0] ""
msgstr[1] ""
-#: ../roundup/date.py:1098
+#: ../roundup/date.py:1100
#, python-format
msgid "%s ago"
msgstr ""
-#: ../roundup/date.py:1100
+#: ../roundup/date.py:1102
#, python-format
msgid "in %s"
msgstr ""
-#: ../roundup/hyperdb.py:126
+#: ../roundup/hyperdb.py:128
#, python-format
msgid "property %(property)s: %(errormsg)s"
msgstr ""
-#: ../roundup/hyperdb.py:161
+#: ../roundup/hyperdb.py:163
#, python-format
msgid "property %(property)s: %(value)r is an invalid date (%(errormsg)s)"
msgstr ""
-#: ../roundup/hyperdb.py:184
+#: ../roundup/hyperdb.py:186
#, python-format
msgid ""
"property %(property)s: %(value)r is an invalid date interval (%(errormsg)s)"
msgstr ""
-#: ../roundup/hyperdb.py:446
+#: ../roundup/hyperdb.py:447
#, python-format
msgid "property %(property)s: %(value)r is not a number"
msgstr ""
-#: ../roundup/hyperdb.py:460
+#: ../roundup/hyperdb.py:461
#, python-format
msgid "property %(property)s: %(value)r is not an integer"
msgstr ""
-#: ../roundup/hyperdb.py:482
+#: ../roundup/hyperdb.py:483
#, python-format
msgid "\"%s\" not a node designator"
msgstr ""
-#: ../roundup/hyperdb.py:1515 ../roundup/hyperdb.py:1523
-#: ../roundup/hyperdb.py:1515:1523
+#: ../roundup/hyperdb.py:1521 ../roundup/hyperdb.py:1529
+#: ../roundup/hyperdb.py:1521:1529
#, python-format
msgid "Not a property name: %s"
msgstr ""
-#: ../roundup/hyperdb.py:2054
+#: ../roundup/hyperdb.py:2057
#, python-format
msgid "property %(property)s: %(value)r is not a %(classname)s."
msgstr ""
-#: ../roundup/hyperdb.py:2060
+#: ../roundup/hyperdb.py:2063
#, python-format
msgid "you may only enter ID values for property %s"
msgstr ""
-#: ../roundup/hyperdb.py:2095
+#: ../roundup/hyperdb.py:2098
#, python-format
msgid "%(property)r is not a property of %(classname)s"
msgstr ""
-#: ../roundup/init.py:141
+#: ../roundup/init.py:150
#, python-format
msgid ""
"WARNING: directory '%s'\n"
"\tcontains old-style template - ignored"
msgstr ""
-#: ../roundup/mailgw.py:215 ../roundup/mailgw.py:228
-#: ../roundup/mailgw.py:215:228
+#: ../roundup/mailgw.py:215 ../roundup/mailgw.py:228 ../roundup/pygettext.py:53
#, python-format
msgid "Message signed with unknown key: %s"
msgstr ""
-#: ../roundup/mailgw.py:218
+#: ../roundup/mailgw.py:218 ../roundup/pygettext.py:56
#, python-format
msgid "Message signed with an expired key: %s"
msgstr ""
-#: ../roundup/mailgw.py:221
+#: ../roundup/mailgw.py:221 ../roundup/pygettext.py:59
#, python-format
msgid "Message signed with a revoked key: %s"
msgstr ""
-#: ../roundup/mailgw.py:224
+#: ../roundup/mailgw.py:224 ../roundup/pygettext.py:62
msgid "Invalid PGP signature detected."
msgstr ""
@@ -2423,8 +2528,7 @@ msgid ""
" %(message)s\n"
msgstr ""
-#: ../roundup/mailgw.py:1366 ../roundup/mailgw.py:1835
-#: ../roundup/mailgw.py:1366:1835
+#: ../roundup/mailgw.py:1366 ../roundup/mailgw.py:1839
#, python-format
msgid ""
"\n"
@@ -2433,7 +2537,7 @@ msgid ""
" %(errors)s\n"
msgstr ""
-#: ../roundup/mailgw.py:1821
+#: ../roundup/mailgw.py:1825
#, python-format
msgid ""
"\n"
@@ -2442,11 +2546,11 @@ msgid ""
" %(classname)s\n"
msgstr ""
-#: ../roundup/mailgw.py:1856
+#: ../roundup/mailgw.py:1860
msgid "not of form [arg=value,value,...;arg=value,value,...]"
msgstr ""
-#: ../roundup/mlink_expr.py:161
+#: ../roundup/mlink_expr.py:175
#, python-format
msgid ""
"There was an error searching %(class)s by %(attr)s using: %(opcodes)s. The "
@@ -2454,7 +2558,7 @@ msgid ""
"arguments."
msgstr ""
-#: ../roundup/mlink_expr.py:174
+#: ../roundup/mlink_expr.py:188
#, python-format
msgid ""
"There was an error searching %(class)s by %(attr)s using: %(opcodes)s. There "
@@ -2462,205 +2566,206 @@ msgid ""
"are: %(stack)s"
msgstr ""
-#: ../roundup/pygettext.py:400
+#: ../roundup/pygettext.py:398 ../roundup/pygettext.py:463
#, python-format
msgid "*** %(file)s:%(lineno)s: Seen unexpected token \"%(token)s\""
msgstr ""
-#: ../roundup/pygettext.py:456
+#: ../roundup/pygettext.py:454 ../roundup/pygettext.py:519
#, python-format
msgid "# File: %(filename)s, line: %(lineno)d"
msgstr ""
-#: ../roundup/pygettext.py:463
+#: ../roundup/pygettext.py:461 ../roundup/pygettext.py:526
#, python-format
msgid " %(filename)s:%(lineno)d"
msgstr ""
-#: ../roundup/pygettext.py:539
+#: ../roundup/pygettext.py:537 ../roundup/pygettext.py:602
#, python-format
msgid "Invalid value for --style: %s"
msgstr ""
-#: ../roundup/pygettext.py:547
+#: ../roundup/pygettext.py:545 ../roundup/pygettext.py:610
#, python-format
msgid "pygettext.py (xgettext for Python) %s"
msgstr ""
-#: ../roundup/pygettext.py:553
+#: ../roundup/pygettext.py:551 ../roundup/pygettext.py:616
#, python-format
msgid "--width argument must be an integer: %s"
msgstr ""
-#: ../roundup/pygettext.py:581
+#: ../roundup/pygettext.py:575 ../roundup/pygettext.py:640
#, python-format
msgid "Can't read --exclude-file: %s"
msgstr ""
-#: ../roundup/pygettext.py:600
+#: ../roundup/pygettext.py:594 ../roundup/pygettext.py:659
msgid "Reading standard input"
msgstr ""
-#: ../roundup/pygettext.py:605
+#: ../roundup/pygettext.py:599 ../roundup/pygettext.py:664
#, python-format
msgid "Working on %s"
msgstr ""
-#: ../roundup/pygettext.py:639
+#: ../roundup/pygettext.py:633
msgid "a unicode string"
msgstr ""
-#: ../roundup/pygettext.py:641
+#: ../roundup/pygettext.py:635 ../roundup/pygettext.py:700
#, python-format
msgid "*** Seen unexpected token \"%(token)s\""
msgstr ""
-#: ../roundup/pygettext.py:642
+#: ../roundup/pygettext.py:636 ../roundup/pygettext.py:701
msgid "morethanonestring"
msgstr ""
-#: ../roundup/rest.py:417
+#: ../roundup/rest.py:412
#, python-format
msgid "Method %(m)s not allowed. Allowed: %(a)s"
msgstr ""
-#: ../roundup/rest.py:964
+#: ../roundup/rest.py:968
#, python-format
msgid ""
"Page size %(page_size)s must be less than admin limit on query result size: "
"%(max_size)s."
msgstr ""
-#: ../roundup/rest.py:1211
+#: ../roundup/rest.py:1215
#, python-format
msgid "Invalid attribute %s"
msgstr ""
-#: ../roundup/rest.py:2199
+#: ../roundup/rest.py:2203
#, python-format
msgid "Api rate limits exceeded. Please wait: %s seconds."
msgstr ""
-#: ../roundup/rest.py:2284
+#: ../roundup/rest.py:2360
#, python-format
msgid ""
"Content type '%(requested)s' requested in URL is not available.\n"
"Acceptable types: %(acceptable)s\n"
msgstr ""
-#: ../roundup/rest.py:2306
+#: ../roundup/rest.py:2382
#, python-format
msgid ""
"Unable to parse Accept Header. %(error)s. Acceptable types: */*, "
"%(acceptable_types)s"
msgstr ""
-#: ../roundup/rest.py:2378 ../roundup/rest.py:2603 ../roundup/rest.py:2378:2603
+#: ../roundup/rest.py:2440 ../roundup/rest.py:2658 ../roundup/rest.py:2440:2658
#, python-format
msgid ""
"Unrecognized api version: %s. See /rest without specifying api version for "
"supported versions."
msgstr ""
-#: ../roundup/rest.py:2413 ../roundup/rest.py:2424 ../roundup/rest.py:2413:2424
+#: ../roundup/rest.py:2479
#, python-format
msgid ""
"Requested content type(s) '%(requested)s' not available.\n"
-"Acceptable mime types are: */*, %(acceptable)s"
+"Acceptable mime types are: %(acceptable)s"
msgstr ""
-#: ../roundup/rest.py:2702
+#: ../roundup/rest.py:2757
msgid ""
"Internal error while formatting response.\n"
"accept_mime_type is not defined. This should\n"
"never happen\n"
msgstr ""
-#: ../roundup/roundupdb.py:140
+#: ../roundup/roundupdb.py:138
#, python-format
msgid "Username '%s' already exists."
msgstr ""
-#: ../roundup/roundupdb.py:224
+#: ../roundup/roundupdb.py:221
msgid "files"
msgstr ""
-#: ../roundup/roundupdb.py:224
+#: ../roundup/roundupdb.py:221
msgid "messages"
msgstr ""
-#: ../roundup/roundupdb.py:224
+#: ../roundup/roundupdb.py:221
msgid "nosy"
msgstr ""
-#: ../roundup/roundupdb.py:224
+#: ../roundup/roundupdb.py:221
msgid "superseder"
msgstr ""
-#: ../roundup/roundupdb.py:224
+#. noqa: B018
+#: ../roundup/roundupdb.py:221
msgid "title"
msgstr ""
-#: ../roundup/roundupdb.py:225
+#: ../roundup/roundupdb.py:222
msgid "assignedto"
msgstr ""
-#: ../roundup/roundupdb.py:225
+#: ../roundup/roundupdb.py:222
msgid "keyword"
msgstr ""
-#: ../roundup/roundupdb.py:225
+#: ../roundup/roundupdb.py:222
msgid "priority"
msgstr ""
-#: ../roundup/roundupdb.py:225
+#: ../roundup/roundupdb.py:222
msgid "status"
msgstr ""
-#: ../roundup/roundupdb.py:228
+#: ../roundup/roundupdb.py:225
msgid "activity"
msgstr ""
#. following properties are common for all hyperdb classes
#. they are listed here to keep things in one place
-#: ../roundup/roundupdb.py:228
+#: ../roundup/roundupdb.py:225
msgid "actor"
msgstr ""
-#: ../roundup/roundupdb.py:228
+#: ../roundup/roundupdb.py:225
msgid "creation"
msgstr ""
-#: ../roundup/roundupdb.py:228
+#: ../roundup/roundupdb.py:225
msgid "creator"
msgstr ""
-#: ../roundup/roundupdb.py:447
+#: ../roundup/roundupdb.py:443
#, python-format
msgid "No key for \"%(adr)s\" in keyring"
msgstr ""
-#: ../roundup/roundupdb.py:525
+#: ../roundup/roundupdb.py:521
#, python-format
msgid "New submission from %(authname)s%(authaddr)s:"
msgstr ""
-#: ../roundup/roundupdb.py:528
+#: ../roundup/roundupdb.py:524
#, python-format
msgid "%(authname)s%(authaddr)s added the comment:"
msgstr ""
-#: ../roundup/roundupdb.py:531
+#: ../roundup/roundupdb.py:527
#, python-format
msgid "Change by %(authname)s%(authaddr)s:"
msgstr ""
-#: ../roundup/roundupdb.py:550
+#: ../roundup/roundupdb.py:546
#, python-format
msgid "File '%(filename)s' not attached - you can download it from %(link)s."
msgstr ""
-#: ../roundup/roundupdb.py:925
+#: ../roundup/roundupdb.py:916
#, python-format
msgid ""
"\n"
@@ -2682,7 +2787,7 @@ msgstr ""
msgid "Enter directory path to create demo tracker [%s]: "
msgstr ""
-#: ../roundup/scripts/roundup_gettext.py:33
+#: ../roundup/scripts/roundup_gettext.py:29
msgid ""
"\n"
"Extracting translatable strings only from html templates.\n"
@@ -2691,36 +2796,36 @@ msgid ""
"The 'polib' module can be installed with pip.\n"
msgstr ""
-#: ../roundup/scripts/roundup_gettext.py:71
+#: ../roundup/scripts/roundup_gettext.py:67
#, python-format
msgid "Usage: %(program)s "
msgstr ""
-#: ../roundup/scripts/roundup_gettext.py:89
+#: ../roundup/scripts/roundup_gettext.py:85
#, python-format
msgid "No tracker templates found in directory %s"
msgstr ""
-#: ../roundup/scripts/roundup_mailgw.py:182
+#: ../roundup/scripts/roundup_mailgw.py:185
msgid ""
"\n"
"Error: The instance home must be specified"
msgstr ""
-#: ../roundup/scripts/roundup_mailgw.py:200
+#: ../roundup/scripts/roundup_mailgw.py:203
msgid ""
"\n"
"Error: not enough source specification information"
msgstr ""
-#: ../roundup/scripts/roundup_mailgw.py:231
+#: ../roundup/scripts/roundup_mailgw.py:239
#, python-format
msgid ""
"\n"
"Error: %s specification not valid"
msgstr ""
-#: ../roundup/scripts/roundup_mailgw.py:254
+#: ../roundup/scripts/roundup_mailgw.py:262
msgid ""
"\n"
"Error: The source must be either \"mailbox\", \"pop\", \"pops\", \"apop\", "
@@ -2753,64 +2858,64 @@ msgid ""
" '-----END CERTIFICATE-----' markers?"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:354
+#: ../roundup/scripts/roundup_server.py:372
msgid ""
"Roundup trackers index\n"
"
Roundup trackers index
\n"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:602
+#: ../roundup/scripts/roundup_server.py:626
#, python-format
msgid "Error: %(type)s: %(value)s"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:614
+#: ../roundup/scripts/roundup_server.py:638
msgid "WARNING: ignoring \"-g\" argument, not root"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:620
+#: ../roundup/scripts/roundup_server.py:644
msgid "Can't change groups - no grp module"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:629
+#: ../roundup/scripts/roundup_server.py:653
#, python-format
msgid "Group %(group)s doesn't exist"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:641
+#: ../roundup/scripts/roundup_server.py:665
msgid "Can't run as root!"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:644
+#: ../roundup/scripts/roundup_server.py:668
msgid "WARNING: ignoring \"-u\" argument, not root"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:650
+#: ../roundup/scripts/roundup_server.py:674
msgid "Can't change users - no pwd module"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:659
+#: ../roundup/scripts/roundup_server.py:683
#, python-format
msgid "User %(user)s doesn't exist"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:871
+#: ../roundup/scripts/roundup_server.py:895
#, python-format
msgid "Multiprocess mode \"%s\" is not available, switching to single-process"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:899
+#: ../roundup/scripts/roundup_server.py:923
#, python-format
msgid "Unable to bind to port %s, port already in use."
msgstr ""
-#: ../roundup/scripts/roundup_server.py:903
+#: ../roundup/scripts/roundup_server.py:927
#, python-format
msgid ""
"Unable to bind to port %(port)s, access not allowed, errno: %(errno)s %(msg)s"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:982
+#: ../roundup/scripts/roundup_server.py:1006
msgid ""
" -c Windows Service options.\n"
" If you want to run the server as a Windows Service, you\n"
@@ -2820,7 +2925,7 @@ msgid ""
" specifics."
msgstr ""
-#: ../roundup/scripts/roundup_server.py:989
+#: ../roundup/scripts/roundup_server.py:1013
msgid ""
" -u runs the Roundup web server as this UID\n"
" -g runs the Roundup web server as this GID\n"
@@ -2830,7 +2935,7 @@ msgid ""
" -D run the server in the foreground even when -d is used."
msgstr ""
-#: ../roundup/scripts/roundup_server.py:997
+#: ../roundup/scripts/roundup_server.py:1021
#, python-format
msgid ""
"\n"
@@ -2902,24 +3007,24 @@ msgid ""
" any url-unsafe characters like spaces, as these confuse IE.\n"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:1177
+#: ../roundup/scripts/roundup_server.py:1201
msgid "Instances must be name=home"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:1191
+#: ../roundup/scripts/roundup_server.py:1215
#, python-format
msgid "Configuration saved to %s"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:1206
+#: ../roundup/scripts/roundup_server.py:1230
msgid "If you specify a PID file you must use -l or -L."
msgstr ""
-#: ../roundup/scripts/roundup_server.py:1212
+#: ../roundup/scripts/roundup_server.py:1236
msgid "Sorry, you can't run the server as a daemon on this Operating System"
msgstr ""
-#: ../roundup/scripts/roundup_server.py:1232
+#: ../roundup/scripts/roundup_server.py:1256
#, python-format
msgid "Roundup server started on %(HOST)s:%(PORT)s"
msgstr ""
@@ -3067,13 +3172,13 @@ msgstr ""
#: ../share/roundup/templates/classic/html/_generic.help.html:53
#: ../share/roundup/templates/classic/html/help.html:28
-#: ../share/roundup/templates/classic/html/issue.index.html:88
+#: ../share/roundup/templates/classic/html/issue.index.html:91
#: ../share/roundup/templates/classic/html/user.index.html:90
#: ../share/roundup/templates/devel/html/_generic.help.html:54
-#: ../share/roundup/templates/devel/html/bug.index.html:102
+#: ../share/roundup/templates/devel/html/bug.index.html:105
#: ../share/roundup/templates/devel/html/help.html:57
#: ../share/roundup/templates/devel/html/milestone.index.html:67
-#: ../share/roundup/templates/devel/html/task.index.html:93
+#: ../share/roundup/templates/devel/html/task.index.html:96
#: ../share/roundup/templates/devel/html/user.index.html:84
#: ../share/roundup/templates/minimal/html/_generic.help.html:53
#: ../share/roundup/templates/responsive/html/_generic.help.html:54
@@ -3084,13 +3189,13 @@ msgstr ""
#: ../share/roundup/templates/classic/html/_generic.help.html:57
#: ../share/roundup/templates/classic/html/help.html:32
-#: ../share/roundup/templates/classic/html/issue.index.html:91
+#: ../share/roundup/templates/classic/html/issue.index.html:94
#: ../share/roundup/templates/classic/html/user.index.html:93
#: ../share/roundup/templates/devel/html/_generic.help.html:58
-#: ../share/roundup/templates/devel/html/bug.index.html:105
+#: ../share/roundup/templates/devel/html/bug.index.html:108
#: ../share/roundup/templates/devel/html/help.html:61
#: ../share/roundup/templates/devel/html/milestone.index.html:70
-#: ../share/roundup/templates/devel/html/task.index.html:96
+#: ../share/roundup/templates/devel/html/task.index.html:99
#: ../share/roundup/templates/devel/html/user.index.html:87
#: ../share/roundup/templates/minimal/html/_generic.help.html:57
#: ../share/roundup/templates/responsive/html/_generic.help.html:58
@@ -3175,9 +3280,9 @@ msgid ""
"delicately. You may include commas and newlines by enclosing the values in "
"double-quotes (\"). Double quotes themselves must be quoted by doubling "
"(\"\").
Multilink properties have their multiple "
-"values colon (\":\") separated (... ,\"one:two:three\", ...)
Remove entries by deleting their line. Add new entries by "
-"appending them to the table - put an X in the id column. If you wish to "
+"values colon (\":\") separated (... ,\"one:two:three\", ...)
Remove entries by deleting their line. Add new entries "
+"by appending them to the table - put an X in the id column. If you wish to "
"restore a removed item and you know its id then just put that id in the id "
"column.
A problem occurred while running a Python script. Here is the sequence of "
"function calls leading up to the error, with the most recent (innermost) "
@@ -1978,22 +2081,22 @@ msgstr ""
"
A problem occurred while running a Python script. Here is the sequence of "
"function calls leading up to the error, with the most recent (innermost) "
@@ -1979,22 +2082,22 @@ msgstr ""
"
")
@@ -883,7 +883,7 @@ def g(f=f):
print_arguments()
print_form(form)
print_environ(environ)
- except:
+ except Exception:
print_exception()
def print_exception(type=None, value=None, tb=None, limit=None):
diff --git a/roundup/backends/__init__.py b/roundup/backends/__init__.py
index 9d50d284d..d9b291d7c 100644
--- a/roundup/backends/__init__.py
+++ b/roundup/backends/__init__.py
@@ -76,10 +76,7 @@ def list_backends():
because we do not need to monkey-patch list_backends.
'''
- backend_list = []
- for name in 'anydbm', 'mysql', 'sqlite', 'postgresql', 'memorydb':
- if have_backend(name):
- backend_list.append(name)
- return backend_list
+ all_backends = ('anydbm', 'mysql', 'sqlite', 'postgresql', 'memorydb')
+ return [name for name in all_backends if have_backend(name)]
# vim: set filetype=python sts=4 sw=4 et si :
diff --git a/roundup/backends/back_anydbm.py b/roundup/backends/back_anydbm.py
index ca02f1717..9369c853c 100644
--- a/roundup/backends/back_anydbm.py
+++ b/roundup/backends/back_anydbm.py
@@ -759,7 +759,7 @@ def getCachedJournalDB(self, classname):
def doSaveJournal(self, classname, nodeid, action, params, creator,
creation):
# serialise the parameters now if necessary
- if isinstance(params, type({})):
+ if isinstance(params, dict):
if action in ('set', 'create'):
params = self.serialise(classname, params)
@@ -791,7 +791,7 @@ def doSetJournal(self, classname, nodeid, journal):
l = []
for nodeid, journaldate, journaltag, action, params in journal:
# serialise the parameters now if necessary
- if isinstance(params, type({})):
+ if isinstance(params, dict):
if action in ('set', 'create'):
params = self.serialise(classname, params)
journaldate = journaldate.serialise()
@@ -984,7 +984,7 @@ def create_inner(self, **propvalues):
(self.classname, newid, key), value)
elif isinstance(prop, hyperdb.Password):
- if not isinstance(value, password.Password):
+ if value is not None and not isinstance(value, password.Password):
raise TypeError('new property "%s" not a Password' % key)
elif isinstance(prop, hyperdb.Date):
@@ -1121,7 +1121,7 @@ def get(self, nodeid, propname, default=_marker, cache=1, allow_abort=True):
ids = cls.find(**{prop.rev_propname: nodeid})
return ids
- if propname not in d:
+ if propname not in d or d[propname] is None:
if default is _marker:
if isinstance(prop, hyperdb.Multilink):
return []
@@ -1240,10 +1240,10 @@ def set_inner(self, nodeid, **propvalues):
if isinstance(prop, hyperdb.Link):
link_class = prop.classname
# if it isn't a number, it's a key
- if value is not None and not isinstance(value, type('')):
+ if value is not None and not isinstance(value, str):
raise ValueError('property "%s" link value be a string' % (
propname))
- if isinstance(value, type('')) and not num_re.match(value):
+ if isinstance(value, str) and not num_re.match(value):
try:
value = self.db.classes[link_class].lookup(value)
except (TypeError, KeyError):
@@ -1344,7 +1344,7 @@ def set_inner(self, nodeid, **propvalues):
value)
elif isinstance(prop, hyperdb.Password):
- if not isinstance(value, password.Password):
+ if value is not None and not isinstance(value, password.Password):
raise TypeError('new property "%s" not a '
'Password' % propname)
propvalues[propname] = value
@@ -1590,7 +1590,7 @@ def find(self, **propspec):
continue
for propname, itemids in propspec.items():
if not isinstance(itemids, dict):
- if itemids is None or isinstance(itemids, type("")):
+ if itemids is None or isinstance(itemids, str):
itemids = {itemids: 1}
else:
itemids = dict.fromkeys(itemids)
@@ -2208,11 +2208,11 @@ def export_journals(self):
pass
elif isinstance(prop, hyperdb.Date):
# this is a hack - some dates are stored as strings
- if not isinstance(value, type('')):
+ if not isinstance(value, str):
value = value.get_tuple()
elif isinstance(prop, hyperdb.Interval):
# hack too - some intervals are stored as strings
- if not isinstance(value, type('')):
+ if not isinstance(value, str):
value = value.get_tuple()
elif isinstance(prop, hyperdb.Password):
value = str(value)
diff --git a/roundup/backends/blobfiles.py b/roundup/backends/blobfiles.py
index 372edcf21..b9dd60ed6 100644
--- a/roundup/backends/blobfiles.py
+++ b/roundup/backends/blobfiles.py
@@ -1,3 +1,4 @@
+# ruff: noqa: ARG002 ignore **database not used
#
# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
# This module is free software, and you may redistribute it and/or modify
@@ -22,13 +23,12 @@
import os
-from roundup.anypy import scandir_
-def files_in_dir(dir):
- if not os.path.exists(dir):
+def files_in_dir(directory):
+ if not os.path.exists(directory):
return 0
num_files = 0
- for dir_entry in os.scandir(dir):
+ for dir_entry in os.scandir(directory):
if dir_entry.is_file():
num_files = num_files + 1
elif dir_entry.is_dir():
@@ -213,10 +213,10 @@ class FileStorage(object):
def __init__(self, umask):
self.umask = umask
- def subdirFilename(self, classname, nodeid, property=None):
+ def subdirFilename(self, classname, nodeid, property_=None):
"""Determine what the filename and subdir for nodeid + classname is."""
- if property:
- name = '%s%s.%s' % (classname, nodeid, property)
+ if property_: # noqa: SIM108 don't use ternary
+ name = '%s%s.%s' % (classname, nodeid, property_)
else:
# roundupdb.FileClass never specified the property name, so don't
# include it
@@ -233,20 +233,20 @@ def _tempfile(self, filename):
return filename + self.tempext
- def _editInProgress(self, classname, nodeid, property):
+ def _editInProgress(self, classname, nodeid, property_):
"""Return true if the file indicated is being edited.
returns -- True if the current transaction includes an edit to
the file indicated."""
- for method, args in self.transactions:
+ for method, args in self.transactions: # noqa: SIM110 don't use any()
if (method == self.doStoreFile and
- args == (classname, nodeid, property)):
+ args == (classname, nodeid, property_)):
return True
return False
- def filename(self, classname, nodeid, property=None, create=0):
+ def filename(self, classname, nodeid, property_=None, create=0):
"""Determine what the filename for the given node and optionally
property is.
@@ -255,7 +255,7 @@ def filename(self, classname, nodeid, property=None, create=0):
could be in an old-style, backwards-compatible flat directory.
"""
filename = os.path.join(self.dir, 'files', classname,
- self.subdirFilename(classname, nodeid, property))
+ self.subdirFilename(classname, nodeid, property_))
# If the caller is going to create the file, return the
# post-commit filename. It is the callers responsibility to
# add self.tempext when actually creating the file.
@@ -266,7 +266,7 @@ def filename(self, classname, nodeid, property=None, create=0):
# If an edit to this file is in progress, then return the name
# of the temporary file containing the edited content.
- if self._editInProgress(classname, nodeid, property):
+ if self._editInProgress(classname, nodeid, property_):
if not os.path.exists(tempfile):
raise IOError('content file for %s not found' % tempfile)
return tempfile
@@ -293,9 +293,9 @@ def filename(self, classname, nodeid, property=None, create=0):
return filename
# ok, try flat (very old-style)
- if property:
+ if property_:
filename = os.path.join(self.dir, 'files', '%s%s.%s' % (
- classname, nodeid, property))
+ classname, nodeid, property_))
else:
filename = os.path.join(self.dir, 'files', '%s%s' % (classname,
nodeid))
@@ -305,17 +305,17 @@ def filename(self, classname, nodeid, property=None, create=0):
# file just ain't there
raise IOError('content file for %s not found' % filename)
- def filesize(self, classname, nodeid, property=None, create=0):
- filename = self.filename(classname, nodeid, property, create)
+ def filesize(self, classname, nodeid, property_=None, create=0):
+ filename = self.filename(classname, nodeid, property_, create)
return os.path.getsize(filename)
- def storefile(self, classname, nodeid, property, content):
+ def storefile(self, classname, nodeid, property_, content):
"""Store the content of the file in the database. The property may be
None, in which case the filename does not indicate which property
is being saved.
"""
# determine the name of the file to write to
- name = self.filename(classname, nodeid, property, create=1)
+ name = self.filename(classname, nodeid, property_, create=1)
# make sure the file storage dir exists
if not os.path.exists(os.path.dirname(name)):
@@ -325,29 +325,24 @@ def storefile(self, classname, nodeid, property, content):
name = self._tempfile(name)
# make sure we don't register the rename action more than once
- if not self._editInProgress(classname, nodeid, property):
+ if not self._editInProgress(classname, nodeid, property_):
# save off the rename action
self.transactions.append((self.doStoreFile, (classname, nodeid,
- property)))
+ property_)))
# always set umask before writing to make sure we have the proper one
# in multi-tracker (i.e. multi-umask) or modpython scenarios
# the umask may have changed since last we set it.
os.umask(self.umask)
- fd = open(name, 'wb')
- fd.write(content)
- fd.close()
+ with open(name, 'wb') as fd:
+ fd.write(content)
- def getfile(self, classname, nodeid, property):
+ def getfile(self, classname, nodeid, property_):
"""Get the content of the file in the database.
"""
- filename = self.filename(classname, nodeid, property)
+ filename = self.filename(classname, nodeid, property_)
- f = open(filename, 'rb')
- try:
- # snarf the contents and make sure we close the file
+ with open(filename, 'rb') as f:
return f.read()
- finally:
- f.close()
def numfiles(self):
"""Get number of files in storage, even across subdirectories.
@@ -355,11 +350,11 @@ def numfiles(self):
files_dir = os.path.join(self.dir, 'files')
return files_in_dir(files_dir)
- def doStoreFile(self, classname, nodeid, property, **databases):
+ def doStoreFile(self, classname, nodeid, property_, **databases):
"""Store the file as part of a transaction commit.
"""
# determine the name of the file to write to
- name = self.filename(classname, nodeid, property, 1)
+ name = self.filename(classname, nodeid, property_, 1)
# the file is currently ".tmp" - move it to its real name to commit
if name.endswith(self.tempext):
@@ -380,11 +375,11 @@ def doStoreFile(self, classname, nodeid, property, **databases):
# return the classname, nodeid so we reindex this content
return (classname, nodeid)
- def rollbackStoreFile(self, classname, nodeid, property, **databases):
+ def rollbackStoreFile(self, classname, nodeid, property_, **databases):
"""Remove the temp file as a part of a rollback
"""
# determine the name of the file to delete
- name = self.filename(classname, nodeid, property)
+ name = self.filename(classname, nodeid, property_)
if not name.endswith(self.tempext):
name += self.tempext
os.remove(name)
diff --git a/roundup/backends/indexer_common.py b/roundup/backends/indexer_common.py
index 604cb6fac..5dc93219e 100644
--- a/roundup/backends/indexer_common.py
+++ b/roundup/backends/indexer_common.py
@@ -10,8 +10,7 @@
def _isLink(propclass):
- return (isinstance(propclass, hyperdb.Link) or
- isinstance(propclass, hyperdb.Multilink))
+ return (isinstance(propclass, (hyperdb.Link, hyperdb.Multilink)))
class Indexer:
@@ -65,10 +64,10 @@ def search(self, search_terms, klass, ignore=None):
ignore = {}
# don't unpack hits entries as sqlite3's Row can't be unpacked :(
for entry in hits:
- # skip this result if we don't care about this class/property
+ # skip this result if we don't care about this class/prop(erty)
classname = entry[0]
- property = entry[2]
- if (classname, property) in ignore:
+ prop = entry[2]
+ if (classname, prop) in ignore:
continue
# if it's a property on klass, it's easy
@@ -96,13 +95,12 @@ def search(self, search_terms, klass, ignore=None):
# klass.find tells me the klass nodeids the linked nodes relate to
propdefs = klass.getprops()
for resid in klass.find(**propspec):
- resid = str(resid)
if resid in nodeids:
continue # we ignore duplicate resids
nodeids[resid] = {}
node_dict = nodeids[resid]
# now figure out where it came from
- for linkprop in propspec:
+ for linkprop, linkprop_value in propspec.items():
v = klass.get(resid, linkprop)
# the link might be a Link so deal with a single result or None
if isinstance(propdefs[linkprop], hyperdb.Link):
@@ -110,7 +108,7 @@ def search(self, search_terms, klass, ignore=None):
continue
v = [v]
for nodeid in v:
- if nodeid in propspec[linkprop]:
+ if nodeid in linkprop_value:
# OK, this node[propname] has a winner
if linkprop not in node_dict:
node_dict[linkprop] = [nodeid]
diff --git a/roundup/backends/indexer_dbm.py b/roundup/backends/indexer_dbm.py
index d3f9560b2..d05216561 100644
--- a/roundup/backends/indexer_dbm.py
+++ b/roundup/backends/indexer_dbm.py
@@ -20,7 +20,6 @@
'''
__docformat__ = 'restructuredtext'
-import errno
import marshal
import os
import re
@@ -57,9 +56,8 @@ def __init__(self, db):
# for now the file itself is a flag
self.force_reindex()
elif os.path.exists(version):
- fd = open(version)
- version = fd.read()
- fd.close()
+ with open(version) as fd:
+ version = fd.read()
# check the value and reindex if it's not the latest
if version.strip() != '1':
self.force_reindex()
@@ -70,10 +68,9 @@ def force_reindex(self):
if os.path.exists(self.indexdb_path):
shutil.rmtree(self.indexdb_path)
os.makedirs(self.indexdb_path)
- os.chmod(self.indexdb_path, 0o775) # nosec - allow group write
- fd = open(os.path.join(self.indexdb_path, 'version'), 'w')
- fd.write('1\n')
- fd.close()
+ os.chmod(self.indexdb_path, 0o775) # noqa: S103 allow group write
+ with open(os.path.join(self.indexdb_path, 'version'), 'w') as fd:
+ fd.write('1\n')
self.reindex = 1
self.changed = 1
@@ -98,7 +95,7 @@ def add_text(self, identifier, text, mime_type='text/plain'):
# Find new file index, and assign it to identifier
# (_TOP uses trick of negative to avoid conflict with file index)
- self.files['_TOP'] = (self.files['_TOP'][0]-1, None)
+ self.files['_TOP'] = (self.files['_TOP'][0] - 1, None)
file_index = abs(self.files['_TOP'][0])
self.files[identifier] = (file_index, len(words))
self.fileids[file_index] = identifier
@@ -109,12 +106,12 @@ def add_text(self, identifier, text, mime_type='text/plain'):
if self.is_stopword(word):
continue
if word in filedict:
- filedict[word] = filedict[word]+1
+ filedict[word] = filedict[word] + 1
else:
filedict[word] = 1
# now add to the totals
- for word in filedict:
+ for word, word_dict in filedict.items():
# each word has a dict of {identifier: count}
if word in self.words:
entry = self.words[word]
@@ -124,7 +121,7 @@ def add_text(self, identifier, text, mime_type='text/plain'):
self.words[word] = entry
# make a reference to the file for this word
- entry[file_index] = filedict[word]
+ entry[file_index] = word_dict
# save needed
self.changed = 1
@@ -165,7 +162,7 @@ def find(self, wordlist):
if not self.minlength <= len(word) <= self.maxlength:
# word outside the bounds of what we index - ignore
continue
- word = word.upper()
+ word = word.upper() # noqa: PLW2901 # set loop var is ok
if self.is_stopword(word):
continue
entry = self.words.get(word) # For each word, get index
@@ -192,7 +189,7 @@ def find(self, wordlist):
def load_index(self, reload=0, wordlist=None):
# Unless reload is indicated, do not load twice
if self.index_loaded() and not reload:
- return 0
+ return
# Ok, now let's actually load it
db = {'WORDS': {}, 'FILES': {'_TOP': (0, None)}, 'FILEIDS': {}}
@@ -211,13 +208,13 @@ def load_index(self, reload=0, wordlist=None):
# Load the segments
for segment in segments:
try:
- f = open(self.indexdb + segment, 'rb')
- except IOError as error:
+ with open(self.indexdb + segment, 'rb') as f:
+ pickle_str = zlib.decompress(f.read())
+ except FileNotFoundError: # noqa: PERF203 allow except inside loop
# probably just nonexistent segment index file
- if error.errno != errno.ENOENT: raise # noqa: E701
+ pass
else:
- pickle_str = zlib.decompress(f.read())
- f.close()
+ # FIXME 3.13 add allow_code=False to call
dbslice = marshal.loads(pickle_str)
if dbslice.get('WORDS'):
# if it has some words, add them
@@ -244,15 +241,14 @@ def save_index(self):
for segment in self.segments:
try:
os.remove(self.indexdb + segment)
- except OSError as error:
+ except FileNotFoundError: # noqa: PERF203 allow except inside loop
# probably just nonexistent segment index file
- if error.errno != errno.ENOENT: raise # noqa: E701
+ pass
# First write the much simpler filename/fileid dictionaries
dbfil = {'WORDS': None, 'FILES': self.files, 'FILEIDS': self.fileids}
- marshal_fh = open(self.indexdb+'-', 'wb')
- marshal_fh.write(zlib.compress(marshal.dumps(dbfil)))
- marshal_fh.close()
+ with open(self.indexdb + '-', 'wb') as marshal_fh:
+ marshal_fh.write(zlib.compress(marshal.dumps(dbfil)))
# The hard part is splitting the word dictionary up, of course
letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ#_"
@@ -271,10 +267,9 @@ def save_index(self):
db = {'WORDS': segdicts[initchar], 'FILES': None, 'FILEIDS': None}
pickle_str = marshal.dumps(db)
filename = self.indexdb + initchar
- pickle_fh = open(filename, 'wb')
- pickle_fh.write(zlib.compress(pickle_str))
- pickle_fh.close()
- os.chmod(filename, 0o664)
+ with open(filename, 'wb') as pickle_fh:
+ pickle_fh.write(zlib.compress(pickle_str))
+ os.chmod(filename, 0o664) # noqa: S103 allow group write
# save done
self.changed = 0
@@ -292,7 +287,7 @@ def purge_entry(self, identifier):
del self.fileids[file_index]
# The much harder part, cleanup the word index
- for _key, occurs in self.words.items():
+ for occurs in self.words.values():
if file_index in occurs:
del occurs[file_index]
diff --git a/roundup/backends/indexer_postgresql_fts.py b/roundup/backends/indexer_postgresql_fts.py
index 466fcdaee..9ed4e6f81 100644
--- a/roundup/backends/indexer_postgresql_fts.py
+++ b/roundup/backends/indexer_postgresql_fts.py
@@ -6,19 +6,19 @@
import re
+from psycopg2.errors import InFailedSqlTransaction, SyntaxError, UndefinedObject # noqa: A004
+
from roundup.backends.indexer_common import Indexer as IndexerBase
-from roundup.i18n import _
from roundup.cgi.exceptions import IndexerQueryError
-
-from psycopg2.errors import InFailedSqlTransaction, SyntaxError, \
- UndefinedObject
+from roundup.i18n import _
class Indexer(IndexerBase):
def __init__(self, db):
IndexerBase.__init__(self, db)
self.db = db
- if db.conn.server_version < 110000:
+ min_fts_server_version = 110000
+ if db.conn.server_version < min_fts_server_version:
db.sql("select version()")
server_descr = db.cursor.fetchone()
raise ValueError("Postgres native_fts indexing requires postgres "
@@ -79,11 +79,11 @@ def add_text(self, identifier, text, mime_type='text/plain'):
# we get a ValueError. For right now ignore it.
pass
else:
- id = r[0]
+ row_id = r[0]
sql = 'update __fts set _tsv=to_tsvector(%s, %s) where ctid=%s' % \
(a, a, a)
self.db.cursor.execute(sql, (self.db.config['INDEXER_LANGUAGE'],
- text, id))
+ text, row_id))
def find(self, wordlist):
"""look up all the words in the wordlist.
@@ -106,21 +106,21 @@ def find(self, wordlist):
sql = ('select _class, _itemid, _prop from __fts '
'where _tsv @@ to_tsquery(%s, %s)' % (a, a))
- else:
- if re.search(r'[<>!&|()*]', " ".join(wordlist)):
- # assume this is a ts query processed by websearch_to_tsquery.
- # since it has operator characters in it.
- raise IndexerQueryError(_('You have non-word/operator '
+ elif re.search(r'[<>!&|()*]', " ".join(wordlist)):
+ # assume this is a ts query processed by websearch_to_tsquery.
+ # since it has operator characters in it.
+ raise IndexerQueryError(_(
+ 'You have non-word/operator '
'characters "<>!&|()*" in your query. Did you want to '
'do a tsquery search and forgot to start it with "ts:"?'))
- else:
- sql = 'select _class, _itemid, _prop from __fts '\
- 'where _tsv @@ websearch_to_tsquery(%s, %s)' % (a, a)
+ else:
+ sql = 'select _class, _itemid, _prop from __fts '\
+ 'where _tsv @@ websearch_to_tsquery(%s, %s)' % (a, a)
try:
# tests supply a multi element word list. Join them.
self.db.cursor.execute(sql, (self.db.config['INDEXER_LANGUAGE'],
- " ".join(wordlist),))
+ " ".join(wordlist)))
except SyntaxError as e:
# reset the cursor as it's invalid currently
# reuse causes an InFailedSqlTransaction
@@ -145,7 +145,6 @@ def find(self, wordlist):
raise ValueError(_("Check tracker config.ini for a bad "
"indexer_language setting. Error is: %s") %
e)
- else:
- raise
+ raise
return self.db.cursor.fetchall()
diff --git a/roundup/backends/indexer_rdbms.py b/roundup/backends/indexer_rdbms.py
index 42d9914d5..5813011cb 100644
--- a/roundup/backends/indexer_rdbms.py
+++ b/roundup/backends/indexer_rdbms.py
@@ -4,8 +4,8 @@
"""
import re
+from roundup.anypy.strings import u2s, us2u
from roundup.backends.indexer_common import Indexer as IndexerBase
-from roundup.anypy.strings import us2u, u2s
class Indexer(IndexerBase):
@@ -52,15 +52,15 @@ def add_text(self, identifier, text, mime_type='text/plain'):
r = self.db.cursor.fetchone()
if not r:
# not previously indexed
- id = self.db.newid('__textids')
+ text_id = self.db.newid('__textids')
sql = 'insert into __textids (_textid, _class, _itemid, _prop)'\
' values (%s, %s, %s, %s)' % (a, a, a, a)
- self.db.cursor.execute(sql, (id, ) + identifier)
+ self.db.cursor.execute(sql, (text_id, ) + identifier)
else:
- id = int(r[0])
+ text_id = int(r[0])
# clear out any existing indexed values
sql = 'delete from __words where _textid=%s' % a
- self.db.cursor.execute(sql, (id, ))
+ self.db.cursor.execute(sql, (text_id, ))
# ok, find all the unique words in the text
text = us2u(text, "replace")
@@ -77,7 +77,7 @@ def add_text(self, identifier, text, mime_type='text/plain'):
# for each word, add an entry in the db
sql = 'insert into __words (_word, _textid) values (%s, %s)' % (a, a)
- words = [(word, id) for word in words]
+ words = [(word, text_id) for word in words]
self.db.cursor.executemany(sql, words)
def find(self, wordlist):
@@ -88,6 +88,7 @@ def find(self, wordlist):
if not wordlist:
return []
+ a = self.db.arg # placeholder for prepared statement
cap_wl = [word.upper() for word in wordlist
if self.minlength <= len(word) <= self.maxlength]
clean_wl = [word for word in cap_wl if not self.is_stopword(word)]
@@ -97,17 +98,16 @@ def find(self, wordlist):
if self.db.implements_intersect:
# simple AND search
- sql = 'select distinct(_textid) from __words where _word=%s' % (
- self.db.arg)
- sql = '\nINTERSECT\n'.join([sql]*len(clean_wl))
+ sql = 'select distinct(_textid) from __words where _word=%s' % a
+ sql = '\nINTERSECT\n'.join([sql] * len(clean_wl))
self.db.cursor.execute(sql, tuple(clean_wl))
r = self.db.cursor.fetchall()
if not r:
return []
- a = ','.join([self.db.arg] * len(r))
+ a = ','.join([a] * len(r))
sql = 'select _class, _itemid, _prop from __textids '\
'where _textid in (%s)' % a
- self.db.cursor.execute(sql, tuple([int(row[0]) for row in r]))
+ self.db.cursor.execute(sql, tuple(int(row[0]) for row in r))
else:
# A more complex version for MySQL since it doesn't
@@ -126,9 +126,9 @@ def find(self, wordlist):
match_list = []
for n in range(len(clean_wl) - 1):
join_list.append(join_tmpl % (n + 2))
- match_list.append(match_tmpl % (n + 2, self.db.arg))
+ match_list.append(match_tmpl % (n + 2, a))
- sql = sql % (' '.join(join_list), self.db.arg,
+ sql = sql % (' '.join(join_list), a,
' '.join(match_list))
self.db.cursor.execute(sql, clean_wl)
@@ -136,7 +136,7 @@ def find(self, wordlist):
if not r:
return []
- a = ','.join([self.db.arg] * len(r))
+ a = ','.join([a] * len(r))
sql = 'select _class, _itemid, _prop from __textids '\
'where _textid in (%s)' % a
diff --git a/roundup/backends/indexer_sqlite_fts.py b/roundup/backends/indexer_sqlite_fts.py
index c25efd084..0958568f6 100644
--- a/roundup/backends/indexer_sqlite_fts.py
+++ b/roundup/backends/indexer_sqlite_fts.py
@@ -19,8 +19,8 @@
"""
from roundup.backends.indexer_common import Indexer as IndexerBase
-from roundup.i18n import _
from roundup.cgi.exceptions import IndexerQueryError
+from roundup.i18n import _
try:
import sqlite3 as sqlite
@@ -84,10 +84,10 @@ def add_text(self, identifier, text, mime_type='text/plain'):
' values (%s, %s, %s, %s)' % (a, a, a, a)
self.db.cursor.execute(sql, identifier + (text,))
else:
- id = int(r[0])
+ text_id = int(r[0])
sql = 'update __fts set _textblob=%s where rowid=%s' % \
(a, a)
- self.db.cursor.execute(sql, (text, id))
+ self.db.cursor.execute(sql, (text, text_id))
def find(self, wordlist):
"""look up all the words in the wordlist.
@@ -132,8 +132,8 @@ def find(self, wordlist):
raise IndexerQueryError(
_("Search failed. Try quoting any terms that "
"include a '-' and retry the search."))
- else:
- raise IndexerQueryError(e.args[0].replace("fts5:",
- "Query error:"))
+
+ raise IndexerQueryError(e.args[0].replace("fts5:",
+ "Query error:"))
return self.db.cursor.fetchall()
diff --git a/roundup/backends/indexer_whoosh.py b/roundup/backends/indexer_whoosh.py
index a67a478d1..299319b8b 100644
--- a/roundup/backends/indexer_whoosh.py
+++ b/roundup/backends/indexer_whoosh.py
@@ -2,10 +2,10 @@
'''
import os
-from whoosh import fields, qparser, index, query, analysis
+from whoosh import analysis, fields, index, query
-from roundup.backends.indexer_common import Indexer as IndexerBase
from roundup.anypy.strings import us2u
+from roundup.backends.indexer_common import Indexer as IndexerBase
class Indexer(IndexerBase):
diff --git a/roundup/backends/indexer_xapian.py b/roundup/backends/indexer_xapian.py
index 6dc3e8583..a58ddefeb 100644
--- a/roundup/backends/indexer_xapian.py
+++ b/roundup/backends/indexer_xapian.py
@@ -3,6 +3,7 @@
import os
import re
import time
+
import xapian
from roundup.anypy.strings import b2s, s2b
@@ -28,7 +29,7 @@ def _get_database(self):
try:
# if successful return
return xapian.WritableDatabase(index, xapian.DB_CREATE_OR_OPEN)
- except xapian.DatabaseLockError:
+ except xapian.DatabaseLockError: # noqa: PERF203 except in loop ok
# adaptive sleep. Get longer as count increases.
time_to_sleep = 0.01 * (2 << min(5, n))
time.sleep(time_to_sleep)
@@ -118,11 +119,11 @@ def find(self, wordlist):
enquire = xapian.Enquire(database)
stemmer = xapian.Stem(self.language)
- terms = []
- for term in [word.upper() for word in wordlist
- if self.minlength <= len(word) <= self.maxlength]:
- if not self.is_stopword(term):
- terms.append(stemmer(s2b(term.lower())))
+
+ terms = [stemmer(s2b(word.lower())) for word in wordlist
+ if (self.minlength <= len(word) <= self.maxlength) and
+ not self.is_stopword(word.upper())]
+
query = xapian.Query(xapian.Query.OP_AND, terms)
enquire.set_query(query)
diff --git a/roundup/backends/portalocker.py b/roundup/backends/portalocker.py
index 61f9dca54..1354c4651 100644
--- a/roundup/backends/portalocker.py
+++ b/roundup/backends/portalocker.py
@@ -4,6 +4,8 @@
#
# http://code.activestate.com/recipes/65203-portalocker-cross-platform-posixnt-api-for-flock-s/
#
+
+#ruff: noqa: SIM103 return true/false statement without using if.
"""
Cross-platform (posix/nt) API for flock-style file locking.
@@ -47,14 +49,13 @@
- Added return result
"""
-from __future__ import print_function
__docformat__ = 'restructuredtext'
import os
if os.name == 'nt':
- import msvcrt
import ctypes
+ import msvcrt
from ctypes import windll
from ctypes.wintypes import BOOL, DWORD, HANDLE
@@ -67,10 +68,9 @@
# detect size of ULONG_PTR
def is_64bit():
return ctypes.sizeof(ctypes.c_ulong) != ctypes.sizeof(ctypes.c_void_p)
- if is_64bit():
- ULONG_PTR = ctypes.c_int64
- else:
- ULONG_PTR = ctypes.c_ulong
+
+ ULONG_PTR = ctypes.c_int64 if is_64bit() else ctypes.c_ulong
+
PVOID = ctypes.c_void_p
# --- Union inside Structure by stackoverflow:3480240 ---
@@ -119,33 +119,29 @@ def lock(file, flags):
if LockFileEx(hfile, flags, 0, 0, 0xFFFF0000,
ctypes.byref(overlapped)):
return True
- else:
- return False
+ return False
def unlock(file):
hfile = msvcrt.get_osfhandle(file.fileno())
overlapped = OVERLAPPED()
if UnlockFileEx(hfile, 0, 0, 0xFFFF0000, ctypes.byref(overlapped)):
return True
- else:
- return False
+ return False
elif os.name == 'posix':
def lock(file, flags):
if fcntl.flock(file.fileno(), flags) == 0:
return True
- else:
- return False
+ return False
def unlock(file):
if fcntl.flock(file.fileno(), fcntl.LOCK_UN) == 0:
return True
- else:
- return False
+ return False
if __name__ == '__main__':
- from time import time, strftime, localtime
import sys
+ from time import localtime, strftime, time
log = open('log.txt', "a+")
lock(log, LOCK_EX)
diff --git a/roundup/backends/rdbms_common.py b/roundup/backends/rdbms_common.py
index 771afecc7..18bdef09a 100644
--- a/roundup/backends/rdbms_common.py
+++ b/roundup/backends/rdbms_common.py
@@ -506,7 +506,7 @@ def _convert_string_properties(self):
l = []
for entry in row:
# mysql will already be a string - psql needs "help"
- if entry is not None and not isinstance(entry, type('')):
+ if entry is not None and not isinstance(entry, str):
entry = str(entry)
l.append(entry)
self.cursor.execute(sql, l)
@@ -597,7 +597,7 @@ def determine_columns(self, properties):
mls.append(col)
continue
- if isinstance(prop, type('')):
+ if isinstance(prop, str):
raise ValueError("string property spec!")
# and prop.find('Multilink') != -1:
# mls.append(col)
@@ -1419,7 +1419,7 @@ def addjournal(self, classname, nodeid, action, params, creator=None,
classname, nodeid, journaldate, journaltag, action, params))
# make the journalled data marshallable
- if isinstance(params, type({})):
+ if isinstance(params, dict):
self._journal_marshal(params, classname)
params = repr_export(params)
@@ -1446,7 +1446,7 @@ def setjournal(self, classname, nodeid, journal):
params))
# make the journalled data marshallable
- if isinstance(params, type({})):
+ if isinstance(params, dict):
self._journal_marshal(params, classname)
params = repr_export(params)
@@ -1487,7 +1487,7 @@ def getjournal(self, classname, nodeid):
properties = self.getclass(classname).getprops()
for nodeid, date_stamp, user, action, params in journal:
params = eval_import(params)
- if isinstance(params, type({})):
+ if isinstance(params, dict):
for param, value in params.items():
if not value:
continue
@@ -1752,7 +1752,7 @@ def create_inner(self, **propvalues):
key))
if value is not None and isinstance(prop, Link):
- if not isinstance(value, type('')):
+ if not isinstance(value, str):
raise ValueError('link value must be String')
link_class = self.properties[key].classname
# if it isn't a number, it's a key
@@ -1778,14 +1778,14 @@ def create_inner(self, **propvalues):
if value is None:
value = []
if not hasattr(value, '__iter__') or \
- isinstance(value, type('')):
+ isinstance(value, str):
raise TypeError('new property "%s" not an iterable of ids'
% key)
# clean up and validate the list of links
link_class = self.properties[key].classname
l = []
for entry in value:
- if not isinstance(entry, type('')):
+ if not isinstance(entry, str):
raise ValueError('"%s" multilink value (%r) '
'must contain Strings' % (
key, value))
@@ -1818,7 +1818,7 @@ def create_inner(self, **propvalues):
value)
elif isinstance(prop, Password):
- if not isinstance(value, password.Password):
+ if value is not None and not isinstance(value, password.Password):
raise TypeError('new property "%s" not a Password' % key)
elif isinstance(prop, Date):
@@ -2132,7 +2132,7 @@ def set_inner(self, nodeid, **propvalues):
(self.classname, nodeid, propname), value)
elif isinstance(prop, Password):
- if not isinstance(value, password.Password):
+ if value is not None and not isinstance(value, password.Password):
raise TypeError(
'new property "%s" not a Password' % propname)
propvalues[propname] = value
@@ -2612,7 +2612,7 @@ def collect_values(n):
e.context['class'] = pln
e.context['attr'] = prp
raise
- except:
+ except Exception:
pass
# Fallback to original code
args = []
@@ -2693,7 +2693,7 @@ def collect_values(n):
e.context['class'] = classname
e.context['attr'] = proptree.name
raise
- except:
+ except Exception:
# fallback behavior when expression parsing above fails
orclause = ''
if '-1' in v:
@@ -2781,7 +2781,7 @@ def _filter_sql(self, search_matches, filterspec, srt=[], grp=[], retr=0,
# Note: need the where-clause if p has
# children that compute additional restrictions
if (not p.has_values or
- (not isinstance(v, type([])) and v != '-1') or
+ (not isinstance(v, list) and v != '-1') or
p.children):
where.append('_%s.id=%s.%s' % (pln, tn, nid))
if v in ('-1', ['-1'], []):
@@ -2790,7 +2790,7 @@ def _filter_sql(self, search_matches, filterspec, srt=[], grp=[], retr=0,
where.append(self._subselect(p))
else:
if p.has_values:
- if isinstance(v, type([])):
+ if isinstance(v, list):
# The where-clause above is conditionally
# created in _filter_multilink_expression
w, arg = self._filter_multilink_expression(p, v)
@@ -2806,7 +2806,7 @@ def _filter_sql(self, search_matches, filterspec, srt=[], grp=[], retr=0,
assert not p.attr_sort_done and not p.sort_ids_needed
elif k == 'id':
if 'search' in p.need_for:
- if isinstance(v, type([])):
+ if isinstance(v, list):
# If there are no permitted values, then the
# where clause will always be false, and we
# can optimize the query away.
@@ -2823,7 +2823,7 @@ def _filter_sql(self, search_matches, filterspec, srt=[], grp=[], retr=0,
elif isinstance(propclass, String):
if 'search' in p.need_for:
exact = []
- if not isinstance(v, type([])):
+ if not isinstance(v, list):
v = [v]
new_v = []
for x in v:
@@ -2867,7 +2867,7 @@ def _filter_sql(self, search_matches, filterspec, srt=[], grp=[], retr=0,
if c:
where.append('_%s._%s=_%s.id' % (pln, k, ln))
if p.has_values:
- if isinstance(v, type([])):
+ if isinstance(v, list):
w, arg = self._filter_link_expression(p, v)
if w:
where.append(w)
@@ -2895,7 +2895,7 @@ def _filter_sql(self, search_matches, filterspec, srt=[], grp=[], retr=0,
rc = '_%s._%s' % (pln, k)
elif isinstance(propclass, Date) and 'search' in p.need_for:
dc = self.db.to_sql_value(hyperdb.Date)
- if isinstance(v, type([])):
+ if isinstance(v, list):
s = ','.join([a for x in v])
where.append('_%s._%s in (%s)' % (pln, k, s))
args = args + [dc(date.Date(x)) for x in v]
@@ -2925,7 +2925,7 @@ def _filter_sql(self, search_matches, filterspec, srt=[], grp=[], retr=0,
elif isinstance(propclass, Interval):
# filter/sort using the ___int__ column
if 'search' in p.need_for:
- if isinstance(v, type([])):
+ if isinstance(v, list):
s = ','.join([a for x in v])
where.append('_%s.__%s_int__ in (%s)' % (pln, k, s))
args = args + [date.Interval(x).as_seconds() for x in v]
@@ -2967,7 +2967,7 @@ def _filter_sql(self, search_matches, filterspec, srt=[], grp=[], retr=0,
where.append('_%s._%s in (%s)' % (pln, k, s))
args = args + bv
elif 'search' in p.need_for:
- if isinstance(v, type([])):
+ if isinstance(v, list):
s = ','.join([a for x in v])
where.append('_%s._%s in (%s)' % (pln, k, s))
args = args + v
diff --git a/roundup/backends/sessions_common.py b/roundup/backends/sessions_common.py
index f833c0899..5f549c4f2 100644
--- a/roundup/backends/sessions_common.py
+++ b/roundup/backends/sessions_common.py
@@ -1,6 +1,7 @@
-import base64, logging
+import base64
+import logging
-import roundup.anypy.random_ as random_
+from roundup.anypy import random_
from roundup.anypy.strings import b2s
logger = logging.getLogger('roundup.hyperdb.backend.sessions')
@@ -36,7 +37,7 @@ def get_logger(self):
# to do it only once.
if not hasattr(self, '__logger'):
self.__logger = logging.getLogger('roundup.hyperdb.backends.%s' %
- self.name or "basicdb" )
+ self.name or "basicdb")
return self.__logger
diff --git a/roundup/backends/sessions_dbm.py b/roundup/backends/sessions_dbm.py
index 5eca055a4..8ffdd53fd 100644
--- a/roundup/backends/sessions_dbm.py
+++ b/roundup/backends/sessions_dbm.py
@@ -6,14 +6,16 @@
"""
__docformat__ = 'restructuredtext'
-import marshal, os, random, time
-
-from roundup.anypy.html import html_escape as escape
+import marshal
+import os
+import random
+import time
from roundup import hyperdb
-from roundup.i18n import _
from roundup.anypy.dbm_ import anydbm, whichdb
+from roundup.anypy.html import html_escape as escape
from roundup.backends.sessions_common import SessionCommon
+from roundup.i18n import _
class BasicDatabase(SessionCommon):
@@ -40,11 +42,11 @@ def clear(self):
path = os.path.join(self.dir, self.name)
if os.path.exists(path):
os.remove(path)
- elif os.path.exists(path+'.db'): # dbm appends .db
- os.remove(path+'.db')
- elif os.path.exists(path+".dir"): # dumb dbm
- os.remove(path+".dir")
- os.remove(path+".dat")
+ elif os.path.exists(path + '.db'): # dbm appends .db
+ os.remove(path + '.db')
+ elif os.path.exists(path + ".dir"): # dumb dbm
+ os.remove(path + ".dir")
+ os.remove(path + ".dat")
#self._db_type = None
@@ -59,7 +61,7 @@ def cache_db_type(self, path):
if not db_type:
raise hyperdb.DatabaseError(
_("Couldn't identify database type"))
- elif os.path.exists(path+'.db'):
+ elif os.path.exists(path + '.db'):
# if the path ends in '.db', it's a dbm database, whether
# anydbm says it's dbhash or not!
db_type = 'dbm'
@@ -71,6 +73,7 @@ def get(self, infoid, value, default=_marker):
db = self.opendb('c')
try:
if infoid in db:
+ # FIXME: 3.13 add allow_code=False
values = marshal.loads(db[infoid])
else:
if default != self._marker:
@@ -84,6 +87,7 @@ def getall(self, infoid):
db = self.opendb('c')
try:
try:
+ # FIXME: 3.13 add allow_code=False
d = marshal.loads(db[infoid])
del d['__timestamp']
return d
@@ -97,6 +101,7 @@ def set(self, infoid, **newvalues):
timestamp = None
try:
if infoid in db:
+ # FIXME: 3.13 add allow_code=False
values = marshal.loads(db[infoid])
try:
timestamp = values['__timestamp']
@@ -115,6 +120,7 @@ def set(self, infoid, **newvalues):
newvalues['__timestamp'] = time.time()
values.update(newvalues)
+ # FIXME: 3.13 add allow_code=False
db[infoid] = marshal.dumps(values)
finally:
db.close()
@@ -163,19 +169,22 @@ def opendb(self, mode):
# Primarily we want to catch and retry:
# [Errno 11] Resource temporarily unavailable retry
# FIXME: make this more specific
- if retries_left < 10:
+
+ # warn if threshold is crossed
+ retry_warning_threshold = 10
+ if retries_left < retry_warning_threshold:
self.log_warning(
'dbm.open failed on ...%s, retry %s left: %s, %s' %
- (path[-15:], 15-retries_left, retries_left, e))
+ (path[-15:], 15 - retries_left, retries_left, e))
if retries_left < 0:
# We have used up the retries. Reraise the exception
# that got us here.
raise
- else:
- # stagger retry to try to get around thundering herd issue.
- time.sleep(random.randint(0, 25)*.005)
- retries_left = retries_left - 1
- continue # the while loop
+
+ # stagger retry to try to get around thundering herd issue.
+ time.sleep(random.randint(0, 25) * .005) # noqa: S311
+ retries_left = retries_left - 1
+ continue # the while loop
return handle
def commit(self):
@@ -186,7 +195,7 @@ def lifetime(self, key_lifetime=0):
in seconds. Default lifetime is 0.
"""
now = time.time()
- week = 60*60*24*7
+ week = 60 * 60 * 24 * 7
return now - week + key_lifetime
def close(self):
@@ -201,8 +210,12 @@ def updateTimestamp(self, sessid):
def clean(self):
''' Remove session records that haven't been used for a week. '''
+ ''' Note: deletion of old keys must be completed when this method
+ returns. Calling code must not have any expired keys present
+ after this returns or expired keys could be used to validate
+ a user. This can mean a long delay when expiring but ....'''
now = time.time()
- week = 60*60*24*7
+ week = 60 * 60 * 24 * 7
a_week_ago = now - week
for sessid in self.list():
sess = self.get(sessid, '__timestamp', None)
@@ -213,9 +226,13 @@ def clean(self):
self.destroy(sessid)
run_time = time.time() - now
- if run_time > 3:
+
+ # warn if long_time_threshold (in seconds) is passed.
+ long_time_threshold = 3
+ if run_time > long_time_threshold:
self.log_warning("clean() took %.2fs", run_time)
+
class Sessions(BasicDatabase):
name = 'sessions'
diff --git a/roundup/backends/sessions_rdbms.py b/roundup/backends/sessions_rdbms.py
index 69b0dc735..a2b7064b8 100644
--- a/roundup/backends/sessions_rdbms.py
+++ b/roundup/backends/sessions_rdbms.py
@@ -5,17 +5,37 @@
class. It's now also used for One Time Key handling too.
"""
__docformat__ = 'restructuredtext'
+import ast
import time
from roundup.anypy.html import html_escape as escape
from roundup.backends.sessions_common import SessionCommon
+def safe_eval(s):
+ """Restricted eval to eval a repr of a dict of constants.
+ """
+ return ast.literal_eval(s)
+
+
class BasicDatabase(SessionCommon):
''' Provide a nice encapsulation of an RDBMS table.
Keys are id strings, values are automatically marshalled data.
'''
+ """ In methods the variables ``n`` and ``a`` should be used
+ only for the values of self.name and self.db.arg
+ respectively. Only these values should be interpolated
+ into a sql string as they are safe.
+
+ self.name is ``otk`` or ``session`` for the otk or
+ session databases.
+
+ self.db.arg is ``%s`` or ``?`` and is the placeholder for
+ parameterized queries in mysql/postgresql or sqlite
+ respectively.
+ """
+
name = None
def __init__(self, db):
@@ -41,8 +61,8 @@ def get(self, infoid, value, default=_marker):
if not res:
if default != self._marker:
return default
- raise KeyError('No such %s "%s"' % (self.name, escape(infoid)))
- values = eval(res[0])
+ raise KeyError('No such %s "%s"' % (n, escape(infoid)))
+ values = safe_eval(res[0])
return values.get(value, None)
def getall(self, infoid):
@@ -51,8 +71,8 @@ def getall(self, infoid):
(n, n, n, self.db.arg), (infoid,))
res = self.cursor.fetchone()
if not res:
- raise KeyError('No such %s "%s"' % (self.name, escape(infoid)))
- return eval(res[0])
+ raise KeyError('No such %s "%s"' % (n, escape(infoid)))
+ return safe_eval(res[0])
def set(self, infoid, **newvalues):
""" Store all newvalues under key infoid with a timestamp in database.
@@ -68,11 +88,9 @@ def set(self, infoid, **newvalues):
(n, n, n, a), (infoid,))
res = c.fetchone()
+ values = safe_eval(res[0]) if res else {}
+
timestamp = time.time()
- if res:
- values = eval(res[0])
- else:
- values = {}
if '__timestamp' in newvalues:
try:
@@ -81,7 +99,7 @@ def set(self, infoid, **newvalues):
except ValueError:
if res:
# keep the original timestamp
- del(newvalues['__timestamp'])
+ del newvalues['__timestamp']
else:
# here timestamp is the new timestamp
newvalues['__timestamp'] = timestamp
@@ -103,25 +121,29 @@ def list(self):
return [res[0] for res in c.fetchall()]
def destroy(self, infoid):
+ n = self.name
self.cursor.execute('delete from %ss where %s_key=%s' %
- (self.name, self.name, self.db.arg), (infoid,))
+ (n, n, self.db.arg), (infoid,))
def updateTimestamp(self, infoid):
""" don't update every hit - once a minute should be OK """
+ n = self.name
+ a = self.db.arg
now = time.time()
self.cursor.execute('''update %ss set %s_time=%s where %s_key=%s '''
'''and %s_time < %s''' %
- (self.name, self.name, self.db.arg, self.name,
- self.db.arg, self.name, self.db.arg),
- (now, infoid, now-60))
+ (n, n, a, n, a, n, a),
+ (now, infoid, now - 60))
def clean(self):
''' Remove session records that haven't been used for a week. '''
+ n = self.name
+ a = self.db.arg
now = time.time()
- week = 60*60*24*7
+ week = 60 * 60 * 24 * 7
old = now - week
self.cursor.execute('delete from %ss where %s_time < %s' %
- (self.name, self.name, self.db.arg), (old, ))
+ (n, n, a), (old, ))
def commit(self):
self.log_info('commit %s' % self.name)
@@ -133,7 +155,7 @@ def lifetime(self, item_lifetime=0):
in seconds. Default lifetime is 0.
"""
now = time.time()
- week = 60*60*24*7
+ week = 60 * 60 * 24 * 7
return now - week + item_lifetime
def close(self):
diff --git a/roundup/backends/sessions_redis.py b/roundup/backends/sessions_redis.py
index 87872c24e..8fd948843 100644
--- a/roundup/backends/sessions_redis.py
+++ b/roundup/backends/sessions_redis.py
@@ -1,3 +1,4 @@
+# ruff: noqa: SIM108
"""This module defines a redis based store that's used by
the CGI interface to store session and one-time-key
information.
@@ -19,14 +20,13 @@
__docformat__ = 'restructuredtext'
import marshal
-import redis
import time
-from roundup.anypy.html import html_escape as escape
-
-from roundup.i18n import _
+import redis
+from roundup.anypy.html import html_escape as escape
from roundup.backends.sessions_common import SessionCommon
+from roundup.i18n import _
class BasicDatabase(SessionCommon):
@@ -35,7 +35,7 @@ class BasicDatabase(SessionCommon):
Keys are id strings, values are automatically marshalled data.
'''
name = None
- default_lifetime = 60*60*24*7 # 1 week
+ default_lifetime = 60 * 60 * 24 * 7 # 1 week
# FIXME: figure out how to allow admin to change this
# to repr/eval using interfaces.py or other method.
@@ -98,7 +98,8 @@ def getall(self, infoid):
'database.') % {"name": self.name,
"key": escape(infoid)})
- ''' def set_no_tranaction(self, infoid, **newvalues):
+ '''
+ def set_no_transaction(self, infoid, **newvalues):
""" this is missing transaction and may be affected by
a race condition on update. This will work for
redis-like embedded databases that don't support
@@ -130,7 +131,7 @@ def getall(self, infoid):
self.redis.set(infoid, self.tostr(values))
self.redis.expireat(infoid, int(values['__timestamp']))
- '''
+ '''
def set(self, infoid, **newvalues):
""" Implement set using watch/multi/exec to get some
diff --git a/roundup/cgi/PageTemplates/Expressions.py b/roundup/cgi/PageTemplates/Expressions.py
index 1bfd15bdf..78c49733a 100644
--- a/roundup/cgi/PageTemplates/Expressions.py
+++ b/roundup/cgi/PageTemplates/Expressions.py
@@ -117,7 +117,7 @@ def __init__(self, path):
dp.reverse()
def _eval(self, econtext,
- list=list, isinstance=isinstance, StringType=type('')):
+ list=list, isinstance=isinstance, StringType=str):
vars = econtext.vars
path = self._path
if self._dp:
@@ -171,7 +171,7 @@ def _exists(self, econtext):
return 0
def _eval(self, econtext,
- isinstance=isinstance, StringType=type(''), render=render):
+ isinstance=isinstance, StringType=str, render=render):
for expr in self._subexprs[:-1]:
# Try all but the last subexpression, skipping undefined ones.
try:
diff --git a/roundup/cgi/PageTemplates/PageTemplate.py b/roundup/cgi/PageTemplates/PageTemplate.py
index 13333dc8e..bbc9895d4 100644
--- a/roundup/cgi/PageTemplates/PageTemplate.py
+++ b/roundup/cgi/PageTemplates/PageTemplate.py
@@ -106,7 +106,7 @@ def pt_errors(self):
if not self.expand: return
try:
self.pt_render(source=1)
- except:
+ except Exception:
return ('Macro expansion failed', '%s: %s' % sys.exc_info()[:2])
def pt_warnings(self):
@@ -147,7 +147,7 @@ def read(self):
return self._text
try:
return self.pt_render(source=1)
- except:
+ except Exception:
return ('%s\n Macro expansion failed\n %s\n-->\n%s' %
(self._error_start, "%s: %s" % sys.exc_info()[:2],
self._text) )
@@ -177,7 +177,7 @@ def _cook(self):
try:
parser.parseString(self._text)
self._v_program, self._v_macros = parser.getCode()
- except:
+ except Exception:
self._v_errors = ["Compilation failed",
"%s: %s" % sys.exc_info()[:2]]
self._v_warnings = parser.getWarnings()
diff --git a/roundup/cgi/PageTemplates/PathIterator.py b/roundup/cgi/PageTemplates/PathIterator.py
index 10584978d..ca73d6622 100644
--- a/roundup/cgi/PageTemplates/PathIterator.py
+++ b/roundup/cgi/PageTemplates/PathIterator.py
@@ -32,7 +32,7 @@ def __bobo_traverse__(self, REQUEST, name):
def same_part(self, name, ob1, ob2):
if name is None:
return ob1 == ob2
- if isinstance(name, type('')):
+ if isinstance(name, str):
name = name.split('/')
name = list(filter(None, name))
securityManager = getSecurityManager()
diff --git a/roundup/cgi/PageTemplates/PythonExpr.py b/roundup/cgi/PageTemplates/PythonExpr.py
index 2243c51f4..b13478270 100644
--- a/roundup/cgi/PageTemplates/PythonExpr.py
+++ b/roundup/cgi/PageTemplates/PythonExpr.py
@@ -36,7 +36,7 @@ def __init__(self, name, expr, engine):
self.f_code = 'def f():\n return %s\n' % expr.strip()
exec(self.f_code, d)
self._f = d['f']
- except:
+ except Exception:
raise CompilerError(('Python expression error:\n'
'%s: %s') % exc_info()[:2])
self._get_used_names()
diff --git a/roundup/cgi/PageTemplates/TALES.py b/roundup/cgi/PageTemplates/TALES.py
index 63fce8c94..0a86105a5 100644
--- a/roundup/cgi/PageTemplates/TALES.py
+++ b/roundup/cgi/PageTemplates/TALES.py
@@ -230,7 +230,7 @@ def evaluateText(self, expr):
text = self.evaluate(expr)
if text is Default or text is None:
return text
- if isinstance(text, type(u'')):
+ if isinstance(text, str):
return text
else:
return ustr(text)
diff --git a/roundup/cgi/TAL/DummyEngine.py b/roundup/cgi/TAL/DummyEngine.py
index 58a5d9415..94137695b 100644
--- a/roundup/cgi/TAL/DummyEngine.py
+++ b/roundup/cgi/TAL/DummyEngine.py
@@ -118,7 +118,7 @@ def evaluate(self, expression):
if type == "python":
try:
return eval(expr, self.globals, self.locals)
- except:
+ except Exception:
raise TALESError("evaluation error in %s" % repr(expr))
if type == "position":
# Insert the current source file name, line number,
diff --git a/roundup/cgi/TAL/TALInterpreter.py b/roundup/cgi/TAL/TALInterpreter.py
index 27d7e6c77..2bbd12d1c 100644
--- a/roundup/cgi/TAL/TALInterpreter.py
+++ b/roundup/cgi/TAL/TALInterpreter.py
@@ -724,7 +724,7 @@ def do_onError_tal(self, block_handler):
self._stream_write = stream.write
try:
self.interpret(block)
- except:
+ except Exception:
exc = sys.exc_info()[1]
self.restoreState(state)
engine = self.engine
diff --git a/roundup/cgi/TAL/talgettext.py b/roundup/cgi/TAL/talgettext.py
index 6cb30265b..f81600593 100644
--- a/roundup/cgi/TAL/talgettext.py
+++ b/roundup/cgi/TAL/talgettext.py
@@ -33,7 +33,6 @@
found.
"""
-from __future__ import print_function
import sys
import time
import getopt
diff --git a/roundup/cgi/ZTUtils/Iterator.py b/roundup/cgi/ZTUtils/Iterator.py
index ed1e458a2..225330687 100644
--- a/roundup/cgi/ZTUtils/Iterator.py
+++ b/roundup/cgi/ZTUtils/Iterator.py
@@ -189,7 +189,7 @@ def _supports(self, ob):
try:
if hasattr(ob, 'next') and (ob is iter(ob)):
return 1
- except:
+ except Exception:
return 0
def prep_next(self, it):
diff --git a/roundup/cgi/actions.py b/roundup/cgi/actions.py
index 7d12af4a6..0243772c8 100644
--- a/roundup/cgi/actions.py
+++ b/roundup/cgi/actions.py
@@ -92,9 +92,9 @@ def examine_url(self, url):
Finally paste the whole thing together and return the new url.
'''
- parsed_url_tuple = urllib_.urlparse(url)
+ parsed_url_tuple = urllib_.urlsplit(url)
if self.base:
- parsed_base_url_tuple = urllib_.urlparse(self.base)
+ parsed_base_url_tuple = urllib_.urlsplit(self.base)
else:
raise ValueError(self._("Base url not set. Check configuration."))
@@ -106,7 +106,6 @@ def examine_url(self, url):
'url_scheme': parsed_url_tuple.scheme,
'url_netloc': parsed_url_tuple.netloc,
'url_path': parsed_url_tuple.path,
- 'url_params': parsed_url_tuple.params,
'url_query': parsed_url_tuple.query,
'url_fragment': parsed_url_tuple.fragment}
@@ -142,16 +141,15 @@ def examine_url(self, url):
raise ValueError(self._("Path component (%(url_path)s) in %(url)s "
"is not properly escaped") % info)
- if not allowed_pattern.match(parsed_url_tuple.params):
- raise ValueError(self._("Params component (%(url_params)s) in %(url)s is not properly escaped") % info)
-
if not allowed_pattern.match(parsed_url_tuple.query):
raise ValueError(self._("Query component (%(url_query)s) in %(url)s is not properly escaped") % info)
if not allowed_pattern.match(parsed_url_tuple.fragment):
raise ValueError(self._("Fragment component (%(url_fragment)s) in %(url)s is not properly escaped") % info)
- return urllib_.urlunparse(parsed_url_tuple)
+ return urllib_.urlunparse((*parsed_url_tuple[0:3],
+ "", # urlsplit has no .params
+ *parsed_url_tuple[3:]))
name = ''
permissionType = None
@@ -360,6 +358,23 @@ def handle(self):
"Please choose another name.") % (queryname)
self.client.add_error_message(message)
+ # Assume we duplicate query "a". When the
+ # response is generated, @old-queryname is set
+ # to 'a' in the template. This causes the 'a'
+ # query to be changed to the new query name
+ # and values.
+
+ # So we change @queryname by appending ' -
+ # duplicate'. This way @old-queryname will be
+ # 'a - duplicate' and will not overwrite the
+ # query named 'a'. (but it will
+ # overwrite/change a query named "a -
+ # duplicate". Hopefully nobody uses that name
+ # 8-).
+ self.client.form['@queryname'].value = (
+ # .Hint suffix added to queryname when
+ # a new queryname is already used.
+ queryname + _(" - duplicate"))
return
# edit the new way, query name not a key any more
@@ -416,7 +431,7 @@ def fakeFilterVars(self):
prop = cls.get_transitive_prop(key)
if not prop:
continue
- if isinstance(self.form[key], type([])):
+ if isinstance(self.form[key], list):
# search for at least one entry which is not empty
for minifield in self.form[key]:
if minifield.value:
@@ -549,7 +564,7 @@ def handle(self):
# extract the new values
d = {}
- for name, value in zip(props_without_id, values):
+ for name, value in zip(props_without_id, values, strict=True):
# check permission to edit this property on this item
if exists and not self.hasPermission('Edit', itemid=itemid,
classname=self.classname,
@@ -1278,7 +1293,7 @@ def handle(self):
# a new error message
clean_url = self.examine_url(self.form['__came_from'].value)
- redirect_url_tuple = urllib_.urlparse(clean_url)
+ redirect_url_tuple = urllib_.urlsplit(clean_url)
# now I have a tuple form for the __came_from url
try:
query = urllib_.parse_qs(redirect_url_tuple.query)
@@ -1300,7 +1315,7 @@ def handle(self):
(redirect_url_tuple.scheme,
redirect_url_tuple.netloc,
redirect_url_tuple.path,
- redirect_url_tuple.params,
+ "", # urlsplit() has no .params
urllib_.urlencode(list(sorted(query.items())), doseq=True),
redirect_url_tuple.fragment))
@@ -1318,7 +1333,7 @@ def handle(self):
(redirect_url_tuple.scheme,
redirect_url_tuple.netloc,
redirect_url_tuple.path,
- redirect_url_tuple.params,
+ "", # urlsplit() has no .params
urllib_.urlencode(list(sorted(query.items())), doseq=True),
redirect_url_tuple.fragment))
raise exceptions.Redirect(redirect_url)
@@ -1341,7 +1356,7 @@ def handle(self):
(redirect_url_tuple.scheme,
redirect_url_tuple.netloc,
redirect_url_tuple.path,
- redirect_url_tuple.params,
+ "", # urlsplit() has no .params
urllib_.urlencode(list(sorted(query.items())), doseq=True),
redirect_url_tuple.fragment))
raise exceptions.Redirect(redirect_url)
@@ -1363,7 +1378,7 @@ def handle(self):
redirect_url = urllib_.urlunparse((redirect_url_tuple.scheme,
redirect_url_tuple.netloc,
redirect_url_tuple.path,
- redirect_url_tuple.params,
+ "", # urlsplit has no .params
urllib_.urlencode(list(sorted(query.items())), doseq=True),
redirect_url_tuple.fragment))
@@ -1688,7 +1703,9 @@ def fct(arg):
if isinstance(props[col], hyperdb.Multilink):
cname = props[col].classname
cclass = self.db.getclass(cname)
- represent[col] = repr_list(cclass, 'name')
+ # Use id by default to handle cases like messages
+ # which have no useful label field issue2551413
+ represent[col] = repr_list(cclass, 'id')
if not self.hasPermission(self.permissionType, classname=cname):
represent[col] = repr_no_right(cclass, 'name')
else:
diff --git a/roundup/cgi/cgitb.py b/roundup/cgi/cgitb.py
index a4a14f7ff..012869189 100644
--- a/roundup/cgi/cgitb.py
+++ b/roundup/cgi/cgitb.py
@@ -4,7 +4,6 @@
"""Extended CGI traceback handler by Ka-Ping Yee, .
"""
-from __future__ import print_function
__docformat__ = 'restructuredtext'
import inspect
diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
index ed091c507..83b414325 100644
--- a/roundup/cgi/client.py
+++ b/roundup/cgi/client.py
@@ -14,6 +14,7 @@
import socket
import stat
import sys
+import tempfile
import time
from email.mime.multipart import MIMEMultipart
from traceback import format_exc
@@ -56,9 +57,9 @@ class SysCallError(Exception):
UsageError,
)
-from roundup.mlink_expr import ExpressionError
-
+from roundup.logcontext import gen_trace_id, store_trace_reason
from roundup.mailer import Mailer, MessageSendError
+from roundup.mlink_expr import ExpressionError
logger = logging.getLogger('roundup')
@@ -105,6 +106,7 @@ def add_message(msg_list, msg, escape=True):
msg_list.append(msg)
return msg_list # for unittests
+
# if set to False via interfaces.py do not log a warning when
# xmlrpc is used and defusedxml is not installed.
WARN_FOR_MISSING_DEFUSEDXML = True
@@ -130,6 +132,12 @@ def seed_pseudorandom():
import random
random.seed()
+_safe_char_set = {chr(x) for x in range(32,127)}
+def are_header_values_safe(header_list):
+ for header, value in header_list.items():
+ if (set(value) - _safe_char_set):
+ return header, value
+ return None
class LiberalCookie(SimpleCookie):
""" Python's SimpleCookie throws an exception if the cookie uses invalid
@@ -278,7 +286,6 @@ class BinaryFieldStorage(cgi.FieldStorage, object):
'''
def make_file(self, mode=None):
''' work around https://bugs.python.org/issue27777 '''
- import tempfile
if self.length >= 0:
return tempfile.TemporaryFile("wb+")
return super(BinaryFieldStorage, self).make_file()
@@ -434,6 +441,8 @@ class Client:
# content-type.
precompressed_mime_types = ["image/png", "image/jpeg"]
+ @gen_trace_id()
+ @store_trace_reason('client', extract="args[3]['PATH_INFO']")
def __init__(self, instance, request, env, form=None, translator=None):
# re-seed the random number generator. Is this is an instance of
# random.SystemRandom it has no effect.
@@ -449,9 +458,12 @@ def __init__(self, instance, request, env, form=None, translator=None):
self.env = env
if translator is not None:
self.setTranslator(translator)
- # XXX we should set self.language to "translator"'s language,
- # but how to get it ?
- self.language = ""
+ # set self.language to "translator"'s language
+ try:
+ self.language = translator.info()["language"]
+ except (AttributeError, KeyError):
+ # info() missing or no language key
+ self.language = ""
else:
self.setTranslator(TranslationService.NullTranslationService())
self.language = "" # as is the default from determine_language
@@ -477,7 +489,7 @@ def __init__(self, instance, request, env, form=None, translator=None):
# this is the "cookie path" for this tracker (ie. the path part of
# the "base" url)
- self.cookie_path = urllib_.urlparse(self.base)[2]
+ self.cookie_path = urllib_.urlsplit(self.base)[2]
# cookies to set in http responce
# {(path, name): (value, expire)}
self._cookies = {}
@@ -581,6 +593,8 @@ def setTranslator(self, translator=None):
self._ = self.gettext = translator.gettext
self.ngettext = translator.ngettext
+ @gen_trace_id()
+ @store_trace_reason('client_main', extract="args[0].env['PATH_INFO']")
def main(self):
""" Wrap the real main in a try/finally so we always close off the db.
"""
@@ -803,13 +817,18 @@ def handle_rest(self):
# Must check supplied Origin header for bad value first.
csrf_ok = self.handle_csrf(api=True)
except (Unauthorised, UsageError) as msg:
+ response_code = self.response_code
+ if response_code < 400:
+ # set the code to a 400 error as this is an error
+ # condition.
+ response_code = 400
# FIXME should format return value according to
# client's accept header, so application/xml, text/plain etc..
- output = s2b('{ "error": { "status": 400, "msg": "%s"}}' %
- str(msg))
+ output = s2b('{ "error": { "status": %s, "msg": "%s"}}' %
+ (response_code, str(msg)))
self.reject_request(output,
message_type="application/json",
- status=400)
+ status=response_code)
csrf_ok = False # we had an error, failed check
return
@@ -949,12 +968,40 @@ def inner_main(self):
self.serve_file(designator)
except SendStaticFile as file:
self.serve_static_file(str(file))
- except IOError:
- # IOErrors here are due to the client disconnecting before
- # receiving the reply.
+ except ConnectionAbortedError:
+ # ConnectionAbortedError (subclass of IOErrors which
+ # is alias for OSError as of python 3.3) here are due
+ # to the client disconnecting before receiving the
+ # reply.
pass
+ except IOError as e:
+ # We report this so other IOErrors aren't silenced.
+ # https://issues.roundup-tracker.org/issue2551405
+ if self.response_code == http_.client.OK:
+ self.response_code = http_.client.INTERNAL_SERVER_ERROR
+ # Help the administrator work out what went wrong.
+ try:
+ html = ("
Traceback
"
+ + cgitb.html(i18n=self.translator)
+ + ("
Environment Variables
%s
"
+ % cgitb.niceDict("", self.env)))
+ except Exception as e:
+ # something broke getting a nice formatted traceback.
+ # use logger.exception to dump something useful to the log.
+ logger.exception("Unexpected IOError exception %s", e)
+ # then assign html default value so we return
+ # something to the client/admin.
+ html = self._(default_err_msg)
+ if not self.instance.config.WEB_DEBUG:
+ exc_info = sys.exc_info()
+ subject = "Error: %s" % exc_info[1]
+ self.send_error_to_admin(subject, html, format_exc())
+ self.write_html(self._(default_err_msg))
+ else:
+ self.write_html(html)
except SysCallError:
- # OpenSSL.SSL.SysCallError is similar to IOError above
+ # OpenSSL.SSL.SysCallError is similar to
+ # ConnectionAbortedError (IOError) above
pass
except RateLimitExceeded:
raise
@@ -1026,9 +1073,11 @@ def inner_main(self):
except RateLimitExceeded as e:
self.add_error_message(str(e))
self.write_html(self.renderContext())
- except IOError:
- # IOErrors here are due to the client disconnecting before
- # receiving the reply.
+ except ConnectionAbortedError:
+ # ConnectionAbortedError (subclass of IOErrors which
+ # is alias for OSError as of python 3.3) here are due
+ # to the client disconnecting before receiving the
+ # reply.
# may happen during write_html and serve_file, too.
pass
except SysCallError:
@@ -1050,10 +1099,19 @@ def inner_main(self):
if self.response_code == http_.client.OK:
self.response_code = http_.client.INTERNAL_SERVER_ERROR
# Help the administrator work out what went wrong.
- html = ("
"
+ % cgitb.niceDict("", self.env)))
+ except Exception:
+ # something broke getting a nice formatted traceback.
+ # use logger.exception to dump something useful to the log.
+ logger.exception("Last Resort Exception handler %s", e)
+ # then assign html default value so we return
+ # something to the client/admin.
+ html = self._(default_err_msg)
+
if not self.instance.config.WEB_DEBUG:
exc_info = sys.exc_info()
subject = "Error: %s" % exc_info[1]
@@ -1391,8 +1449,9 @@ def check_anonymous_access(self):
# allow Anonymous to view the "user" "register" template if they're
# allowed to register
- if (self.db.security.hasPermission('Register', self.userid, 'user')
- and self.classname == 'user' and self.template == 'register'):
+ if (self.template == 'register' and self.classname == 'user'
+ and self.db.security.hasPermission('Register',
+ self.userid, 'user')):
return
# otherwise for everything else
@@ -1422,7 +1481,7 @@ def is_origin_header_ok(self, api=False, credentials=False):
try:
origin = self.env['HTTP_ORIGIN']
except KeyError:
- return self.env['REQUEST_METHOD'] == 'GET'
+ return self.env['REQUEST_METHOD'] in {"GET", "OPTIONS", "HEAD"}
# note base https://host/... ends host with with a /,
# so add it to origin.
@@ -1450,7 +1509,7 @@ def is_origin_header_ok(self, api=False, credentials=False):
def is_referer_header_ok(self, api=False):
referer = self.env['HTTP_REFERER']
# parse referer and create an origin
- referer_comp = urllib_.urlparse(referer)
+ referer_comp = urllib_.urlsplit(referer)
# self.base always has trailing /, so add trailing / to referer_origin
referer_origin = "%s://%s/" % (referer_comp[0], referer_comp[1])
@@ -1476,9 +1535,171 @@ def is_referer_header_ok(self, api=False):
return True
return False
+ def expire_exposed_keys(self, method):
+ """A nonce is used with a method it should not be. If the
+ nonce exists, report to admin so they can fix the nonce
+ leakage and destroy it. (Nonces used in a get are more
+ exposed than those used in a post.) If nonce exists in the
+ database, report the referer and origin headers to try to
+ find where this comes from so it can be fixed. If nonce
+ doesn't exist just ignore it. If we reported invalid
+ nonces, somebody could spam us with a ton of invalid keys
+ and fill up the logs.
+
+ Use ?@csrf=key in a GET, HEAD, or OPTIONS request to
+ test this code. Python's http server library will not parse
+ Content sent via one of these methods. So smuggle it
+ via a query string when testing.
+ """
+ came_from = []
+ otks = self.db.getOTKManager()
+
+ if 'HTTP_REFERER' in self.env:
+ came_from.append("Referer(%s)" % self.env['HTTP_REFERER'])
+ if 'HTTP_ORIGIN' in self.env:
+ came_from.append("Origin(%s)" % self.env['HTTP_ORIGIN'])
+ if not came_from:
+ came_from.append(self._("Request source headers not available."))
+ key = self.form['@csrf'].value
+ if otks.exists(key):
+ logger.error(
+ self._("csrf key used with method %(method)s from: %(source)s"),
+ {"method": method, "source": ", ".join(came_from)})
+ otks.destroy(key)
+ otks.commit()
+
+ def handle_csrf_tokenless(self):
+ r'''Modern way to handle csrf prevention quoted from:
+
+ https://words.filippo.io/csrf/
+
+ and is reformatted with added commentary in []'s:
+
+ "In summary, to protect against CSRF applications (or,
+ rather, libraries and frameworks) should reject
+ cross-origin non-safe browser requests. The most
+ developer-friendly way to do so is using primarily Fetch
+ metadata, which requires no extra instrumentation or
+ configuration.
+
+ 1. Allow all GET, HEAD, or OPTIONS requests. These are
+ safe methods, and are assumed not to change state at
+ various layers of the stack already.
+
+ 2. If the Origin header matches an allow-list [see \*1 below]
+ of trusted origins, allow the request.
+
+ Trusted origins should be configured as full origins
+ (e.g. https://example.com) and compared by simple
+ equality with the header value.
+
+ 3. If the Sec-Fetch-Site header is present: if its value is
+ same-origin or none, allow the request; otherwise,
+ reject the request.
+
+ This secures all major up-to-date browsers for sites
+ hosted on trustworthy (HTTPS or localhost) origins.
+
+ 4. If neither the Sec-Fetch-Site nor the Origin headers are
+ present, allow the request.
+
+ These requests are not from (post-2020) browsers, and
+ can't be affected by CSRF.
+
+ 5. If the Origin header's host (including the port) matches
+ the Host header, allow the request, otherwise reject it.
+
+ This is either a request to an HTTP origin, or by an
+ out-of-date browser.
+
+ The only false positives (unnecessary blocking) of this
+ algorithm are requests to non-trustworthy (plain HTTP)
+ origins that go through a reverse proxy that changes the
+ Host header. That edge case can be worked around by adding
+ the origin [see \*2 below] to the allow-list.
+
+ There are no false negatives in modern browsers, but
+ pre-2023 browsers will be vulnerable to HTTP→HTTPS
+ requests, because the Origin fallback is
+ scheme-agnostic. HSTS can be used to mitigate that (in
+ post-2020 browsers), but note that out-of-date browsers
+ are likely to have more pressing security issues."
+
+ **Local Notes**
+
+ \*1. The allow list of trusted origins is obtained from
+ the tracker's config.ini file in two places:
+
+ 1. the web setting of the tracker section
+ 2. the allowed_api_origins setting in the web section
+
+ \*2. I am not sure what is meant in this section. If the
+ reverse proxy changes the ORIGIN header, then setting
+ allowed_api_origins is a remedy. However the HOST header
+ is only used in step 5 and is compared to the ORIGIN
+ header not to a list of possible origins so....
+
+ The GET/HEAD/OPTIONS requests are scanned for @csrf
+ tokens. If any are found, they are removed from the
+ database. The @csrf token removal code can be deleted when
+ @csrf token support is removed.
+ '''
+ found_origin = found_fetch = False
+
+ method = self.env['REQUEST_METHOD']
+ if method in {'GET', 'OPTIONS', 'HEAD'}: # list item 1
+ # query arguments are mapped into form.list
+ # so ?@csrf=..... will pass this test.
+ if (self.form.list is not None) and ("@csrf" in self.form):
+ self.expire_exposed_keys(method)
+ # do return here. Keys have been obsoleted.
+ # we didn't do a expire cycle of session keys,
+ # but that's ok.
+ return True
+
+ # local addition to fail fast if invalid method.
+ if method not in {'POST', 'PUT', 'DELETE', 'PATCH'}:
+ self.response_code = 405 # bad method
+ raise UsageError("Bad method: %s" % method)
+
+ if 'HTTP_ORIGIN' in self.env: # list item 2
+ found_origin = True
+ origin = self.env['HTTP_ORIGIN']
+
+ tracker_web = self.db.config['TRACKER_WEB']
+ allowed_api_origins = self.db.config['WEB_ALLOWED_API_ORIGINS']
+
+ # tracker_web always ends with a /, so include it in
+ # the find.
+ if tracker_web.find(origin + '/') == 0:
+ return True
+ if origin in allowed_api_origins:
+ return True
+
+ if 'HTTP_SEC_FETCH_SITE' in self.env: # list item 3
+ if self.env['HTTP_SEC_FETCH_SITE'] in ['same-origin', 'none']:
+ return True
+
+ raise UsageError(self._("Unable to authorize request"))
+
+ if not (found_origin or found_fetch): # list item 4
+ # not a browser request so not a CSRF by definition
+ return True
+
+ parsed_origin = urllib_.urlsplit(self.env['HTTP_ORIGIN'])
+ if self.env['HTTP_HOST'] == parsed_origin.netloc: # list item 5
+ return True
+
+ raise UsageError(self._("Unable to authorize request"))
+
def handle_csrf(self, api=False):
'''Handle csrf token lookup and validate current user and session
+ If the config.ini setting:
+ WEB_USE_TOKENLESS_CSRF_PROTECTION is enabled, this routine
+ returns the result from handle_csrf_tokenless()
+ and doesn't use Nonces at all.
+
This implements (or tries to implement) the
Session-Dependent Nonce from
https://seclab.stanford.edu/websec/csrf/csrf.pdf.
@@ -1523,41 +1744,36 @@ def handle_csrf(self, api=False):
debugging seems to be an advantage.
'''
+
+ method = self.env['REQUEST_METHOD']
+
+ # reject api methods used with non-api endpoint.
+ if not api and method not in {"HEAD", "GET", "POST"}:
+ self.response_code = 405
+ self.additional_headers['Allow'] = "HEAD, GET, POST"
+ raise UsageError("Bad method: %s" % method)
+
+ if self.db.config['WEB_USE_TOKENLESS_CSRF_PROTECTION']:
+ return self.handle_csrf_tokenless()
+
# Create the otks handle here as we need it almost immediately.
# If this is perf issue, set to None here and check below
# once all header checks have passed if it needs to be opened.
otks = self.db.getOTKManager()
- # Assume: never allow changes via GET
- if self.env['REQUEST_METHOD'] not in ['POST', 'PUT', 'DELETE']:
+ # Assume: never allow changes via GET/OPTIONS/HEAD
+ if method in {'GET', 'OPTIONS', 'HEAD'}:
if (self.form.list is not None) and ("@csrf" in self.form):
- # We have a nonce being used with a method it should
- # not be. If the nonce exists, report to admin so they
- # can fix the nonce leakage and destroy it. (nonces
- # used in a get are more exposed than those used in a
- # post.) Note, I don't attempt to validate here since
- # existence here is the sign of a failure. If nonce
- # exists try to report the referer header to try to
- # find where this comes from so it can be fixed. If
- # nonce doesn't exist just ignore it. Maybe we should
- # report, but somebody could spam us with a ton of
- # invalid keys and fill up the logs.
- if 'HTTP_REFERER' in self.env:
- referer = self.env['HTTP_REFERER']
- else:
- referer = self._("Referer header not available.")
- key = self.form['@csrf'].value
- if otks.exists(key):
- logger.error(
- self._("csrf key used with wrong method from: %s"),
- referer)
- otks.destroy(key)
- otks.commit()
+ self.expire_exposed_keys(method)
# do return here. Keys have been obsoleted.
# we didn't do a expire cycle of session keys,
# but that's ok.
return True
+ if method not in {'POST', 'PUT', 'DELETE', 'PATCH'}:
+ self.response_code = 405
+ raise UsageError("Bad method: %s" % method)
+
config = self.instance.config
current_user = self.db.getuid()
@@ -1817,10 +2033,10 @@ def determine_context(self, dre=dre_url):
is generally only one entry long.
- if there is no path, then we are in the "home" context.
- - if the path is "_file", then the additional path entry
- specifies the filename of a static file we're to serve up
- from the instance "html" directory. Raises a SendStaticFile
- exception.(*)
+ - if the path is "@@file" (or "_file", then the additional
+ path entry specifies the filename of a static file we're to
+ serve up from the instance "html" directory. Raises a
+ SendStaticFile exception.(*)
- if there is something in the path (eg "issue"), it identifies
the tracker class we're to display.
- if the path is an item designator (eg "issue123"), then we're
@@ -1882,7 +2098,7 @@ def determine_context(self, dre=dre_url):
else:
self.template = ''
return
- if path[0] in ('_file', '@@file'):
+ if path[0] in ('@@file', '_file'):
raise SendStaticFile(os.path.join(*path[1:]))
self.classname = path[0]
@@ -1926,19 +2142,19 @@ def reauth(self, exception):
Can be overridden by code in tracker's interfaces.py.
"""
-
- from roundup.anypy.vendored.cgi import MiniFieldStorage
+
+ from roundup.anypy.cgi_ import MiniFieldStorage
original_action = self.form['@action'].value if '@action' \
in self.form else ""
original_template = self.template
self.template = 'reauth'
- self.form.list = [ x for x in self.form.list
- if x.name not in ('@action',
- '@csrf',
- '@template'
- )]
+ self.form.list = [x for x in self.form.list
+ if x.name not in ('@action',
+ '@csrf',
+ '@template'
+ )]
# save the action and template used when the Reauth as
# triggered. Will be used to resolve the change by the reauth
@@ -2307,22 +2523,33 @@ def renderContext(self):
if 'Content-Type' not in self.additional_headers:
self.additional_headers['Content-Type'] = pt.content_type
- if self.env.get('CGI_SHOW_TIMING', ''):
+
+ show_timing = self.env.get('CGI_SHOW_TIMING', '').upper()
+
+ # check content_type so we don't calculate timing if
+ # we can't display it. This also prevents matching
+ # '' in js, css, svg or comment strings in
+ # images that will mangle result.
+ content_type = (self.additional_headers.get('Content-Type', "")
+ or pt.content_type)
+ if (show_timing in ('COMMENT', 'INLINE') and
+ content_type == "text/html"):
+ timings = {}
if self.env['CGI_SHOW_TIMING'].upper() == 'COMMENT':
- timings = {'starttag': ''}
+ delims = {'starttag': ''}
else:
- timings = {'starttag': '
', 'endtag': '
'}
+ delims = {'starttag': '
', 'endtag': '
'}
timings['seconds'] = time.time() - self.start
s = self._(
'%(starttag)sTime elapsed: %(seconds)fs%(endtag)s\n'
- ) % timings
+ ) % {**timings, **delims}
if hasattr(self.db, 'stats'):
timings.update(self.db.stats)
- s += self._("%(starttag)sCache hits: %(cache_hits)d,"
- " misses %(cache_misses)d."
- " Loading items: %(get_items)f secs."
- " Filtering: %(filtering)f secs."
- "%(endtag)s\n") % timings
+ allstats = ", ".join(["{}: {:.6g}".format(key, value)
+ for key, value in timings.items()
+ if key != 'seconds'])
+ s += '%(starttag)s %(allstats)s %(endtag)s\n' % {
+ 'allstats': allstats, **delims}
s += ''
result = result.replace('', s)
return result
@@ -2348,9 +2575,7 @@ def renderContext(self):
# receive an error message, and the adminstrator will
# receive a traceback, albeit with less information
# than the one we tried to generate above.
- if sys.version_info[0] > 2:
- raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
- exec('raise exc_info[0], exc_info[1], exc_info[2]') # nosec
+ raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
def renderError(self, error, response_code=400, use_template=True):
self.response_code = response_code
@@ -2445,7 +2670,7 @@ def handle_action(self):
action_klass = self.get_action_class(action)
# call the mapped action
- if isinstance(action_klass, type('')):
+ if isinstance(action_klass, str):
# old way of specifying actions
return getattr(self, action_klass)()
diff --git a/roundup/cgi/engine_jinja2.py b/roundup/cgi/engine_jinja2.py
index e0f590ffd..7856515d7 100644
--- a/roundup/cgi/engine_jinja2.py
+++ b/roundup/cgi/engine_jinja2.py
@@ -29,7 +29,6 @@
https://github.com/mitsuhiko/jinja2/issues/174
"""
-from __future__ import print_function
import jinja2
import mimetypes
import sys
diff --git a/roundup/cgi/engine_zopetal.py b/roundup/cgi/engine_zopetal.py
index 6b8987d6f..f319b3339 100644
--- a/roundup/cgi/engine_zopetal.py
+++ b/roundup/cgi/engine_zopetal.py
@@ -3,16 +3,14 @@
"""
__docformat__ = 'restructuredtext'
-import errno
import mimetypes
import os
-import os.path
import stat
-from roundup.cgi.templating import StringIO, context, TALLoaderBase
from roundup.cgi.PageTemplates import PageTemplate
from roundup.cgi.PageTemplates.Expressions import getEngine
from roundup.cgi.TAL import TALInterpreter
+from roundup.cgi.templating import StringIO, TALLoaderBase, context
class Loader(TALLoaderBase):
@@ -32,9 +30,8 @@ def load(self, tplname):
# has it changed?
try:
stime = os.stat(src)[stat.ST_MTIME]
- except os.error as error:
- if error.errno != errno.ENOENT:
- raise
+ except FileNotFoundError:
+ pass
if src in self.templates and \
stime <= self.templates[src].mtime:
diff --git a/roundup/cgi/form_parser.py b/roundup/cgi/form_parser.py
index 24e359a08..5e2802ec5 100644
--- a/roundup/cgi/form_parser.py
+++ b/roundup/cgi/form_parser.py
@@ -362,13 +362,13 @@ def parse(self, create=0):
assert isinstance(proptype, hyperdb.Multilink)
# value is a file upload... we *always* handle multiple
# files here (html5)
- if not isinstance(value, type([])):
+ if not isinstance(value, list):
value = [value]
elif isinstance(proptype, hyperdb.Multilink):
value = self.extractFormList(value)
else:
# multiple values are not OK
- if isinstance(value, type([])):
+ if isinstance(value, list):
raise FormError(self._(
'You have submitted more than one '
'value for the %s property') % propname)
@@ -397,7 +397,7 @@ def parse(self, create=0):
else:
raise FormError(self._('Password and confirmation text '
'do not match'))
- if isinstance(confirm, type([])):
+ if isinstance(confirm, list):
raise FormError(self._(
'You have submitted more than one '
'value for the %s property') % propname)
@@ -657,7 +657,7 @@ def extractFormList(self, value):
MiniFieldStorage('value')
'''
# multiple values are OK
- if isinstance(value, type([])):
+ if isinstance(value, list):
# it's a list of MiniFieldStorages - join then into
values = ','.join([i.value.strip() for i in value])
else:
diff --git a/roundup/cgi/templating.py b/roundup/cgi/templating.py
index b0b53f62b..175bcce16 100644
--- a/roundup/cgi/templating.py
+++ b/roundup/cgi/templating.py
@@ -27,7 +27,6 @@
import textwrap
from roundup import date, hyperdb, support
-from roundup.anypy import scandir_
from roundup.anypy import urllib_
from roundup.anypy.cgi_ import cgi
from roundup.anypy.html import html_escape
@@ -40,6 +39,7 @@
try:
from docutils.core import publish_parts as ReStructuredText
+ from docutils.writers.html4css1 import Writer as html_writer
except ImportError:
ReStructuredText = None
try:
@@ -240,11 +240,19 @@ def _options(config):
def anti_csrf_nonce(client, lifetime=None):
''' Create a nonce for defending against CSRF attack.
- Then it stores the nonce, the session id for the user
- and the user id in the one time key database for use
- by the csrf validator that runs in the client::inner_main
+ If tokenless/nonceless CSRF is activated, return 0 and
+ continue. 0 should never be a value in the otks db, but even
+ if it is, it will never be checked if tokenless csrf is used.
+
+ If the nonce is used, store the nonce, the session id for the
+ user and the user id in the one time key database for use by
+ the csrf validator that runs in the client::inner_main
module/function.
+
'''
+ if client.db.config['WEB_USE_TOKENLESS_CSRF_PROTECTION']:
+ return "0"
+
otks = client.db.getOTKManager()
key = otks.getUniqueKey()
# lifetime is in minutes.
@@ -776,7 +784,7 @@ def getItem(self, itemid, num_re=num_re):
""" Get an item of this class by its item id.
"""
# make sure we're looking at an itemid
- if not isinstance(itemid, type(1)) and not num_re.match(itemid):
+ if not isinstance(itemid, int) and not num_re.match(itemid):
itemid = self._klass.lookup(itemid)
return HTMLItem(self._client, self.classname, itemid)
@@ -846,7 +854,7 @@ def csv(self):
value = self._klass.get(nodeid, name)
if value is None:
l.append('')
- elif isinstance(value, type([])):
+ elif isinstance(value, list):
l.append(':'.join(map(str, value)))
else:
l.append(str(self._klass.get(nodeid, name)))
@@ -1265,7 +1273,7 @@ def history(self, direction='descending', dre=re.compile(r'^\d+$'),
if isinstance(prop, hyperdb.Multilink) and args[k]:
ml = []
for linkid in args[k]:
- if isinstance(linkid, type(())):
+ if isinstance(linkid, tuple):
sublabel = linkid[0] + ' '
linkids = linkid[1]
else:
@@ -1887,7 +1895,7 @@ def rst(self, hyperlink=1):
raise
return u2s(ReStructuredText(
- s, writer_name="html",
+ s, writer=html_writer(),
settings_overrides=self.rst_defaults)["html_body"])
def markdown(self, hyperlink=1):
@@ -2717,7 +2725,7 @@ def __init__(self, *args, **kwargs):
# ignore these errors to preserve user input
try:
display_value.sort(key=keyfun)
- except:
+ except Exception:
pass
self._value = display_value
@@ -3034,7 +3042,7 @@ def handleListCGIValue(value):
""" Value is either a single item or a list of items. Each item has a
.value that we're actually interested in.
"""
- if isinstance(value, type([])):
+ if isinstance(value, list):
return [value.value for value in value]
else:
value = value.value.strip()
@@ -3200,7 +3208,7 @@ def _post_init(self):
self.filterspec[name] = lookupIds(db, prop,
handleListCGIValue(fv))
else:
- if isinstance(fv, type([])):
+ if isinstance(fv, list):
self.filterspec[name] = [v.value for v in fv]
elif name == 'id':
# special case "id" property
@@ -3439,6 +3447,9 @@ def base_javascript(self):
alert("Your request is being processed.\\nPlease be patient.");
return false;
}
+
+ window.setTimeout("submitted = false;", 5000)
+
submitted = true;
return true;
}
diff --git a/roundup/cgi/wsgi_handler.py b/roundup/cgi/wsgi_handler.py
index f730503dc..50ffec43d 100644
--- a/roundup/cgi/wsgi_handler.py
+++ b/roundup/cgi/wsgi_handler.py
@@ -13,6 +13,7 @@
from roundup.anypy.strings import s2b
from roundup.cgi import TranslationService
from roundup.cgi.client import BinaryFieldStorage
+from roundup.logcontext import gen_trace_id, set_processName, store_trace_reason
BaseHTTPRequestHandler = http_.server.BaseHTTPRequestHandler
DEFAULT_ERROR_MESSAGE = http_.server.DEFAULT_ERROR_MESSAGE
@@ -96,12 +97,21 @@ def __init__(self, home, debug=False, timing=False, lang=None,
else:
self.translator = None
- if "cache_tracker" not in self.feature_flags or \
- self.feature_flags["cache_tracker"] is not False:
+ if self.use_cached_tracker():
self.tracker = roundup.instance.open(self.home, not self.debug)
else:
self.preload()
+ def use_cached_tracker(self):
+ return (
+ "cache_tracker" not in self.feature_flags or
+ self.feature_flags["cache_tracker"] is not False)
+
+ # Client entry point strips initial / from PATH_INFO so
+ # We need to do it here as well to prevent mismatch.
+ @set_processName("wsgi_handler")
+ @gen_trace_id()
+ @store_trace_reason("wsgi", extract="args[1]['PATH_INFO'][1:]")
def __call__(self, environ, start_response):
"""Initialize with `apache.Request` object"""
request = RequestHandler(environ, start_response)
@@ -112,7 +122,7 @@ def __call__(self, environ, start_response):
# This I hope will result in self.form=None
environ['CONTENT_LENGTH'] = 0
else:
- code = 501
+ code = 405
message, explain = BaseHTTPRequestHandler.responses[code]
request.start_response([('Content-Type', 'text/html')],
code)
@@ -132,8 +142,7 @@ def __call__(self, environ, start_response):
else:
form = BinaryFieldStorage(fp=environ['wsgi.input'], environ=environ)
- if "cache_tracker" not in self.feature_flags or \
- self.feature_flags["cache_tracker"] is not False:
+ if self.use_cached_tracker():
client = self.tracker.Client(self.tracker, request, environ, form,
self.translator)
try:
diff --git a/roundup/configuration.py b/roundup/configuration.py
index 421d91bcd..a278c221d 100644
--- a/roundup/configuration.py
+++ b/roundup/configuration.py
@@ -8,7 +8,6 @@
# where not expected by the Python code. Thus, a version check is
# used here instead of try/except.
import binascii
-import errno
import getopt
import logging
import logging.config
@@ -17,12 +16,14 @@
import smtplib
import sys
import time
+import traceback
import roundup.date
from roundup.anypy import random_
from roundup.anypy.strings import b2s
from roundup.backends import list_backends
from roundup.i18n import _
+from roundup.logcontext import gen_trace_id, get_context_info
if sys.version_info[0] > 2:
import configparser # Python 3
@@ -43,6 +44,16 @@ def __str__(self):
return self.args[0]
+class LoggingConfigError(ConfigurationError):
+ def __init__(self, message, **attrs):
+ super().__init__(message)
+ for key, value in attrs.items():
+ self.__setattr__(key, value)
+
+ def __str__(self):
+ return self.args[0]
+
+
class NoConfigError(ConfigurationError):
"""Raised when configuration loading fails
@@ -263,7 +274,8 @@ def load_ini(self, config):
try:
if config.has_option(self.section, self.setting):
self.set(config.get(self.section, self.setting))
- except configparser.InterpolationSyntaxError as e:
+ except (configparser.InterpolationSyntaxError,
+ configparser.InterpolationMissingOptionError) as e:
raise ParsingOptionError(
_("Error in %(filepath)s with section [%(section)s] at "
"option %(option)s: %(message)s") % {
@@ -286,7 +298,7 @@ def _value2str(self, value):
return "no"
def str2value(self, value):
- if isinstance(value, type("")):
+ if isinstance(value, str):
_val = value.lower()
if _val in ("yes", "true", "on", "1"):
_val = 1
@@ -371,17 +383,17 @@ class HtmlToTextOption(Option):
"""What module should be used to convert emails with only text/html
parts into text for display in roundup. Choose from beautifulsoup
- 4, dehtml - the internal code or none to disable html to text
- conversion. If beautifulsoup chosen but not available, dehtml will
- be used.
+ 4, justhtml, dehtml - the internal code or none to disable html to
+ text conversion. If beautifulsoup or justhtml is chosen but not
+ available, dehtml will be used.
"""
- class_description = "Allowed values: beautifulsoup, dehtml, none"
+ class_description = "Allowed values: beautifulsoup, justhtml, dehtml, none"
def str2value(self, value):
_val = value.lower()
- if _val in ("beautifulsoup", "dehtml", "none"):
+ if _val in ("beautifulsoup", "justhtml", "dehtml", "none"):
return _val
else:
raise OptionValueError(self, value, self.class_description)
@@ -565,6 +577,52 @@ def get(self):
return None
+class LoggingFormatOption(Option):
+ """Escape/unescape logging format string '%(' <-> '%%('
+
+ Config file parsing allows variable interpolation using
+ %(keyname)s. However this is exactly the format that we need
+ for creating a logging format string. So we tell the user to
+ quote the string using %%(...). Then we turn %%( -> %( when
+ retrieved and turn %( into %%( when saving the file.
+ """
+
+ class_description = ("Allowed value: Python LogRecord attribute named "
+ "formats with % *sign doubled*.\n"
+ "Also you can include the following attributes:\n"
+ " %%(trace_id)s %%(trace_reason)s and %%(pct_char)s"
+ )
+
+ def str2value(self, value):
+ """Validate and convert value of logging format string.
+
+ This does a dirty check to see if a token is missing a
+ specifier. So "%%(ascdate)s %%(level) " would fail because of
+ the 's' missing after 'level)'. But "%%(ascdate)s %%(level)s"
+ would pass.
+
+ Note that '%(foo)s' (i.e. unescaped substitution) generates
+ a error from the ini parser with a less than wonderful message.
+ """
+ unquoted_val = value.replace("%%(", "%(")
+
+ # regexp matches all current logging record object attribute names.
+ scanned_result = re.sub(r'%\([A-Za-z_]+\)\S', '', unquoted_val)
+ if scanned_result.find('%(') != -1:
+ raise OptionValueError(
+ self, unquoted_val,
+ "Check that all substitution tokens have a format "
+ "specifier after the ). Unrecognized use of %%(...) in: "
+ "%s" % scanned_result)
+
+ return str(unquoted_val)
+
+ def _value2str(self, value):
+ """Replace %( with %%( to quote the format substitution param.
+ """
+ return value.replace("%(", "%%(")
+
+
class OriginHeadersListOption(Option):
"""List of space seperated origin header values.
@@ -727,16 +785,11 @@ def get(self):
try:
with open(filepath) as f:
_val = f.readline().rstrip()
- # except FileNotFoundError: py2/py3
- # compatible version
- except EnvironmentError as e:
- if e.errno != errno.ENOENT:
- raise
- else:
- raise OptionValueError(
- self, _val,
- "Unable to read value for %s. Error opening "
- "%s: %s." % (self.name, e.filename, e.args[1]))
+ except FileNotFoundError as e:
+ raise OptionValueError(
+ self, _val,
+ "Unable to read value for %s. Error opening "
+ "%s: %s." % (self.name, e.filename, e.args[1]))
return self.str2value(_val)
def validate(self, options):
@@ -988,9 +1041,9 @@ def _value2str(self, value):
return value.pattern
def str2value(self, value):
- if not isinstance(value, type(u'')):
+ if not isinstance(value, str):
value = str(value)
- if not isinstance(value, type(u'')):
+ if not isinstance(value, str):
# if it is 7-bit ascii, use it as string,
# otherwise convert to unicode.
try:
@@ -1316,6 +1369,31 @@ def str2value(self, value):
"Defines the interval in seconds over which api login failures\n"
"are recorded. It allows api_failed_login_limit login failures\n"
"in this time interval. Tune this as needed.\n"),
+ (BooleanOption, 'use_tokenless_csrf_protection', 'no',
+ "Use a more modern CSRF protection method. If enabled, the\n"
+ "other CSRF settings below are disabled and a CSRF token is\n"
+ "not needed when creating your forms.\n"),
+ (OriginHeadersListOption, 'allowed_api_origins', "",
+ """A comma separated list of additonal valid Origin header
+values used when enforcing the origin header. If token style CSRF
+protection is enabled, it is used only for the api URLs (/rest and
+/xmlrpc). It is not used for the standard html URL's. If tokenless
+style csrf protection is enabled, it is used for URLs.
+
+These strings must precisely match the value of the Origin header. For
+example, 'https://bar.edu' and 'https://Bar.edu' are two different
+Origin values. Note origin values consist of scheme://host without any
+path component. Thus 'https://bar.edu/' is not valid. The value '*'
+can be used to match any origin when token style CSRF is enabled. It
+must be placed first in the list if used. Note that the '*' value
+allows any web page on the internet to make anonymous requests against
+your Roundup tracker.
+
+You must configure this if you have a web application on a different
+origin that accesses your Roundup instance.
+
+(The origin from the tracker.web setting in config.ini is
+always valid and does not need to be specified.)"""),
(CsrfSettingOption, 'csrf_enforce_token', "yes",
"""How do we deal with @csrf fields in posted forms.
Set this to 'required' to block the post and notify
@@ -1366,25 +1444,6 @@ def str2value(self, value):
log if the header is invalid or missing, but accept
the post.
Set this to 'no' to ignore the header and accept the post."""),
- (OriginHeadersListOption, 'allowed_api_origins', "",
- """A comma separated list of additonal valid Origin header
-values used when enforcing the header origin. They are used
-only for the api URL's (/rest and /xmlrpc). They are not
-used for the usual html URL's. These strings must match the
-value of the Origin header exactly. So 'https://bar.edu' and
-'https://Bar.edu' are two different Origin values. Note that
-the origin value is scheme://host. There is no path
-component. So 'https://bar.edu/' would never be valid.
-The value '*' can be used to match any origin. It must be
-first in the list if used. Note that this value allows
-any web page on the internet to make anonymous requests
-against your Roundup tracker.
-
-You need to set these if you have a web application on a
-different origin accessing your Roundup instance.
-
-(The origin from the tracker.web setting in config.ini is
-always valid and does not need to be specified.)"""),
(CsrfSettingOption, 'csrf_enforce_header_x-forwarded-host', "yes",
"""Verify that the X-Forwarded-Host http header matches
the host part of the tracker.web setting in config.ini.
@@ -1604,6 +1663,10 @@ def str2value(self, value):
"Minimal severity level of messages written to log file.\n"
"If above 'config' option is set, this option has no effect.\n"
"Allowed values: DEBUG, INFO, WARNING, ERROR"),
+ (LoggingFormatOption, "format",
+ "%(asctime)s %(trace_id)s %(levelname)s %(message)s",
+ "Format of the logging messages with all '%' signs\n"
+ "doubled so they are not interpreted by the config file."),
(BooleanOption, "disable_loggers", "no",
"If set to yes, only the loggers configured in this section will\n"
"be used. Yes will disable gunicorn's --access-logfile.\n"),
@@ -1748,11 +1811,11 @@ def str2value(self, value):
(HtmlToTextOption, "convert_htmltotext", "none",
"If an email has only text/html parts, use this module\n"
"to convert the html to text. Choose from beautifulsoup 4,\n"
- "dehtml - (internal code), or none to disable conversion.\n"
- "If 'none' is selected, email without a text/plain part\n"
- "will be returned to the user with a message. If\n"
- "beautifulsoup is selected but not installed dehtml will\n"
- "be used instead."),
+ "justhtml, dehtml - (internal code), or none to disable\n"
+ "conversion. If 'none' is selected, email without a text/plain\n"
+ "part will be returned to the user with a message. If\n"
+ "beautifulsoup or justhtml is selected but not installed\n"
+ "dehtml will be used instead."),
(BooleanOption, "keep_real_from", "no",
"When handling emails ignore the Resent-From:-header\n"
"and use the original senders From:-header instead.\n"
@@ -2322,6 +2385,18 @@ def _get_unset_options(self):
def _get_name(self):
return self["TRACKER_NAME"]
+ @gen_trace_id()
+ def _logging_test(self, sinfo, msg="test %(a)s\n%(sinfo)s", args=None):
+ """Test method for logging formatting.
+
+ Not used in production.
+ """
+ logger = logging.getLogger('roundup')
+ if args:
+ logger.info(msg, *args)
+ else:
+ logger.info(msg, extra={"a": "a_var", "sinfo": sinfo})
+
def reset(self):
Config.reset(self)
if self.ext:
@@ -2330,22 +2405,324 @@ def reset(self):
self.detectors.reset()
self.init_logging()
+ def gather_callstack(self, keep_full_stack=False):
+ # Locate logging call in stack
+ stack = traceback.extract_stack()
+ if keep_full_stack:
+ last_frame_index = len(stack)
+ else:
+ for last_frame_index, frame in enumerate(stack):
+ # Walk from the top of stack looking for
+ # "logging" in filename (really
+ # filepath). Can't use /logging/ as
+ # windows uses \logging\.
+ #
+ # Filtering by looking for "logging" in
+ # the filename isn't great.
+ if "logging" in frame.filename:
+ break
+ if not keep_full_stack:
+ stack = stack[0:last_frame_index]
+ return (stack, last_frame_index)
+
+ def context_filter(self, record):
+ """Add context to record, expand context references in record.msg
+ Define pct_char as '%'
+ """
+
+ # This method can be called multiple times on different handlers.
+ # However it modifies the record on the first call and the changes
+ # persist in the record. So we only want to be called once per
+ # record.
+ if hasattr(record, "ROUNDUP_CONTEXT_FILTER_CALLED"):
+ return True
+
+ for name, value in get_context_info():
+ if not hasattr(record, name):
+ setattr(record, name, value)
+ continue
+ if (name == "processName" and
+ isinstance(value, str) and
+ getattr(record, name) == "MainProcess"):
+ setattr(record, name, value)
+
+ record.pct_char = "%"
+ record.ROUNDUP_CONTEXT_FILTER_CALLED = True
+
+ if hasattr(record, "sinfo"):
+ # sinfo has to be set via extras argument to logging commands
+ # to activate this.
+ #
+ # sinfo value can be:
+ # non-integer: "", None etc. Print 5 elements of
+ # stack before logging call
+ # integer N > 0: print N elements of stack before
+ # logging call
+ # 0: print whole stack before logging call
+ # integer N < 0: undocumented print stack starting at
+ # extract_stack() below. I.E. do not set bottom of
+ # stack to the logging call.
+ # if |N| is greater than stack height, print whole stack.
+ stack_height = record.sinfo
+ keep_full_stack = False
+
+ if isinstance(stack_height, int):
+ if stack_height < 0:
+ keep_full_stack = True
+ stack_height = abs(stack_height)
+ if stack_height == 0:
+ # None will set value to actual stack height.
+ stack_height = None
+ else:
+ stack_height = 5
+
+ stack, last_frame_index = self.gather_callstack(keep_full_stack)
+
+ if stack_height is None:
+ stack_height = last_frame_index
+ elif stack_height > last_frame_index:
+ stack_height = last_frame_index # start at frame 0
+
+ # report the stack info
+ record.sinfo = "".join(
+ traceback.format_list(
+ stack[last_frame_index - stack_height:last_frame_index]
+ )
+ )
+
+ # if args are present, just return. Logging will
+ # expand the arguments.
+ if record.args:
+ return True
+
+ # Since args not present, try formatting msg using
+ # named arguments.
+ try:
+ record.msg = record.msg % record.__dict__
+ except (ValueError, TypeError):
+ # ValueError: means there is a % sign in the msg
+ # somewhere that is not meant to be a format token. So
+ # just leave msg unexpanded.
+ #
+ # TypeError - a positional format string is being
+ # handled without setting record.args. E.G.
+ # .info("result is %f")
+ # Leave message unexpanded.
+ pass
+ return True
+
+ def add_logging_context_filter(self):
+ """Update log record with contextvar values and expand msg
+
+ The contextvar values are stored as attributes on the log
+ record object in record.__dict__. They should not exist
+ when this is called. Do not overwrite them if they do exist
+ as they can be set in a logger call using::
+
+ logging.warning("a message", extra = {"trace_id": foo})
+
+ the extra argument/parameter.
+
+ Attempt to expand msg using the variables in
+ record.__dict__. This makes::
+
+ logging.warning("the URL was: %(trace_reason)s")
+
+ work and replaces the ``%(trace_reason)s`` token with the value.
+ Note that you can't use positional params and named params
+ together. For example::
+
+ logging.warning("user = %s and URL was: %(trace_reason)s", user)
+
+ will result in an exception in logging when it formats the
+ message.
+
+ Also ``%(pct_char)`` is defined to allow the addition of %
+ characters in the format string as bare % chars can't make
+ it past the configparser and %% encoded ones run into issue
+ with the format verifier.
+
+ Calling a logger (.warning() etc.) with the argument::
+
+ extra={"sinfo": an_int}
+
+ will result in a stack trace starting at the logger call
+ and going up the stack for `an_int` frames. Using "True"
+ in place of `an_int` will print only the call to the logger.
+
+ Note that logging untrusted strings in the msg set by user
+ (untrusted) may be an issue. So don't do something like:
+
+ .info("%s" % web_url)
+
+ as web_url could include '%(trace_id)s'. Instead use:
+
+ .info("%(url)s", extra=("url": web_url))
+
+ Even in the problem case, I think the damage is contained since:
+
+ * data doesn't leak as the log string is not exposed to the
+ user.
+
+ * the log string isn't executed or used internally.
+
+ * log formating can raise an exception. But this won't
+ affect the application as the exception is swallowed in
+ the logging package. The raw message would be printed by
+ the fallback logging handler.
+
+ but if it is a concern, make sure user data is added using
+ the extra dict when calling one of the logging functions.
+ """
+ loggers = [logging.getLogger(name) for name in
+ logging.root.manager.loggerDict]
+ # append the root logger as it is not listed in loggerDict
+ loggers.append(logging.getLogger())
+ for logger in loggers:
+ for hdlr in logger.handlers:
+ hdlr.addFilter(self.context_filter)
+
+ def load_config_dict_from_json_file(self, filename):
+ import json
+ comment_re = re.compile(
+ r"""^\s*//#.* # comment at beginning of line possibly indented.
+ | # or
+ ^(.*)\s\s\s\//.* # comment char preceeded by at least three spaces.
+ """, re.VERBOSE)
+
+ config_list = []
+ with open(filename) as config_file:
+ for line in config_file:
+ match = comment_re.search(line)
+ if match:
+ if match.lastindex:
+ config_list.append(match.group(1) + "\n")
+ else:
+ # insert blank line for comment line to
+ # keep line numbers in sync.
+ config_list.append("\n")
+ continue
+ config_list.append(line)
+
+ try:
+ config_dict = json.loads("".join(config_list))
+ except json.decoder.JSONDecodeError as e:
+ error_at_doc_line = e.lineno
+ # subtract 1 - zero index on config_list
+ # remove '\n' for display
+ try:
+ line = config_list[error_at_doc_line - 1][:-1]
+ except IndexError:
+ line = _("Error found at end of file. Maybe missing a "
+ "block closing '}'.")
+
+ hint = ""
+ if line.find('//') != -1:
+ hint = "\nMaybe bad inline comment, 3 spaces needed before //."
+
+ raise LoggingConfigError(
+ 'Error parsing json logging dict (%(file)s) '
+ 'near \n\n %(line)s\n\n'
+ '%(msg)s: line %(lineno)s column %(colno)s.%(hint)s' %
+ {"file": filename,
+ "line": line,
+ "msg": e.msg,
+ "lineno": error_at_doc_line,
+ "colno": e.colno,
+ "hint": hint},
+ config_file=self.filepath,
+ source="json.loads"
+ )
+
+ return config_dict
+
def init_logging(self):
_file = self["LOGGING_CONFIG"]
if _file and os.path.isfile(_file):
- logging.config.fileConfig(
- _file,
- disable_existing_loggers=self["LOGGING_DISABLE_LOGGERS"])
+ if _file.endswith(".ini"):
+ try:
+ logging.config.fileConfig(
+ _file,
+ disable_existing_loggers=self[
+ "LOGGING_DISABLE_LOGGERS"])
+ except (ValueError, RuntimeError, configparser.ParsingError,
+ KeyError, NameError, ModuleNotFoundError) as e:
+ # configparser.DuplicateOptionError includes
+ # filename, line number and a useful error.
+ # so we don't have to augment it.
+ context = []
+ if hasattr(e, '__context__') and e.__context__:
+ # get additional error info.
+ context.append(str(e.__context__))
+ if hasattr(e, '__doc__') and e.__doc__:
+ context.append(e.__doc__)
+
+ if isinstance(e, KeyError):
+ context.append("No section found with this name.")
+ if not context:
+ context = ["No additional information available."]
+
+ raise LoggingConfigError(
+ "Error loading logging config from %(file)s.\n\n"
+ " %(msg)s\n\n%(context)s\n" % {
+ "file": _file,
+ "msg": type(e).__name__ + ": " + str(e),
+ "context": " ".join(context),
+ }
+ )
+ elif _file.endswith(".json"):
+ config_dict = self.load_config_dict_from_json_file(_file)
+ try:
+ logging.config.dictConfig(config_dict)
+ except ValueError as e:
+ # docs say these exceptions:
+ # ValueError, TypeError, AttributeError, ImportError
+ # could be raised, but
+ # looking through the code, it looks like
+ # configure() maps all exceptions (including
+ # ImportError, TypeError) raised by functions to
+ # ValueError.
+ context = "No additional information available."
+ if hasattr(e, '__context__') and e.__context__:
+ # get additional error info. E.G. if INFO
+ # is replaced by MANGO, context is:
+ # ValueError("Unknown level: 'MANGO'")
+ # while str(e) is "Unable to configure handler 'access'"
+ context = e.__context__
+
+ raise LoggingConfigError(
+ 'Error loading logging dict from %(file)s.\n'
+ '%(msg)s\n%(context)s\n' % {
+ "file": _file,
+ "msg": type(e).__name__ + ": " + str(e),
+ "context": context
+ },
+ config_file=self.filepath,
+ source="dictConfig"
+ )
+ else:
+ raise OptionValueError(
+ self.options['LOGGING_CONFIG'],
+ _file,
+ "Unable to load logging config file. "
+ "File extension must be '.ini' or '.json'.\n"
+ )
+
+ self.add_logging_context_filter()
return
+ if _file:
+ raise OptionValueError(self.options['LOGGING_CONFIG'],
+ _file,
+ "Unable to find logging config file.")
+
_file = self["LOGGING_FILENAME"]
# set file & level on the roundup logger
logger = logging.getLogger('roundup')
hdlr = logging.FileHandler(_file) if _file else \
logging.StreamHandler(sys.stdout)
- formatter = logging.Formatter(
- '%(asctime)s %(levelname)s %(message)s')
+ formatter = logging.Formatter(self["LOGGING_FORMAT"])
hdlr.setFormatter(formatter)
# no logging API to remove all existing handlers!?!
for h in logger.handlers:
@@ -2353,6 +2730,15 @@ def init_logging(self):
logger.removeHandler(hdlr)
logger.handlers = [hdlr]
logger.setLevel(self["LOGGING_LEVEL"] or "ERROR")
+ if 'pytest' not in sys.modules:
+ # logger.propatgate is True by default. This is
+ # needed so that pytest caplog code will work. In
+ # production leaving it set to True relogs everything
+ # using the root logger with logging BASIC_FORMAT:
+ # "%(level)s:%(name)s:%(message)s
+ logger.propagate = False
+
+ self.add_logging_context_filter()
def validator(self, options):
""" Validate options once all options are loaded.
diff --git a/roundup/date.py b/roundup/date.py
index 6515ee320..eda57407f 100644
--- a/roundup/date.py
+++ b/roundup/date.py
@@ -17,7 +17,6 @@
"""Date, time and time interval handling.
"""
-from __future__ import print_function
__docformat__ = 'restructuredtext'
import calendar
@@ -324,6 +323,9 @@ class Date:
>>> test_fin(u)
'''
+ __slots__ = ("year", "month", "day", "hour", "minute", "second",
+ "_", "ngettext", "translator")
+
def __init__(self, spec='.', offset=0, add_granularity=False,
translator=i18n):
"""Construct a date given a specification and a time zone offset.
@@ -355,7 +357,7 @@ def __init__(self, spec='.', offset=0, add_granularity=False,
self.second += spec.microsecond/1000000.
return
- if isinstance(spec, type('')):
+ if isinstance(spec, str):
self.set(spec, offset=offset, add_granularity=add_granularity)
return
elif hasattr(spec, 'tuple'):
@@ -962,7 +964,7 @@ def __add__(self, other):
asgn = a[0]
b = other.get_tuple()
bsgn = b[0]
- i = [asgn*x + bsgn*y for x, y in zip(a[1:], b[1:])]
+ i = [asgn*x + bsgn*y for x, y in zip(a[1:], b[1:], strict=True)]
i.insert(0, 1)
i = fixTimeOverflow(i)
return Interval(i, translator=self.translator)
@@ -982,7 +984,7 @@ def __sub__(self, other):
asgn = a[0]
b = other.get_tuple()
bsgn = b[0]
- i = [asgn*x - bsgn*y for x, y in zip(a[1:], b[1:])]
+ i = [asgn*x - bsgn*y for x, y in zip(a[1:], b[1:], strict=True)]
i.insert(0, 1)
i = fixTimeOverflow(i)
return Interval(i, translator=self.translator)
diff --git a/roundup/dehtml.py b/roundup/dehtml.py
index cd913a0d1..9a58733ea 100644
--- a/roundup/dehtml.py
+++ b/roundup/dehtml.py
@@ -1,10 +1,12 @@
-from __future__ import print_function
-
import sys
from roundup.anypy.strings import u2s, uchr
+# ruff PLC0415 ignore imports not at top of file
+# ruff RET505 ignore else after return
+# ruff: noqa: PLC0415 RET505
+
_pyver = sys.version_info[0]
@@ -28,6 +30,108 @@ def html2text(html):
return u2s(soup.get_text("\n", strip=True))
+ self.html2text = html2text
+ elif converter == "justhtml":
+ from justhtml import stream
+
+ def html2text(html):
+ # The below does not work.
+ # Using stream parser since I couldn't seem to strip
+ # 'script' and 'style' blocks. But stream doesn't
+ # have error reporting or stripping of text nodes
+ # and dropping empty nodes. Also I would like to try
+ # its GFM markdown output too even though it keeps
+ # tables as html and doesn't completely covert as
+ # this would work well for those supporting markdown.
+ #
+ # ctx used for for testing since I have a truncated
+ # test doc. It eliminates error from missing DOCTYPE
+ # and head.
+ #
+ #from justhtml import JustHTML
+ # from justhtml.context import FragmentContext
+ #
+ #ctx = FragmentContext('html')
+ #justhtml = JustHTML(html,collect_errors=True,
+ # fragment_context=ctx)
+ # I still have the text output inside style/script tags.
+ # with :not(style, script). I do get text contents
+ # with query("style, script").
+ #
+ #return u2s("\n".join(
+ # [elem.to_text(separator="\n", strip=True)
+ # for elem in justhtml.query(":not(style, script)")])
+ # )
+
+ # define inline elements so I can accumulate all unbroken
+ # text in a single line with embedded inline elements.
+ # 'br' is inline but should be treated it as a line break
+ # and element before/after should not be accumulated
+ # together.
+ inline_elements = (
+ "a",
+ "address",
+ "b",
+ "cite",
+ "code",
+ "em",
+ "i",
+ "img",
+ "mark",
+ "q",
+ "s",
+ "small",
+ "span",
+ "strong",
+ "sub",
+ "sup",
+ "time")
+
+ # each line is appended and joined at the end
+ text = []
+ # the accumulator for all text in inline elements
+ text_accumulator = ""
+ # if set skip all lines till matching end tag found
+ # used to skip script/style blocks
+ skip_till_endtag = None
+ # used to force text_accumulator into text with added
+ # newline so we have a blank line between paragraphs.
+ _need_parabreak = False
+
+ for event, data in stream(html):
+ if event == "end" and skip_till_endtag == data:
+ skip_till_endtag = None
+ continue
+ if skip_till_endtag:
+ continue
+ if (event == "start" and
+ data[0] in ('script', 'style')):
+ skip_till_endtag = data[0]
+ continue
+ if (event == "start" and
+ text_accumulator and
+ data[0] not in inline_elements):
+ # add accumulator to "text"
+ text.append(text_accumulator)
+ text_accumulator = ""
+ _need_parabreak = False
+ elif event == "text":
+ if not data.isspace():
+ text_accumulator = text_accumulator + data
+ _need_parabreak = True
+ elif (_need_parabreak and
+ event == "start" and
+ data[0] == "p"):
+ text.append(text_accumulator + "\n")
+ text_accumulator = ""
+ _need_parabreak = False
+
+ # save anything left in the accumulator at end of document
+ if text_accumulator:
+ # add newline to match dehtml and beautifulsoup
+ text.append(text_accumulator + "\n")
+ return u2s("\n".join(text))
+
self.html2text = html2text
else:
raise ImportError
@@ -96,6 +200,16 @@ def html2text(html):
if __name__ == "__main__":
+ # ruff: noqa: B011 S101
+
+ try:
+ assert False
+ except AssertionError:
+ pass
+ else:
+ print("Error, assertions turned off. Test fails")
+ sys.exit(1)
+
html = """
#\n #' html/search.html
# sphinx inserts \: for : in meta tags. Get rid of the \ in
@@ -26,7 +28,6 @@ html: doc_links ## make standalone HTML files (KEEP_HTML=1 speeds build)
cp robots.txt html/robots.txt
mkdir -p html/signatures && cp signatures/*.asc html/signatures
cp --no-clobber -r docs/html_extra/. html/docs/.
- cp htaccess html/.htaccess
@echo; \
l=$$(find html -name '*.orig' -o -name '*~' | tee /dev/tty | wc -l);\
if [ $$l -ne 0 ]; then echo "Garbage files found" && false; fi
@@ -39,7 +40,7 @@ clean: ## remove all produced files
linkcheck: ## check external links for existence
mkdir -p $(TMP)/linkcheck $(TMP)/doctrees
- sphinx-build -b linkcheck -d $(TMP)/doctrees . $(TMP)/linkcheck
+ $(SPHINX) -b linkcheck -d $(TMP)/doctrees . $(TMP)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in .build/linkcheck/output.txt."
@@ -62,23 +63,36 @@ sourceforge_dev_sync: ## sync html directory to sourceforce /dev_docs subdir
# --delete-exclude
# IS NOT (and must not be) SET
read -p "sync to dev_docs y/N? " resp; echo "$$resp" | grep -i "^y"
- rsync -av --no-times --delete --exclude 'docs_backup*' \
- --exclude '.buildinfo' \
- --backup --backup-dir docs_backup-`date --iso-8601=seconds` \
+ # --mkpath doesn't work for rsync on sourceforge so use scripted sftp
+ # if it fails (directory exists), that's ok it still exits with 0
+ printf 'cd /home/project-web/roundup/htdocs\nmkdir dev_docs' | \
+ sftp rouilj@web.sourceforge.net
+ rsync -avz --no-times --delete \
+ --exclude 'docs_backup*' --exclude '.buildinfo' --backup \
+ --backup-dir docs_backup-`date --iso-8601=seconds` \
html/. \
web.sourceforge.net:/home/project-web/roundup/htdocs/dev_docs/.
sourceforge_prod_sync: ## sync html directory to sourceforce production website
read -p "sync to production y/N? " resp; echo "$$resp" | grep -i "^y"
- rsync -av --no-times --delete --exclude 'docs_backup*' \
- --backup --backup-dir docs_backup-`date --iso-8601=seconds` \
- html/. \
+ rsync -avz --no-times --delete --exclude 'docs_backup*' \
+ --exclude 'ahrefs_*' --backup \
+ --backup-dir docs_backup-`date --iso-8601=seconds` \
+ --checksum html/. \
web.sourceforge.net:/home/project-web/roundup/htdocs/.
+sourceforge_prod_pull: ## sync sourceforce production website to local web_html
+ read -p "pull from production y/N? " resp; echo "$$resp" | grep -i "^y"
+ test -d web_html || mkdir web_html
+ rsync -avz --no-times --delete \
+ --checksum \
+ web.sourceforge.net:/home/project-web/roundup/htdocs/. \
+ web_html/.
+
sourceforge_home_sync: ## sync html directory to sourceforge:~/roundup_docs
read -p "sync to home y/N? " resp; echo "$$resp" | grep -i "^y"
- rsync -av --no-times --delete --exclude 'docs_backup*' \
+ rsync -avz --no-times --delete --exclude 'docs_backup*' \
--backup --backup-dir docs_backup-`date --iso-8601=seconds` \
html/. \
web.sourceforge.net:roundup_docs/.
diff --git a/website/www/_static/style.css b/website/www/_static/style.css
index aa319c4b2..99d0d7834 100644
--- a/website/www/_static/style.css
+++ b/website/www/_static/style.css
@@ -186,6 +186,8 @@ div[class^=highlight-], div[class^=highlight-] * { width:
/* style */
:link { color: rgb(220,0,0); text-decoration: none;}
+/* improve contrast to AA */
+.admonition.note :link { color: rgb(170,1,1); text-decoration: none;}
:link:hover {
text-decoration: underline solid clamp(1px, .3ex, 4px);
text-underline-position: under;
@@ -438,6 +440,14 @@ dd > ul:first-child {
white-space: break-spaces;
}*/
+/* improve color contrast to AA against yellowish highlight bg */
+div.highlight .c1 {
+ color: rgb(3,114,3);
+}
+div.highlight .na {
+ color: rgb(220,2,2);
+}
+
/* Forcing wrap in a pre leads to some confusing line breaks.
Use a horizontal scroll. Indicate the scroll by using
rounded scroll shadows.
@@ -517,6 +527,12 @@ aside.footnote > span + p { margin-block-start: unset; }
scroll-margin-block-start: 2em;
}
+/* Make feature blocks look like raised cards for some variety. */
+div.card {
+ box-shadow: rgba(6, 24, 44, 0.4) 0px 0px 0px 2px, rgba(6, 24, 44, 0.65) 0px 4px 6px -1px, rgba(255, 255, 255, 0.08) 0px 1px 0px inset;
+ padding: 1em;
+}
+
/* website only */
/* assume desktop reader for local html files, also no contact
page for local doc files */
@@ -594,3 +610,4 @@ div#contact table td {
white-space: nowrap;
padding: 1pt 1em;
}
+
diff --git a/website/www/_templates/layout.html b/website/www/_templates/layout.html
index e40290fb6..c95fc7338 100644
--- a/website/www/_templates/layout.html
+++ b/website/www/_templates/layout.html
@@ -5,25 +5,16 @@
'_static/tabs.js',
] %}
-
+
{%- if builder != 'htmlhelp' %}
{%- set titlesuffix = docstitle|e %}
{%- set titlesuffix = " - " + titlesuffix %}
- {%- endif %}
+
{{ title|striptags }}{{ titlesuffix }}
- {%- if builder != 'htmlhelp' %}
-
{%- if pagename == 'search' %}
{%- for scriptfile in script_files %}
@@ -111,7 +102,9 @@
-
+
+
-
+
{% block body %} {% endblock %}
@@ -151,7 +144,7 @@
{%- for cssfile in css_files %}
-
+
{%- endfor %}