diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index faec9f09..15297cc3 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,8 +1,21 @@
FROM rssbridge/rss-bridge:latest
-RUN apt-get update && \
- apt-get install --yes --no-install-recommends \
- git && \
- pecl install xdebug && \
- pear install PHP_CodeSniffer && \
- docker-php-ext-enable xdebug
\ No newline at end of file
+COPY --chmod=755 post-create-command.sh /usr/local/bin/post-create-command
+
+ADD https://raw.githubusercontent.com/docker-library/php/master/docker-php-ext-enable /usr/local/bin/docker-php-ext-enable
+RUN chmod u+x /usr/local/bin/docker-php-ext-enable
+
+ADD https://getcomposer.org/installer /usr/local/bin/composer-installer.php
+RUN chmod u+x /usr/local/bin/composer-installer.php
+RUN php /usr/local/bin/composer-installer.php --check && \
+ php /usr/local/bin/composer-installer.php --filename=composer --install-dir=/usr/local/bin
+
+RUN apt-get update && \
+ apt-get install -y \
+ git \
+ php-dev \
+ make \
+ unzip
+
+RUN pecl install xdebug && \
+ PHP_INI_DIR=/etc/php/8.2/fpm docker-php-ext-enable xdebug
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 6e625b8a..564da38a 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -6,9 +6,9 @@
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
- "php.validate.executablePath": "/usr/local/bin/php",
- "phpSniffer.executablesFolder": "/usr/local/bin/",
- "phpcs.executablePath": "/usr/local/bin/phpcs",
+ "php.validate.executablePath": "/usr/bin/php",
+ "phpSniffer.executablesFolder": "/root/.config/composer/vendor/bin",
+ "phpcs.executablePath": "/root/.config/composer/vendor/bin/phpcs",
"phpcs.lintOnType": false
},
@@ -22,6 +22,9 @@
]
}
},
+ "remoteEnv": {
+ "PATH": "${containerEnv:PATH}:/root/.config/composer/vendor/bin",
+ },
"forwardPorts": [3100, 9000, 9003],
- "postCreateCommand": "cp .devcontainer/nginx.conf /etc/nginx/conf.d/default.conf && cp .devcontainer/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini && mkdir .vscode && cp .devcontainer/launch.json .vscode && echo '*' > whitelist.txt && chmod a+x \"$(pwd)\" && rm -rf /var/www/html && ln -s \"$(pwd)\" /var/www/html && nginx && php-fpm -D"
+ "postCreateCommand": "/usr/local/bin/post-create-command"
}
\ No newline at end of file
diff --git a/.devcontainer/launch.json b/.devcontainer/launch.json
index e1b473b8..7fe617e9 100644
--- a/.devcontainer/launch.json
+++ b/.devcontainer/launch.json
@@ -9,7 +9,8 @@
"type": "php",
"request": "launch",
"port": 9003,
- "auto": true
+ "auto": true,
+ "log": true
},
{
"name": "Launch currently open script",
diff --git a/.devcontainer/post-create-command.sh b/.devcontainer/post-create-command.sh
new file mode 100755
index 00000000..aa3819eb
--- /dev/null
+++ b/.devcontainer/post-create-command.sh
@@ -0,0 +1,27 @@
+#/bin/sh
+
+cp .devcontainer/nginx.conf /etc/nginx/conf.d/default.conf
+cp .devcontainer/xdebug.ini /etc/php/8.2/fpm/conf.d/xdebug.ini
+
+# This should download some dev-dependencies, like phpunit and the PHP code sniffers
+composer global require "phpunit/phpunit:^9"
+composer global require "squizlabs/php_codesniffer:^3.6"
+composer global require "phpcompatibility/php-compatibility:^9.3"
+
+# We need to this manually for running the PHPCompatibility ruleset
+phpcs --config-set installed_paths /root/.config/composer/vendor/phpcompatibility/php-compatibility
+
+mkdir -p .vscode
+cp .devcontainer/launch.json .vscode
+
+echo '*' > whitelist.txt
+
+chmod a+x $(pwd)
+rm -rf /var/www/html
+ln -s $(pwd) /var/www/html
+
+# Solves possible issue of cache directory not being accessible
+chown www-data:www-data -R $(pwd)/cache
+
+nginx
+php-fpm8.2 -D
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
index 5981fb34..28053256 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -47,8 +47,6 @@ phpcs.xml export-ignore
phpcompatibility.xml export-ignore
tests/ export-ignore
cache/.gitkeep export-ignore
-bridges/DemoBridge.php export-ignore
-bridges/FeedExpanderExampleBridge.php export-ignore
## Composer
#
diff --git a/.github/.gitignore b/.github/.gitignore
index 6310b3dd..7ebb4030 100644
--- a/.github/.gitignore
+++ b/.github/.gitignore
@@ -4,3 +4,4 @@
# Generated files
comment*.md
comment*.txt
+*.html
diff --git a/.github/ISSUE_TEMPLATE/bridge-request.md b/.github/ISSUE_TEMPLATE/bridge-request.md
index 174dc095..088cc3d6 100644
--- a/.github/ISSUE_TEMPLATE/bridge-request.md
+++ b/.github/ISSUE_TEMPLATE/bridge-request.md
@@ -49,9 +49,9 @@ Please describe what you expect from the bridge. Whenever possible provide sampl
- _Default limit_: 5
- [ ] Load full articles
- _Cache articles_ (articles are stored in a local cache on first request): yes
- - _Cache timeout_ (max = 24 hours): 24 hours
+ - _Cache timeout_ : 24 hours
- [X] Balance requests (RSS-Bridge uses cached versions to reduce bandwith usage)
- - _Timeout_ (default = 5 minutes, max = 24 hours): 5 minutes
+ - _Timeout_ (default = 5 minutes): 5 minutes
diff --git a/.github/prtester.py b/.github/prtester.py
index 30a9f43b..c5c5be22 100644
--- a/.github/prtester.py
+++ b/.github/prtester.py
@@ -4,7 +4,9 @@ import re
from bs4 import BeautifulSoup
from datetime import datetime
from typing import Iterable
-import os.path
+import os
+import glob
+import urllib
# This script is specifically written to be used in automation for https://github.com/RSS-Bridge/rss-bridge
#
@@ -13,18 +15,33 @@ import os.path
# It also add a tag with the url of em's public instance, so viewing
# the HTML file locally will actually work as designed.
+ARTIFACT_FILE_EXTENSION = '.html'
+
class Instance:
name = ''
url = ''
def main(instances: Iterable[Instance], with_upload: bool, with_reduced_upload: bool, title: str, output_file: str):
start_date = datetime.now()
+
+ prid = os.getenv('PR')
+ artifact_base_url = f'https://rss-bridge.github.io/rss-bridge-tests/prs/{prid}'
+ artifact_directory = os.getcwd()
+ for file in glob.glob(f'*{ARTIFACT_FILE_EXTENSION}', root_dir=artifact_directory):
+ os.remove(file)
+
table_rows = []
for instance in instances:
page = requests.get(instance.url) # Use python requests to grab the rss-bridge main page
soup = BeautifulSoup(page.content, "html.parser") # use bs4 to turn the page into soup
bridge_cards = soup.select('.bridge-card') # get a soup-formatted list of all bridges on the rss-bridge page
- table_rows += testBridges(instance, bridge_cards, with_upload, with_reduced_upload) # run the main scraping code with the list of bridges
+ table_rows += testBridges(
+ instance=instance,
+ bridge_cards=bridge_cards,
+ with_upload=with_upload,
+ with_reduced_upload=with_reduced_upload,
+ artifact_directory=artifact_directory,
+ artifact_base_url=artifact_base_url) # run the main scraping code with the list of bridges
with open(file=output_file, mode='w+', encoding='utf-8') as file:
table_rows_value = '\n'.join(sorted(table_rows))
file.write(f'''
@@ -36,7 +53,7 @@ def main(instances: Iterable[Instance], with_upload: bool, with_reduced_upload:
*last change: {start_date.strftime("%A %Y-%m-%d %H:%M:%S")}*
'''.strip())
-def testBridges(instance: Instance, bridge_cards: Iterable, with_upload: bool, with_reduced_upload: bool) -> Iterable:
+def testBridges(instance: Instance, bridge_cards: Iterable, with_upload: bool, with_reduced_upload: bool, artifact_directory: str, artifact_base_url: str) -> Iterable:
instance_suffix = ''
if instance.name:
instance_suffix = f' ({instance.name})'
@@ -45,15 +62,14 @@ def testBridges(instance: Instance, bridge_cards: Iterable, with_upload: bool, w
bridgeid = bridge_card.get('id')
bridgeid = bridgeid.split('-')[1] # this extracts a readable bridge name from the bridge metadata
print(f'{bridgeid}{instance_suffix}')
- bridgestring = '/?action=display&bridge=' + bridgeid + '&format=Html'
bridge_name = bridgeid.replace('Bridge', '')
context_forms = bridge_card.find_all("form")
form_number = 1
for context_form in context_forms:
# a bridge can have multiple contexts, named 'forms' in html
- # this code will produce a fully working formstring that should create a working feed when called
+ # this code will produce a fully working url that should create a working feed when called
# this will create an example feed for every single context, to test them all
- formstring = ''
+ context_parameters = {}
error_messages = []
context_name = '*untitled*'
context_name_element = context_form.find_previous_sibling('h5')
@@ -62,27 +78,27 @@ def testBridges(instance: Instance, bridge_cards: Iterable, with_upload: bool, w
parameters = context_form.find_all("input")
lists = context_form.find_all("select")
# this for/if mess cycles through all available input parameters, checks if it required, then pulls
- # the default or examplevalue and then combines it all together into the formstring
+ # the default or examplevalue and then combines it all together into the url parameters
# if an example or default value is missing for a required attribute, it will throw an error
# any non-required fields are not tested!!!
for parameter in parameters:
- if parameter.get('type') == 'hidden' and parameter.get('name') == 'context':
- cleanvalue = parameter.get('value').replace(" ","+")
- formstring = formstring + '&' + parameter.get('name') + '=' + cleanvalue
- if parameter.get('type') == 'number' or parameter.get('type') == 'text':
+ parameter_type = parameter.get('type')
+ parameter_name = parameter.get('name')
+ if parameter_type == 'hidden':
+ context_parameters[parameter_name] = parameter.get('value')
+ if parameter_type == 'number' or parameter_type == 'text':
if parameter.has_attr('required'):
if parameter.get('placeholder') == '':
if parameter.get('value') == '':
- name_value = parameter.get('name')
- error_messages.append(f'Missing example or default value for parameter "{name_value}"')
+ error_messages.append(f'Missing example or default value for parameter "{parameter_name}"')
else:
- formstring = formstring + '&' + parameter.get('name') + '=' + parameter.get('value')
+ context_parameters[parameter_name] = parameter.get('value')
else:
- formstring = formstring + '&' + parameter.get('name') + '=' + parameter.get('placeholder')
- # same thing, just for checkboxes. If a checkbox is checked per default, it gets added to the formstring
- if parameter.get('type') == 'checkbox':
+ context_parameters[parameter_name] = parameter.get('placeholder')
+ # same thing, just for checkboxes. If a checkbox is checked per default, it gets added to the url parameters
+ if parameter_type == 'checkbox':
if parameter.has_attr('checked'):
- formstring = formstring + '&' + parameter.get('name') + '=on'
+ context_parameters[parameter_name] = 'on'
for listing in lists:
selectionvalue = ''
listname = listing.get('name')
@@ -102,15 +118,21 @@ def testBridges(instance: Instance, bridge_cards: Iterable, with_upload: bool, w
if 'selected' in selectionentry.attrs:
selectionvalue = selectionentry.get('value')
break
- formstring = formstring + '&' + listname + '=' + selectionvalue
- termpad_url = 'about:blank'
+ context_parameters[listname] = selectionvalue
+ artifact_url = 'about:blank'
if error_messages:
status = '
'.join(map(lambda m: f'❌ `{m}`', error_messages))
else:
- # if all example/default values are present, form the full request string, run the request, add a tag with
+ # if all example/default values are present, form the full request url, run the request, add a tag with
# the url of em's public instance to the response text (so that relative paths work, e.g. to the static css file) and
- # then upload it to termpad.com, a pastebin-like-site.
- response = requests.get(instance.url + bridgestring + formstring)
+ # then save it to a html file.
+ context_parameters.update({
+ 'action': 'display',
+ 'bridge': bridgeid,
+ 'format': 'Html',
+ })
+ request_url = f'{instance.url}/?{urllib.parse.urlencode(context_parameters)}'
+ response = requests.get(request_url)
page_text = response.text.replace('
','')
page_text = page_text.encode("utf_8")
soup = BeautifulSoup(page_text, "html.parser")
@@ -134,16 +156,18 @@ def testBridges(instance: Instance, bridge_cards: Iterable, with_upload: bool, w
if status_is_ok:
status = '✔️'
if with_upload and (not with_reduced_upload or not status_is_ok):
- termpad = requests.post(url="https://termpad.com/", data=page_text)
- termpad_url = termpad.text.strip()
- termpad_url = termpad_url.replace('termpad.com/','termpad.com/raw/')
- table_rows.append(f'| {bridge_name} | [{form_number} {context_name}{instance_suffix}]({termpad_url}) | {status} |')
+ filename = f'{bridge_name} {form_number}{instance_suffix}{ARTIFACT_FILE_EXTENSION}'
+ filename = re.sub(r'[^a-z0-9 \_\-\.]', '', filename, flags=re.I).replace(' ', '_')
+ with open(file=f'{artifact_directory}/{filename}', mode='wb') as file:
+ file.write(page_text)
+ artifact_url = f'{artifact_base_url}/{filename}'
+ table_rows.append(f'| {bridge_name} | [{form_number} {context_name}{instance_suffix}]({artifact_url}) | {status} |')
form_number += 1
return table_rows
def getFirstLine(value: str) -> str:
# trim whitespace and remove text that can break the table or is simply unnecessary
- clean_value = re.sub('^\[[^\]]+\]\s*rssbridge\.|[\|`]', '', value.strip())
+ clean_value = re.sub(r'^\[[^\]]+\]\s*rssbridge\.|[\|`]', '', value.strip())
first_line = next(iter(clean_value.splitlines()), '')
max_length = 250
if (len(first_line) > max_length):
@@ -163,8 +187,8 @@ if __name__ == '__main__':
for instance_arg in args.instances:
instance_arg_parts = instance_arg.split('::')
instance = Instance()
- instance.name = instance_arg_parts[1] if len(instance_arg_parts) >= 2 else ''
- instance.url = instance_arg_parts[0]
+ instance.name = instance_arg_parts[1].strip() if len(instance_arg_parts) >= 2 else ''
+ instance.url = instance_arg_parts[0].strip().rstrip("/")
instances.append(instance)
else:
instance = Instance()
@@ -181,4 +205,4 @@ if __name__ == '__main__':
with_reduced_upload=args.reduced_upload and not args.no_upload,
title=args.title,
output_file=args.output_file
- );
\ No newline at end of file
+ );
diff --git a/.github/workflows/dockerbuild.yml b/.github/workflows/dockerbuild.yml
index 82d8611a..39645558 100644
--- a/.github/workflows/dockerbuild.yml
+++ b/.github/workflows/dockerbuild.yml
@@ -21,7 +21,7 @@ jobs:
-
name: Docker meta
id: docker_meta
- uses: docker/metadata-action@v4
+ uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKERHUB_SLUG }}
@@ -33,26 +33,26 @@ jobs:
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/20') }}
-
name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@v3
-
name: Login to DockerHub
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Login to GitHub Container Registry
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Build and push
- uses: docker/bake-action@v2
+ uses: docker/bake-action@v5
with:
files: |
./docker-bake.hcl
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index b00c898a..e0201022 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -9,7 +9,7 @@ jobs:
documentation:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup PHP
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 83911ab6..79201edd 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -8,12 +8,12 @@ on:
jobs:
phpcs:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
strategy:
matrix:
php-versions: ['7.4']
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
@@ -21,12 +21,12 @@ jobs:
- run: phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p
phpcompatibility:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
strategy:
matrix:
php-versions: ['7.4']
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
@@ -36,9 +36,9 @@ jobs:
- run: ~/.composer/vendor/bin/phpcs . --standard=phpcompatibility.xml --warning-severity=0 --extensions=php -p
executable_php_files_check:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- run: |
if find -name "*.php" -executable -type f -print -exec false {} +
then
diff --git a/.github/workflows/prhtmlgenerator.yml b/.github/workflows/prhtmlgenerator.yml
index 7985250a..bfc89e7a 100644
--- a/.github/workflows/prhtmlgenerator.yml
+++ b/.github/workflows/prhtmlgenerator.yml
@@ -5,15 +5,30 @@ on:
branches: [ master ]
jobs:
+ check-bridges:
+ name: Check if bridges were changed
+ runs-on: ubuntu-latest
+ outputs:
+ BRIDGES: ${{ steps.check1.outputs.BRIDGES }}
+ steps:
+ - name: Check number of bridges
+ id: check1
+ run: |
+ PR=${{github.event.number}};
+ wget https://patch-diff.githubusercontent.com/raw/$GITHUB_REPOSITORY/pull/$PR.patch;
+ bridgeamount=$(cat $PR.patch | grep "\bbridges/[A-Za-z0-9]*Bridge\.php\b" | sed "s=.*\bbridges/\([A-Za-z0-9]*\)Bridge\.php\b.*=\1=g" | sort | uniq | wc -l);
+ echo "BRIDGES=$bridgeamount" >> "$GITHUB_OUTPUT"
test-pr:
name: Generate HTML
runs-on: ubuntu-latest
+ needs: check-bridges
+ if: needs.check-bridges.outputs.BRIDGES > 0
env:
PYTHONUNBUFFERED: 1
# Needs additional permissions https://github.com/actions/first-interaction/issues/10#issuecomment-1041402989
steps:
- name: Check out self
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
@@ -33,9 +48,9 @@ jobs:
docker build -t prbuild .;
docker run -d -v $GITHUB_WORKSPACE/whitelist.txt:/app/whitelist.txt -v $GITHUB_WORKSPACE/DEBUG:/app/DEBUG -p 3001:80 prbuild
- name: Setup python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
- python-version: '3.7'
+ python-version: '3.13'
cache: 'pip'
- name: Install requirements
run: |
@@ -51,9 +66,17 @@ jobs:
body="${body//$'\n'/'%0A'}";
body="${body//$'\r'/'%0D'}";
echo "bodylength=${#body}" >> $GITHUB_OUTPUT
+ env:
+ PR: ${{ github.event.number }}
+ - name: Upload generated tests
+ uses: actions/upload-artifact@v4
+ id: upload-generated-tests
+ with:
+ name: tests
+ path: '*.html'
- name: Find Comment
if: ${{ steps.testrun.outputs.bodylength > 130 }}
- uses: peter-evans/find-comment@v2
+ uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -61,9 +84,43 @@ jobs:
body-includes: Pull request artifacts
- name: Create or update comment
if: ${{ steps.testrun.outputs.bodylength > 130 }}
- uses: peter-evans/create-or-update-comment@v2
+ uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-file: comment.txt
edit-mode: replace
+ upload_tests:
+ name: Upload tests
+ runs-on: ubuntu-latest
+ needs: test-pr
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ repository: 'RSS-Bridge/rss-bridge-tests'
+ ref: 'main'
+ token: ${{ secrets.RSSTESTER_ACTION }}
+
+ - name: Setup git config
+ run: |
+ git config --global user.name "GitHub Actions"
+ git config --global user.email "<>"
+
+ - name: Download tests
+ uses: actions/download-artifact@v4
+ with:
+ name: tests
+
+ - name: Move tests
+ run: |
+ cd prs
+ mkdir -p ${{github.event.number}}
+ cd ${{github.event.number}}
+ mv -f $GITHUB_WORKSPACE/*.html .
+
+ - name: Commit and push generated tests
+ run: |
+ export COMMIT_MESSAGE="Added tests for PR ${{github.event.number}}"
+ git add .
+ git commit -m "$COMMIT_MESSAGE" || exit 0
+ git push
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index e7684e6b..435369fa 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -8,14 +8,16 @@ on:
jobs:
phpunit8:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
strategy:
matrix:
- php-versions: ['7.4', '8.0', '8.1']
+ php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
+ env:
+ update: true
- run: composer install
- run: composer test
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f1080743..d27421aa 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -15,7 +15,7 @@
* [Astalaseven](https://github.com/Astalaseven)
* [Astyan-42](https://github.com/Astyan-42)
* [austinhuang0131](https://github.com/austinhuang0131)
-* [AxorPL](https://github.com/AxorPL)
+* [axor-mst](https://github.com/axor-mst)
* [ayacoo](https://github.com/ayacoo)
* [az5he6ch](https://github.com/az5he6ch)
* [b1nj](https://github.com/b1nj)
@@ -23,6 +23,7 @@
* [Binnette](https://github.com/Binnette)
* [BoboTiG](https://github.com/BoboTiG)
* [Bockiii](https://github.com/Bockiii)
+* [brtsos](https://github.com/brtsos)
* [captn3m0](https://github.com/captn3m0)
* [chemel](https://github.com/chemel)
* [Chouchen](https://github.com/Chouchen)
@@ -144,6 +145,7 @@
* [Niehztog](https://github.com/Niehztog)
* [NikNikYkt](https://github.com/NikNikYkt)
* [Nono-m0le](https://github.com/Nono-m0le)
+* [NotsoanoNimus](https://github.com/NotsoanoNimus)
* [obsiwitch](https://github.com/obsiwitch)
* [Ololbu](https://github.com/Ololbu)
* [ORelio](https://github.com/ORelio)
diff --git a/Dockerfile b/Dockerfile
index 2f1f4f3d..fb783344 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,3 @@
-FROM lwthiker/curl-impersonate:0.5-ff-slim-buster AS curlimpersonate
-
FROM debian:12-slim AS rssbridge
LABEL description="RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one."
@@ -7,7 +5,8 @@ LABEL repository="https://github.com/RSS-Bridge/rss-bridge"
LABEL website="https://github.com/RSS-Bridge/rss-bridge"
ARG DEBIAN_FRONTEND=noninteractive
-RUN apt-get update && \
+RUN set -xe && \
+ apt-get update && \
apt-get install --yes --no-install-recommends \
ca-certificates \
nginx \
@@ -24,18 +23,47 @@ RUN apt-get update && \
php-xml \
php-zip \
# php-zlib is enabled by default with PHP 8.2 in Debian 12
+ # for downloading libcurl-impersonate
+ curl \
+ # for patching libcurl-impersonate
+ patchelf \
&& \
+ # install curl-impersonate library
+ curlimpersonate_version=1.0.0rc2 && \
+ { \
+ { \
+ [ $(arch) = 'aarch64' ] && \
+ archive="libcurl-impersonate-v${curlimpersonate_version}.aarch64-linux-gnu.tar.gz" && \
+ sha512sum="c8add80e7a0430a074edea1a11f73d03044c48e848e164af2d6f362866623e29bede207a50f18f95b1bc5ab3d33f5c31408be60a6da66b74a0d176eebe299116" \
+ ; } \
+ || { \
+ [ $(arch) = 'armv7l' ] && \
+ archive="libcurl-impersonate-v${curlimpersonate_version}.arm-linux-gnueabihf.tar.gz" && \
+ sha512sum="d0403ca4ad55a8d499b120e5675c7b5a0dc4946af49c933e91fc24455ffe5e122aa21ee95554612ff5d1bd6faea1556e1e1b9c821918e2644cc9bcbddc05747a" \
+ ; } \
+ || { \
+ [ $(arch) = 'x86_64' ] && \
+ archive="libcurl-impersonate-v${curlimpersonate_version}.x86_64-linux-gnu.tar.gz" && \
+ sha512sum="35cafda2b96df3218a6d8545e0947a899837ede51c90f7ef2980bd2d99dbd67199bc620000df28b186727300b8c7046d506807fb48ee0fbc068dc8ae01986339" \
+ ; } \
+ } && \
+ curl -LO "https://github.com/lexiforest/curl-impersonate/releases/download/v${curlimpersonate_version}/${archive}" && \
+ echo "$sha512sum $archive" | sha512sum -c - && \
+ mkdir -p /usr/local/lib/curl-impersonate && \
+ tar xaf "$archive" -C /usr/local/lib/curl-impersonate && \
+ patchelf --set-soname libcurl.so.4 /usr/local/lib/curl-impersonate/libcurl-impersonate.so && \
+ rm "$archive" && \
+ apt-get purge --assume-yes curl patchelf && \
rm -rf /var/lib/apt/lists/*
+ENV LD_PRELOAD /usr/local/lib/curl-impersonate/libcurl-impersonate.so
+ENV CURL_IMPERSONATE chrome131
+
# logs should go to stdout / stderr
RUN ln -sfT /dev/stderr /var/log/nginx/error.log; \
ln -sfT /dev/stdout /var/log/nginx/access.log; \
chown -R --no-dereference www-data:adm /var/log/nginx/
-COPY --from=curlimpersonate /usr/local/lib/libcurl-impersonate-ff.so /usr/local/lib/curl-impersonate/
-ENV LD_PRELOAD /usr/local/lib/curl-impersonate/libcurl-impersonate-ff.so
-ENV CURL_IMPERSONATE ff91esr
-
COPY ./config/nginx.conf /etc/nginx/sites-available/default
COPY ./config/php-fpm.conf /etc/php/8.2/fpm/pool.d/rss-bridge.conf
COPY ./config/php.ini /etc/php/8.2/fpm/conf.d/90-rss-bridge.ini
diff --git a/README.md b/README.md
index cadba3b9..05042630 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ Requires minimum PHP 7.4.
|||
|||
-## A subset of bridges (16/447)
+## A subset of bridges (15/447)
* `CssSelectorBridge`: [Scrape out a feed using CSS selectors](https://rss-bridge.org/bridge01/#bridge-CssSelectorBridge)
* `FeedMergeBridge`: [Combine multiple feeds into one](https://rss-bridge.org/bridge01/#bridge-FeedMergeBridge)
@@ -44,18 +44,18 @@ Requires minimum PHP 7.4.
* `ThePirateBayBridge:` [Fetches torrents by search/user/category](https://rss-bridge.org/bridge01/#bridge-ThePirateBayBridge)
* `TikTokBridge`: [Fetches posts by username](https://rss-bridge.org/bridge01/#bridge-TikTokBridge)
* `TwitchBridge`: [Fetches videos from channel](https://rss-bridge.org/bridge01/#bridge-TwitchBridge)
-* `VkBridge`: [Fetches posts from user/group](https://rss-bridge.org/bridge01/#bridge-VkBridge)
* `XPathBridge`: [Scrape out a feed using XPath expressions](https://rss-bridge.org/bridge01/#bridge-XPathBridge)
* `YoutubeBridge`: [Fetches videos by username/channel/playlist/search](https://rss-bridge.org/bridge01/#bridge-YoutubeBridge)
-* `YouTubeCommunityTabBridge`: [Fetches posts from a channel's community tab](https://rss-bridge.org/bridge01/#bridge-YouTubeCommunityTabBridge)
+* `YouTubeCommunityTabBridge`: [Fetches posts from a channel's Posts tab](https://rss-bridge.org/bridge01/#bridge-YouTubeCommunityTabBridge)
## Tutorial
### How to install on traditional shared web hosting
-RSS-Bridge can basically be unzipped in a web folder. Should be working instantly.
+RSS-Bridge can basically be unzipped into a web folder. Should be working instantly.
-Latest zip as of Sep 2023: https://github.com/RSS-Bridge/rss-bridge/archive/refs/tags/2023-09-24.zip
+Latest zip:
+https://github.com/RSS-Bridge/rss-bridge/archive/refs/heads/master.zip (2MB)
### How to install on Debian 12 (nginx + php-fpm)
@@ -64,34 +64,34 @@ These instructions have been tested on a fresh Debian 12 VM from Digital Ocean (
```shell
timedatectl set-timezone Europe/Oslo
-apt install git nginx php8.2-fpm php-mbstring php-simplexml php-curl
+apt install git nginx php8.2-fpm php-mbstring php-simplexml php-curl php-intl
-# Create a new user account
+# Create a user account
useradd --shell /bin/bash --create-home rss-bridge
cd /var/www
-# Create folder and change ownership
+# Create folder and change its ownership to rss-bridge
mkdir rss-bridge && chown rss-bridge:rss-bridge rss-bridge/
-# Become user
+# Become rss-bridge
su rss-bridge
-# Fetch latest master
+# Clone master branch into existing folder
git clone https://github.com/RSS-Bridge/rss-bridge.git rss-bridge/
cd rss-bridge
-# Copy over the default config
+# Copy over the default config (OPTIONAL)
cp -v config.default.ini.php config.ini.php
-# Give full permissions only to owner (rss-bridge)
-chmod 700 -R ./
+# Recursively give full permissions to user/owner
+chmod 700 --recursive ./
-# Give read and execute to others (nginx and php-fpm)
+# Give read and execute to others on folder ./static
chmod o+rx ./ ./static
-# Give read to others (nginx)
-chmod o+r -R ./static
+# Recursively give give read to others on folder ./static
+chmod o+r --recursive ./static
```
Nginx config:
@@ -101,37 +101,37 @@ Nginx config:
server {
listen 80;
+
+ # TODO: change to your own server name
server_name example.com;
+
access_log /var/log/nginx/rss-bridge.access.log;
error_log /var/log/nginx/rss-bridge.error.log;
+ log_not_found off;
- # Intentionally not setting a root folder here
-
- # autoindex is off by default but feels good to explicitly turn off
- autoindex off;
+ # Intentionally not setting a root folder
# Static content only served here
location /static/ {
alias /var/www/rss-bridge/static/;
}
- # Pass off to php-fpm only when location is exactly /
+ # Pass off to php-fpm only when location is EXACTLY == /
location = / {
root /var/www/rss-bridge/;
include snippets/fastcgi-php.conf;
+ fastcgi_read_timeout 45s;
fastcgi_pass unix:/run/php/rss-bridge.sock;
}
- # Reduce spam
+ # Reduce log noise
location = /favicon.ico {
access_log off;
- log_not_found off;
}
- # Reduce spam
+ # Reduce log noise
location = /robots.txt {
access_log off;
- log_not_found off;
}
}
```
@@ -150,8 +150,11 @@ listen = /run/php/rss-bridge.sock
listen.owner = www-data
listen.group = www-data
+; Create 10 workers standing by to serve requests
pm = static
pm.max_children = 10
+
+; Respawn worker after 500 requests (workaround for memory leaks etc.)
pm.max_requests = 500
```
@@ -167,12 +170,10 @@ Restart fpm and nginx:
```shell
# Lint and restart php-fpm
-php-fpm8.2 -t
-systemctl restart php8.2-fpm
+php-fpm8.2 -t && systemctl restart php8.2-fpm
# Lint and restart nginx
-nginx -t
-systemctl restart nginx
+nginx -t && systemctl restart nginx
```
### How to install from Composer
@@ -181,7 +182,7 @@ Install the latest release.
```shell
cd /var/www
-composer create-project -v --no-dev rss-bridge/rss-bridge
+composer create-project -v --no-dev --no-scripts rss-bridge/rss-bridge
```
### How to install with Caddy
@@ -194,8 +195,16 @@ Install by downloading the docker image from Docker Hub:
```bash
# Create container
-docker create --name=rss-bridge --publish 3000:80 rssbridge/rss-bridge
+docker create --name=rss-bridge --publish 3000:80 --volume $(pwd)/config:/config rssbridge/rss-bridge
+```
+You can put custom `config.ini.php` and bridges into `./config`.
+
+**You must restart container for custom changes to take effect.**
+
+See `docker-entrypoint.sh` for details.
+
+```bash
# Start container
docker start rss-bridge
```
@@ -209,30 +218,29 @@ Browse http://localhost:3000/
docker build -t rss-bridge .
# Create container
-docker create --name rss-bridge --publish 3000:80 rss-bridge
+docker create --name rss-bridge --publish 3000:80 --volume $(pwd)/config:/config rss-bridge
+```
+You can put custom `config.ini.php` and bridges into `./config`.
+
+**You must restart container for custom changes to take effect.**
+
+See `docker-entrypoint.sh` for details.
+
+```bash
# Start container
docker start rss-bridge
```
Browse http://localhost:3000/
-### Install with docker-compose
+### Install with docker-compose (using Docker Hub)
-Create a `docker-compose.yml` file locally with with the following content:
-```yml
-version: '2'
-services:
- rss-bridge:
- image: rssbridge/rss-bridge:latest
- volumes:
- - :/config
- ports:
- - 3000:80
- restart: unless-stopped
-```
+You can put custom `config.ini.php` and bridges into `./config`.
-Then launch with `docker-compose`:
+**You must restart container for custom changes to take effect.**
+
+See `docker-entrypoint.sh` for details.
```bash
docker-compose up
@@ -313,13 +321,23 @@ The sqlite files (db, wal and shm) are not writeable.
rm cache/*
-### How to create a new bridge from scratch
+### How to create a completely new bridge
+
+New code files MUST have `declare(strict_types=1);` at the top of file:
+
+```php
+bridgeFactory = new BridgeFactory();
+ public function __construct(
+ BridgeFactory $bridgeFactory
+ ) {
+ $this->bridgeFactory = $bridgeFactory;
}
- public function execute(Request $request)
+ public function __invoke(Request $request): Response
{
- if (!Debug::isEnabled()) {
- return new Response('This action is only available in debug mode!', 403);
+ if (Configuration::getConfig('system', 'env') !== 'dev') {
+ return new Response('This action is only available in dev environment!', 403);
}
$bridgeName = $request->get('bridge');
if (!$bridgeName) {
- return render_template('connectivity.html.php');
+ return new Response(render_template('connectivity.html.php'));
}
$bridgeClassName = $this->bridgeFactory->createBridgeClassName($bridgeName);
if (!$bridgeClassName) {
@@ -54,8 +55,8 @@ class ConnectivityAction implements ActionInterface
];
try {
$response = getContents($bridge::URI, [], $curl_opts, true);
- $result['http_code'] = $response['code'];
- if (in_array($response['code'], [200])) {
+ $result['http_code'] = $response->getCode();
+ if (in_array($result['http_code'], [200])) {
$result['successful'] = true;
}
} catch (\Exception $e) {
diff --git a/actions/DetectAction.php b/actions/DetectAction.php
index 0c61f1b6..8d3d6263 100644
--- a/actions/DetectAction.php
+++ b/actions/DetectAction.php
@@ -2,7 +2,15 @@
class DetectAction implements ActionInterface
{
- public function execute(Request $request)
+ private BridgeFactory $bridgeFactory;
+
+ public function __construct(
+ BridgeFactory $bridgeFactory
+ ) {
+ $this->bridgeFactory = $bridgeFactory;
+ }
+
+ public function __invoke(Request $request): Response
{
$url = $request->get('url');
$format = $request->get('format');
@@ -14,14 +22,12 @@ class DetectAction implements ActionInterface
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'You must specify a format']));
}
- $bridgeFactory = new BridgeFactory();
-
- foreach ($bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
- if (!$bridgeFactory->isEnabled($bridgeClassName)) {
+ foreach ($this->bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
+ if (!$this->bridgeFactory->isEnabled($bridgeClassName)) {
continue;
}
- $bridge = $bridgeFactory->create($bridgeClassName);
+ $bridge = $this->bridgeFactory->create($bridgeClassName);
$bridgeParams = $bridge->detectParameters($url);
diff --git a/actions/DisplayAction.php b/actions/DisplayAction.php
index ed063825..97f53caa 100644
--- a/actions/DisplayAction.php
+++ b/actions/DisplayAction.php
@@ -4,42 +4,28 @@ class DisplayAction implements ActionInterface
{
private CacheInterface $cache;
private Logger $logger;
+ private BridgeFactory $bridgeFactory;
- public function __construct()
- {
- $this->cache = RssBridge::getCache();
- $this->logger = RssBridge::getLogger();
+ public function __construct(
+ CacheInterface $cache,
+ Logger $logger,
+ BridgeFactory $bridgeFactory
+ ) {
+ $this->cache = $cache;
+ $this->logger = $logger;
+ $this->bridgeFactory = $bridgeFactory;
}
- public function execute(Request $request)
+ public function __invoke(Request $request): Response
{
$bridgeName = $request->get('bridge');
$format = $request->get('format');
$noproxy = $request->get('_noproxy');
- $cacheKey = 'http_' . json_encode($request->toArray());
- /** @var Response $cachedResponse */
- $cachedResponse = $this->cache->get($cacheKey);
- if ($cachedResponse) {
- $ifModifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? null;
- $lastModified = $cachedResponse->getHeader('last-modified');
- if ($ifModifiedSince && $lastModified) {
- $lastModified = new \DateTimeImmutable($lastModified);
- $lastModifiedTimestamp = $lastModified->getTimestamp();
- $modifiedSince = strtotime($ifModifiedSince);
- if ($lastModifiedTimestamp <= $modifiedSince) {
- $modificationTimeGMT = gmdate('D, d M Y H:i:s ', $lastModifiedTimestamp);
- return new Response('', 304, ['last-modified' => $modificationTimeGMT . 'GMT']);
- }
- }
- return $cachedResponse;
- }
-
if (!$bridgeName) {
- return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Missing bridge parameter']), 400);
+ return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Missing bridge name parameter']), 400);
}
- $bridgeFactory = new BridgeFactory();
- $bridgeClassName = $bridgeFactory->createBridgeClassName($bridgeName);
+ $bridgeClassName = $this->bridgeFactory->createBridgeClassName($bridgeName);
if (!$bridgeClassName) {
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Bridge not found']), 404);
}
@@ -47,11 +33,11 @@ class DisplayAction implements ActionInterface
if (!$format) {
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'You must specify a format']), 400);
}
- if (!$bridgeFactory->isEnabled($bridgeClassName)) {
+ if (!$this->bridgeFactory->isEnabled($bridgeClassName)) {
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'This bridge is not whitelisted']), 400);
}
-
+ // Disable proxy (if enabled and per user's request)
if (
Configuration::getConfig('proxy', 'url')
&& Configuration::getConfig('proxy', 'by_bridge')
@@ -61,9 +47,9 @@ class DisplayAction implements ActionInterface
define('NOPROXY', true);
}
- $bridge = $bridgeFactory->create($bridgeClassName);
- $formatFactory = new FormatFactory();
- $format = $formatFactory->create($format);
+ $cacheKey = 'http_' . json_encode($request->toArray());
+
+ $bridge = $this->bridgeFactory->create($bridgeClassName);
$response = $this->createResponse($request, $bridge, $format);
@@ -77,26 +63,12 @@ class DisplayAction implements ActionInterface
$this->cache->set($cacheKey, $response, $ttl);
}
- if (in_array($response->getCode(), [403, 429, 503])) {
- // Cache these responses for about ~20 mins on average
- $this->cache->set($cacheKey, $response, 60 * 15 + rand(1, 60 * 10));
- }
-
- if ($response->getCode() === 500) {
- $this->cache->set($cacheKey, $response, 60 * 15);
- }
-
- if (rand(1, 100) === 2) {
- $this->cache->prune();
- }
-
return $response;
}
- private function createResponse(Request $request, BridgeAbstract $bridge, FormatAbstract $format)
+ private function createResponse(Request $request, BridgeAbstract $bridge, string $format)
{
$items = [];
- $feed = [];
try {
$bridge->loadConfiguration();
@@ -116,28 +88,22 @@ class DisplayAction implements ActionInterface
$bridge->setInput($input);
$bridge->collectData();
$items = $bridge->getItems();
- if (isset($items[0]) && is_array($items[0])) {
- $feedItems = [];
- foreach ($items as $item) {
- $feedItems[] = FeedItem::fromArray($item);
+ } catch (\Throwable $e) {
+ if ($e instanceof ClientException) {
+ $this->logger->debug(sprintf('Exception in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
+ } elseif ($e instanceof RateLimitException) {
+ $this->logger->debug(sprintf('Exception in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
+ return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), 429);
+ } elseif ($e instanceof HttpException) {
+ if (in_array($e->getCode(), [429, 503])) {
+ // Log with debug, immediately reproduce and return
+ $this->logger->debug(sprintf('Exception in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
+ return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), $e->getCode());
}
- $items = $feedItems;
+ // Some other status code which we let fail normally (but don't log it)
+ } else {
+ $this->logger->error(sprintf('Exception in DisplayAction(%s)', $bridge->getShortName()), ['e' => $e]);
}
- $feed = $bridge->getFeed();
- } catch (\Exception $e) {
- // Probably an exception inside a bridge
- if ($e instanceof HttpException) {
- // Reproduce (and log) these responses regardless of error output and report limit
- if ($e->getCode() === 429) {
- $this->logger->info(sprintf('Exception in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
- return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), 429);
- }
- if ($e->getCode() === 503) {
- $this->logger->info(sprintf('Exception in DisplayAction(%s): %s', $bridge->getShortName(), create_sane_exception_message($e)));
- return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), 503);
- }
- }
- $this->logger->error(sprintf('Exception in DisplayAction(%s)', $bridge->getShortName()), ['e' => $e]);
$errorOutput = Configuration::getConfig('error', 'output');
$reportLimit = Configuration::getConfig('error', 'report_limit');
$errorCount = 1;
@@ -148,7 +114,7 @@ class DisplayAction implements ActionInterface
if ($errorCount >= $reportLimit) {
if ($errorOutput === 'feed') {
// Render the exception as a feed item
- $items[] = $this->createFeedItemFromException($e, $bridge);
+ $items = [$this->createFeedItemFromException($e, $bridge)];
} elseif ($errorOutput === 'http') {
return new Response(render(__DIR__ . '/../templates/exception.html.php', ['e' => $e]), 500);
} elseif ($errorOutput === 'none') {
@@ -157,38 +123,49 @@ class DisplayAction implements ActionInterface
}
}
+ $formatFactory = new FormatFactory();
+ $format = $formatFactory->create($format);
+
$format->setItems($items);
- $format->setFeed($feed);
+ $format->setFeed($bridge->getFeed());
$now = time();
$format->setLastModified($now);
$headers = [
'last-modified' => gmdate('D, d M Y H:i:s ', $now) . 'GMT',
- 'content-type' => $format->getMimeType() . '; charset=' . $format->getCharset(),
+ 'content-type' => $format->getMimeType() . '; charset=UTF-8',
];
- return new Response($format->stringify(), 200, $headers);
+ $body = $format->render();
+
+ // This is supposed to remove non-utf8 byte sequences, but I'm unsure if it works
+ ini_set('mbstring.substitute_character', 'none');
+ $body = mb_convert_encoding($body, 'UTF-8', 'UTF-8');
+
+ return new Response($body, 200, $headers);
}
- private function createFeedItemFromException($e, BridgeAbstract $bridge): FeedItem
+ private function createFeedItemFromException($e, BridgeAbstract $bridge): array
{
- $item = new FeedItem();
+ $item = [];
// Create a unique identifier every 24 hours
$uniqueIdentifier = urlencode((int)(time() / 86400));
$title = sprintf('Bridge returned error %s! (%s)', $e->getCode(), $uniqueIdentifier);
- $item->setTitle($title);
- $item->setURI(get_current_url());
- $item->setTimestamp(time());
+
+ $item['title'] = $title;
+ $item['uri'] = get_current_url();
+ $item['timestamp'] = time();
// Create an item identifier for feed readers e.g. "staysafetv twitch videos_19389"
- $item->setUid($bridge->getName() . '_' . $uniqueIdentifier);
+ $item['uid'] = $bridge->getName() . '_' . $uniqueIdentifier;
$content = render_template(__DIR__ . '/../templates/bridge-error.html.php', [
'error' => render_template(__DIR__ . '/../templates/exception.html.php', ['e' => $e]),
'searchUrl' => self::createGithubSearchUrl($bridge),
- 'issueUrl' => self::createGithubIssueUrl($bridge, $e, create_sane_exception_message($e)),
+ 'issueUrl' => self::createGithubIssueUrl($bridge, $e),
'maintainer' => $bridge->getMaintainer(),
]);
- $item->setContent($content);
+ $item['content'] = $content;
+
return $item;
}
@@ -213,22 +190,34 @@ class DisplayAction implements ActionInterface
return $report['count'];
}
- private static function createGithubIssueUrl($bridge, $e, string $message): string
+ private static function createGithubIssueUrl(BridgeAbstract $bridge, \Throwable $e): string
{
- return sprintf('https://github.com/RSS-Bridge/rss-bridge/issues/new?%s', http_build_query([
- 'title' => sprintf('%s failed with error %s', $bridge->getName(), $e->getCode()),
+ $maintainer = $bridge->getMaintainer();
+ if (str_contains($maintainer, ',')) {
+ $maintainers = explode(',', $maintainer);
+ } else {
+ $maintainers = [$maintainer];
+ }
+ $maintainers = array_map('trim', $maintainers);
+
+ $queryString = $_SERVER['QUERY_STRING'] ?? '';
+ $query = [
+ 'title' => $bridge->getName() . ' failed with: ' . $e->getMessage(),
'body' => sprintf(
- "```\n%s\n\n%s\n\nQuery string: %s\nVersion: %s\nOs: %s\nPHP version: %s\n```",
- $message,
+ "```\n%s\n\n%s\n\nQuery string: %s\nVersion: %s\nOs: %s\nPHP version: %s\n```\nMaintainer: @%s",
+ create_sane_exception_message($e),
implode("\n", trace_to_call_points(trace_from_exception($e))),
- $_SERVER['QUERY_STRING'] ?? '',
+ $queryString,
Configuration::getVersion(),
PHP_OS_FAMILY,
- phpversion() ?: 'Unknown'
+ phpversion() ?: 'Unknown',
+ implode(', @', $maintainers),
),
'labels' => 'Bridge-Broken',
- 'assignee' => $bridge->getMaintainer(),
- ]));
+ 'assignee' => $maintainer[0],
+ ];
+
+ return 'https://github.com/RSS-Bridge/rss-bridge/issues/new?' . http_build_query($query);
}
private static function createGithubSearchUrl($bridge): string
diff --git a/actions/FindfeedAction.php b/actions/FindfeedAction.php
index 94dc6b72..e18c3e1d 100644
--- a/actions/FindfeedAction.php
+++ b/actions/FindfeedAction.php
@@ -7,7 +7,15 @@
*/
class FindfeedAction implements ActionInterface
{
- public function execute(Request $request)
+ private BridgeFactory $bridgeFactory;
+
+ public function __construct(
+ BridgeFactory $bridgeFactory
+ ) {
+ $this->bridgeFactory = $bridgeFactory;
+ }
+
+ public function __invoke(Request $request): Response
{
$url = $request->get('url');
$format = $request->get('format');
@@ -19,15 +27,13 @@ class FindfeedAction implements ActionInterface
return new Response('You must specify a format', 400);
}
- $bridgeFactory = new BridgeFactory();
-
$results = [];
- foreach ($bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
- if (!$bridgeFactory->isEnabled($bridgeClassName)) {
+ foreach ($this->bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
+ if (!$this->bridgeFactory->isEnabled($bridgeClassName)) {
continue;
}
- $bridge = $bridgeFactory->create($bridgeClassName);
+ $bridge = $this->bridgeFactory->create($bridgeClassName);
$bridgeParams = $bridge->detectParameters($url);
diff --git a/actions/FrontpageAction.php b/actions/FrontpageAction.php
index 32795c45..79ffb4f5 100644
--- a/actions/FrontpageAction.php
+++ b/actions/FrontpageAction.php
@@ -2,15 +2,24 @@
final class FrontpageAction implements ActionInterface
{
- public function execute(Request $request)
+ private BridgeFactory $bridgeFactory;
+
+ public function __construct(
+ BridgeFactory $bridgeFactory
+ ) {
+ $this->bridgeFactory = $bridgeFactory;
+ }
+
+ public function __invoke(Request $request): Response
{
+ $token = $request->getAttribute('token');
+
$messages = [];
$activeBridges = 0;
- $bridgeFactory = new BridgeFactory();
- $bridgeClassNames = $bridgeFactory->getBridgeClassNames();
+ $bridgeClassNames = $this->bridgeFactory->getBridgeClassNames();
- foreach ($bridgeFactory->getMissingEnabledBridges() as $missingEnabledBridge) {
+ foreach ($this->bridgeFactory->getMissingEnabledBridges() as $missingEnabledBridge) {
$messages[] = [
'body' => sprintf('Warning : Bridge "%s" not found', $missingEnabledBridge),
'level' => 'warning'
@@ -19,20 +28,22 @@ final class FrontpageAction implements ActionInterface
$body = '';
foreach ($bridgeClassNames as $bridgeClassName) {
- if ($bridgeFactory->isEnabled($bridgeClassName)) {
- $body .= BridgeCard::render($bridgeClassName, $request);
+ if ($this->bridgeFactory->isEnabled($bridgeClassName)) {
+ $body .= BridgeCard::render($this->bridgeFactory, $bridgeClassName, $token);
$activeBridges++;
}
}
- // todo: cache this renderered template?
- return render(__DIR__ . '/../templates/frontpage.html.php', [
- 'messages' => $messages,
- 'admin_email' => Configuration::getConfig('admin', 'email'),
- 'admin_telegram' => Configuration::getConfig('admin', 'telegram'),
- 'bridges' => $body,
- 'active_bridges' => $activeBridges,
- 'total_bridges' => count($bridgeClassNames),
- ]);
+ $response = new Response(render(__DIR__ . '/../templates/frontpage.html.php', [
+ 'messages' => $messages,
+ 'admin_email' => Configuration::getConfig('admin', 'email'),
+ 'admin_telegram' => Configuration::getConfig('admin', 'telegram'),
+ 'bridges' => $body,
+ 'active_bridges' => $activeBridges,
+ 'total_bridges' => count($bridgeClassNames),
+ ]));
+
+ // TODO: The rendered template could be cached, but beware config changes that changes the html
+ return $response;
}
}
diff --git a/actions/HealthAction.php b/actions/HealthAction.php
index a38879c2..13365a3c 100644
--- a/actions/HealthAction.php
+++ b/actions/HealthAction.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
class HealthAction implements ActionInterface
{
- public function execute(Request $request)
+ public function __invoke(Request $request): Response
{
$response = [
'code' => 200,
diff --git a/actions/ListAction.php b/actions/ListAction.php
index 3d9cdd73..f6347f9c 100644
--- a/actions/ListAction.php
+++ b/actions/ListAction.php
@@ -2,19 +2,25 @@
class ListAction implements ActionInterface
{
- public function execute(Request $request)
+ private BridgeFactory $bridgeFactory;
+
+ public function __construct(
+ BridgeFactory $bridgeFactory
+ ) {
+ $this->bridgeFactory = $bridgeFactory;
+ }
+
+ public function __invoke(Request $request): Response
{
$list = new \stdClass();
$list->bridges = [];
$list->total = 0;
- $bridgeFactory = new BridgeFactory();
-
- foreach ($bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
- $bridge = $bridgeFactory->create($bridgeClassName);
+ foreach ($this->bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
+ $bridge = $this->bridgeFactory->create($bridgeClassName);
$list->bridges[$bridgeClassName] = [
- 'status' => $bridgeFactory->isEnabled($bridgeClassName) ? 'active' : 'inactive',
+ 'status' => $this->bridgeFactory->isEnabled($bridgeClassName) ? 'active' : 'inactive',
'uri' => $bridge->getURI(),
'donationUri' => $bridge->getDonationURI(),
'name' => $bridge->getName(),
diff --git a/bin/cache-clear b/bin/cache-clear
index 3563abad..2ca84ce6 100755
--- a/bin/cache-clear
+++ b/bin/cache-clear
@@ -6,9 +6,11 @@
*/
require __DIR__ . '/../lib/bootstrap.php';
+require __DIR__ . '/../lib/config.php';
-$rssBridge = new RssBridge();
+$container = require __DIR__ . '/../lib/dependencies.php';
-$cache = RssBridge::getCache();
+/** @var CacheInterface $cache */
+$cache = $container['cache'];
$cache->clear();
diff --git a/bin/cache-prune b/bin/cache-prune
index 7b7a6031..bb72c4ac 100755
--- a/bin/cache-prune
+++ b/bin/cache-prune
@@ -6,9 +6,19 @@
*/
require __DIR__ . '/../lib/bootstrap.php';
+require __DIR__ . '/../lib/config.php';
-$rssBridge = new RssBridge();
+$container = require __DIR__ . '/../lib/dependencies.php';
-$cache = RssBridge::getCache();
+if (
+ Configuration::getConfig('cache', 'type') === 'file'
+ && !Configuration::getConfig('FileCache', 'enable_purge')
+) {
+ // Override enable_purge for this particular execution
+ Configuration::setConfig('FileCache', 'enable_purge', true);
+}
+
+/** @var CacheInterface $cache */
+$cache = $container['cache'];
$cache->prune();
diff --git a/bin/test b/bin/test
new file mode 100755
index 00000000..74692410
--- /dev/null
+++ b/bin/test
@@ -0,0 +1,20 @@
+#!/usr/bin/env php
+debug('This is a test debug message');
+
+$logger->info('This is a test info message');
+
+$logger->error('This is a test error message');
diff --git a/bridges/ABCNewsBridge.php b/bridges/ABCNewsBridge.php
index c00fed1c..154eb489 100644
--- a/bridges/ABCNewsBridge.php
+++ b/bridges/ABCNewsBridge.php
@@ -31,17 +31,17 @@ class ABCNewsBridge extends BridgeAbstract
{
$url = sprintf('https://www.abc.net.au/news/%s', $this->getInput('topic'));
$dom = getSimpleHTMLDOM($url);
- $dom = $dom->find('div[data-component="CardList"]', 0);
+ $dom = $dom->find('div[data-component="PaginationList"]', 0);
if (!$dom) {
throw new \Exception(sprintf('Unable to find css selector on `%s`', $url));
}
$dom = defaultLinkTo($dom, $this->getURI());
- foreach ($dom->find('div[data-component="GenericCard"]') as $article) {
+ foreach ($dom->find('article[data-component="DetailCard"]') as $article) {
$a = $article->find('a', 0);
$this->items[] = [
'title' => $a->plaintext,
'uri' => $a->href,
- 'content' => $article->find('[data-component="CardDescription"]', 0)->plaintext,
+ 'content' => $article->find('p', 0)->plaintext,
'timestamp' => strtotime($article->find('time', 0)->datetime),
];
}
diff --git a/bridges/AO3Bridge.php b/bridges/AO3Bridge.php
index e30c6b70..2697dbc7 100644
--- a/bridges/AO3Bridge.php
+++ b/bridges/AO3Bridge.php
@@ -12,9 +12,29 @@ class AO3Bridge extends BridgeAbstract
'url' => [
'name' => 'url',
'required' => true,
- // Example: F/F tag, complete works only
- 'exampleValue' => 'https://archiveofourown.org/works?work_search[complete]=T&tag_id=F*s*F',
+ // Example: F/F tag
+ 'exampleValue' => 'https://archiveofourown.org/tags/F*s*F/works',
],
+ 'range' => [
+ 'name' => 'Chapter Content',
+ 'title' => 'Chapter(s) to include in each work\'s feed entry',
+ 'defaultValue' => null,
+ 'type' => 'list',
+ 'values' => [
+ 'None' => null,
+ 'First' => 'first',
+ 'Latest' => 'last',
+ 'Entire work' => 'all',
+ ],
+ ],
+ 'unique' => [
+ 'name' => 'Make separate entries for new fic chapters',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'title' => 'Make separate entries for new fic chapters',
+ 'defaultValue' => 'checked',
+ ],
+ 'limit' => self::LIMIT,
],
'Bookmarks' => [
'user' => [
@@ -39,18 +59,13 @@ class AO3Bridge extends BridgeAbstract
{
switch ($this->queriedContext) {
case 'Bookmarks':
- $user = $this->getInput('user');
- $this->title = $user;
- $url = self::URI
- . '/users/' . $user
- . '/bookmarks?bookmark_search[sort_column]=bookmarkable_date';
- $this->collectList($url);
+ $this->collectList($this->getURI());
break;
case 'List':
- $this->collectList($this->getInput('url'));
+ $this->collectList($this->getURI());
break;
case 'Work':
- $this->collectWork($this->getInput('id'));
+ $this->collectWork($this->getURI());
break;
}
}
@@ -61,9 +76,24 @@ class AO3Bridge extends BridgeAbstract
*/
private function collectList($url)
{
- $html = getSimpleHTMLDOM($url);
+ $version = 'v0.0.1';
+ $headers = [
+ "useragent: rss-bridge $version (https://github.com/RSS-Bridge/rss-bridge)"
+ ];
+ $response = getContents($url, $headers);
+
+ $html = \str_get_html($response);
$html = defaultLinkTo($html, self::URI);
+ // Get list title. Will include page range + count in some cases
+ $heading = ($html->find('#main h2', 0));
+ if ($heading->find('a.tag')) {
+ $heading = $heading->find('a.tag', 0);
+ }
+ $this->title = $heading->plaintext;
+
+ $limit = $this->getInput('limit') ?? 3;
+ $count = 0;
foreach ($html->find('.index.group > li') as $element) {
$item = [];
@@ -72,16 +102,70 @@ class AO3Bridge extends BridgeAbstract
continue; // discard deleted works
}
$item['title'] = $title->plaintext;
- $item['content'] = $element;
$item['uri'] = $title->href;
$strdate = $element->find('div p.datetime', 0)->plaintext;
$item['timestamp'] = strtotime($strdate);
+ // detach from rest of page because remove() is buggy
+ $element = str_get_html($element->outertext());
+ $tags = $element->find('ul.required-tags', 0);
+ foreach ($tags->childNodes() as $tag) {
+ $item['categories'][] = html_entity_decode($tag->plaintext);
+ }
+ $tags->remove();
+ $tags = $element->find('ul.tags', 0);
+ foreach ($tags->childNodes() as $tag) {
+ $item['categories'][] = html_entity_decode($tag->plaintext);
+ }
+ $tags->remove();
+
+ $item['content'] = implode('', $element->childNodes());
+
$chapters = $element->find('dl dd.chapters', 0);
// bookmarked series and external works do not have a chapters count
$chapters = (isset($chapters) ? $chapters->plaintext : 0);
- $item['uid'] = $item['uri'] . "/$strdate/$chapters";
+ if ($this->getInput('unique')) {
+ $item['uid'] = $item['uri'] . "/$strdate/$chapters";
+ } else {
+ $item['uid'] = $item['uri'];
+ }
+
+
+ // Fetch workskin of desired chapter(s) in list
+ if ($this->getInput('range') && ($limit == 0 || $count++ < $limit)) {
+ $url = $item['uri'];
+ switch ($this->getInput('range')) {
+ case ('all'):
+ $url .= '?view_full_work=true';
+ break;
+ case ('first'):
+ break;
+ case ('last'):
+ // only way to get this is using the navigate page unfortunately
+ $url .= '/navigate';
+ $response = getContents($url, $headers);
+ $html = \str_get_html($response);
+ $html = defaultLinkTo($html, self::URI);
+ $url = $html->find('ol.index.group > li > a', -1)->href;
+ break;
+ }
+ $response = getContents($url, $headers);
+
+ $html = \str_get_html($response);
+ $html = defaultLinkTo($html, self::URI);
+ // remove duplicate fic summary
+ if ($ficsum = $html->find('#workskin > .preface > .summary', 0)) {
+ $ficsum->remove();
+ }
+ $item['content'] .= $html->find('#workskin', 0);
+ }
+
+ // Use predictability of download links to generate enclosures
+ $wid = explode('/', $item['uri'])[4];
+ foreach (['azw3', 'epub', 'mobi', 'pdf', 'html'] as $ext) {
+ $item['enclosures'][] = 'https://archiveofourown.org/downloads/' . $wid . '/work.' . $ext;
+ }
$this->items[] = $item;
}
@@ -90,26 +174,31 @@ class AO3Bridge extends BridgeAbstract
/**
* Feed for recent chapters of a specific work.
*/
- private function collectWork($id)
+ private function collectWork($url)
{
- $url = self::URI . "/works/$id/navigate";
- $httpClient = RssBridge::getHttpClient();
-
$version = 'v0.0.1';
- $response = $httpClient->request($url, [
- 'useragent' => "rss-bridge $version (https://github.com/RSS-Bridge/rss-bridge)",
- ]);
+ $headers = [
+ "useragent: rss-bridge $version (https://github.com/RSS-Bridge/rss-bridge)"
+ ];
+ $response = getContents($url . '/navigate', $headers);
- $html = \str_get_html($response->getBody());
+ $html = \str_get_html($response);
$html = defaultLinkTo($html, self::URI);
+ $response = getContents($url . '?view_full_work=true', $headers);
+
+ $workhtml = \str_get_html($response);
+ $workhtml = defaultLinkTo($workhtml, self::URI);
+
$this->title = $html->find('h2 a', 0)->plaintext;
- foreach ($html->find('ol.index.group > li') as $element) {
+ $nav = $html->find('ol.index.group > li');
+ for ($i = 0; $i < count($nav); $i++) {
$item = [];
+ $element = $nav[$i];
$item['title'] = $element->find('a', 0)->plaintext;
- $item['content'] = $element;
+ $item['content'] = $workhtml->find('#chapter-' . ($i + 1), 0);
$item['uri'] = $element->find('a', 0)->href;
$strdate = $element->find('span.datetime', 0)->plaintext;
@@ -138,4 +227,24 @@ class AO3Bridge extends BridgeAbstract
{
return self::URI . '/favicon.ico';
}
+
+ public function getURI()
+ {
+ $url = parent::getURI();
+ switch ($this->queriedContext) {
+ case 'Bookmarks':
+ $user = $this->getInput('user');
+ $url = self::URI
+ . '/users/' . $user
+ . '/bookmarks?bookmark_search[sort_column]=bookmarkable_date';
+ break;
+ case 'List':
+ $url = $this->getInput('url');
+ break;
+ case 'Work':
+ $url = self::URI . '/works/' . $this->getInput('id');
+ break;
+ }
+ return $url;
+ }
}
diff --git a/bridges/ARDAudiothekBridge.php b/bridges/ARDAudiothekBridge.php
index 02b6b007..e6b82775 100644
--- a/bridges/ARDAudiothekBridge.php
+++ b/bridges/ARDAudiothekBridge.php
@@ -71,7 +71,7 @@ class ARDAudiothekBridge extends BridgeAbstract
$pathComponents = explode('/', $path);
if (empty($pathComponents)) {
- returnClientError('Path may not be empty');
+ throwClientException('Path may not be empty');
}
if (count($pathComponents) < 2) {
$showID = $pathComponents[0];
diff --git a/bridges/ARDMediathekBridge.php b/bridges/ARDMediathekBridge.php
index 6de8dad7..6f7272dc 100644
--- a/bridges/ARDMediathekBridge.php
+++ b/bridges/ARDMediathekBridge.php
@@ -40,6 +40,11 @@ class ARDMediathekBridge extends BridgeAbstract
* @const IMAGEWIDTHPLACEHOLDER
*/
const IMAGEWIDTHPLACEHOLDER = '{width}';
+ /**
+ * Title of the current show
+ * @var string
+ */
+ private $title;
const PARAMETERS = [
[
@@ -60,7 +65,7 @@ class ARDMediathekBridge extends BridgeAbstract
$pathComponents = explode('/', $this->getInput('path'));
if (empty($pathComponents)) {
- returnClientError('Path may not be empty');
+ throwClientException('Path may not be empty');
}
if (count($pathComponents) < 2) {
$showID = $pathComponents[0];
@@ -72,7 +77,7 @@ class ARDMediathekBridge extends BridgeAbstract
}
}
- $url = self::APIENDPOINT . $showID . '/?pageSize=' . self::PAGESIZE;
+ $url = self::APIENDPOINT . $showID . '?pageSize=' . self::PAGESIZE;
$rawJSON = getContents($url);
$processedJSON = json_decode($rawJSON);
@@ -93,6 +98,17 @@ class ARDMediathekBridge extends BridgeAbstract
$this->items[] = $item;
}
+ $this->title = $processedJSON->title;
+
date_default_timezone_set($oldTz);
}
+
+ /** {@inheritdoc} */
+ public function getName()
+ {
+ if (!empty($this->title)) {
+ return $this->title;
+ }
+ return parent::getName();
+ }
}
diff --git a/bridges/ActivisionResearchBridge.php b/bridges/ActivisionResearchBridge.php
new file mode 100644
index 00000000..88af4b46
--- /dev/null
+++ b/bridges/ActivisionResearchBridge.php
@@ -0,0 +1,45 @@
+find('div[id="home-blog-feed"]', 0);
+ if (!$dom) {
+ throw new \Exception(sprintf('Unable to find css selector on `%s`', $url));
+ }
+ $dom = defaultLinkTo($dom, $this->getURI());
+ foreach ($dom->find('div[class="blog-entry"]') as $article) {
+ $a = $article->find('a', 0);
+
+ $blogimg = extractFromDelimiters($article->find('div[class="blog-img"]', 0)->style, 'url(', ')');
+
+ $title = htmlspecialchars_decode($article->find('div[class="title"]', 0)->plaintext);
+ $author = htmlspecialchars_decode($article->find('div[class="author]', 0)->plaintext);
+ $date = $article->find('div[class="pubdate"]', 0)->plaintext;
+
+ $entry = getSimpleHTMLDOMCached($a->href, static::CACHE_TIMEOUT * 7 * 4);
+ $entry = defaultLinkTo($entry, $this->getURI());
+
+ $content = $entry->find('div[class="blog-body"]', 0);
+ $tagsremove = ['script', 'iframe', 'input', 'form'];
+ $content = sanitize($content, $tagsremove);
+ $content = '
' . $content;
+
+ $this->items[] = [
+ 'title' => $title,
+ 'author' => $author,
+ 'uri' => $a->href,
+ 'content' => $content,
+ 'timestamp' => strtotime($date),
+ ];
+ }
+ }
+}
diff --git a/bridges/AirBreizhBridge.php b/bridges/AirBreizhBridge.php
index a822625f..272c74ee 100644
--- a/bridges/AirBreizhBridge.php
+++ b/bridges/AirBreizhBridge.php
@@ -32,8 +32,7 @@ class AirBreizhBridge extends BridgeAbstract
public function collectData()
{
$html = '';
- $html = getSimpleHTMLDOM(static::URI . 'publications/?fwp_publications_thematiques=' . $this->getInput('theme'))
- or returnClientError('No results for this query.');
+ $html = getSimpleHTMLDOM(static::URI . 'publications/?fwp_publications_thematiques=' . $this->getInput('theme'));
foreach ($html->find('article') as $article) {
$item = [];
diff --git a/bridges/AllegroBridge.php b/bridges/AllegroBridge.php
index 7cad11f1..3509afa1 100644
--- a/bridges/AllegroBridge.php
+++ b/bridges/AllegroBridge.php
@@ -13,13 +13,10 @@ class AllegroBridge extends BridgeAbstract
'exampleValue' => 'https://allegro.pl/kategoria/swieze-warzywa-cebula-318660',
'required' => true,
],
- 'sessioncookie' => [
- 'name' => 'The \'wdctx\' session cookie',
- 'title' => 'Paste the value of the \'wdctx\' cookie from your browser if you want to prevent Allegro imposing rate limits',
- 'pattern' => '^.{70,};?$',
- // phpcs:ignore
- 'exampleValue' => 'v4.1-oCrmXTMqv2ppC21GTUCKLmUwRPP1ssQVALKuqwsZ1VXjcKgL2vO5TTRM5xMxS9GiyqxF1gAeyc-63dl0coUoBKXCXi_nAmr95yyqGpq2RAFoneZ4L399E8n6iYyemcuGARjAoSfjvLHJCEwvvHHynSgaxlFBu7hUnKfuy39zo9sSQdyTUjotJg3CAZ53q9v2raAnPCyGOAR4ytRILd9p24EJnxp7_oR0XbVPIo1hDa4WmjXFOxph8rHaO5tWd',
- 'required' => false,
+ 'cookie' => [
+ 'name' => 'The complete cookie value',
+ 'title' => 'Paste the cookie value from your browser, otherwise 403 gets returned',
+ 'required' => true,
],
'includeSponsoredOffers' => [
'type' => 'checkbox',
@@ -68,93 +65,56 @@ class AllegroBridge extends BridgeAbstract
$url = preg_replace('/([?&])order=[^&]+(&|$)/', '$1', $this->getInput('url'));
$url .= (parse_url($url, PHP_URL_QUERY) ? '&' : '?') . 'order=n';
- $opts = [];
+ $html = getContents($url, [], [CURLOPT_COOKIE => $this->getInput('cookie')]);
- // If a session cookie is provided
- if ($sessioncookie = $this->getInput('sessioncookie')) {
- $opts[CURLOPT_COOKIE] = 'wdctx=' . $sessioncookie;
+ $storeData = null;
+ if (preg_match('/