codysnider /
tt-rss
| 1 | <?php |
||||||||
| 2 | class Af_Psql_Trgm extends Plugin { |
||||||||
| 3 | |||||||||
| 4 | /* @var PluginHost $host */ |
||||||||
| 5 | private $host; |
||||||||
| 6 | |||||||||
| 7 | public function about() { |
||||||||
| 8 | return array(1.0, |
||||||||
| 9 | "Marks similar articles as read (requires pg_trgm)", |
||||||||
| 10 | "fox"); |
||||||||
| 11 | } |
||||||||
| 12 | |||||||||
| 13 | public function save() { |
||||||||
| 14 | $similarity = (float) $_POST["similarity"]; |
||||||||
| 15 | $min_title_length = (int) $_POST["min_title_length"]; |
||||||||
| 16 | $enable_globally = checkbox_to_sql_bool($_POST["enable_globally"]); |
||||||||
| 17 | |||||||||
| 18 | if ($similarity < 0) { |
||||||||
| 19 | $similarity = 0; |
||||||||
| 20 | } |
||||||||
| 21 | if ($similarity > 1) { |
||||||||
| 22 | $similarity = 1; |
||||||||
| 23 | } |
||||||||
| 24 | |||||||||
| 25 | if ($min_title_length < 0) { |
||||||||
| 26 | $min_title_length = 0; |
||||||||
| 27 | } |
||||||||
| 28 | |||||||||
| 29 | $similarity = sprintf("%.2f", $similarity); |
||||||||
| 30 | |||||||||
| 31 | $this->host->set($this, "similarity", $similarity); |
||||||||
| 32 | $this->host->set($this, "min_title_length", $min_title_length); |
||||||||
| 33 | $this->host->set($this, "enable_globally", $enable_globally); |
||||||||
| 34 | |||||||||
| 35 | echo T_sprintf("Data saved (%s, %d)", $similarity, $enable_globally); |
||||||||
| 36 | } |
||||||||
| 37 | |||||||||
| 38 | public function init($host) { |
||||||||
| 39 | $this->host = $host; |
||||||||
| 40 | |||||||||
| 41 | $host->add_hook($host::HOOK_ARTICLE_FILTER, $this); |
||||||||
| 42 | $host->add_hook($host::HOOK_PREFS_TAB, $this); |
||||||||
| 43 | $host->add_hook($host::HOOK_PREFS_EDIT_FEED, $this); |
||||||||
| 44 | $host->add_hook($host::HOOK_PREFS_SAVE_FEED, $this); |
||||||||
| 45 | $host->add_hook($host::HOOK_ARTICLE_BUTTON, $this); |
||||||||
| 46 | |||||||||
| 47 | } |
||||||||
| 48 | |||||||||
| 49 | public function get_js() { |
||||||||
| 50 | return file_get_contents(__DIR__."/init.js"); |
||||||||
| 51 | } |
||||||||
| 52 | |||||||||
| 53 | public function showrelated() { |
||||||||
| 54 | $id = (int) $_REQUEST['param']; |
||||||||
| 55 | $owner_uid = $_SESSION["uid"]; |
||||||||
| 56 | |||||||||
| 57 | $sth = $this->pdo->prepare("SELECT title FROM ttrss_entries, ttrss_user_entries |
||||||||
| 58 | WHERE ref_id = id AND id = ? AND owner_uid = ?"); |
||||||||
| 59 | $sth->execute([$id, $owner_uid]); |
||||||||
| 60 | |||||||||
| 61 | if ($row = $sth->fetch()) { |
||||||||
| 62 | |||||||||
| 63 | $title = $row['title']; |
||||||||
| 64 | |||||||||
| 65 | print "<p>$title</p>"; |
||||||||
| 66 | |||||||||
| 67 | $sth = $this->pdo->prepare("SELECT ttrss_entries.id AS id, |
||||||||
| 68 | feed_id, |
||||||||
| 69 | ttrss_entries.title AS title, |
||||||||
| 70 | updated, link, |
||||||||
| 71 | ttrss_feeds.title AS feed_title, |
||||||||
| 72 | SIMILARITY(ttrss_entries.title, ?) AS sm |
||||||||
| 73 | FROM |
||||||||
| 74 | ttrss_entries, ttrss_user_entries LEFT JOIN ttrss_feeds ON (ttrss_feeds.id = feed_id) |
||||||||
| 75 | WHERE |
||||||||
| 76 | ttrss_entries.id = ref_id AND |
||||||||
| 77 | ttrss_user_entries.owner_uid = ? AND |
||||||||
| 78 | ttrss_entries.id != ? AND |
||||||||
| 79 | date_entered >= NOW() - INTERVAL '2 weeks' |
||||||||
| 80 | ORDER BY |
||||||||
| 81 | sm DESC, date_entered DESC |
||||||||
| 82 | LIMIT 10"); |
||||||||
| 83 | |||||||||
| 84 | $sth->execute([$title, $owner_uid, $id]); |
||||||||
| 85 | |||||||||
| 86 | print "<ul class='panel panel-scrollable'>"; |
||||||||
| 87 | |||||||||
| 88 | while ($line = $sth->fetch()) { |
||||||||
| 89 | print "<li style='display : flex'>"; |
||||||||
| 90 | print "<i class='material-icons'>bookmark_outline</i>"; |
||||||||
| 91 | |||||||||
| 92 | $sm = sprintf("%.2f", $line['sm']); |
||||||||
| 93 | $article_link = htmlspecialchars($line["link"]); |
||||||||
| 94 | |||||||||
| 95 | print "<div style='flex-grow : 2'>"; |
||||||||
| 96 | |||||||||
| 97 | print " <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"$article_link\">". |
||||||||
| 98 | $line["title"]."</a>"; |
||||||||
| 99 | |||||||||
| 100 | print " (<a href=\"#\" onclick=\"Feeds.open({feed:".$line["feed_id"]."})\">". |
||||||||
| 101 | htmlspecialchars($line["feed_title"])."</a>)"; |
||||||||
| 102 | |||||||||
| 103 | print " — $sm"; |
||||||||
| 104 | |||||||||
| 105 | print "</div>"; |
||||||||
| 106 | |||||||||
| 107 | print "<div style='text-align : right' class='text-muted'>".smart_date_time(strtotime($line["updated"]))."</div>"; |
||||||||
| 108 | |||||||||
| 109 | print "</li>"; |
||||||||
| 110 | } |
||||||||
| 111 | |||||||||
| 112 | print "</ul>"; |
||||||||
| 113 | |||||||||
| 114 | } |
||||||||
| 115 | |||||||||
| 116 | print "<footer class='text-center'>"; |
||||||||
| 117 | print "<button dojoType='dijit.form.Button' onclick=\"dijit.byId('trgmRelatedDlg').hide()\">".__('Close this window')."</button>"; |
||||||||
| 118 | print "</footer>"; |
||||||||
| 119 | |||||||||
| 120 | |||||||||
| 121 | } |
||||||||
| 122 | |||||||||
| 123 | public function hook_article_button($line) { |
||||||||
| 124 | return "<i style=\"cursor : pointer\" class='material-icons' |
||||||||
| 125 | onclick=\"Plugins.Psql_Trgm.showRelated(".$line["id"].")\" |
||||||||
| 126 | title='".__('Show related articles')."'>bookmark_outline</i>"; |
||||||||
| 127 | } |
||||||||
| 128 | |||||||||
| 129 | public function hook_prefs_tab($args) { |
||||||||
| 130 | if ($args != "prefFeeds") { |
||||||||
| 131 | return; |
||||||||
| 132 | } |
||||||||
| 133 | |||||||||
| 134 | print "<div dojoType=\"dijit.layout.AccordionPane\" |
||||||||
| 135 | title=\"<i class='material-icons'>extension</i> ".__('Mark similar articles as read')."\">"; |
||||||||
| 136 | |||||||||
| 137 | if (DB_TYPE != "pgsql") { |
||||||||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||||||
| 138 | print_error("Database type not supported."); |
||||||||
|
0 ignored issues
–
show
The function
print_error() has been deprecated: Use twig function errorMessage
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. Loading history...
The call to
print_error() has too many arguments starting with 'Database type not supported.'.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. Loading history...
|
|||||||||
| 139 | } else { |
||||||||
| 140 | |||||||||
| 141 | $res = $this->pdo->query("select 'similarity'::regproc"); |
||||||||
| 142 | |||||||||
| 143 | if (!$res->fetch()) { |
||||||||
| 144 | print_error("pg_trgm extension not found."); |
||||||||
|
0 ignored issues
–
show
The function
print_error() has been deprecated: Use twig function errorMessage
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. Loading history...
|
|||||||||
| 145 | } |
||||||||
| 146 | |||||||||
| 147 | $similarity = $this->host->get($this, "similarity"); |
||||||||
| 148 | $min_title_length = $this->host->get($this, "min_title_length"); |
||||||||
| 149 | $enable_globally = $this->host->get($this, "enable_globally"); |
||||||||
| 150 | |||||||||
| 151 | if (!$similarity) { |
||||||||
| 152 | $similarity = '0.75'; |
||||||||
| 153 | } |
||||||||
| 154 | if (!$min_title_length) { |
||||||||
| 155 | $min_title_length = '32'; |
||||||||
| 156 | } |
||||||||
| 157 | |||||||||
| 158 | print "<form dojoType=\"dijit.form.Form\">"; |
||||||||
| 159 | |||||||||
| 160 | print "<script type=\"dojo/method\" event=\"onSubmit\" args=\"evt\"> |
||||||||
| 161 | evt.preventDefault(); |
||||||||
| 162 | if (this.validate()) { |
||||||||
| 163 | console.log(dojo.objectToQuery(this.getValues())); |
||||||||
| 164 | new Ajax.Request('backend.php', { |
||||||||
| 165 | parameters: dojo.objectToQuery(this.getValues()), |
||||||||
| 166 | onComplete: function(transport) { |
||||||||
| 167 | Notify.info(transport.responseText); |
||||||||
| 168 | } |
||||||||
| 169 | }); |
||||||||
| 170 | //this.reset(); |
||||||||
| 171 | } |
||||||||
| 172 | </script>"; |
||||||||
| 173 | |||||||||
| 174 | print_hidden("op", "pluginhandler"); |
||||||||
| 175 | print_hidden("method", "save"); |
||||||||
| 176 | print_hidden("plugin", "af_psql_trgm"); |
||||||||
| 177 | |||||||||
| 178 | print "<h2>".__("Global settings")."</h2>"; |
||||||||
| 179 | |||||||||
| 180 | print_notice("Enable for specific feeds in the feed editor."); |
||||||||
|
0 ignored issues
–
show
The call to
print_notice() has too many arguments starting with 'Enable for specific feeds in the feed editor.'.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. Loading history...
The function
print_notice() has been deprecated: Use twig function noticeMessage
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This function has been deprecated. The supplier of the function has supplied an explanatory message. The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead. Loading history...
|
|||||||||
| 181 | |||||||||
| 182 | print "<fieldset>"; |
||||||||
| 183 | |||||||||
| 184 | print "<label>".__("Minimum similarity:")."</label> "; |
||||||||
| 185 | print "<input dojoType=\"dijit.form.NumberSpinner\" |
||||||||
| 186 | placeholder=\"0.75\" id='psql_trgm_similarity' |
||||||||
| 187 | required=\"1\" name=\"similarity\" value=\"$similarity\">"; |
||||||||
| 188 | |||||||||
| 189 | print "<div dojoType='dijit.Tooltip' connectId='psql_trgm_similarity' position='below'>". |
||||||||
| 190 | __("PostgreSQL trigram extension returns string similarity as a floating point number (0-1). Setting it too low might produce false positives, zero disables checking."). |
||||||||
| 191 | "</div>"; |
||||||||
| 192 | |||||||||
| 193 | print "</fieldset><fieldset>"; |
||||||||
| 194 | |||||||||
| 195 | print "<label>".__("Minimum title length:")."</label> "; |
||||||||
| 196 | print "<input dojoType=\"dijit.form.NumberSpinner\" |
||||||||
| 197 | placeholder=\"32\" |
||||||||
| 198 | required=\"1\" name=\"min_title_length\" value=\"$min_title_length\">"; |
||||||||
| 199 | |||||||||
| 200 | print "</fieldset><fieldset>"; |
||||||||
| 201 | |||||||||
| 202 | print "<label class='checkbox'>"; |
||||||||
| 203 | print_checkbox("enable_globally", $enable_globally); |
||||||||
| 204 | print " ".__("Enable for all feeds:"); |
||||||||
| 205 | print "</label>"; |
||||||||
| 206 | |||||||||
| 207 | print "</fieldset>"; |
||||||||
| 208 | |||||||||
| 209 | print_button("submit", __("Save"), "class='alt-primary'"); |
||||||||
| 210 | print "</form>"; |
||||||||
| 211 | |||||||||
| 212 | $enabled_feeds = $this->host->get($this, "enabled_feeds"); |
||||||||
| 213 | if (!array($enabled_feeds)) { |
||||||||
| 214 | $enabled_feeds = array(); |
||||||||
| 215 | } |
||||||||
| 216 | |||||||||
| 217 | $enabled_feeds = $this->filter_unknown_feeds($enabled_feeds); |
||||||||
| 218 | $this->host->set($this, "enabled_feeds", $enabled_feeds); |
||||||||
| 219 | |||||||||
| 220 | if (count($enabled_feeds) > 0) { |
||||||||
| 221 | print "<h3>".__("Currently enabled for (click to edit):")."</h3>"; |
||||||||
| 222 | |||||||||
| 223 | print "<ul class=\"panel panel-scrollable list list-unstyled\">"; |
||||||||
| 224 | foreach ($enabled_feeds as $f) { |
||||||||
| 225 | print "<li>". |
||||||||
| 226 | "<i class='material-icons'>rss_feed</i> <a href='#' |
||||||||
| 227 | onclick='CommonDialogs.editFeed($f)'>". |
||||||||
| 228 | Feeds::getFeedTitle($f)."</a></li>"; |
||||||||
| 229 | } |
||||||||
| 230 | print "</ul>"; |
||||||||
| 231 | } |
||||||||
| 232 | } |
||||||||
| 233 | |||||||||
| 234 | print "</div>"; |
||||||||
| 235 | } |
||||||||
| 236 | |||||||||
| 237 | public function hook_prefs_edit_feed($feed_id) { |
||||||||
| 238 | print "<header>".__("Similarity (pg_trgm)")."</header>"; |
||||||||
| 239 | print "<section>"; |
||||||||
| 240 | |||||||||
| 241 | $enabled_feeds = $this->host->get($this, "enabled_feeds"); |
||||||||
| 242 | if (!array($enabled_feeds)) { |
||||||||
| 243 | $enabled_feeds = array(); |
||||||||
| 244 | } |
||||||||
| 245 | |||||||||
| 246 | $key = array_search($feed_id, $enabled_feeds); |
||||||||
| 247 | $checked = $key !== false ? "checked" : ""; |
||||||||
| 248 | |||||||||
| 249 | print "<fieldset>"; |
||||||||
| 250 | |||||||||
| 251 | print "<label class='checkbox'><input dojoType='dijit.form.CheckBox' type='checkbox' id='trgm_similarity_enabled' |
||||||||
| 252 | name='trgm_similarity_enabled' $checked> ".__('Mark similar articles as read')."</label>"; |
||||||||
| 253 | |||||||||
| 254 | print "</fieldset>"; |
||||||||
| 255 | |||||||||
| 256 | print "</section>"; |
||||||||
| 257 | } |
||||||||
| 258 | |||||||||
| 259 | public function hook_prefs_save_feed($feed_id) { |
||||||||
| 260 | $enabled_feeds = $this->host->get($this, "enabled_feeds"); |
||||||||
| 261 | if (!is_array($enabled_feeds)) { |
||||||||
| 262 | $enabled_feeds = array(); |
||||||||
| 263 | } |
||||||||
| 264 | |||||||||
| 265 | $enable = checkbox_to_sql_bool($_POST["trgm_similarity_enabled"]); |
||||||||
| 266 | $key = array_search($feed_id, $enabled_feeds); |
||||||||
| 267 | |||||||||
| 268 | if ($enable) { |
||||||||
| 269 | if ($key === false) { |
||||||||
| 270 | array_push($enabled_feeds, $feed_id); |
||||||||
| 271 | } |
||||||||
| 272 | } else { |
||||||||
| 273 | if ($key !== false) { |
||||||||
| 274 | unset($enabled_feeds[$key]); |
||||||||
| 275 | } |
||||||||
| 276 | } |
||||||||
| 277 | |||||||||
| 278 | $this->host->set($this, "enabled_feeds", $enabled_feeds); |
||||||||
| 279 | } |
||||||||
| 280 | |||||||||
| 281 | public function hook_article_filter($article) { |
||||||||
| 282 | |||||||||
| 283 | if (DB_TYPE != "pgsql") { |
||||||||
|
0 ignored issues
–
show
|
|||||||||
| 284 | return $article; |
||||||||
| 285 | } |
||||||||
| 286 | |||||||||
| 287 | $res = $this->pdo->query("select 'similarity'::regproc"); |
||||||||
| 288 | if (!$res->fetch()) { |
||||||||
| 289 | return $article; |
||||||||
| 290 | } |
||||||||
| 291 | |||||||||
| 292 | $enable_globally = $this->host->get($this, "enable_globally"); |
||||||||
| 293 | |||||||||
| 294 | if (!$enable_globally) { |
||||||||
| 295 | $enabled_feeds = $this->host->get($this, "enabled_feeds"); |
||||||||
| 296 | $key = array_search($article["feed"]["id"], $enabled_feeds); |
||||||||
|
0 ignored issues
–
show
It seems like
$enabled_feeds can also be of type false; however, parameter $haystack of array_search() does only seem to accept array, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||||
| 297 | if ($key === false) { |
||||||||
| 298 | return $article; |
||||||||
| 299 | } |
||||||||
| 300 | } |
||||||||
| 301 | |||||||||
| 302 | $similarity = (float) $this->host->get($this, "similarity"); |
||||||||
| 303 | if ($similarity < 0.01) { |
||||||||
| 304 | return $article; |
||||||||
| 305 | } |
||||||||
| 306 | |||||||||
| 307 | $min_title_length = (int) $this->host->get($this, "min_title_length"); |
||||||||
| 308 | if (mb_strlen($article["title"]) < $min_title_length) { |
||||||||
| 309 | return $article; |
||||||||
| 310 | } |
||||||||
| 311 | |||||||||
| 312 | $owner_uid = $article["owner_uid"]; |
||||||||
| 313 | $entry_guid = $article["guid_hashed"]; |
||||||||
| 314 | $title_escaped = $article["title"]; |
||||||||
| 315 | |||||||||
| 316 | // trgm does not return similarity=1 for completely equal strings |
||||||||
| 317 | |||||||||
| 318 | $sth = $this->pdo->prepare("SELECT COUNT(id) AS nequal |
||||||||
| 319 | FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id AND |
||||||||
| 320 | date_entered >= NOW() - interval '3 days' AND |
||||||||
| 321 | title = ? AND |
||||||||
| 322 | guid != ? AND |
||||||||
| 323 | owner_uid = ?"); |
||||||||
| 324 | $sth->execute([$title_escaped, $entry_guid, $owner_uid]); |
||||||||
| 325 | |||||||||
| 326 | $row = $sth->fetch(); |
||||||||
| 327 | $nequal = $row['nequal']; |
||||||||
| 328 | |||||||||
| 329 | Debug::log("af_psql_trgm: num equals: $nequal", Debug::$LOG_EXTENDED); |
||||||||
| 330 | |||||||||
| 331 | if ($nequal != 0) { |
||||||||
| 332 | $article["force_catchup"] = true; |
||||||||
| 333 | return $article; |
||||||||
| 334 | } |
||||||||
| 335 | |||||||||
| 336 | $sth = $this->pdo->prepare("SELECT MAX(SIMILARITY(title, ?)) AS ms |
||||||||
| 337 | FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id AND |
||||||||
| 338 | date_entered >= NOW() - interval '1 day' AND |
||||||||
| 339 | guid != ? AND |
||||||||
| 340 | owner_uid = ?"); |
||||||||
| 341 | $sth->execute([$title_escaped, $entry_guid, $owner_uid]); |
||||||||
| 342 | |||||||||
| 343 | $row = $sth->fetch(); |
||||||||
| 344 | $similarity_result = $row['ms']; |
||||||||
| 345 | |||||||||
| 346 | Debug::log("af_psql_trgm: similarity result: $similarity_result", Debug::$LOG_EXTENDED); |
||||||||
| 347 | |||||||||
| 348 | if ($similarity_result >= $similarity) { |
||||||||
| 349 | $article["force_catchup"] = true; |
||||||||
| 350 | } |
||||||||
| 351 | |||||||||
| 352 | return $article; |
||||||||
| 353 | |||||||||
| 354 | } |
||||||||
| 355 | |||||||||
| 356 | public function api_version() { |
||||||||
| 357 | return 2; |
||||||||
| 358 | } |
||||||||
| 359 | |||||||||
| 360 | private function filter_unknown_feeds($enabled_feeds) { |
||||||||
| 361 | $tmp = array(); |
||||||||
| 362 | |||||||||
| 363 | foreach ($enabled_feeds as $feed) { |
||||||||
| 364 | |||||||||
| 365 | $sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ? AND owner_uid = ?"); |
||||||||
| 366 | $sth->execute([$feed, $_SESSION['uid']]); |
||||||||
| 367 | |||||||||
| 368 | if ($row = $sth->fetch()) { |
||||||||
|
0 ignored issues
–
show
|
|||||||||
| 369 | array_push($tmp, $feed); |
||||||||
| 370 | } |
||||||||
| 371 | } |
||||||||
| 372 | |||||||||
| 373 | return $tmp; |
||||||||
| 374 | } |
||||||||
| 375 | |||||||||
| 376 | } |
||||||||
| 377 |