2015-01-19 09:52:15 +00:00
< ? php
class Af_Psql_Trgm extends Plugin {
2021-11-13 16:55:30 +00:00
/** @var PluginHost $host */
2015-01-19 09:52:15 +00:00
private $host ;
2021-11-13 16:55:30 +00:00
/** @var float */
2021-01-11 09:23:46 +00:00
private $default_similarity = 0.75 ;
2021-11-13 16:55:30 +00:00
/** @var int */
2021-01-11 09:23:46 +00:00
private $default_min_length = 32 ;
2015-01-19 09:52:15 +00:00
function about () {
2021-03-01 09:11:42 +00:00
return array ( null ,
2015-01-19 09:52:15 +00:00
" Marks similar articles as read (requires pg_trgm) " ,
" fox " );
}
2021-11-13 16:55:30 +00:00
/** @return void */
2015-01-19 09:52:15 +00:00
function save () {
2017-12-03 07:26:38 +00:00
$similarity = ( float ) $_POST [ " similarity " ];
$min_title_length = ( int ) $_POST [ " min_title_length " ];
2021-02-17 11:14:17 +00:00
$enable_globally = checkbox_to_sql_bool ( $_POST [ " enable_globally " ] ? ? " " );
2015-01-19 09:52:15 +00:00
if ( $similarity < 0 ) $similarity = 0 ;
if ( $min_title_length < 0 ) $min_title_length = 0 ;
$similarity = sprintf ( " %.2f " , $similarity );
$this -> host -> set ( $this , " similarity " , $similarity );
$this -> host -> set ( $this , " min_title_length " , $min_title_length );
2015-06-23 04:36:25 +00:00
$this -> host -> set ( $this , " enable_globally " , $enable_globally );
2015-01-19 09:52:15 +00:00
2015-06-23 04:36:25 +00:00
echo T_sprintf ( " Data saved (%s, %d) " , $similarity , $enable_globally );
2015-01-19 09:52:15 +00:00
}
function init ( $host ) {
$this -> host = $host ;
$host -> add_hook ( $host :: HOOK_ARTICLE_FILTER , $this );
$host -> add_hook ( $host :: HOOK_PREFS_TAB , $this );
$host -> add_hook ( $host :: HOOK_PREFS_EDIT_FEED , $this );
$host -> add_hook ( $host :: HOOK_PREFS_SAVE_FEED , $this );
2015-01-19 12:46:15 +00:00
$host -> add_hook ( $host :: HOOK_ARTICLE_BUTTON , $this );
2015-01-19 09:52:15 +00:00
}
2015-01-19 12:46:15 +00:00
function get_js () {
2015-01-19 11:59:33 +00:00
return file_get_contents ( __DIR__ . " /init.js " );
}
2021-11-13 16:55:30 +00:00
/** @return void */
2015-01-19 11:59:33 +00:00
function showrelated () {
2021-02-17 11:08:06 +00:00
$id = ( int ) $_REQUEST [ 'id' ];
2015-01-19 11:59:33 +00:00
$owner_uid = $_SESSION [ " uid " ];
2017-12-03 07:26:38 +00:00
$sth = $this -> pdo -> prepare ( " SELECT title FROM ttrss_entries, ttrss_user_entries
WHERE ref_id = id AND id = ? AND owner_uid = ? " );
$sth -> execute ([ $id , $owner_uid ]);
2015-01-19 11:59:33 +00:00
2017-12-03 07:26:38 +00:00
if ( $row = $sth -> fetch ()) {
2015-01-19 11:59:33 +00:00
2017-12-03 07:26:38 +00:00
$title = $row [ 'title' ];
2015-01-19 11:59:33 +00:00
2018-12-06 09:13:59 +00:00
print " <p> $title </p> " ;
2017-12-03 07:26:38 +00:00
2022-06-19 18:36:33 +00:00
if ( Config :: get ( Config :: DB_TYPE ) == " pgsql " ) {
$sth = $this -> pdo -> prepare ( " SELECT ttrss_entries.id AS id,
feed_id ,
ttrss_entries . title AS title ,
updated , link ,
ttrss_feeds . title AS feed_title ,
SIMILARITY ( ttrss_entries . title , : title ) AS sm
FROM
ttrss_entries , ttrss_user_entries LEFT JOIN ttrss_feeds ON ( ttrss_feeds . id = feed_id )
WHERE
ttrss_entries . id = ref_id AND
ttrss_user_entries . owner_uid = : owner_uid AND
ttrss_entries . id != : id AND
date_entered >= NOW () - INTERVAL '2 weeks'
ORDER BY
sm DESC , date_entered DESC
LIMIT 10 " );
} else {
$sth = $this -> pdo -> prepare ( " SELECT ttrss_entries.id AS id,
feed_id ,
ttrss_entries . title AS title ,
updated , link ,
ttrss_feeds . title AS feed_title ,
2022-06-19 20:04:10 +00:00
( MATCH ( ttrss_entries . title ) AGAINST ( : title ) / LENGTH ( ttrss_entries . title )) AS sm
2022-06-19 18:36:33 +00:00
FROM
ttrss_entries , ttrss_user_entries LEFT JOIN ttrss_feeds ON ( ttrss_feeds . id = feed_id )
WHERE
ttrss_entries . id = ref_id AND
ttrss_user_entries . owner_uid = : owner_uid AND
ttrss_entries . id != : id AND
date_entered >= DATE_SUB ( NOW (), INTERVAL 2 WEEK )
ORDER BY
sm DESC , date_entered DESC
LIMIT 10 " );
}
$sth -> execute ([ 'title' => $title , " owner_uid " => $owner_uid , " id " => $id ]);
2017-12-03 07:26:38 +00:00
2018-12-07 11:03:33 +00:00
print " <ul class='panel panel-scrollable'> " ;
2015-01-19 11:59:33 +00:00
2017-12-03 07:26:38 +00:00
while ( $line = $sth -> fetch ()) {
2018-12-06 09:13:59 +00:00
print " <li style='display : flex'> " ;
print " <i class='material-icons'>bookmark_outline</i> " ;
2015-01-19 12:46:15 +00:00
2017-12-03 07:26:38 +00:00
$sm = sprintf ( " %.2f " , $line [ 'sm' ]);
$article_link = htmlspecialchars ( $line [ " link " ]);
2018-12-06 09:13:59 +00:00
print " <div style='flex-grow : 2'> " ;
2017-12-03 07:26:38 +00:00
print " <a target= \" _blank \" rel= \" noopener noreferrer \" href= \" $article_link\ " > " .
$line [ " title " ] . " </a> " ;
2015-01-19 12:46:15 +00:00
2018-12-02 05:57:22 +00:00
print " (<a href= \" # \" onclick= \" Feeds.open( { feed: " . $line [ " feed_id " ] . " }) \" > " .
2017-12-03 07:26:38 +00:00
htmlspecialchars ( $line [ " feed_title " ]) . " </a>) " ;
2015-01-19 12:46:15 +00:00
2018-12-06 09:13:59 +00:00
print " — $sm " ;
print " </div> " ;
2020-09-23 10:04:26 +00:00
print " <div style='text-align : right' class='text-muted'> " . TimeHelper :: smart_date_time ( strtotime ( $line [ " updated " ])) . " </div> " ;
2015-06-23 01:35:24 +00:00
2017-12-03 07:26:38 +00:00
print " </li> " ;
}
2015-01-19 11:59:33 +00:00
2017-12-03 07:26:38 +00:00
print " </ul> " ;
}
2015-01-19 11:59:33 +00:00
2021-02-12 06:02:44 +00:00
print " <footer class='text-center'>
< button dojoType = 'dijit.form.Button' type = 'submit' class = 'alt-primary' > " .__('Close this window'). " </ button >
</ footer > " ;
2015-01-19 11:59:33 +00:00
2015-01-19 12:46:15 +00:00
}
function hook_article_button ( $line ) {
2018-12-05 19:48:14 +00:00
return " <i style= \" cursor : pointer \" class='material-icons'
2018-12-03 07:51:14 +00:00
onclick = \ " Plugins.Psql_Trgm.showRelated( " . $line [ " id " ] . " ) \"
2022-01-14 15:03:50 +00:00
title = \ " " . __ ( 'Show related articles' ) . " \" >bookmark_outline</i> " ;
2015-01-19 12:46:15 +00:00
}
2015-01-19 11:59:33 +00:00
2015-01-19 09:52:15 +00:00
function hook_prefs_tab ( $args ) {
if ( $args != " prefFeeds " ) return ;
2021-02-18 10:41:40 +00:00
$similarity = $this -> host -> get ( $this , " similarity " , $this -> default_similarity );
$min_title_length = $this -> host -> get ( $this , " min_title_length " , $this -> default_min_length );
$enable_globally = sql_bool_to_bool ( $this -> host -> get ( $this , " enable_globally " ));
2015-01-19 09:52:15 +00:00
2021-02-18 10:41:40 +00:00
?>
2015-01-19 09:52:15 +00:00
2021-02-18 10:41:40 +00:00
< div dojoType = " dijit.layout.AccordionPane "
title = " <i class='material-icons'>extension</i> <?= __('Mark similar articles as read (af_psql_trgm)') ?> " >
2015-01-19 09:52:15 +00:00
2021-02-18 10:41:40 +00:00
< ? php
2022-06-19 18:36:33 +00:00
if ( Config :: get ( Config :: DB_TYPE ) == " pgsql " ) {
2021-02-18 10:41:40 +00:00
$res = $this -> pdo -> query ( " select 'similarity'::regproc " );
2016-08-25 06:47:02 +00:00
2021-02-18 10:41:40 +00:00
if ( ! $res || ! $res -> fetch ()) {
print_error ( " pg_trgm extension not found. " );
2016-08-25 06:47:02 +00:00
}
2021-02-18 10:41:40 +00:00
} ?>
2016-08-25 06:47:02 +00:00
2021-02-18 10:41:40 +00:00
< form dojoType = " dijit.form.Form " >
2016-08-25 06:47:02 +00:00
2021-02-18 10:41:40 +00:00
< ? = \Controls\pluginhandler_tags ( $this , " save " ) ?>
2016-08-25 06:47:02 +00:00
2021-02-18 10:41:40 +00:00
< script type = " dojo/method " event = " onSubmit " args = " evt " >
evt . preventDefault ();
if ( this . validate ()) {
Notify . progress ( 'Saving data...' , true );
2021-02-19 10:44:56 +00:00
xhr . post ( " backend.php " , this . getValues (), ( reply ) => {
Notify . info ( reply );
2021-02-18 10:41:40 +00:00
})
}
</ script >
< ? = format_notice ( " Enable for specific feeds in the feed editor. " ) ?>
< fieldset >
< label >< ? = __ ( " Minimum similarity: " ) ?> </label>
< input dojoType = " dijit.form.NumberSpinner "
placeholder = " <?= $this->default_similarity ?> "
id = 'psql_trgm_similarity'
required = " 1 "
name = " similarity " value = " <?= htmlspecialchars( $similarity ) ?> " >
< div dojoType = 'dijit.Tooltip' connectId = 'psql_trgm_similarity' position = 'below' >
2022-06-19 18:36:33 +00:00
< ? php if ( Config :: get ( Config :: DB_TYPE ) == " pgsql " ) { ?>
< ? = __ ( " PostgreSQL trigram extension returns string similarity as a floating point number (0-1). Setting it too low might produce false positives, zero disables checking. " ) ?>
< ? php } else { ?>
< ? = __ ( " Setting this value too low might produce false positives, zero disables checking. " ) ?>
< ? php } ?>
2021-02-18 10:41:40 +00:00
</ div >
</ fieldset >
< fieldset >
< label >< ? = __ ( " Minimum title length: " ) ?> </label>
< input dojoType = " dijit.form.NumberSpinner "
placeholder = " <?= $this->default_min_length ?> "
required = " 1 "
name = " min_title_length " value = " <?= htmlspecialchars( $min_title_length ) ?> " >
</ fieldset >
< fieldset >
< label class = 'checkbox' >
< ? = \Controls\checkbox_tag ( " enable_globally " , $enable_globally ) ?>
< ? = __ ( " Enable for all feeds. " ) ?>
</ label >
</ fieldset >
< hr />
< ? = \Controls\submit_tag ( __ ( " Save " )) ?>
</ form >
< ? php
/* cleanup */
$enabled_feeds = $this -> filter_unknown_feeds (
2021-11-14 17:36:55 +00:00
$this -> host -> get_array ( $this , " enabled_feeds " ));
2021-02-18 10:41:40 +00:00
$this -> host -> set ( $this , " enabled_feeds " , $enabled_feeds );
?>
< ? php if ( count ( $enabled_feeds ) > 0 ) { ?>
< hr />
< h3 >< ? = __ ( " Currently enabled for (click to edit): " ) ?> </h3>
< ul class = " panel panel-scrollable list list-unstyled " >
< ? php foreach ( $enabled_feeds as $f ) { ?>
< li >
< i class = 'material-icons' > rss_feed </ i >
< a href = '#' onclick = " CommonDialogs.editFeed(<?= $f ?>) " >
< ? = Feeds :: _get_title ( $f ) ?>
</ a >
</ li >
< ? php } ?>
</ ul >
< ? php } ?>
</ div >
< ? php
2015-01-19 09:52:15 +00:00
}
function hook_prefs_edit_feed ( $feed_id ) {
2021-11-14 17:36:55 +00:00
$enabled_feeds = $this -> host -> get_array ( $this , " enabled_feeds " );
2021-02-18 10:41:40 +00:00
?>
< header >< ? = __ ( " Similarity (af_psql_trgm) " ) ?> </header>
< section >
< fieldset >
< label class = " checkbox " >
< ? = \Controls\checkbox_tag ( " trgm_similarity_enabled " , in_array ( $feed_id , $enabled_feeds )) ?>
< ? = __ ( 'Mark similar articles as read' ) ?>
</ label >
</ fieldset >
</ section >
</ section >
< ? php
2015-01-19 09:52:15 +00:00
}
function hook_prefs_save_feed ( $feed_id ) {
2021-11-14 17:36:55 +00:00
$enabled_feeds = $this -> host -> get_array ( $this , " enabled_feeds " );
2015-01-19 09:52:15 +00:00
2021-02-08 08:47:41 +00:00
$enable = checkbox_to_sql_bool ( $_POST [ " trgm_similarity_enabled " ] ? ? " " );
2015-01-19 09:52:15 +00:00
$key = array_search ( $feed_id , $enabled_feeds );
if ( $enable ) {
2020-09-17 16:02:27 +00:00
if ( $key === false ) {
2015-01-19 09:52:15 +00:00
array_push ( $enabled_feeds , $feed_id );
}
} else {
2020-09-17 16:02:27 +00:00
if ( $key !== false ) {
2015-01-19 09:52:15 +00:00
unset ( $enabled_feeds [ $key ]);
}
}
$this -> host -> set ( $this , " enabled_feeds " , $enabled_feeds );
}
function hook_article_filter ( $article ) {
2022-06-19 18:36:33 +00:00
if ( Config :: get ( Config :: DB_TYPE ) == " pgsql " ) {
$res = $this -> pdo -> query ( " select 'similarity'::regproc " );
if ( ! $res || ! $res -> fetch ()) return $article ;
}
2015-01-19 09:52:15 +00:00
2015-06-23 04:36:25 +00:00
$enable_globally = $this -> host -> get ( $this , " enable_globally " );
2021-01-11 09:23:46 +00:00
if ( ! $enable_globally &&
! in_array ( $article [ " feed " ][ " id " ],
2021-11-14 17:36:55 +00:00
$this -> host -> get_array ( $this , " enabled_feeds " ))) {
2021-01-11 09:23:46 +00:00
return $article ;
2015-06-23 04:36:25 +00:00
}
2015-01-19 09:52:15 +00:00
2021-01-11 09:23:46 +00:00
$similarity = ( float ) $this -> host -> get ( $this , " similarity " , $this -> default_similarity );
2015-01-19 09:52:15 +00:00
2021-01-11 09:23:46 +00:00
if ( $similarity < 0.01 ) {
2021-11-14 17:36:55 +00:00
Debug :: log ( " af_psql_trgm: similarity is set too low ( $similarity ) " , Debug :: LOG_EXTENDED );
2021-01-11 09:23:46 +00:00
return $article ;
}
$min_title_length = ( int ) $this -> host -> get ( $this , " min_title_length " , $this -> default_min_length );
if ( mb_strlen ( $article [ " title " ]) < $min_title_length ) {
2021-11-14 17:36:55 +00:00
Debug :: log ( " af_psql_trgm: article title is too short (min: $min_title_length ) " , Debug :: LOG_EXTENDED );
2021-01-11 09:23:46 +00:00
return $article ;
}
2015-01-19 09:52:15 +00:00
$owner_uid = $article [ " owner_uid " ];
2015-07-15 10:15:00 +00:00
$entry_guid = $article [ " guid_hashed " ];
2017-12-03 07:26:38 +00:00
$title_escaped = $article [ " title " ];
2015-01-19 09:52:15 +00:00
2015-06-23 01:35:24 +00:00
// trgm does not return similarity=1 for completely equal strings
2021-01-11 09:23:46 +00:00
// this seems to be no longer the case (fixed in upstream?)
2015-06-23 01:35:24 +00:00
2021-01-11 09:23:46 +00:00
/* $sth = $this -> pdo -> prepare ( " SELECT COUNT(id) AS nequal
2015-06-23 01:35:24 +00:00
FROM ttrss_entries , ttrss_user_entries WHERE ref_id = id AND
2016-08-01 05:26:11 +00:00
date_entered >= NOW () - interval '3 days' AND
2017-12-03 07:26:38 +00:00
title = ? AND
guid != ? AND
owner_uid = ? " );
$sth -> execute ([ $title_escaped , $entry_guid , $owner_uid ]);
$row = $sth -> fetch ();
$nequal = $row [ 'nequal' ];
2015-06-23 01:35:24 +00:00
2018-11-30 05:34:29 +00:00
Debug :: log ( " af_psql_trgm: num equals: $nequal " , Debug :: $LOG_EXTENDED );
2015-06-23 01:35:24 +00:00
if ( $nequal != 0 ) {
$article [ " force_catchup " ] = true ;
return $article ;
2021-01-11 09:23:46 +00:00
} */
2015-06-23 01:35:24 +00:00
2022-06-19 18:36:33 +00:00
if ( Config :: get ( Config :: DB_TYPE ) == " pgsql " ) {
$sth = $this -> pdo -> prepare ( " SELECT MAX(SIMILARITY(title, :title)) AS ms
FROM ttrss_entries , ttrss_user_entries WHERE ref_id = id AND
date_entered >= NOW () - interval '1 day' AND
guid != : guid AND
owner_uid = : uid " );
} else {
2022-06-19 20:04:10 +00:00
$sth = $this -> pdo -> prepare ( " SELECT (MATCH(title) AGAINST (:title) / LENGTH(title)) AS ms
2022-06-19 18:36:33 +00:00
FROM ttrss_entries , ttrss_user_entries WHERE ref_id = id AND
date_entered >= DATE_SUB ( NOW (), INTERVAL 1 DAY ) AND
guid != : guid AND
owner_uid = : uid
ORDER BY ms DESC
LIMIT 1 " );
}
$sth -> execute ([ 'title' => $title_escaped , 'guid' => $entry_guid , 'uid' => $owner_uid ]);
2015-01-19 09:52:15 +00:00
2017-12-03 07:26:38 +00:00
$row = $sth -> fetch ();
$similarity_result = $row [ 'ms' ];
2015-01-19 09:52:15 +00:00
2021-11-14 17:36:55 +00:00
Debug :: log ( " af_psql_trgm: similarity result for $title_escaped : $similarity_result " , Debug :: LOG_EXTENDED );
2015-01-19 09:52:15 +00:00
if ( $similarity_result >= $similarity ) {
2021-11-14 17:36:55 +00:00
Debug :: log ( " af_psql_trgm: marking article as read ( $similarity_result >= $similarity ) " , Debug :: LOG_EXTENDED );
2021-01-11 09:23:46 +00:00
2015-01-19 09:52:15 +00:00
$article [ " force_catchup " ] = true ;
}
return $article ;
}
function api_version () {
return 2 ;
}
2021-11-14 17:36:55 +00:00
/**
* @ param array < int > $enabled_feeds
* @ return array < int >
* @ throws PDOException
*/
private function filter_unknown_feeds ( $enabled_feeds ) : array {
2015-06-11 14:17:19 +00:00
$tmp = array ();
foreach ( $enabled_feeds as $feed ) {
2017-12-03 07:26:38 +00:00
$sth = $this -> pdo -> prepare ( " SELECT id FROM ttrss_feeds WHERE id = ? AND owner_uid = ? " );
$sth -> execute ([ $feed , $_SESSION [ 'uid' ]]);
2015-06-11 14:17:19 +00:00
2017-12-03 07:26:38 +00:00
if ( $row = $sth -> fetch ()) {
2015-06-11 14:17:19 +00:00
array_push ( $tmp , $feed );
}
}
return $tmp ;
}
2019-02-20 11:37:59 +00:00
}