diff --git a/classes/article.php b/classes/article.php
index ff7f111801c34dd3b8d4b09f258b93c8db012636..de39f05d9c697b5f1d4144a051bc955bd34d2afe 100755
--- a/classes/article.php
+++ b/classes/article.php
@@ -31,14 +31,14 @@ class Article extends Handler_Protected {
 			$pluginhost->load_all(PluginHost::KIND_ALL, $owner_uid);
 			//$pluginhost->load_data();
 
-			foreach ($pluginhost->get_hooks(PluginHost::HOOK_GET_FULL_TEXT) as $p) {
-				$extracted_content = $p->hook_get_full_text($url);
-
-				if ($extracted_content) {
-					$content = $extracted_content;
-					break;
-				}
-			}
+			$pluginhost->run_hooks_callback(PluginHost::HOOK_GET_FULL_TEXT,
+				function ($result) use (&$content) {
+					if ($result) {
+						$content = $result;
+						return true;
+					}
+				},
+				$url);
 		}
 
 		$content_hash = sha1($content);
diff --git a/classes/diskcache.php b/classes/diskcache.php
index dcd7791d88a47dfecf3dfa79b91ba022a4845dd2..3fd099d3ccd092b77952168f485f91936b2182a8 100644
--- a/classes/diskcache.php
+++ b/classes/diskcache.php
@@ -399,9 +399,8 @@ class DiskCache {
 			$tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
 			//$tmppluginhost->load_data();
 
-			foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
-				if ($plugin->hook_send_local_file($filename)) return true;
-			}
+			if ($tmppluginhost->run_hooks_until(PluginHost::HOOK_SEND_LOCAL_FILE, true, $filename))
+				return true;
 
 			header("Content-type: $mimetype");
 
diff --git a/classes/pluginhost.php b/classes/pluginhost.php
index 7e5f6029c06ee3c283604aa42a24db0e30f61f81..673053b9e53f1ff5b807d35a098d26fb967e4a0e 100755
--- a/classes/pluginhost.php
+++ b/classes/pluginhost.php
@@ -135,7 +135,7 @@ class PluginHost {
 		$method = strtolower($hook);
 
 		foreach ($this->get_hooks($hook) as $plugin) {
-			Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE);
+			//Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE);
 
 			try {
 				$plugin->$method(...$args);
@@ -147,6 +147,26 @@ class PluginHost {
 		}
 	}
 
+	function run_hooks_until($hook, $check, ...$args) {
+		$method = strtolower($hook);
+
+		foreach ($this->get_hooks($hook) as $plugin) {
+			try {
+				$result = $plugin->$method(...$args);
+
+				if ($result == $check)
+					return true;
+
+			} catch (Exception $ex) {
+				user_error($ex, E_USER_WARNING);
+			} catch (Error $err) {
+				user_error($err, E_USER_WARNING);
+			}
+		}
+
+		return false;
+	}
+
 	function run_hooks_callback($hook, $callback, ...$args) {
 		$method = strtolower($hook);
 
@@ -154,7 +174,25 @@ class PluginHost {
 			//Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE);
 
 			try {
-				$callback($plugin->$method(...$args), $plugin);
+				if ($callback($plugin->$method(...$args), $plugin))
+					break;
+			} catch (Exception $ex) {
+				user_error($ex, E_USER_WARNING);
+			} catch (Error $err) {
+				user_error($err, E_USER_WARNING);
+			}
+		}
+	}
+
+	function chain_hooks_callback($hook, $callback, &...$args) {
+		$method = strtolower($hook);
+
+		foreach ($this->get_hooks($hook) as $plugin) {
+			//Debug::log("invoking: " . get_class($plugin) . "->$hook()", Debug::$LOG_VERBOSE);
+
+			try {
+				if ($callback($plugin->$method(...$args), $plugin))
+					break;
 			} catch (Exception $ex) {
 				user_error($ex, E_USER_WARNING);
 			} catch (Error $err) {
diff --git a/classes/pref/filters.php b/classes/pref/filters.php
index 11702103a6ff5f534aac0e00deda9ca5cd3e3920..43a625989db8a1c53902f56a3a9c69611c41f059 100755
--- a/classes/pref/filters.php
+++ b/classes/pref/filters.php
@@ -140,9 +140,13 @@ class Pref_Filters extends Handler_Protected {
 
 				$line["content_preview"] = truncate_string(strip_tags($line["content"]), 200, '…');
 
-				foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_QUERY_HEADLINES) as $p) {
-					$line = $p->hook_query_headlines($line, 100);
-				}
+				$excerpt_length = 100;
+
+				PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_QUERY_HEADLINES,
+					function ($result) use (&$line) {
+						$line = $result;
+					},
+					$line, $excerpt_length);
 
 				$content_preview = $line["content_preview"];
 
diff --git a/classes/rpc.php b/classes/rpc.php
index 9f86c940145b6a54095f28f0b82a193efd0c623e..fa5aa9a86eb8528fca5bd6c8d6aea39786399ee4 100755
--- a/classes/rpc.php
+++ b/classes/rpc.php
@@ -641,9 +641,11 @@ class RPC extends Handler_Protected {
 				"help_dialog" => __("Show help dialog"))
 		);
 
-		foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
-			$hotkeys = $plugin->hook_hotkey_info($hotkeys);
-		}
+		PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HOTKEY_INFO,
+			function ($result) use (&$hotkeys) {
+				$hotkeys = $result;
+			},
+			$hotkeys);
 
 		return $hotkeys;
 	}
@@ -712,9 +714,11 @@ class RPC extends Handler_Protected {
 			"?" => "help_dialog",
 		);
 
-		foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
-			$hotkeys = $plugin->hook_hotkey_map($hotkeys);
-		}
+		PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_HOTKEY_MAP,
+			function ($result) use (&$hotkeys) {
+				$hotkeys = $result;
+			},
+			$hotkeys);
 
 		$prefixes = array();
 
diff --git a/classes/rssutils.php b/classes/rssutils.php
index 9fc9f6c3f55f6b3f92a3bae68523491ac9d55515..c0025a02204e812c1502b4873b5191ace7a7205f 100755
--- a/classes/rssutils.php
+++ b/classes/rssutils.php
@@ -279,10 +279,11 @@ class RSSUtils {
 			$pluginhost->load($user_plugins, PluginHost::KIND_USER, $owner_uid);
 			//$pluginhost->load_data();
 
-			$basic_info = array();
-			foreach ($pluginhost->get_hooks(PluginHost::HOOK_FEED_BASIC_INFO) as $plugin) {
-				$basic_info = $plugin->hook_feed_basic_info($basic_info, $fetch_url, $owner_uid, $feed, $auth_login, $auth_pass);
-			}
+			$basic_info = [];
+
+			$pluginhost->run_hooks_callback(PluginHost::HOOK_FEED_BASIC_INFO, function ($result) use (&$basic_info) {
+				$basic_info = $result;
+			}, $basic_info, $fetch_url, $owner_uid, $feed, $auth_login, $auth_pass);
 
 			if (!$basic_info) {
 				$feed_data = UrlHelper::fetch($fetch_url, false,
@@ -810,27 +811,16 @@ class RSSUtils {
 
 				$start_ts = microtime(true);
 
-				PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_ARTICLE_FILTER,
-				function ($result, $plugin) use (&$article, &$entry_plugin_data, $start_ts) {
-					$article = $result;
-
-					$entry_plugin_data .= mb_strtolower(get_class($plugin)) . ",";
-
-					Debug::log(sprintf("=== %.4f (sec) %s", microtime(true) - $start_ts, get_class($plugin)),
-						Debug::$LOG_VERBOSE);
-				},
-				$article);
-
-				/* foreach ($pluginhost->get_hooks(PluginHost::HOOK_ARTICLE_FILTER) as $plugin) {
-					Debug::log("... " . get_class($plugin), Debug::$LOG_VERBOSE);
-
-					$start = microtime(true);
-					$article = $plugin->hook_article_filter($article);
+				$pluginhost->chain_hooks_callback(PluginHost::HOOK_ARTICLE_FILTER,
+					function ($result, $plugin) use (&$article, &$entry_plugin_data, $start_ts) {
+						$article = $result;
 
-					Debug::log(sprintf("=== %.4f (sec)", microtime(true) - $start), Debug::$LOG_VERBOSE);
+						$entry_plugin_data .= mb_strtolower(get_class($plugin)) . ",";
 
-					$entry_plugin_data .= mb_strtolower(get_class($plugin)) . ",";
-				} */
+						Debug::log(sprintf("=== %.4f (sec) %s", microtime(true) - $start_ts, get_class($plugin)),
+							Debug::$LOG_VERBOSE);
+					},
+					$article);
 
 				if (Debug::get_loglevel() >= 3) {
 					print "processed content: ";
diff --git a/classes/sanitizer.php b/classes/sanitizer.php
index 5a054c3b056ea5d33404fe0fe8d7a61bdccf5070..2682471d0f9743c724b1e8908843870cdfd007fe 100644
--- a/classes/sanitizer.php
+++ b/classes/sanitizer.php
@@ -41,14 +41,10 @@ class Sanitizer {
 	}
 
 	public static function iframe_whitelisted($entry) {
-		@$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
+		$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
 
-		if ($src) {
-			foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_IFRAME_WHITELISTED) as $plugin) {
-				if ($plugin->hook_iframe_whitelisted($src))
-					return true;
-			}
-		}
+		if (!empty($src))
+			return PluginHost::getInstance()->run_hooks_until(PluginHost::HOOK_IFRAME_WHITELISTED, true, $src);
 
 		return false;
 	}
@@ -153,16 +149,17 @@ class Sanitizer {
 
 		$disallowed_attributes = array('id', 'style', 'class', 'width', 'height', 'allow');
 
-		foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
-			$retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
-			if (is_array($retval)) {
-				$doc = $retval[0];
-				$allowed_elements = $retval[1];
-				$disallowed_attributes = $retval[2];
-			} else {
-				$doc = $retval;
-			}
-		}
+		PluginHost::getInstance()->chain_hooks_callback(PluginHost::HOOK_SANITIZE,
+			function ($result) use (&$doc, &$allowed_elements, &$disallowed_attributes) {
+				if (is_array($result)) {
+					$doc = $result[0];
+					$allowed_elements = $result[1];
+					$disallowed_attributes = $result[2];
+				} else {
+					$doc = $result;
+				}
+			},
+			$doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
 
 		$doc->removeChild($doc->firstChild); //remove doctype
 		$doc = self::strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
diff --git a/index.php b/index.php
index c10b21e5c66b3b42bfa97227006a53e8f772579c..6834891eeef1eefeec173f464ea8bcbbff356a4a 100644
--- a/index.php
+++ b/index.php
@@ -153,9 +153,9 @@
             <img src='images/indicator_tiny.gif'/>
             <?php echo  __("Loading, please wait..."); ?></div>
         <?php
-          foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_FEED_TREE) as $p) {
-            echo $p->hook_feed_tree();
-          }
+			 PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_FEED_TREE, function ($result) {
+				 echo $result;
+			 });
         ?>
         <div id="feedTree"></div>
     </div>
@@ -174,9 +174,10 @@
                title="<?php echo __('Updates are available from Git.') ?>">new_releases</i>
 
             <?php
-            foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_MAIN_TOOLBAR_BUTTON) as $p) {
-                echo $p->hook_main_toolbar_button();
-            }
+
+            PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_MAIN_TOOLBAR_BUTTON, function ($result) {
+                echo $result;
+				});
             ?>
 
             <form id="toolbar-headlines" action="" style="order : 10" onsubmit='return false'>
@@ -206,13 +207,13 @@
                 <option value="date_reverse"><?php echo __('Oldest first') ?></option>
                 <option value="title"><?php echo __('Title') ?></option>
 
-				<?php foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HEADLINES_CUSTOM_SORT_MAP) as $p) {
-					$sort_map = $p->hook_headlines_custom_sort_map();
-
-					foreach ($sort_map as $sort_value => $sort_title) {
-						print "<option value=\"" . htmlspecialchars($sort_value) . "\">$sort_title</option>";
-					}
-				} ?>
+				<?php
+					PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_HEADLINES_CUSTOM_SORT_MAP, function ($result) {
+						foreach ($result as $sort_value => $sort_title) {
+							print "<option value=\"" . htmlspecialchars($sort_value) . "\">$sort_title</option>";
+						}
+					});
+				?>
             </select>
 
             <div dojoType="fox.form.ComboButton" onclick="Feeds.catchupCurrent()">
@@ -235,9 +236,9 @@
             <div class="action-chooser" style="order : 30">
 
                 <?php
-                    foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_TOOLBAR_BUTTON) as $p) {
-                         echo $p->hook_toolbar_button();
-                    }
+						  PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_TOOLBAR_BUTTON, function ($result) {
+							echo $result;
+						});
                 ?>
 
                 <div dojoType="fox.form.DropDownButton" class="action-button" title="<?php echo __('Actions...') ?>">
@@ -257,9 +258,9 @@
                         <div dojoType="dijit.MenuItem" onclick="App.onActionSelected('qmcHKhelp')"><?php echo __('Keyboard shortcuts help') ?></div>
 
                         <?php
-                            foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_ACTION_ITEM) as $p) {
-                             echo $p->hook_action_item();
-                            }
+									PluginHost::getInstance()->run_hooks_callback(PluginHost::HOOK_ACTION_ITEM, function ($result) {
+										echo $result;
+									});
                         ?>
 
                         <?php if (empty($_SESSION["hide_logout"])) { ?>