mirror of
				https://github.com/dgtlmoon/changedetection.io.git
				synced 2025-11-04 08:34:57 +00:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			PDF-diff-i
			...
			highlight-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6ff424de24 | ||
| 
						 | 
					6ad0eb736d | 
@@ -713,7 +713,6 @@ def changedetection_app(config=None, datastore_o=None):
 | 
			
		||||
                                     available_processors=processors.available_processors(),
 | 
			
		||||
                                     browser_steps_config=browser_step_ui_config,
 | 
			
		||||
                                     emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
 | 
			
		||||
                                     extra_title=f" - Edit - {watch.label}",
 | 
			
		||||
                                     form=form,
 | 
			
		||||
                                     has_default_notification_urls=True if len(datastore.data['settings']['application']['notification_urls']) else False,
 | 
			
		||||
                                     has_empty_checktime=using_default_check_time,
 | 
			
		||||
@@ -913,29 +912,21 @@ def changedetection_app(config=None, datastore_o=None):
 | 
			
		||||
 | 
			
		||||
        # Read as binary and force decode as UTF-8
 | 
			
		||||
        # Windows may fail decode in python if we just use 'r' mode (chardet decode exception)
 | 
			
		||||
        from_version = request.args.get('from_version')
 | 
			
		||||
        from_version_index = -2 # second newest
 | 
			
		||||
        if from_version and from_version in dates:
 | 
			
		||||
            from_version_index = dates.index(from_version)
 | 
			
		||||
        else:
 | 
			
		||||
            from_version = dates[from_version_index]
 | 
			
		||||
        try:
 | 
			
		||||
            newest_version_file_contents = watch.get_history_snapshot(dates[-1])
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            newest_version_file_contents = "Unable to read {}.\n".format(dates[-1])
 | 
			
		||||
 | 
			
		||||
        previous_version = request.args.get('previous_version')
 | 
			
		||||
        previous_timestamp = dates[-2]
 | 
			
		||||
        if previous_version:
 | 
			
		||||
            previous_timestamp = previous_version
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            from_version_file_contents = watch.get_history_snapshot(dates[from_version_index])
 | 
			
		||||
            previous_version_file_contents = watch.get_history_snapshot(previous_timestamp)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            from_version_file_contents = "Unable to read to-version at index{}.\n".format(dates[from_version_index])
 | 
			
		||||
            previous_version_file_contents = "Unable to read {}.\n".format(previous_timestamp)
 | 
			
		||||
 | 
			
		||||
        to_version = request.args.get('to_version')
 | 
			
		||||
        to_version_index = -1
 | 
			
		||||
        if to_version and to_version in dates:
 | 
			
		||||
            to_version_index = dates.index(to_version)
 | 
			
		||||
        else:
 | 
			
		||||
            to_version = dates[to_version_index]
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            to_version_file_contents = watch.get_history_snapshot(dates[to_version_index])
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            to_version_file_contents = "Unable to read to-version at index{}.\n".format(dates[to_version_index])
 | 
			
		||||
 | 
			
		||||
        screenshot_url = watch.get_screenshot()
 | 
			
		||||
 | 
			
		||||
@@ -951,24 +942,22 @@ def changedetection_app(config=None, datastore_o=None):
 | 
			
		||||
 | 
			
		||||
        output = render_template("diff.html",
 | 
			
		||||
                                 current_diff_url=watch['url'],
 | 
			
		||||
                                 from_version=str(from_version),
 | 
			
		||||
                                 to_version=str(to_version),
 | 
			
		||||
                                 current_previous_version=str(previous_version),
 | 
			
		||||
                                 extra_stylesheets=extra_stylesheets,
 | 
			
		||||
                                 extra_title=f" - Diff - {watch.label}",
 | 
			
		||||
                                 extra_title=" - Diff - {}".format(watch['title'] if watch['title'] else watch['url']),
 | 
			
		||||
                                 extract_form=extract_form,
 | 
			
		||||
                                 is_html_webdriver=is_html_webdriver,
 | 
			
		||||
                                 last_error=watch['last_error'],
 | 
			
		||||
                                 last_error_screenshot=watch.get_error_snapshot(),
 | 
			
		||||
                                 last_error_text=watch.get_error_text(),
 | 
			
		||||
                                 left_sticky=True,
 | 
			
		||||
                                 newest=to_version_file_contents,
 | 
			
		||||
                                 newest=newest_version_file_contents,
 | 
			
		||||
                                 newest_version_timestamp=dates[-1],
 | 
			
		||||
                                 password_enabled_and_share_is_off=password_enabled_and_share_is_off,
 | 
			
		||||
                                 from_version_file_contents=from_version_file_contents,
 | 
			
		||||
                                 to_version_file_contents=to_version_file_contents,
 | 
			
		||||
                                 previous=previous_version_file_contents,
 | 
			
		||||
                                 screenshot=screenshot_url,
 | 
			
		||||
                                 uuid=uuid,
 | 
			
		||||
                                 versions=dates, # All except current/last
 | 
			
		||||
                                 versions=dates[:-1], # All except current/last
 | 
			
		||||
                                 watch_a=watch
 | 
			
		||||
                                 )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,9 @@
 | 
			
		||||
 | 
			
		||||
from bs4 import BeautifulSoup
 | 
			
		||||
from inscriptis import get_text
 | 
			
		||||
from inscriptis.model.config import ParserConfig
 | 
			
		||||
from jsonpath_ng.ext import parse
 | 
			
		||||
from typing import List
 | 
			
		||||
from inscriptis.css_profiles import CSS_PROFILES, HtmlElement
 | 
			
		||||
from inscriptis.html_properties import Display
 | 
			
		||||
from inscriptis.model.config import ParserConfig
 | 
			
		||||
from xml.sax.saxutils import escape as xml_escape
 | 
			
		||||
import json
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
@@ -71,15 +68,10 @@ def element_removal(selectors: List[str], html_content):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Return str Utf-8 of matched rules
 | 
			
		||||
def xpath_filter(xpath_filter, html_content, append_pretty_line_formatting=False, is_rss=False):
 | 
			
		||||
def xpath_filter(xpath_filter, html_content, append_pretty_line_formatting=False):
 | 
			
		||||
    from lxml import etree, html
 | 
			
		||||
 | 
			
		||||
    parser = None
 | 
			
		||||
    if is_rss:
 | 
			
		||||
        # So that we can keep CDATA for cdata_in_document_to_text() to process
 | 
			
		||||
        parser = etree.XMLParser(strip_cdata=False)
 | 
			
		||||
 | 
			
		||||
    tree = html.fromstring(bytes(html_content, encoding='utf-8'), parser=parser)
 | 
			
		||||
    tree = html.fromstring(bytes(html_content, encoding='utf-8'))
 | 
			
		||||
    html_block = ""
 | 
			
		||||
 | 
			
		||||
    r = tree.xpath(xpath_filter.strip(), namespaces={'re': 'http://exslt.org/regular-expressions'})
 | 
			
		||||
@@ -98,13 +90,11 @@ def xpath_filter(xpath_filter, html_content, append_pretty_line_formatting=False
 | 
			
		||||
        elif type(element) == etree._ElementUnicodeResult:
 | 
			
		||||
            html_block += str(element)
 | 
			
		||||
        else:
 | 
			
		||||
            if not is_rss:
 | 
			
		||||
                html_block += etree.tostring(element, pretty_print=True).decode('utf-8')
 | 
			
		||||
            else:
 | 
			
		||||
                html_block += f"<div>{element.text}</div>\n"
 | 
			
		||||
            html_block += etree.tostring(element, pretty_print=True).decode('utf-8')
 | 
			
		||||
 | 
			
		||||
    return html_block
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Extract/find element
 | 
			
		||||
def extract_element(find='title', html_content=''):
 | 
			
		||||
 | 
			
		||||
@@ -270,15 +260,8 @@ def strip_ignore_text(content, wordlist, mode="content"):
 | 
			
		||||
 | 
			
		||||
    return "\n".encode('utf8').join(output)
 | 
			
		||||
 | 
			
		||||
def cdata_in_document_to_text(html_content: str, render_anchor_tag_content=False) -> str:
 | 
			
		||||
    pattern = '<!\[CDATA\[(\s*(?:.(?<!\]\]>)\s*)*)\]\]>'
 | 
			
		||||
    def repl(m):
 | 
			
		||||
        text = m.group(1)
 | 
			
		||||
        return xml_escape(html_to_text(html_content=text))
 | 
			
		||||
 | 
			
		||||
    return re.sub(pattern, repl, html_content)
 | 
			
		||||
 | 
			
		||||
def html_to_text(html_content: str, render_anchor_tag_content=False, is_rss=False) -> str:
 | 
			
		||||
def html_to_text(html_content: str, render_anchor_tag_content=False) -> str:
 | 
			
		||||
    """Converts html string to a string with just the text. If ignoring
 | 
			
		||||
    rendering anchor tag content is enable, anchor tag content are also
 | 
			
		||||
    included in the text
 | 
			
		||||
@@ -294,22 +277,17 @@ def html_to_text(html_content: str, render_anchor_tag_content=False, is_rss=Fals
 | 
			
		||||
    #  if anchor tag content flag is set to True define a config for
 | 
			
		||||
    #  extracting this content
 | 
			
		||||
    if render_anchor_tag_content:
 | 
			
		||||
 | 
			
		||||
        parser_config = ParserConfig(
 | 
			
		||||
            annotation_rules={"a": ["hyperlink"]}, display_links=True
 | 
			
		||||
        )
 | 
			
		||||
    # otherwise set config to None/default
 | 
			
		||||
 | 
			
		||||
    # otherwise set config to None
 | 
			
		||||
    else:
 | 
			
		||||
        parser_config = None
 | 
			
		||||
 | 
			
		||||
    # RSS Mode - Inscriptis will treat `title` as something else.
 | 
			
		||||
    # Make it as a regular block display element (//item/title)
 | 
			
		||||
    if is_rss:
 | 
			
		||||
        css = CSS_PROFILES['strict'].copy()
 | 
			
		||||
        css['title'] = HtmlElement(display=Display.block)
 | 
			
		||||
        text_content = get_text(html_content, ParserConfig(css=css))
 | 
			
		||||
    else:
 | 
			
		||||
        # get text and annotations via inscriptis
 | 
			
		||||
        text_content = get_text(html_content, config=parser_config)
 | 
			
		||||
    # get text and annotations via inscriptis
 | 
			
		||||
    text_content = get_text(html_content, config=parser_config)
 | 
			
		||||
 | 
			
		||||
    return text_content
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -167,7 +167,9 @@ class model(dict):
 | 
			
		||||
    @property
 | 
			
		||||
    def label(self):
 | 
			
		||||
        # Used for sorting
 | 
			
		||||
        return self.get('title') if self.get('title') else self.get('url')
 | 
			
		||||
        if self['title']:
 | 
			
		||||
            return self['title']
 | 
			
		||||
        return self['url']
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def last_changed(self):
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ from changedetectionio import content_fetcher, html_tools
 | 
			
		||||
from changedetectionio.blueprint.price_data_follower import PRICE_DATA_TRACK_ACCEPT, PRICE_DATA_TRACK_REJECT
 | 
			
		||||
from copy import deepcopy
 | 
			
		||||
from . import difference_detection_processor
 | 
			
		||||
from ..html_tools import PERL_STYLE_REGEX, cdata_in_document_to_text
 | 
			
		||||
from ..html_tools import PERL_STYLE_REGEX
 | 
			
		||||
 | 
			
		||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
 | 
			
		||||
 | 
			
		||||
@@ -153,22 +153,13 @@ class perform_site_check(difference_detection_processor):
 | 
			
		||||
 | 
			
		||||
        is_json = 'application/json' in fetcher.get_all_headers().get('content-type', '').lower()
 | 
			
		||||
        is_html = not is_json
 | 
			
		||||
        is_rss = False
 | 
			
		||||
 | 
			
		||||
        ctype_header = fetcher.get_all_headers().get('content-type', '').lower()
 | 
			
		||||
        # Go into RSS preprocess for converting CDATA/comment to usable text
 | 
			
		||||
        if any(substring in ctype_header for substring in ['application/xml', 'application/rss', 'text/xml']):
 | 
			
		||||
            if '<rss' in fetcher.content[:100].lower():
 | 
			
		||||
                fetcher.content = cdata_in_document_to_text(html_content=fetcher.content)
 | 
			
		||||
                is_rss = True
 | 
			
		||||
 | 
			
		||||
        # source: support, basically treat it as plaintext
 | 
			
		||||
        if is_source:
 | 
			
		||||
            is_html = False
 | 
			
		||||
            is_json = False
 | 
			
		||||
 | 
			
		||||
        inline_pdf = fetcher.get_all_headers().get('content-disposition', '') and '%PDF-1' in fetcher.content[:10]
 | 
			
		||||
        if watch.is_pdf or 'application/pdf' in fetcher.get_all_headers().get('content-type', '').lower() or inline_pdf:
 | 
			
		||||
        if watch.is_pdf or 'application/pdf' in fetcher.get_all_headers().get('content-type', '').lower():
 | 
			
		||||
            from shutil import which
 | 
			
		||||
            tool = os.getenv("PDF_TO_HTML_TOOL", "pdftohtml")
 | 
			
		||||
            if not which(tool):
 | 
			
		||||
@@ -251,8 +242,7 @@ class perform_site_check(difference_detection_processor):
 | 
			
		||||
                        if filter_rule[0] == '/' or filter_rule.startswith('xpath:'):
 | 
			
		||||
                            html_content += html_tools.xpath_filter(xpath_filter=filter_rule.replace('xpath:', ''),
 | 
			
		||||
                                                                    html_content=fetcher.content,
 | 
			
		||||
                                                                    append_pretty_line_formatting=not is_source,
 | 
			
		||||
                                                                    is_rss=is_rss)
 | 
			
		||||
                                                                    append_pretty_line_formatting=not is_source)
 | 
			
		||||
                        else:
 | 
			
		||||
                            # CSS Filter, extract the HTML that matches and feed that into the existing inscriptis::get_text
 | 
			
		||||
                            html_content += html_tools.include_filters(include_filters=filter_rule,
 | 
			
		||||
@@ -272,9 +262,8 @@ class perform_site_check(difference_detection_processor):
 | 
			
		||||
                    do_anchor = self.datastore.data["settings"]["application"].get("render_anchor_tag_content", False)
 | 
			
		||||
                    stripped_text_from_html = \
 | 
			
		||||
                        html_tools.html_to_text(
 | 
			
		||||
                            html_content=html_content,
 | 
			
		||||
                            render_anchor_tag_content=do_anchor,
 | 
			
		||||
                            is_rss=is_rss
 | 
			
		||||
                            html_content,
 | 
			
		||||
                            render_anchor_tag_content=do_anchor
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
        # Re #340 - return the content before the 'ignore text' was applied
 | 
			
		||||
 
 | 
			
		||||
@@ -1,120 +1,110 @@
 | 
			
		||||
$(document).ready(function () {
 | 
			
		||||
    var a = document.getElementById("a");
 | 
			
		||||
    var b = document.getElementById("b");
 | 
			
		||||
    var result = document.getElementById("result");
 | 
			
		||||
    var inputs = document.getElementsByClassName("change");
 | 
			
		||||
    inputs.current = 0;
 | 
			
		||||
var a = document.getElementById("a");
 | 
			
		||||
var b = document.getElementById("b");
 | 
			
		||||
var result = document.getElementById("result");
 | 
			
		||||
 | 
			
		||||
    $('#jump-next-diff').click(function () {
 | 
			
		||||
function changed() {
 | 
			
		||||
  // https://github.com/kpdecker/jsdiff/issues/389
 | 
			
		||||
  // I would love to use `{ignoreWhitespace: true}` here but it breaks the formatting
 | 
			
		||||
  options = {
 | 
			
		||||
    ignoreWhitespace: document.getElementById("ignoreWhitespace").checked,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
        var element = inputs[inputs.current];
 | 
			
		||||
        var headerOffset = 80;
 | 
			
		||||
        var elementPosition = element.getBoundingClientRect().top;
 | 
			
		||||
        var offsetPosition = elementPosition - headerOffset + window.scrollY;
 | 
			
		||||
 | 
			
		||||
        window.scrollTo({
 | 
			
		||||
            top: offsetPosition,
 | 
			
		||||
            behavior: "smooth",
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        inputs.current++;
 | 
			
		||||
        if (inputs.current >= inputs.length) {
 | 
			
		||||
            inputs.current = 0;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function changed() {
 | 
			
		||||
        // https://github.com/kpdecker/jsdiff/issues/389
 | 
			
		||||
        // I would love to use `{ignoreWhitespace: true}` here but it breaks the formatting
 | 
			
		||||
        options = {
 | 
			
		||||
            ignoreWhitespace: document.getElementById("ignoreWhitespace").checked,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        var diff = Diff[window.diffType](a.textContent, b.textContent, options);
 | 
			
		||||
        var fragment = document.createDocumentFragment();
 | 
			
		||||
        for (var i = 0; i < diff.length; i++) {
 | 
			
		||||
            if (diff[i].added && diff[i + 1] && diff[i + 1].removed) {
 | 
			
		||||
                var swap = diff[i];
 | 
			
		||||
                diff[i] = diff[i + 1];
 | 
			
		||||
                diff[i + 1] = swap;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var node;
 | 
			
		||||
            if (diff[i].removed) {
 | 
			
		||||
                node = document.createElement("del");
 | 
			
		||||
                node.classList.add("change");
 | 
			
		||||
                const wrapper = node.appendChild(document.createElement("span"));
 | 
			
		||||
                wrapper.appendChild(document.createTextNode(diff[i].value));
 | 
			
		||||
            } else if (diff[i].added) {
 | 
			
		||||
                node = document.createElement("ins");
 | 
			
		||||
                node.classList.add("change");
 | 
			
		||||
                const wrapper = node.appendChild(document.createElement("span"));
 | 
			
		||||
                wrapper.appendChild(document.createTextNode(diff[i].value));
 | 
			
		||||
            } else {
 | 
			
		||||
                node = document.createTextNode(diff[i].value);
 | 
			
		||||
            }
 | 
			
		||||
            fragment.appendChild(node);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        result.textContent = "";
 | 
			
		||||
        result.appendChild(fragment);
 | 
			
		||||
 | 
			
		||||
        // Jump at start
 | 
			
		||||
        inputs.current = 0;
 | 
			
		||||
 | 
			
		||||
        // For nice mouse-over hover/title information
 | 
			
		||||
        const removed_current_option = $('#diff-version option:selected')
 | 
			
		||||
        if (removed_current_option) {
 | 
			
		||||
            $('del').each(function () {
 | 
			
		||||
                $(this).prop('title', 'Removed '+removed_current_option[0].label);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        const inserted_current_option = $('#current-version option:selected')
 | 
			
		||||
        if (removed_current_option) {
 | 
			
		||||
            $('ins').each(function () {
 | 
			
		||||
                $(this).prop('title', 'Inserted '+inserted_current_option[0].label);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $('#jump-next-diff').click();
 | 
			
		||||
  var diff = Diff[window.diffType](a.textContent, b.textContent, options);
 | 
			
		||||
  var fragment = document.createDocumentFragment();
 | 
			
		||||
  for (var i = 0; i < diff.length; i++) {
 | 
			
		||||
    if (diff[i].added && diff[i + 1] && diff[i + 1].removed) {
 | 
			
		||||
      var swap = diff[i];
 | 
			
		||||
      diff[i] = diff[i + 1];
 | 
			
		||||
      diff[i + 1] = swap;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $('.needs-localtime').each(function () {
 | 
			
		||||
        for (var option of this.options) {
 | 
			
		||||
            var dateObject = new Date(option.value * 1000);
 | 
			
		||||
            option.label = dateObject.toLocaleString(undefined, {dateStyle: "full", timeStyle: "medium"});
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
    onDiffTypeChange(
 | 
			
		||||
        document.querySelector('#settings [name="diff_type"]:checked'),
 | 
			
		||||
    );
 | 
			
		||||
    changed();
 | 
			
		||||
 | 
			
		||||
    a.onpaste = a.onchange = b.onpaste = b.onchange = changed;
 | 
			
		||||
 | 
			
		||||
    if ("oninput" in a) {
 | 
			
		||||
        a.oninput = b.oninput = changed;
 | 
			
		||||
    var node;
 | 
			
		||||
    if (diff[i].removed) {
 | 
			
		||||
      node = document.createElement("del");
 | 
			
		||||
      node.classList.add("change");
 | 
			
		||||
      const wrapper = node.appendChild(document.createElement("span"));
 | 
			
		||||
      wrapper.appendChild(document.createTextNode(diff[i].value));
 | 
			
		||||
    } else if (diff[i].added) {
 | 
			
		||||
      node = document.createElement("ins");
 | 
			
		||||
      node.classList.add("change");
 | 
			
		||||
      const wrapper = node.appendChild(document.createElement("span"));
 | 
			
		||||
      wrapper.appendChild(document.createTextNode(diff[i].value));
 | 
			
		||||
    } else {
 | 
			
		||||
        a.onkeyup = b.onkeyup = changed;
 | 
			
		||||
      node = document.createTextNode(diff[i].value);
 | 
			
		||||
    }
 | 
			
		||||
    fragment.appendChild(node);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    function onDiffTypeChange(radio) {
 | 
			
		||||
        window.diffType = radio.value;
 | 
			
		||||
        // Not necessary
 | 
			
		||||
        //	document.title = "Diff " + radio.value.slice(4);
 | 
			
		||||
  result.textContent = "";
 | 
			
		||||
  result.appendChild(fragment);
 | 
			
		||||
 | 
			
		||||
  // Jump at start
 | 
			
		||||
  inputs.current = 0;
 | 
			
		||||
  next_diff();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.onload = function () {
 | 
			
		||||
  /* Convert what is options from UTC time.time() to local browser time */
 | 
			
		||||
  var diffList = document.getElementById("diff-version");
 | 
			
		||||
  if (typeof diffList != "undefined" && diffList != null) {
 | 
			
		||||
    for (var option of diffList.options) {
 | 
			
		||||
      var dateObject = new Date(option.value * 1000);
 | 
			
		||||
      option.label = dateObject.toLocaleString();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    var radio = document.getElementsByName("diff_type");
 | 
			
		||||
    for (var i = 0; i < radio.length; i++) {
 | 
			
		||||
        radio[i].onchange = function (e) {
 | 
			
		||||
            onDiffTypeChange(e.target);
 | 
			
		||||
            changed();
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
  /* Set current version date as local time in the browser also */
 | 
			
		||||
  var current_v = document.getElementById("current-v-date");
 | 
			
		||||
  var dateObject = new Date(newest_version_timestamp * 1000);
 | 
			
		||||
  current_v.innerHTML = dateObject.toLocaleString();
 | 
			
		||||
  onDiffTypeChange(
 | 
			
		||||
    document.querySelector('#settings [name="diff_type"]:checked'),
 | 
			
		||||
  );
 | 
			
		||||
  changed();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
    document.getElementById("ignoreWhitespace").onchange = function (e) {
 | 
			
		||||
        changed();
 | 
			
		||||
    };
 | 
			
		||||
a.onpaste = a.onchange = b.onpaste = b.onchange = changed;
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
if ("oninput" in a) {
 | 
			
		||||
  a.oninput = b.oninput = changed;
 | 
			
		||||
} else {
 | 
			
		||||
  a.onkeyup = b.onkeyup = changed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDiffTypeChange(radio) {
 | 
			
		||||
  window.diffType = radio.value;
 | 
			
		||||
  // Not necessary
 | 
			
		||||
  //	document.title = "Diff " + radio.value.slice(4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var radio = document.getElementsByName("diff_type");
 | 
			
		||||
for (var i = 0; i < radio.length; i++) {
 | 
			
		||||
  radio[i].onchange = function (e) {
 | 
			
		||||
    onDiffTypeChange(e.target);
 | 
			
		||||
    changed();
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
document.getElementById("ignoreWhitespace").onchange = function (e) {
 | 
			
		||||
  changed();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
var inputs = document.getElementsByClassName("change");
 | 
			
		||||
inputs.current = 0;
 | 
			
		||||
 | 
			
		||||
function next_diff() {
 | 
			
		||||
  var element = inputs[inputs.current];
 | 
			
		||||
  var headerOffset = 80;
 | 
			
		||||
  var elementPosition = element.getBoundingClientRect().top;
 | 
			
		||||
  var offsetPosition = elementPosition - headerOffset + window.scrollY;
 | 
			
		||||
 | 
			
		||||
  window.scrollTo({
 | 
			
		||||
    top: offsetPosition,
 | 
			
		||||
    behavior: "smooth",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  inputs.current++;
 | 
			
		||||
  if (inputs.current >= inputs.length) {
 | 
			
		||||
    inputs.current = 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,14 +4,6 @@ $(function () {
 | 
			
		||||
        $(this).closest('.unviewed').removeClass('unviewed');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('td[data-timestamp]').each(function () {
 | 
			
		||||
        $(this).prop('title', new Intl.DateTimeFormat(undefined,
 | 
			
		||||
            {
 | 
			
		||||
                dateStyle: 'full',
 | 
			
		||||
                timeStyle: 'long'
 | 
			
		||||
            }).format($(this).data('timestamp') * 1000));
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    $("#checkbox-assign-tag").click(function (e) {
 | 
			
		||||
        $('#op_extradata').val(prompt("Enter a tag name"));
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -187,10 +187,6 @@ ins {
 | 
			
		||||
    padding: 0.5em; }
 | 
			
		||||
  #settings ins {
 | 
			
		||||
    padding: 0.5em; }
 | 
			
		||||
  #settings option:checked {
 | 
			
		||||
    font-weight: bold; }
 | 
			
		||||
  #settings [type=radio], #settings [type=checkbox] {
 | 
			
		||||
    vertical-align: middle; }
 | 
			
		||||
 | 
			
		||||
.source {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
 
 | 
			
		||||
@@ -77,13 +77,6 @@ ins {
 | 
			
		||||
  ins {
 | 
			
		||||
    padding: 0.5em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  option:checked {
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
  }
 | 
			
		||||
  [type=radio],[type=checkbox] {
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.source {
 | 
			
		||||
 
 | 
			
		||||
@@ -471,11 +471,7 @@ footer {
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
 | 
			
		||||
  &#left-sticky {
 | 
			
		||||
    left: 0;
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    border-top-right-radius: 5px;
 | 
			
		||||
    border-bottom-right-radius: 5px;
 | 
			
		||||
    box-shadow: 1px 1px 4px var(--color-shadow-jump);
 | 
			
		||||
    left: 0px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &#right-sticky {
 | 
			
		||||
 
 | 
			
		||||
@@ -667,11 +667,7 @@ footer {
 | 
			
		||||
  background: var(--color-background);
 | 
			
		||||
  padding: 10px; }
 | 
			
		||||
  .sticky-tab#left-sticky {
 | 
			
		||||
    left: 0;
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    border-top-right-radius: 5px;
 | 
			
		||||
    border-bottom-right-radius: 5px;
 | 
			
		||||
    box-shadow: 1px 1px 4px var(--color-shadow-jump); }
 | 
			
		||||
    left: 0px; }
 | 
			
		||||
  .sticky-tab#right-sticky {
 | 
			
		||||
    right: 0px; }
 | 
			
		||||
  .sticky-tab#hosted-sticky {
 | 
			
		||||
 
 | 
			
		||||
@@ -96,14 +96,6 @@ class ChangeDetectionStore:
 | 
			
		||||
                self.add_watch(url='https://changedetection.io/CHANGELOG.txt',
 | 
			
		||||
                               tag='changedetection.io',
 | 
			
		||||
                               extras={'fetch_backend': 'html_requests'})
 | 
			
		||||
 | 
			
		||||
            updates_available = self.get_updates_available()
 | 
			
		||||
            self.__data['settings']['application']['schema_version'] = updates_available.pop()
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            # Bump the update version by running updates
 | 
			
		||||
            self.run_updates()
 | 
			
		||||
 | 
			
		||||
        self.__data['version_tag'] = version_tag
 | 
			
		||||
 | 
			
		||||
        # Just to test that proxies.json if it exists, doesnt throw a parsing error on startup
 | 
			
		||||
@@ -133,6 +125,9 @@ class ChangeDetectionStore:
 | 
			
		||||
            secret = secrets.token_hex(16)
 | 
			
		||||
            self.__data['settings']['application']['api_access_token'] = secret
 | 
			
		||||
 | 
			
		||||
        # Bump the update version by running updates
 | 
			
		||||
        self.run_updates()
 | 
			
		||||
 | 
			
		||||
        self.needs_write = True
 | 
			
		||||
 | 
			
		||||
        # Finally start the thread that will manage periodic data saves to JSON
 | 
			
		||||
@@ -630,8 +625,14 @@ class ChangeDetectionStore:
 | 
			
		||||
    def tag_exists_by_name(self, tag_name):
 | 
			
		||||
        return any(v.get('title', '').lower() == tag_name.lower() for k, v in self.__data['settings']['application']['tags'].items())
 | 
			
		||||
 | 
			
		||||
    def get_updates_available(self):
 | 
			
		||||
    # Run all updates
 | 
			
		||||
    # IMPORTANT - Each update could be run even when they have a new install and the schema is correct
 | 
			
		||||
    #             So therefor - each `update_n` should be very careful about checking if it needs to actually run
 | 
			
		||||
    #             Probably we should bump the current update schema version with each tag release version?
 | 
			
		||||
    def run_updates(self):
 | 
			
		||||
        import inspect
 | 
			
		||||
        import shutil
 | 
			
		||||
 | 
			
		||||
        updates_available = []
 | 
			
		||||
        for i, o in inspect.getmembers(self, predicate=inspect.ismethod):
 | 
			
		||||
            m = re.search(r'update_(\d+)$', i)
 | 
			
		||||
@@ -639,15 +640,6 @@ class ChangeDetectionStore:
 | 
			
		||||
                updates_available.append(int(m.group(1)))
 | 
			
		||||
        updates_available.sort()
 | 
			
		||||
 | 
			
		||||
        return updates_available
 | 
			
		||||
 | 
			
		||||
    # Run all updates
 | 
			
		||||
    # IMPORTANT - Each update could be run even when they have a new install and the schema is correct
 | 
			
		||||
    #             So therefor - each `update_n` should be very careful about checking if it needs to actually run
 | 
			
		||||
    #             Probably we should bump the current update schema version with each tag release version?
 | 
			
		||||
    def run_updates(self):
 | 
			
		||||
        import shutil
 | 
			
		||||
        updates_available = self.get_updates_available()
 | 
			
		||||
        for update_n in updates_available:
 | 
			
		||||
            if update_n > self.__data['settings']['application']['schema_version']:
 | 
			
		||||
                print ("Applying update_{}".format((update_n)))
 | 
			
		||||
 
 | 
			
		||||
@@ -121,8 +121,7 @@
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {% if left_sticky %}
 | 
			
		||||
      <div class="sticky-tab" id="left-sticky">
 | 
			
		||||
        <a href="{{url_for('preview_page', uuid=uuid)}}">Show current snapshot</a><br>
 | 
			
		||||
          Visualise <strong>triggers</strong> and <strong>ignored text</strong>
 | 
			
		||||
        <a href="{{url_for('preview_page', uuid=uuid)}}">Show current snapshot</a>
 | 
			
		||||
      </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {% if right_sticky %}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,31 +13,10 @@
 | 
			
		||||
<script src="{{url_for('static_content', group='js', filename='diff-overview.js')}}" defer></script>
 | 
			
		||||
 | 
			
		||||
<div id="settings">
 | 
			
		||||
    <h1>Differences</h1>
 | 
			
		||||
    <form class="pure-form " action="" method="GET">
 | 
			
		||||
        <fieldset>
 | 
			
		||||
            {% if versions|length >= 1 %}
 | 
			
		||||
                <strong>Compare</strong>
 | 
			
		||||
                <del class="change"><span>from</span></del>
 | 
			
		||||
                <select id="diff-version" name="from_version" class="needs-localtime">
 | 
			
		||||
                    {% for version in versions|reverse %}
 | 
			
		||||
                        <option value="{{ version }}" {% if version== from_version %} selected="" {% endif %}>
 | 
			
		||||
                            {{ version }}
 | 
			
		||||
                        </option>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </select>
 | 
			
		||||
                <ins class="change"><span>to</span></ins>
 | 
			
		||||
                <select id="current-version" name="to_version" class="needs-localtime">
 | 
			
		||||
                    {% for version in versions|reverse %}
 | 
			
		||||
                        <option value="{{ version }}" {% if version== to_version %} selected="" {% endif %}>
 | 
			
		||||
                            {{ version }}
 | 
			
		||||
                        </option>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </select>
 | 
			
		||||
                <button type="submit" class="pure-button pure-button-primary">Go</button>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </fieldset>
 | 
			
		||||
        <fieldset>
 | 
			
		||||
            <strong>Style</strong>
 | 
			
		||||
 | 
			
		||||
            <label for="diffWords" class="pure-checkbox">
 | 
			
		||||
                <input type="radio" name="diff_type" id="diffWords" value="diffWords"> Words</label>
 | 
			
		||||
            <label for="diffLines" class="pure-checkbox">
 | 
			
		||||
@@ -47,20 +26,32 @@
 | 
			
		||||
                <input type="radio" name="diff_type" id="diffChars" value="diffChars"> Chars</label>
 | 
			
		||||
            <!-- @todo - when mimetype is JSON, select this by default? -->
 | 
			
		||||
            <label for="diffJson" class="pure-checkbox">
 | 
			
		||||
                <input type="radio" name="diff_type" id="diffJson" value="diffJson"> JSON</label>
 | 
			
		||||
                <input type="radio" name="diff_type" id="diffJson" value="diffJson" > JSON</label>
 | 
			
		||||
 | 
			
		||||
            <span>
 | 
			
		||||
        <!-- https://github.com/kpdecker/jsdiff/issues/389 ? -->
 | 
			
		||||
        <label for="ignoreWhitespace" class="pure-checkbox" id="label-diff-ignorewhitespace">
 | 
			
		||||
            <input type="checkbox" id="ignoreWhitespace" name="ignoreWhitespace"> Ignore Whitespace</label>
 | 
			
		||||
    </span>
 | 
			
		||||
            {% if versions|length >= 1 %}
 | 
			
		||||
            <label for="diff-version">Compare newest (<span id="current-v-date"></span>) with</label>
 | 
			
		||||
            <select id="diff-version" name="previous_version">
 | 
			
		||||
                {% for version in versions|reverse %}
 | 
			
		||||
                <option value="{{version}}" {% if version== current_previous_version %} selected="" {% endif %}>
 | 
			
		||||
                    {{version}}
 | 
			
		||||
                </option>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </select>
 | 
			
		||||
            <button type="submit" class="pure-button pure-button-primary">Go</button>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </fieldset>
 | 
			
		||||
    </form>
 | 
			
		||||
 | 
			
		||||
    <del>Removed text</del>
 | 
			
		||||
    <ins>Inserted Text</ins>
 | 
			
		||||
    <span>
 | 
			
		||||
        <!-- https://github.com/kpdecker/jsdiff/issues/389 ? -->
 | 
			
		||||
        <label for="ignoreWhitespace" class="pure-checkbox" id="label-diff-ignorewhitespace">
 | 
			
		||||
            <input type="checkbox" id="ignoreWhitespace" name="ignoreWhitespace" > Ignore Whitespace</label>
 | 
			
		||||
    </span>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div id="diff-jump">
 | 
			
		||||
    <a id="jump-next-diff">Jump</a>
 | 
			
		||||
    <a onclick="next_diff();">Jump</a>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
 | 
			
		||||
@@ -88,6 +79,8 @@
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
     <div class="tab-pane-inner" id="text">
 | 
			
		||||
         <div class="tip">Pro-tip: Use <strong>show current snapshot</strong> tab to visualise what will be ignored, highlight text to add to ignore filters</div>
 | 
			
		||||
 | 
			
		||||
         {% if password_enabled_and_share_is_off %}
 | 
			
		||||
           <div class="tip">Pro-tip: You can enable <strong>"share access when password is enabled"</strong> from settings</div>
 | 
			
		||||
         {% endif %}
 | 
			
		||||
@@ -98,8 +91,8 @@
 | 
			
		||||
             <tbody>
 | 
			
		||||
             <tr>
 | 
			
		||||
                 <!-- just proof of concept copied straight from github.com/kpdecker/jsdiff -->
 | 
			
		||||
                 <td id="a" style="display: none;">{{from_version_file_contents}}</td>
 | 
			
		||||
                 <td id="b" style="display: none;">{{to_version_file_contents}}</td>
 | 
			
		||||
                 <td id="a" style="display: none;">{{previous}}</td>
 | 
			
		||||
                 <td id="b" style="display: none;">{{newest}}</td>
 | 
			
		||||
                 <td id="diff-col">
 | 
			
		||||
                     <span id="result" class="highlightable-filter"></span>
 | 
			
		||||
                 </td>
 | 
			
		||||
 
 | 
			
		||||
@@ -109,7 +109,7 @@
 | 
			
		||||
                        <span class="pure-form-message-inline">
 | 
			
		||||
                            <p>Use the <strong>Basic</strong> method (default) where your watched site doesn't need Javascript to render.</p>
 | 
			
		||||
                            <p>The <strong>Chrome/Javascript</strong> method requires a network connection to a running WebDriver+Chrome server, set by the ENV var 'WEBDRIVER_URL'. </p>
 | 
			
		||||
                            Tip: <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support">Connect using Bright Data and Oxylabs Proxies, find out more here.</a>
 | 
			
		||||
                            Tip: <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support">Connect using BrightData Proxies, find out more here.</a>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                {% if form.proxy %}
 | 
			
		||||
 
 | 
			
		||||
@@ -109,7 +109,7 @@
 | 
			
		||||
                        <p>The <strong>Chrome/Javascript</strong> method requires a network connection to a running WebDriver+Chrome server, set by the ENV var 'WEBDRIVER_URL'. </p>
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <br>
 | 
			
		||||
                    Tip: <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support">Connect using Bright Data and Oxylabs Proxies, find out more here.</a>
 | 
			
		||||
                    Tip: <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support">Connect using BrightData Proxies, find out more here.</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <fieldset class="pure-group" id="webdriver-override-options">
 | 
			
		||||
                    <div class="pure-form-message-inline">
 | 
			
		||||
@@ -229,7 +229,7 @@ nav
 | 
			
		||||
 | 
			
		||||
                <div class="pure-control-group">
 | 
			
		||||
                {{ render_field(form.requests.form.extra_proxies) }}
 | 
			
		||||
                <span class="pure-form-message-inline">"Name" will be used for selecting the proxy in the Watch Edit settings</span><br>
 | 
			
		||||
                <span class="pure-form-message-inline">"Name" will be used for selecting the proxy in the Watch Edit settings</span>
 | 
			
		||||
                <span class="pure-form-message-inline">SOCKS5 proxies with authentication are only supported with 'plain requests' fetcher, for other fetchers you should whitelist the IP access instead</span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -154,8 +154,8 @@
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="last-checked" data-timestamp="{{ watch.last_checked }}">{{watch|format_last_checked_time|safe}}</td>
 | 
			
		||||
                <td class="last-changed" data-timestamp="{{ watch.last_changed }}">{% if watch.history_n >=2 and watch.last_changed >0 %}
 | 
			
		||||
                <td class="last-checked">{{watch|format_last_checked_time|safe}}</td>
 | 
			
		||||
                <td class="last-changed">{% if watch.history_n >=2 and watch.last_changed >0 %}
 | 
			
		||||
                    {{watch.last_changed|format_timestamp_timeago}}
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    Not yet
 | 
			
		||||
 
 | 
			
		||||
@@ -89,7 +89,7 @@ def test_check_basic_change_detection_functionality(client, live_server):
 | 
			
		||||
 | 
			
		||||
    # Following the 'diff' link, it should no longer display as 'unviewed' even after we recheck it a few times
 | 
			
		||||
    res = client.get(url_for("diff_history_page", uuid="first"))
 | 
			
		||||
    assert b'selected=""' in res.data, "Confirm diff history page loaded"
 | 
			
		||||
    assert b'Compare newest' in res.data
 | 
			
		||||
 | 
			
		||||
    # Check the [preview] pulls the right one
 | 
			
		||||
    res = client.get(
 | 
			
		||||
 
 | 
			
		||||
@@ -2,61 +2,12 @@
 | 
			
		||||
 | 
			
		||||
import time
 | 
			
		||||
from flask import url_for
 | 
			
		||||
from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks, extract_rss_token_from_UI, \
 | 
			
		||||
    extract_UUID_from_client
 | 
			
		||||
from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks, extract_rss_token_from_UI
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def set_original_cdata_xml():
 | 
			
		||||
    test_return_data = """<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
 | 
			
		||||
    <channel>
 | 
			
		||||
    <title>Gizi</title>
 | 
			
		||||
    <link>https://test.com</link>
 | 
			
		||||
    <atom:link href="https://testsite.com" rel="self" type="application/rss+xml"/>
 | 
			
		||||
    <description>
 | 
			
		||||
    <![CDATA[ The Future Could Be Here ]]>
 | 
			
		||||
    </description>
 | 
			
		||||
    <language>en</language>
 | 
			
		||||
    <item>
 | 
			
		||||
    <title>
 | 
			
		||||
    <![CDATA[ <img src="https://testsite.com/hacked.jpg"> Hackers can access your computer ]]>
 | 
			
		||||
    </title>
 | 
			
		||||
    <link>https://testsite.com/news/12341234234</link>
 | 
			
		||||
    <description>
 | 
			
		||||
    <![CDATA[ <img class="type:primaryImage" src="https://testsite.com/701c981da04869e.jpg"/><p>The days of Terminator and The Matrix could be closer. But be positive.</p><p><a href="https://testsite.com">Read more link...</a></p> ]]>
 | 
			
		||||
    </description>
 | 
			
		||||
    <category>cybernetics</category>
 | 
			
		||||
    <category>rand corporation</category>
 | 
			
		||||
    <pubDate>Tue, 17 Oct 2023 15:10:00 GMT</pubDate>
 | 
			
		||||
    <guid isPermaLink="false">1850933241</guid>
 | 
			
		||||
    <dc:creator>
 | 
			
		||||
    <![CDATA[ Mr Hacker News ]]>
 | 
			
		||||
    </dc:creator>
 | 
			
		||||
    <media:thumbnail url="https://testsite.com/thumbnail-c224e10d81488e818701c981da04869e.jpg"/>
 | 
			
		||||
    </item>
 | 
			
		||||
 | 
			
		||||
    <item>
 | 
			
		||||
        <title>    Some other title    </title>
 | 
			
		||||
        <link>https://testsite.com/news/12341234236</link>
 | 
			
		||||
        <description>
 | 
			
		||||
        Some other description
 | 
			
		||||
        </description>
 | 
			
		||||
    </item>    
 | 
			
		||||
    </channel>
 | 
			
		||||
    </rss>
 | 
			
		||||
            """
 | 
			
		||||
 | 
			
		||||
    with open("test-datastore/endpoint-content.txt", "w") as f:
 | 
			
		||||
        f.write(test_return_data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_setup(client, live_server):
 | 
			
		||||
    live_server_setup(live_server)
 | 
			
		||||
 | 
			
		||||
def test_rss_and_token(client, live_server):
 | 
			
		||||
    #    live_server_setup(live_server)
 | 
			
		||||
 | 
			
		||||
    set_original_response()
 | 
			
		||||
    rss_token = extract_rss_token_from_UI(client)
 | 
			
		||||
    live_server_setup(live_server)
 | 
			
		||||
 | 
			
		||||
    # Add our URL to the import page
 | 
			
		||||
    res = client.post(
 | 
			
		||||
@@ -66,11 +17,11 @@ def test_rss_and_token(client, live_server):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    assert b"1 Imported" in res.data
 | 
			
		||||
    rss_token = extract_rss_token_from_UI(client)
 | 
			
		||||
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
    set_modified_response()
 | 
			
		||||
    time.sleep(2)
 | 
			
		||||
    client.get(url_for("form_watch_checknow"), follow_redirects=True)
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
    time.sleep(2)
 | 
			
		||||
 | 
			
		||||
    # Add our URL to the import page
 | 
			
		||||
    res = client.get(
 | 
			
		||||
@@ -86,77 +37,3 @@ def test_rss_and_token(client, live_server):
 | 
			
		||||
    )
 | 
			
		||||
    assert b"Access denied, bad token" not in res.data
 | 
			
		||||
    assert b"Random content" in res.data
 | 
			
		||||
 | 
			
		||||
    res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
 | 
			
		||||
 | 
			
		||||
def test_basic_cdata_rss_markup(client, live_server):
 | 
			
		||||
    #live_server_setup(live_server)
 | 
			
		||||
 | 
			
		||||
    set_original_cdata_xml()
 | 
			
		||||
 | 
			
		||||
    test_url = url_for('test_endpoint', content_type="application/xml", _external=True)
 | 
			
		||||
 | 
			
		||||
    # Add our URL to the import page
 | 
			
		||||
    res = client.post(
 | 
			
		||||
        url_for("import_page"),
 | 
			
		||||
        data={"urls": test_url},
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    assert b"1 Imported" in res.data
 | 
			
		||||
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("preview_page", uuid="first"),
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
    assert b'CDATA' not in res.data
 | 
			
		||||
    assert b'<![' not in res.data
 | 
			
		||||
    assert b'Hackers can access your computer' in res.data
 | 
			
		||||
    assert b'The days of Terminator' in res.data
 | 
			
		||||
    res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
 | 
			
		||||
 | 
			
		||||
def test_rss_xpath_filtering(client, live_server):
 | 
			
		||||
#    live_server_setup(live_server)
 | 
			
		||||
 | 
			
		||||
    set_original_cdata_xml()
 | 
			
		||||
 | 
			
		||||
    test_url = url_for('test_endpoint', content_type="application/xml", _external=True)
 | 
			
		||||
 | 
			
		||||
    res = client.post(
 | 
			
		||||
        url_for("form_quick_watch_add"),
 | 
			
		||||
        data={"url": test_url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
    assert b"Watch added in Paused state, saving will unpause" in res.data
 | 
			
		||||
 | 
			
		||||
    uuid = extract_UUID_from_client(client)
 | 
			
		||||
    res = client.post(
 | 
			
		||||
        url_for("edit_page", uuid=uuid, unpause_on_save=1),
 | 
			
		||||
        data={
 | 
			
		||||
                "include_filters": "//item/title",
 | 
			
		||||
                "fetch_backend": "html_requests",
 | 
			
		||||
                "headers": "",
 | 
			
		||||
                "proxy": "no-proxy",
 | 
			
		||||
                "tags": "",
 | 
			
		||||
                "url": test_url,
 | 
			
		||||
              },
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
    assert b"unpaused" in res.data
 | 
			
		||||
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("preview_page", uuid="first"),
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
    assert b'CDATA' not in res.data
 | 
			
		||||
    assert b'<![' not in res.data
 | 
			
		||||
    assert b'Hackers can access your computer' in res.data # Should ONLY be selected by the xpath
 | 
			
		||||
    assert b'Some other title' in res.data  # Should ONLY be selected by the xpath
 | 
			
		||||
    assert b'The days of Terminator' not in res.data # Should NOT be selected by the xpath
 | 
			
		||||
    assert b'Some other description' not in res.data  # Should NOT be selected by the xpath
 | 
			
		||||
 | 
			
		||||
    res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
import time
 | 
			
		||||
from flask import url_for
 | 
			
		||||
from .util import live_server_setup, wait_for_all_checks
 | 
			
		||||
from . util import live_server_setup
 | 
			
		||||
 | 
			
		||||
from ..html_tools import *
 | 
			
		||||
 | 
			
		||||
@@ -86,14 +86,14 @@ def test_check_xpath_filter_utf8(client, live_server):
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
    assert b"1 Imported" in res.data
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
    time.sleep(1)
 | 
			
		||||
    res = client.post(
 | 
			
		||||
        url_for("edit_page", uuid="first"),
 | 
			
		||||
        data={"include_filters": filter, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"},
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
    assert b"Updated watch." in res.data
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
    time.sleep(3)
 | 
			
		||||
    res = client.get(url_for("index"))
 | 
			
		||||
    assert b'Unicode strings with encoding declaration are not supported.' not in res.data
 | 
			
		||||
    res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
 | 
			
		||||
@@ -140,14 +140,14 @@ def test_check_xpath_text_function_utf8(client, live_server):
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
    assert b"1 Imported" in res.data
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
    time.sleep(1)
 | 
			
		||||
    res = client.post(
 | 
			
		||||
        url_for("edit_page", uuid="first"),
 | 
			
		||||
        data={"include_filters": filter, "url": test_url, "tags": "", "headers": "", 'fetch_backend': "html_requests"},
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
    assert b"Updated watch." in res.data
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
    time.sleep(3)
 | 
			
		||||
    res = client.get(url_for("index"))
 | 
			
		||||
    assert b'Unicode strings with encoding declaration are not supported.' not in res.data
 | 
			
		||||
 | 
			
		||||
@@ -164,6 +164,7 @@ def test_check_xpath_text_function_utf8(client, live_server):
 | 
			
		||||
    assert b'Deleted' in res.data
 | 
			
		||||
 | 
			
		||||
def test_check_markup_xpath_filter_restriction(client, live_server):
 | 
			
		||||
    sleep_time_for_fetch_thread = 3
 | 
			
		||||
 | 
			
		||||
    xpath_filter = "//*[contains(@class, 'sametext')]"
 | 
			
		||||
 | 
			
		||||
@@ -182,7 +183,7 @@ def test_check_markup_xpath_filter_restriction(client, live_server):
 | 
			
		||||
    assert b"1 Imported" in res.data
 | 
			
		||||
 | 
			
		||||
    # Give the thread time to pick it up
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
    time.sleep(sleep_time_for_fetch_thread)
 | 
			
		||||
 | 
			
		||||
    # Goto the edit page, add our ignore text
 | 
			
		||||
    # Add our URL to the import page
 | 
			
		||||
@@ -194,7 +195,7 @@ def test_check_markup_xpath_filter_restriction(client, live_server):
 | 
			
		||||
    assert b"Updated watch." in res.data
 | 
			
		||||
 | 
			
		||||
    # Give the thread time to pick it up
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
    time.sleep(sleep_time_for_fetch_thread)
 | 
			
		||||
 | 
			
		||||
    # view it/reset state back to viewed
 | 
			
		||||
    client.get(url_for("diff_history_page", uuid="first"), follow_redirects=True)
 | 
			
		||||
@@ -205,7 +206,7 @@ def test_check_markup_xpath_filter_restriction(client, live_server):
 | 
			
		||||
    # Trigger a check
 | 
			
		||||
    client.get(url_for("form_watch_checknow"), follow_redirects=True)
 | 
			
		||||
    # Give the thread time to pick it up
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
    time.sleep(sleep_time_for_fetch_thread)
 | 
			
		||||
 | 
			
		||||
    res = client.get(url_for("index"))
 | 
			
		||||
    assert b'unviewed' not in res.data
 | 
			
		||||
@@ -215,6 +216,9 @@ def test_check_markup_xpath_filter_restriction(client, live_server):
 | 
			
		||||
 | 
			
		||||
def test_xpath_validation(client, live_server):
 | 
			
		||||
 | 
			
		||||
    # Give the endpoint time to spin up
 | 
			
		||||
    time.sleep(1)
 | 
			
		||||
 | 
			
		||||
    # Add our URL to the import page
 | 
			
		||||
    test_url = url_for('test_endpoint', _external=True)
 | 
			
		||||
    res = client.post(
 | 
			
		||||
@@ -223,7 +227,7 @@ def test_xpath_validation(client, live_server):
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
    assert b"1 Imported" in res.data
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
    time.sleep(2)
 | 
			
		||||
 | 
			
		||||
    res = client.post(
 | 
			
		||||
        url_for("edit_page", uuid="first"),
 | 
			
		||||
@@ -240,8 +244,11 @@ def test_check_with_prefix_include_filters(client, live_server):
 | 
			
		||||
    res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
 | 
			
		||||
    assert b'Deleted' in res.data
 | 
			
		||||
 | 
			
		||||
    # Give the endpoint time to spin up
 | 
			
		||||
    time.sleep(1)
 | 
			
		||||
 | 
			
		||||
    set_original_response()
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
 | 
			
		||||
    # Add our URL to the import page
 | 
			
		||||
    test_url = url_for('test_endpoint', _external=True)
 | 
			
		||||
    res = client.post(
 | 
			
		||||
@@ -250,7 +257,7 @@ def test_check_with_prefix_include_filters(client, live_server):
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
    assert b"1 Imported" in res.data
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
    time.sleep(3)
 | 
			
		||||
 | 
			
		||||
    res = client.post(
 | 
			
		||||
        url_for("edit_page", uuid="first"),
 | 
			
		||||
@@ -259,7 +266,7 @@ def test_check_with_prefix_include_filters(client, live_server):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    assert b"Updated watch." in res.data
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
    time.sleep(3)
 | 
			
		||||
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("preview_page", uuid="first"),
 | 
			
		||||
@@ -270,46 +277,3 @@ def test_check_with_prefix_include_filters(client, live_server):
 | 
			
		||||
    assert b"Some text that will change" not in res.data #not in selector
 | 
			
		||||
 | 
			
		||||
    client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
 | 
			
		||||
def test_various_rules(client, live_server):
 | 
			
		||||
    # Just check these don't error
 | 
			
		||||
    #live_server_setup(live_server)
 | 
			
		||||
    with open("test-datastore/endpoint-content.txt", "w") as f:
 | 
			
		||||
        f.write("""<html>
 | 
			
		||||
       <body>
 | 
			
		||||
     Some initial text<br>
 | 
			
		||||
     <p>Which is across multiple lines</p>
 | 
			
		||||
     <br>
 | 
			
		||||
     So let's see what happens.  <br>
 | 
			
		||||
     <div class="sametext">Some text thats the same</div>
 | 
			
		||||
     <div class="changetext">Some text that will change</div>
 | 
			
		||||
     <a href=''>some linky </a>
 | 
			
		||||
     <a href=''>another some linky </a>
 | 
			
		||||
     <!-- related to https://github.com/dgtlmoon/changedetection.io/pull/1774 -->
 | 
			
		||||
     <input   type="email"   id="email" />
 | 
			
		||||
     </body>
 | 
			
		||||
     </html>
 | 
			
		||||
    """)
 | 
			
		||||
    test_url = url_for('test_endpoint', _external=True)
 | 
			
		||||
    res = client.post(
 | 
			
		||||
        url_for("import_page"),
 | 
			
		||||
        data={"urls": test_url},
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
    assert b"1 Imported" in res.data
 | 
			
		||||
    wait_for_all_checks(client)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    for r in ['//div', '//a', 'xpath://div', 'xpath://a']:
 | 
			
		||||
        res = client.post(
 | 
			
		||||
            url_for("edit_page", uuid="first"),
 | 
			
		||||
            data={"include_filters": r,
 | 
			
		||||
                  "url": test_url,
 | 
			
		||||
                  "tags": "",
 | 
			
		||||
                  "headers": "",
 | 
			
		||||
                  'fetch_backend': "html_requests"},
 | 
			
		||||
            follow_redirects=True
 | 
			
		||||
        )
 | 
			
		||||
        wait_for_all_checks(client)
 | 
			
		||||
        assert b"Updated watch." in res.data
 | 
			
		||||
        res = client.get(url_for("index"))
 | 
			
		||||
        assert b'fetch-error' not in res.data, f"Should not see errors after '{r} filter"
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,7 @@ services:
 | 
			
		||||
#        restart: unless-stopped
 | 
			
		||||
 | 
			
		||||
     # Used for fetching pages via Playwright+Chrome where you need Javascript support.
 | 
			
		||||
     # Note: Playwright/browserless not supported on ARM type devices (rPi etc)
 | 
			
		||||
 | 
			
		||||
#    playwright-chrome:
 | 
			
		||||
#        hostname: playwright-chrome
 | 
			
		||||
#        image: browserless/chrome
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ dnspython<2.3.0
 | 
			
		||||
# jq not available on Windows so must be installed manually
 | 
			
		||||
 | 
			
		||||
# Notification library
 | 
			
		||||
apprise~=1.6.0
 | 
			
		||||
apprise~=1.5.0
 | 
			
		||||
 | 
			
		||||
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
 | 
			
		||||
paho-mqtt
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user