PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 All 128 releases
extendify / app / Mcp / Log.php

Log.php in Extendify 3.2.2, at app/Mcp/Log.php

178 lines 5.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The tool calls a connection has made on this site.
5 */
6
7 namespace Extendify\Mcp;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 /**
12 * Arguments are never stored: they carry post bodies, alt text, user filters and
13 * email addresses, while a tool name and its outcome answer what support asks.
14 * If the shape of a call is ever wanted, store argument keys, never values.
15 */
16 class Log
17 {
18 // phpcs:disable PSR12.Properties.ConstantVisibility.NotFound
19 const KEEP_ROWS = 5000;
20
21 const KEEP_DAYS = 90;
22
23 const ERROR_LENGTH = 500;
24
25 const SHOWN = 10;
26 // phpcs:enable PSR12.Properties.ConstantVisibility.NotFound
27
28 /**
29 * @return string
30 */
31 public static function table()
32 {
33 return $GLOBALS['wpdb']->prefix . 'extendify_mcp_log';
34 }
35
36 /**
37 * @param array $connection - The connection the call arrived on.
38 * @param string $tool - The tool that was called.
39 * @param array $details - outcome, error and duration in seconds.
40 * @return void
41 */
42 public static function write(array $connection, $tool, array $details)
43 {
44 self::install();
45 $wpdb = $GLOBALS['wpdb'];
46 $wpdb->insert(self::table(), [
47 'created_at' => gmdate('Y-m-d H:i:s'),
48 'user_id' => (int) ($connection['userId'] ?? \get_current_user_id()),
49 'connection' => (string) ($connection['id'] ?? ''),
50 'client' => substr((string) ($connection['data']['label'] ?? ''), 0, Connections::LABEL_LENGTH),
51 'tool' => substr((string) $tool, 0, Surface::MAX_NAME),
52 'outcome' => (string) $details['outcome'],
53 'error' => substr((string) ($details['error'] ?? ''), 0, self::ERROR_LENGTH),
54 'duration_ms' => (int) round(((float) ($details['duration'] ?? 0)) * 1000),
55 ]);
56
57 self::prune();
58 }
59
60 /**
61 * @param integer $userId - The user the connection belongs to.
62 * @param string $connection - The connection's id.
63 * @param integer $limit - How many of the newest calls to read.
64 * @return array
65 */
66 public static function recent($userId, $connection, $limit = self::SHOWN)
67 {
68 if (!self::exists()) {
69 return [];
70 }
71
72 $wpdb = $GLOBALS['wpdb'];
73 $table = self::table();
74
75 return $wpdb->get_results($wpdb->prepare(
76 "SELECT created_at, tool, outcome, error FROM {$table}"
77 . ' WHERE user_id = %d AND `connection` = %s ORDER BY id DESC LIMIT %d',
78 (int) $userId,
79 (string) $connection,
80 (int) $limit
81 ), ARRAY_A) ?: [];
82 }
83
84 /**
85 * @param integer $userId - The user the connection belonged to.
86 * @param string $id - The connection's id.
87 * @return void
88 */
89 public static function forget($userId, $id)
90 {
91 if (!self::exists()) {
92 return;
93 }
94
95 $GLOBALS['wpdb']->delete(self::table(), [
96 'user_id' => (int) $userId,
97 'connection' => (string) $id,
98 ]);
99 }
100
101 /**
102 * @return void
103 */
104 private static function prune()
105 {
106 $wpdb = $GLOBALS['wpdb'];
107 $table = self::table();
108 $wpdb->query($wpdb->prepare(
109 "DELETE FROM {$table} WHERE created_at < %s",
110 gmdate('Y-m-d H:i:s', time() - (self::KEEP_DAYS * DAY_IN_SECONDS))
111 ));
112
113 $held = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
114 if ($held <= self::KEEP_ROWS) {
115 return;
116 }
117
118 $wpdb->query($wpdb->prepare("DELETE FROM {$table} ORDER BY id LIMIT %d", $held - self::KEEP_ROWS));
119 }
120
121 /**
122 * @return void
123 */
124 public static function install()
125 {
126 $wpdb = $GLOBALS['wpdb'];
127 $table = self::table();
128 if (self::exists()) {
129 $held = array_column($wpdb->get_results("SHOW COLUMNS FROM {$table}", ARRAY_A), 'Field');
130 foreach (self::columns() as $name => $type) {
131 if (!in_array($name, $held, true)) {
132 $wpdb->query("ALTER TABLE {$table} ADD COLUMN `{$name}` {$type}");
133 }
134 }
135
136 return;
137 }
138
139 $columns = [];
140 foreach (self::columns() as $name => $type) {
141 $columns[] = "`{$name}` {$type}";
142 }
143
144 $wpdb->query(
145 "CREATE TABLE {$table} (" . implode(',', $columns) . ', INDEX(user_id), INDEX(created_at)) '
146 . $wpdb->get_charset_collate() . ';'
147 );
148 }
149
150 /**
151 * @return boolean
152 */
153 private static function exists()
154 {
155 $wpdb = $GLOBALS['wpdb'];
156
157 return (bool) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', self::table()));
158 }
159
160 /**
161 * @return array
162 */
163 private static function columns()
164 {
165 return [
166 'id' => 'BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY',
167 'created_at' => 'DATETIME NOT NULL',
168 'user_id' => 'BIGINT UNSIGNED NOT NULL',
169 'connection' => 'VARCHAR(191) NOT NULL DEFAULT \'\'',
170 'client' => 'VARCHAR(' . Connections::LABEL_LENGTH . ') NOT NULL DEFAULT \'\'',
171 'tool' => 'VARCHAR(' . Surface::MAX_NAME . ') NOT NULL DEFAULT \'\'',
172 'outcome' => 'VARCHAR(16) NOT NULL DEFAULT \'\'',
173 'error' => 'VARCHAR(' . self::ERROR_LENGTH . ') NOT NULL DEFAULT \'\'',
174 'duration_ms' => 'INT UNSIGNED NOT NULL DEFAULT 0',
175 ];
176 }
177 }
178