diff --git a/backend.php b/backend.php
index 9ecc2291454b484016b77d584c900b3545304cd3..e64c6561fb5dfeb50c8369fafd890501bae99fc3 100644
--- a/backend.php
+++ b/backend.php
@@ -88,6 +88,17 @@
 		5 => __("Power User"),
 		10 => __("Administrator"));
 
+	// shortcut syntax for plugin methods (?op=plugin--pmethod&...params)
+	/* if (strpos($op, PluginHost::PUBLIC_METHOD_DELIMITER) !== false) {
+		list ($plugin, $pmethod) = explode(PluginHost::PUBLIC_METHOD_DELIMITER, $op, 2);
+
+		// TODO: better implementation that won't modify $_REQUEST
+		$_REQUEST["plugin"] = $plugin;
+		$method = $pmethod;
+		$op = "pluginhandler";
+	} */
+
+	// TODO: figure out if is this still needed
 	$op = str_replace("-", "_", $op);
 
 	$override = PluginHost::getInstance()->lookup_handler($op, $method);
diff --git a/classes/plugin.php b/classes/plugin.php
index 2416418cd1854a18e2b0851b718637fdc5d20f25..6c572467a33f1eb8ad99da96af9fc391252b6771 100644
--- a/classes/plugin.php
+++ b/classes/plugin.php
@@ -54,4 +54,8 @@ abstract class Plugin {
 
 		return vsprintf($this->__($msgid), $args);
 	}
+
+	function csrf_ignore($method) {
+		return false;
+	}
 }
diff --git a/classes/pluginhandler.php b/classes/pluginhandler.php
index a0e60b4e6160555d5136b4c03e5b29c1cb13f009..608f80dcbaec42002e89723ab88dad0207891da6 100644
--- a/classes/pluginhandler.php
+++ b/classes/pluginhandler.php
@@ -11,7 +11,7 @@ class PluginHandler extends Handler_Protected {
 
 		if ($plugin) {
 			if (method_exists($plugin, $method)) {
-				if (validate_csrf($csrf_token)) {
+				if (validate_csrf($csrf_token) || $plugin->csrf_ignore($method)) {
 					$plugin->$method();
 				} else {
 					user_error("Rejected ${plugin_name}->${method}(): invalid CSRF token.", E_USER_WARNING);
diff --git a/classes/pluginhost.php b/classes/pluginhost.php
index 097bf987c41b6366cc137778d1093374fe693bdf..065fa99c44d9a2104c7e2f1ca45c257c5bca6773 100755
--- a/classes/pluginhost.php
+++ b/classes/pluginhost.php
@@ -611,6 +611,17 @@ class PluginHost {
 					$params));
 	}
 
+	// shortcut syntax (disabled for now)
+	/* function get_method_url(Plugin $sender, string $method, $params)  {
+		return get_self_url_prefix() . "/backend.php?" .
+			http_build_query(
+				array_merge(
+					[
+						"op" => strtolower(get_class($sender) . self::PUBLIC_METHOD_DELIMITER . $method),
+					],
+					$params));
+	} */
+
 	// WARNING: endpoint in public.php, exposed to unauthenticated users
 	function get_public_method_url(Plugin $sender, string $method, $params)  {
 		if ($sender->is_public_method($method)) {
@@ -618,7 +629,7 @@ class PluginHost {
 				http_build_query(
 					array_merge(
 						[
-							"op" => strtolower(get_class($sender) . PluginHost::PUBLIC_METHOD_DELIMITER . $method),
+							"op" => strtolower(get_class($sender) . self::PUBLIC_METHOD_DELIMITER . $method),
 						],
 						$params));
 		} else {
diff --git a/include/controls.php b/include/controls.php
index 4c60d94f365af25253abf48e74a358e7d55995ae..d8506877b9867c5c39d7c15cd1c826f18459242b 100755
--- a/include/controls.php
+++ b/include/controls.php
@@ -11,6 +11,17 @@
       return $rv;
    }
 
+   // shortcut syntax (disabled)
+   /* function pluginhandler_tags(\Plugin $plugin, string $method) {
+      return hidden_tag("op", strtolower(get_class($plugin) . \PluginHost::PUBLIC_METHOD_DELIMITER . $method));
+   } */
+
+   function pluginhandler_tags(\Plugin $plugin, string $method) {
+      return hidden_tag("op", "pluginhandler") .
+               hidden_tag("plugin", strtolower(get_class($plugin))) .
+               hidden_tag("method", $method);
+   }
+
    function button_tag(string $value, string $type, array $attributes = []) {
       return "<button dojoType=\"dijit.form.Button\" ".attributes_to_string($attributes)." type=\"$type\">".htmlspecialchars($value)."</button>";
    }
@@ -155,4 +166,3 @@
 
       return $ret;
    }
-
diff --git a/js/App.js b/js/App.js
index 9d8f6c275bab5b249e5d80e2d87b3569c5893994..aeca688b7ae4c7b90adbe183f2c81892e9464268 100644
--- a/js/App.js
+++ b/js/App.js
@@ -101,6 +101,9 @@ const App = {
 
       return dijit.getEnclosingWidget(elem.closest('.dijitDialog'));
    },
+   getPhArgs(plugin, method, args = {}) {
+      return {...{op: "pluginhandler", plugin: plugin, method: method}, ...args};
+   },
    label_to_feed_id: function(label) {
       return this.LABEL_BASE_INDEX - 1 - Math.abs(label);
    },
diff --git a/plugins/af_proxy_http/init.php b/plugins/af_proxy_http/init.php
index 5804e450fc1c07dc813b1a034524e1c364cf5324..d6cee5fcd5451212b7c655fabf5c234681a40e27 100644
--- a/plugins/af_proxy_http/init.php
+++ b/plugins/af_proxy_http/init.php
@@ -229,9 +229,7 @@ class Af_Proxy_Http extends Plugin {
 			}
 			</script>";
 
-		print \Controls\hidden_tag("op", "pluginhandler");
-		print \Controls\hidden_tag("method", "save");
-		print \Controls\hidden_tag("plugin", "af_proxy_http");
+		print \Controls\pluginhandler_tags($this, "save");
 
 		$proxy_all = sql_bool_to_bool($this->host->get($this, "proxy_all"));
 		print \Controls\checkbox_tag("proxy_all", $proxy_all);
diff --git a/plugins/af_psql_trgm/init.php b/plugins/af_psql_trgm/init.php
index 1d83ce5e04f51de58569299bd72cd7e156481b51..bfbbdf49cb3105ad1cef4723316d9db227537756 100644
--- a/plugins/af_psql_trgm/init.php
+++ b/plugins/af_psql_trgm/init.php
@@ -157,9 +157,7 @@ class Af_Psql_Trgm extends Plugin {
 				}
 				</script>";
 
-			print \Controls\hidden_tag("op", "pluginhandler");
-			print \Controls\hidden_tag("method", "save");
-			print \Controls\hidden_tag("plugin", "af_psql_trgm");
+			print \Controls\pluginhandler_tags($this, "save");
 
 			print "<h2>" . __("Global settings") . "</h2>";
 
diff --git a/plugins/af_readability/init.js b/plugins/af_readability/init.js
index 3155475ccf98f8f505c1d390ae431eaa397e65f7..ff2d94e8bdbc94fbdbb50cb1a30fd57d23c8cb80 100644
--- a/plugins/af_readability/init.js
+++ b/plugins/af_readability/init.js
@@ -16,7 +16,7 @@ Plugins.Af_Readability = {
 
         Notify.progress("Loading, please wait...");
 
-        xhrJson("backend.php",{ op: "pluginhandler", plugin: "af_readability", method: "embed", param: id }, (reply) => {
+        xhrJson("backend.php", App.getPhArgs("af_readability", "embed", {id: id}), (reply) => {
 
             if (content && reply.content) {
                 content.setAttribute(self.orig_attr_name, content.innerHTML);
diff --git a/plugins/af_readability/init.php b/plugins/af_readability/init.php
index aeef8cddc2496b7a9d843c71c4542a34c0353566..43d064fc7f791d7f75465bd8e19baed745e8960b 100755
--- a/plugins/af_readability/init.php
+++ b/plugins/af_readability/init.php
@@ -67,9 +67,7 @@ class Af_Readability extends Plugin {
 
 			<form dojoType='dijit.form.Form'>
 
-				<?= \Controls\hidden_tag("op", "pluginhandler") ?>
-				<?= \Controls\hidden_tag("method", "save") ?>
-				<?= \Controls\hidden_tag("plugin", "af_readability") ?>
+				<?= \Controls\pluginhandler_tags($this, "save") ?>
 
 				<script type='dojo/method' event='onSubmit' args='evt'>
 					evt.preventDefault();
@@ -329,7 +327,7 @@ class Af_Readability extends Plugin {
 	}
 
 	function embed() {
-		$article_id = (int) $_REQUEST["param"];
+		$article_id = (int) $_REQUEST["id"];
 
 		$sth = $this->pdo->prepare("SELECT link FROM ttrss_entries WHERE id = ?");
 		$sth->execute([$article_id]);
diff --git a/plugins/af_redditimgur/init.php b/plugins/af_redditimgur/init.php
index 63a23cd369fb656537d9c7048c281474df4ff58a..5066186db274b54a56d6695bf2949b9346c435d1 100755
--- a/plugins/af_redditimgur/init.php
+++ b/plugins/af_redditimgur/init.php
@@ -41,9 +41,7 @@ class Af_RedditImgur extends Plugin {
 
 			<form dojoType='dijit.form.Form'>
 
-				<?= \Controls\hidden_tag("op", "pluginhandler") ?>
-				<?= \Controls\hidden_tag("method", "save") ?>
-				<?= \Controls\hidden_tag("plugin", "af_redditimgur") ?>
+				<?= \Controls\pluginhandler_tags($this, "save") ?>
 
 				<script type='dojo/method' event='onSubmit' args='evt'>
 					evt.preventDefault();
@@ -633,6 +631,10 @@ class Af_RedditImgur extends Plugin {
 		$entry->parentNode->insertBefore($img, $entry);*/
 	}
 
+	function csrf_ignore($method) {
+		return $method === "testurl";
+	}
+
 	function testurl() {
 
 		$url = clean($_POST["url"]);
@@ -651,7 +653,6 @@ class Af_RedditImgur extends Plugin {
 				<input type="hidden" name="op" value="pluginhandler">
 				<input type="hidden" name="method" value="testurl">
 				<input type="hidden" name="plugin" value="af_redditimgur">
-				<input type="hidden" name="csrf_token" value="<?= $_SESSION["csrf_token"] ?>">
 				<fieldset>
 					<label>URL:</label>
 					<input name="url" size="100" value="<?= htmlspecialchars($url) ?>"></input>
diff --git a/plugins/mail/init.php b/plugins/mail/init.php
index bb576a4d9970ea6bdc27c59f00f86b2b7507b963..4b62d1e6484138f760e5c0526ed0a520f51ca78a 100644
--- a/plugins/mail/init.php
+++ b/plugins/mail/init.php
@@ -45,9 +45,7 @@ class Mail extends Plugin {
 			title="<i class='material-icons'>mail</i> <?= __('Mail plugin') ?>">
 
 			<form dojoType="dijit.form.Form">
-				<?= \Controls\hidden_tag("op", "pluginhandler") ?>
-				<?= \Controls\hidden_tag("method", "save") ?>
-				<?= \Controls\hidden_tag("plugin", "mail") ?>
+				<?= \Controls\pluginhandler_tags($this, "save") ?>
 
 				<script type="dojo/method" event="onSubmit" args="evt">
 					evt.preventDefault();
@@ -150,12 +148,10 @@ class Mail extends Plugin {
 
 		<form dojoType='dijit.form.Form'>
 
-			<?= \Controls\hidden_tag("op", "pluginhandler") ?>
-			<?= \Controls\hidden_tag("plugin", "mail") ?>
-			<?= \Controls\hidden_tag("method", "sendEmail") ?>
+			<?= \Controls\pluginhandler_tags($this, "sendemail") ?>
 
-			<?= \Controls\hidden_tag("from_email", "$user_email") ?>
-			<?= \Controls\hidden_tag("from_name", "$user_name") ?>
+			<?= \Controls\hidden_tag("from_email", $user_email) ?>
+			<?= \Controls\hidden_tag("from_name", $user_name) ?>
 
 			<script type='dojo/method' event='onSubmit' args='evt'>
 				evt.preventDefault();
diff --git a/plugins/mail/mail.js b/plugins/mail/mail.js
index 4cdf6999d89cf123231a9f3c5be399fddfc1da12..36b0baac565d0f4c3605c23e5ddd0d6ce380729d 100644
--- a/plugins/mail/mail.js
+++ b/plugins/mail/mail.js
@@ -38,7 +38,7 @@ Plugins.Mail = {
 		const tmph = dojo.connect(dialog, 'onShow', function () {
 			dojo.disconnect(tmph);
 
-			xhrPost("backend.php", {op: "pluginhandler", plugin: "mail", method: "emailArticle", ids: id}, (transport) => {
+			xhrPost("backend.php", App.getPhArgs("mail", "emailArticle", {ids: id}), (transport) => {
 				dialog.attr('content', transport.responseText);
 			});
 		});
diff --git a/plugins/mailto/init.js b/plugins/mailto/init.js
index 56afa1cca87916ceefdebef1fda51f39de701aaa..b5e68680b7bb7811a23e3b9f847765170411cc24 100644
--- a/plugins/mailto/init.js
+++ b/plugins/mailto/init.js
@@ -21,7 +21,7 @@ Plugins.Mailto = {
 		const tmph = dojo.connect(dialog, 'onShow', function () {
 			dojo.disconnect(tmph);
 
-			xhrPost("backend.php", {op: "pluginhandler", plugin: "mailto", method: "emailArticle", ids: id}, (transport) => {
+			xhrPost("backend.php", App.getPhArgs("mailto", "emailArticle", {ids: id}), (transport) => {
 				dialog.attr('content', transport.responseText);
 			});
 		});
diff --git a/plugins/note/init.php b/plugins/note/init.php
index 65e1f0eef7e55b0f868785df9c240eb7fa16e76c..278cfe6c303c9cb9d52b7a18adb3dff8dcf58e1b 100644
--- a/plugins/note/init.php
+++ b/plugins/note/init.php
@@ -38,9 +38,7 @@ class Note extends Plugin {
 			$note = $row['note'];
 
 			print \Controls\hidden_tag("id", $id);
-			print \Controls\hidden_tag("op", "pluginhandler");
-			print \Controls\hidden_tag("method", "setNote");
-			print \Controls\hidden_tag("plugin", "note");
+			print \Controls\pluginhandler_tags($this, "setnote");
 
 			?>
 			<textarea dojoType='dijit.form.SimpleTextarea'
diff --git a/plugins/note/note.js b/plugins/note/note.js
index 215058b21b0d5c97855cdadd145d22ba425f8756..bc6c4815673cf5e2e1d06e421f56fcf3b112de5e 100644
--- a/plugins/note/note.js
+++ b/plugins/note/note.js
@@ -33,7 +33,7 @@ Plugins.Note = {
 		const tmph = dojo.connect(dialog, 'onShow', function () {
 			dojo.disconnect(tmph);
 
-			xhrPost("backend.php", {op: "pluginhandler", plugin: "note", method: "edit", id: id}, (transport) => {
+			xhrPost("backend.php", App.getPhArgs("note", "edit", {id: id}), (transport) => {
 				dialog.attr('content', transport.responseText);
 			});
 		});
diff --git a/plugins/nsfw/init.php b/plugins/nsfw/init.php
index fecbc62af0b7f04caf171eb6eaa6a8d487175d02..0ee3aebc1803d2d4a6a44c602d36a2c2e6fb70fe 100644
--- a/plugins/nsfw/init.php
+++ b/plugins/nsfw/init.php
@@ -50,9 +50,7 @@ class NSFW extends Plugin {
 			title="<i class='material-icons'>extension</i> <?= __("NSFW Plugin") ?>">
 			<form dojoType="dijit.form.Form">
 
-				<?= \Controls\hidden_tag("op", "pluginhandler") ?>
-				<?= \Controls\hidden_tag("method", "save") ?>
-				<?= \Controls\hidden_tag("plugin", "nsfw") ?>
+				<?= \Controls\pluginhandler_tags($this, "save") ?>
 
 				<script type="dojo/method" event="onSubmit" args="evt">
 					evt.preventDefault();
diff --git a/plugins/share/share.js b/plugins/share/share.js
index 09fb145c950edf62c8faed4ef7dd37695698d2a7..46b62ca5b65e25a805ae466ceeb3197726d1f032 100644
--- a/plugins/share/share.js
+++ b/plugins/share/share.js
@@ -10,9 +10,7 @@ Plugins.Share = {
 
 					Notify.progress("Trying to change URL...", true);
 
-					const query = {op: "pluginhandler", plugin: "share", method: "newkey", id: id};
-
-					xhrJson("backend.php", query, (reply) => {
+					xhrJson("backend.php", App.getPhArgs("share", "newkey", {id: id}), (reply) => {
 						if (reply) {
 							const new_link = reply.link;
 							const target = dialog.domNode.querySelector(".target-url");
@@ -45,7 +43,7 @@ Plugins.Share = {
 			},
 			unshare: function () {
 				if (confirm(__("Remove sharing for this article?"))) {
-					xhrPost("backend.php", {op: "pluginhandler", plugin: "share", method: "unshare", id: id}, (transport) => {
+					xhrPost("backend.php", App.getPhArgs("share", "unshare", {id: id}), (transport) => {
 						Notify.info(transport.responseText);
 
 						const icon = document.querySelector(".share-icon-" + id);
@@ -64,7 +62,7 @@ Plugins.Share = {
 		const tmph = dojo.connect(dialog, 'onShow', function () {
 			dojo.disconnect(tmph);
 
-			xhrPost("backend.php", {op: "pluginhandler", plugin: "share", method: "shareDialog", id: id}, (transport) => {
+			xhrPost("backend.php", App.getPhArgs("share", "shareDialog", {id: id}), (transport) => {
 				dialog.attr('content', transport.responseText)
 
 				const icon = document.querySelector(".share-icon-" + id);
diff --git a/plugins/share/share_prefs.js b/plugins/share/share_prefs.js
index 29c9aeaf83c8875ae490371b29ed487a626839a7..91f979daf25ac612822f80d2db62bc5b6a766b92 100644
--- a/plugins/share/share_prefs.js
+++ b/plugins/share/share_prefs.js
@@ -5,7 +5,7 @@ Plugins.Share = {
 		if (confirm(__("This will invalidate all previously shared article URLs. Continue?"))) {
 			Notify.progress("Clearing URLs...");
 
-			xhrPost("backend.php", {op: "pluginhandler", plugin: "share", method: "clearArticleKeys"}, (transport) => {
+			xhrPost("backend.php", App.getPhArgs("share", "clearArticleKeys"), (transport) => {
 				Notify.info(transport.responseText);
 			});
 		}