Passed
Pull Request — master (#5)
by Cody
03:17
created

Af_Psql_Trgm   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Importance

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

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