summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohannes 'josch' Schauer <josch@debian.org>2018-12-16 18:39:01 +0100
committerJohannes 'josch' Schauer <josch@debian.org>2018-12-16 18:39:01 +0100
commit16703e11d73e73861fffef276c5d6fe761bec683 (patch)
tree5814952282a42b0fdf8a99e583fb22acf9411550
parent8aedb2a478db0e9aa29deaefcc9affa21046fdad (diff)
import upstream version 2018-12-11
-rw-r--r--.dockerignore7
-rw-r--r--.gitattributes22
-rw-r--r--.github/CONTRIBUTING.md47
-rw-r--r--.github/ISSUE_TEMPLATE/bridge-request-template.md61
-rw-r--r--.gitignore238
-rw-r--r--.travis.yml45
-rw-r--r--Dockerfile5
-rw-r--r--README.md172
-rw-r--r--bridges/CrewbayBridge.php82
-rw-r--r--bridges/FB2Bridge.php2
-rw-r--r--bridges/FacebookBridge.php18
-rw-r--r--bridges/FilterBridge.php2
-rw-r--r--bridges/GithubIssueBridge.php141
-rw-r--r--[-rwxr-xr-x]bridges/GlassdoorBridge.php0
-rw-r--r--bridges/JustETFBridge.php8
-rw-r--r--bridges/MozillaSecurity.php28
-rw-r--r--bridges/PixivBridge.php2
-rw-r--r--bridges/SoundcloudBridge.php10
-rw-r--r--bridges/TwitterBridge.php47
-rw-r--r--bridges/WordPressBridge.php2
-rw-r--r--bridges/WordPressPluginUpdateBridge.php4
-rw-r--r--bridges/XenForoBridge.php2
-rw-r--r--bridges/YGGTorrentBridge.php1
-rw-r--r--bridges/ZoneTelechargementBridge.php17
-rw-r--r--cache/.gitkeep0
-rw-r--r--index.php124
-rw-r--r--lib/Authentication.php57
-rw-r--r--lib/Bridge.php306
-rw-r--r--lib/BridgeAbstract.php128
-rw-r--r--lib/BridgeCard.php114
-rw-r--r--lib/BridgeInterface.php61
-rw-r--r--lib/BridgeList.php85
-rw-r--r--lib/Cache.php137
-rw-r--r--lib/CacheInterface.php44
-rw-r--r--lib/Configuration.php158
-rw-r--r--lib/Debug.php121
-rw-r--r--lib/Exceptions.php78
-rw-r--r--lib/FeedExpander.php226
-rw-r--r--lib/Format.php167
-rw-r--r--lib/FormatAbstract.php123
-rw-r--r--lib/FormatInterface.php73
-rw-r--r--lib/ParameterValidator.php72
-rw-r--r--lib/RssBridge.php37
-rw-r--r--lib/contents.php198
-rw-r--r--lib/error.php49
-rw-r--r--lib/html.php141
-rw-r--r--lib/rssbridge.php79
-rw-r--r--phpcompatibility.xml47
-rw-r--r--phpcs.xml84
-rw-r--r--phpunit.xml16
-rw-r--r--scalingo.json6
-rw-r--r--tests/BridgeImplementationTest.php191
52 files changed, 2494 insertions, 1391 deletions
diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index f2bc0e8..0000000
--- a/.dockerignore
+++ /dev/null
@@ -1,7 +0,0 @@
-.git
-cache/*
-DEBUG
-Dockerfile
-whitelist.txt
-phpcs.xml
-CONTRIBUTING.md \ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 412eeda..0000000
--- a/.gitattributes
+++ /dev/null
@@ -1,22 +0,0 @@
-# Auto detect text files and perform LF normalization
-* text=auto
-
-# Custom for Visual Studio
-*.cs diff=csharp
-*.sln merge=union
-*.csproj merge=union
-*.vbproj merge=union
-*.fsproj merge=union
-*.dbproj merge=union
-
-# Standard to msysgit
-*.doc diff=astextplain
-*.DOC diff=astextplain
-*.docx diff=astextplain
-*.DOCX diff=astextplain
-*.dot diff=astextplain
-*.DOT diff=astextplain
-*.pdf diff=astextplain
-*.PDF diff=astextplain
-*.rtf diff=astextplain
-*.RTF diff=astextplain
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
deleted file mode 100644
index 18fc93f..0000000
--- a/.github/CONTRIBUTING.md
+++ /dev/null
@@ -1,47 +0,0 @@
-### Pull request policy
-
-* [Fix one issue per pull request](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#fix-one-issue-per-pull-request)
-* [Respect the coding style policy](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#respect-the-coding-style-policy)
-* [Properly name your commits](https://github.com/RSS-Bridge/rss-bridge/wiki/Pull-request-policy#properly-name-your-commits)
- * When fixing a bridge (located in the `bridges` directory), write `[BridgeName] Feature` <br>(i.e. `[YoutubeBridge] Fix typo in video titles`).
- * When fixing other files, use `[FileName] Feature` <br>(i.e. `[index.php] Add multilingual support`).
- * When fixing a general problem that applies to multiple files, write `category: feature` <br>(i.e. `bridges: Fix various typos`).
-
-Note that all pull-requests must pass all tests before they can be merged.
-
-### Coding style
-
-* [Whitespace](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace)
- * [Add a new line at the end of a file](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#add-a-new-line-at-the-end-of-a-file)
- * [Do not add a whitespace before a semicolon](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#add-a-new-line-at-the-end-of-a-file)
- * [Do not add whitespace at start or end of a file or end of a line](https://github.com/RSS-Bridge/rss-bridge/wiki/Whitespace#do-not-add-whitespace-at-start-or-end-of-a-file-or-end-of-a-line)
-* [Indentation](https://github.com/RSS-Bridge/rss-bridge/wiki/Indentation)
- * [Use tabs for indentation](https://github.com/RSS-Bridge/rss-bridge/wiki/Indentation#use-tabs-for-indentation)
-* [Maximum line length](https://github.com/RSS-Bridge/rss-bridge/wiki/Maximum-line-length)
- * [The maximum line length should not exceed 80 characters](https://github.com/RSS-Bridge/rss-bridge/wiki/Maximum-line-length#the-maximum-line-length-should-not-exceed-80-characters)
-* [Strings](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings)
- * [Whenever possible use single quoted strings](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#whenever-possible-use-single-quote-strings)
- * [Add spaces around the concatenation operator](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#add-spaces-around-the-concatenation-operator)
- * [Use a single string instead of concatenating](https://github.com/RSS-Bridge/rss-bridge/wiki/Strings#use-a-single-string-instead-of-concatenating)
-* [Constants](https://github.com/RSS-Bridge/rss-bridge/wiki/Constants)
- * [Use UPPERCASE for constants](https://github.com/RSS-Bridge/rss-bridge/wiki/Constants#use-uppercase-for-constants)
-* [Keywords](https://github.com/RSS-Bridge/rss-bridge/wiki/Keywords)
- * [Use lowercase for `true`, `false` and `null`](https://github.com/RSS-Bridge/rss-bridge/wiki/Keywords#use-lowercase-for-true-false-and-null)
-* [Operators](https://github.com/RSS-Bridge/rss-bridge/wiki/Operators)
- * [Operators must have a space around them](https://github.com/RSS-Bridge/rss-bridge/wiki/Operators#operators-must-have-a-space-around-them)
-* [Functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions)
- * [Parameters with default values must appear last in functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#parameters-with-default-values-must-appear-last-in-functions)
- * [Calling functions](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#calling-functions)
- * [Do not add spaces after opening or before closing bracket](https://github.com/RSS-Bridge/rss-bridge/wiki/Functions#do-not-add-spaces-after-opening-or-before-closing-bracket)
-* [Structures](https://github.com/RSS-Bridge/rss-bridge/wiki/Structures)
- * [Structures must always be formatted as multi-line blocks](https://github.com/RSS-Bridge/rss-bridge/wiki/Structures#structures-must-always-be-formatted-as-multi-line-blocks)
-* [If-Statement](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement)
- * [Use `elseif` instead of `else if`](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#use-elseif-instead-of-else-if)
- * [Do not write empty statements](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#do-not-write-empty-statements)
- * [Do not write unconditional if-statements](https://github.com/RSS-Bridge/rss-bridge/wiki/if-Statement#do-not-write-unconditional-if-statements)
-* [Classes](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes)
- * [Use PascalCase for class names](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#use-pascalcase-for-class-names)
- * [Do not use final statements inside final classes](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#do-not-use-final-statements-inside-final-classes)
- * [Do not override methods to call their parent](https://github.com/RSS-Bridge/rss-bridge/wiki/Classes#do-not-override-methods-to-call-their-parent)
-* [Casting](https://github.com/RSS-Bridge/rss-bridge/wiki/Casting)
- * [Do not add spaces when casting](https://github.com/RSS-Bridge/rss-bridge/wiki/Casting#do-not-add-spaces-when-casting)
diff --git a/.github/ISSUE_TEMPLATE/bridge-request-template.md b/.github/ISSUE_TEMPLATE/bridge-request-template.md
deleted file mode 100644
index f4b1119..0000000
--- a/.github/ISSUE_TEMPLATE/bridge-request-template.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-name: Bridge request template
-about: Use this template for requesting a new bridge
-
----
-
-# Bridge request
-
-<!--
-This is a bridge request. Start by adding a descriptive title (i.e. `Bridge request for GitHub`). Use the "Preview" button to see a preview of your request. Make sure your request is complete before submitting!
-
-Notice: This comment is only visible to you while you work on your request. Please do not remove any of the lines in the template (you may add your own outside the "<!--" and "- ->" lines!)
--->
-
-## General information
-
-<!--
-Please describe what you expect from the bridge. Whenever possible provide sample links and screenshots (you can just paste them here) to express your expectations and help others understand your request. If possible, mark relevant areas in your screenshot. Use the following questions for reference:
--->
-
-- _Host URI for the bridge_ (i.e. `https://github.com`):
-
-- Which information would you like to see?
-
-
-
-- How should the information be displayed/formatted?
-
-
-
-- Which of the following parameters do you expect?
-
- - [X] Title
- - [X] URI (link to the original article)
- - [ ] Author
- - [ ] Timestamp
- - [X] Content (the content of the article)
- - [ ] Enclosures (pictures, videos, etc...)
- - [ ] Categories (categories, tags, etc...)
-
-## Options
-
-<!--Select options from the list below. Add your own option if one is missing:-->
-
-- [ ] Limit number of returned items
- - _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
-- [X] Balance requests (RSS-Bridge uses cached versions to reduce bandwith usage)
- - _Timeout_ (default = 5 minutes, max = 24 hours): 5 minutes
-
-<!--Be aware that some options might not be available for your specific request due to technical limitations!-->
-
-<!--
-## Additional notes
-
-Keep in mind that opening a request does not guarantee the bridge being implemented! That depends entirely on the interest and time of others to make the bridge for you.
-
-You can also implement your own bridge (with support of the community if needed). Find more information in the [RSS-Bridge Wiki](https://github.com/RSS-Bridge/rss-bridge/wiki/For-developers) developer section.
--->
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index a848b5f..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,238 +0,0 @@
-#################
-## Eclipse
-#################
-vendor/*
-data/
-*.pydevproject
-.project
-.metadata
-bin/
-tmp/
-*.tmp
-*.bak
-*.swp
-*~.nib
-local.properties
-.classpath
-.settings/
-.loadpath
-
-# External tool builders
-.externalToolBuilders/
-
-# Locally stored "Eclipse launch configurations"
-*.launch
-
-# CDT-specific
-.cproject
-
-# PDT-specific
-.buildpath
-
-
-#################
-## Visual Studio
-#################
-
-## Ignore Visual Studio temporary files, build results, and
-## files generated by popular Visual Studio add-ons.
-
-# User-specific files
-*.suo
-*.user
-*.sln.docstates
-
-# Build results
-
-[Dd]ebug/
-[Rr]elease/
-x64/
-build/
-[Bb]in/
-[Oo]bj/
-
-# MSTest test Results
-[Tt]est[Rr]esult*/
-[Bb]uild[Ll]og.*
-
-*_i.c
-*_p.c
-*.ilk
-*.meta
-*.obj
-*.pch
-*.pdb
-*.pgc
-*.pgd
-*.rsp
-*.sbr
-*.tlb
-*.tli
-*.tlh
-*.tmp
-*.tmp_proj
-*.log
-*.vspscc
-*.vssscc
-.builds
-*.pidb
-*.log
-*.scc
-
-# Visual C++ cache files
-ipch/
-*.aps
-*.ncb
-*.opensdf
-*.sdf
-*.cachefile
-
-# Visual Studio profiler
-*.psess
-*.vsp
-*.vspx
-
-# Guidance Automation Toolkit
-*.gpState
-
-# ReSharper is a .NET coding add-in
-_ReSharper*/
-*.[Rr]e[Ss]harper
-
-# TeamCity is a build add-in
-_TeamCity*
-
-# DotCover is a Code Coverage Tool
-*.dotCover
-
-# NCrunch
-*.ncrunch*
-.*crunch*.local.xml
-
-# Installshield output folder
-[Ee]xpress/
-
-# DocProject is a documentation generator add-in
-DocProject/buildhelp/
-DocProject/Help/*.HxT
-DocProject/Help/*.HxC
-DocProject/Help/*.hhc
-DocProject/Help/*.hhk
-DocProject/Help/*.hhp
-DocProject/Help/Html2
-DocProject/Help/html
-
-# Click-Once directory
-publish/
-
-# Publish Web Output
-*.Publish.xml
-*.pubxml
-
-# NuGet Packages Directory
-## TODO: If you have NuGet Package Restore enabled, uncomment the next line
-#packages/
-
-# Windows Azure Build Output
-csx
-*.build.csdef
-
-# Windows Store app package directory
-AppPackages/
-
-# Others
-sql/
-*.Cache
-ClientBin/
-[Ss]tyle[Cc]op.*
-~$*
-*~
-*.dbmdl
-*.[Pp]ublish.xml
-*.pfx
-*.publishsettings
-
-# RIA/Silverlight projects
-Generated_Code/
-
-# Backup & report files from converting an old project file to a newer
-# Visual Studio version. Backup files are not needed, because we have git ;-)
-_UpgradeReport_Files/
-Backup*/
-UpgradeLog*.XML
-UpgradeLog*.htm
-
-# SQL Server files
-App_Data/*.mdf
-App_Data/*.ldf
-
-#################
-## Other ide stuff
-#################
-.idea/*
-[#]*[#]
-
-#############
-## Windows detritus
-#############
-
-# Windows image file caches
-Thumbs.db
-ehthumbs.db
-
-# Folder config file
-Desktop.ini
-
-# Recycle Bin used on file shares
-$RECYCLE.BIN/
-
-# Mac crap
-.DS_Store
-
-
-#############
-## Python
-#############
-
-*.py[co]
-
-# Packages
-*.egg
-*.egg-info
-dist/
-build/
-eggs/
-parts/
-var/
-sdist/
-develop-eggs/
-.installed.cfg
-
-# Installer logs
-pip-log.txt
-
-# Unit test / coverage reports
-.coverage
-.tox
-
-#Translations
-*.mo
-
-#Mr Developer
-.mr.developer.cfg
-
-##############
-## RSS-Bridge
-##############
-/cache
-/whitelist.txt
-DEBUG
-config.ini.php
-
-######################
-## VisualStudioCode ##
-######################
-.vscode/*
-
-#Builder
-.buildconfig
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 80f141b..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,45 +0,0 @@
-dist: trusty
-sudo: false
-language: php
-
-install:
- - composer global require dealerdirect/phpcodesniffer-composer-installer;
- - composer global require phpcompatibility/php-compatibility;
- # Use PHPUnit 6 for unit tests (stable), requires PHP 7
- - if [[ $TRAVIS_PHP_VERSION == "7.0" ]]; then
- composer global require phpunit/phpunit ^6;
- fi
- # Use latest PHPUnit on nightly to detect breaking changes
- - if [[ $TRAVIS_PHP_VERSION == "nightly" ]]; then
- composer global require phpunit/phpunit;
- fi
-
-script:
- - phpenv rehash
- # Run PHP_CodeSniffer on all versions
- - ~/.composer/vendor/bin/phpcs . --standard=phpcs.xml --warning-severity=0 --extensions=php -p;
- # Check PHP compatibility for the lowest supported version
- - if [[ $TRAVIS_PHP_VERSION == "5.6" ]]; then
- ~/.composer/vendor/bin/phpcs . --standard=phpcompatibility.xml --warning-severity=0 --extensions=php -p;
- fi
- # Run unit tests (stable)
- - if [[ $TRAVIS_PHP_VERSION == "7.0" ]]; then
- phpunit --configuration=phpunit.xml --include-path=lib/;
- fi
- # Run unit tests (latest/nightly)
- # Check PHP compatibility for all versions, starting at the lowest supported version in order to detect breaking changes
- - if [[ $TRAVIS_PHP_VERSION == "nightly" ]]; then
- phpunit --configuration=phpunit.xml --include-path=lib/;
- ~/.composer/vendor/bin/phpcs . --standard=PHPCompatibility --warning-severity=0 --extensions=php -p --runtime-set testVersion 5.6-;
- fi
-
-matrix:
- fast_finish: true
-
- include:
- - php: 5.6
- - php: 7.0
- - php: nightly
-
- allow_failures:
- - php: nightly
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 35caac8..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,5 +0,0 @@
-FROM ulsmith/alpine-apache-php7
-
-COPY ./ /app/public/
-
-RUN chown -R apache:root /app/public \ No newline at end of file
diff --git a/README.md b/README.md
index b72bb6a..8229aa7 100644
--- a/README.md
+++ b/README.md
@@ -110,88 +110,96 @@ Use this script to generate the list automatically (using the GitHub API):
https://gist.github.com/LogMANOriginal/da00cd1e5f0ca31cef8e193509b17fd8
-->
- * [16mhz](https://api.github.com/users/16mhz)
- * [Ahiles3005](https://api.github.com/users/Ahiles3005)
- * [Albirew](https://api.github.com/users/Albirew)
- * [AmauryCarrade](https://api.github.com/users/AmauryCarrade)
- * [ArthurHoaro](https://api.github.com/users/ArthurHoaro)
- * [Astalaseven](https://api.github.com/users/Astalaseven)
- * [Astyan-42](https://api.github.com/users/Astyan-42)
- * [Daiyousei](https://api.github.com/users/Daiyousei)
- * [Djuuu](https://api.github.com/users/Djuuu)
- * [Draeli](https://api.github.com/users/Draeli)
- * [EtienneM](https://api.github.com/users/EtienneM)
- * [Frenzie](https://api.github.com/users/Frenzie)
- * [Ginko-Aloe](https://api.github.com/users/Ginko-Aloe)
- * [Glandos](https://api.github.com/users/Glandos)
- * [GregThib](https://api.github.com/users/GregThib)
- * [Grummfy](https://api.github.com/users/Grummfy)
- * [JackNUMBER](https://api.github.com/users/JackNUMBER)
- * [JeremyRand](https://api.github.com/users/JeremyRand)
- * [Jocker666z](https://api.github.com/users/Jocker666z)
- * [LogMANOriginal](https://api.github.com/users/LogMANOriginal)
- * [MonsieurPoutounours](https://api.github.com/users/MonsieurPoutounours)
- * [ORelio](https://api.github.com/users/ORelio)
- * [PaulVayssiere](https://api.github.com/users/PaulVayssiere)
- * [Piranhaplant](https://api.github.com/users/Piranhaplant)
- * [Riduidel](https://api.github.com/users/Riduidel)
- * [Strubbl](https://api.github.com/users/Strubbl)
- * [TheRadialActive](https://api.github.com/users/TheRadialActive)
- * [TwizzyDizzy](https://api.github.com/users/TwizzyDizzy)
- * [WalterBarrett](https://api.github.com/users/WalterBarrett)
- * [ZeNairolf](https://api.github.com/users/ZeNairolf)
- * [adamchainz](https://api.github.com/users/adamchainz)
- * [aledeg](https://api.github.com/users/aledeg)
- * [alexAubin](https://api.github.com/users/alexAubin)
- * [az5he6ch](https://api.github.com/users/az5he6ch)
- * [b1nj](https://api.github.com/users/b1nj)
- * [benasse](https://api.github.com/users/benasse)
- * [captn3m0](https://api.github.com/users/captn3m0)
- * [chemel](https://api.github.com/users/chemel)
- * [ckiw](https://api.github.com/users/ckiw)
- * [cnlpete](https://api.github.com/users/cnlpete)
- * [corenting](https://api.github.com/users/corenting)
- * [da2x](https://api.github.com/users/da2x)
- * [eMerzh](https://api.github.com/users/eMerzh)
- * [em92](https://api.github.com/users/em92)
- * [griffaurel](https://api.github.com/users/griffaurel)
- * [hunhejj](https://api.github.com/users/hunhejj)
- * [j0k3r](https://api.github.com/users/j0k3r)
- * [jdigilio](https://api.github.com/users/jdigilio)
- * [kranack](https://api.github.com/users/kranack)
- * [kraoc](https://api.github.com/users/kraoc)
- * [laBecasse](https://api.github.com/users/laBecasse)
- * [lagaisse](https://api.github.com/users/lagaisse)
- * [lalannev](https://api.github.com/users/lalannev)
- * [ldidry](https://api.github.com/users/ldidry)
- * [m0zes](https://api.github.com/users/m0zes)
- * [matthewseal](https://api.github.com/users/matthewseal)
- * [mcbyte-it](https://api.github.com/users/mcbyte-it)
- * [mdemoss](https://api.github.com/users/mdemoss)
- * [melangue](https://api.github.com/users/melangue)
- * [metaMMA](https://api.github.com/users/metaMMA)
- * [mickael-bertrand](https://api.github.com/users/mickael-bertrand)
- * [mitsukarenai](https://api.github.com/users/mitsukarenai)
- * [mro](https://api.github.com/users/mro)
- * [mxmehl](https://api.github.com/users/mxmehl)
- * [nel50n](https://api.github.com/users/nel50n)
- * [niawag](https://api.github.com/users/niawag)
- * [pellaeon](https://api.github.com/users/pellaeon)
- * [pit-fgfjiudghdf](https://api.github.com/users/pit-fgfjiudghdf)
- * [pitchoule](https://api.github.com/users/pitchoule)
- * [pmaziere](https://api.github.com/users/pmaziere)
- * [prysme01](https://api.github.com/users/prysme01)
- * [quentinus95](https://api.github.com/users/quentinus95)
- * [qwertygc](https://api.github.com/users/qwertygc)
- * [regisenguehard](https://api.github.com/users/regisenguehard)
- * [rogerdc](https://api.github.com/users/rogerdc)
- * [sebsauvage](https://api.github.com/users/sebsauvage)
- * [sublimz](https://api.github.com/users/sublimz)
- * [sysadminstory](https://api.github.com/users/sysadminstory)
- * [tameroski](https://api.github.com/users/tameroski)
- * [teromene](https://api.github.com/users/teromene)
- * [triatic](https://api.github.com/users/triatic)
- * [wtuuju](https://api.github.com/users/wtuuju)
+ * [16mhz](https://github.com/16mhz)
+ * [Ahiles3005](https://github.com/Ahiles3005)
+ * [Albirew](https://github.com/Albirew)
+ * [AmauryCarrade](https://github.com/AmauryCarrade)
+ * [AntoineTurmel](https://github.com/AntoineTurmel)
+ * [ArthurHoaro](https://github.com/ArthurHoaro)
+ * [Astalaseven](https://github.com/Astalaseven)
+ * [Astyan-42](https://github.com/Astyan-42)
+ * [Daiyousei](https://github.com/Daiyousei)
+ * [Djuuu](https://github.com/Djuuu)
+ * [Draeli](https://github.com/Draeli)
+ * [EtienneM](https://github.com/EtienneM)
+ * [Frenzie](https://github.com/Frenzie)
+ * [Ginko-Aloe](https://github.com/Ginko-Aloe)
+ * [Glandos](https://github.com/Glandos)
+ * [GregThib](https://github.com/GregThib)
+ * [Grummfy](https://github.com/Grummfy)
+ * [JackNUMBER](https://github.com/JackNUMBER)
+ * [JeremyRand](https://github.com/JeremyRand)
+ * [Jocker666z](https://github.com/Jocker666z)
+ * [LogMANOriginal](https://github.com/LogMANOriginal)
+ * [MonsieurPoutounours](https://github.com/MonsieurPoutounours)
+ * [Nono-m0le](https://github.com/Nono-m0le)
+ * [ORelio](https://github.com/ORelio)
+ * [PaulVayssiere](https://github.com/PaulVayssiere)
+ * [Piranhaplant](https://github.com/Piranhaplant)
+ * [Riduidel](https://github.com/Riduidel)
+ * [Roliga](https://github.com/Roliga)
+ * [Strubbl](https://github.com/Strubbl)
+ * [TheRadialActive](https://github.com/TheRadialActive)
+ * [TwizzyDizzy](https://github.com/TwizzyDizzy)
+ * [WalterBarrett](https://github.com/WalterBarrett)
+ * [ZeNairolf](https://github.com/ZeNairolf)
+ * [adamchainz](https://github.com/adamchainz)
+ * [aledeg](https://github.com/aledeg)
+ * [alexAubin](https://github.com/alexAubin)
+ * [az5he6ch](https://github.com/az5he6ch)
+ * [b1nj](https://github.com/b1nj)
+ * [benasse](https://github.com/benasse)
+ * [captn3m0](https://github.com/captn3m0)
+ * [chemel](https://github.com/chemel)
+ * [ckiw](https://github.com/ckiw)
+ * [cnlpete](https://github.com/cnlpete)
+ * [corenting](https://github.com/corenting)
+ * [couraudt](https://github.com/couraudt)
+ * [da2x](https://github.com/da2x)
+ * [disk0x](https://github.com/disk0x)
+ * [eMerzh](https://github.com/eMerzh)
+ * [em92](https://github.com/em92)
+ * [fluffy-critter](https://github.com/fluffy-critter)
+ * [griffaurel](https://github.com/griffaurel)
+ * [hunhejj](https://github.com/hunhejj)
+ * [j0k3r](https://github.com/j0k3r)
+ * [jdigilio](https://github.com/jdigilio)
+ * [kranack](https://github.com/kranack)
+ * [kraoc](https://github.com/kraoc)
+ * [laBecasse](https://github.com/laBecasse)
+ * [lagaisse](https://github.com/lagaisse)
+ * [lalannev](https://github.com/lalannev)
+ * [ldidry](https://github.com/ldidry)
+ * [m0zes](https://github.com/m0zes)
+ * [matthewseal](https://github.com/matthewseal)
+ * [mcbyte-it](https://github.com/mcbyte-it)
+ * [mdemoss](https://github.com/mdemoss)
+ * [melangue](https://github.com/melangue)
+ * [metaMMA](https://github.com/metaMMA)
+ * [mickael-bertrand](https://github.com/mickael-bertrand)
+ * [mitsukarenai](https://github.com/mitsukarenai)
+ * [mr-flibble](https://github.com/mr-flibble)
+ * [mro](https://github.com/mro)
+ * [mxmehl](https://github.com/mxmehl)
+ * [nel50n](https://github.com/nel50n)
+ * [niawag](https://github.com/niawag)
+ * [pellaeon](https://github.com/pellaeon)
+ * [pit-fgfjiudghdf](https://github.com/pit-fgfjiudghdf)
+ * [pitchoule](https://github.com/pitchoule)
+ * [pmaziere](https://github.com/pmaziere)
+ * [prysme01](https://github.com/prysme01)
+ * [quentinus95](https://github.com/quentinus95)
+ * [qwertygc](https://github.com/qwertygc)
+ * [regisenguehard](https://github.com/regisenguehard)
+ * [rogerdc](https://github.com/rogerdc)
+ * [sebsauvage](https://github.com/sebsauvage)
+ * [sublimz](https://github.com/sublimz)
+ * [sysadminstory](https://github.com/sysadminstory)
+ * [tameroski](https://github.com/tameroski)
+ * [teromene](https://github.com/teromene)
+ * [triatic](https://github.com/triatic)
+ * [wtuuju](https://github.com/wtuuju)
+ * [yardenac](https://github.com/yardenac)
Licenses
===
diff --git a/bridges/CrewbayBridge.php b/bridges/CrewbayBridge.php
index 6951777..a3c52b9 100644
--- a/bridges/CrewbayBridge.php
+++ b/bridges/CrewbayBridge.php
@@ -113,7 +113,33 @@ class CrewbayBridge extends BridgeAbstract {
foreach ($annonces as $annonce) {
$detail = $annonce->find('.btn--profile', 0);
- $htmlDetail = getSimpleHTMLDOMCached($detail->getAttribute('data-modal-href'));
+ $htmlDetail = getSimpleHTMLDOMCached($detail->href);
+
+ if (!empty($this->getInput('recreational_position')) || !empty($this->getInput('professional_position'))) {
+ if ($this->getInput('type') == 'boats') {
+ if ($this->getInput('status') == 'professional') {
+ $positions = array($annonce->find('.title .position', 0)->plaintext);
+ } else {
+ $positions = array(str_replace('Wanted:', '', $annonce->find('.content li', 0)->plaintext));
+ }
+ } else {
+ $list = $htmlDetail->find('.viewer-details .viewer-list');
+ $positions = explode("\r\n", end($list)->find('span.value', 0)->plaintext);
+ }
+
+ $found = false;
+ $keyword = $this->getInput('status') == 'professional' ? 'professional_position' : 'recreational_position';
+ foreach ($positions as $position) {
+ if (strpos(trim($position), $this->getInput($keyword)) !== false) {
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) {
+ continue;
+ }
+ }
$item = array();
@@ -134,22 +160,24 @@ class CrewbayBridge extends BridgeAbstract {
$images = $annonce->find('.avatar img');
$item['enclosures'] = array(end($images)->getAttribute('src'));
- if ($this->getInput('type') == 'boats') {
- $fields = array('job', 'boat', 'skipper');
- } else {
- $fields = array('profile', 'positions', 'info', 'qualifications' , 'skills', 'references');
- }
+ $content = $htmlDetail->find('.viewer-intro--info', 0)->innertext;
- $content = '';
- foreach ($fields as $field) {
- $info = $htmlDetail->find('.profile--modal-body .info-' . $field, 0);
- if ($info) {
- $content .= $htmlDetail->find('.profile--modal-body .info-' . $field, 0)->innertext;
+ $sections = $htmlDetail->find('.viewer-container .viewer-section');
+ foreach ($sections as $section) {
+ if ($section->find('.viewer-section-title', 0)) {
+ $class = str_replace('viewer-', '', explode(' ', $section->getAttribute('class'))[0]);
+ if (!in_array($class, array('apply', 'photos', 'reviews', 'contact', 'experience', 'qa'))) {
+ // Basic sections
+ $content .= $section->find('.viewer-section-title h3', 0)->outertext;
+ $content .= $section->find('.viewer-section-content', 0)->innertext;
+ }
+ } else {
+ // Info section
+ $content .= $section->find('.viewer-section-content h3', 0)->outertext;
+ $content .= $section->find('.viewer-section-content p', 0)->outertext;
}
}
- $item['content'] = $content;
-
if (!empty($this->getInput('keyword'))) {
$keyword = strtolower($this->getInput('keyword'));
if (strpos(strtolower($item['title']), $keyword) === false) {
@@ -159,28 +187,16 @@ class CrewbayBridge extends BridgeAbstract {
}
}
- if (!empty($this->getInput('recreational_position')) || !empty($this->getInput('professional_position'))) {
- if ($this->getInput('type') == 'boats') {
- if ($this->getInput('status') == 'professional') {
- $positions = array($annonce->find('.title .position', 0)->plaintext);
- } else {
- $positions = array(str_replace('Wanted:', '', $annonce->find('.content li', 0)->plaintext));
- }
- } else {
- $positions = explode("\r\n", trim($htmlDetail->find('.info-positions .value', 0)->plaintext));
- }
+ $item['content'] = $content;
- $found = false;
- $keyword = $this->getInput('status') == 'professional' ? 'professional_position' : 'recreational_position';
- foreach ($positions as $position) {
- if (strpos(trim($position), $this->getInput($keyword)) !== false) {
- $found = true;
- break;
- }
+ $tags = $htmlDetail->find('li.viewer-tags--tag');
+ foreach ($tags as $tag) {
+ if (!isset($item['categories'])) {
+ $item['categories'] = array();
}
-
- if (!$found) {
- continue;
+ $text = trim($tag->plaintext);
+ if (!in_array($text, $item['categories'])) {
+ $item['categories'][] = $text;
}
}
diff --git a/bridges/FB2Bridge.php b/bridges/FB2Bridge.php
index fd65c9f..862d20b 100644
--- a/bridges/FB2Bridge.php
+++ b/bridges/FB2Bridge.php
@@ -103,7 +103,7 @@ EOD;
$timestamp = 0;
$item['uri'] = html_entity_decode('http://touch.facebook.com'
- . $content->find("div[class='_52jc _5qc4 _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href'), ENT_QUOTES);
+ . $content->find("div[class='_52jc _5qc4 _78cz _24u0 _36xo']", 0)->find('a', 0)->getAttribute('href'), ENT_QUOTES);
//Decode images
$imagecleaned = preg_replace_callback('/<i [^>]* style="[^"]*url\(\'(.*?)\'\).*?><\/i>/m', function ($matches) {
diff --git a/bridges/FacebookBridge.php b/bridges/FacebookBridge.php
index ab39c10..fcdb570 100644
--- a/bridges/FacebookBridge.php
+++ b/bridges/FacebookBridge.php
@@ -659,14 +659,8 @@ EOD;
$date = 0;
}
- // Build title from username and content
- $title = $author;
-
- if(strlen($title) > 24)
- $title = substr($title, 0, strpos(wordwrap($title, 24), "\n")) . '...';
-
- $title = $title . ' | ' . strip_tags($content);
-
+ // Build title from content
+ $title = strip_tags($post->find('.userContent', 0)->innertext);
if(strlen($title) > 64)
$title = substr($title, 0, strpos(wordwrap($title, 64), "\n")) . '...';
@@ -677,10 +671,10 @@ EOD;
}
//Build and add final item
- $item['uri'] = htmlspecialchars_decode($uri);
- $item['content'] = htmlspecialchars_decode($content);
- $item['title'] = $title;
- $item['author'] = $author;
+ $item['uri'] = htmlspecialchars_decode($uri, ENT_QUOTES);
+ $item['content'] = htmlspecialchars_decode($content, ENT_QUOTES);
+ $item['title'] = htmlspecialchars_decode($title, ENT_QUOTES);
+ $item['author'] = htmlspecialchars_decode($author, ENT_QUOTES);
$item['timestamp'] = $date;
if(strpos($item['content'], '<img') === false) {
diff --git a/bridges/FilterBridge.php b/bridges/FilterBridge.php
index c644911..696b100 100644
--- a/bridges/FilterBridge.php
+++ b/bridges/FilterBridge.php
@@ -94,7 +94,7 @@ class FilterBridge extends FeedExpander {
}
try{
$this->collectExpandableDatas($this->getURI());
- } catch (HttpException $e) {
+ } catch (Exception $e) {
$this->collectExpandableDatas($this->getURI());
}
}
diff --git a/bridges/GithubIssueBridge.php b/bridges/GithubIssueBridge.php
index 0ed775d..c193610 100644
--- a/bridges/GithubIssueBridge.php
+++ b/bridges/GithubIssueBridge.php
@@ -37,10 +37,9 @@ class GithubIssueBridge extends BridgeAbstract {
$name = $this->getInput('u') . '/' . $this->getInput('p');
switch($this->queriedContext) {
case 'Project Issues':
+ $prefix = static::NAME . 's for ';
if($this->getInput('c')) {
$prefix = static::NAME . 's comments for ';
- } else {
- $prefix = static::NAME . 's for ';
}
$name = $prefix . $name;
break;
@@ -53,8 +52,9 @@ class GithubIssueBridge extends BridgeAbstract {
}
public function getURI(){
- if(!is_null($this->getInput('u')) && !is_null($this->getInput('p'))) {
- $uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p') . '/issues';
+ if(null !== $this->getInput('u') && null !== $this->getInput('p')) {
+ $uri = static::URI . $this->getInput('u') . '/'
+ . $this->getInput('p') . '/issues';
if($this->queriedContext === 'Issue comments') {
$uri .= '/' . $this->getInput('i');
} elseif($this->getInput('c')) {
@@ -66,54 +66,54 @@ class GithubIssueBridge extends BridgeAbstract {
return parent::getURI();
}
- protected function extractIssueComment($issueNbr, $title, $comment){
- $class = $comment->getAttribute('class');
- $classes = explode(' ', $class);
- $event = false;
- if(in_array('discussion-item', $classes)) {
- $event = true;
- }
-
- $author = 'unknown';
- if($comment->find('.author', 0)) {
- $author = $comment->find('.author', 0)->plaintext;
- }
+ protected function extractIssueEvent($issueNbr, $title, $comment){
+ $comment = $comment->firstChild();
+ $uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p')
+ . '/issues/' . $issueNbr . '#' . $comment->getAttribute('id');
- $uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p') . '/issues/' . $issueNbr;
+ $author = $comment->find('.author', 0)->plaintext;
- $comment = $comment->firstChild();
- if(!$event) {
- $comment = $comment->nextSibling();
- }
+ $title .= ' / ' . trim($comment->plaintext);
- if($event) {
- $title .= ' / ' . substr($class, strpos($class, 'discussion-item-') + strlen('discussion-item-'));
- if(!$comment->hasAttribute('id')) {
- $items = array();
- $timestamp = strtotime($comment->find('relative-time', 0)->getAttribute('datetime'));
- $content = $comment->innertext;
- while($comment = $comment->nextSibling()) {
- $item = array();
- $item['author'] = $author;
- $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
- $item['timestamp'] = $timestamp;
- $item['content'] = $content . '<p>' . $comment->children(1)->innertext . '</p>';
- $item['uri'] = $uri . '#' . $comment->children(1)->getAttribute('id');
- $items[] = $item;
- }
- return $items;
+ $content = $title;
+ if (null !== $comment->nextSibling()) {
+ $content = $comment->nextSibling()->innertext;
+ if ($comment->nextSibling()->nodeName() === 'span') {
+ $content = $comment->nextSibling()->nextSibling()->innertext;
}
- $content = $comment->parent()->innertext;
- } else {
- $title .= ' / ' . trim($comment->firstChild()->plaintext);
- $content = '<pre>' . $comment->find('.comment-body', 0)->innertext . '</pre>';
}
$item = array();
$item['author'] = $author;
- $item['uri'] = $uri . '#' . $comment->getAttribute('id');
+ $item['uri'] = $uri;
+ $item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
+ $item['timestamp'] = strtotime(
+ $comment->find('relative-time', 0)->getAttribute('datetime')
+ );
+ $item['content'] = $content;
+ return $item;
+ }
+
+ protected function extractIssueComment($issueNbr, $title, $comment){
+ $uri = static::URI . $this->getInput('u') . '/'
+ . $this->getInput('p') . '/issues/' . $issueNbr;
+
+ $author = $comment->find('.author', 0)->plaintext;
+
+ $title .= ' / ' . trim(
+ $comment->find('.comment .timeline-comment-header-text', 0)->plaintext
+ );
+
+ $content = $comment->find('.comment-body', 0)->innertext;
+
+ $item = array();
+ $item['author'] = $author;
+ $item['uri'] = $uri
+ . '#' . $comment->firstChild()->nextSibling()->getAttribute('id');
$item['title'] = html_entity_decode($title, ENT_QUOTES, 'UTF-8');
- $item['timestamp'] = strtotime($comment->find('relative-time', 0)->getAttribute('datetime'));
+ $item['timestamp'] = strtotime(
+ $comment->find('relative-time', 0)->getAttribute('datetime')
+ );
$item['content'] = $content;
return $item;
}
@@ -121,17 +121,29 @@ class GithubIssueBridge extends BridgeAbstract {
protected function extractIssueComments($issue){
$items = array();
$title = $issue->find('.gh-header-title', 0)->plaintext;
- $issueNbr = trim(substr($issue->find('.gh-header-number', 0)->plaintext, 1));
+ $issueNbr = trim(
+ substr($issue->find('.gh-header-number', 0)->plaintext, 1)
+ );
$comments = $issue->find('.js-discussion', 0);
foreach($comments->children() as $comment) {
+ if (!$comment->hasChildNodes()) {
+ continue;
+ }
+ $comment = $comment->firstChild();
$classes = explode(' ', $comment->getAttribute('class'));
- if(in_array('discussion-item', $classes)
- || in_array('timeline-comment-wrapper', $classes)) {
+ if (in_array('timeline-comment-wrapper', $classes)) {
$item = $this->extractIssueComment($issueNbr, $title, $comment);
- if(array_keys($item) !== range(0, count($item) - 1)) {
- $item = array($item);
+ $items[] = $item;
+ continue;
+ }
+ while (in_array('discussion-item', $classes)) {
+ $item = $this->extractIssueEvent($issueNbr, $title, $comment);
+ $items[] = $item;
+ $comment = $comment->nextSibling();
+ if (null == $comment) {
+ break;
}
- $items = array_merge($items, $item);
+ $classes = explode(' ', $comment->getAttribute('class'));
}
}
return $items;
@@ -139,7 +151,9 @@ class GithubIssueBridge extends BridgeAbstract {
public function collectData(){
$html = getSimpleHTMLDOM($this->getURI())
- or returnServerError('No results for Github Issue ' . $this->getURI());
+ or returnServerError(
+ 'No results for Github Issue ' . $this->getURI()
+ );
switch($this->queriedContext) {
case 'Issue comments':
@@ -148,31 +162,40 @@ class GithubIssueBridge extends BridgeAbstract {
case 'Project Issues':
foreach($html->find('.js-active-navigation-container .js-navigation-item') as $issue) {
$info = $issue->find('.opened-by', 0);
- $issueNbr = substr(trim($info->plaintext), 1, strpos(trim($info->plaintext), ' '));
+ $issueNbr = substr(
+ trim($info->plaintext), 1, strpos(trim($info->plaintext), ' ')
+ );
$item = array();
$item['content'] = '';
if($this->getInput('c')) {
- $uri = static::URI . $this->getInput('u') . '/' . $this->getInput('p') . '/issues/' . $issueNbr;
+ $uri = static::URI . $this->getInput('u')
+ . '/' . $this->getInput('p') . '/issues/' . $issueNbr;
$issue = getSimpleHTMLDOMCached($uri, static::CACHE_TIMEOUT);
if($issue) {
- $this->items = array_merge($this->items, $this->extractIssueComments($issue));
+ $this->items = array_merge(
+ $this->items,
+ $this->extractIssueComments($issue)
+ );
continue;
}
$item['content'] = 'Can not extract comments from ' . $uri;
}
$item['author'] = $info->find('a', 0)->plaintext;
- $item['timestamp'] = strtotime($info->find('relative-time', 0)->getAttribute('datetime'));
+ $item['timestamp'] = strtotime(
+ $info->find('relative-time', 0)->getAttribute('datetime')
+ );
$item['title'] = html_entity_decode(
$issue->find('.js-navigation-open', 0)->plaintext,
ENT_QUOTES,
'UTF-8'
);
- $comments = $issue->find('.col-5', 0)->plaintext;
+ $comments = trim($issue->find('.col-5', 0)->plaintext);
$item['content'] .= "\n" . 'Comments: ' . ($comments ? $comments : '0');
- $item['uri'] = self::URI . $issue->find('.js-navigation-open', 0)->getAttribute('href');
+ $item['uri'] = self::URI
+ . $issue->find('.js-navigation-open', 0)->getAttribute('href');
$this->items[] = $item;
}
break;
@@ -180,7 +203,11 @@ class GithubIssueBridge extends BridgeAbstract {
array_walk($this->items, function(&$item){
$item['content'] = preg_replace('/\s+/', ' ', $item['content']);
- $item['content'] = str_replace('href="/', 'href="' . static::URI, $item['content']);
+ $item['content'] = str_replace(
+ 'href="/',
+ 'href="' . static::URI,
+ $item['content']
+ );
$item['content'] = str_replace(
'href="#',
'href="' . substr($item['uri'], 0, strpos($item['uri'], '#') + 1),
diff --git a/bridges/GlassdoorBridge.php b/bridges/GlassdoorBridge.php
index 72b7a16..72b7a16 100755..100644
--- a/bridges/GlassdoorBridge.php
+++ b/bridges/GlassdoorBridge.php
diff --git a/bridges/JustETFBridge.php b/bridges/JustETFBridge.php
index f0223e9..32aa65e 100644
--- a/bridges/JustETFBridge.php
+++ b/bridges/JustETFBridge.php
@@ -132,7 +132,7 @@ class JustETFBridge extends BridgeAbstract {
date_time_set($df, 0, 0);
- // debugMessage(date_format($df, 'U'));
+ // Debug::log(date_format($df, 'U'));
return date_format($df, 'U');
}
@@ -210,7 +210,7 @@ class JustETFBridge extends BridgeAbstract {
$element = $article->find('div.subheadline', 0)
or returnServerError('Date not found!');
- // debugMessage($element->plaintext);
+ // Debug::log($element->plaintext);
$date = trim(explode('|', $element->plaintext)[0]);
@@ -223,7 +223,7 @@ class JustETFBridge extends BridgeAbstract {
$element->find('a', 0)->onclick = '';
- // debugMessage($element->innertext);
+ // Debug::log($element->innertext);
return $element->innertext;
}
@@ -288,7 +288,7 @@ class JustETFBridge extends BridgeAbstract {
$element = $html->find('div.infobox div.vallabel', 0)
or returnServerError('Date not found!');
- // debugMessage($element->plaintext);
+ // Debug::log($element->plaintext);
$date = trim(explode("\r\n", $element->plaintext)[1]);
diff --git a/bridges/MozillaSecurity.php b/bridges/MozillaSecurity.php
new file mode 100644
index 0000000..0b951a1
--- /dev/null
+++ b/bridges/MozillaSecurity.php
@@ -0,0 +1,28 @@
+<?php
+class MozillaSecurityBridge extends BridgeAbstract {
+
+ const MAINTAINER = 'm0le.net';
+ const NAME = 'Mozilla Security Advisories';
+ const URI = 'https://www.mozilla.org/en-US/security/advisories/';
+ const CACHE_TIMEOUT = 7200; // 2h
+ const DESCRIPTION = 'Mozilla Security Advisories';
+ const WEBROOT = 'https://www.mozilla.org';
+
+ public function collectData(){
+ $html = getSimpleHTMLDOM(self::URI)
+ or returnServerError('Could not request MSA.');
+
+ $html = defaultLinkTo($html, self::WEBROOT);
+
+ $item = array();
+ $articles = $html->find('div[itemprop="articleBody"] h2');
+
+ foreach ($articles as $element) {
+ $item['title'] = $element->innertext;
+ $item['timestamp'] = strtotime($element->innertext);
+ $item['content'] = $element->next_sibling()->innertext;
+ $item['uri'] = self::URI;
+ $this->items[] = $item;
+ }
+ }
+}
diff --git a/bridges/PixivBridge.php b/bridges/PixivBridge.php
index 4e4cf65..b1cba4e 100644
--- a/bridges/PixivBridge.php
+++ b/bridges/PixivBridge.php
@@ -53,7 +53,7 @@ class PixivBridge extends BridgeAbstract {
$url = str_replace('_master1200', '', $url);
$url = str_replace('c/240x240/img-master/', 'img-original/', $url);
- $path = PATH_CACHE . '/pixiv_img';
+ $path = PATH_CACHE . 'pixiv_img/';
if(!is_dir($path))
mkdir($path, 0755, true);
diff --git a/bridges/SoundcloudBridge.php b/bridges/SoundcloudBridge.php
index bfd97cb..3853883 100644
--- a/bridges/SoundcloudBridge.php
+++ b/bridges/SoundcloudBridge.php
@@ -34,13 +34,13 @@ class SoundCloudBridge extends BridgeAbstract {
for($i = 0; $i < 10; $i++) {
$item = array();
- $item['author'] = $tracks[$i]->user->username . ' - ' . $tracks[$i]->title;
+ $item['author'] = $tracks[$i]->user->username;
$item['title'] = $tracks[$i]->user->username . ' - ' . $tracks[$i]->title;
- $item['content'] = '<audio src="'
- . $tracks[$i]->uri
+ $item['timestamp'] = strtotime($tracks[$i]->created_at);
+ $item['content'] = $tracks[$i]->description;
+ $item['enclosures'] = array($tracks[$i]->uri
. '/stream?client_id='
- . self::CLIENT_ID
- . '">';
+ . self::CLIENT_ID);
$item['id'] = self::URI
. urlencode($this->getInput('u'))
diff --git a/bridges/TwitterBridge.php b/bridges/TwitterBridge.php
index aedb372..20f90d7 100644
--- a/bridges/TwitterBridge.php
+++ b/bridges/TwitterBridge.php
@@ -66,6 +66,41 @@ class TwitterBridge extends BridgeAbstract {
)
);
+ public function detectParameters($url){
+ $params = array();
+
+ // By keyword or hashtag (search)
+ $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/search.*(\?|&)q=([^\/&?\n]+)/';
+ if(preg_match($regex, $url, $matches) > 0) {
+ $params['q'] = urldecode($matches[4]);
+ return $params;
+ }
+
+ // By hashtag
+ $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/hashtag\/([^\/?\n]+)/';
+ if(preg_match($regex, $url, $matches) > 0) {
+ $params['q'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ // By list
+ $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)\/lists\/([^\/?\n]+)/';
+ if(preg_match($regex, $url, $matches) > 0) {
+ $params['user'] = urldecode($matches[3]);
+ $params['list'] = urldecode($matches[4]);
+ return $params;
+ }
+
+ // By username
+ $regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)/';
+ if(preg_match($regex, $url, $matches) > 0) {
+ $params['u'] = urldecode($matches[3]);
+ return $params;
+ }
+
+ return null;
+ }
+
public function getName(){
switch($this->queriedContext) {
case 'By keyword or hashtag':
@@ -144,9 +179,9 @@ class TwitterBridge extends BridgeAbstract {
$item = array();
// extract username and sanitize
- $item['username'] = $tweet->getAttribute('data-screen-name');
+ $item['username'] = htmlspecialchars_decode($tweet->getAttribute('data-screen-name'), ENT_QUOTES);
// extract fullname (pseudonym)
- $item['fullname'] = $tweet->getAttribute('data-name');
+ $item['fullname'] = htmlspecialchars_decode($tweet->getAttribute('data-name'), ENT_QUOTES);
// get author
$item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
// get avatar link
@@ -158,7 +193,8 @@ class TwitterBridge extends BridgeAbstract {
// extract tweet timestamp
$item['timestamp'] = $tweet->find('span.js-short-timestamp', 0)->getAttribute('data-time');
// generate the title
- $item['title'] = strip_tags($this->fixAnchorSpacing($tweet->find('p.js-tweet-text', 0), '<a>'));
+ $item['title'] = strip_tags($this->fixAnchorSpacing(htmlspecialchars_decode(
+ $tweet->find('p.js-tweet-text', 0), ENT_QUOTES), '<a>'));
switch($this->queriedContext) {
case 'By list':
@@ -258,16 +294,17 @@ EOD;
}
$item['content'] = <<<EOD
+{$item['content']}
+<hr>
<div style="display: inline-block; vertical-align: top;">
<blockquote>{$cleanedQuotedTweet}</blockquote>
</div>
<div style="display: block; vertical-align: top;">
<blockquote>{$quotedImage_html}</blockquote>
</div>
-<hr>
-{$item['content']}
EOD;
}
+ $item['content'] = htmlspecialchars_decode($item['content'], ENT_QUOTES);
// put out
$this->items[] = $item;
diff --git a/bridges/WordPressBridge.php b/bridges/WordPressBridge.php
index 2a750a9..1589c72 100644
--- a/bridges/WordPressBridge.php
+++ b/bridges/WordPressBridge.php
@@ -93,7 +93,7 @@ class WordPressBridge extends FeedExpander {
}
try{
$this->collectExpandableDatas($this->getURI() . '/feed/atom/');
- } catch (HttpException $e) {
+ } catch (Exception $e) {
$this->collectExpandableDatas($this->getURI() . '/?feed=atom');
}
diff --git a/bridges/WordPressPluginUpdateBridge.php b/bridges/WordPressPluginUpdateBridge.php
index fb4a57e..19ec02b 100644
--- a/bridges/WordPressPluginUpdateBridge.php
+++ b/bridges/WordPressPluginUpdateBridge.php
@@ -74,10 +74,10 @@ class WordPressPluginUpdateBridge extends BridgeAbstract {
}
private function getCachedDate($url){
- debugMessage('getting pubdate from url ' . $url . '');
+ Debug::log('getting pubdate from url ' . $url . '');
// Initialize cache
$cache = Cache::create('FileCache');
- $cache->setPath(PATH_CACHE . '/pages');
+ $cache->setPath(PATH_CACHE . 'pages/');
$params = [$url];
$cache->setParameters($params);
// Get cachefile timestamp
diff --git a/bridges/XenForoBridge.php b/bridges/XenForoBridge.php
index 75c0f6d..ad4df84 100644
--- a/bridges/XenForoBridge.php
+++ b/bridges/XenForoBridge.php
@@ -453,7 +453,7 @@ class XenForoBridge extends BridgeAbstract {
}
- // debugMessage(date_format($df, 'U'));
+ // Debug::log(date_format($df, 'U'));
return date_format($df, 'U');
diff --git a/bridges/YGGTorrentBridge.php b/bridges/YGGTorrentBridge.php
index f057d87..3e37c93 100644
--- a/bridges/YGGTorrentBridge.php
+++ b/bridges/YGGTorrentBridge.php
@@ -115,6 +115,7 @@ class YGGTorrentBridge extends BridgeAbstract {
$item = array();
$item['timestamp'] = $row->find('.hidden', 1)->plaintext;
$item['title'] = $row->find('a', 1)->plaintext;
+ $item['uri'] = $row->find('a', 1)->href;
$torrentData = $this->collectTorrentData($row->find('a', 1)->href);
$item['author'] = $torrentData['author'];
$item['content'] = $torrentData['content'];
diff --git a/bridges/ZoneTelechargementBridge.php b/bridges/ZoneTelechargementBridge.php
index 670a50c..44cdfce 100644
--- a/bridges/ZoneTelechargementBridge.php
+++ b/bridges/ZoneTelechargementBridge.php
@@ -1,8 +1,15 @@
<?php
class ZoneTelechargementBridge extends BridgeAbstract {
- const NAME = 'Zone Telechargement';
- const URI = 'https://www.zone-telechargement1.org/';
- const DESCRIPTION = 'Suivi de série sur Zone Telechargement';
+
+ /* This bridge was initally done for the Website Zone Telechargement,
+ * but the website changed it's name and URL.
+ * Therefore, the class name and filename does not correspond to the
+ * name of the bridge. This permits to keep the same RSS Feed URL.
+ */
+
+ const NAME = 'Annuaire Telechargement';
+ const URI = 'https://www.annuaire-telechargement.com/';
+ const DESCRIPTION = 'Suivi de série sur Annuaire Telechargement';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = array(
'Suivre la publication des épisodes d\'une série en cours de diffusion' => array(
@@ -10,14 +17,14 @@ class ZoneTelechargementBridge extends BridgeAbstract {
'name' => 'URL de la série',
'type' => 'text',
'required' => true,
- 'title' => 'URL d\'une série sans le https://ww4.zone-telechargement1.org/',
+ 'title' => 'URL d\'une série sans le https://www.annuaire-telechargement.com/',
'exampleValue' => 'telecharger-series/31079-halt-and-catch-fire-saison-4-french-hd720p.html'
)
)
);
public function getIcon() {
- return 'https://ww7.zone-telechargement1.org/templates/Default/images/favicon.ico';
+ return 'https://www.annuaire-telechargement.com/templates/Default/images/favicon.ico';
}
public function collectData(){
diff --git a/cache/.gitkeep b/cache/.gitkeep
deleted file mode 100644
index e69de29..0000000
--- a/cache/.gitkeep
+++ /dev/null
diff --git a/index.php b/index.php
index b44eb36..0998c82 100644
--- a/index.php
+++ b/index.php
@@ -1,35 +1,5 @@
<?php
-/*
- Create a file named 'DEBUG' for enabling debug mode.
- For further security, you may put whitelisted IP addresses in the file,
- one IP per line. Empty file allows anyone(!).
- Debugging allows displaying PHP error messages and bypasses the cache: this
- can allow a malicious client to retrieve data about your server and hammer
- a provider throught your rss-bridge instance.
-*/
-if(file_exists('DEBUG')) {
- $debug_whitelist = trim(file_get_contents('DEBUG'));
-
- $debug_enabled = empty($debug_whitelist)
- || in_array($_SERVER['REMOTE_ADDR'],
- explode("\n", str_replace("\r", '', $debug_whitelist)
- )
- );
-
- if($debug_enabled) {
- ini_set('display_errors', '1');
- error_reporting(E_ALL);
- define('DEBUG', true);
- if (empty($debug_whitelist)) {
- define('DEBUG_INSECURE', true);
- }
- }
-}
-
-require_once __DIR__ . '/lib/RssBridge.php';
-
-// Specify path for whitelist file
-define('WHITELIST_FILE', __DIR__ . '/whitelist.txt');
+require_once __DIR__ . '/lib/rssbridge.php';
Configuration::verifyInstallation();
Configuration::loadConfiguration();
@@ -66,7 +36,7 @@ $whitelist_default = array(
'DansTonChatBridge',
'DuckDuckGoBridge',
'FacebookBridge',
- 'FlickrExploreBridge',
+ 'FlickrBridge',
'GooglePlusPostBridge',
'GoogleSearchBridge',
'IdenticaBridge',
@@ -80,26 +50,7 @@ $whitelist_default = array(
try {
- Bridge::setDir(PATH_LIB_BRIDGES);
- Format::setDir(PATH_LIB_FORMATS);
- Cache::setDir(PATH_LIB_CACHES);
-
- if(!file_exists(WHITELIST_FILE)) {
- $whitelist_selection = $whitelist_default;
- $whitelist_write = implode("\n", $whitelist_default);
- file_put_contents(WHITELIST_FILE, $whitelist_write);
- } else {
-
- $whitelist_file_content = file_get_contents(WHITELIST_FILE);
- if($whitelist_file_content != "*\n") {
- $whitelist_selection = explode("\n", $whitelist_file_content);
- } else {
- $whitelist_selection = Bridge::listBridges();
- }
-
- // Prepare for case-insensitive match
- $whitelist_selection = array_map('strtolower', $whitelist_selection);
- }
+ Bridge::setWhitelist($whitelist_default);
$showInactive = filter_input(INPUT_GET, 'show_inactive', FILTER_VALIDATE_BOOLEAN);
$action = array_key_exists('action', $params) ? $params['action'] : null;
@@ -112,7 +63,7 @@ try {
$list->bridges = array();
$list->total = 0;
- foreach(Bridge::listBridges() as $bridgeName) {
+ foreach(Bridge::getBridgeNames() as $bridgeName) {
$bridge = Bridge::create($bridgeName);
@@ -126,7 +77,7 @@ try {
}
- $status = Bridge::isWhitelisted($whitelist_selection, strtolower($bridgeName)) ? 'active' : 'inactive';
+ $status = Bridge::isWhitelisted($bridgeName) ? 'active' : 'inactive';
$list->bridges[$bridgeName] = array(
'status' => $status,
@@ -145,13 +96,44 @@ try {
header('Content-Type: application/json');
echo json_encode($list, JSON_PRETTY_PRINT);
- } elseif($action === 'display' && !empty($bridge)) {
- // DEPRECATED: 'nameBridge' scheme is replaced by 'name' in bridge parameter values
- // this is to keep compatibility until futher complete removal
- if(($pos = strpos($bridge, 'Bridge')) === (strlen($bridge) - strlen('Bridge'))) {
- $bridge = substr($bridge, 0, $pos);
+ } elseif($action === 'detect') {
+
+ $targetURL = $params['url']
+ or returnClientError('You must specify a url!');
+
+ $format = $params['format']
+ or returnClientError('You must specify a format!');
+
+ foreach(Bridge::getBridgeNames() as $bridgeName) {
+
+ if(!Bridge::isWhitelisted($bridgeName)) {
+ continue;
+ }
+
+ $bridge = Bridge::create($bridgeName);
+
+ if($bridge === false) {
+ continue;
+ }
+
+ $bridgeParams = $bridge->detectParameters($targetURL);
+
+ if(is_null($bridgeParams)) {
+ continue;
+ }
+
+ $bridgeParams['bridge'] = $bridgeName;
+ $bridgeParams['format'] = $format;
+
+ header('Location: ?action=display&' . http_build_query($bridgeParams), true, 301);
+ die();
+
}
+ returnClientError('No bridge found for given URL: ' . $targetURL);
+
+ } elseif($action === 'display' && !empty($bridge)) {
+
$format = $params['format']
or returnClientError('You must specify a format!');
@@ -162,8 +144,8 @@ try {
}
// whitelist control
- if(!Bridge::isWhitelisted($whitelist_selection, strtolower($bridge))) {
- throw new \HttpException('This bridge is not whitelisted', 401);
+ if(!Bridge::isWhitelisted($bridge)) {
+ throw new \Exception('This bridge is not whitelisted', 401);
die;
}
@@ -180,7 +162,10 @@ try {
if(array_key_exists('_cache_timeout', $params)) {
if(!CUSTOM_CACHE_TIMEOUT) {
- throw new \HttpException('This server doesn\'t support "_cache_timeout"!');
+ unset($params['_cache_timeout']);
+ $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($params);
+ header('Location: ' . $uri, true, 301);
+ die();
}
$cache_timeout = filter_var($params['_cache_timeout'], FILTER_VALIDATE_INT);
@@ -228,7 +213,7 @@ try {
if($mtime !== false
&& (time() - $cache_timeout < $mtime)
- && (!defined('DEBUG') || DEBUG !== true)) { // Load cached data
+ && !Debug::isEnabled()) { // Load cached data
// Send "Not Modified" response if client supports it
// Implementation based on https://stackoverflow.com/a/10847262
@@ -275,7 +260,8 @@ try {
$item['title'] = 'Bridge returned error ' . $e->getCode() . '! (' . $params['_error_time'] . ')';
}
- $item['uri'] = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($params);
+ $item['uri'] = (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
+ . '?' . http_build_query($params);
$item['timestamp'] = time();
$item['content'] = buildBridgeException($e, $bridge);
@@ -288,7 +274,8 @@ try {
// Create "new" error message every 24 hours
$params['_error_time'] = urlencode((int)(time() / 86400));
- $item['uri'] = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($params);
+ $item['uri'] = (isset($_SERVER['REQUEST_URI']) ? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) : '')
+ . '?' . http_build_query($params);
$item['title'] = 'Bridge returned error ' . $e->getCode() . '! (' . $params['_error_time'] . ')';
$item['timestamp'] = time();
$item['content'] = buildBridgeException($e, $bridge);
@@ -321,13 +308,10 @@ try {
die(buildTransformException($e, $bridge));
}
} else {
- echo BridgeList::create($whitelist_selection, $showInactive);
+ echo BridgeList::create($showInactive);
}
-} catch(HttpException $e) {
- error_log($e);
- header('Content-Type: text/plain', true, $e->getCode());
- die($e->getMessage());
} catch(\Exception $e) {
error_log($e);
+ header('Content-Type: text/plain', true, $e->getCode());
die($e->getMessage());
}
diff --git a/lib/Authentication.php b/lib/Authentication.php
index da24763..99deaac 100644
--- a/lib/Authentication.php
+++ b/lib/Authentication.php
@@ -1,6 +1,56 @@
<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * Authentication module for RSS-Bridge.
+ *
+ * This class implements an authentication module for RSS-Bridge, utilizing the
+ * HTTP authentication capabilities of PHP.
+ *
+ * _Notice_: Authentication via HTTP does not prevent users from accessing files
+ * on your server. If your server supports `.htaccess`, you should globally restrict
+ * access to files instead.
+ *
+ * @link https://php.net/manual/en/features.http-auth.php HTTP authentication with PHP
+ * @link https://httpd.apache.org/docs/2.4/howto/htaccess.html Apache HTTP Server
+ * Tutorial: .htaccess files
+ *
+ * @todo Configuration parameters should be stored internally instead of accessing
+ * the configuration class directly.
+ * @todo Add functions to detect if a user is authenticated or not. This can be
+ * utilized for limiting access to authorized users only.
+ */
class Authentication {
+ /**
+ * Throw an exception when trying to create a new instance of this class.
+ * Use {@see Authentication::showPromptIfNeeded()} instead!
+ *
+ * @throws \LogicException if called.
+ */
+ public function __construct(){
+ throw new \LogicException('Use ' . __CLASS__ . '::showPromptIfNeeded()!');
+ }
+
+ /**
+ * Requests the user for login credentials if necessary.
+ *
+ * Responds to an authentication request or returns the `WWW-Authenticate`
+ * header if authentication is enabled in the configuration of RSS-Bridge
+ * (`[authentication] enable = true`).
+ *
+ * @return void
+ */
public static function showPromptIfNeeded() {
if(Configuration::getConfig('authentication', 'enable') === true) {
@@ -13,6 +63,13 @@ class Authentication {
}
+ /**
+ * Verifies if an authentication request was received and compares the
+ * provided username and password to the configuration of RSS-Bridge
+ * (`[authentication] username` and `[authentication] password`).
+ *
+ * @return bool True if authentication succeeded.
+ */
public static function verifyPrompt() {
if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
diff --git a/lib/Bridge.php b/lib/Bridge.php
index e0010df..c9561c8 100644
--- a/lib/Bridge.php
+++ b/lib/Bridge.php
@@ -1,88 +1,296 @@
<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+/**
+ * Factory class responsible for creating bridge objects from a given working
+ * directory, limited by a whitelist.
+ *
+ * This class is capable of:
+ * - Locating bridge classes in the specified working directory (see {@see Bridge::$workingDir})
+ * - Filtering bridges based on a whitelist (see {@see Bridge::$whitelist})
+ * - Creating new bridge instances based on the bridge's name (see {@see Bridge::create()})
+ *
+ * The following example illustrates the intended use for this class.
+ *
+ * ```PHP
+ * require_once __DIR__ . '/rssbridge.php';
+ *
+ * // Step 1: Set the working directory
+ * Bridge::setWorkingDir(__DIR__ . '/../bridges/');
+ *
+ * // Step 2: Add bridges to the whitelist
+ * Bridge::setWhitelist(array('GitHubIssue', 'GoogleSearch', 'Facebook', 'Twitter'));
+ *
+ * // Step 3: Create a new instance of a bridge (based on the name)
+ * $bridge = Bridge::create('GitHubIssue');
+ * ```
+ */
class Bridge {
- static protected $dirBridge;
+ /**
+ * Holds a path to the working directory.
+ *
+ * Do not access this property directly!
+ * Use {@see Bridge::setWorkingDir()} and {@see Bridge::getWorkingDir()} instead.
+ *
+ * @var string|null
+ */
+ protected static $workingDir = null;
+
+ /**
+ * Holds a list of whitelisted bridges.
+ *
+ * Do not access this property directly!
+ * Use {@see Bridge::getWhitelist()} instead.
+ *
+ * @var array
+ */
+ protected static $whitelist = array();
+ /**
+ * Throws an exception when trying to create a new instance of this class.
+ * Use {@see Bridge::create()} to instanciate a new bridge from the working
+ * directory.
+ *
+ * @throws \LogicException if called.
+ */
public function __construct(){
- throw new \LogicException('Please use ' . __CLASS__ . '::create for new object.');
+ throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create bridge objects!');
}
/**
- * Create a new bridge object
- * @param string $nameBridge Defined bridge name you want use
- * @return Bridge object dedicated
- */
- static public function create($nameBridge){
- if(!preg_match('@^[A-Z][a-zA-Z0-9-]*$@', $nameBridge)) {
- $message = <<<EOD
-'nameBridge' must start with one uppercase character followed or not by
-alphanumeric or dash characters!
-EOD;
- throw new \InvalidArgumentException($message);
+ * Creates a new bridge object from the working directory.
+ *
+ * @throws \InvalidArgumentException if the requested bridge name is invalid.
+ * @throws \Exception if the requested bridge doesn't exist in the working
+ * directory.
+ * @param string $name Name of the bridge object.
+ * @return object|bool The bridge object or false if the class is not instantiable.
+ */
+ public static function create($name){
+ if(!self::isBridgeName($name)) {
+ throw new \InvalidArgumentException('Bridge name invalid!');
}
- $nameBridge = $nameBridge . 'Bridge';
- $pathBridge = self::getDir() . $nameBridge . '.php';
+ $name = self::sanitizeBridgeName($name) . 'Bridge';
+ $filePath = self::getWorkingDir() . $name . '.php';
- if(!file_exists($pathBridge)) {
- throw new \Exception('The bridge you looking for does not exist. It should be at path '
- . $pathBridge);
+ if(!file_exists($filePath)) {
+ throw new \Exception('Bridge file ' . $filePath . ' does not exist!');
}
- require_once $pathBridge;
+ require_once $filePath;
- if((new ReflectionClass($nameBridge))->isInstantiable()) {
- return new $nameBridge();
+ if((new \ReflectionClass($name))->isInstantiable()) {
+ return new $name();
}
return false;
}
- static public function setDir($dirBridge){
- if(!is_string($dirBridge)) {
- throw new \InvalidArgumentException('Dir bridge must be a string.');
+ /**
+ * Sets the working directory.
+ *
+ * @param string $dir Path to the directory containing bridges.
+ * @throws \LogicException if the provided path is not a valid string.
+ * @throws \Exception if the provided path does not exist.
+ * @throws \InvalidArgumentException if $dir is not a directory.
+ * @return void
+ */
+ public static function setWorkingDir($dir){
+ self::$workingDir = null;
+
+ if(!is_string($dir)) {
+ throw new \InvalidArgumentException('Working directory is not a valid string!');
}
- if(!file_exists($dirBridge)) {
- throw new \Exception('Dir bridge does not exist.');
+ if(!file_exists($dir)) {
+ throw new \Exception('Working directory does not exist!');
}
- self::$dirBridge = $dirBridge;
+ if(!is_dir($dir)) {
+ throw new \InvalidArgumentException('Working directory is not a directory!');
+ }
+
+ self::$workingDir = realpath($dir) . '/';
}
- static public function getDir(){
- if(is_null(self::$dirBridge)) {
- throw new \LogicException(__CLASS__ . ' class need to know bridge path !');
+ /**
+ * Returns the working directory.
+ * The working directory must be specified with {@see Bridge::setWorkingDir()}!
+ *
+ * @throws \LogicException if the working directory is not set.
+ * @return string The current working directory.
+ */
+ public static function getWorkingDir(){
+ if(is_null(self::$workingDir)) {
+ throw new \LogicException('Working directory is not set!');
}
- return self::$dirBridge;
+ return self::$workingDir;
+ }
+
+ /**
+ * Returns true if the provided name is a valid bridge name.
+ *
+ * A valid bridge name starts with a capital letter ([A-Z]), followed by
+ * zero or more alphanumeric characters or hyphen ([A-Za-z0-9-]).
+ *
+ * @param string $name The bridge name.
+ * @return bool true if the name is a valid bridge name, false otherwise.
+ */
+ public static function isBridgeName($name){
+ return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
}
/**
- * Lists the available bridges.
- * @return array List of the bridges
- */
- static public function listBridges(){
- $listBridge = array();
- $dirFiles = scandir(self::getDir());
-
- if($dirFiles !== false) {
- foreach($dirFiles as $fileName) {
- if(preg_match('@^([^.]+)Bridge\.php$@U', $fileName, $out)) {
- $listBridge[] = $out[1];
+ * Returns the list of bridge names from the working directory.
+ *
+ * The list is cached internally to allow for successive calls.
+ *
+ * @return array List of bridge names
+ */
+ public static function getBridgeNames(){
+
+ static $bridgeNames = array(); // Initialized on first call
+
+ if(empty($bridgeNames)) {
+ $files = scandir(self::getWorkingDir());
+
+ if($files !== false) {
+ foreach($files as $file) {
+ if(preg_match('/^([^.]+)Bridge\.php$/U', $file, $out)) {
+ $bridgeNames[] = $out[1];
+ }
}
}
}
- return $listBridge;
+ return $bridgeNames;
+ }
+
+ /**
+ * Checks if a bridge is whitelisted.
+ *
+ * @param string $name Name of the bridge.
+ * @return bool True if the bridge is whitelisted.
+ */
+ public static function isWhitelisted($name){
+ return in_array(self::sanitizeBridgeName($name), self::getWhitelist());
}
- static public function isWhitelisted($whitelist, $name){
- return in_array($name, $whitelist)
- || in_array($name . '.php', $whitelist)
- || in_array($name . 'bridge', $whitelist) // DEPRECATED
- || in_array($name . 'bridge.php', $whitelist) // DEPRECATED
- || (count($whitelist) === 1 && trim($whitelist[0]) === '*');
+ /**
+ * Returns the whitelist.
+ *
+ * On first call this function reads the whitelist from {@see WHITELIST}.
+ * * Each line in the file specifies one bridge on the whitelist.
+ * * An empty file disables all bridges.
+ * * If the file only only contains `*`, all bridges are whitelisted.
+ *
+ * Use {@see Bridge::setWhitelist()} to specify a default whitelist **before**
+ * calling this function! The list is cached internally to allow for
+ * successive calls. If {@see Bridge::setWhitelist()} gets called after this
+ * function, the whitelist is **not** updated again!
+ *
+ * @return array Array of whitelisted bridges
+ */
+ public static function getWhitelist() {
+
+ static $firstCall = true; // Initialized on first call
+
+ if($firstCall) {
+
+ // Create initial whitelist or load from disk
+ if (!file_exists(WHITELIST) && !empty(self::$whitelist)) {
+ file_put_contents(WHITELIST, implode("\n", self::$whitelist));
+ } else {
+
+ $contents = trim(file_get_contents(WHITELIST));
+
+ if($contents === '*') { // Whitelist all bridges
+ self::$whitelist = self::getBridgeNames();
+ } else {
+ self::$whitelist = array_map('self::sanitizeBridgeName', explode("\n", $contents));
+ }
+
+ }
+
+ }
+
+ return self::$whitelist;
+
+ }
+
+ /**
+ * Sets the (default) whitelist.
+ *
+ * If this function is called **before** {@see Bridge::getWhitelist()}, the
+ * provided whitelist will be replaced by a custom whitelist specified in
+ * {@see WHITELIST} (if it exists).
+ *
+ * If this function is called **after** {@see Bridge::getWhitelist()}, the
+ * provided whitelist is taken as is (not updated by the custom whitelist
+ * again).
+ *
+ * @param array $default The whitelist as array of bridge names.
+ * @return void
+ */
+ public static function setWhitelist($default = array()) {
+ self::$whitelist = array_map('self::sanitizeBridgeName', $default);
+ }
+
+ /**
+ * Returns the sanitized bridge name.
+ *
+ * The bridge name can be specified in various ways:
+ * * The PHP file name (i.e. `GitHubIssueBridge.php`)
+ * * The PHP file name without file extension (i.e. `GitHubIssueBridge`)
+ * * The bridge name (i.e. `GitHubIssue`)
+ *
+ * Casing is ignored (i.e. `GITHUBISSUE` and `githubissue` are the same).
+ *
+ * A bridge file matching the given bridge name must exist in the working
+ * directory!
+ *
+ * @param string $name The bridge name
+ * @return string|null The sanitized bridge name if the provided name is
+ * valid, null otherwise.
+ */
+ protected static function sanitizeBridgeName($name) {
+
+ if(is_string($name)) {
+
+ // Trim trailing '.php' if exists
+ if(preg_match('/(.+)(?:\.php)/', $name, $matches)) {
+ $name = $matches[1];
+ }
+
+ // Trim trailing 'Bridge' if exists
+ if(preg_match('/(.+)(?:Bridge)/i', $name, $matches)) {
+ $name = $matches[1];
+ }
+
+ // The name is valid if a corresponding bridge file is found on disk
+ if(in_array(strtolower($name), array_map('strtolower', self::getBridgeNames()))) {
+ $index = array_search(strtolower($name), array_map('strtolower', self::getBridgeNames()));
+ return self::getBridgeNames()[$index];
+ }
+
+ Debug::log('Invalid bridge name specified: "' . $name . '"!');
+
+ }
+
+ return null; // Bad parameter
+
}
}
diff --git a/lib/BridgeAbstract.php b/lib/BridgeAbstract.php
index aa65411..a2b1580 100644
--- a/lib/BridgeAbstract.php
+++ b/lib/BridgeAbstract.php
@@ -1,32 +1,112 @@
<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+/**
+ * An abstract class for bridges
+ *
+ * This class implements {@see BridgeInterface} with most common functions in
+ * order to reduce code duplication. Bridges should inherit from this class
+ * instead of implementing the interface manually.
+ *
+ * @todo Move constants to the interface (this is supported by PHP)
+ * @todo Change visibility of constants to protected
+ * @todo Return `self` on more functions to allow chaining
+ * @todo Add specification for PARAMETERS ()
+ * @todo Add specification for $items
+ */
abstract class BridgeAbstract implements BridgeInterface {
+ /**
+ * Name of the bridge
+ *
+ * Use {@see BridgeAbstract::getName()} to read this parameter
+ */
const NAME = 'Unnamed bridge';
+
+ /**
+ * URI to the site the bridge is intended to be used for.
+ *
+ * Use {@see BridgeAbstract::getURI()} to read this parameter
+ */
const URI = '';
+
+ /**
+ * A brief description of what the bridge can do
+ *
+ * Use {@see BridgeAbstract::getDescription()} to read this parameter
+ */
const DESCRIPTION = 'No description provided';
+
+ /**
+ * The name of the maintainer. Multiple maintainers can be separated by comma
+ *
+ * Use {@see BridgeAbstract::getMaintainer()} to read this parameter
+ */
const MAINTAINER = 'No maintainer';
+
+ /**
+ * The default cache timeout for the bridge
+ *
+ * Use {@see BridgeAbstract::getCacheTimeout()} to read this parameter
+ */
const CACHE_TIMEOUT = 3600;
+
+ /**
+ * Parameters for the bridge
+ *
+ * Use {@see BridgeAbstract::getParameters()} to read this parameter
+ */
const PARAMETERS = array();
+ /**
+ * Holds the list of items collected by the bridge
+ *
+ * Items must be collected by {@see BridgeInterface::collectData()}
+ *
+ * Use {@see BridgeAbstract::getItems()} to access items.
+ *
+ * @var array
+ */
protected $items = array();
+
+ /**
+ * Holds the list of input parameters used by the bridge
+ *
+ * Do not access this parameter directly!
+ * Use {@see BridgeAbstract::setInputs()} and {@see BridgeAbstract::getInput()} instead!
+ *
+ * @var array
+ */
protected $inputs = array();
- protected $queriedContext = '';
/**
- * Return items stored in the bridge
- * @return mixed
- */
+ * Holds the name of the queried context
+ *
+ * @var string
+ */
+ protected $queriedContext = '';
+
+ /** {@inheritdoc} */
public function getItems(){
return $this->items;
}
/**
- * Sets the input values for a given context. Existing values are
- * overwritten.
+ * Sets the input values for a given context.
*
* @param array $inputs Associative array of inputs
- * @param string $context The context name
+ * @param string $queriedContext The context name
+ * @return void
*/
protected function setInputs(array $inputs, $queriedContext){
// Import and assign all inputs to their context
@@ -103,9 +183,15 @@ abstract class BridgeAbstract implements BridgeInterface {
}
/**
- * Defined datas with parameters depending choose bridge
- * @param array array with expected bridge paramters
- */
+ * Set inputs for the bridge
+ *
+ * Returns errors and aborts execution if the provided input parameters are
+ * invalid.
+ *
+ * @param array List of input parameters. Each element in this list must
+ * relate to an item in {@see BridgeAbstract::PARAMETERS}
+ * @return void
+ */
public function setDatas(array $inputs){
if(empty(static::PARAMETERS)) {
@@ -148,7 +234,7 @@ abstract class BridgeAbstract implements BridgeInterface {
* Returns the value for the provided input
*
* @param string $input The input name
- * @return mixed Returns the input value or null if the input is not defined
+ * @return mixed|null The input value or null if the input is not defined
*/
protected function getInput($input){
if(!isset($this->inputs[$this->queriedContext][$input]['value'])) {
@@ -157,32 +243,52 @@ abstract class BridgeAbstract implements BridgeInterface {
return $this->inputs[$this->queriedContext][$input]['value'];
}
+ /** {@inheritdoc} */
public function getDescription(){
return static::DESCRIPTION;
}
+ /** {@inheritdoc} */
public function getMaintainer(){
return static::MAINTAINER;
}
+ /** {@inheritdoc} */
public function getName(){
return static::NAME;
}
+ /** {@inheritdoc} */
public function getIcon(){
return '';
}
+ /** {@inheritdoc} */
public function getParameters(){
return static::PARAMETERS;
}
+ /** {@inheritdoc} */
public function getURI(){
return static::URI;
}
+ /** {@inheritdoc} */
public function getCacheTimeout(){
return static::CACHE_TIMEOUT;
}
+ /** {@inheritdoc} */
+ public function detectParameters($url){
+ $regex = '/^(https?:\/\/)?(www\.)?(.+?)(\/)?$/';
+ if(empty(static::PARAMETERS)
+ && preg_match($regex, $url, $urlMatches) > 0
+ && preg_match($regex, static::URI, $bridgeUriMatches) > 0
+ && $urlMatches[3] === $bridgeUriMatches[3]) {
+ return array();
+ } else {
+ return null;
+ }
+ }
+
}
diff --git a/lib/BridgeCard.php b/lib/BridgeCard.php
index 28e74fe..9cfa9ce 100644
--- a/lib/BridgeCard.php
+++ b/lib/BridgeCard.php
@@ -1,6 +1,32 @@
<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * A generator class for a single bridge card on the home page of RSS-Bridge.
+ *
+ * This class generates the HTML content for a single bridge card for the home
+ * page of RSS-Bridge.
+ *
+ * @todo Return error if a caller creates an object of this class.
+ */
final class BridgeCard {
+ /**
+ * Build a HTML document string of buttons for each of the provided formats
+ *
+ * @param array $formats A list of format names
+ * @return string The document string
+ */
private static function buildFormatButtons($formats) {
$buttons = '';
@@ -16,6 +42,13 @@ final class BridgeCard {
return $buttons;
}
+ /**
+ * Get the form header for a bridge card
+ *
+ * @param string $bridgeName The bridge name
+ * @param bool $isHttps If disabled, adds a warning to the form
+ * @return string The form header
+ */
private static function getFormHeader($bridgeName, $isHttps = false) {
$form = <<<EOD
<form method="GET" action="?">
@@ -31,13 +64,24 @@ This bridge is not fetching its content through a secure connection</div>';
return $form;
}
+ /**
+ * Get the form body for a bridge
+ *
+ * @param string $bridgeName The bridge name
+ * @param array $formats A list of supported formats
+ * @param bool $isActive Indicates if a bridge is enabled or not
+ * @param bool $isHttps Indicates if a bridge uses HTTPS or not
+ * @param string $parameterName Sets the bridge context for the current form
+ * @param array $parameters The bridge parameters
+ * @return string The form body
+ */
private static function getForm($bridgeName,
$formats,
$isActive = false,
$isHttps = false,
$parameterName = '',
$parameters = array()) {
- $form = BridgeCard::getFormHeader($bridgeName, $isHttps);
+ $form = self::getFormHeader($bridgeName, $isHttps);
if(count($parameters) > 0) {
@@ -65,13 +109,13 @@ This bridge is not fetching its content through a secure connection</div>';
. PHP_EOL;
if(!isset($inputEntry['type']) || $inputEntry['type'] === 'text') {
- $form .= BridgeCard::getTextInput($inputEntry, $idArg, $id);
+ $form .= self::getTextInput($inputEntry, $idArg, $id);
} elseif($inputEntry['type'] === 'number') {
- $form .= BridgeCard::getNumberInput($inputEntry, $idArg, $id);
+ $form .= self::getNumberInput($inputEntry, $idArg, $id);
} else if($inputEntry['type'] === 'list') {
- $form .= BridgeCard::getListInput($inputEntry, $idArg, $id);
+ $form .= self::getListInput($inputEntry, $idArg, $id);
} elseif($inputEntry['type'] === 'checkbox') {
- $form .= BridgeCard::getCheckboxInput($inputEntry, $idArg, $id);
+ $form .= self::getCheckboxInput($inputEntry, $idArg, $id);
}
}
@@ -80,7 +124,7 @@ This bridge is not fetching its content through a secure connection</div>';
}
if($isActive) {
- $form .= BridgeCard::buildFormatButtons($formats);
+ $form .= self::buildFormatButtons($formats);
} else {
$form .= '<span style="font-weight: bold;">Inactive</span>';
}
@@ -88,6 +132,12 @@ This bridge is not fetching its content through a secure connection</div>';
return $form . '</form>' . PHP_EOL;
}
+ /**
+ * Get input field attributes
+ *
+ * @param array $entry The current entry
+ * @return string The input field attributes
+ */
private static function getInputAttributes($entry) {
$retVal = '';
@@ -103,9 +153,17 @@ This bridge is not fetching its content through a secure connection</div>';
return $retVal;
}
+ /**
+ * Get text input
+ *
+ * @param array $entry The current entry
+ * @param string $id The field ID
+ * @param string $name The field name
+ * @return string The text input field
+ */
private static function getTextInput($entry, $id, $name) {
return '<input '
- . BridgeCard::getInputAttributes($entry)
+ . self::getInputAttributes($entry)
. ' id="'
. $id
. '" type="text" value="'
@@ -118,9 +176,17 @@ This bridge is not fetching its content through a secure connection</div>';
. PHP_EOL;
}
+ /**
+ * Get number input
+ *
+ * @param array $entry The current entry
+ * @param string $id The field ID
+ * @param string $name The field name
+ * @return string The number input field
+ */
private static function getNumberInput($entry, $id, $name) {
return '<input '
- . BridgeCard::getInputAttributes($entry)
+ . self::getInputAttributes($entry)
. ' id="'
. $id
. '" type="number" value="'
@@ -133,9 +199,17 @@ This bridge is not fetching its content through a secure connection</div>';
. PHP_EOL;
}
+ /**
+ * Get list input
+ *
+ * @param array $entry The current entry
+ * @param string $id The field ID
+ * @param string $name The field name
+ * @return string The list input field
+ */
private static function getListInput($entry, $id, $name) {
$list = '<select '
- . BridgeCard::getInputAttributes($entry)
+ . self::getInputAttributes($entry)
. ' id="'
. $id
. '" name="'
@@ -185,9 +259,17 @@ This bridge is not fetching its content through a secure connection</div>';
return $list;
}
+ /**
+ * Get checkbox input
+ *
+ * @param array $entry The current entry
+ * @param string $id The field ID
+ * @param string $name The field name
+ * @return string The checkbox input field
+ */
private static function getCheckboxInput($entry, $id, $name) {
return '<input '
- . BridgeCard::getInputAttributes($entry)
+ . self::getInputAttributes($entry)
. ' id="'
. $id
. '" type="checkbox" name="'
@@ -198,6 +280,14 @@ This bridge is not fetching its content through a secure connection</div>';
. PHP_EOL;
}
+ /**
+ * Gets a single bridge card
+ *
+ * @param string $bridgeName The bridge name
+ * @param array $formats A list of formats
+ * @param bool $isActive Indicates if the bridge is active or not
+ * @return string The bridge card
+ */
static function displayBridgeCard($bridgeName, $formats, $isActive = true){
$bridge = Bridge::create($bridgeName);
@@ -240,7 +330,7 @@ CARD;
if(count($parameters) === 0
|| count($parameters) === 1 && array_key_exists('global', $parameters)) {
- $card .= BridgeCard::getForm($bridgeName, $formats, $isActive, $isHttps);
+ $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps);
} else {
@@ -254,7 +344,7 @@ CARD;
if(!is_numeric($parameterName))
$card .= '<h5>' . $parameterName . '</h5>' . PHP_EOL;
- $card .= BridgeCard::getForm($bridgeName, $formats, $isActive, $isHttps, $parameterName, $parameter);
+ $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps, $parameterName, $parameter);
}
}
diff --git a/lib/BridgeInterface.php b/lib/BridgeInterface.php
index f2ff11d..803bb3c 100644
--- a/lib/BridgeInterface.php
+++ b/lib/BridgeInterface.php
@@ -1,4 +1,57 @@
<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * The bridge interface
+ *
+ * A bridge is a class that is responsible for collecting and transforming data
+ * from one hosting provider into an internal representation of feed data, that
+ * can later be transformed into different feed formats (see {@see FormatInterface}).
+ *
+ * For this purpose, all bridges need to perform three common operations:
+ *
+ * 1. Collect data from a remote site.
+ * 2. Extract the required contents.
+ * 3. Add the contents to the internal data structure.
+ *
+ * Bridges can optionally specify parameters to customize bridge behavior based
+ * on user input. For example, a user could specify how many items to return in
+ * the feed and where to get them.
+ *
+ * In order to present a bridge on the home page, and for the purpose of bridge
+ * specific behaviour, additional information must be provided by the bridge:
+ *
+ * * **Name**
+ * The name of the bridge that can be displayed to users.
+ *
+ * * **Description**
+ * A brief description for the bridge that can be displayed to users.
+ *
+ * * **URI**
+ * A link to the hosting provider.
+ *
+ * * **Maintainer**
+ * The GitHub username of the bridge maintainer
+ *
+ * * **Parameters**
+ * A list of parameters for customization
+ *
+ * * **Icon**
+ * A link to the favicon of the hosting provider
+ *
+ * * **Cache timeout**
+ * The default cache timeout for the bridge.
+ */
interface BridgeInterface {
/**
@@ -61,4 +114,12 @@ interface BridgeInterface {
* @return int Cache timeout
*/
public function getCacheTimeout();
+
+ /**
+ * Returns parameters from given URL or null if URL is not applicable
+ *
+ * @param string $url URL to extract parameters from
+ * @return array|null List of bridge parameters or null if detection failed.
+ */
+ public function detectParameters($url);
}
diff --git a/lib/BridgeList.php b/lib/BridgeList.php
index 4c59f10..5ff13cf 100644
--- a/lib/BridgeList.php
+++ b/lib/BridgeList.php
@@ -1,6 +1,31 @@
<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * A generator class for the home page of RSS-Bridge.
+ *
+ * This class generates the HTML content for displaying all bridges on the home
+ * page of RSS-Bridge.
+ *
+ * @todo Return error if a caller creates an object of this class.
+ */
final class BridgeList {
+ /**
+ * Get the document head
+ *
+ * @return string The document head
+ */
private static function getHead() {
return <<<EOD
<head>
@@ -22,20 +47,29 @@ final class BridgeList {
EOD;
}
- private static function getBridges($whitelist, $showInactive, &$totalBridges, &$totalActiveBridges) {
+ /**
+ * Get the document body for all bridge cards
+ *
+ * @param bool $showInactive Inactive bridges are visible on the home page if
+ * enabled.
+ * @param int $totalBridges (ref) Returns the total number of bridges.
+ * @param int $totalActiveBridges (ref) Returns the number of active bridges.
+ * @return string The document body for all bridge cards.
+ */
+ private static function getBridges($showInactive, &$totalBridges, &$totalActiveBridges) {
$body = '';
$totalActiveBridges = 0;
$inactiveBridges = '';
- $bridgeList = Bridge::listBridges();
- $formats = Format::searchInformation();
+ $bridgeList = Bridge::getBridgeNames();
+ $formats = Format::getFormatNames();
$totalBridges = count($bridgeList);
foreach($bridgeList as $bridgeName) {
- if(Bridge::isWhitelisted($whitelist, strtolower($bridgeName))) {
+ if(Bridge::isWhitelisted($bridgeName)) {
$body .= BridgeCard::displayBridgeCard($bridgeName, $formats);
$totalActiveBridges++;
@@ -54,19 +88,24 @@ EOD;
return $body;
}
+ /**
+ * Get the document header
+ *
+ * @return string The document header
+ */
private static function getHeader() {
$warning = '';
- if(defined('DEBUG') && DEBUG === true) {
- if(defined('DEBUG_INSECURE') && DEBUG_INSECURE === true) {
+ if(Debug::isEnabled()) {
+ if(!Debug::isSecure()) {
$warning .= <<<EOD
-<section class="critical-warning">Warning : Debug mode is active from any location,
-make sure only you can access RSS-Bridge.</section>
+<section class="critical-warning">Warning : Debug mode is active from any location,
+ make sure only you can access RSS-Bridge.</section>
EOD;
} else {
$warning .= <<<EOD
-<section class="warning">Warning : Debug mode is active from your IP address,
-your requests will bypass the cache.</section>
+<section class="warning">Warning : Debug mode is active from your IP address,
+ your requests will bypass the cache.</section>
EOD;
}
}
@@ -80,6 +119,11 @@ EOD;
EOD;
}
+ /**
+ * Get the searchbar
+ *
+ * @return string The searchbar
+ */
private static function getSearchbar() {
$query = filter_input(INPUT_GET, 'q');
@@ -93,6 +137,16 @@ EOD;
EOD;
}
+ /**
+ * Get the document footer
+ *
+ * @param int $totalBridges The total number of bridges, shown in the footer
+ * @param int $totalActiveBridges The total number of active bridges, shown
+ * in the footer.
+ * @param bool $showInactive Sets the 'Show active'/'Show inactive' text in
+ * the footer.
+ * @return string The document footer
+ */
private static function getFooter($totalBridges, $totalActiveBridges, $showInactive) {
$version = Configuration::getVersion();
@@ -131,7 +185,14 @@ EOD;
EOD;
}
- static function create($whitelist, $showInactive = true) {
+ /**
+ * Create the entire home page
+ *
+ * @param bool $showInactive Inactive bridges are displayed on the home page,
+ * if enabled.
+ * @return string The home page
+ */
+ static function create($showInactive = true) {
$totalBridges = 0;
$totalActiveBridges = 0;
@@ -141,7 +202,7 @@ EOD;
. '<body onload="search()">'
. BridgeList::getHeader()
. BridgeList::getSearchbar()
- . BridgeList::getBridges($whitelist, $showInactive, $totalBridges, $totalActiveBridges)
+ . BridgeList::getBridges($showInactive, $totalBridges, $totalActiveBridges)
. BridgeList::getFooter($totalBridges, $totalActiveBridges, $showInactive)
. '</body></html>';
diff --git a/lib/Cache.php b/lib/Cache.php
index 88be5a1..a0d2ac7 100644
--- a/lib/Cache.php
+++ b/lib/Cache.php
@@ -1,53 +1,140 @@
<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+/**
+ * Factory class responsible for creating cache objects from a given working
+ * directory.
+ *
+ * This class is capable of:
+ * - Locating cache classes in the specified working directory (see {@see Cache::$workingDir})
+ * - Creating new cache instances based on the cache's name (see {@see Cache::create()})
+ *
+ * The following example illustrates the intended use for this class.
+ *
+ * ```PHP
+ * require_once __DIR__ . '/rssbridge.php';
+ *
+ * // Step 1: Set the working directory
+ * Cache::setWorkingDir(__DIR__ . '/../caches/');
+ *
+ * // Step 2: Create a new instance of a cache object (based on the name)
+ * $cache = Cache::create('FileCache');
+ * ```
+ */
class Cache {
- static protected $dirCache;
+ /**
+ * Holds a path to the working directory.
+ *
+ * Do not access this property directly!
+ * Use {@see Cache::setWorkingDir()} and {@see Cache::getWorkingDir()} instead.
+ *
+ * @var string|null
+ */
+ protected static $workingDir = null;
+ /**
+ * Throws an exception when trying to create a new instance of this class.
+ * Use {@see Cache::create()} to create a new cache object from the working
+ * directory.
+ *
+ * @throws \LogicException if called.
+ */
public function __construct(){
- throw new \LogicException('Please use ' . __CLASS__ . '::create for new object.');
+ throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create cache objects!');
}
- static public function create($nameCache){
- if(!static::isValidNameCache($nameCache)) {
- throw new \InvalidArgumentException('Name cache must be at least one
- uppercase follow or not by alphanumeric or dash characters.');
+ /**
+ * Creates a new cache object from the working directory.
+ *
+ * @throws \InvalidArgumentException if the requested cache name is invalid.
+ * @throws \Exception if the requested cache file doesn't exist in the
+ * working directory.
+ * @param string $name Name of the cache object.
+ * @return object|bool The cache object or false if the class is not instantiable.
+ */
+ public static function create($name){
+ if(!self::isCacheName($name)) {
+ throw new \InvalidArgumentException('Cache name invalid!');
}
- $pathCache = self::getDir() . $nameCache . '.php';
+ $filePath = self::getWorkingDir() . $name . '.php';
- if(!file_exists($pathCache)) {
- throw new \Exception('The cache you looking for does not exist.');
+ if(!file_exists($filePath)) {
+ throw new \Exception('Cache file ' . $filePath . ' does not exist!');
}
- require_once $pathCache;
+ require_once $filePath;
- return new $nameCache();
+ if((new \ReflectionClass($name))->isInstantiable()) {
+ return new $name();
+ }
+
+ return false;
}
- static public function setDir($dirCache){
- if(!is_string($dirCache)) {
- throw new \InvalidArgumentException('Dir cache must be a string.');
+ /**
+ * Sets the working directory.
+ *
+ * @param string $dir Path to a directory containing cache classes
+ * @throws \InvalidArgumentException if $dir is not a string.
+ * @throws \Exception if the working directory doesn't exist.
+ * @throws \InvalidArgumentException if $dir is not a directory.
+ * @return void
+ */
+ public static function setWorkingDir($dir){
+ self::$workingDir = null;
+
+ if(!is_string($dir)) {
+ throw new \InvalidArgumentException('Working directory is not a valid string!');
}
- if(!file_exists($dirCache)) {
- throw new \Exception('Dir cache does not exist.');
+ if(!file_exists($dir)) {
+ throw new \Exception('Working directory does not exist!');
}
- self::$dirCache = $dirCache;
- }
+ if(!is_dir($dir)) {
+ throw new \InvalidArgumentException('Working directory is not a directory!');
+ }
- static public function getDir(){
- $dirCache = self::$dirCache;
+ self::$workingDir = realpath($dir) . '/';
+ }
- if(is_null($dirCache)) {
- throw new \LogicException(__CLASS__ . ' class need to know cache path !');
+ /**
+ * Returns the working directory.
+ * The working directory must be set with {@see Cache::setWorkingDir()}!
+ *
+ * @throws \LogicException if the working directory is not set.
+ * @return string The current working directory.
+ */
+ public static function getWorkingDir(){
+ if(is_null(self::$workingDir)) {
+ throw new \LogicException('Working directory is not set!');
}
- return $dirCache;
+ return self::$workingDir;
}
- static public function isValidNameCache($nameCache){
- return preg_match('@^[A-Z][a-zA-Z0-9-]*$@', $nameCache);
+ /**
+ * Returns true if the provided name is a valid cache name.
+ *
+ * A valid cache name starts with a capital letter ([A-Z]), followed by
+ * zero or more alphanumeric characters or hyphen ([A-Za-z0-9-]).
+ *
+ * @param string $name The cache name.
+ * @return bool true if the name is a valid cache name, false otherwise.
+ */
+ public static function isCacheName($name){
+ return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
}
}
diff --git a/lib/CacheInterface.php b/lib/CacheInterface.php
index 5753c0e..bd2d561 100644
--- a/lib/CacheInterface.php
+++ b/lib/CacheInterface.php
@@ -1,7 +1,51 @@
<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * The cache interface
+ *
+ * @todo Add missing function to the interface
+ * @todo Explain parameters and return values in more detail
+ * @todo Return self more often (to allow call chaining)
+ */
interface CacheInterface {
+
+ /**
+ * Loads data from cache
+ *
+ * @return mixed The cache data
+ */
public function loadData();
+
+ /**
+ * Stores data to the cache
+ *
+ * @param mixed $datas The data to store
+ * @return self The cache object
+ */
public function saveData($datas);
+
+ /**
+ * Returns the timestamp for the curent cache file
+ *
+ * @return int Timestamp
+ */
public function getTime();
+
+ /**
+ * Removes any data that is older than the specified duration from cache
+ *
+ * @param int $duration The cache duration in seconds
+ */
public function purgeCache($duration);
}
diff --git a/lib/Configuration.php b/lib/Configuration.php
index 3002822..a7b429b 100644
--- a/lib/Configuration.php
+++ b/lib/Configuration.php
@@ -1,10 +1,81 @@
<?php
-class Configuration {
-
- public static $VERSION = '2018-11-10';
-
- public static $config = null;
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * Configuration module for RSS-Bridge.
+ *
+ * This class implements a configuration module for RSS-Bridge.
+ */
+final class Configuration {
+
+ /**
+ * Holds the current release version of RSS-Bridge.
+ *
+ * Do not access this property directly!
+ * Use {@see Configuration::getVersion()} instead.
+ *
+ * @var string
+ *
+ * @todo Replace this property by a constant.
+ */
+ public static $VERSION = '2018-12-11';
+
+ /**
+ * Holds the configuration data.
+ *
+ * Do not access this property directly!
+ * Use {@see Configuration::getConfig()} instead.
+ *
+ * @var array|null
+ */
+ private static $config = null;
+
+ /**
+ * Throw an exception when trying to create a new instance of this class.
+ *
+ * @throws \LogicException if called.
+ */
+ public function __construct(){
+ throw new \LogicException('Can\'t create object of this class!');
+ }
+ /**
+ * Verifies the current installation of RSS-Bridge and PHP.
+ *
+ * Returns an error message and aborts execution if the installation does
+ * not satisfy the requirements of RSS-Bridge.
+ *
+ * **Requirements**
+ * - PHP 5.6.0 or higher
+ * - `openssl` extension
+ * - `libxml` extension
+ * - `mbstring` extension
+ * - `simplexml` extension
+ * - `curl` extension
+ * - `json` extension
+ * - The cache folder specified by {@see PATH_CACHE} requires write permission
+ * - The whitelist file specified by {@see WHITELIST} requires write permission
+ *
+ * @link http://php.net/supported-versions.php PHP Supported Versions
+ * @link http://php.net/manual/en/book.openssl.php OpenSSL
+ * @link http://php.net/manual/en/book.libxml.php libxml
+ * @link http://php.net/manual/en/book.mbstring.php Multibyte String (mbstring)
+ * @link http://php.net/manual/en/book.simplexml.php SimpleXML
+ * @link http://php.net/manual/en/book.curl.php Client URL Library (curl)
+ * @link http://php.net/manual/en/book.json.php JavaScript Object Notation (json)
+ *
+ * @return void
+ */
public static function verifyInstallation() {
// Check PHP version
@@ -34,24 +105,50 @@ class Configuration {
if(!is_writable(PATH_CACHE))
die('RSS-Bridge does not have write permissions for ' . PATH_CACHE . '!');
- // Check whitelist file permissions (only in DEBUG mode)
- if(!file_exists(WHITELIST_FILE) && !is_writable(dirname(WHITELIST_FILE)))
- die('RSS-Bridge does not have write permissions for ' . WHITELIST_FILE . '!');
+ // Check whitelist file permissions
+ if(!file_exists(WHITELIST) && !is_writable(dirname(WHITELIST)))
+ die('RSS-Bridge does not have write permissions for ' . WHITELIST . '!');
}
+ /**
+ * Loads the configuration from disk and checks if the parameters are valid.
+ *
+ * Returns an error message and aborts execution if the configuration is invalid.
+ *
+ * The RSS-Bridge configuration is split into two files:
+ * - `config.default.ini.php`: The default configuration file that ships with
+ * every release of RSS-Bridge (do not modify this file!).
+ * - `config.ini.php`: The local configuration file that can be modified by
+ * server administrators.
+ *
+ * The files must be located at {@see PATH_ROOT}
+ *
+ * RSS-Bridge will first load `config.default.ini.php` into memory and then
+ * replace parameters with the contents of `config.ini.php`. That way new
+ * parameters are automatically initialized with default values and custom
+ * configurations can be reduced to the minimum set of parametes necessary
+ * (only the ones that changed).
+ *
+ * The configuration files must be placed in the root folder of RSS-Bridge
+ * (next to `index.php`).
+ *
+ * _Notice_: The configuration is stored in {@see Configuration::$config}.
+ *
+ * @return void
+ */
public static function loadConfiguration() {
- if(!file_exists('config.default.ini.php'))
+ if(!file_exists(PATH_ROOT . 'config.default.ini.php'))
die('The default configuration file "config.default.ini.php" is missing!');
- Configuration::$config = parse_ini_file('config.default.ini.php', true, INI_SCANNER_TYPED);
+ Configuration::$config = parse_ini_file(PATH_ROOT . 'config.default.ini.php', true, INI_SCANNER_TYPED);
if(!Configuration::$config)
die('Error parsing config.default.ini.php');
- if(file_exists('config.ini.php')) {
+ if(file_exists(PATH_ROOT . 'config.ini.php')) {
// Replace default configuration with custom settings
- foreach(parse_ini_file('config.ini.php', true, INI_SCANNER_TYPED) as $header => $section) {
+ foreach(parse_ini_file(PATH_ROOT . 'config.ini.php', true, INI_SCANNER_TYPED) as $header => $section) {
foreach($section as $key => $value) {
// Skip unknown sections and keys
if(array_key_exists($header, Configuration::$config) && array_key_exists($key, Configuration::$config[$header])) {
@@ -64,22 +161,27 @@ class Configuration {
if(!is_string(self::getConfig('proxy', 'url')))
die('Parameter [proxy] => "url" is not a valid string! Please check "config.ini.php"!');
- if(!empty(self::getConfig('proxy', 'url')))
+ if(!empty(self::getConfig('proxy', 'url'))) {
+ /** URL of the proxy server */
define('PROXY_URL', self::getConfig('proxy', 'url'));
+ }
if(!is_bool(self::getConfig('proxy', 'by_bridge')))
die('Parameter [proxy] => "by_bridge" is not a valid Boolean! Please check "config.ini.php"!');
+ /** True if proxy usage can be enabled selectively for each bridge */
define('PROXY_BYBRIDGE', self::getConfig('proxy', 'by_bridge'));
if(!is_string(self::getConfig('proxy', 'name')))
die('Parameter [proxy] => "name" is not a valid string! Please check "config.ini.php"!');
+ /** Name of the proxy server */
define('PROXY_NAME', self::getConfig('proxy', 'name'));
if(!is_bool(self::getConfig('cache', 'custom_timeout')))
die('Parameter [cache] => "custom_timeout" is not a valid Boolean! Please check "config.ini.php"!');
+ /** True if the cache timeout can be specified by the user */
define('CUSTOM_CACHE_TIMEOUT', self::getConfig('cache', 'custom_timeout'));
if(!is_bool(self::getConfig('authentication', 'enable')))
@@ -91,21 +193,41 @@ class Configuration {
if(!is_string(self::getConfig('authentication', 'password')))
die('Parameter [authentication] => "password" is not a valid string! Please check "config.ini.php"!');
- }
+ if(!empty(self::getConfig('admin', 'email'))
+ && !filter_var(self::getConfig('admin', 'email'), FILTER_VALIDATE_EMAIL))
+ die('Parameter [admin] => "email" is not a valid email address! Please check "config.ini.php"!');
- public static function getConfig($category, $key) {
+ }
- if(array_key_exists($category, self::$config) && array_key_exists($key, self::$config[$category])) {
- return self::$config[$category][$key];
+ /**
+ * Returns the value of a parameter identified by section and key.
+ *
+ * @param string $section The section name.
+ * @param string $key The property name (key).
+ * @return mixed|null The parameter value.
+ */
+ public static function getConfig($section, $key) {
+
+ if(array_key_exists($section, self::$config) && array_key_exists($key, self::$config[$section])) {
+ return self::$config[$section][$key];
}
return null;
}
+ /**
+ * Returns the current version string of RSS-Bridge.
+ *
+ * This function returns the contents of {@see Configuration::$VERSION} for
+ * regular installations and the git branch name and commit id for instances
+ * running in a git environment.
+ *
+ * @return string The version string.
+ */
public static function getVersion() {
- $headFile = '.git/HEAD';
+ $headFile = PATH_ROOT . '.git/HEAD';
// '@' is used to mute open_basedir warning
if(@is_readable($headFile)) {
diff --git a/lib/Debug.php b/lib/Debug.php
new file mode 100644
index 0000000..f912fb3
--- /dev/null
+++ b/lib/Debug.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * Implements functions for debugging purposes. Debugging can be enabled by
+ * placing a file named DEBUG in {@see PATH_ROOT}.
+ *
+ * The file specifies a whitelist of IP addresses on which debug mode will be
+ * enabled. An empty file enables debug mode for everyone (highly discouraged
+ * for public servers!). Each line in the file specifies one client in the
+ * whitelist. For example:
+ *
+ * * `192.168.1.72`
+ * * `127.0.0.1`
+ * * `::1`
+ *
+ * Notice: If you are running RSS-Bridge on your local machine, you need to add
+ * localhost (either `127.0.0.1` for IPv4 or `::1` for IPv6) to your whitelist!
+ *
+ * Warning: In debug mode your server may display sensitive information! For
+ * security reasons it is recommended to whitelist only specific IP addresses.
+ */
+class Debug {
+
+ /**
+ * Indicates if debug mode is enabled.
+ *
+ * Do not access this property directly!
+ * Use {@see Debug::isEnabled()} instead.
+ *
+ * @var bool
+ */
+ private static $enabled = false;
+
+ /**
+ * Indicates if debug mode is secure.
+ *
+ * Do not access this property directly!
+ * Use {@see Debug::isSecure()} instead.
+ *
+ * @var bool
+ */
+ private static $secure = false;
+
+ /**
+ * Returns true if debug mode is enabled
+ *
+ * If debug mode is enabled, sets `display_errors = 1` and `error_reporting = E_ALL`
+ *
+ * @return bool True if enabled.
+ */
+ public static function isEnabled() {
+ static $firstCall = true; // Initialized on first call
+
+ if($firstCall && file_exists(PATH_ROOT . 'DEBUG')) {
+
+ $debug_whitelist = trim(file_get_contents(PATH_ROOT . 'DEBUG'));
+
+ self::$enabled = empty($debug_whitelist) || in_array($_SERVER['REMOTE_ADDR'],
+ explode("\n", str_replace("\r", '', $debug_whitelist)
+ )
+ );
+
+ if(self::$enabled) {
+ ini_set('display_errors', '1');
+ error_reporting(E_ALL);
+
+ self::$secure = !empty($debug_whitelist);
+ }
+
+ $firstCall = false; // Skip check on next call
+
+ }
+
+ return self::$enabled;
+ }
+
+ /**
+ * Returns true if debug mode is enabled only for specific IP addresses.
+ *
+ * Notice: The security flag is set by {@see Debug::isEnabled()}. If this
+ * function is called before {@see Debug::isEnabled()}, the default value is
+ * false!
+ *
+ * @return bool True if debug mode is secure
+ */
+ public static function isSecure() {
+ return self::$secure;
+ }
+
+ /**
+ * Adds a debug message to error_log if debug mode is enabled
+ *
+ * @param string $text The message to add to error_log
+ */
+ public static function log($text) {
+ if(!self::isEnabled()) {
+ return;
+ }
+
+ $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
+ $calling = end($backtrace);
+ $message = $calling['file'] . ':'
+ . $calling['line'] . ' class '
+ . (isset($calling['class']) ? $calling['class'] : '<no-class>') . '->'
+ . $calling['function'] . ' - '
+ . $text;
+
+ error_log($message);
+ }
+}
diff --git a/lib/Exceptions.php b/lib/Exceptions.php
index 32b33f2..ac452d0 100644
--- a/lib/Exceptions.php
+++ b/lib/Exceptions.php
@@ -1,17 +1,28 @@
<?php
-class HttpException extends \Exception{}
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
/**
* Returns an URL that automatically populates a new issue on GitHub based
* on the information provided
*
- * @param $title string Sets the title of the issue
- * @param $body string Sets the body of the issue (GitHub markdown applies)
- * @param $labels mixed (optional) Specifies labels to add to the issue
- * @param $maintainer string (optional) Specifies the maintainer for the issue.
+ * @param string $title string Sets the title of the issue
+ * @param string $body string Sets the body of the issue (GitHub markdown applies)
+ * @param string $labels mixed (optional) Specifies labels to add to the issue
+ * @param string $maintainer string (optional) Specifies the maintainer for the issue.
* The maintainer only applies if part of the development team!
- * @return string Returns a qualified URL to a new issue with populated conent.
- * Returns null if title or body is null or empty
+ * @return string|null A qualified URL to a new issue with populated conent or null.
+ *
+ * @todo This function belongs inside a class
*/
function buildGitHubIssueQuery($title, $body, $labels = null, $maintainer = null){
if(!isset($title) || !isset($body) || empty($title) || empty($body)) {
@@ -49,10 +60,11 @@ function buildGitHubIssueQuery($title, $body, $labels = null, $maintainer = null
/**
* Returns the exception message as HTML string
*
- * @param $e Exception The exception to show
- * @param $bridge object The bridge object
- * @return string Returns the exception as HTML string. Returns null if the
- * provided parameter are invalid
+ * @param object $e Exception The exception to show
+ * @param object $bridge object The bridge object
+ * @return string|null Returns the exception as HTML string or null.
+ *
+ * @todo This function belongs inside a class
*/
function buildBridgeException($e, $bridge){
if(( !($e instanceof \Exception) && !($e instanceof \Error)) || !($bridge instanceof \BridgeInterface)) {
@@ -65,7 +77,7 @@ function buildBridgeException($e, $bridge){
$body = 'Error message: `'
. $e->getMessage()
. "`\nQuery string: `"
- . $_SERVER['QUERY_STRING']
+ . (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '')
. "`\nVersion: `"
. Configuration::getVersion()
. '`';
@@ -87,10 +99,11 @@ EOD;
/**
* Returns the exception message as HTML string
*
- * @param $e Exception The exception to show
- * @param $bridge object The bridge object
- * @return string Returns the exception as HTML string. Returns null if the
- * provided parameter are invalid
+ * @param object $e Exception The exception to show
+ * @param object $bridge object The bridge object
+ * @return string|null Returns the exception as HTML string or null.
+ *
+ * @todo This function belongs inside a class
*/
function buildTransformException($e, $bridge){
if(( !($e instanceof \Exception) && !($e instanceof \Error)) || !($bridge instanceof \BridgeInterface)) {
@@ -103,7 +116,8 @@ function buildTransformException($e, $bridge){
$body = 'Error message: `'
. $e->getMessage()
. "`\nQuery string: `"
- . $_SERVER['QUERY_STRING'] . '`';
+ . (isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '')
+ . '`';
$link = buildGitHubIssueQuery($title, $body, 'bug report', $bridge->getMaintainer());
$header = buildHeader($e, $bridge);
@@ -114,6 +128,15 @@ function buildTransformException($e, $bridge){
return buildPage($title, $header, $section);
}
+/**
+ * Builds a new HTML header with data from a exception an a bridge
+ *
+ * @param object $e The exception object
+ * @param object $bridge The bridge object
+ * @return string The HTML header
+ *
+ * @todo This function belongs inside a class
+ */
function buildHeader($e, $bridge){
return <<<EOD
<header>
@@ -124,6 +147,17 @@ function buildHeader($e, $bridge){
EOD;
}
+/**
+ * Builds a new HTML section
+ *
+ * @param object $e The exception object
+ * @param object $bridge The bridge object
+ * @param string $message The message to display
+ * @param string $link The link to include in the anchor
+ * @return string The HTML section
+ *
+ * @todo This function belongs inside a class
+ */
function buildSection($e, $bridge, $message, $link){
return <<<EOD
<section>
@@ -142,6 +176,16 @@ function buildSection($e, $bridge, $message, $link){
EOD;
}
+/**
+ * Builds a new HTML page
+ *
+ * @param string $title The HTML title
+ * @param string $header The HTML header
+ * @param string $section The HTML section
+ * @return string The HTML page
+ *
+ * @todo This function belongs inside a class
+ */
function buildPage($title, $header, $section){
return <<<EOD
<!DOCTYPE html>
diff --git a/lib/FeedExpander.php b/lib/FeedExpander.php
index 269de49..b669351 100644
--- a/lib/FeedExpander.php
+++ b/lib/FeedExpander.php
@@ -1,17 +1,86 @@
<?php
-
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * An abstract class for bridges that need to transform existing RSS or Atom
+ * feeds.
+ *
+ * This class extends {@see BridgeAbstract} with functions to extract contents
+ * from existing RSS or Atom feeds. Bridges that need to transform existing feeds
+ * should inherit from this class instead of {@see BridgeAbstract}.
+ *
+ * Bridges that extend this class don't need to concern themselves with getting
+ * contents from existing feeds, but can focus on adding additional contents
+ * (i.e. by downloading additional data), filtering or just transforming a feed
+ * into another format.
+ *
+ * @link http://www.rssboard.org/rss-0-9-1 RSS 0.91 Specification
+ * @link http://web.resource.org/rss/1.0/spec RDF Site Summary (RSS) 1.0
+ * @link http://www.rssboard.org/rss-specification RSS 2.0 Specification
+ * @link https://tools.ietf.org/html/rfc4287 The Atom Syndication Format
+ *
+ * @todo The parsing functions should all be private. This class is complicated
+ * enough without having to consider children overriding functions.
+ */
abstract class FeedExpander extends BridgeAbstract {
- private $name;
+ /** Indicates an RSS 1.0 feed */
+ const FEED_TYPE_RSS_1_0 = 'RSS_1_0';
+
+ /** Indicates an RSS 2.0 feed */
+ const FEED_TYPE_RSS_2_0 = 'RSS_2_0';
+
+ /** Indicates an Atom 1.0 feed */
+ const FEED_TYPE_ATOM_1_0 = 'ATOM_1_0';
+
+ /**
+ * Holds the title of the current feed
+ *
+ * @var string
+ */
+ private $title;
+
+ /**
+ * Holds the URI of the feed
+ *
+ * @var string
+ */
private $uri;
+
+ /**
+ * Holds the feed type during internal operations.
+ *
+ * @var string
+ */
private $feedType;
+ /**
+ * Collects data from an existing feed.
+ *
+ * Children should call this function in {@see BridgeInterface::collectData()}
+ * to extract a feed.
+ *
+ * @param string $url URL to the feed.
+ * @param int $maxItems Maximum number of items to collect from the feed
+ * (`-1`: no limit).
+ * @return self
+ */
public function collectExpandableDatas($url, $maxItems = -1){
if(empty($url)) {
returnServerError('There is no $url for this RSS expander');
}
- debugMessage('Loading from ' . $url);
+ Debug::log('Loading from ' . $url);
/* Notice we do not use cache here on purpose:
* we want a fresh view of the RSS stream each time
@@ -20,34 +89,49 @@ abstract class FeedExpander extends BridgeAbstract {
or returnServerError('Could not request ' . $url);
$rssContent = simplexml_load_string(trim($content));
- debugMessage('Detecting feed format/version');
+ Debug::log('Detecting feed format/version');
switch(true) {
case isset($rssContent->item[0]):
- debugMessage('Detected RSS 1.0 format');
- $this->feedType = 'RSS_1_0';
+ Debug::log('Detected RSS 1.0 format');
+ $this->feedType = self::FEED_TYPE_RSS_1_0;
break;
case isset($rssContent->channel[0]):
- debugMessage('Detected RSS 0.9x or 2.0 format');
- $this->feedType = 'RSS_2_0';
+ Debug::log('Detected RSS 0.9x or 2.0 format');
+ $this->feedType = self::FEED_TYPE_RSS_2_0;
break;
case isset($rssContent->entry[0]):
- debugMessage('Detected ATOM format');
- $this->feedType = 'ATOM_1_0';
+ Debug::log('Detected ATOM format');
+ $this->feedType = self::FEED_TYPE_ATOM_1_0;
break;
default:
- debugMessage('Unknown feed format/version');
+ Debug::log('Unknown feed format/version');
returnServerError('The feed format is unknown!');
break;
}
- debugMessage('Calling function "collect_' . $this->feedType . '_data"');
+ Debug::log('Calling function "collect_' . $this->feedType . '_data"');
$this->{'collect_' . $this->feedType . '_data'}($rssContent, $maxItems);
+
+ return $this;
}
+ /**
+ * Collect data from a RSS 1.0 compatible feed
+ *
+ * @link http://web.resource.org/rss/1.0/spec RDF Site Summary (RSS) 1.0
+ *
+ * @param string $rssContent The RSS content
+ * @param int $maxItems Maximum number of items to collect from the feed
+ * (`-1`: no limit).
+ * @return void
+ *
+ * @todo Instead of passing $maxItems to all functions, just add all items
+ * and remove excessive items later.
+ */
protected function collect_RSS_1_0_data($rssContent, $maxItems){
$this->load_RSS_2_0_feed_data($rssContent->channel[0]);
foreach($rssContent->item as $item) {
- debugMessage('parsing item ' . var_export($item, true));
+ Debug::log('parsing item ' . var_export($item, true));
$tmp_item = $this->parseItem($item);
if (!empty($tmp_item)) {
$this->items[] = $tmp_item;
@@ -56,15 +140,28 @@ abstract class FeedExpander extends BridgeAbstract {
}
}
+ /**
+ * Collect data from a RSS 2.0 compatible feed
+ *
+ * @link http://www.rssboard.org/rss-specification RSS 2.0 Specification
+ *
+ * @param object $rssContent The RSS content
+ * @param int $maxItems Maximum number of items to collect from the feed
+ * (`-1`: no limit).
+ * @return void
+ *
+ * @todo Instead of passing $maxItems to all functions, just add all items
+ * and remove excessive items later.
+ */
protected function collect_RSS_2_0_data($rssContent, $maxItems){
$rssContent = $rssContent->channel[0];
- debugMessage('RSS content is ===========\n'
+ Debug::log('RSS content is ===========\n'
. var_export($rssContent, true)
. '===========');
$this->load_RSS_2_0_feed_data($rssContent);
foreach($rssContent->item as $item) {
- debugMessage('parsing item ' . var_export($item, true));
+ Debug::log('parsing item ' . var_export($item, true));
$tmp_item = $this->parseItem($item);
if (!empty($tmp_item)) {
$this->items[] = $tmp_item;
@@ -73,10 +170,23 @@ abstract class FeedExpander extends BridgeAbstract {
}
}
+ /**
+ * Collect data from a Atom 1.0 compatible feed
+ *
+ * @link https://tools.ietf.org/html/rfc4287 The Atom Syndication Format
+ *
+ * @param object $content The Atom content
+ * @param int $maxItems Maximum number of items to collect from the feed
+ * (`-1`: no limit).
+ * @return void
+ *
+ * @todo Instead of passing $maxItems to all functions, just add all items
+ * and remove excessive items later.
+ */
protected function collect_ATOM_1_0_data($content, $maxItems){
$this->load_ATOM_feed_data($content);
foreach($content->entry as $item) {
- debugMessage('parsing item ' . var_export($item, true));
+ Debug::log('parsing item ' . var_export($item, true));
$tmp_item = $this->parseItem($item);
if (!empty($tmp_item)) {
$this->items[] = $tmp_item;
@@ -85,18 +195,37 @@ abstract class FeedExpander extends BridgeAbstract {
}
}
+ /**
+ * Convert RSS 2.0 time to timestamp
+ *
+ * @param object $item A feed item
+ * @return int The timestamp
+ */
protected function RSS_2_0_time_to_timestamp($item){
return DateTime::createFromFormat('D, d M Y H:i:s e', $item->pubDate)->getTimestamp();
}
- // TODO set title, link, description, language, and so on
+ /**
+ * Load RSS 2.0 feed data into RSS-Bridge
+ *
+ * @param object $rssContent The RSS content
+ * @return void
+ *
+ * @todo set title, link, description, language, and so on
+ */
protected function load_RSS_2_0_feed_data($rssContent){
- $this->name = trim((string)$rssContent->title);
+ $this->title = trim((string)$rssContent->title);
$this->uri = trim((string)$rssContent->link);
}
+ /**
+ * Load Atom feed data into RSS-Bridge
+ *
+ * @param object $content The Atom content
+ * @return void
+ */
protected function load_ATOM_feed_data($content){
- $this->name = (string)$content->title;
+ $this->title = (string)$content->title;
// Find best link (only one, or first of 'alternate')
if(!isset($content->link)) {
@@ -114,6 +243,16 @@ abstract class FeedExpander extends BridgeAbstract {
}
}
+ /**
+ * Parse the contents of a single Atom feed item into a RSS-Bridge item for
+ * further transformation.
+ *
+ * @param object $feedItem A single feed item
+ * @return object The RSS-Bridge item
+ *
+ * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
+ * of its own?
+ */
protected function parseATOMItem($feedItem){
// Some ATOM entries also contain RSS 2.0 fields
$item = $this->parseRSS_2_0_Item($feedItem);
@@ -139,6 +278,16 @@ abstract class FeedExpander extends BridgeAbstract {
return $item;
}
+ /**
+ * Parse the contents of a single RSS 0.91 feed item into a RSS-Bridge item
+ * for further transformation.
+ *
+ * @param object $feedItem A single feed item
+ * @return object The RSS-Bridge item
+ *
+ * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
+ * of its own?
+ */
protected function parseRSS_0_9_1_Item($feedItem){
$item = array();
if(isset($feedItem->link)) $item['uri'] = (string)$feedItem->link;
@@ -150,6 +299,16 @@ abstract class FeedExpander extends BridgeAbstract {
return $item;
}
+ /**
+ * Parse the contents of a single RSS 1.0 feed item into a RSS-Bridge item
+ * for further transformation.
+ *
+ * @param object $feedItem A single feed item
+ * @return object The RSS-Bridge item
+ *
+ * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
+ * of its own?
+ */
protected function parseRSS_1_0_Item($feedItem){
// 1.0 adds optional elements around the 0.91 standard
$item = $this->parseRSS_0_9_1_Item($feedItem);
@@ -164,6 +323,16 @@ abstract class FeedExpander extends BridgeAbstract {
return $item;
}
+ /**
+ * Parse the contents of a single RSS 2.0 feed item into a RSS-Bridge item
+ * for further transformation.
+ *
+ * @param object $feedItem A single feed item
+ * @return object The RSS-Bridge item
+ *
+ * @todo To reduce confusion, the RSS-Bridge item should maybe have a class
+ * of its own?
+ */
protected function parseRSS_2_0_Item($feedItem){
// Primary data is compatible to 0.91 with some additional data
$item = $this->parseRSS_0_9_1_Item($feedItem);
@@ -211,33 +380,38 @@ abstract class FeedExpander extends BridgeAbstract {
}
/**
- * Method should return, from a source RSS item given by lastRSS, one of our Items objects
- * @param $item the input rss item
- * @return a RSS-Bridge Item, with (hopefully) the whole content)
+ * Parse the contents of a single feed item, depending on the current feed
+ * type, into a RSS-Bridge item.
+ *
+ * @param object $item The current feed item
+ * @return object A RSS-Bridge item, with (hopefully) the whole content
*/
protected function parseItem($item){
switch($this->feedType) {
- case 'RSS_1_0':
+ case self::FEED_TYPE_RSS_1_0:
return $this->parseRSS_1_0_Item($item);
break;
- case 'RSS_2_0':
+ case self::FEED_TYPE_RSS_2_0:
return $this->parseRSS_2_0_Item($item);
break;
- case 'ATOM_1_0':
+ case self::FEED_TYPE_ATOM_1_0:
return $this->parseATOMItem($item);
break;
default: returnClientError('Unknown version ' . $this->getInput('version') . '!');
}
}
+ /** {@inheritdoc} */
public function getURI(){
return !empty($this->uri) ? $this->uri : parent::getURI();
}
+ /** {@inheritdoc} */
public function getName(){
- return !empty($this->name) ? $this->name : parent::getName();
+ return !empty($this->title) ? $this->title : parent::getName();
}
+ /** {@inheritdoc} */
public function getIcon(){
return !empty($this->icon) ? $this->icon : parent::getIcon();
}
diff --git a/lib/Format.php b/lib/Format.php
index d0e4e72..061b1f2 100644
--- a/lib/Format.php
+++ b/lib/Format.php
@@ -1,73 +1,166 @@
<?php
-
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * Factory class responsible for creating format objects from a given working
+ * directory.
+ *
+ * This class is capable of:
+ * - Locating format classes in the specified working directory (see {@see Format::$workingDir})
+ * - Creating new format instances based on the format's name (see {@see Format::create()})
+ *
+ * The following example illustrates the intended use for this class.
+ *
+ * ```PHP
+ * require_once __DIR__ . '/rssbridge.php';
+ *
+ * // Step 1: Set the working directory
+ * Format::setWorkingDir(__DIR__ . '/../formats/');
+ *
+ * // Step 2: Create a new instance of a format object (based on the name)
+ * $format = Format::create('Atom');
+ * ```
+ */
class Format {
- static protected $dirFormat;
+ /**
+ * Holds a path to the working directory.
+ *
+ * Do not access this property directly!
+ * Use {@see Format::setWorkingDir()} and {@see Format::getWorkingDir()} instead.
+ *
+ * @var string|null
+ */
+ protected static $workingDir = null;
+ /**
+ * Throws an exception when trying to create a new instance of this class.
+ * Use {@see Format::create()} to create a new format object from the working
+ * directory.
+ *
+ * @throws \LogicException if called.
+ */
public function __construct(){
- throw new \LogicException('Please use ' . __CLASS__ . '::create for new object.');
+ throw new \LogicException('Use ' . __CLASS__ . '::create($name) to create cache objects!');
}
- static public function create($nameFormat){
- if(!preg_match('@^[A-Z][a-zA-Z]*$@', $nameFormat)) {
- throw new \InvalidArgumentException('Name format must be at least
- one uppercase follow or not by alphabetic characters.');
+ /**
+ * Creates a new format object from the working directory.
+ *
+ * @throws \InvalidArgumentException if the requested format name is invalid.
+ * @throws \Exception if the requested format file doesn't exist in the
+ * working directory.
+ * @param string $name Name of the format object.
+ * @return object|bool The format object or false if the class is not instantiable.
+ */
+ public static function create($name){
+ if(!self::isFormatName($name)) {
+ throw new \InvalidArgumentException('Format name invalid!');
}
- $nameFormat = $nameFormat . 'Format';
- $pathFormat = self::getDir() . $nameFormat . '.php';
+ $name = $name . 'Format';
+ $pathFormat = self::getWorkingDir() . $name . '.php';
if(!file_exists($pathFormat)) {
- throw new \Exception('The format you looking for does not exist.');
+ throw new \Exception('Format file ' . $filePath . ' does not exist!');
}
require_once $pathFormat;
- return new $nameFormat();
+ if((new \ReflectionClass($name))->isInstantiable()) {
+ return new $name();
+ }
+
+ return false;
}
- static public function setDir($dirFormat){
- if(!is_string($dirFormat)) {
+ /**
+ * Sets the working directory.
+ *
+ * @param string $dir Path to a directory containing cache classes
+ * @throws \InvalidArgumentException if $dir is not a string.
+ * @throws \Exception if the working directory doesn't exist.
+ * @throws \InvalidArgumentException if $dir is not a directory.
+ * @return void
+ */
+ public static function setWorkingDir($dir){
+ self::$workingDir = null;
+
+ if(!is_string($dir)) {
throw new \InvalidArgumentException('Dir format must be a string.');
}
- if(!file_exists($dirFormat)) {
- throw new \Exception('Dir format does not exist.');
+ if(!file_exists($dir)) {
+ throw new \Exception('Working directory does not exist!');
}
- self::$dirFormat = $dirFormat;
- }
-
- static public function getDir(){
- $dirFormat = self::$dirFormat;
-
- if(is_null($dirFormat)) {
- throw new \LogicException(__CLASS__ . ' class need to know format path !');
+ if(!is_dir($dir)) {
+ throw new \InvalidArgumentException('Working directory is not a directory!');
}
- return $dirFormat;
+ self::$workingDir = realpath($dir) . '/';
}
/**
- * Read format dir and catch informations about each format depending annotation
- * @return array Informations about each format
- */
- static public function searchInformation(){
- $pathDirFormat = self::getDir();
+ * Returns the working directory.
+ * The working directory must be set with {@see Format::setWorkingDir()}!
+ *
+ * @throws \LogicException if the working directory is not set.
+ * @return string The current working directory.
+ */
+ public static function getWorkingDir(){
+ if(is_null(self::$workingDir)) {
+ throw new \LogicException('Working directory is not set!');
+ }
- $listFormat = array();
+ return self::$workingDir;
+ }
- $searchCommonPattern = array('name');
+ /**
+ * Returns true if the provided name is a valid format name.
+ *
+ * A valid format name starts with a capital letter ([A-Z]), followed by
+ * zero or more alphanumeric characters or hyphen ([A-Za-z0-9-]).
+ *
+ * @param string $name The format name.
+ * @return bool true if the name is a valid format name, false otherwise.
+ */
+ public static function isFormatName($name){
+ return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1;
+ }
- $dirFiles = scandir($pathDirFormat);
- if($dirFiles !== false) {
- foreach($dirFiles as $fileName) {
- if(preg_match('@^([^.]+)Format\.php$@U', $fileName, $out)) { // Is PHP file ?
- $listFormat[] = $out[1];
+ /**
+ * Returns the list of format names from the working directory.
+ *
+ * The list is cached internally to allow for successive calls.
+ *
+ * @return array List of format names
+ */
+ public static function getFormatNames(){
+ static $formatNames = array(); // Initialized on first call
+
+ if(empty($formatNames)) {
+ $files = scandir(self::getWorkingDir());
+
+ if($files !== false) {
+ foreach($files as $file) {
+ if(preg_match('/^([^.]+)Format\.php$/U', $file, $out)) {
+ $formatNames[] = $out[1];
+ }
}
}
}
- return $listFormat;
+ return $formatNames;
}
}
diff --git a/lib/FormatAbstract.php b/lib/FormatAbstract.php
index 8fb642c..5bbd380 100644
--- a/lib/FormatAbstract.php
+++ b/lib/FormatAbstract.php
@@ -1,41 +1,103 @@
<?php
-
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license https://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * An abstract class for format implementations
+ *
+ * This class implements {@see FormatInterface}
+ */
abstract class FormatAbstract implements FormatInterface {
+
+ /** The default charset (UTF-8) */
const DEFAULT_CHARSET = 'UTF-8';
- protected
- $contentType,
- $charset,
- $items,
- $lastModified,
- $extraInfos;
+ /** @var string|null $contentType The content type */
+ protected $contentType = null;
+
+ /** @var string $charset The charset */
+ protected $charset;
+
+ /** @var array $items The items */
+ protected $items;
+
+ /**
+ * @var int $lastModified A timestamp to indicate the last modified time of
+ * the output data.
+ */
+ protected $lastModified;
+
+ /** @var array $extraInfos The extra infos */
+ protected $extraInfos;
+ /**
+ * {@inheritdoc}
+ *
+ * @param string $charset {@inheritdoc}
+ */
public function setCharset($charset){
$this->charset = $charset;
return $this;
}
+ /** {@inheritdoc} */
public function getCharset(){
$charset = $this->charset;
return is_null($charset) ? static::DEFAULT_CHARSET : $charset;
}
+ /**
+ * Set the content type
+ *
+ * @param string $contentType The content type
+ * @return self The format object
+ */
protected function setContentType($contentType){
$this->contentType = $contentType;
return $this;
}
+ /**
+ * Set the last modified time
+ *
+ * @param int $lastModified The last modified time
+ * @return void
+ */
public function setLastModified($lastModified){
$this->lastModified = $lastModified;
}
+ /**
+ * Send header with the currently specified content type
+ *
+ * @throws \LogicException if the content type is not set
+ * @throws \LogicException if the content type is not a string
+ *
+ * @return void
+ */
protected function callContentType(){
+ if(empty($this->contentType))
+ throw new \LogicException('Content-Type is not set!');
+
+ if(!is_string($this->contentType))
+ throw new \LogicException('Content-Type must be a string!');
+
header('Content-Type: ' . $this->contentType);
}
+ /** {@inheritdoc} */
public function display(){
if ($this->lastModified) {
header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $this->lastModified) . 'GMT');
@@ -45,12 +107,18 @@ abstract class FormatAbstract implements FormatInterface {
return $this;
}
+ /**
+ * {@inheritdoc}
+ *
+ * @param array $items {@inheritdoc}
+ */
public function setItems(array $items){
$this->items = array_map(array($this, 'array_trim'), $items);
return $this;
}
+ /** {@inheritdoc} */
public function getItems(){
if(!is_array($this->items))
throw new \LogicException('Feed the ' . get_class($this) . ' with "setItems" method before !');
@@ -59,10 +127,10 @@ abstract class FormatAbstract implements FormatInterface {
}
/**
- * Define common informations can be required by formats and set default value for unknown values
- * @param array $extraInfos array with know informations (there isn't merge !!!)
- * @return this
- */
+ * {@inheritdoc}
+ *
+ * @param array $extraInfos {@inheritdoc}
+ */
public function setExtraInfos(array $extraInfos = array()){
foreach(array('name', 'uri', 'icon') as $infoName) {
if(!isset($extraInfos[$infoName])) {
@@ -75,10 +143,7 @@ abstract class FormatAbstract implements FormatInterface {
return $this;
}
- /**
- * Return extra infos
- * @return array See "setExtraInfos" detail method to know what extra are disponibles
- */
+ /** {@inheritdoc} */
public function getExtraInfos(){
if(is_null($this->extraInfos)) { // No extra info ?
$this->setExtraInfos(); // Define with default value
@@ -88,12 +153,17 @@ abstract class FormatAbstract implements FormatInterface {
}
/**
- * Sanitized html while leaving it functionnal.
- * The aim is to keep html as-is (with clickable hyperlinks)
- * while reducing annoying and potentially dangerous things.
- * Yes, I know sanitizing HTML 100% is an impossible task.
- * Maybe we'll switch to http://htmlpurifier.org/
- * or http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/index.php
+ * Sanitize HTML while leaving it functional.
+ *
+ * Keeps HTML as-is (with clickable hyperlinks) while reducing annoying and
+ * potentially dangerous things.
+ *
+ * @param string $html The HTML content
+ * @return string The sanitized HTML content
+ *
+ * @todo This belongs into `html.php`
+ * @todo Maybe switch to http://htmlpurifier.org/
+ * @todo Maybe switch to http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/index.php
*/
protected function sanitizeHtml($html)
{
@@ -104,6 +174,17 @@ abstract class FormatAbstract implements FormatInterface {
return $html;
}
+ /**
+ * Trim each element of an array
+ *
+ * This function applies `trim()` to all elements in the array, if the element
+ * is a valid string.
+ *
+ * @param array $elements The array to trim
+ * @return array The trimmed array
+ *
+ * @todo This is a utility function that doesn't belong here, find a new home.
+ */
protected function array_trim($elements){
foreach($elements as $key => $value) {
if(is_string($value))
diff --git a/lib/FormatInterface.php b/lib/FormatInterface.php
index f99d214..d59a4ef 100644
--- a/lib/FormatInterface.php
+++ b/lib/FormatInterface.php
@@ -1,11 +1,84 @@
<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * The format interface
+ *
+ * @todo Add missing function to the interface
+ * @todo Explain parameters and return values in more detail
+ * @todo Return self more often (to allow call chaining)
+ */
interface FormatInterface {
+
+ /**
+ * Generate a string representation of the current data
+ *
+ * @return string The string representation
+ */
public function stringify();
+
+ /**
+ * Display the current data to the user
+ *
+ * @return self The format object
+ */
public function display();
+
+ /**
+ * Set items
+ *
+ * @param array $bridges The items
+ * @return self The format object
+ *
+ * @todo Rename parameter `$bridges` to `$items`
+ */
public function setItems(array $bridges);
+
+ /**
+ * Return items
+ *
+ * @throws \LogicException if the items are not set
+ * @return array The items
+ */
public function getItems();
+
+ /**
+ * Set extra information
+ *
+ * @param array $infos Extra information
+ * @return self The format object
+ */
public function setExtraInfos(array $infos);
+
+ /**
+ * Return extra information
+ *
+ * @return array Extra information
+ */
public function getExtraInfos();
+
+ /**
+ * Set charset
+ *
+ * @param string $charset The charset
+ * @return self The format object
+ */
public function setCharset($charset);
+
+ /**
+ * Return current charset
+ *
+ * @return string The charset
+ */
public function getCharset();
}
diff --git a/lib/ParameterValidator.php b/lib/ParameterValidator.php
index c278e4d..91fe7c9 100644
--- a/lib/ParameterValidator.php
+++ b/lib/ParameterValidator.php
@@ -1,10 +1,35 @@
<?php
/**
- * Implements a validator for bridge parameters
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * Validator for bridge parameters
*/
class ParameterValidator {
+
+ /**
+ * Holds the list of invalid parameters
+ *
+ * @var array
+ */
private $invalid = array();
+ /**
+ * Add item to list of invalid parameters
+ *
+ * @param string $name The name of the parameter
+ * @param string $reason The reason for that parameter being invalid
+ * @return void
+ */
private function addInvalidParameter($name, $reason){
$this->invalid[] = array(
'name' => $name,
@@ -13,13 +38,23 @@ class ParameterValidator {
}
/**
- * Returns an array of invalid parameters, where each element is an
- * array of 'name' and 'reason'.
+ * Return list of invalid parameters.
+ *
+ * Each element is an array of 'name' and 'reason'.
+ *
+ * @return array List of invalid parameters
*/
public function getInvalidParameters() {
return $this->invalid;
}
+ /**
+ * Validate value for a text input
+ *
+ * @param string $value The value of a text input
+ * @param string|null $pattern (optional) A regex pattern
+ * @return string|null The filtered value or null if the value is invalid
+ */
private function validateTextValue($value, $pattern = null){
if(!is_null($pattern)) {
$filteredValue = filter_var($value,
@@ -38,6 +73,12 @@ class ParameterValidator {
return $filteredValue;
}
+ /**
+ * Validate value for a number input
+ *
+ * @param int $value The value of a number input
+ * @return int|null The filtered value or null if the value is invalid
+ */
private function validateNumberValue($value){
$filteredValue = filter_var($value, FILTER_VALIDATE_INT);
@@ -47,10 +88,23 @@ class ParameterValidator {
return $filteredValue;
}
+ /**
+ * Validate value for a checkbox
+ *
+ * @param bool $value The value of a checkbox
+ * @return bool The filtered value
+ */
private function validateCheckboxValue($value){
return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}
+ /**
+ * Validate value for a list
+ *
+ * @param string $value The value of a list
+ * @param array $expectedValues A list of expected values
+ * @return string|null The filtered value or null if the value is invalid
+ */
private function validateListValue($value, $expectedValues){
$filteredValue = filter_var($value);
@@ -69,9 +123,11 @@ class ParameterValidator {
}
/**
- * Checks if all required parameters are supplied by the user
- * @param $data An array of parameters provided by the user
- * @param $parameters An array of bridge parameters
+ * Check if all required parameters are satisfied
+ *
+ * @param array $data (ref) A list of input values
+ * @param array $parameters The bridge parameters
+ * @return bool True if all parameters are satisfied
*/
public function validateData(&$data, $parameters){
@@ -122,11 +178,11 @@ class ParameterValidator {
}
/**
- * Returns the name of the context matching the provided inputs
+ * Get the name of the context matching the provided inputs
*
* @param array $data Associative array of user data
* @param array $parameters Array of bridge parameters
- * @return mixed Returns the context name or null if no match was found
+ * @return string|null Returns the context name or null if no match was found
*/
public function getQueriedContext($data, $parameters){
$queriedContexts = array();
diff --git a/lib/RssBridge.php b/lib/RssBridge.php
deleted file mode 100644
index c0ab675..0000000
--- a/lib/RssBridge.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-define('PATH_LIB', __DIR__ . '/../lib/'); // Path to core library
-define('PATH_LIB_VENDOR', __DIR__ . '/../vendor/'); // Path to vendor library
-define('PATH_LIB_BRIDGES', __DIR__ . '/../bridges/'); // Path to bridges library
-define('PATH_LIB_FORMATS', __DIR__ . '/../formats/'); // Path to formats library
-define('PATH_LIB_CACHES', __DIR__ . '/../caches/'); // Path to caches library
-define('PATH_CACHE', __DIR__ . '/../cache'); // Path to cache folder
-define('REPOSITORY', 'https://github.com/RSS-Bridge/rss-bridge/');
-
-// Interfaces
-require_once PATH_LIB . 'BridgeInterface.php';
-require_once PATH_LIB . 'CacheInterface.php';
-require_once PATH_LIB . 'FormatInterface.php';
-
-// Classes
-require_once PATH_LIB . 'Exceptions.php';
-require_once PATH_LIB . 'Format.php';
-require_once PATH_LIB . 'FormatAbstract.php';
-require_once PATH_LIB . 'Bridge.php';
-require_once PATH_LIB . 'BridgeAbstract.php';
-require_once PATH_LIB . 'FeedExpander.php';
-require_once PATH_LIB . 'Cache.php';
-require_once PATH_LIB . 'Authentication.php';
-require_once PATH_LIB . 'Configuration.php';
-require_once PATH_LIB . 'BridgeCard.php';
-require_once PATH_LIB . 'BridgeList.php';
-require_once PATH_LIB . 'ParameterValidator.php';
-
-// Functions
-require_once PATH_LIB . 'html.php';
-require_once PATH_LIB . 'error.php';
-require_once PATH_LIB . 'contents.php';
-
-// Vendor
-require_once PATH_LIB_VENDOR . 'simplehtmldom/simple_html_dom.php';
-require_once PATH_LIB_VENDOR . 'php-urljoin/src/urljoin.php';
diff --git a/lib/contents.php b/lib/contents.php
index 3f3d36c..5d79557 100644
--- a/lib/contents.php
+++ b/lib/contents.php
@@ -1,6 +1,56 @@
<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * Gets contents from the Internet.
+ *
+ * **Content caching** (disabled in debug mode)
+ *
+ * A copy of the received content is stored in a local cache folder `server/` at
+ * {@see PATH_CACHE}. The `If-Modified-Since` header is added to the request, if
+ * the provided URL has been cached before.
+ *
+ * When the server responds with `304 Not Modified`, the cached data is returned.
+ * This will improve response times and reduce bandwidth for servers that support
+ * the `If-Modified-Since` header.
+ *
+ * Cached files are forcefully removed after 24 hours.
+ *
+ * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
+ * If-Modified-Since
+ *
+ * @param string $url The URL.
+ * @param array $header (optional) A list of cURL header.
+ * For more information follow the links below.
+ * * https://php.net/manual/en/function.curl-setopt.php
+ * * https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html
+ * @param array $opts (optional) A list of cURL options as associative array in
+ * the format `$opts[$option] = $value;`, where `$option` is any `CURLOPT_XXX`
+ * option and `$value` the corresponding value.
+ *
+ * For more information see http://php.net/manual/en/function.curl-setopt.php
+ * @return string The contents.
+ */
function getContents($url, $header = array(), $opts = array()){
- debugMessage('Reading contents from "' . $url . '"');
+ Debug::log('Reading contents from "' . $url . '"');
+
+ // Initialize cache
+ $cache = Cache::create('FileCache');
+ $cache->setPath(PATH_CACHE . 'server/');
+ $cache->purgeCache(86400); // 24 hours (forced)
+
+ $params = [$url];
+ $cache->setParameters($params);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
@@ -8,7 +58,7 @@ function getContents($url, $header = array(), $opts = array()){
if(is_array($header) && count($header) !== 0) {
- debugMessage('Setting headers: ' . json_encode($header));
+ Debug::log('Setting headers: ' . json_encode($header));
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
}
@@ -19,7 +69,7 @@ function getContents($url, $header = array(), $opts = array()){
if(is_array($opts) && count($opts) !== 0) {
- debugMessage('Setting options: ' . json_encode($opts));
+ Debug::log('Setting options: ' . json_encode($opts));
foreach($opts as $key => $value) {
curl_setopt($ch, $key, $value);
@@ -29,7 +79,7 @@ function getContents($url, $header = array(), $opts = array()){
if(defined('PROXY_URL') && !defined('NOPROXY')) {
- debugMessage('Setting proxy url: ' . PROXY_URL);
+ Debug::log('Setting proxy url: ' . PROXY_URL);
curl_setopt($ch, CURLOPT_PROXY, PROXY_URL);
}
@@ -37,43 +87,104 @@ function getContents($url, $header = array(), $opts = array()){
// We always want the response header as part of the data!
curl_setopt($ch, CURLOPT_HEADER, true);
+ // Build "If-Modified-Since" header
+ if(!Debug::isEnabled() && $time = $cache->getTime()) { // Skip if cache file doesn't exist
+ Debug::log('Adding If-Modified-Since');
+ curl_setopt($ch, CURLOPT_TIMEVALUE, $time);
+ curl_setopt($ch, CURLOPT_TIMECONDITION, CURL_TIMECOND_IFMODSINCE);
+ }
+
+ // Enables logging for the outgoing header
+ curl_setopt($ch, CURLINFO_HEADER_OUT, true);
+
$data = curl_exec($ch);
$curlError = curl_error($ch);
$curlErrno = curl_errno($ch);
+ $curlInfo = curl_getinfo($ch);
+
+ Debug::log('Outgoing header: ' . json_encode($curlInfo));
if($data === false)
- debugMessage('Cant\'t download ' . $url . ' cUrl error: ' . $curlError . ' (' . $curlErrno . ')');
+ Debug::log('Cant\'t download ' . $url . ' cUrl error: ' . $curlError . ' (' . $curlErrno . ')');
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$errorCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$header = substr($data, 0, $headerSize);
- debugMessage('Response header: ' . $header);
+ Debug::log('Response header: ' . $header);
$headers = parseResponseHeader($header);
$finalHeader = end($headers);
- if($errorCode !== 200) {
+ curl_close($ch);
- if(array_key_exists('Server', $finalHeader) && strpos($finalHeader['Server'], 'cloudflare') !== false) {
+ switch($errorCode) {
+ case 200: // Contents received
+ Debug::log('New contents received');
+ $data = substr($data, $headerSize);
+ // Disable caching if the server responds with "Cache-Control: no-cache"
+ // or "Cache-Control: no-store"
+ $finalHeader = array_change_key_case($finalHeader, CASE_LOWER);
+ if(array_key_exists('cache-control', $finalHeader)) {
+ Debug::log('Server responded with "Cache-Control" header');
+ $directives = explode(',', $finalHeader['cache-control']);
+ $directives = array_map('trim', $directives);
+ if(in_array('no-cache', $directives)
+ || in_array('no-store', $directives)) { // Skip caching
+ Debug::log('Skip server side caching');
+ return $data;
+ }
+ }
+ Debug::log('Store response to cache');
+ $cache->saveData($data);
+ return $data;
+ case 304: // Not modified, use cached data
+ Debug::log('Contents not modified on host, returning cached data');
+ return $cache->loadData();
+ default:
+ if(array_key_exists('Server', $finalHeader) && strpos($finalHeader['Server'], 'cloudflare') !== false) {
returnServerError(<<< EOD
The server responded with a Cloudflare challenge, which is not supported by RSS-Bridge!
If this error persists longer than a week, please consider opening an issue on GitHub!
EOD
- );
- }
+ );
+ }
- returnError(<<<EOD
+ returnError(<<<EOD
The requested resource cannot be found!
Please make sure your input parameters are correct!
+cUrl error: $curlError ($curlErrno)
EOD
- , $errorCode);
+ , $errorCode);
}
-
- curl_close($ch);
- return substr($data, $headerSize);
}
+/**
+ * Gets contents from the Internet as simplhtmldom object.
+ *
+ * @param string $url The URL.
+ * @param array $header (optional) A list of cURL header.
+ * For more information follow the links below.
+ * * https://php.net/manual/en/function.curl-setopt.php
+ * * https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html
+ * @param array $opts (optional) A list of cURL options as associative array in
+ * the format `$opts[$option] = $value;`, where `$option` is any `CURLOPT_XXX`
+ * option and `$value` the corresponding value.
+ *
+ * For more information see http://php.net/manual/en/function.curl-setopt.php
+ * @param bool $lowercase Force all selectors to lowercase.
+ * @param bool $forceTagsClosed Forcefully close tags in malformed HTML.
+ *
+ * _Remarks_: Forcefully closing tags is great for malformed HTML, but it can
+ * lead to parsing errors.
+ * @param string $target_charset Defines the target charset.
+ * @param bool $stripRN Replace all occurrences of `"\r"` and `"\n"` by `" "`.
+ * @param string $defaultBRText Specifies the replacement text for `<br>` tags
+ * when returning plaintext.
+ * @param string $defaultSpanText Specifies the replacement text for `<span />`
+ * tags when returning plaintext.
+ * @return string Contents as simplehtmldom object.
+ */
function getSimpleHTMLDOM($url,
$header = array(),
$opts = array(),
@@ -94,10 +205,34 @@ $defaultSpanText = DEFAULT_SPAN_TEXT){
}
/**
- * Maintain locally cached versions of pages to avoid multiple downloads.
- * @param url url to cache
- * @param duration duration of the cache file in seconds (default: 24h/86400s)
- * @return content of the file as string
+ * Gets contents from the Internet as simplhtmldom object. Contents are cached
+ * and re-used for subsequent calls until the cache duration elapsed.
+ *
+ * _Notice_: Cached contents are forcefully removed after 24 hours (86400 seconds).
+ *
+ * @param string $url The URL.
+ * @param int $duration Cache duration in seconds.
+ * @param array $header (optional) A list of cURL header.
+ * For more information follow the links below.
+ * * https://php.net/manual/en/function.curl-setopt.php
+ * * https://curl.haxx.se/libcurl/c/CURLOPT_HTTPHEADER.html
+ * @param array $opts (optional) A list of cURL options as associative array in
+ * the format `$opts[$option] = $value;`, where `$option` is any `CURLOPT_XXX`
+ * option and `$value` the corresponding value.
+ *
+ * For more information see http://php.net/manual/en/function.curl-setopt.php
+ * @param bool $lowercase Force all selectors to lowercase.
+ * @param bool $forceTagsClosed Forcefully close tags in malformed HTML.
+ *
+ * _Remarks_: Forcefully closing tags is great for malformed HTML, but it can
+ * lead to parsing errors.
+ * @param string $target_charset Defines the target charset.
+ * @param bool $stripRN Replace all occurrences of `"\r"` and `"\n"` by `" "`.
+ * @param string $defaultBRText Specifies the replacement text for `<br>` tags
+ * when returning plaintext.
+ * @param string $defaultSpanText Specifies the replacement text for `<span />`
+ * tags when returning plaintext.
+ * @return string Contents as simplehtmldom object.
*/
function getSimpleHTMLDOMCached($url,
$duration = 86400,
@@ -109,11 +244,11 @@ $target_charset = DEFAULT_TARGET_CHARSET,
$stripRN = true,
$defaultBRText = DEFAULT_BR_TEXT,
$defaultSpanText = DEFAULT_SPAN_TEXT){
- debugMessage('Caching url ' . $url . ', duration ' . $duration);
+ Debug::log('Caching url ' . $url . ', duration ' . $duration);
// Initialize cache
$cache = Cache::create('FileCache');
- $cache->setPath(PATH_CACHE . '/pages');
+ $cache->setPath(PATH_CACHE . 'pages/');
$cache->purgeCache(86400); // 24 hours (forced)
$params = [$url];
@@ -123,7 +258,7 @@ $defaultSpanText = DEFAULT_SPAN_TEXT){
$time = $cache->getTime();
if($time !== false
&& (time() - $duration < $time)
- && (!defined('DEBUG') || DEBUG !== true)) { // Contents within duration
+ && Debug::isEnabled()) { // Contents within duration
$content = $cache->loadData();
} else { // Content not within duration
$content = getContents($url, $header, $opts);
@@ -142,9 +277,12 @@ $defaultSpanText = DEFAULT_SPAN_TEXT){
}
/**
- * Parses the provided response header into an associative array
+ * Parses the cURL response header into an associative array
*
* Based on https://stackoverflow.com/a/18682872
+ *
+ * @param string $header The cURL response header.
+ * @return array An associative array of response headers.
*/
function parseResponseHeader($header) {
@@ -177,10 +315,18 @@ function parseResponseHeader($header) {
}
/**
- * Determine MIME type from URL/Path file extension
- * Remark: Built-in functions mime_content_type or fileinfo requires fetching remote content
- * Remark: A bridge can hint for a MIME type by appending #.ext to a URL, e.g. #.image
+ * Determines the MIME type from a URL/Path file extension.
+ *
+ * _Remarks_:
+ *
+ * * The built-in functions `mime_content_type` and `fileinfo` require fetching
+ * remote contents.
+ * * A caller can hint for a MIME type by appending `#.ext` to the URL (i.e. `#.image`).
+ *
* Based on https://stackoverflow.com/a/1147952
+ *
+ * @param string $url The URL or path to the file.
+ * @return string The MIME type of the file.
*/
function getMimeType($url) {
static $mime = null;
diff --git a/lib/error.php b/lib/error.php
index 34ad9dd..9a0756f 100644
--- a/lib/error.php
+++ b/lib/error.php
@@ -1,28 +1,43 @@
<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/**
+ * Throws an exception when called.
+ *
+ * @throws \Exception when called
+ * @param string $message The error message
+ * @param int $code The HTTP error code
+ * @link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes List of HTTP
+ * status codes
+ */
function returnError($message, $code){
- throw new \HttpException($message, $code);
+ throw new \Exception($message, $code);
}
+/**
+ * Returns HTTP Error 400 (Bad Request) when called.
+ *
+ * @param string $message The error message
+ */
function returnClientError($message){
returnError($message, 400);
}
+/**
+ * Returns HTTP Error 500 (Internal Server Error) when called.
+ *
+ * @param string $message The error message
+ */
function returnServerError($message){
returnError($message, 500);
}
-
-function debugMessage($text){
- if(!file_exists('DEBUG')) {
- return;
- }
-
- $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
- $calling = $backtrace[2];
- $message = $calling['file'] . ':'
- . $calling['line'] . ' class '
- . (isset($calling['class']) ? $calling['class'] : '<no-class>') . '->'
- . $calling['function'] . ' - '
- . $text;
-
- error_log($message);
-}
diff --git a/lib/html.php b/lib/html.php
index f87991f..e49ca7a 100644
--- a/lib/html.php
+++ b/lib/html.php
@@ -1,18 +1,55 @@
<?php
-function sanitize($textToSanitize,
-$removedTags = array('script', 'iframe', 'input', 'form'),
-$keptAttributes = array('title', 'href', 'src'),
-$keptText = array()){
- $htmlContent = str_get_html($textToSanitize);
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+/**
+ * Removes unwanted tags from a given HTML text.
+ *
+ * @param string $html The HTML text to sanitize.
+ * @param array $tags_to_remove A list of tags to remove from the DOM.
+ * @param array $attributes_to_keep A list of attributes to keep on tags (other
+ * attributes are removed).
+ * @param array $text_to_keep A list of tags where the innertext replaces the tag
+ * (i.e. `<p>Hello World!</p>` becomes `Hello World!`).
+ * @return object A simplehtmldom object of the remaining contents.
+ *
+ * @todo Check if this implementation is still necessary, because simplehtmldom
+ * already removes some of the tags (search for `remove_noise` in simple_html_dom.php).
+ */
+function sanitize($html,
+$tags_to_remove = array('script', 'iframe', 'input', 'form'),
+$attributes_to_keep = array('title', 'href', 'src'),
+$text_to_keep = array()){
+ $htmlContent = str_get_html($html);
+
+ /*
+ * Notice: simple_html_dom currently doesn't support "->find(*)", which is a
+ * known issue: https://sourceforge.net/p/simplehtmldom/bugs/157/
+ *
+ * A solution to this is to find all nodes WITHOUT a specific attribute. If
+ * the attribute is very unlikely to appear in the DOM, this is essentially
+ * returning all nodes.
+ *
+ * "*[!b38fd2b1fe7f4747d6b1c1254ccd055e]" is doing exactly that. The attrib
+ * "b38fd2b1fe7f4747d6b1c1254ccd055e" is very unlikely to appear in any DOM.
+ */
foreach($htmlContent->find('*[!b38fd2b1fe7f4747d6b1c1254ccd055e]') as $element) {
- if(in_array($element->tag, $keptText)) {
+ if(in_array($element->tag, $text_to_keep)) {
$element->outertext = $element->plaintext;
- } elseif(in_array($element->tag, $removedTags)) {
+ } elseif(in_array($element->tag, $tags_to_remove)) {
$element->outertext = '';
} else {
foreach($element->getAllAttributes() as $attributeName => $attribute) {
- if(!in_array($attributeName, $keptAttributes))
+ if(!in_array($attributeName, $attributes_to_keep))
$element->removeAttribute($attributeName);
}
}
@@ -21,11 +58,48 @@ $keptText = array()){
return $htmlContent;
}
+/**
+ * Replace background by image
+ *
+ * Replaces tags with styles of `backgroud-image` by `<img />` tags.
+ *
+ * For example:
+ *
+ * ```HTML
+ * <html>
+ * <body style="background-image: url('bgimage.jpg');">
+ * <h1>Hello world!</h1>
+ * </body>
+ * </html>
+ * ```
+ *
+ * results in this output:
+ *
+ * ```HTML
+ * <html>
+ * <img style="display:block;" src="bgimage.jpg" />
+ * </html>
+ * ```
+ *
+ * @param string $htmlContent The HTML content
+ * @return string The HTML content with all ocurrences replaced
+ */
function backgroundToImg($htmlContent) {
$regex = '/background-image[ ]{0,}:[ ]{0,}url\([\'"]{0,}(.*?)[\'"]{0,}\)/';
$htmlContent = str_get_html($htmlContent);
+ /*
+ * Notice: simple_html_dom currently doesn't support "->find(*)", which is a
+ * known issue: https://sourceforge.net/p/simplehtmldom/bugs/157/
+ *
+ * A solution to this is to find all nodes WITHOUT a specific attribute. If
+ * the attribute is very unlikely to appear in the DOM, this is essentially
+ * returning all nodes.
+ *
+ * "*[!b38fd2b1fe7f4747d6b1c1254ccd055e]" is doing exactly that. The attrib
+ * "b38fd2b1fe7f4747d6b1c1254ccd055e" is very unlikely to appear in any DOM.
+ */
foreach($htmlContent->find('*[!b38fd2b1fe7f4747d6b1c1254ccd055e]') as $element) {
if(preg_match($regex, $element->style, $matches) > 0) {
@@ -42,9 +116,14 @@ function backgroundToImg($htmlContent) {
/**
* Convert relative links in HTML into absolute links
- * @param $content HTML content to fix. Supports HTML objects or string objects
- * @param $server full URL to the page containing relative links
- * @return content with fixed URLs, as HTML object or string depending on input type
+ *
+ * This function is based on `php-urljoin`.
+ *
+ * @link https://github.com/plaidfluff/php-urljoin php-urljoin
+ *
+ * @param string|object $content The HTML content. Supports HTML objects or string objects
+ * @param string $server Fully qualified URL to the page containing relative links
+ * @return object Content with fixed URLs.
*/
function defaultLinkTo($content, $server){
$string_convert = false;
@@ -70,10 +149,12 @@ function defaultLinkTo($content, $server){
/**
* Extract the first part of a string matching the specified start and end delimiters
- * @param $string input string, e.g. '<div>Post author: John Doe</div>'
- * @param $start start delimiter, e.g. 'author: '
- * @param $end end delimiter, e.g. '<'
- * @return extracted string, e.g. 'John Doe', or false if the delimiters were not found.
+ *
+ * @param string $string Input string, e.g. `<div>Post author: John Doe</div>`
+ * @param string $start Start delimiter, e.g. `author: `
+ * @param string $end End delimiter, e.g. `<`
+ * @return string|bool Extracted string, e.g. `John Doe`, or false if the
+ * delimiters were not found.
*/
function extractFromDelimiters($string, $start, $end) {
if (strpos($string, $start) !== false) {
@@ -85,10 +166,11 @@ function extractFromDelimiters($string, $start, $end) {
/**
* Remove one or more part(s) of a string using a start and end delmiters
- * @param $string input string, e.g. 'foo<script>superscript()</script>bar'
- * @param $start start delimiter, e.g. '<script'
- * @param $end end delimiter, e.g. '</script>'
- * @return cleaned string, e.g. 'foobar'
+ *
+ * @param string $string Input string, e.g. `foo<script>superscript()</script>bar`
+ * @param string $start Start delimiter, e.g. `<script`
+ * @param string $end End delimiter, e.g. `</script>`
+ * @return string Cleaned string, e.g. `foobar`
*/
function stripWithDelimiters($string, $start, $end) {
while(strpos($string, $start) !== false) {
@@ -101,10 +183,13 @@ function stripWithDelimiters($string, $start, $end) {
/**
* Remove HTML sections containing one or more sections using the same HTML tag
- * @param $string input string, e.g. 'foo<div class="ads"><div>ads</div>ads</div>bar'
- * @param $tag_name name of the HTML tag, e.g. 'div'
- * @param $tag_start start of the HTML tag to remove, e.g. '<div class="ads">'
- * @return cleaned string, e.g. 'foobar'
+ *
+ * @param string $string Input string, e.g. `foo<div class="ads"><div>ads</div>ads</div>bar`
+ * @param string $tag_name Name of the HTML tag, e.g. `div`
+ * @param string $tag_start Start of the HTML tag to remove, e.g. `<div class="ads">`
+ * @return string Cleaned String, e.g. `foobar`
+ *
+ * @todo This function needs more documentation to make it maintainable.
*/
function stripRecursiveHTMLSection($string, $tag_name, $tag_start){
$open_tag = '<' . $tag_name;
@@ -131,9 +216,13 @@ function stripRecursiveHTMLSection($string, $tag_name, $tag_start){
}
/**
- * Convert Markdown tags into HTML tags. Only a subset of the Markdown syntax is implemented.
- * @param $string input string in Markdown format
- * @return output string in HTML format
+ * Convert Markdown into HTML. Only a subset of the Markdown syntax is implemented.
+ *
+ * @link https://daringfireball.net/projects/markdown/ Markdown
+ * @link https://github.github.com/gfm/ GitHub Flavored Markdown Spec
+ *
+ * @param string $string Input string in Markdown format
+ * @return string output string in HTML format
*/
function markdownToHtml($string) {
diff --git a/lib/rssbridge.php b/lib/rssbridge.php
new file mode 100644
index 0000000..01d11d7
--- /dev/null
+++ b/lib/rssbridge.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * This file is part of RSS-Bridge, a PHP project capable of generating RSS and
+ * Atom feeds for websites that don't have one.
+ *
+ * For the full license information, please view the UNLICENSE file distributed
+ * with this source code.
+ *
+ * @package Core
+ * @license http://unlicense.org/ UNLICENSE
+ * @link https://github.com/rss-bridge/rss-bridge
+ */
+
+/** Path to the root folder of RSS-Bridge (where index.php is located) */
+define('PATH_ROOT', __DIR__ . '/../');
+
+/** Path to the core library */
+define('PATH_LIB', __DIR__ . '/../lib/'); // Path to core library
+
+/** Path to the vendor library */
+define('PATH_LIB_VENDOR', __DIR__ . '/../vendor/');
+
+/** Path to the bridges library */
+define('PATH_LIB_BRIDGES', __DIR__ . '/../bridges/');
+
+/** Path to the formats library */
+define('PATH_LIB_FORMATS', __DIR__ . '/../formats/');
+
+/** Path to the caches library */
+define('PATH_LIB_CACHES', __DIR__ . '/../caches/');
+
+/** Path to the cache folder */
+define('PATH_CACHE', __DIR__ . '/../cache/');
+
+/** Path to the whitelist file */
+define('WHITELIST', __DIR__ . '/../whitelist.txt');
+
+/** URL to the RSS-Bridge repository */
+define('REPOSITORY', 'https://github.com/RSS-Bridge/rss-bridge/');
+
+// Interfaces
+require_once PATH_LIB . 'BridgeInterface.php';
+require_once PATH_LIB . 'CacheInterface.php';
+require_once PATH_LIB . 'FormatInterface.php';
+
+// Classes
+require_once PATH_LIB . 'Debug.php';
+require_once PATH_LIB . 'Exceptions.php';
+require_once PATH_LIB . 'Format.php';
+require_once PATH_LIB . 'FormatAbstract.php';
+require_once PATH_LIB . 'Bridge.php';
+require_once PATH_LIB . 'BridgeAbstract.php';
+require_once PATH_LIB . 'FeedExpander.php';
+require_once PATH_LIB . 'Cache.php';
+require_once PATH_LIB . 'Authentication.php';
+require_once PATH_LIB . 'Configuration.php';
+require_once PATH_LIB . 'BridgeCard.php';
+require_once PATH_LIB . 'BridgeList.php';
+require_once PATH_LIB . 'ParameterValidator.php';
+
+// Functions
+require_once PATH_LIB . 'html.php';
+require_once PATH_LIB . 'error.php';
+require_once PATH_LIB . 'contents.php';
+
+// Vendor
+require_once PATH_LIB_VENDOR . 'simplehtmldom/simple_html_dom.php';
+require_once PATH_LIB_VENDOR . 'php-urljoin/src/urljoin.php';
+
+// Initialize static members
+try {
+ Bridge::setWorkingDir(PATH_LIB_BRIDGES);
+ Format::setWorkingDir(PATH_LIB_FORMATS);
+ Cache::setWorkingDir(PATH_LIB_CACHES);
+} catch(Exception $e) {
+ error_log($e);
+ header('Content-type: text/plain', true, 500);
+ die($e->getMessage());
+}
diff --git a/phpcompatibility.xml b/phpcompatibility.xml
deleted file mode 100644
index f232523..0000000
--- a/phpcompatibility.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ruleset name="RSS-Bridge PHPCompatibility">
- <description>Defines rules for PHPCompatibility</description>
- <exclude-pattern>./static</exclude-pattern>
- <exclude-pattern>./vendor</exclude-pattern>
-
- <!-- Run against the PHPCompatibility ruleset -->
- <!--
-
- -->
- <config name="testVersion" value="5.6"/>
- <rule ref="PHPCompatibility">
- <!--
- "PHP 7 changes how most errors are reported by PHP. Instead of reporting
- errors through the traditional error reporting mechanism used by PHP 5,
- most errors are now reported by throwing Error exceptions."
-
- from: http://php.net/manual/en/language.errors.php7.php
-
- Skip this check for PHP 5.6 in order to support PHP 7.x
-
- Catch Exception and Error separately to support both versions.
-
- Example:
-
- <code>
- try {
- // Run your code here
- } catch(Error $e) {
- // Handle errors (PHP 7.0+)
- } catch(Exception $e) {
- // Handle exceptions (PHP 5.6+)
- }
- </code>
- -->
- <exclude name="PHPCompatibility.Classes.NewClasses.errorFound"/>
- <!--
- RSS-Bridge uses parse_ini_file with INI_SCANNER_TYPED to load configuration
- settings. INI_SCANNER_TYPED was added in PHP 5.6.1. Skip checking for that
- specific constant.
-
- References: http://php.net/manual/de/function.parse-ini-file.php
- -->
- <exclude name="PHPCompatibility.Constants.NewConstants.ini_scanner_typedFound"/>
- </rule>
-
-</ruleset>
diff --git a/phpcs.xml b/phpcs.xml
deleted file mode 100644
index 119d4b2..0000000
--- a/phpcs.xml
+++ /dev/null
@@ -1,84 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ruleset name="RSS-Bridge Ruleset">
- <description>Created with the PHP Coding Standard Generator. http://edorian.github.com/php-coding-standard-generator/</description>
- <exclude-pattern>./static</exclude-pattern>
- <exclude-pattern>./vendor</exclude-pattern>
- <!-- Duplicate class names are not allowed -->
- <rule ref="Generic.Classes.DuplicateClassName"/>
- <!-- Statements must not be empty -->
- <rule ref="Generic.CodeAnalysis.EmptyStatement"/>
- <!-- Unconditional if-statements are not allowed -->
- <rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
- <!-- Do not use final statements inside final classes -->
- <rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
- <!-- Do not override methods to call their parent -->
- <rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
- <!-- Make sure the concatenation operator has spaces around it -->
- <rule ref="Squiz.Strings.ConcatenationSpacing">
- <properties>
- <property name="spacing" value="1"/>
- <property name="ignoreNewlines" value="true"/>
- </properties>
- </rule>
- <!-- One line should not have more than 80 characters -->
- <!-- One line must never exceed 120 characters -->
- <rule ref="Generic.Files.LineLength">
- <properties>
- <property name="lineLimit" value="80"/>
- <property name="absoluteLineLimit" value="120"/>
- </properties>
- </rule>
- <!-- When calling a function: -->
- <!-- Do not add a space before the opening parenthesis -->
- <!-- Do not add a space after the opening parenthesis -->
- <!-- Do not add a space before the closing parenthesis -->
- <!-- Do not add a space before a comma -->
- <!-- Add a space after a comma -->
- <rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
- <!-- Use UPPERCARE for constants -->
- <rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
- <!-- Use lowercase for 'true', 'false' and 'null' -->
- <rule ref="Generic.PHP.LowerCaseConstant"/>
- <!-- Use a single string instead of concating -->
- <rule ref="Generic.Strings.UnnecessaryStringConcat"/>
- <!-- Use tabs for indentation -->
- <rule ref="Generic.WhiteSpace.DisallowSpaceIndent"/>
- <!-- Parameters with default values must appear last in functions -->
- <rule ref="PEAR.Functions.ValidDefaultValue"/>
- <!-- Use PascalCase for class names -->
- <rule ref="PEAR.NamingConventions.ValidClassName"/>
- <!-- Use 'elseif' instead of 'else if' -->
- <rule ref="PSR2.ControlStructures.ElseIfDeclaration"/>
- <!-- Do not add spaces after opening or before closing bracket -->
- <rule ref="PSR2.ControlStructures.ControlStructureSpacing"/>
- <!-- Add a new line at the end of a file -->
- <rule ref="PSR2.Files.EndFileNewline"/>
- <!-- Add space after closing parenthesis -->
- <!-- Add body into new line -->
- <!-- Close body in new line -->
- <rule ref="Squiz.ControlStructures.ControlSignature">
- <!-- No space after keyword (before opening parenthesis) -->
- <exclude name="Squiz.ControlStructures.ControlSignature.SpaceAfterKeyword"/>
- </rule>
- <!-- When declaring a function: -->
- <!-- Do not add a space before a comma -->
- <!-- Add a space after a comma -->
- <!-- Add a space before and after an equal sign -->
- <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing">
- <properties>
- <property name="equalsSpacing" value="1"/>
- </properties>
- </rule>
- <!-- Do not add spaces when casting -->
- <rule ref="Squiz.WhiteSpace.CastSpacing"/>
- <!-- Operators must have a space around them -->
- <rule ref="Squiz.WhiteSpace.OperatorSpacing"/>
- <!-- Do not add a whitespace before a semicolon -->
- <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
- <!-- Do not add whitespace at start or end of a file or end of a line -->
- <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/>
- <!-- Whenever possible use single quote strings -->
- <rule ref="Squiz.Strings.DoubleQuoteUsage">
- <exclude name="Squiz.Strings.DoubleQuoteUsage.ContainsVar" />
- </rule>
-</ruleset>
diff --git a/phpunit.xml b/phpunit.xml
deleted file mode 100644
index 16a082d..0000000
--- a/phpunit.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-<phpunit
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd"
- colors="true"
- processIsolation="false"
- timeoutForSmallTests="1"
- timeoutForMediumTests="1"
- timeoutForLargeTests="6" >
-
- <testsuites>
- <testsuite name="Standard test suite">
- <file>tests/BridgeImplementationTest.php</file>
- </testsuite>
- </testsuites>
-
-</phpunit>
diff --git a/scalingo.json b/scalingo.json
deleted file mode 100644
index 9b1d51e..0000000
--- a/scalingo.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "RSS Bridge",
- "description": "rss-bridge is a PHP project capable of generating ATOM feeds for websites which don't have one.",
- "repository": "https://github.com/sebsauvage/rss-bridge",
- "website": "https://github.com/sebsauvage/rss-bridge"
-}
diff --git a/tests/BridgeImplementationTest.php b/tests/BridgeImplementationTest.php
deleted file mode 100644
index b7d13f1..0000000
--- a/tests/BridgeImplementationTest.php
+++ /dev/null
@@ -1,191 +0,0 @@
-<?php
-
-use PHPUnit\Framework\TestCase;
-use PHPUnit\Framework\TestResult;
-use PHPUnit\Framework\AssertionFailedError;
-
-require_once(__DIR__ . '/../lib/RssBridge.php');
-
-Bridge::setDir(PATH_LIB_BRIDGES);
-
-/**
- * This class checks bridges for implementation details:
- *
- * - A bridge must not implement public functions other than the ones specified
- * by the bridge interfaces. Custom functions must be defined in private or
- * protected scope.
- * - getName() must return a valid string (non-empty)
- * - getURI() must return a valid URI
- * - A bridge must define constants for NAME, URI, DESCRIPTION and MAINTAINER,
- * CACHE_TIMEOUT and PARAMETERS are optional
- */
-final class BridgeImplementationTest extends TestCase {
-
- private function CheckBridgePublicFunctions($bridgeName){
-
- $parent_methods = array();
-
- if(in_array('BridgeInterface', class_parents($bridgeName))) {
- $parent_methods = array_merge($parent_methods, get_class_methods('BridgeInterface'));
- }
-
- if(in_array('BridgeAbstract', class_parents($bridgeName))) {
- $parent_methods = array_merge($parent_methods, get_class_methods('BridgeAbstract'));
- }
-
- if(in_array('FeedExpander', class_parents($bridgeName))) {
- $parent_methods = array_merge($parent_methods, get_class_methods('FeedExpander'));
- }
-
- // Receive all non abstract methods
- $methods = array_diff(get_class_methods($bridgeName), $parent_methods);
- $method_names = implode(', ', $methods);
-
- $errmsg = $bridgeName
- . ' implements additional public method(s): '
- . $method_names
- . '! Custom functions must be defined in private or protected scope!';
-
- $this->assertEmpty($method_names, $errmsg);
-
- }
-
- private function CheckBridgeGetNameDefaultValue($bridgeName){
-
- if(in_array('BridgeAbstract', class_parents($bridgeName))) { // Is bridge
-
- if(!$this->isFunctionMemberOf($bridgeName, 'getName'))
- return;
-
- $bridge = new $bridgeName();
- $abstract = new BridgeAbstractTest();
-
- $message = $bridgeName . ': \'getName\' must return a valid name!';
-
- $this->assertNotEmpty(trim($bridge->getName()), $message);
-
- }
-
- }
-
- // Checks whether the getURI function returns empty or default values
- private function CheckBridgeGetURIDefaultValue($bridgeName){
-
- if(in_array('BridgeAbstract', class_parents($bridgeName))) { // Is bridge
-
- if(!$this->isFunctionMemberOf($bridgeName, 'getURI'))
- return;
-
- $bridge = new $bridgeName();
- $abstract = new BridgeAbstractTest();
-
- $message = $bridgeName . ': \'getURI\' must return a valid URI!';
-
- $this->assertNotEmpty(trim($bridge->getURI()), $message);
-
- }
-
- }
-
- private function CheckBridgePublicConstants($bridgeName){
-
- // Assertion only works for BridgeAbstract!
- if(in_array('BridgeAbstract', class_parents($bridgeName))) {
-
- $ref = new ReflectionClass($bridgeName);
- $constants = $ref->getConstants();
-
- $ref = new ReflectionClass('BridgeAbstract');
- $parent_constants = $ref->getConstants();
-
- foreach($parent_constants as $key => $value) {
-
- $this->assertArrayHasKey($key, $constants, 'Constant ' . $key . ' missing in ' . $bridgeName);
-
- // Skip optional constants
- if($key !== 'PARAMETERS' && $key !== 'CACHE_TIMEOUT') {
- $this->assertNotEquals($value, $constants[$key], 'Constant ' . $key . ' missing in ' . $bridgeName);
- }
-
- }
-
- }
-
- }
-
- private function isFunctionMemberOf($bridgeName, $functionName){
-
- $bridgeReflector = new ReflectionClass($bridgeName);
- $bridgeMethods = $bridgeReflector->GetMethods();
- $bridgeHasMethod = false;
-
- foreach($bridgeMethods as $method) {
-
- if($method->name === $functionName && $method->class === $bridgeReflector->name) {
- return true;
- }
-
- }
-
- return false;
-
- }
-
- public function testBridgeImplementation($bridgeName){
-
- require_once('bridges/' . $bridgeName . '.php');
-
- $this->CheckBridgePublicFunctions($bridgeName);
- $this->CheckBridgePublicConstants($bridgeName);
- $this->CheckBridgeGetNameDefaultValue($bridgeName);
- $this->CheckBridgeGetURIDefaultValue($bridgeName);
-
- }
-
- public function count() {
- return count(Bridge::listBridges());
- }
-
- public function run(TestResult $result = null) {
-
- if ($result === null) {
- $result = new TestResult;
- }
-
- foreach (Bridge::listBridges() as $bridge) {
-
- $bridge .= 'Bridge';
-
- $result->startTest($this);
- PHP_Timer::start();
- $stopTime = null;
-
- try {
- $this->testBridgeImplementation($bridge);
- } catch (AssertionFailedError $e) {
-
- $stopTime = PHP_Timer::stop();
- $result->addFailure($this, $e, $stopTime);
-
- } catch (Exception $e) {
-
- $stopTime = PHP_Timer::stop();
- $result->addError($this, $e, $stopTime);
-
- }
-
- if ($stopTime === null) {
- $stopTime = PHP_Timer::stop();
- }
-
- $result->endTest($this, $stopTime);
-
- }
-
- return $result;
- }
-}
-
-class BridgeAbstractTest extends BridgeAbstract {
- public function collectData(){}
-}