Af_Psql_Trgm   A
last analyzed

Complexity

Total Complexity 41

Size/Duplication

Total Lines 372
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 183
c 1
b 0
f 0
dl 0
loc 372
rs 9.1199
wmc 41

12 Methods

Rating   Name   Duplication   Size   Complexity  
A about() 0 4 1
A get_js() 0 2 1
A init() 0 8 1
A showrelated() 0 66 3
A save() 0 23 4
A api_version() 0 2 1
A hook_prefs_edit_feed() 0 20 3
B hook_article_filter() 0 72 9
A hook_prefs_save_feed() 0 20 5
A filter_unknown_feeds() 0 14 3
A hook_article_button() 0 4 1
B hook_prefs_tab() 0 106 9

How to fix   Complexity   

Complex Class

Complex classes like Af_Psql_Trgm often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Af_Psql_Trgm, and based on these observations, apply Extract Interface, too.

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 " &mdash; $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
The constant DB_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
138
            print_error("Database type not supported.");
0 ignored issues
show
Deprecated Code introduced by
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 ignore-deprecated  annotation

138
            /** @scrutinizer ignore-deprecated */ print_error("Database type not supported.");

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...
Unused Code introduced by
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 ignore-call  annotation

138
            /** @scrutinizer ignore-call */ 
139
            print_error("Database type not supported.");

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
Deprecated Code introduced by
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 ignore-deprecated  annotation

144
                /** @scrutinizer ignore-deprecated */ print_error("pg_trgm extension not found.");

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
Unused Code introduced by
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 ignore-call  annotation

180
            /** @scrutinizer ignore-call */ 
181
            print_notice("Enable for specific feeds in the feed editor.");

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...
Deprecated Code introduced by
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 ignore-deprecated  annotation

180
            /** @scrutinizer ignore-deprecated */ print_notice("Enable for specific feeds in the feed editor.");

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
Bug introduced by
The constant DB_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
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
Bug introduced by
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 ignore-type  annotation

296
            $key = array_search($article["feed"]["id"], /** @scrutinizer ignore-type */ $enabled_feeds);
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
Unused Code introduced by
The assignment to $row is dead and can be removed.
Loading history...
369
                array_push($tmp, $feed);
370
            }
371
        }
372
373
        return $tmp;
374
    }
375
376
}
377