Forráskód Böngészése

Add core extensions: UserCSS, UserJS (#6267)

* Copy CustomCSS and CustomJS

Original: FreshRSS/Extensions@9f21984

* Rename CustomCSS -> UserCSS

* Rename CustomJS -> UserJS

* Change metadata

The name is used for the directory where the configuration
is stored and should not contain spaces.
Since the name was changed, I reset the version number and
changed to semantic versioning.

* Change data directory

Changed the location of the configuration file to
the user data directory, because it is not `static`.
That way, the user's configurations are gathered
in the user directory, which makes it easier to backup them.

* Edit documentations

Remove procedures to install the extension
because it is no longer necessary.

* Fix wrong variables in the configuration page

Remove permission error indication because the storage location
is now in the user data directory managed by the application.

* Remove the `xExtension-` prefix for core extensions

* Set version to 1.0.0 for UserCSS, UserJS

* Refactoring

* Remove unused variables

* Remove version 0.0.1 in Changelog

Version 0.0.1 will not be merged, so only version 1.0.0 will remain.

* public getFileUrl

* Revert more protected

* Use entrypoint for extension user path instead of name

* Add space to extension name

* Add `#[\Override]`

* Add explains of User CSS and User JS to docs

* Remove README of User CSS and User JS

* Add migration code for extension user path

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
hkcomori 1 éve
szülő
commit
99b1d551e6

+ 7 - 2
docs/en/developers/03_Backend/05_Extensions.md

@@ -56,8 +56,11 @@ If you want to write a `HelloWorld` extension, the directory name should be `xEx
 
 In the file `freshrss/extensions/xExtension-HelloWorld/extension.php` you need the structure:
 ```php
-class HelloWorldExtension extends Minz_Extension {
-	public function init() {
+final class HelloWorldExtension extends Minz_Extension {
+	#[\Override]
+	public function init(): void {
+		parent::init();
+
 		// your code here
 	}
 }
@@ -136,6 +139,8 @@ final class HelloWorldExtension extends Minz_Extension
 {
 	#[\Override]
 	public function init(): void {
+		parent::init();
+
 		$this->registerHook('entry_before_display', [$this, 'renderEntry']);
 		$this->registerHook('check_url_before_add', [self::class, 'checkUrl']);
 	}

+ 39 - 1
docs/en/users/05_Configuration.md

@@ -177,6 +177,45 @@ You can change your email address or password here. The authentication token is
 
 Extensions can be managed from this menu. Note that while extensions can be removed from the web interface, they cannot be added from it.
 
+Some extensions have configurations and these can be changed in the manage page, which opens with the button near the name of the extension.
+
+## User CSS
+
+It gives ability to create user-specific CSS rules to apply in addition of the actual theme.
+
+### Example: Getting rid of Top Menu Items
+
+The Top Menu within the mobile view might look a little bit cluttered, depending on the theme. The following CSS rules allow to hide unnecessary top menu buttons or input boxes.
+
+```css
+@media (max-width: 840px)
+{
+    /* Hides "Actions" Menu in Mobile View */
+    #nav_menu_actions {
+        display: none;
+    }
+
+    /* Hides "Views" Menu in Mobile View */
+    #nav_menu_views {
+        display: none;
+    }
+
+    /* Hides "Search" Input Box in Mobile View */
+    .nav_menu .item.search {
+        display: none;
+    }
+
+    /* Hides the Dropdown Menu Button next to the "Mark all read" Button in Mobile View */
+    #mark-read-menu .dropdown {
+        display: none;
+    }
+}
+```
+
+## User JS
+
+It gives ability to create user-specific JS.
+
 # Users
 
 > **TODO**
@@ -199,4 +238,3 @@ Require user marie
 ```
 
 More information can be found in the [Apache documentation](http://httpd.apache.org/docs/trunk/howto/auth.html#gettingitworking).
-

+ 6 - 1
docs/fr/developers/03_Backend/05_Extensions.md

@@ -81,8 +81,11 @@ class name `HelloWorldExtension`.
 In the file `freshrss/extensions/xExtension-HelloWorld/extension.php` you
 need the structure:
 ```html
-class HelloWorldExtension extends Minz_Extension {
+final class HelloWorldExtension extends Minz_Extension {
+	#[\Override]
 	public function init() {
+		parent::init();
+
 		// your code here
 	}
 }
@@ -192,6 +195,8 @@ final class HelloWorldExtension extends Minz_Extension
 {
 	#[\Override]
 	public function init(): void {
+		parent::init();
+
 		$this->registerHook('entry_before_display', [$this, 'renderEntry']);
 		$this->registerHook('check_url_before_add', [self::class, 'checkUrl']);
 	}

+ 10 - 4
docs/i18n/freshrss.fr.po

@@ -1756,8 +1756,11 @@ msgstr ""
 #: en/./developers/03_Backend/05_Extensions.md:58
 #, no-wrap
 msgid ""
-"class HelloWorldExtension extends Minz_Extension {\n"
-"\tpublic function init() {\n"
+"final class HelloWorldExtension extends Minz_Extension {\n"
+"\t#[\Override]\n"
+"\tpublic function init(): void {\n"
+"\t\tparent::init();\n"
+"\n"
 "\t\t// your code here\n"
 "\t}\n"
 "}\n"
@@ -2003,9 +2006,12 @@ msgstr ""
 #: en/./developers/03_Backend/05_Extensions.md:134
 #, no-wrap
 msgid ""
-"class HelloWorldExtension extends Minz_Extension\n"
+"final class HelloWorldExtension extends Minz_Extension\n"
 "{\n"
-"\tpublic function init() {\n"
+"\t#[\Override]\n"
+"\tpublic function init(): void {\n"
+"\t\tparent::init();\n"
+"\n"
 "\t\t$this->registerHook('entry_before_display', array($this, 'renderEntry'));\n"
 "\t}\n"
 "\tpublic function renderEntry($entry) {\n"

+ 10 - 4
docs/i18n/templates/freshrss.pot

@@ -1630,8 +1630,11 @@ msgstr ""
 #: en/./developers/03_Backend/05_Extensions.md:58
 #, no-wrap
 msgid ""
-"class HelloWorldExtension extends Minz_Extension {\n"
-"\tpublic function init() {\n"
+"final class HelloWorldExtension extends Minz_Extension {\n"
+"\t#[\Override]\n"
+"\tpublic function init(): void {\n"
+"\t\tparent::init();\n"
+"\n"
 "\t\t// your code here\n"
 "\t}\n"
 "}\n"
@@ -1911,9 +1914,12 @@ msgstr ""
 #: en/./developers/03_Backend/05_Extensions.md:134
 #, no-wrap
 msgid ""
-"class HelloWorldExtension extends Minz_Extension\n"
+"final class HelloWorldExtension extends Minz_Extension\n"
 "{\n"
-"\tpublic function init() {\n"
+"\t#[\Override]\n"
+"\tpublic function init(): void {\n"
+"\t\tparent::init();\n"
+"\n"
 "\t\t$this->registerHook('entry_before_display', array($this, "
 "'renderEntry'));\n"
 "\t}\n"

+ 48 - 24
lib/Minz/Extension.php

@@ -80,7 +80,9 @@ abstract class Minz_Extension {
 	 * enabled by the extension manager).
 	 * @return void
 	 */
-	abstract public function init();
+	public function init() {
+		$this->migrateExtensionUserPath();
+	}
 
 	/**
 	 * Set the current extension to enable.
@@ -118,7 +120,9 @@ abstract class Minz_Extension {
 	 * Handle the configure action.
 	 * @return void
 	 */
-	public function handleConfigureAction() {}
+	public function handleConfigureAction() {
+		$this->migrateExtensionUserPath();
+	}
 
 	/**
 	 * Getters and setters.
@@ -154,6 +158,32 @@ abstract class Minz_Extension {
 		$this->type = $type;
 	}
 
+	/** Return the user-specific, extension-specific, folder where this extension can save user-specific data */
+	protected final function getExtensionUserPath(): string {
+		$username = Minz_User::name() ?: '_';
+		return USERS_PATH . "/{$username}/extensions/{$this->getEntrypoint()}";
+	}
+
+	private function migrateExtensionUserPath(): void {
+		$username = Minz_User::name() ?: '_';
+		$old_extension_user_path = USERS_PATH . "/{$username}/extensions/{$this->getName()}";
+		$new_extension_user_path = $this->getExtensionUserPath();
+		if (is_dir($old_extension_user_path)) {
+			rename($old_extension_user_path, $new_extension_user_path);
+		}
+	}
+
+	/** Return whether a user-specific, extension-specific, file exists */
+	protected final function hasFile(string $filename): bool {
+		return file_exists($this->getExtensionUserPath() . '/' . $filename);
+	}
+
+	/** Return the user-specific, extension-specific, file content, or null if it does not exist */
+	protected final function getFile(string $filename): ?string {
+		$content = @file_get_contents($this->getExtensionUserPath() . '/' . $filename);
+		return is_string($content) ? $content : null;
+	}
+
 	/**
 	 * Return the url for a given file.
 	 *
@@ -172,8 +202,8 @@ abstract class Minz_Extension {
 			if ($username == null) {
 				return '';
 			}
-			$path = USERS_PATH . "/{$username}/extensions/{$this->getName()}/{$filename}";
-			$file_name_url = urlencode("{$username}/extensions/{$this->getName()}/{$filename}");
+			$path = $this->getExtensionUserPath() . "/{$filename}";
+			$file_name_url = urlencode("{$username}/extensions/{$this->getEntrypoint()}/{$filename}");
 			$mtime = @filemtime($path);
 		}
 
@@ -185,21 +215,21 @@ abstract class Minz_Extension {
 	 *
 	 * @param string $base_name the base name of the controller. Final name will be FreshExtension_<base_name>_Controller.
 	 */
-	public final function registerController(string $base_name): void {
+	protected final function registerController(string $base_name): void {
 		Minz_Dispatcher::registerController($base_name, $this->path);
 	}
 
 	/**
 	 * Register the views in order to be accessible by the application.
 	 */
-	public final function registerViews(): void {
+	protected final function registerViews(): void {
 		Minz_View::addBasePathname($this->path);
 	}
 
 	/**
 	 * Register i18n files from ext_dir/i18n/
 	 */
-	public final function registerTranslates(): void {
+	protected final function registerTranslates(): void {
 		$i18n_dir = $this->path . '/i18n';
 		Minz_Translate::registerPath($i18n_dir);
 	}
@@ -210,7 +240,7 @@ abstract class Minz_Extension {
 	 * @param string $hook_name the hook name (must exist).
 	 * @param callable $hook_function the function name to call (must be callable).
 	 */
-	public final function registerHook(string $hook_name, $hook_function): void {
+	protected final function registerHook(string $hook_name, $hook_function): void {
 		Minz_ExtensionManager::addHook($hook_name, $hook_function);
 	}
 
@@ -249,7 +279,7 @@ abstract class Minz_Extension {
 	/**
 	 * @return array<string,mixed>
 	 */
-	public final function getSystemConfiguration(): array {
+	protected final function getSystemConfiguration(): array {
 		if ($this->isConfigurationEnabled('system') && $this->isExtensionConfigured('system')) {
 			return FreshRSS_Context::systemConf()->extensions[$this->getName()];
 		}
@@ -259,7 +289,7 @@ abstract class Minz_Extension {
 	/**
 	 * @return array<string,mixed>
 	 */
-	public final function getUserConfiguration(): array {
+	protected final function getUserConfiguration(): array {
 		if ($this->isConfigurationEnabled('user') && $this->isExtensionConfigured('user')) {
 			return FreshRSS_Context::userConf()->extensions[$this->getName()];
 		}
@@ -324,13 +354,13 @@ abstract class Minz_Extension {
 	}
 
 	/** @param array<string,mixed> $configuration */
-	public final function setSystemConfiguration(array $configuration): void {
+	protected final function setSystemConfiguration(array $configuration): void {
 		$this->setConfiguration('system', $configuration);
 		$this->system_configuration = $configuration;
 	}
 
 	/** @param array<string,mixed> $configuration */
-	public final function setUserConfiguration(array $configuration): void {
+	protected final function setUserConfiguration(array $configuration): void {
 		$this->setConfiguration('user', $configuration);
 		$this->user_configuration = $configuration;
 	}
@@ -361,19 +391,18 @@ abstract class Minz_Extension {
 		$conf->save();
 	}
 
-	public final function removeSystemConfiguration(): void {
+	protected final function removeSystemConfiguration(): void {
 		$this->removeConfiguration('system');
 		$this->system_configuration = null;
 	}
 
-	public final function removeUserConfiguration(): void {
+	protected final function removeUserConfiguration(): void {
 		$this->removeConfiguration('user');
 		$this->user_configuration = null;
 	}
 
-	public final function saveFile(string $filename, string $content): void {
-		$username = Minz_User::name();
-		$path = USERS_PATH . "/{$username}/extensions/{$this->getName()}";
+	protected final function saveFile(string $filename, string $content): void {
+		$path = $this->getExtensionUserPath();
 
 		if (!file_exists($path)) {
 			mkdir($path, 0777, true);
@@ -382,13 +411,8 @@ abstract class Minz_Extension {
 		file_put_contents("{$path}/{$filename}", $content);
 	}
 
-	public final function removeFile(string $filename): void {
-		$username = Minz_User::name();
-		if ($username == null) {
-			return;
-		}
-		$path = USERS_PATH . "/{$username}/extensions/{$this->getName()}/{$filename}";
-
+	protected final function removeFile(string $filename): void {
+		$path = $path = $this->getExtensionUserPath() . '/' . $filename;
 		if (file_exists($path)) {
 			unlink($path);
 		}

+ 20 - 0
lib/core-extensions/UserCSS/configure.phtml

@@ -0,0 +1,20 @@
+<?php
+	declare(strict_types=1);
+	/** @var UserCSSExtension $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="css-rules"><?= _t('ext.user_css.write_css') ?></label>
+		<div class="group-controls">
+			<textarea name="css-rules" id="css-rules"><?= $this->css_rules ?></textarea>
+		</div>
+	</div>
+
+	<div class="form-group form-actions">
+		<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>
+	</div>
+</form>

+ 34 - 0
lib/core-extensions/UserCSS/extension.php

@@ -0,0 +1,34 @@
+<?php
+declare(strict_types=1);
+
+final class UserCSSExtension extends Minz_Extension {
+	public string $css_rules = '';
+	private const FILENAME = 'style.css';
+
+	#[\Override]
+	public function init(): void {
+		parent::init();
+
+		$this->registerTranslates();
+		if ($this->hasFile(self::FILENAME)) {
+			Minz_View::appendStyle($this->getFileUrl(self::FILENAME, 'css', false));
+		}
+	}
+
+	#[\Override]
+	public function handleConfigureAction(): void {
+		parent::init();
+
+		$this->registerTranslates();
+
+		if (Minz_Request::isPost()) {
+			$css_rules = html_entity_decode(Minz_Request::paramString('css-rules'));
+			$this->saveFile(self::FILENAME, $css_rules);
+		}
+
+		$this->css_rules = '';
+		if ($this->hasFile(self::FILENAME)) {
+			$this->css_rules = htmlentities($this->getFile(self::FILENAME) ?? '');
+		}
+	}
+}

+ 7 - 0
lib/core-extensions/UserCSS/i18n/de/ext.php

@@ -0,0 +1,7 @@
+<?php
+
+return array(
+	'user_css' => array(
+		'write_css' => 'Benutzerspezifische CSS Regeln',
+	),
+);

+ 7 - 0
lib/core-extensions/UserCSS/i18n/en/ext.php

@@ -0,0 +1,7 @@
+<?php
+
+return array(
+	'user_css' => array(
+		'write_css' => 'Additional CSS rules',
+	),
+);

+ 7 - 0
lib/core-extensions/UserCSS/i18n/fr/ext.php

@@ -0,0 +1,7 @@
+<?php
+
+return array(
+	'user_css' => array(
+		'write_css' => 'Règles CSS supplémentaires',
+	),
+);

+ 7 - 0
lib/core-extensions/UserCSS/i18n/ja/ext.php

@@ -0,0 +1,7 @@
+<?php
+
+return array(
+	'user_css' => array(
+		'write_css' => '追加のCSSルール',
+	),
+);

+ 8 - 0
lib/core-extensions/UserCSS/metadata.json

@@ -0,0 +1,8 @@
+{
+	"name": "User CSS",
+	"author": "hkcomori, Marien Fressinaud",
+	"description": "Give possibility to overwrite the CSS with a user-specific rules.",
+	"version": "1.0.0",
+	"entrypoint": "UserCSS",
+	"type": "user"
+}

+ 20 - 0
lib/core-extensions/UserJS/configure.phtml

@@ -0,0 +1,20 @@
+<?php
+	declare(strict_types=1);
+	/** @var UserJSExtension $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="js-rules"><?= _t('ext.user_js.write_js') ?></label>
+		<div class="group-controls">
+			<textarea name="js-rules" id="js-rules"><?= $this->js_rules ?></textarea>
+		</div>
+	</div>
+
+	<div class="form-group form-actions">
+		<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>
+	</div>
+</form>

+ 34 - 0
lib/core-extensions/UserJS/extension.php

@@ -0,0 +1,34 @@
+<?php
+declare(strict_types=1);
+
+final class UserJSExtension extends Minz_Extension {
+	public string $js_rules = '';
+	private const FILENAME = 'script.js';
+
+	#[\Override]
+	public function init(): void {
+		parent::init();
+
+		$this->registerTranslates();
+		if ($this->hasFile(self::FILENAME)) {
+			Minz_View::appendScript($this->getFileUrl(self::FILENAME, 'js', false));
+		}
+	}
+
+	#[\Override]
+	public function handleConfigureAction(): void {
+		parent::init();
+
+		$this->registerTranslates();
+
+		if (Minz_Request::isPost()) {
+			$js_rules = html_entity_decode(Minz_Request::paramString('js-rules'));
+			$this->saveFile(self::FILENAME, $js_rules);
+		}
+
+		$this->js_rules = '';
+		if ($this->hasFile(self::FILENAME)) {
+			$this->js_rules = htmlentities($this->getFile(self::FILENAME) ?? '');
+		}
+	}
+}

+ 7 - 0
lib/core-extensions/UserJS/i18n/de/ext.php

@@ -0,0 +1,7 @@
+<?php
+
+return array(
+	'user_js' => array(
+		'write_js' => 'Benutzerspezifische Javascript Regeln',
+	),
+);

+ 7 - 0
lib/core-extensions/UserJS/i18n/en/ext.php

@@ -0,0 +1,7 @@
+<?php
+
+return array(
+	'user_js' => array(
+		'write_js' => 'Additional JS',
+	),
+);

+ 7 - 0
lib/core-extensions/UserJS/i18n/fr/ext.php

@@ -0,0 +1,7 @@
+<?php
+
+return array(
+	'user_js' => array(
+		'write_js' => 'JS supplémentaires',
+	),
+);

+ 7 - 0
lib/core-extensions/UserJS/i18n/ja/ext.php

@@ -0,0 +1,7 @@
+<?php
+
+return array(
+	'user_js' => array(
+		'write_js' => '追加のJS',
+	),
+);

+ 8 - 0
lib/core-extensions/UserJS/metadata.json

@@ -0,0 +1,8 @@
+{
+	"name": "User JS",
+	"author": "hkcomori, Frans de Jonge",
+	"description": "Apply user JS.",
+	"version": "1.0.0",
+	"entrypoint": "UserJS",
+	"type": "user"
+}