mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-05-07 10:10:44 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0149d88b2d |
@@ -2,7 +2,7 @@
|
||||
|
||||
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
||||
# Semver means never use .01, or 00. Should be .1.
|
||||
__version__ = '0.55.3'
|
||||
__version__ = '0.54.10'
|
||||
|
||||
from changedetectionio.strtobool import strtobool
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
@@ -377,7 +377,7 @@
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const LIVE_PROVIDERS = ['openai', 'anthropic', 'gemini', 'ollama', 'openrouter'];
|
||||
const LIVE_PROVIDERS = ['openai', 'anthropic', 'gemini', 'ollama'];
|
||||
const BASE_DEFAULTS = { ollama: 'http://localhost:11434' };
|
||||
const KEY_HINTS = {
|
||||
openai: '{{ _("platform.openai.com → API keys") }}',
|
||||
@@ -401,7 +401,7 @@
|
||||
|
||||
fetchGroup.style.display = LIVE_PROVIDERS.includes(provider) ? '' : 'none';
|
||||
|
||||
const needsBase = provider === 'ollama';
|
||||
const needsBase = provider === 'ollama' || provider === 'openrouter';
|
||||
baseGroup.style.display = needsBase ? '' : 'none';
|
||||
if (BASE_DEFAULTS[provider] !== undefined) {
|
||||
if (!baseField.value) baseField.value = BASE_DEFAULTS[provider];
|
||||
|
||||
@@ -283,8 +283,6 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
# Check cache — keyed by version pair + prompt hash (invalidates if prompt changes)
|
||||
cached = watch.get_llm_diff_summary(from_version, to_version, prompt=cache_prompt)
|
||||
if cached:
|
||||
import time
|
||||
datastore.set_last_viewed(uuid, int(time.time()))
|
||||
return jsonify({'summary': cached, 'error': None, 'cached': True})
|
||||
|
||||
# Check global monthly token budget before making an LLM call
|
||||
@@ -318,8 +316,6 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not cache llm summary for {uuid}: {e}")
|
||||
|
||||
import time
|
||||
datastore.set_last_viewed(uuid, int(time.time()))
|
||||
return jsonify({'summary': summary, 'error': None, 'cached': False})
|
||||
|
||||
@diff_blueprint.route("/diff/<uuid_str:uuid>/extract", methods=['GET'])
|
||||
|
||||
@@ -223,8 +223,8 @@ window.watchOverviewI18n = {
|
||||
{%- if any_has_restock_price_processor -%}
|
||||
<th>{{ _('Restock & Price') }}</th>
|
||||
{%- endif -%}
|
||||
<th><a class="{{ 'active '+link_order if sort_attribute == 'last_checked' else 'inactive' }}" href="{{url_for('watchlist.index', sort='last_checked', order=link_order, tag=active_tag_uuid)}}"><span class="hide-on-mobile">{{ _('Last Checked') }}</span><span class="hide-on-desktop">{{ _('Checked') }}</span> <span class='arrow {{link_order}}'></span></a></th>
|
||||
<th><a class="{{ 'active '+link_order if sort_attribute == 'last_changed' else 'inactive' }}" href="{{url_for('watchlist.index', sort='last_changed', order=link_order, tag=active_tag_uuid)}}"><span class="hide-on-mobile">{{ _('Last Changed') }}</span><span class="hide-on-desktop">{{ _('Changed') }}</span> <span class='arrow {{link_order}}'></span></a></th>
|
||||
<th><a class="{{ 'active '+link_order if sort_attribute == 'last_checked' else 'inactive' }}" href="{{url_for('watchlist.index', sort='last_checked', order=link_order, tag=active_tag_uuid)}}"><span class="hide-on-mobile">{{ _('Last') }}</span> {{ _('Checked') }} <span class='arrow {{link_order}}'></span></a></th>
|
||||
<th><a class="{{ 'active '+link_order if sort_attribute == 'last_changed' else 'inactive' }}" href="{{url_for('watchlist.index', sort='last_changed', order=link_order, tag=active_tag_uuid)}}"><span class="hide-on-mobile">{{ _('Last') }}</span> {{ _('Changed') }} <span class='arrow {{link_order}}'></span></a></th>
|
||||
<th class="empty-cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -981,11 +981,6 @@ def changedetection_app(config=None, datastore_o=None):
|
||||
"queued_data": all_queued
|
||||
})
|
||||
|
||||
if strtobool(os.getenv('HISTORY_SNAPSHOT_FILE_ALLOW_OUTSIDE_WATCH_DATADIR', 'False')):
|
||||
logger.warning("SECURITY WARNING: HISTORY_SNAPSHOT_FILE_ALLOW_OUTSIDE_WATCH_DATADIR is enabled — "
|
||||
"snapshot reads are NOT confined to the watch data directory. "
|
||||
"This disables protection against path traversal via restored backups (GHSA-8757-69j2-hx56).")
|
||||
|
||||
# Start the async workers during app initialization
|
||||
# Can be overridden by ENV or use the default settings
|
||||
n_workers = int(os.getenv("FETCH_WORKERS", datastore.data['settings']['requests']['workers']))
|
||||
|
||||
@@ -282,7 +282,7 @@ def xpath_filter(xpath_filter, html_content, append_pretty_line_formatting=False
|
||||
try:
|
||||
if is_xml:
|
||||
# So that we can keep CDATA for cdata_in_document_to_text() to process
|
||||
parser = etree.XMLParser(strip_cdata=False, resolve_entities=False, no_network=True)
|
||||
parser = etree.XMLParser(strip_cdata=False)
|
||||
# For XML/RSS content, use etree.fromstring to properly handle XML declarations
|
||||
tree = etree.fromstring(html_content.encode('utf-8') if isinstance(html_content, str) else html_content, parser=parser)
|
||||
else:
|
||||
@@ -346,7 +346,7 @@ def xpath1_filter(xpath_filter, html_content, append_pretty_line_formatting=Fals
|
||||
try:
|
||||
if is_xml:
|
||||
# So that we can keep CDATA for cdata_in_document_to_text() to process
|
||||
parser = etree.XMLParser(strip_cdata=False, resolve_entities=False, no_network=True)
|
||||
parser = etree.XMLParser(strip_cdata=False)
|
||||
# For XML/RSS content, use etree.fromstring to properly handle XML declarations
|
||||
tree = etree.fromstring(html_content.encode('utf-8') if isinstance(html_content, str) else html_content, parser=parser)
|
||||
else:
|
||||
|
||||
@@ -465,21 +465,22 @@ class model(EntityPersistenceMixin, watch_base):
|
||||
if ',' in i:
|
||||
k, v = i.strip().split(',', 2)
|
||||
|
||||
# Always resolve history entries to within the watch's own data directory.
|
||||
# Entries restored from backup could contain absolute or traversal paths —
|
||||
# never trust them. Use realpath to also block symlink-based escapes.
|
||||
safe_data_dir = os.path.realpath(self.data_dir)
|
||||
snapshot_fname = os.path.basename(v.strip())
|
||||
resolved_path = os.path.realpath(os.path.join(self.data_dir, snapshot_fname))
|
||||
# The index history could contain a relative path, so we need to make the fullpath
|
||||
# so that python can read it
|
||||
# Cross-platform: check for any path separator (works on Windows and Unix)
|
||||
if os.sep not in v and '/' not in v and '\\' not in v:
|
||||
# Relative filename only, no path separators
|
||||
v = os.path.join(self.data_dir, v)
|
||||
else:
|
||||
# It's possible that they moved the datadir on older versions
|
||||
# So the snapshot exists but is in a different path
|
||||
# Cross-platform: use os.path.basename instead of split('/')
|
||||
snapshot_fname = os.path.basename(v)
|
||||
proposed_new_path = os.path.join(self.data_dir, snapshot_fname)
|
||||
if not os.path.exists(v) and os.path.exists(proposed_new_path):
|
||||
v = proposed_new_path
|
||||
|
||||
if not resolved_path.startswith(safe_data_dir + os.sep) and resolved_path != safe_data_dir:
|
||||
logger.warning(f"Skipping unsafe history entry for {self.get('uuid')}: {v!r}")
|
||||
continue
|
||||
|
||||
if not os.path.exists(resolved_path):
|
||||
continue
|
||||
|
||||
tmp_history[k] = resolved_path
|
||||
tmp_history[k] = v
|
||||
|
||||
if len(tmp_history):
|
||||
self.__newest_history_key = list(tmp_history.keys())[-1]
|
||||
@@ -562,15 +563,6 @@ class model(EntityPersistenceMixin, watch_base):
|
||||
if not filepath:
|
||||
filepath = self.history[timestamp]
|
||||
|
||||
# Confine every read to the watch's own data directory — defence in depth
|
||||
# against any path that bypasses the history parser (e.g. direct filepath= callers).
|
||||
# Set HISTORY_SNAPSHOT_FILE_ALLOW_OUTSIDE_WATCH_DATADIR=true to disable (not recommended).
|
||||
if self.data_dir and not strtobool(os.getenv('HISTORY_SNAPSHOT_FILE_ALLOW_OUTSIDE_WATCH_DATADIR', 'False')):
|
||||
safe_data_dir = os.path.realpath(self.data_dir)
|
||||
resolved = os.path.realpath(filepath)
|
||||
if not (resolved.startswith(safe_data_dir + os.sep) or resolved == safe_data_dir):
|
||||
raise PermissionError(f"Snapshot path {filepath!r} is outside the watch data directory")
|
||||
|
||||
# Check if binary file (image, PDF, etc.)
|
||||
# Binary files are NEVER saved with .br compression, only text files are
|
||||
binary_extensions = ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.pdf', '.bin', '.jfif')
|
||||
|
||||
@@ -3,13 +3,6 @@
|
||||
* Provides accessible, animated confirmation dialogs
|
||||
*/
|
||||
|
||||
// Escapes a string for safe insertion via innerHTML
|
||||
function _modalEscapeHTML(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
const ModalDialog = {
|
||||
/**
|
||||
* Show a confirmation dialog
|
||||
@@ -132,10 +125,9 @@ const ModalDialog = {
|
||||
* @param {Function} onConfirm - Callback when confirmed
|
||||
*/
|
||||
confirmDelete: function(itemName, onConfirm) {
|
||||
const safeName = _modalEscapeHTML(itemName);
|
||||
return this.confirm({
|
||||
title: 'Delete ' + safeName + '?',
|
||||
message: `<p>Are you sure you want to delete <strong>${safeName}</strong>?</p><p>This action cannot be undone.</p>`,
|
||||
title: 'Delete ' + itemName + '?',
|
||||
message: `<p>Are you sure you want to delete <strong>${itemName}</strong>?</p><p>This action cannot be undone.</p>`,
|
||||
type: 'danger',
|
||||
confirmText: 'Delete',
|
||||
cancelText: 'Cancel',
|
||||
@@ -149,10 +141,9 @@ const ModalDialog = {
|
||||
* @param {Function} onConfirm - Callback when confirmed
|
||||
*/
|
||||
confirmUnlink: function(itemName, onConfirm) {
|
||||
const safeName = _modalEscapeHTML(itemName);
|
||||
return this.confirm({
|
||||
title: 'Unlink ' + safeName + '?',
|
||||
message: `<p>Are you sure you want to unlink all watches from <strong>${safeName}</strong>?</p><p>The tag will be kept but watches will be removed from it.</p>`,
|
||||
title: 'Unlink ' + itemName + '?',
|
||||
message: `<p>Are you sure you want to unlink all watches from <strong>${itemName}</strong>?</p><p>The tag will be kept but watches will be removed from it.</p>`,
|
||||
type: 'warning',
|
||||
confirmText: 'Unlink',
|
||||
cancelText: 'Cancel',
|
||||
@@ -181,11 +172,11 @@ $(document).ready(function() {
|
||||
const url = $element.attr('href');
|
||||
|
||||
const config = {
|
||||
type: $element.attr('data-confirm-type') || 'danger',
|
||||
title: $element.attr('data-confirm-title') || 'Confirm Action',
|
||||
message: $element.attr('data-confirm-message') || '<p>Are you sure you want to proceed?</p>',
|
||||
confirmText: $element.attr('data-confirm-button') || 'Confirm',
|
||||
cancelText: $element.attr('data-cancel-button') || 'Cancel',
|
||||
type: $element.data('confirm-type') || 'danger',
|
||||
title: $element.data('confirm-title') || 'Confirm Action',
|
||||
message: $element.data('confirm-message') || '<p>Are you sure you want to proceed?</p>',
|
||||
confirmText: $element.data('confirm-button') || 'Confirm',
|
||||
cancelText: $element.data('cancel-button') || 'Cancel',
|
||||
onConfirm: function() {
|
||||
// If it's a link, navigate to the URL
|
||||
if ($element.is('a')) {
|
||||
|
||||
@@ -180,10 +180,4 @@ $grid-gap: 0.5rem;
|
||||
.pure-table td {
|
||||
padding: 3px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.watch-table thead tr th .hide-on-desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -52,6 +52,10 @@
|
||||
<td><code>{{ '{{diff_url}}' }}</code></td>
|
||||
<td>{{ _('The URL of the diff output for the watch.') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{{ '{{diff_url}}' }}</code></td>
|
||||
<td>{{ _('The URL of the diff output for the watch.') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>{{ '{{diff}}' }}</code></td>
|
||||
<td>{{ _('The diff output - only changes, additions, and removals') }}<br>
|
||||
|
||||
@@ -336,58 +336,6 @@ def test_hardcoded_fallback_when_nothing_set(
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_llm_summary_ajax_sets_last_viewed(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
Calling /diff/<uuid>/llm-summary via AJAX should mark the watch as viewed
|
||||
(set last_viewed) for both fresh and cached responses.
|
||||
"""
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
_configure_llm(client)
|
||||
ds = client.application.config.get('DATASTORE')
|
||||
|
||||
test_url = url_for('test_endpoint', content_type='text/html', content='v1', _external=True)
|
||||
uuid = ds.add_watch(url=test_url)
|
||||
watch = ds.data['watching'][uuid]
|
||||
|
||||
watch.save_history_blob('old content\n', '4000000000', 'snap-old')
|
||||
watch.save_history_blob('new content\n', '4000000001', 'snap-new')
|
||||
|
||||
assert watch['last_viewed'] == 0, "last_viewed should start at 0"
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = 'Content changed from old to new.'
|
||||
mock_response.usage = MagicMock(total_tokens=50, prompt_tokens=40, completion_tokens=10)
|
||||
|
||||
with patch('litellm.completion', return_value=mock_response):
|
||||
res = client.get(
|
||||
url_for('ui.ui_diff.diff_llm_summary', uuid=uuid,
|
||||
from_version='4000000000', to_version='4000000001'),
|
||||
)
|
||||
|
||||
assert res.status_code == 200
|
||||
data = res.get_json()
|
||||
assert data['summary'] == 'Content changed from old to new.'
|
||||
assert watch['last_viewed'] > 0, "last_viewed should be set after fresh LLM summary"
|
||||
|
||||
# Reset and verify the cached path also sets last_viewed
|
||||
watch['last_viewed'] = 0
|
||||
with patch('litellm.completion', return_value=mock_response):
|
||||
res2 = client.get(
|
||||
url_for('ui.ui_diff.diff_llm_summary', uuid=uuid,
|
||||
from_version='4000000000', to_version='4000000001'),
|
||||
)
|
||||
|
||||
assert res2.status_code == 200
|
||||
data2 = res2.get_json()
|
||||
assert data2.get('cached') is True
|
||||
assert watch['last_viewed'] > 0, "last_viewed should be set even when returning cached summary"
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_global_default_saved_and_loaded_via_settings_form(
|
||||
client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import pytest
|
||||
|
||||
from flask import url_for
|
||||
from zipfile import ZipFile, ZIP_DEFLATED
|
||||
|
||||
from changedetectionio.tests.util import set_modified_response
|
||||
from .util import live_server_setup, wait_for_all_checks, delete_all_watches
|
||||
@@ -827,75 +823,3 @@ def test_unresolvable_hostname_is_allowed(client, live_server, monkeypatch):
|
||||
res = client.get(url_for('watchlist.index'))
|
||||
assert b'this-host-does-not-exist-xyz987.invalid' in res.data, \
|
||||
"Unresolvable hostname watch should appear in the watch overview list"
|
||||
|
||||
|
||||
def test_ghsa_8757_69j2_hx56_backup_restore_history_path_traversal(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
GHSA-8757-69j2-hx56: Crafted backup ZIP with absolute path in history.txt must not
|
||||
expose arbitrary local files through the preview or API endpoints.
|
||||
|
||||
Attack chain:
|
||||
1. Attacker creates a backup ZIP with a malicious history.txt containing an absolute
|
||||
path (e.g. /etc/passwd) as a snapshot reference.
|
||||
2. Victim restores the backup.
|
||||
3. Attacker reads the targeted file via the Preview page.
|
||||
|
||||
The fix ensures history entries are always resolved to os.path.basename() joined with
|
||||
the watch's data_dir, and rejects entries that escape that directory.
|
||||
"""
|
||||
set_original_response(datastore_path=datastore_path)
|
||||
|
||||
datastore = live_server.app.config['DATASTORE']
|
||||
watch_url = url_for('test_endpoint', _external=True)
|
||||
|
||||
# Create a real watch and trigger a check so we have a valid backup structure
|
||||
uuid = datastore.add_watch(url=watch_url)
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# Download a legitimate backup to use as a template
|
||||
client.get(url_for("backups.request_backup"), follow_redirects=True)
|
||||
time.sleep(4)
|
||||
res = client.get(url_for("backups.download_backup", filename="latest"), follow_redirects=True)
|
||||
assert res.content_type == "application/zip"
|
||||
|
||||
# Tamper: replace the history.txt inside the backup with a malicious entry
|
||||
# that points at /etc/passwd (a file that exists on any Unix system)
|
||||
original_zip = ZipFile(io.BytesIO(res.data))
|
||||
tampered_buf = io.BytesIO()
|
||||
with ZipFile(tampered_buf, 'w', ZIP_DEFLATED) as new_zip:
|
||||
for item in original_zip.infolist():
|
||||
data = original_zip.read(item.filename)
|
||||
# Replace the watch's history.txt with a malicious absolute path entry
|
||||
if item.filename.endswith('history.txt') and uuid in item.filename:
|
||||
data = b'1776969105,/etc/passwd\n'
|
||||
new_zip.writestr(item, data)
|
||||
|
||||
tampered_buf.seek(0)
|
||||
tampered_zip_data = tampered_buf.read()
|
||||
|
||||
# Restore the tampered backup
|
||||
res = client.post(
|
||||
url_for("backups.restore.backups_restore_start"),
|
||||
data={
|
||||
'zip_file': (io.BytesIO(tampered_zip_data), 'malicious_backup.zip'),
|
||||
'include_watches': 'y',
|
||||
'include_watches_replace_existing': 'y',
|
||||
},
|
||||
content_type='multipart/form-data',
|
||||
follow_redirects=True
|
||||
)
|
||||
assert res.status_code == 200
|
||||
time.sleep(2)
|
||||
|
||||
# Now try to read the /etc/passwd contents via the Preview page using the injected timestamp
|
||||
res = client.get(
|
||||
url_for("ui.ui_preview.preview_page", uuid=uuid) + "?timestamp=1776969105",
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
# The preview must NOT contain typical /etc/passwd content
|
||||
assert b'root:' not in res.data, \
|
||||
"Preview must not expose /etc/passwd — history path traversal not blocked"
|
||||
assert b'/bin/' not in res.data or b'No history' in res.data or res.status_code in [404, 500], \
|
||||
"Preview must not serve arbitrary local files from a malicious history entry"
|
||||
|
||||
@@ -311,72 +311,5 @@ class TestLLMDiffSummaryCache(unittest.TestCase):
|
||||
assert watch.get_llm_diff_summary('1000', '2000', prompt=self.PROMPT) == 'Updated summary'
|
||||
|
||||
|
||||
class TestHistoryPathTraversal(unittest.TestCase):
|
||||
"""GHSA-8757-69j2-hx56: history.txt must not allow reads outside the watch data dir."""
|
||||
|
||||
def _make_watch(self):
|
||||
mock_datastore = {'settings': {'application': {}}, 'watching': {}}
|
||||
watch = Watch.model(datastore_path='/tmp', __datastore=mock_datastore, default={})
|
||||
watch.ensure_data_dir_exists()
|
||||
return watch
|
||||
|
||||
def _write_history_txt(self, watch, lines):
|
||||
"""Directly write raw lines to history.txt to simulate a restored backup."""
|
||||
fname = os.path.join(watch.data_dir, watch.history_index_filename)
|
||||
with open(fname, 'w', encoding='utf-8') as f:
|
||||
f.writelines(lines)
|
||||
|
||||
def test_absolute_path_in_history_is_rejected(self):
|
||||
"""An absolute path like /etc/passwd must not appear in history."""
|
||||
watch = self._make_watch()
|
||||
self._write_history_txt(watch, ['1000000000,/etc/passwd\n'])
|
||||
history = watch.history
|
||||
self.assertEqual(history, {}, "Absolute path entry must be rejected")
|
||||
|
||||
def test_traversal_path_in_history_is_rejected(self):
|
||||
"""A relative traversal path like ../../etc/passwd must not appear in history."""
|
||||
watch = self._make_watch()
|
||||
self._write_history_txt(watch, ['1000000000,../../etc/passwd\n'])
|
||||
history = watch.history
|
||||
self.assertEqual(history, {}, "Path traversal entry must be rejected")
|
||||
|
||||
def test_normal_snapshot_entry_is_accepted(self):
|
||||
"""A bare filename written by save_history_blob must still load correctly."""
|
||||
import uuid as uuid_builder
|
||||
watch = self._make_watch()
|
||||
watch.save_history_blob(contents="hello world", timestamp=1000000000, snapshot_id=str(uuid_builder.uuid4()))
|
||||
history = watch.history
|
||||
self.assertEqual(len(history), 1, "Normal snapshot entry must be accepted")
|
||||
self.assertTrue(
|
||||
list(history.values())[0].startswith(watch.data_dir),
|
||||
"Resolved path must be inside the watch data directory"
|
||||
)
|
||||
|
||||
def test_get_history_snapshot_blocks_outside_path_directly(self):
|
||||
"""get_history_snapshot(filepath=...) must raise if the path escapes data_dir."""
|
||||
watch = self._make_watch()
|
||||
with self.assertRaises(PermissionError):
|
||||
watch.get_history_snapshot(filepath='/etc/passwd')
|
||||
|
||||
def test_get_history_snapshot_blocks_traversal_directly(self):
|
||||
"""get_history_snapshot(filepath=...) must raise on ../../ traversal paths."""
|
||||
watch = self._make_watch()
|
||||
with self.assertRaises(PermissionError):
|
||||
watch.get_history_snapshot(filepath=os.path.join(watch.data_dir, '../../etc/passwd'))
|
||||
|
||||
def test_resolved_path_stays_inside_data_dir(self):
|
||||
"""All resolved history paths must reside within the watch's data_dir."""
|
||||
import uuid as uuid_builder
|
||||
watch = self._make_watch()
|
||||
for ts in [1000000001, 1000000002, 1000000003]:
|
||||
watch.save_history_blob(contents=f"content {ts}", timestamp=ts, snapshot_id=str(uuid_builder.uuid4()))
|
||||
safe_dir = os.path.realpath(watch.data_dir)
|
||||
for path in watch.history.values():
|
||||
self.assertTrue(
|
||||
os.path.realpath(path).startswith(safe_dir),
|
||||
f"Path {path!r} escapes the watch data directory"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# run from dir above changedetectionio/ dir
|
||||
# python3 -m pytest changedetectionio/tests/unit/test_xml_security.py
|
||||
|
||||
import pytest
|
||||
from changedetectionio import html_tools
|
||||
|
||||
|
||||
def _xxe_payload(file_path: str) -> str:
|
||||
return f"""<?xml version="1.0"?>
|
||||
<!DOCTYPE root [
|
||||
<!ENTITY xxe SYSTEM "file://{file_path}">
|
||||
]>
|
||||
<root><item>&xxe;</item></root>"""
|
||||
|
||||
|
||||
def test_xxe_not_expanded_xpath_filter(tmp_path):
|
||||
"""xpath_filter must not expand external entities (CVE-2026-41895)."""
|
||||
sentinel_file = tmp_path / "sentinel.txt"
|
||||
sentinel = "xxe_sentinel_should_never_appear_in_output"
|
||||
sentinel_file.write_text(sentinel)
|
||||
|
||||
result = html_tools.xpath_filter("//item", _xxe_payload(sentinel_file), is_xml=True)
|
||||
assert sentinel not in result
|
||||
|
||||
|
||||
def test_xxe_not_expanded_xpath1_filter(tmp_path):
|
||||
"""xpath1_filter must not expand external entities (CVE-2026-41895)."""
|
||||
sentinel_file = tmp_path / "sentinel.txt"
|
||||
sentinel = "xxe_sentinel_should_never_appear_in_output"
|
||||
sentinel_file.write_text(sentinel)
|
||||
|
||||
result = html_tools.xpath1_filter("//item", _xxe_payload(sentinel_file), is_xml=True)
|
||||
assert sentinel not in result
|
||||
@@ -213,9 +213,6 @@ Never fix one language and move on.
|
||||
pybabel init -i changedetectionio/translations/messages.pot \
|
||||
-d changedetectionio/translations \
|
||||
-l NEW_LANG_CODE
|
||||
# Reset POT-Creation-Date to the sentinel so it matches the other catalogs
|
||||
sed -i 's|^"POT-Creation-Date: .*\\n"$|"POT-Creation-Date: 1970-01-01 00:00+0000\\n"|' \
|
||||
changedetectionio/translations/NEW_LANG_CODE/LC_MESSAGES/messages.po
|
||||
python setup.py compile_catalog
|
||||
```
|
||||
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-01-02 11:40+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: cs\n"
|
||||
@@ -2217,17 +2217,13 @@ msgid "Checked"
|
||||
msgstr "Zkontrolováno"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr "Poslední Zkontrolováno"
|
||||
msgid "Last"
|
||||
msgstr "Poslední"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr "Změněno"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr "Poslední Změněno"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr "Nejsou nakonfigurována žádná sledování webových stránek, do výše uvedeného pole přidejte adresu URL nebo"
|
||||
@@ -2276,10 +2272,18 @@ msgstr "Cena"
|
||||
msgid "No information"
|
||||
msgstr "Žádné informace"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr "Probíhá kontrola"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr "Ve frontě"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-01-14 03:57+0100\n"
|
||||
"Last-Translator: \n"
|
||||
"Language: de\n"
|
||||
@@ -2268,17 +2268,13 @@ msgid "Checked"
|
||||
msgstr "Geprüft"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr "Zuletzt Geprüft"
|
||||
msgid "Last"
|
||||
msgstr "Zuletzt"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr "Geändert"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr "Zuletzt Geändert"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr "Es sind keine Website-Überwachungen konfiguriert. Bitte fügen Sie im Feld oben eine URL hinzu, oder"
|
||||
@@ -2327,10 +2323,18 @@ msgstr "Preis"
|
||||
msgid "No information"
|
||||
msgstr "Keine Informationen"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr "Jetzt prüfen"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr "Wartend"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/dgtlmoon/changedetection.io\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-01-12 16:33+0100\n"
|
||||
"Last-Translator: British English Translation Team\n"
|
||||
"Language: en_GB\n"
|
||||
@@ -2213,17 +2213,13 @@ msgid "Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgid "Last"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr ""
|
||||
@@ -2272,10 +2268,18 @@ msgstr ""
|
||||
msgid "No information"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/dgtlmoon/changedetection.io\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-01-12 16:37+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: en_US\n"
|
||||
@@ -2213,17 +2213,13 @@ msgid "Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgid "Last"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr ""
|
||||
@@ -2272,10 +2268,18 @@ msgstr ""
|
||||
msgid "No information"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
@@ -3,7 +3,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.53.6\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-03-20 18:13+0100\n"
|
||||
"Last-Translator: Adrian Gonzalez <adrian@example.com>\n"
|
||||
"Language: es\n"
|
||||
@@ -2282,17 +2282,13 @@ msgid "Checked"
|
||||
msgstr "Comprobado"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr "Último Comprobado"
|
||||
msgid "Last"
|
||||
msgstr "Último"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr "Cambiadp"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr "Último Cambiadp"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr "No hay monitores de detección de cambios de página web configuradas; agregue una URL en el cuadro de arriba, o"
|
||||
@@ -2341,10 +2337,18 @@ msgstr "Precio"
|
||||
msgid "No information"
|
||||
msgstr "Sin información"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr "Comprobando ahora"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr "En cola"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-01-02 11:40+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: fr\n"
|
||||
@@ -2224,17 +2224,13 @@ msgid "Checked"
|
||||
msgstr "Vérification"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr "Dernier Vérification"
|
||||
msgid "Last"
|
||||
msgstr "Dernier"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr "Modifié"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr "Dernier Modifié"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr "Aucune surveillance de site Web configurée, veuillez ajouter une URL dans la case ci-dessus, ou"
|
||||
@@ -2283,10 +2279,18 @@ msgstr "Prix"
|
||||
msgid "No information"
|
||||
msgstr "Aucune information"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr "Vérifier maintenant"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr "En file d'attente"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-01-02 15:32+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: it\n"
|
||||
@@ -2215,17 +2215,13 @@ msgid "Checked"
|
||||
msgstr "Controllo"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr "Ultimo Controllo"
|
||||
msgid "Last"
|
||||
msgstr "Ultimo"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr "Modifica"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr "Ultimo Modifica"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr "Nessun monitoraggio configurato, aggiungi un URL nella casella sopra, oppure"
|
||||
@@ -2274,10 +2270,18 @@ msgstr "Prezzo"
|
||||
msgid "No information"
|
||||
msgstr "Nessuna informazione"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr "Controllo in corso"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr "In coda"
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.53.6\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-03-31 23:52+0900\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: ja\n"
|
||||
@@ -2232,17 +2232,13 @@ msgid "Checked"
|
||||
msgstr "チェック済み"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr "前回チェック"
|
||||
msgid "Last"
|
||||
msgstr "最終"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr "変更済み"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr "前回更新"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr "ウェブページ変更検知ウォッチが設定されていません。上のボックスにURLを追加するか、"
|
||||
@@ -2291,10 +2287,18 @@ msgstr "価格"
|
||||
msgid "No information"
|
||||
msgstr "情報なし"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr "前回チェック"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr "今すぐチェック"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr "前回更新"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr "キュー済み"
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,9 @@
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.55.3\n"
|
||||
"Project-Id-Version: changedetection.io 0.54.10\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-04-28 15:26+1000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -2212,17 +2212,13 @@ msgid "Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgid "Last"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr ""
|
||||
@@ -2271,10 +2267,18 @@ msgstr ""
|
||||
msgid "No information"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr ""
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.54.8\n"
|
||||
"Report-Msgid-Bugs-To: mstrey@gmail.com\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-04-07 22:00-0300\n"
|
||||
"Last-Translator: Gemini AI\n"
|
||||
"Language: pt_BR\n"
|
||||
@@ -2261,17 +2261,13 @@ msgid "Checked"
|
||||
msgstr "Verificado"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr "Último Verificado"
|
||||
msgid "Last"
|
||||
msgstr "Último"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr "Alterado"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr "Último Alterado"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr "Nenhum monitoramento configurado, adicione uma URL na caixa acima ou"
|
||||
@@ -2320,10 +2316,18 @@ msgstr "Preço"
|
||||
msgid "No information"
|
||||
msgstr "Sem informações"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr "Verificando agora"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr "Enfileirado"
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.53.6\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-04-10 20:38+0300\n"
|
||||
"Last-Translator: \n"
|
||||
"Language: tr\n"
|
||||
@@ -2266,17 +2266,13 @@ msgid "Checked"
|
||||
msgstr "Kontrol Edildi"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr "Son Kontrol Edildi"
|
||||
msgid "Last"
|
||||
msgstr "Son"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr "Değiştirildi"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr "Son Değiştirildi"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr "Yapılandırılmış web sayfası değişiklik tespiti izleyicisi yok, lütfen yukarıdaki kutuya bir URL ekleyin veya"
|
||||
@@ -2325,10 +2321,18 @@ msgstr "Fiyat"
|
||||
msgid "No information"
|
||||
msgstr "Bilgi yok"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr "Şimdi kontrol ediliyor"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr "Sırada"
|
||||
|
||||
Binary file not shown.
@@ -6,7 +6,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/dgtlmoon/changedetection.io\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-02-19 12:30+0100\n"
|
||||
"Last-Translator: \n"
|
||||
"Language: uk\n"
|
||||
@@ -2243,17 +2243,13 @@ msgid "Checked"
|
||||
msgstr "Перевірено"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr "Останній Перевірено"
|
||||
msgid "Last"
|
||||
msgstr "Останній"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr "Змінено"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr "Останній Змінено"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr "Немає налаштованих завдань для відстеження змін, будь ласка, додайте URL у поле вище або"
|
||||
@@ -2302,10 +2298,18 @@ msgstr "Ціна"
|
||||
msgid "No information"
|
||||
msgstr "Немає інформації"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr "Перевірка..."
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr "В черзі"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-01-18 21:31+0800\n"
|
||||
"Last-Translator: 吾爱分享 <admin@wuaishare.cn>\n"
|
||||
"Language: zh\n"
|
||||
@@ -2218,17 +2218,13 @@ msgid "Checked"
|
||||
msgstr "检查"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr "最近检查"
|
||||
msgid "Last"
|
||||
msgstr "最近"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr "变更"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr "最近变更"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr "尚未配置网站监控项,请在上方输入 URL 或"
|
||||
@@ -2277,10 +2273,18 @@ msgstr "价格"
|
||||
msgid "No information"
|
||||
msgstr "暂无信息"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr "正在检查"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr "队列中"
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 1970-01-01 00:00+0000\n"
|
||||
"POT-Creation-Date: 2026-04-26 22:34+1000\n"
|
||||
"PO-Revision-Date: 2026-01-15 12:00+0800\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: zh_Hant_TW\n"
|
||||
@@ -1429,7 +1429,7 @@ msgstr "已將 1 個監測任務排入複查佇列。"
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
msgid "Queued {} watches for rechecking ({} already queued or running)."
|
||||
msgstr "已將 {} 個監測任務排入複查佇列({} 個已在佇列中或正在執行)。"
|
||||
msgstr "已將 {} 個監測任務排入複查佇列。"
|
||||
|
||||
#: changedetectionio/blueprint/ui/__init__.py
|
||||
#, python-brace-format
|
||||
@@ -2217,17 +2217,13 @@ msgid "Checked"
|
||||
msgstr "檢查"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr "上次檢查"
|
||||
msgid "Last"
|
||||
msgstr "上次"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Changed"
|
||||
msgstr "變更"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr "上次變更"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "No web page change detection watches configured, please add a URL in the box above, or"
|
||||
msgstr "未設定網站監測任務,請在上方欄位新增 URL,或"
|
||||
@@ -2276,10 +2272,18 @@ msgstr "價格"
|
||||
msgid "No information"
|
||||
msgstr "無資訊"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Checked"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/templates/base.html
|
||||
msgid "Checking now"
|
||||
msgstr "正在檢查"
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Last Changed"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
|
||||
msgid "Queued"
|
||||
msgstr "已排程"
|
||||
|
||||
Reference in New Issue
Block a user