Merge branch 'FreshRSS:master' into master

This commit is contained in:
Ramazan Sancar 2024-11-01 00:10:18 +03:00 committed by GitHub
commit 699a771e48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 2559 additions and 2386 deletions

View file

@ -1,7 +0,0 @@
.git/
*.min.js
node_modules/
symbolic/
third-party/
tmp/
vendor/

View file

@ -1,29 +0,0 @@
{
"env": {
"browser": true
},
"extends": [
"eslint:recommended",
"standard"
],
"rules": {
"camelcase": "off",
"comma-dangle": ["warn", {
"arrays": "always-multiline",
"objects": "always-multiline"
}],
"eqeqeq": "off",
"indent": ["warn", "tab", { "SwitchCase": 1 }],
"linebreak-style": ["error", "unix"],
"max-len": ["warn", 165],
"no-tabs": "off",
"semi": ["warn", "always"],
"space-before-function-paren": ["warn", {
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}],
"yoda": "off"
},
"root": true
}

View file

@ -6,11 +6,12 @@
"line-length": false,
"no-hard-tabs": false,
"no-inline-html": {
"allowed_elements": ["br", "kbd"]
"allowed_elements": ["br", "img", "kbd"]
},
"no-multiple-blanks": {
"maximum": 2
},
"no-trailing-spaces": true,
"ul-indent": false,
"ul-style": {
"style": "consistent"

View file

@ -3,7 +3,7 @@
"plugins": [
"stylelint-order",
"stylelint-scss",
"stylelint-stylistic"
"@stylistic/stylelint-plugin"
],
"rules": {
"at-rule-empty-line-before": [
@ -11,27 +11,27 @@
"ignoreAtRules": [ "after-comment", "else" ]
}
],
"stylistic/at-rule-name-space-after": [
"@stylistic/at-rule-name-space-after": [
"always", {
"ignoreAtRules": [ "after-comment" ]
}
],
"stylistic/block-closing-brace-newline-after": [
"@stylistic/block-closing-brace-newline-after": [
"always", {
"ignoreAtRules": [ "if", "else" ]
}
],
"stylistic/block-closing-brace-newline-before": "always-multi-line",
"stylistic/block-opening-brace-newline-after": "always-multi-line",
"stylistic/block-opening-brace-space-before": "always",
"stylistic/color-hex-case": "lower",
"@stylistic/block-closing-brace-newline-before": "always-multi-line",
"@stylistic/block-opening-brace-newline-after": "always-multi-line",
"@stylistic/block-opening-brace-space-before": "always",
"@stylistic/color-hex-case": "lower",
"color-hex-length": "short",
"color-no-invalid-hex": true,
"stylistic/declaration-colon-space-after": "always",
"stylistic/declaration-colon-space-before": "never",
"stylistic/indentation": "tab",
"@stylistic/declaration-colon-space-after": "always",
"@stylistic/declaration-colon-space-before": "never",
"@stylistic/indentation": "tab",
"no-descending-specificity": null,
"stylistic/no-eol-whitespace": true,
"@stylistic/no-eol-whitespace": true,
"property-no-vendor-prefix": true,
"rule-empty-line-before": [
"always", {

View file

@ -56,11 +56,13 @@ There are some FreshRSS extensions out there, developed by community members:
### By [@CN-Tools](https://github.com/cn-tools)
* [Black List](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-BlackList): Blacklist to block feeds for users
* [Copy 2 Clipboard](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-Copy2Clipboard): Add a button in the navigation bar to copy the destination links of all visible entries into clipboard
* [Feed Title Builder](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-FeedTitleBuilder): Build your own feed title based on url, the original feed title and the date the feed was added
* [FilterTitle](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-FilterTitle): Filter out feed entries by keywords parsed by the feed entry title
* [RemoveEmojis](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-RemoveEmojis): Remove emojis in the title of newly added feed entries.
* [YouTube Channel 2 RSSFeed](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-YouTubeChannel2RssFeed): You can add a YouTube Channel URL and will get it as RSSFeed
* [RemoveEmojis](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-RemoveEmojis): Remove emojis in the title of newly added feed entries
* [SendToMyJD2](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-SendToMyJD2): Send links to a jDownloader2 instance with the myJDownloader2 API
* [YouTube Channel 2 RSSFeed](https://github.com/cn-tools/cntools_FreshRssExtensions/tree/master/xExtension-YouTubeChannel2RssFeed): You can add a YouTube Channel URL and will get it as RSSFeed. Additional you can detect YouTube shorts.
### By [@DevonHess](https://github.com/DevonHess)
@ -82,6 +84,11 @@ There are some FreshRSS extensions out there, developed by community members:
* [Pocket Button](https://github.com/christian-putzke/freshrss-pocket-button): Add articles to Pocket with one simple button click or a keyboard shortcut.
### By [@Joedmin](https://github.com/Joedmin/)
* [Readeck Button](https://github.com/Joedmin/xExtension-readeck-button): Add articles to a selected Readeck instance with one simple button click or a keyboard shortcut.
* [Wallabag Button](https://github.com/Joedmin/xExtension-wallabag-button): Add articles to a selected Wallabag instance with one simple button click or a keyboard shortcut.
### By [@printfuck](https://github.com/printfuck/)
* [Readable](https://github.com/printfuck/xExtension-Readable): Fetch article content for selected feeds with [Readability](https://github.com/mozilla/readability) or [Mercury](https://github.com/postlight/mercury-parser)
@ -134,3 +141,7 @@ There are some FreshRSS extensions out there, developed by community members:
### By [@kalvn](https://github.com/kalvn)
* [Mark Previous as Read](https://github.com/kalvn/freshrss-mark-previous-as-read): Adds a button in the footer of each entry. Clicking this button will mark all previous entries belonging to the current feed, as read.
### By [@lukasMega](https://github.com/lukasMega)
* [Word Highlighter](https://github.com/lukasMega/Extensions-FreshRSS-): Gives you ability to highlight user-defined words (using [mark.js](https://github.com/julkue/mark.js))

View file

@ -49,8 +49,8 @@
"ext-phar": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan-strict-rules": "^1.5",
"phpstan/phpstan": "^1.11",
"phpstan/phpstan-strict-rules": "^1.6",
"squizlabs/php_codesniffer": "^3.9"
},
"scripts": {

42
composer.lock generated
View file

@ -4,21 +4,21 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bf0d2d1a05ed08841ca6bd3e7ec96b74",
"content-hash": "52101009acffc9684a721cc20ec9e731",
"packages": [],
"packages-dev": [
{
"name": "phpstan/phpstan",
"version": "1.10.66",
"version": "1.11.10",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "94779c987e4ebd620025d9e5fdd23323903950bd"
"reference": "640410b32995914bde3eed26fa89552f9c2c082f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/94779c987e4ebd620025d9e5fdd23323903950bd",
"reference": "94779c987e4ebd620025d9e5fdd23323903950bd",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/640410b32995914bde3eed26fa89552f9c2c082f",
"reference": "640410b32995914bde3eed26fa89552f9c2c082f",
"shasum": ""
},
"require": {
@ -61,31 +61,27 @@
{
"url": "https://github.com/phpstan",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
"type": "tidelift"
}
],
"time": "2024-03-28T16:17:31+00:00"
"time": "2024-08-08T09:02:50+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
"version": "1.5.3",
"version": "1.6.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
"reference": "568210bd301f94a0d4b1e5a0808c374c1b9cf11b"
"reference": "363f921dd8441777d4fc137deb99beb486c77df1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/568210bd301f94a0d4b1e5a0808c374c1b9cf11b",
"reference": "568210bd301f94a0d4b1e5a0808c374c1b9cf11b",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/363f921dd8441777d4fc137deb99beb486c77df1",
"reference": "363f921dd8441777d4fc137deb99beb486c77df1",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.10.60"
"phpstan/phpstan": "^1.11"
},
"require-dev": {
"nikic/php-parser": "^4.13.0",
@ -114,22 +110,22 @@
"description": "Extra strict and opinionated rules for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.3"
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.0"
},
"time": "2024-04-06T07:43:25+00:00"
"time": "2024-04-20T06:37:51+00:00"
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.9.1",
"version": "3.10.2",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
"reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909"
"reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/267a4405fff1d9c847134db3a3c92f1ab7f77909",
"reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909",
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/86e5f5dd9a840c46810ebe5ff1885581c42a3017",
"reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017",
"shasum": ""
},
"require": {
@ -196,7 +192,7 @@
"type": "open_collective"
}
],
"time": "2024-03-31T21:03:09+00:00"
"time": "2024-07-21T23:26:44+00:00"
}
],
"aliases": [],
@ -233,5 +229,5 @@
"ext-tokenizer": "*",
"ext-xmlwriter": "*"
},
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

55
eslint.config.js Normal file
View file

@ -0,0 +1,55 @@
import globals from "globals";
import js from "@eslint/js";
import neostandard, { resolveIgnoresFromGitignore } from 'neostandard';
import stylistic from '@stylistic/eslint-plugin';
export default [
{
files: ["**/*.js"],
languageOptions: {
globals: {
...globals.browser,
},
sourceType: "script",
},
},
{
ignores: [
...resolveIgnoresFromGitignore(),
"**/*.min.js",
"extensions/",
"p/scripts/vendor/",
],
},
js.configs.recommended,
// stylistic.configs['recommended-flat'],
...neostandard(),
{
plugins: {
"@stylistic": stylistic,
},
rules: {
"camelcase": "off",
"eqeqeq": "off",
"no-empty": ["error", { "allowEmptyCatch": true }],
"no-unused-vars": ["error", {
"args": "none",
"caughtErrors": "none",
}],
"object-shorthand": "off",
"yoda": "off",
"@stylistic/indent": ["warn", "tab", { "SwitchCase": 1 }],
"@stylistic/linebreak-style": ["error", "unix"],
"@stylistic/max-len": ["warn", 165],
"@stylistic/no-tabs": "off",
"@stylistic/quotes": ["off", "single", { "avoidEscape": true }],
"@stylistic/quote-props": ["warn", "consistent"],
"@stylistic/semi": ["warn", "always"],
"@stylistic/space-before-function-paren": ["warn", {
"anonymous": "always",
"asyncArrow": "always",
"named": "never",
}],
},
},
];

View file

@ -27,7 +27,7 @@
"name": "AutoTTL",
"author": "Magnus Kokk",
"description": "A FreshRSS extension for automatic feed refresh TTL based on the average frequency of entries.",
"version": "0.5.5",
"version": "0.5.8",
"entrypoint": "AutoTTL",
"type": "user",
"url": "https://github.com/mgnsk/FreshRSS-AutoTTL",
@ -93,7 +93,7 @@
"name": "Custom CSS",
"author": "Marien Fressinaud",
"description": "Give possibility to overwrite the CSS with a user-specific rules.",
"version": "0.5.1",
"version": "0.5.2",
"entrypoint": "CustomCSS",
"type": "user",
"url": "https://github.com/FreshRSS/Extensions",
@ -104,7 +104,7 @@
"name": "Custom JS",
"author": "Frans de Jonge",
"description": "Apply custom JS.",
"version": "0.5.1",
"version": "0.5.2",
"entrypoint": "CustomJS",
"type": "user",
"url": "https://github.com/FreshRSS/Extensions",
@ -170,7 +170,7 @@
"name": "FreshRss FlareSolverr",
"author": "James Ravenscroft",
"description": "Use a Flaresolverr instance to bypass cloudflare security checks",
"version": "0.1",
"version": "0.2",
"entrypoint": "FlareSolverr",
"type": "system",
"url": "https://github.com/ravenscroftj/freshrss-flaresolverr-extension",
@ -192,7 +192,7 @@
"name": "Image Cache",
"author": "Victrid",
"description": "Cache feed images on your own facility or Cloudflare cache.",
"version": "0.3",
"version": "0.4.0",
"entrypoint": "ImageCache",
"type": "user",
"url": "https://github.com/Victrid/freshrss-image-cache-plugin",
@ -203,7 +203,7 @@
"name": "Image Proxy",
"author": "Frans de Jonge",
"description": "No insecure content warnings or disappearing images.",
"version": "0.7.2",
"version": "0.7.3",
"entrypoint": "ImageProxy",
"type": "user",
"url": "https://github.com/FreshRSS/Extensions",
@ -236,7 +236,7 @@
"name": "Kagi Summarizer",
"author": "Rudis Muiznieks",
"description": "Add buttons to summarize articles with the Kagi Universal Summarizer.",
"version": "0.2",
"version": "0.3",
"entrypoint": "KagiSummarizer",
"type": "user",
"url": "https://code.sitosis.com/rudism/freshrss-kagi-summarizer",
@ -268,8 +268,8 @@
{
"name": "Mark Previous as Read",
"author": "kalvn",
"description": "This extension adds a button in the footer of each entry. Clicking this button will mark all previous entries belonging to the current feed, as read. The goal is, when going through a very long list of entries without reading them all, to be able to stop and continue later.",
"version": "1.0.1",
"description": "This extension adds a button in the footer of each entry. Clicking this button will mark all previous entries as read.",
"version": "1.1.1",
"entrypoint": "MarkPreviousAsRead",
"type": "user",
"url": "https://github.com/kalvn/freshrss-mark-previous-as-read",
@ -291,7 +291,7 @@
"name": "News Assistant",
"author": "Mervyn Zhan",
"description": "Using the ai api of `OpenAI`, `Anthropic`, `Groq` by [Portkey-AI/gateway](https://github.com/Portkey-AI/gateway/) to summary the news.",
"version": "0.11.0",
"version": "0.11.2",
"entrypoint": "NewsAssistant",
"type": "system",
"url": "https://github.com/reply2future/xExtension-NewsAssistant",
@ -302,7 +302,7 @@
"name": "Pocket Button",
"author": "Christian Putzke",
"description": "Add articles to Pocket with one simple button click or a keyboard shortcut.",
"version": "0.4",
"version": "0.5",
"entrypoint": "PocketButton",
"type": "user",
"url": "https://github.com/christian-putzke/freshrss-pocket-button",
@ -313,7 +313,7 @@
"name": "Quick Collapse",
"author": "romibi and Marien Fressinaud",
"description": "Quickly change from folded to unfolded articles",
"version": "0.2.1",
"version": "0.2.2",
"entrypoint": "QuickCollapse",
"type": "user",
"url": "https://github.com/FreshRSS/Extensions",
@ -335,13 +335,24 @@
"name": "Readable",
"author": "printfuck",
"description": "Fetch article content for selected feeds with readability or mercury",
"version": "0.2",
"version": "0.3",
"entrypoint": "Readable",
"type": "user",
"url": "https://github.com/printfuck/xExtension-Readable",
"method": "git",
"directory": "."
},
{
"name": "Readeck Button",
"author": "Joedmin",
"description": "Add articles to Readeck with one simple button click or a keyboard shortcut.",
"version": "0.6",
"entrypoint": "ReadeckButton",
"type": "user",
"url": "https://github.com/Joedmin/xExtension-readeck-button",
"method": "git",
"directory": "."
},
{
"name": "Reading Time",
"author": "Lapineige",
@ -386,11 +397,22 @@
"method": "git",
"directory": "xExtension-RemoveEmojis"
},
{
"name": "SendToMyJD2",
"author": "CNTools | Clemens Neubauer",
"description": "Send links to a jDownloader2 instance with the myJDownloader2 API",
"version": "0.0.1-alpha",
"entrypoint": "SendToMyJD2",
"type": "user",
"url": "https://github.com/cn-tools/cntools_FreshRssExtensions",
"method": "git",
"directory": "xExtension-SendToMyJD2"
},
{
"name": "Share By Email",
"author": "Marien Fressinaud",
"description": "Improve the sharing by email system.",
"version": "0.2.2",
"version": "0.2.3",
"entrypoint": "ShareByEmail",
"type": "user",
"url": "https://github.com/FreshRSS/Extensions",
@ -400,8 +422,8 @@
{
"name": "ShowFeedID",
"author": "math-GH",
"description": "Show the feed ID",
"version": "0.2.1",
"description": "Show the ID of feed and category",
"version": "0.3.0",
"entrypoint": "ShowFeedID",
"type": "user",
"url": "https://github.com/FreshRSS/Extensions",
@ -452,17 +474,6 @@
"method": "git",
"directory": "."
},
{
"name": "TinyTinyRSS API",
"author": "Marien Fressinaud",
"description": "Provides an API compliant with TinyTinyRSS applications.",
"version": "0.1",
"entrypoint": "TTRSS_API",
"type": "system",
"url": "https://github.com/FreshRSS/Extensions",
"method": "git",
"directory": "xExtension-TTRSS_API"
},
{
"name": "Title-Wrap",
"author": "₣rans de Jonge, Joris Kinable",
@ -508,6 +519,17 @@
"method": "git",
"directory": "."
},
{
"name": "Wallabag Button",
"author": "Joedmin",
"description": "Add articles to Wallabag with one simple button click or a keyboard shortcut.",
"version": "0.2",
"entrypoint": "WallabagButton",
"type": "user",
"url": "https://github.com/Joedmin/xExtension-wallabag-button",
"method": "git",
"directory": "."
},
{
"name": "White List",
"author": "Alexis Degrugillier",
@ -519,11 +541,22 @@
"method": "git",
"directory": "."
},
{
"name": "Word highlighter",
"author": "Lukas Melega",
"description": "Highlight specific words",
"version": "0.0.2",
"entrypoint": "WordHighlighter",
"type": "user",
"url": "https://github.com/FreshRSS/Extensions",
"method": "git",
"directory": "xExtension-WordHighlighter"
},
{
"name": "YouTube Video Feed",
"author": "Kevin Papst",
"description": "Embed YouTube feeds inside article content.",
"version": "0.11",
"version": "0.12",
"entrypoint": "YouTube",
"type": "user",
"url": "https://github.com/FreshRSS/Extensions",
@ -534,7 +567,7 @@
"name": "YouTubeChannel2RssFeed",
"author": "CNTools | Clemens Neubauer",
"description": "Transfer YouTube URL into RSS Feed URL.",
"version": "0.6.0-alpha",
"version": "0.6.1",
"entrypoint": "YouTubeChannel2RssFeed",
"type": "user",
"url": "https://github.com/cn-tools/cntools_FreshRssExtensions",

3459
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
{
"name": "freshrss-extensions",
"type": "module",
"description": "Extensions for FreshRSS",
"homepage": "https://freshrss.org/",
"readmeFilename": "README.md",
@ -16,11 +17,11 @@
},
"license": "see each extension",
"engines": {
"node": ">=12"
"node": ">=18"
},
"scripts": {
"eslint": "eslint --ext .js .",
"eslint_fix": "eslint --fix --ext .js .",
"eslint": "eslint .",
"eslint_fix": "eslint --fix .",
"markdownlint": "markdownlint '**/*.md'",
"markdownlint_fix": "markdownlint --fix '**/*.md'",
"rtlcss": "npm run symbolic && rtlcss -d symbolic/ && find -L symbolic/ -type f -name '*.rtl.rtl.css' -delete",
@ -31,18 +32,17 @@
"fix": "npm run rtlcss && npm run stylelint_fix && npm run eslint_fix && npm run markdownlint_fix"
},
"devDependencies": {
"eslint": "^8.54.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-n": "^16.3.1",
"eslint-plugin-promise": "^6.1.1",
"markdownlint-cli": "^0.37.0",
"rtlcss": "^4.1.1",
"sass": "^1.69.5",
"stylelint": "^15.11.0",
"stylelint-config-recommended-scss": "^13.1.0",
"stylelint-order": "^6.0.3",
"stylelint-stylistic": "^0.4.3"
"eslint": "^9.8.0",
"@eslint/js": "^9.8.0",
"globals": "^15.9.0",
"markdownlint-cli": "^0.41.0",
"neostandard": "^0.11.2",
"rtlcss": "^4.2.0",
"sass": "^1.77.8",
"stylelint": "^16.8.1",
"stylelint-config-recommended-scss": "^14.1.0",
"stylelint-order": "^6.0.4",
"@stylistic/stylelint-plugin": "^3.0.0"
},
"rtlcssConfig": {}
}

162
phpcs.xml
View file

@ -1,119 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="FreshRSS Ruleset">
<description>Created with the PHP Coding Standard Generator. https://edorian.github.com/php-coding-standard-generator/</description>
<arg name="extensions" value="php,phtml,css,js"/>
<ruleset name="FreshRSS">
<arg name="extensions" value="php,phtml"/>
<arg name="tab-width" value="4"/>
<exclude-pattern>./lib/SimplePie/</exclude-pattern>
<exclude-pattern>./lib/PHPMailer/</exclude-pattern>
<exclude-pattern>./lib/http-conditional.php</exclude-pattern>
<exclude-pattern>./lib/lib_phpQuery.php</exclude-pattern>
<exclude-pattern>./node_modules/</exclude-pattern>
<exclude-pattern>./.git/</exclude-pattern>
<exclude-pattern>./data/config.php</exclude-pattern>
<exclude-pattern>./data/update.php</exclude-pattern>
<exclude-pattern>./data/users/*/config.php</exclude-pattern>
<exclude-pattern>./extensions/</exclude-pattern>
<exclude-pattern>./lib/http-conditional.php</exclude-pattern>
<exclude-pattern>./lib/marienfressinaud/</exclude-pattern>
<exclude-pattern>./lib/phpgt/</exclude-pattern>
<exclude-pattern>./lib/phpmailer/</exclude-pattern>
<exclude-pattern>./lib/simplepie/</exclude-pattern>
<exclude-pattern>./node_modules/</exclude-pattern>
<exclude-pattern>./p/scripts/vendor/</exclude-pattern>
<exclude-pattern>./vendor/</exclude-pattern>
<exclude-pattern>*.min.js$</exclude-pattern>
<!-- Duplicate class names are not allowed -->
<!-- Additional exclusions for Extensions: -->
<exclude-pattern>./symbolic/</exclude-pattern>
<exclude-pattern>./third-party/</exclude-pattern>
<exclude-pattern>./tmp/</exclude-pattern>
<rule ref="PSR12">
<exclude name="Generic.ControlStructures.InlineControlStructure.NotAllowed"/>
<exclude name="Generic.Formatting.DisallowMultipleStatements.SameLine"/>
<exclude name="Generic.WhiteSpace.DisallowTabIndent.NonIndentTabsUsed"/>
<exclude name="Generic.WhiteSpace.DisallowTabIndent.TabsUsed"/>
<exclude name="Generic.WhiteSpace.DisallowTabIndent.TabsUsedHeredocCloser"/>
<exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace"/>
<exclude name="PSR1.Classes.ClassDeclaration.MultipleClasses"/>
<exclude name="PSR1.Files.SideEffects.FoundWithSymbols"/>
<exclude name="PSR1.Methods.CamelCapsMethodName.NotCamelCaps"/>
<exclude name="PSR12.Classes.OpeningBraceSpace.Found"/><!-- Consider using PSR12 defaults instead -->
<exclude name="PSR12.ControlStructures.ControlStructureSpacing.CloseParenthesisLine"/>
<exclude name="PSR12.ControlStructures.ControlStructureSpacing.FirstExpressionLine"/><!-- Consider using PSR12 defaults instead -->
<exclude name="PSR12.Files.FileHeader.IncorrectOrder"/><!-- Consider using PSR12 defaults instead -->
<exclude name="PSR12.Files.FileHeader.SpacingAfterBlock"/><!-- Consider using PSR12 defaults instead -->
<exclude name="PSR12.Traits.UseDeclaration.MultipleImport"/>
<exclude name="PSR2.Classes.ClassDeclaration.CloseBraceAfterBody"/><!-- Consider using PSR12 defaults instead -->
<exclude name="PSR2.Classes.ClassDeclaration.OpenBraceNewLine"/><!-- Consider using PSR12 defaults instead -->
<exclude name="PSR2.ControlStructures.SwitchDeclaration.BodyOnNextLineCASE"/>
<exclude name="PSR2.ControlStructures.SwitchDeclaration.BreakNotNewLine"/>
<exclude name="PSR2.Functions.FunctionCallSignature.ContentAfterOpenBracket"/>
<exclude name="PSR2.Methods.FunctionCallSignature.CloseBracketLine"/>
<exclude name="PSR2.Methods.FunctionCallSignature.ContentAfterOpenBracket"/>
<exclude name="PSR2.Methods.FunctionCallSignature.Indent"/>
<exclude name="PSR2.Methods.FunctionCallSignature.MultipleArguments"/>
<exclude name="PSR2.Methods.MethodDeclaration.Underscore"/>
<exclude name="Squiz.Classes.ValidClassName.NotCamelCaps"/>
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine"/>
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.CloseBracketLine"/>
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.ContentAfterBrace"/>
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.FirstParamSpacing"/>
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.Indent"/>
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.OneParamPerLine"/>
<exclude name="Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore"/>
</rule>
<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"/>
<!-- Maximum line length -->
<rule ref="Generic.Files.LineLength">
<!-- For language strings maximum line lengths make little sense. -->
<exclude-pattern>./app/i18n/</exclude-pattern>
<!-- Dont enforce line length on the HTML; the point is to improve legibility, not reduce it -->
<exclude-pattern>*.phtml$</exclude-pattern>
<properties>
<property name="lineLimit" value="165"/>
<property name="absoluteLineLimit" value="190"/>
</properties>
<exclude-pattern>/app/i18n/*\.php$</exclude-pattern>
<exclude-pattern>*\.phtml$</exclude-pattern>
</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"/>
<rule ref="Generic.PHP.DisallowShortOpenTag">
<exclude name="Generic.PHP.DisallowShortOpenTag.EchoFound"/>
</rule>
<rule ref="Generic.PHP.DeprecatedFunctions" />
<!-- 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 concatenating -->
<rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie"/><!-- Consider using PSR12 defaults instead -->
<rule ref="Generic.PHP.DeprecatedFunctions"/>
<rule ref="Generic.Strings.UnnecessaryStringConcat">
<properties>
<!-- Allow string concatenating across multiple lines -->
<property name="allowMultiline" value="true"/>
</properties>
</rule>
<!-- Use tabs for indentation -->
<rule ref="Generic.WhiteSpace.DisallowSpaceIndent"/>
<!-- Parameters with default values must appear last in functions -->
<rule ref="PEAR.Functions.ValidDefaultValue"/>
<!-- 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"/>
<!-- Use Unix newlines -->
<rule ref="Generic.Files.LineEndings">
<properties>
<property name="eolChar" value="\n" />
</properties>
<rule ref="Generic.WhiteSpace.ScopeIndent.Incorrect">
<exclude-pattern>*\.phtml$</exclude-pattern>
<exclude-pattern>/app/install.php</exclude-pattern>
</rule>
<!-- Add space after closing parenthesis -->
<!-- Add body into new line -->
<!-- Close body in new line -->
<rule ref="Generic.WhiteSpace.ScopeIndent.IncorrectExact">
<exclude-pattern>*\.phtml$</exclude-pattern>
<exclude-pattern>/app/install.php</exclude-pattern>
</rule>
<rule ref="Internal.NoCodeFound">
<exclude-pattern>*\.phtml$</exclude-pattern>
</rule>
<!-- <rule ref="Squiz.Commenting.ClassComment.Missing"/> --><!-- Consider adding -->
<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">
<include-pattern>*\.phtml$</include-pattern>
<properties>
<property name="equalsSpacing" value="1"/>
<property name="requiredSpacesBeforeColon" value="0" />
</properties>
</rule>
<!-- Do not add spaces when casting -->
<rule ref="Squiz.WhiteSpace.CastSpacing"/>
<!-- Operators must have a space around them -->
<rule ref="Squiz.ControlStructures.ControlSignature">
<include-pattern>*\.php$</include-pattern>
</rule>
<rule ref="Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace">
<exclude-pattern>*\.phtml$</exclude-pattern>
</rule>
<rule ref="Squiz.WhiteSpace.OperatorSpacing">
<properties>
<property name="ignoreNewlines" value="true" />
<property name="ignoreNewlines" value="true"/>
</properties>
</rule>
<!-- Do not add a whitespace before a semicolon -->
<rule ref="Squiz.WhiteSpace.ScopeClosingBrace.Indent">
<exclude-pattern>*\.phtml$</exclude-pattern>
</rule>
<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"/>
<!-- Expected space after closing parenthesis -->
<rule ref="Squiz.ControlStructures.ControlSignature.SpaceAfterCloseParenthesis">
<exclude-pattern>.phtml$</exclude-pattern>
</rule>
<!-- Opening brace on same line as function declaration -->
<rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie" />
<!-- Newline required after opening brace -->
<rule ref="Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace">
<exclude-pattern>.phtml$</exclude-pattern>
<exclude-pattern>.js$</exclude-pattern>
</rule>
<!-- No PHP code was found in this file -->
<rule ref="Internal.NoCodeFound">
<exclude-pattern>.phtml$</exclude-pattern>
</rule>
</ruleset>

View file

@ -7,4 +7,3 @@ parameters:
analyse:
- xExtension-ImageProxy/configure.phtml
- xExtension-ImageProxy/extension.php
- xExtension-TTRSS_API/ttrss.php

View file

@ -1,6 +1,5 @@
parameters:
level: 0
treatPhpDocTypesAsCertain: false
fileExtensions:
- php
- phtml
@ -22,3 +21,4 @@ parameters:
dynamicConstantNames:
- TYPE_GIT
reportMaybesInPropertyPhpDocTypes: false
treatPhpDocTypesAsCertain: false

View file

@ -2,7 +2,6 @@ parameters:
# TODO: Increase rule-level https://phpstan.org/user-guide/rule-levels
level: 1
phpVersion: 80399 # TODO: Remove line when moving composer.json to PHP 8+
treatPhpDocTypesAsCertain: false
fileExtensions:
- php
- phtml
@ -23,6 +22,7 @@ parameters:
- TYPE_GIT
checkMissingOverrideMethodAttribute: true
reportMaybesInPropertyPhpDocTypes: false
treatPhpDocTypesAsCertain: false
strictRules:
allRules: false
booleansInConditions: true
@ -37,5 +37,12 @@ parameters:
strictCalls: true
switchConditionsMatchingType: true
uselessCast: true
exceptions:
check:
missingCheckedExceptionInThrows: false # TODO pass
tooWideThrowType: true
implicitThrows: false
checkedExceptionClasses:
- 'Minz_Exception'
includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon

View file

@ -56,6 +56,12 @@
"url": "https://github.com/christian-putzke/freshrss-pocket-button",
"type": "git"
}, {
"url": "https://github.com/Joedmin/xExtension-readeck-button",
"type": "git"
}, {
"url": "https://github.com/Joedmin/xExtension-wallabag-button",
"type": "git"
},{
"url": "https://github.com/printfuck/xExtension-Readable",
"type": "git"
}, {

View file

@ -1,11 +0,0 @@
# TinyTinyRSS API extension
This extension provides a TTRSS-compliant API. It means with this extension you can use applications made for TTRSS initially.
Note the API is NOT FULLY SUPPORTED YET!
Please be sure to enable API in the configuration and set an API password on your profile.
URL to use is something like <http://rss.example.com/api/ttrss.php> or <http://example.com/freshrss/p/api/ttrss.php>.
To use it, upload this directory in your `./extensions` directory and enable it on the extension panel in FreshRSS.

View file

@ -1,58 +0,0 @@
<?php
declare(strict_types=1);
final class TTRSS_APIExtension extends Minz_Extension {
#[\Override]
public function init(): void {
$this->registerHook('post_update', [$this, 'postUpdateHook']);
}
#[\Override]
public function install() {
$filename = 'ttrss.php';
$file_source = join_path($this->getPath(), $filename);
$path_destination = join_path(PUBLIC_PATH, 'api');
$file_destination = join_path($path_destination, $filename);
if (!is_writable($path_destination)) {
return 'server cannot write in ' . $path_destination;
}
if (file_exists($file_destination)) {
if (!unlink($file_destination)) {
return 'API file seems already existing but cannot be removed';
}
}
if (!file_exists($file_source)) {
return 'API file seems not existing in this extension. Try to download it again.';
}
if (!copy($file_source, $file_destination)) {
return 'the API file has failed during installation.';
}
return true;
}
#[\Override]
public function uninstall() {
$filename = 'ttrss.php';
$file_destination = join_path(PUBLIC_PATH, 'api', $filename);
if (file_exists($file_destination) && !unlink($file_destination)) {
return 'API file cannot be removed';
}
return true;
}
public function postUpdateHook(): void {
$res = $this->install();
if ($res !== true) {
Minz_Log::warning('Problem during TTRSS API extension post update: ' . $res);
}
}
}

View file

@ -1,8 +0,0 @@
{
"name": "TinyTinyRSS API",
"author": "Marien Fressinaud",
"description": "Provides an API compliant with TinyTinyRSS applications.",
"version": "0.1",
"entrypoint": "TTRSS_API",
"type": "system"
}

View file

@ -1,537 +0,0 @@
<?php
require('../../constants.php');
require(LIB_PATH . '/lib_rss.php'); // Includes class autoloader
final class MyPDO extends Minz_ModelPdo {
}
final class FreshAPI_TTRSS {
const API_LEVEL = 11;
const STATUS_OK = 0;
const STATUS_ERR = 1;
private $seq = 0;
private $user = '';
private $method = 'index';
private $params = array();
private $system_conf = null;
private $user_conf = null;
public function __construct($params) {
$this->seq = isset($params['seq']) ? $params['seq'] : 0;
$this->user = Minz_Session::paramString('currentUser');
$this->method = $params['op'];
$this->params = $params;
$this->system_conf = Minz_Configuration::get('system');
if ($this->user != '') {
$this->user_conf = get_user_configuration($this->user);
}
}
public function param($param, $default = false) {
if (isset($this->params[$param])) {
return $this->params[$param];
} else {
return $default;
}
}
public function good($reply) {
$this->response($reply, self::STATUS_OK);
}
public function bad($reply) {
$this->response($reply, self::STATUS_ERR);
}
private function response($reply, $status) {
header('Content-Type: text/json; charset=utf-8');
$result = json_encode(array(
'seq' => $this->seq,
'status' => $status,
'content' => $reply,
));
// Minz_Log::debug($result);
print($result);
exit();
}
public function handle() {
if (!$this->system_conf->api_enabled) {
$this->bad(array(
'error' => 'API_DISABLED'
));
}
if ($this->user === '' &&
!in_array($this->method, ['login', 'isloggedin'], true)) {
$this->bad(array(
'error' => 'NOT_LOGGED_IN'
));
}
if (is_callable(array($this, $this->method))) {
call_user_func(array($this, $this->method));
} else {
Minz_Log::warning('TTRSS API: ' . $this->method . '() method does not exist');
}
}
private function auth_user($username, $password) {
if (!function_exists('password_verify')) {
include_once(LIB_PATH . '/password_compat.php');
}
$user_conf = get_user_configuration($username);
if (is_null($user_conf)) {
return false;
}
if ($user_conf->apiPasswordHash != '' &&
password_verify($password, $user_conf->apiPasswordHash)) {
Minz_Session::_param('currentUser', $username);
return true;
} else {
return false;
}
}
public function getApiLevel() {
$this->good(array(
'level' => self::API_LEVEL
));
}
public function getVersion() {
$this->good(array(
'version' => FRESHRSS_VERSION
));
}
public function login() {
$username = $this->param('user');
$password = $this->param('password');
$password_base64 = base64_decode($this->param('password'), true);
if ($this->auth_user($username, $password) ||
$this->auth_user($username, $password_base64)) {
$this->good(array(
'session_id' => session_id(),
'api_level' => self::API_LEVEL,
));
} else {
Minz_Log::warning('TTRSS API: invalid user login: ' . $username);
$this->bad(array(
'error' => 'LOGIN_ERROR'
));
}
}
public function logout() {
Minz_Session::_param('currentUser');
$this->good(array(
'status' => 'OK'
));
}
public function isLoggedIn() {
$this->good(array(
'status' => $this->user !== ''
));
}
public function getCategories() {
$unread_only = $this->param('unread_only', false);
$include_empty = $this->param('include_empty', true);
// $enable_nested = $this->param('enable_nested', true); // not supported
$pdo = new MyPDO();
$sql = <<<SQL
SELECT DISTINCT c.id, c.name, COUNT(f.id) AS nb_feeds,
(SELECT COUNT(e.id) FROM entry e WHERE e.id_feed = f.id AND e.is_read=0) AS unread
FROM `_category` c
LEFT JOIN `_feed` f ON c.id = f.category
GROUP BY c.id, c.name
SQL;
$stm = $pdo->prepare($sql);
$stm->execute();
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$caterogies = array();
foreach ($res as $cat) {
if ($unread_only && $cat['unread'] <= 0 ||
!$include_empty && $cat['nb_feeds'] <= 0) {
continue;
}
$caterogies[] = array(
'id' => $cat['id'],
'title' => $cat['name'],
'unread' => $cat['unread'],
);
}
$this->good($caterogies);
}
public function getFeeds() {
$cat_id = $this->param('cat_id');
$unread_only = $this->param('unread_only', false);
$limit = (int)$this->param('limit', -1);
$offset = (int)$this->param('offset', -1);
// $include_nested = $this->param('include_nested', false) === true; // not supported
$sql_values = array();
$sql_where = '';
if ($cat_id >= 0) {
// special ids are not supported (yet?)
$sql_where = ' WHERE f.category = ?';
$sql_values[] = $cat_id;
}
$sql_limit = '';
if ($limit >= 0 && $offset >= 0) {
$sql_limit = ' LIMIT ? OFFSET ?';
$sql_values[] = $limit;
$sql_values[] = $offset;
}
$pdo = new MyPDO();
$sql = 'SELECT f.id, f.name, f.url, f.category, f.cache_nbUnreads AS unread, f.lastUpdate'
. ' FROM `%_feed` f'
. $sql_where
. $sql_limit;
$stm = $pdo->prepare($sql);
$stm->execute($sql_values);
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
$feeds = array();
foreach ($res as $feed) {
if ($unread_only && $feed['unread'] <= 0) {
continue;
}
$feeds[] = array(
'id' => $feed['id'],
'title' => $feed['name'],
'feed_url' => $feed['url'],
'unread' => $feed['unread'],
'has_icon' => true,
'cat_id' => $feed['category'],
'last_updated' => $feed['lastUpdate']
);
}
$this->good($feeds);
}
public function getHeadlines() {
$feed_id = $this->param('feed_id');
if ($feed_id === false) {
$this->bad(array(
'error' => 'INCORRECT_USAGE'
));
}
$limit = min(200, (int)$this->param('limit', 200));
$offset = (int)$this->param('skip', 0);
$is_cat = $this->param('is_cat', true);
$show_excerpt = $this->param('show_excerpt', false);
$show_content = $this->param('show_content', true);
$view_mode = $this->param('view_mode', 'all_articles');
$since_id = $this->param('since_id', '');
$order_by = $this->param('order_by', 'feed_dates');
$search = $this->param('search', '');
// $filter = $this->param('filter'); // not supported
// $include_attachments = $this->param('include_attachments'); // not supported
// $include_nested = $this->param('include_nested'); // not supported
// $force_update = $this->param('force_update', false); // not supported
// $sanitize = $this->param('sanitize', true); // not supported
// $has_sandbox = $this->param('has_sandbox', false); // not supported
// $search_mode = $this->param('search_mode', 'all_feeds'); // not supported
// $match_on = $this->param('match_on'); // not supported
// Get the current state
$state = 0;
switch ($view_mode) {
case 'unread':
case 'adaptive':
$state = FreshRSS_Entry::STATE_NOT_READ;
break;
case 'marked':
$state = FreshRSS_Entry::STATE_FAVORITE;
break;
case 'updated': // not supported
case 'all_articles':
default:
$state = FreshRSS_Entry::STATE_ALL;
}
// Get the current type
$id = '';
$type = 'A';
switch ($feed_id) {
case 0: // archived (not supported)
case -1: // starred
$type = 's';
break;
case -2: // published (not supported)
case -3: // fresh (not supported)
case -4: // all articles
// nothing to do
break;
default:
// if ($is_cat) {
// $type = 'c';
// } else {
// $type = 'f';
// }
$type = 'f';
$id = $feed_id;
}
// Get the order
$order = 'DESC';
if ($order_by === 'date_reverse') {
$order = 'ASC';
}
// Fix the limit: since we don't have any mechanism of offset in
// listWhere, it will be done in PHP. Limit has to be increased by
// the value of offset.
$limit += $offset;
$entryDAO = FreshRSS_Factory::createEntryDao();
$entries = $entryDAO->listWhere($type, $id, $state, $order, $limit, $since_id, $search, $since_id);
$headlines = array();
$nb_items = 0;
foreach ($entries as $item) {
if ($nb_items < $offset) {
$nb_items++;
continue;
}
$feed = $item->feed(true);
$line = array(
'id' => $item->id(),
'unread' => !$item->isRead(),
'marked' => $item->isFavorite(),
'published' => true,
'updated' => $item->date(true),
'is_updated' => false,
'title' => $item->title(),
'link' => $item->link(),
'tags' => $item->tags(),
'author' => $item->authors(true),
'feed_id' => $feed->id(),
'feed_title' => $feed->name(),
);
if ($show_excerpt) {
// @todo add a facultative max char in content method to get
// an excerpt.
$line['excerpt'] = $item->content();
}
if ($show_content) {
$line['content'] = $item->content();
}
$headlines[] = $line;
}
$this->good($headlines);
}
public function updateArticle() {
$article_ids = $this->param('article_ids', '');
$mode = $this->param('mode'); // 0 set to false, 1 set to true,
// 2 toggle but not supported.
$field = $this->param('field');
// $data = $this->param('data'); // not supported
$article_ids = explode(',', $article_ids);
$entryDAO = FreshRSS_Factory::createEntryDao();
$number_article_updated = 0;
switch ($field) {
case 0: // starred
$number_article_updated = $entryDAO->markFavorite($article_ids, $mode);
break;
case 2: // unread
$number_article_updated = $entryDAO->markRead($article_ids, !$mode);
break;
case 1: // published (not supported)
case 3: // article note (not supported)
default:
// nothing to do
}
$this->good(array(
'status' => 'OK',
'updated' => $number_article_updated,
));
}
public function catchupFeed() {
$id = $this->param('feed_id');
$is_cat = $this->param('is_cat', true);
$entryDAO = FreshRSS_Factory::createEntryDao();
switch ($id) {
case -1: // starred
$entryDAO->markReadEntries(0, true);
break;
case -4: // all articles
$entryDAO->markReadEntries();
break;
case 0: // archived (not supported)
case -2: // published (not supported)
case -3: // fresh (not supported)
break;
default:
// if ($is_cat) {
// $entryDAO->markReadCat($id);
// } else {
// $entryDAO->markReadFeed($id);
// }
$entryDAO->markReadFeed($id);
}
$this->good(array(
'status' => 'OK'
));
}
public function getCounters() {
$categoryDAO = new FreshRSS_CategoryDAO();
$counters = array();
$total_unreads = 0;
// Get feed unread counters
$categories = $categoryDAO->listCategories(true, true);
foreach ($categories as $cat) {
foreach ($cat->feeds() as $feed) {
$counters[] = array(
'id' => $feed->id(),
'counter' => $feed->nbNotRead(),
);
}
$total_unreads += $cat->nbNotRead();
}
// Get global unread counter
$counters[] = array(
'id' => 'global-unread',
'counter' => $total_unreads,
);
// Get favorite unread counter
$entryDAO = FreshRSS_Factory::createEntryDao();
$fav_counters = $entryDAO->countUnreadReadFavorites();
$counters[] = array(
'id' => -1,
'counter' => $fav_counters['unread'],
);
$this->good($counters);
}
public function getFeedTree() {
$include_empty = $this->param('include_empty', true);
$tree = array(
'identifier' => 'id',
'label' => 'name',
'items' => array(),
);
$categoryDAO = new FreshRSS_CategoryDAO();
$categories = $categoryDAO->listCategories(true, true);
foreach ($categories as $cat) {
$tree_cat = array(
'id' => 'CAT:' . $cat->id(),
'name' => $cat->name(),
'unread' => $cat->nbNotRead(),
'type' => 'category',
'bare_id' => $cat->id(),
'items' => array(),
);
foreach ($cat->feeds() as $feed) {
$tree_cat['items'][] = array(
'id' => 'FEED:' . $feed->id(),
'name' => $feed->name(),
'unread' => $feed->nbNotRead(),
'type' => 'feed',
'error' => $feed->inError(),
'updated' => $feed->lastUpdate(),
'bare_id' => $feed->id(),
);
}
if (count($tree_cat['items']) > 0 || $include_empty) {
$tree['items'][] = $tree_cat;
}
}
$this->good(array(
'categories' => $tree
));
}
public function getUnread() {
Minz_Log::warning('TTRSS API: getUnread() not implemented');
}
public function getArticle() {
Minz_Log::warning('TTRSS API: getArticle() not implemented');
}
public function getConfig() {
Minz_Log::warning('TTRSS API: getConfig() not implemented');
}
public function updateFeed() {
Minz_Log::warning('TTRSS API: updateFeed() not implemented');
}
public function getPref() {
Minz_Log::warning('TTRSS API: getPref() not implemented');
}
public function getLabels() {
Minz_Log::warning('TTRSS API: getLabels() not implemented');
}
public function setArticleLabel() {
Minz_Log::warning('TTRSS API: setArticleLabel() not implemented');
}
public function shareToPublish() {
Minz_Log::warning('TTRSS API: shareToPublish() not implemented');
}
public function subscribeToFeed() {
Minz_Log::warning('TTRSS API: subscribeToFeed() not implemented');
}
public function unsubscribeFeed() {
Minz_Log::warning('TTRSS API: unsubscribeFeed() not implemented');
}
}
Minz_Configuration::register('system',
DATA_PATH . '/config.php',
DATA_PATH . '/config.default.php');
$input = file_get_contents("php://input");
// Minz_Log::debug($input);
$input = json_decode($input, true);
if (isset($input["sid"])) {
session_id($input["sid"]);
}
Minz_Session::init('FreshRSS');
$api = new FreshAPI_TTRSS($input);
$api->handle();

View file

@ -0,0 +1,30 @@
# WordHighlighter extension
A FreshRSS extension which give ability to highlight user-defined words.
## Usage
To use it, upload this directory in your `./extensions` directory and enable it on the extension panel in FreshRSS. You can add words to be highlighted by clicking on the manage button ⚙️.
See also official docs at freshrss.github.io/FreshRSS/en/admins/15_extensions.html
## Preview
Light theme:
![snapshot](./snapshot.png)
<!-- markdownlint-disable -->
<details>
<summary>click to see example screenshot in dark theme</summary>
![snapshot-dark-theme](./snapshot-dark.png)
</details>
<!-- markdownlint-enable -->
## Changelog
- 0.0.2 use `json` for storing configuration, add more configuration options
(enable_in_article, enable_logs, case_sensitive, separate_word_search)
and refactored & simplified code
- 0.0.1 initial version (as a proper FreshRSS extension)

View file

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
/** @var WordHighlighterExtension $this */
?>
<form action="<?= _url('extension', 'configure', 'e', urlencode($this->getName())) ?>" method="post">
<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
<div class="form-group">
<label class="group-name" for="words_list">
<?= _t('ext.word_highlighter.write_words') ?> <br />
<?= _t('ext.word_highlighter.write_words_more') ?>
</label>
<div class="group-controls">
<textarea name="words_list"
id="words_list"><?= $this->word_highlighter_conf ?></textarea>
</div>
</div>
<div class="form-group">
<label for="enable-in-article" class="group-name">
<?= _t('ext.word_highlighter.enable_in_article') ?>
</label>
<div class="group-controls">
<?php if ($this->enable_in_article == '') { ?>
<input type="checkbox" name="enable-in-article" id="enable-in-article"></input>
<?php } else { ?>
<input type="checkbox" name="enable-in-article" id="enable-in-article" checked="true"></input>
<?php } ?>
<i><?= _t('ext.word_highlighter.enable_in_article_more') ?></i>
</div>
</div>
<details>
<summary>Click to see more advanced options</summary>
<div class="form-group">
<label for="case_sensitive" class="group-name">
<?= _t('ext.word_highlighter.case_sensitive') ?>
</label>
<div class="group-controls">
<?php if ($this->case_sensitive == '') { ?>
<input type="checkbox" name="case_sensitive" id="case_sensitive"></input>
<?php } else { ?>
<input type="checkbox" name="case_sensitive" id="case_sensitive" checked="true"></input>
<?php } ?>
</div>
</div>
<div class="form-group">
<label for="separate_word_search" class="group-name">
<?= _t('ext.word_highlighter.separate_word_search') ?>
</label>
<div class="group-controls">
<?php if ($this->separate_word_search == '') { ?>
<input type="checkbox" name="separate_word_search" id="separate_word_search"></input>
<?php } else { ?>
<input type="checkbox" name="separate_word_search" id="separate_word_search" checked="true"></input>
<?php } ?>
</div>
</div>
<div class="form-group">
<label for="enable_logs" class="group-name">
<?= _t('ext.word_highlighter.enable_logs') ?>
</label>
<div class="group-controls">
<?php if ($this->enable_logs == '') { ?>
<input type="checkbox" name="enable_logs" id="enable_logs"></input>
<?php } else { ?>
<input type="checkbox" name="enable_logs" id="enable_logs" checked="true"></input>
<?php } ?>
</div>
</div>
</details>
<div class="form-group form-actions">
<?php if ($this->permission_problem !== '') { ?>
<p class="alert alert-error"><?= _t('ext.word_highlighter.permission_problem', $this->permission_problem) ?></p>
<?php } else { ?>
<div class="group-controls">
<button type="submit" class="btn btn-important"><?= _t('gen.action.submit') ?></button>
<button type="reset" class="btn"><?= _t('gen.action.cancel') ?></button>
</div>
<?php } ?>
</div>
</form>

View file

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
final class WordHighlighterExtension extends Minz_Extension
{
const JSON_ENCODE_CONF = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR;
public string $word_highlighter_conf = 'test';
public string $permission_problem = '';
public bool $enable_in_article = false;
public bool $enable_logs = false;
public bool $case_sensitive = false;
public bool $separate_word_search = false;
#[\Override]
public function init(): void
{
$this->registerTranslates();
// register CSS for WordHighlighter:
Minz_View::appendStyle($this->getFileUrl('style.css', 'css'));
Minz_View::appendScript($this->getFileUrl('mark.min.js', 'js'), false, false, false);
$current_user = Minz_Session::paramString('currentUser');
$staticPath = join_path($this->getPath(), 'static');
$configFileJs = join_path($staticPath, ('config.' . $current_user . '.js'));
if (file_exists($configFileJs)) {
Minz_View::appendScript($this->getFileUrl(('config.' . $current_user . '.js'), 'js'));
}
Minz_View::appendScript($this->getFileUrl('word-highlighter.js', 'js'));
}
#[\Override]
public function handleConfigureAction(): void
{
$this->registerTranslates();
$current_user = Minz_Session::paramString('currentUser');
$staticPath = join_path($this->getPath(), 'static');
$configFileJson = join_path($staticPath, ('config.' . $current_user . '.json'));
if (!file_exists($configFileJson) && !is_writable($staticPath)) {
$tmpPath = explode(EXTENSIONS_PATH . '/', $staticPath);
$this->permission_problem = $tmpPath[1] . '/';
} elseif (file_exists($configFileJson) && !is_writable($configFileJson)) {
$tmpPath = explode(EXTENSIONS_PATH . '/', $configFileJson);
$this->permission_problem = $tmpPath[1];
} elseif (Minz_Request::isPost()) {
$configWordList = html_entity_decode(Minz_Request::paramString('words_list'));
$this->word_highlighter_conf = $configWordList;
$this->enable_in_article = (bool) Minz_Request::paramString('enable-in-article');
$this->enable_logs = (bool) Minz_Request::paramString('enable_logs');
$this->case_sensitive = (bool) Minz_Request::paramString('case_sensitive');
$this->separate_word_search = (bool) Minz_Request::paramString('separate_word_search');
$configObj = [
'enable_in_article' => $this->enable_in_article,
'enable_logs' => $this->enable_logs,
'case_sensitive' => $this->case_sensitive,
'separate_word_search' => $this->separate_word_search,
'words' => preg_split("/\r\n|\n|\r/", $configWordList),
];
$configJson = json_encode($configObj, WordHighlighterExtension::JSON_ENCODE_CONF);
file_put_contents(join_path($staticPath, ('config.' . $current_user . '.json')), $configJson . PHP_EOL);
file_put_contents(join_path($staticPath, ('config.' . $current_user . '.js')), $this->jsonToJs($configJson) . PHP_EOL);
}
if (file_exists($configFileJson)) {
try {
$confJson = json_decode(file_get_contents($configFileJson) ?: '', true, 8, JSON_THROW_ON_ERROR);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($confJson)) {
return;
}
$this->enable_in_article = (bool) ($confJson['enable_in_article'] ?? false);
$this->enable_logs = (bool) ($confJson['enable_logs'] ?? false);
$this->case_sensitive = (bool) ($confJson['case_sensitive'] ?? false);
$this->separate_word_search = (bool) ($confJson['separate_word_search'] ?? false);
$this->word_highlighter_conf = implode("\n", (array) ($confJson['words'] ?? []));
} catch (Exception $exception) {
// probably nothing to do needed
}
}
}
private function jsonToJs(string $jsonStr): string
{
$js = "window.WordHighlighterConf = " .
$jsonStr . ";\n" .
"window.WordHighlighterConf.enable_logs && console.log('WordHighlighter: loaded user config:', window.WordHighlighterConf);";
return $js;
}
}

View file

@ -0,0 +1,15 @@
<?php
return array(
'word_highlighter' => array(
'write_words' => 'Words to highlight',
'write_words_more' => '(separated by newline)',
'enable_in_article' => 'Enable highlighting also in article',
'enable_in_article_more' => '(⚠️ may be slower with a lot of words)',
'enable_logs' => 'Enable logs',
'case_sensitive' => 'Case sensitive',
'separate_word_search' => 'Separate word search',
'test_highlighting_word' => 'highlight',
'permission_problem' => 'Your config file is not writable, please change the file permissions for %s',
),
);

View file

@ -0,0 +1,15 @@
<?php
return array(
'word_highlighter' => array(
'write_words' => 'Mots à surligner',
'write_words_more' => '(séparés par une nouvelle ligne)',
'enable_in_article' => 'Activer la mise en évidence également dans larticle',
'enable_in_article_more' => '(⚠️ peut être plus lent avec beaucoup de mots)',
'enable_logs' => 'Activer les journaux',
'case_sensitive' => 'Sensible à la casse',
'separate_word_search' => 'Recherche de mots séparés',
'test_highlighting_word' => 'surligner',
'permission_problem' => 'Votre fichier de configuration nest pas accessible en écriture, veuillez modifier les permissions du fichier %s',
),
);

View file

@ -0,0 +1,8 @@
{
"name": "Word highlighter",
"author": "Lukas Melega",
"description": "Highlight specific words",
"version": "0.0.2",
"entrypoint": "WordHighlighter",
"type": "user"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View file

@ -0,0 +1,2 @@
config-words.*.js
config-words.*.txt

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,14 @@
/* WordHighlighter v0.0.2 (FreshRSS Extension) CSS */
#stream mark {
padding: 2px;
padding-right: 0; /* because in case when part of word is highlighted */
border-radius: 4px;
}
#stream mark.mark-secondary {
background-color: rgba(255, 255, 0, 0.3) !important;
}
html[class*="darkMode"] #stream mark.mark-secondary {
background-color: rgba(255, 255, 0, 0.5) !important;
}

View file

@ -0,0 +1,89 @@
'use strict';
/* WordHighlighter v0.0.2 (FreshRSS Extension) */
function wordHighlighter(c /* console */, Mark, context, OPTIONS) {
const markConf = (done, counter) => ({
caseSensitive: OPTIONS.case_sensitive || false,
separateWordSearch: OPTIONS.separate_word_search || false,
ignoreJoiners: OPTIONS.ignore_joiners || false,
exclude: [
'mark',
...(OPTIONS.enable_in_article ? [] : ['article *']),
],
done: (n) => (counter.value += n) && done(),
noMatch: done,
});
const m = new Mark(context);
const changePageListener = debounce(200, (x) => {
OPTIONS.enable_logs && c.group('WordHighlighter: page change');
stopObserving();
highlightWords(m, startObserving);
});
const mo = new MutationObserver(changePageListener);
mo.observe(context, { subtree: true, childList: true });
function startObserving() {
mo.observe(context, { subtree: true, childList: true });
}
function stopObserving() {
mo.disconnect();
}
function highlightWords(m, done) {
const start = performance.now();
const hCounter = { value: 0 };
new Promise((resolve) =>
m.mark(OPTIONS.words || [], { ...markConf(resolve, hCounter) })
)
.finally(() => {
if (OPTIONS.enable_logs) {
c.log(`WordHighlighter: ${hCounter.value} new highlights added in ${performance.now() - start}ms.`);
c.groupEnd();
}
typeof done === 'function' && done();
});
}
highlightWords(m);
}
// MAIN:
(function main() {
try {
const confName = 'WordHighlighterConf';
const OPTIONS = window[confName] || { };
const onMainPage = !(new URL(window.location)).searchParams.get('c');
if (onMainPage) {
console.log('WordHighlighter: script load...');
const context = document.querySelector('#stream');
wordHighlighter(console, window.Mark || (Error('mark.js library is not loaded ❗️')), context, OPTIONS);
console.log('WordHighlighter: script loaded.✅');
} else {
OPTIONS.enable_logs && console.log('WordHighlighter: ❗️ paused outside of feed page');
}
return Promise.resolve();
} catch (error) {
console.error('WordHighlighter: ❌', error);
return Promise.reject(error);
}
})();
// Util functions:
function debounce(duration, func) {
let timeout;
return function (...args) {
const effect = () => {
timeout = null;
return func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(effect, duration);
};
}

View file

@ -1,8 +1,8 @@
{
"name": "ShowFeedID",
"author": "math-GH",
"description": "Show the feed ID",
"version": "0.2.1",
"description": "Show the ID of feed and category",
"version": "0.3.0",
"entrypoint": "ShowFeedID",
"type": "user"
}

View file

@ -7,7 +7,7 @@ if (url.searchParams.get('c') === 'subscription') {
button.classList.add('btn');
button.id = 'showFeedId';
button.innerHTML = '<img class="icon" src="../themes/icons/look.svg" /> FeedID';
button.innerHTML = '<img class="icon" src="../themes/icons/look.svg" /> Show IDs';
div.appendChild(button);
document.getElementById('showFeedId').addEventListener('click', function (e) {
@ -22,5 +22,17 @@ if (url.searchParams.get('c') === 'subscription') {
feedname_elem.innerHTML = feedname_elem.textContent + ' (ID: ' + feedId + ')';
}
});
const cats = document.querySelectorAll('div.box > ul.box-content');
let catId;
let catname_elem;
cats.forEach(function (cat) {
catId = cat.dataset.catId;
catname_elem = cat.parentElement.querySelectorAll('div.box-title > h2')[0];
if (catname_elem) {
catname_elem.innerHTML = catname_elem.textContent + ' (ID: ' + catId + ')';
}
});
});
}