Helper.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. <?php declare(strict_types = 0);
  2. /*
  3. ** WeCom Center - business logic helpers.
  4. ** Works through the official Zabbix API so every setting is stored in the
  5. ** Zabbix database itself -> survives image upgrades and is portable.
  6. */
  7. namespace Modules\WecomCenter\Lib;
  8. use API;
  9. use Exception;
  10. class Helper {
  11. // Zabbix trigger severity index => media severity bit (1 << index).
  12. public const SEVERITY_BITS = [1, 2, 4, 8, 16, 32];
  13. // Built-in alert schemes: name => [severity_mask, active(0=enabled,1=disabled)]
  14. public const SCHEMES = [
  15. 'standard' => [48, 0], // High + Disaster (recommended)
  16. 'warnplus' => [56, 0], // Average + High + Disaster
  17. 'all' => [63, 0], // everything
  18. 'quiet' => [32, 0], // Disaster only
  19. 'off' => [63, 1] // alerting disabled
  20. ];
  21. /* ------------------------------------------------------------------ */
  22. /* Media types / robot */
  23. /* ------------------------------------------------------------------ */
  24. /**
  25. * All webhook media types (type == 4).
  26. */
  27. public static function webhookMediatypes(): array {
  28. try {
  29. $res = API::Mediatype()->get([
  30. 'output' => ['mediatypeid', 'name', 'type', 'status'],
  31. 'selectParameters' => 'extend',
  32. 'filter' => ['type' => 4]
  33. ]);
  34. return is_array($res) ? $res : [];
  35. }
  36. catch (\Throwable $e) {
  37. return [];
  38. }
  39. }
  40. public static function getMediatype(string $mediatypeid): ?array {
  41. try {
  42. $res = API::Mediatype()->get([
  43. 'output' => ['mediatypeid', 'name', 'type', 'status'],
  44. 'selectParameters' => 'extend',
  45. 'mediatypeids' => [$mediatypeid]
  46. ]);
  47. return (is_array($res) && $res) ? $res[0] : null;
  48. }
  49. catch (\Throwable $e) {
  50. return null;
  51. }
  52. }
  53. /**
  54. * Read the webhook URL / key from a media type parameters list.
  55. */
  56. public static function webhookUrl(array $mediatype): string {
  57. foreach (($mediatype['parameters'] ?? []) as $p) {
  58. $name = strtolower($p['name']);
  59. if (in_array($name, ['webhook_url', 'url', 'webhook'], true)) {
  60. return (string) $p['value'];
  61. }
  62. }
  63. return '';
  64. }
  65. /**
  66. * Update (or add) the webhook_url parameter of a media type, preserving others.
  67. */
  68. public static function setWebhookUrl(string $mediatypeid, string $url): void {
  69. $mt = self::getMediatype($mediatypeid);
  70. if (!$mt) {
  71. throw new Exception('Media type not found: '.$mediatypeid);
  72. }
  73. $params = [];
  74. $found = false;
  75. foreach (($mt['parameters'] ?? []) as $p) {
  76. if (in_array(strtolower($p['name']), ['webhook_url', 'url', 'webhook'], true)) {
  77. $params[] = ['name' => $p['name'], 'value' => $url];
  78. $found = true;
  79. }
  80. else {
  81. $params[] = ['name' => $p['name'], 'value' => $p['value']];
  82. }
  83. }
  84. if (!$found) {
  85. $params[] = ['name' => 'webhook_url', 'value' => $url];
  86. }
  87. API::Mediatype()->update(['mediatypeid' => $mediatypeid, 'parameters' => $params]);
  88. }
  89. /* ------------------------------------------------------------------ */
  90. /* Robot user + media (severity / active) */
  91. /* ------------------------------------------------------------------ */
  92. /**
  93. * Find the dedicated robot user with its media.
  94. */
  95. public static function robotUser(string $username = 'wecomrobot'): ?array {
  96. $res = API::User()->get([
  97. 'output' => ['userid', 'username'],
  98. 'selectMedias' => 'extend',
  99. 'filter' => ['username' => $username]
  100. ]);
  101. return $res ? $res[0] : null;
  102. }
  103. /**
  104. * Current severity mask + active flag of a user's media for a media type.
  105. * Returns ['active'=>int, 'severity'=>int] or null when media absent.
  106. */
  107. public static function currentMedia(?array $user, string $mediatypeid): ?array {
  108. if (!$user || empty($user['medias'])) {
  109. return null;
  110. }
  111. foreach ($user['medias'] as $m) {
  112. if ((string) $m['mediatypeid'] === (string) $mediatypeid) {
  113. return [
  114. 'active' => (int) $m['active'],
  115. 'severity' => (int) $m['severity'],
  116. 'period' => $m['period'] ?? '1-7,00:00-24:00',
  117. 'sendto' => $m['sendto'] ?? ''
  118. ];
  119. }
  120. }
  121. return null;
  122. }
  123. /**
  124. * Ensure a robot user exists; create one (no frontend access) if missing.
  125. */
  126. public static function ensureRobotUser(string $username): array {
  127. $user = self::robotUser($username);
  128. if ($user) {
  129. return $user;
  130. }
  131. // best-effort group / role for a non-login notification account
  132. $grp = API::UserGroup()->get([
  133. 'output' => ['usrgrpid', 'name'],
  134. 'filter' => ['name' => 'No access to the frontend'],
  135. 'limit' => 1
  136. ]);
  137. $usrgrpid = $grp ? $grp[0]['usrgrpid'] : null;
  138. if ($usrgrpid === null) {
  139. $all = API::UserGroup()->get(['output' => ['usrgrpid'], 'limit' => 1]);
  140. $usrgrpid = $all ? $all[0]['usrgrpid'] : null;
  141. }
  142. $role = API::Role()->get([
  143. 'output' => ['roleid', 'name'],
  144. 'filter' => ['name' => 'User role'],
  145. 'limit' => 1
  146. ]);
  147. $roleid = $role ? $role[0]['roleid'] : null;
  148. $create = [
  149. 'username' => $username,
  150. 'passwd' => bin2hex(random_bytes(16)).'Aa1!',
  151. 'usrgrps' => [['usrgrpid' => $usrgrpid]]
  152. ];
  153. if ($roleid !== null) {
  154. $create['roleid'] = $roleid;
  155. }
  156. $res = API::User()->create($create);
  157. return self::robotUser($username);
  158. }
  159. /**
  160. * Apply severity mask + active flag to a user's media for a media type.
  161. * Rebuilds the whole media list (API user.update replaces medias).
  162. */
  163. public static function applyMedia(string $userid, string $mediatypeid, int $severity, int $active,
  164. string $sendto = 'WeCom') {
  165. $res = API::User()->get([
  166. 'output' => ['userid'],
  167. 'selectMedias' => 'extend',
  168. 'userids' => [$userid]
  169. ]);
  170. $medias = [];
  171. if ($res) {
  172. foreach (($res[0]['medias'] ?? []) as $m) {
  173. $medias[] = [
  174. 'mediatypeid' => $m['mediatypeid'],
  175. 'sendto' => $m['sendto'],
  176. 'severity' => (string) $m['severity'],
  177. 'active' => (string) $m['active'],
  178. 'period' => $m['period'] ?: '1-7,00:00-24:00'
  179. ];
  180. }
  181. }
  182. $hit = false;
  183. foreach ($medias as &$m) {
  184. if ((string) $m['mediatypeid'] === (string) $mediatypeid) {
  185. $m['severity'] = (string) $severity;
  186. $m['active'] = (string) $active;
  187. if ($sendto !== '') {
  188. $m['sendto'] = $sendto;
  189. }
  190. $hit = true;
  191. }
  192. }
  193. unset($m);
  194. if (!$hit) {
  195. $medias[] = [
  196. 'mediatypeid' => $mediatypeid,
  197. 'sendto' => $sendto !== '' ? $sendto : 'WeCom',
  198. 'severity' => (string) $severity,
  199. 'active' => (string) $active,
  200. 'period' => '1-7,00:00-24:00'
  201. ];
  202. }
  203. API::User()->update(['userid' => $userid, 'medias' => $medias]);
  204. }
  205. /* ------------------------------------------------------------------ */
  206. /* Send test message */
  207. /* ------------------------------------------------------------------ */
  208. public static function testWebhook(string $url, string $text): array {
  209. $payload = json_encode(['msgtype' => 'text', 'text' => ['content' => $text]], JSON_UNESCAPED_UNICODE);
  210. $ch = curl_init($url);
  211. curl_setopt_array($ch, [
  212. CURLOPT_RETURNTRANSFER => true,
  213. CURLOPT_POST => true,
  214. CURLOPT_POSTFIELDS => $payload,
  215. CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
  216. CURLOPT_TIMEOUT => 15,
  217. CURLOPT_SSL_VERIFYPEER => false,
  218. CURLOPT_SSL_VERIFYHOST => 0
  219. ]);
  220. $body = curl_exec($ch);
  221. $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  222. $err = curl_error($ch);
  223. curl_close($ch);
  224. return ['status' => $status, 'body' => $body !== false ? $body : $err];
  225. }
  226. /* ------------------------------------------------------------------ */
  227. /* Templates */
  228. /* ------------------------------------------------------------------ */
  229. /**
  230. * Bundled templates shipped inside modules/WecomCenter/templates/.
  231. */
  232. public static function bundledTemplates(string $module_dir): array {
  233. $dir = rtrim($module_dir, '/').'/templates';
  234. $out = [];
  235. if (!is_dir($dir)) {
  236. return $out;
  237. }
  238. foreach (scandir($dir) as $f) {
  239. if ($f === '.' || $f === '..') {
  240. continue;
  241. }
  242. $ext = strtolower(pathinfo($f, PATHINFO_EXTENSION));
  243. if (in_array($ext, ['xml', 'yaml', 'yml', 'json'], true)) {
  244. $out[] = ['file' => $f, 'name' => pathinfo($f, PATHINFO_FILENAME)];
  245. }
  246. }
  247. return $out;
  248. }
  249. public static function importTemplate(string $module_dir, string $file): array {
  250. $dir = rtrim($module_dir, '/').'/templates';
  251. $file = basename($file);
  252. $path = $dir.'/'.$file;
  253. if (!is_file($path)) {
  254. throw new Exception('Template file not found: '.$file);
  255. }
  256. $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
  257. $format = ['xml' => 'xml', 'json' => 'json', 'yaml' => 'yaml', 'yml' => 'yaml'][$ext] ?? 'yaml';
  258. $source = file_get_contents($path);
  259. $result = API::Configuration()->import([
  260. 'format' => $format,
  261. 'source' => $source,
  262. 'rules' => [
  263. 'template_groups' => ['createMissing' => true, 'updateExisting' => true],
  264. 'templates' => ['createMissing' => true, 'updateExisting' => true],
  265. 'items' => ['createMissing' => true, 'updateExisting' => true],
  266. 'triggers' => ['createMissing' => true, 'updateExisting' => true]
  267. ]
  268. ]);
  269. return is_array($result) ? $result : [];
  270. }
  271. /* ------------------------------------------------------------------ */
  272. /* Map coordinates */
  273. /* ------------------------------------------------------------------ */
  274. public static function hosts(): array {
  275. return API::Host()->get([
  276. 'output' => ['hostid', 'name'],
  277. 'sortfield' => 'name',
  278. 'limit' => 1000
  279. ]);
  280. }
  281. public static function setHostLocation(string $hostid, string $lat, string $lon): void {
  282. API::Host()->update([
  283. 'hostid' => $hostid,
  284. 'inventory_mode' => 0, // manual
  285. 'inventory' => [
  286. 'location_lat' => $lat,
  287. 'location_lon' => $lon
  288. ]
  289. ]);
  290. }
  291. public static function hostLocation(string $hostid): array {
  292. $res = API::Host()->get([
  293. 'output' => ['hostid', 'name'],
  294. 'selectInventory' => ['location_lat', 'location_lon'],
  295. 'hostids' => [$hostid]
  296. ]);
  297. return $res ? $res[0] : [];
  298. }
  299. /* ------------------------------------------------------------------ */
  300. /* Helpers */
  301. /* ------------------------------------------------------------------ */
  302. public static function severityMask(array $indices): int {
  303. $mask = 0;
  304. foreach ($indices as $i) {
  305. $i = (int) $i;
  306. if ($i >= 1 && $i <= 6) {
  307. $mask |= self::SEVERITY_BITS[$i - 1];
  308. }
  309. }
  310. return $mask;
  311. }
  312. }