diff --git a/Models/UserFingerprint.php b/Models/UserFingerprint.php index 7870fce..de080aa 100644 --- a/Models/UserFingerprint.php +++ b/Models/UserFingerprint.php @@ -253,4 +253,59 @@ public function deleteRotated($max_log_count = 100000, $delete_ratio = 0.01) $delete_id_quoted = DB::quote($delete_id); return DB::delete($this->_table, "`id` <= $delete_id_quoted"); } + + public function findUserByFingerprintOrSid($fingerprint_array, $sid_array) + { + if (!$fingerprint_array || !$fingerprint_array) { + return []; + } + $fingerprint_in = implode(', ', DB::quote($fingerprint_array)); + $sid_in = implode(', ', DB::quote($sid_array)); + return DB::fetch_all("SELECT DISTINCT(`uid`), `username` FROM `{$this->prefixed_table}` WHERE `fingerprint` IN ({$fingerprint_in}) OR `sid` IN ({$sid_in})"); + } + + public function findRelatedUser($uid) + { + $records = DB::fetch_all("SELECT `fingerprint`, `sid` FROM `{$this->prefixed_table}` WHERE `uid` = {$uid}"); + $fingerprint_array = []; + $sid_array = []; + foreach ($records as $row) { + $fingerprint_array[] = $row['fingerprint']; + $sid_array[] = $row['sid']; + } + $related_users = $this->findUserByFingerprintOrSid($fingerprint_array, $sid_array); + return [ + 'fingerprint_array' => $fingerprint_array, + 'sid_array' => $sid_array, + 'related_users' => $related_users, + ]; + } + + public function findMultiAccountUidArray($start = 0, $limit = 20) + { + $fingerprint_array = []; + $sid_array = []; + $records = DB::fetch_all("SELECT `fingerprint`, COUNT(DISTINCT(`uid`)) AS `count` FROM `pre_user_fingerprint_log` GROUP BY `fingerprint` ORDER BY `count` DESC LIMIT {$start}, {$limit}");; + foreach ($records as $row) { + if ((int)$row['count'] > 1) { + $fingerprint_array[] = $row['fingerprint']; + } + } + $records = DB::fetch_all("SELECT `sid`, COUNT(DISTINCT(`uid`)) AS `count` FROM `pre_user_fingerprint_log` GROUP BY `sid` ORDER BY `count` DESC LIMIT {$start}, {$limit}"); + foreach ($records as $row) { + if ((int)$row['count'] > 1) { + $sid_array[] = $row['sid']; + } + } + return $this->findUserByFingerprintOrSid($fingerprint_array, $sid_array); + } + + public function findUserByUid($uid_array) + { + if (!$uid_array) { + return []; + } + $in = implode(', ', DB::quote($uid_array)); + return DB::fetch_all("SELECT DISTINCT(`uid`), `username` FROM `{$this->prefixed_table}` WHERE `uid` IN ({$in})"); + } } diff --git a/admin_chart.inc.php b/admin_chart.inc.php new file mode 100644 index 0000000..d03efa8 --- /dev/null +++ b/admin_chart.inc.php @@ -0,0 +1,131 @@ + ['name' => '1' . _(' Relation Account')], + 1 => ['name' => '2' . _(' Relation Account')], + 2 => ['name' => '3' . _(' Relation Account')], + 3 => ['name' => _('Fingerprint')], + 4 => ['name' => _('Session')], +]; +$nodes = []; +$links = []; +$index = 0; +$all_uid_index = []; +$all_fingerprint_index = []; +$all_sid_index = []; + + +$uid = (int)$_GET['uid']; +if ($uid) { + $uid_to_search = [$uid]; +} else { + $records = $table->findMultiAccountUidArray(); + foreach ($records as $row) { + $uid_to_search[] = (int)$row['uid']; + } +} + +$records = $table->findUserByUid($uid_to_search); +$uid_to_search_2 = []; +foreach ($records as $row) { + $row_uid = (int)$row['uid']; + if (!isset($all_uid_index[$row_uid])) { + $nodes[$index] = [ + 'name' => $row['username'], + 'category' => 0, + 'symbolSize' => 40, + ]; + $all_uid_index[$row_uid] = $index; + ++$index; + $uid_to_search_2[] = $row_uid; + } +} +$uid_to_search = $uid_to_search_2; + +for ($level = 1; $level <= 2; ++$level) { + $uid_to_search_2 = []; + foreach ($uid_to_search as $uid) { + $records = $table->findRelatedUser($uid); + + foreach ($records['fingerprint_array'] as $fingerprint) { + if (!isset($all_fingerprint_index[$fingerprint])) { + $nodes[$index] = [ + 'name' => $fingerprint, + 'category' => 3, + 'symbolSize' => 10, + ]; + $all_fingerprint_index[$fingerprint] = $index; + ++$index; + } + $links[] = [ + 'source' => $all_uid_index[$uid], + 'target' => $all_fingerprint_index[$fingerprint], + ]; + } + + foreach ($records['sid_array'] as $sid) { + if (!isset($all_sid_index[$sid])) { + $nodes[$index] = [ + 'name' => $sid, + 'category' => 4, + 'symbolSize' => 10, + ]; + $all_sid_index[$sid] = $index; + ++$index; + } + $links[] = [ + 'source' => $all_uid_index[$uid], + 'target' => $all_sid_index[$sid], + ]; + } + + foreach ($records['related_users'] as $row) { + $row_uid = (int)$row['uid']; + if (!isset($all_uid_index[$row_uid])) { + $nodes[$index] = [ + 'name' => $row['username'], + 'category' => $level, + 'symbolSize' => (4 - $level) * 10, + ]; + $all_uid_index[$row_uid] = $index; + ++$index; + $uid_to_search_2[] = $row_uid; + } + $links[] = [ + 'source' => $all_uid_index[$uid], + 'target' => $all_uid_index[$row_uid], + ]; + } + } + $uid_to_search = $uid_to_search_2; +} + +$data = [ + 'title' => [ + 'text' => _('User Account Relation Visualization') + ], + 'categories' => $categories, + 'nodes' => $nodes, + 'links' => $links, +]; + + +echo ''; + +echo <<<'EOD' +
+ +EOD; diff --git a/build/build_js.sh b/build/build_js.sh index 64bbfdf..dc293ce 100644 --- a/build/build_js.sh +++ b/build/build_js.sh @@ -5,7 +5,16 @@ cd ../js/ if [[ ! -f fingerprint2.min.js ]]; then wget https://cdn.jsdelivr.net/npm/fingerprintjs2@2.0.3/dist/fingerprint2.min.js fi -cp fingerprint2.min.js bundle.min.js -uglifyjs --compress --mangle --comments "/^!/" -- script.js >> bundle.min.js +uglifyjs --compress --mangle --comments "/^!/" -- script.js > script.min.js +cat fingerprint2.min.js script.min.js > bundle.min.js + +if [[ ! -f echarts.min.js ]]; then + wget https://cdn.jsdelivr.net/npm/echarts@4.2.0-rc.2/dist/echarts.min.js +fi +if [[ ! -f axios.min.js ]]; then + wget https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js +fi +uglifyjs --compress --mangle --comments "/^!/" -- admin_chart.js > admin_chart.min.js +cat axios.min.js echarts.min.js admin_chart.min.js > admin.min.js popd diff --git a/discuz_plugin_user_fingerprint.xml b/discuz_plugin_user_fingerprint.xml index 9ecad71..44178d7 100644 --- a/discuz_plugin_user_fingerprint.xml +++ b/discuz_plugin_user_fingerprint.xml @@ -14,7 +14,7 @@ - + @@ -58,7 +58,7 @@ - + @@ -81,6 +81,19 @@ + + + + + + + + + + + + + @@ -130,6 +143,10 @@ + + + + diff --git a/function/function_admin.php b/function/function_admin.php index 669614c..e9f6045 100644 --- a/function/function_admin.php +++ b/function/function_admin.php @@ -131,7 +131,8 @@ function admin_show_table_row_content($row, $item) dhtmlspecialchars($row['hit']), dhtmlspecialchars(date('m-d H:i', $row['created_at'])), dhtmlspecialchars(date('m-d H:i', $row['last_online_time'])), - '' . _('Find user') . ' ' - . '' . _('User space') . '', + '' . _('Find user') . '' + . ' ' . _('User space') . '' + . ' ' . _('Visualization') . '', ]); } diff --git a/js/admin_chart.js b/js/admin_chart.js new file mode 100644 index 0000000..abdab6f --- /dev/null +++ b/js/admin_chart.js @@ -0,0 +1,85 @@ +/*! + * User Fingerprint Discuz! X plugin + * Copyright (C) 2018 Ganlv + * License: GPL 3.0 + */ +var el = document.getElementById('chart'); + +function resizeChart() { + var width = el.parentElement.clientWidth; + var height = window.innerHeight - el.parentElement.offsetTop - 20; + if (width < 800) { + width = 800; + } + if (height / width < 0.3) { + height = width * 0.3; + } + el.style.width = width + 'px'; + el.style.height = height + 'px'; +} + +resizeChart(); + +var myChart = echarts.init(el); + +window.onresize = function () { + resizeChart(); + if (myChart) { + myChart.resize(); + } +}; + + +function handle(data) { + myChart.hideLoading(); + + var option = { + title: data.title, + legend: { + data: data.categories + }, + series: [{ + type: 'graph', + layout: 'force', + animation: false, + draggable: true, + roam: true, + focusNodeAdjacency: true, + force: { + initLayout: 'circular', + repulsion: 20, + edgeLength: 200, + gravity: 0.1 + }, + label: { + position: 'bottom', + formatter: '{b}' + }, + itemStyle: { + normal: { + borderColor: '#fff', + borderWidth: 1, + shadowBlur: 10, + shadowColor: 'rgba(0, 0, 0, 0.3)' + } + }, + lineStyle: { + color: 'source' + }, + emphasis: { + lineStyle: { + width: 10 + } + }, + data: data.nodes, + categories: data.categories, + edges: data.links + }] + }; + myChart.setOption(option); +} + +myChart.showLoading(); + +handle(window.user_relation_data); +