api.pbxmonitor.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. <?php
  2. /**
  3. * Universal PBX calls recodrings viewer class
  4. */
  5. class PBXMonitor {
  6. /**
  7. * Contains all call records loaded from database
  8. *
  9. * @var array
  10. */
  11. protected $allRecords = array();
  12. /**
  13. * Contains count of call records available
  14. *
  15. * @var int
  16. */
  17. protected $totalRecordsCount = 0;
  18. /**
  19. * Contains filtered records count
  20. *
  21. * @var int
  22. */
  23. protected $filteredRecordsCount = 0;
  24. /**
  25. * Contains system alter config as key=>value
  26. *
  27. * @var array
  28. */
  29. protected $altCfg = array();
  30. /**
  31. * Contains default recorded calls path
  32. *
  33. * @var string
  34. */
  35. protected $voicePath = '';
  36. /**
  37. * Contains voice recors archive path
  38. *
  39. * @var string
  40. */
  41. protected $archivePath = '';
  42. /**
  43. * Flag for telepathy detection of users
  44. *
  45. * @var bool
  46. */
  47. protected $onlyMobileFlag = true;
  48. /**
  49. * Contains user assigned tags as login=>usertags
  50. *
  51. * @var array
  52. */
  53. protected $userTags = array();
  54. /**
  55. * FFmpeg installed?
  56. *
  57. * @var bool
  58. */
  59. protected $ffmpegFlag = false;
  60. /**
  61. * installed ffmpeg path
  62. *
  63. * @var string
  64. */
  65. protected $ffmpegPath = '';
  66. /**
  67. * Basic ffmpeg path to search.
  68. *
  69. * @var string
  70. */
  71. protected $baseConverterPath = '';
  72. /**
  73. * File path for converted voice files
  74. *
  75. * @var string
  76. */
  77. protected $convertedPath = 'exports/';
  78. /**
  79. * ffmpeg log path
  80. *
  81. * @var string
  82. */
  83. protected $converterLogPath = 'exports/voiceconvert.log';
  84. /**
  85. * PBX calls cache database abstraction layer
  86. *
  87. * @var object
  88. */
  89. protected $pbxCallsDb = '';
  90. /**
  91. * Default on-page calls number
  92. *
  93. * @var int
  94. */
  95. protected $onPage = 50;
  96. /**
  97. * Default icons path
  98. */
  99. const ICON_PATH = 'skins/calls/';
  100. /**
  101. * Default module path
  102. */
  103. const URL_ME = '?module=pbxmonitor';
  104. /**
  105. * URL of user profile route
  106. */
  107. const URL_PROFILE = '?module=userprofile&username=';
  108. /**
  109. * Name of database table with calls cache
  110. */
  111. const TABLE_CALLS = 'pbxcalls';
  112. /**
  113. * Cache refill process PID
  114. */
  115. const REFILL_PID = 'PBXCALLS';
  116. /**
  117. * Creates new PBX monitor instance
  118. *
  119. * @return void
  120. */
  121. public function __construct() {
  122. $this->loadConfig();
  123. $this->detectFfmpeg();
  124. $this->initPbxCallsDb();
  125. // _______
  126. // /` _____ `\;,
  127. // /__(^===^)__\';,
  128. // / ::: \ ,;
  129. // | ::: | ,;'
  130. // '._______.'`
  131. }
  132. /**
  133. * Loads all required configs and sets some options
  134. *
  135. * @return void
  136. */
  137. protected function loadConfig() {
  138. global $ubillingConfig;
  139. $this->altCfg = $ubillingConfig->getAlter();
  140. if ((!isset($this->altCfg['WDYC_ONLY_MOBILE'])) or (!@$this->altCfg['WDYC_ONLY_MOBILE'])) {
  141. $this->onlyMobileFlag = false;
  142. }
  143. $this->voicePath = $this->altCfg['PBXMON_RECORDS_PATH'];
  144. $this->archivePath = $this->altCfg['PBXMON_ARCHIVE_PATH'];
  145. $this->baseConverterPath = $this->altCfg['PBXMON_FFMPG_PATH'];
  146. }
  147. /**
  148. * Inits calls cache database abstraction layer
  149. *
  150. * @return void
  151. */
  152. protected function initPbxCallsDb() {
  153. $this->pbxCallsDb = new NyanORM('pbxcalls');
  154. }
  155. /**
  156. * Detects is ffmpeg available on local system and sets ffmpegFlag and path properties.
  157. *
  158. * @return void
  159. */
  160. protected function detectFfmpeg() {
  161. if (file_exists($this->baseConverterPath)) {
  162. $this->ffmpegFlag = true;
  163. $this->ffmpegPath = $this->baseConverterPath;
  164. }
  165. }
  166. /**
  167. * Loads existing tagtypes and usertags into protected props for further usage
  168. *
  169. * @return void
  170. */
  171. protected function loadUserTags() {
  172. $this->userTags = zb_UserGetAllTags();
  173. }
  174. /**
  175. * Catches file download or convert request
  176. *
  177. * @return void
  178. */
  179. public function catchFileDownload() {
  180. if (ubRouting::checkGet('dlpbxcall')) {
  181. $origFileName = ubRouting::get('dlpbxcall');
  182. $downloadableName = '';
  183. //voice records
  184. if (file_exists($this->voicePath . $origFileName)) {
  185. $downloadableName = $this->voicePath . $origFileName;
  186. } else {
  187. //archive download
  188. if (file_exists($this->archivePath . $origFileName)) {
  189. $downloadableName = $this->archivePath . $origFileName;
  190. }
  191. }
  192. //voice files converter installed?
  193. if ($this->ffmpegFlag) {
  194. if (ubRouting::checkGet('playable')) {
  195. //need to run converter
  196. if (!empty($downloadableName)) {
  197. //original file is already located
  198. $newFileExtension = (ubRouting::checkGet('mp3')) ? '.mp3' : '.ogg';
  199. $newFilePath = $this->convertedPath . $origFileName . $newFileExtension;
  200. //convert if not already converted
  201. if (!file_exists($newFilePath)) {
  202. $command = $this->ffmpegPath . ' -y -i ' . $downloadableName . ' ' . $newFilePath . ' 2>> ' . $this->converterLogPath;
  203. shell_exec($command);
  204. }
  205. $downloadableName = $newFilePath;
  206. }
  207. }
  208. } else {
  209. show_error(__('ffmpeg is not installed. Web player and converter not available.'));
  210. }
  211. //file download processing
  212. if (!empty($downloadableName)) {
  213. zb_DownloadFile($downloadableName, 'default');
  214. } else {
  215. show_error(__('File not exist') . ': ' . $origFileName);
  216. }
  217. }
  218. }
  219. /**
  220. * Returns list of all files in directory. Using this instead of rcms_scandir with filters
  221. * to prevent of much of preg_match callbacks and avoid performance issues.
  222. *
  223. * @param string $directory
  224. *
  225. * @return array
  226. */
  227. protected function scanDirectory($directory) {
  228. $result = array();
  229. if (!empty($directory)) {
  230. if (file_exists($directory)) {
  231. $raw = scandir($directory);
  232. $result = array_diff($raw, array('.', '..'));
  233. }
  234. }
  235. return ($result);
  236. }
  237. /**
  238. * Returns available calls files array
  239. *
  240. * @return array
  241. */
  242. protected function getCallsDir() {
  243. return ($this->scanDirectory($this->voicePath));
  244. }
  245. /**
  246. * Returns available archived calls files array
  247. *
  248. * @return array
  249. */
  250. protected function getArchiveDir() {
  251. return ($this->scanDirectory($this->archivePath));
  252. }
  253. /**
  254. * Returns calls list container
  255. *
  256. * @return string
  257. */
  258. public function renderCallsList() {
  259. $opts = '"order": [[ 0, "desc" ]]';
  260. $columns = array(__('Date'), __('Number'), __('User'), __('Tags'), __('File'));
  261. if (ubRouting::checkGet('username')) {
  262. $loginFilter = '&loginfilter=' . ubRouting::get('username');
  263. } else {
  264. $loginFilter = '';
  265. }
  266. if (ubRouting::checkGet('renderall')) {
  267. $filterNumber = '&renderall=true';
  268. } else {
  269. $filterNumber = '';
  270. }
  271. $result = wf_JqDtLoader($columns, self::URL_ME . '&ajax=true' . $loginFilter . $filterNumber, false, __('Calls records'), $this->onPage, $opts, false, '', '', true);
  272. return ($result);
  273. }
  274. /**
  275. * Renders user tags if available
  276. *
  277. * @param string $userLogin
  278. *
  279. * @return string
  280. */
  281. protected function renderUserTags($userLogin) {
  282. $result = '';
  283. if (!empty($userLogin)) {
  284. if (isset($this->userTags[$userLogin])) {
  285. if (!empty($this->userTags[$userLogin])) {
  286. $result .= implode(', ', $this->userTags[$userLogin]);
  287. }
  288. }
  289. }
  290. return ($result);
  291. }
  292. /**
  293. * Refills calls cache with some new calls if found
  294. *
  295. * @return void
  296. */
  297. public function refillCache() {
  298. $process = new StarDust(self::REFILL_PID);
  299. if ($process->notRunning()) {
  300. $process->start();
  301. $allVoiceFiles = $this->getCallsDir();
  302. $allArchiveFiles = $this->getArchiveDir();
  303. $telepathy = new Telepathy(false, true);
  304. $telepathy->usePhones();
  305. $previousCalls = $this->pbxCallsDb->getAll('filename');
  306. //normal voice records
  307. if (!empty($allVoiceFiles)) {
  308. foreach ($allVoiceFiles as $io => $each) {
  309. $fileName = $each;
  310. $explodedFile = explode('_', $fileName);
  311. $cleanDate = explode('.', $explodedFile[2]);
  312. $cleanDate = $cleanDate[0];
  313. //unfinished calls
  314. if ((!ispos($cleanDate, 'in')) and (!ispos($cleanDate, 'out'))) {
  315. //new call?
  316. if (!isset($previousCalls[$fileName])) {
  317. $fileSize = filesize($this->voicePath . $fileName);
  318. if ($fileSize > 0) {
  319. $callingNumber = $explodedFile[1];
  320. $callDirection = ($explodedFile[0] == 'in') ? 'in' : 'out';
  321. $dateString = date_format(date_create_from_format('Y-m-d-H-i-s', $cleanDate), 'Y-m-d H:i:s');
  322. $userLogin = $telepathy->getByPhoneFast($callingNumber, $this->onlyMobileFlag, $this->onlyMobileFlag);
  323. $this->pbxCallsDb->data('filename', ubRouting::filters($fileName, 'mres'));
  324. $this->pbxCallsDb->data('login', ubRouting::filters($userLogin, 'mres'));
  325. $this->pbxCallsDb->data('size', $fileSize);
  326. $this->pbxCallsDb->data('direction', $callDirection);
  327. $this->pbxCallsDb->data('date', $dateString);
  328. $this->pbxCallsDb->data('number', $callingNumber);
  329. $this->pbxCallsDb->data('storage', 'rec');
  330. $this->pbxCallsDb->create();
  331. }
  332. } else {
  333. $callData = $previousCalls[$fileName];
  334. //storage changed?
  335. if ($callData['storage'] != 'rec') {
  336. $callId = $callData['id'];
  337. $this->pbxCallsDb->where('id', '=', $callId);
  338. $this->pbxCallsDb->data('storage', 'rec');
  339. $this->pbxCallsDb->save();
  340. }
  341. }
  342. }
  343. }
  344. }
  345. //archived records
  346. if (!empty($allArchiveFiles)) {
  347. foreach ($allArchiveFiles as $io => $each) {
  348. $fileName = $each;
  349. $explodedFile = explode('_', $fileName);
  350. $cleanDate = explode('.', $explodedFile[2]);
  351. $cleanDate = $cleanDate[0];
  352. //unfinished calls
  353. if ((!ispos($cleanDate, 'in')) and (!ispos($cleanDate, 'out'))) {
  354. //new call?
  355. if (!isset($previousCalls[$fileName])) {
  356. $fileSize = filesize($this->archivePath . $fileName);
  357. if ($fileSize > 0) {
  358. $callingNumber = $explodedFile[1];
  359. $callDirection = ($explodedFile[0] == 'in') ? 'in' : 'out';
  360. $dateString = date_format(date_create_from_format('Y-m-d-H-i-s', $cleanDate), 'Y-m-d H:i:s');
  361. $userLogin = $telepathy->getByPhoneFast($callingNumber, $this->onlyMobileFlag, $this->onlyMobileFlag);
  362. $this->pbxCallsDb->data('filename', ubRouting::filters($fileName, 'mres'));
  363. $this->pbxCallsDb->data('login', ubRouting::filters($userLogin, 'mres'));
  364. $this->pbxCallsDb->data('size', $fileSize);
  365. $this->pbxCallsDb->data('direction', $callDirection);
  366. $this->pbxCallsDb->data('date', $dateString);
  367. $this->pbxCallsDb->data('number', $callingNumber);
  368. $this->pbxCallsDb->data('storage', 'arch');
  369. $this->pbxCallsDb->create();
  370. }
  371. } else {
  372. $callData = $previousCalls[$fileName];
  373. //storage changed?
  374. if ($callData['storage'] != 'arch') {
  375. $callId = $callData['id'];
  376. $this->pbxCallsDb->where('id', '=', $callId);
  377. $this->pbxCallsDb->data('storage', 'arch');
  378. $this->pbxCallsDb->save();
  379. }
  380. }
  381. }
  382. }
  383. }
  384. $telepathy->savePhoneTelepathyCache();
  385. $process->stop();
  386. } else {
  387. log_register('PBXMON REFILL SKIPPED ALREADY RUNNING');
  388. }
  389. }
  390. /**
  391. * Performs records filtering, ordering and load
  392. *
  393. * @return void
  394. */
  395. protected function recordsLoader($filterLogin = '', $renderAll = false) {
  396. $filterLogin = ubRouting::filters($filterLogin, 'mres');
  397. $this->onPage = (ubRouting::checkGet('iDisplayLength')) ? ubRouting::get('iDisplayLength') : $this->onPage;
  398. //login filtering
  399. if ($filterLogin) {
  400. $this->pbxCallsDb->where('login', '=', $filterLogin);
  401. } else {
  402. //date current year filtering
  403. if (!$renderAll) {
  404. $this->pbxCallsDb->where('date', 'LIKE', curyear() . '-%');
  405. }
  406. }
  407. $sortField = 'date';
  408. $sortDir = 'desc';
  409. if (ubRouting::checkGet('iSortCol_0', false)) {
  410. $sortingColumn = ubRouting::get('iSortCol_0', 'int');
  411. $sortDir = ubRouting::get('sSortDir_0', 'gigasafe');
  412. switch ($sortingColumn) {
  413. case 0:
  414. $sortField = 'date';
  415. break;
  416. case 1:
  417. $sortField = 'number';
  418. break;
  419. case 2:
  420. $sortField = 'login';
  421. break;
  422. }
  423. }
  424. $this->pbxCallsDb->orderBy($sortField, $sortDir);
  425. $this->totalRecordsCount = $this->pbxCallsDb->getFieldsCount('id', false);
  426. $offset = 0;
  427. if (ubRouting::checkGet('iDisplayStart')) {
  428. $offset = ubRouting::get('iDisplayStart', 'int');
  429. }
  430. //optional live search
  431. $searchQuery = '';
  432. if (ubRouting::checkGet('sSearch')) {
  433. $searchQuery = ubRouting::get('sSearch', 'mres');
  434. if (!$filterLogin) {
  435. $dateQuery = ubRouting::filters($searchQuery, 'gigasafe', '-: ');
  436. $this->pbxCallsDb->where('number', 'LIKE', '%' . $searchQuery . '%');
  437. $this->pbxCallsDb->orWhere('date', 'LIKE', '%' . $dateQuery . '%');
  438. $this->pbxCallsDb->orWhere('login', 'LIKE', '%' . $searchQuery . '%');
  439. }
  440. }
  441. //optional live search happens
  442. if ($searchQuery) {
  443. $this->filteredRecordsCount = $this->pbxCallsDb->getFieldsCount('id', false) - 1;
  444. } else {
  445. $this->filteredRecordsCount = $this->totalRecordsCount;
  446. }
  447. $this->pbxCallsDb->limit($this->onPage, $offset);
  448. $this->allRecords = $this->pbxCallsDb->getAll();
  449. }
  450. /**
  451. * Renders json recorded calls list
  452. *
  453. * @param string $filterLogin
  454. * @param bool $renderAll
  455. *
  456. * @return void
  457. */
  458. public function jsonCallsList($filterLogin = '', $renderAll = false) {
  459. $allAddress = zb_AddressGetFulladdresslistCached();
  460. $allRealnames = zb_UserGetAllRealnames();
  461. $this->loadUserTags();
  462. $this->recordsLoader($filterLogin, $renderAll);
  463. $json = new wf_JqDtHelper(true);
  464. $json->setTotalRowsCount($this->totalRecordsCount);
  465. $json->setFilteredRowsCount($this->filteredRecordsCount);
  466. $curYear = curyear() . '-';
  467. //current year filter for all calls
  468. if (empty($filterLogin) and ! $renderAll) {
  469. $renderAll = false;
  470. } else {
  471. $renderAll = true;
  472. }
  473. $allCallsLabel = ($renderAll) ? wf_img('skins/allcalls.png', __('All time')) . ' ' : '';
  474. //normal voice records rendering
  475. if (!empty($this->allRecords)) {
  476. foreach ($this->allRecords as $io => $each) {
  477. $archiveLabel = ($each['storage'] == 'arch') ? wf_img('skins/calls/archived.png', __('Archive')) : '';
  478. $userLogin = $each['login'];
  479. $callingNumber = $each['number'];
  480. $callDirection = ($each['direction'] == 'in') ? self::ICON_PATH . 'incoming.png' : self::ICON_PATH . 'outgoing.png';
  481. $userLink = (!empty($userLogin)) ? wf_Link('?module=userprofile&username=' . $userLogin, web_profile_icon() . ' ' . @$allAddress[$userLogin]) . ' ' . @$allRealnames[$userLogin] : '';
  482. $fileUrl = self::URL_ME . '&dlpbxcall=' . $each['filename'];
  483. //append data to results
  484. $data[] = wf_img($callDirection) . ' ' . $each['date'];
  485. $data[] = $callingNumber;
  486. $data[] = $userLink;
  487. $data[] = $this->renderUserTags($userLogin);
  488. $data[] = $this->getSoundcontrols($fileUrl, $each['filename'], $filterLogin) . $archiveLabel . $allCallsLabel;
  489. $json->addRow($data);
  490. unset($data);
  491. }
  492. }
  493. $json->getJson();
  494. }
  495. /**
  496. * Returns controls for some recorded call file
  497. *
  498. * @param string $fileUrl
  499. * @param string $fileName
  500. * @param string $userName
  501. * @return string
  502. */
  503. protected function getSoundcontrols($fileUrl, $fileName, $userName = '') {
  504. $result = '';
  505. $bl = '';
  506. if (!empty($userName)) {
  507. $bl = '&bl=' . $userName;
  508. }
  509. if (!empty($fileUrl)) {
  510. if ($this->ffmpegFlag) {
  511. $playableUrl = $fileUrl . '&playable=true';
  512. $iconPlay = wf_img('skins/play.png', __('Play'));
  513. $result .= wf_Link(self::URL_ME . '&pbxplayer=' . $fileName . $bl, $iconPlay, false) . ' ';
  514. $result .= wf_Link($playableUrl, wf_img('skins/icon_ogg.png', __('Download') . ' ' . __('as OGG'))) . ' ';
  515. $result .= wf_Link($playableUrl . '&mp3=true', wf_img('skins/icon_mp3.png', __('Download') . ' ' . __('as MP3'))) . ' ';
  516. } else {
  517. $result .= wf_Link('#', wf_img('skins/factorcontrol.png', __('ffmpeg is not installed. Web player and converter not available.'))) . ' ';
  518. }
  519. //basic download control
  520. $result .= wf_Link($fileUrl, wf_img('skins/icon_download.png', __('Download') . ' ' . __('as is')));
  521. }
  522. return ($result);
  523. }
  524. /**
  525. * Returns player for some recorded call file
  526. *
  527. * @param string $fileUrl
  528. *
  529. * @return string
  530. */
  531. public function renderSoundPlayer($fileName) {
  532. $result = '';
  533. $backLink = self::URL_ME;
  534. if (ubRouting::checkGet('bl')) {
  535. $backLink = self::URL_ME . '&username=' . ubRouting::get('bl');
  536. }
  537. if (!empty($fileName)) {
  538. $fileUrl = self::URL_ME . '&dlpbxcall=' . $fileName;
  539. if ($this->ffmpegFlag) {
  540. $playableUrl = $fileUrl . '&playable=true';
  541. $playerId = 'pbxcallrecfile';
  542. $result .= wf_tag('audio', false, '', 'id="' . $playerId . '" src="' . $playableUrl . '" preload="auto"') . wf_tag('audio', true);
  543. $result .= wf_tag('div', false, '', 'id="waveformstatus"') . wf_tag('div', true);
  544. $result .= wf_tag('div', false, '', 'id="waveform"') . wf_tag('div', true);
  545. $result .= wf_tag('script', false, '', 'src="https://unpkg.com/wavesurfer.js"') . wf_tag('script', true);
  546. $result .= wf_tag('script', false, '', 'type="text/javascript" src="modules/jsc/pbxmonplayer.js"') . wf_tag('script', true);
  547. $result .= wf_delimiter(0);
  548. $result .= wf_Link($playableUrl, wf_img('skins/icon_ogg.png', __('Download') . ' ' . __('as OGG')) . ' ' . __('Download') . ' ' . __('as OGG'), false, 'ubButton') . ' ';
  549. $result .= wf_Link($playableUrl . '&mp3=true', wf_img('skins/icon_mp3.png', __('Download') . ' ' . __('as MP3')) . ' ' . __('Download') . ' ' . __('as MP3'), false, 'ubButton') . ' ';
  550. } else {
  551. $messages = new UbillingMessageHelper();
  552. $result .= $messages->getStyledMessage(__('ffmpeg is not installed. Web player and converter not available.'), 'warning');
  553. $result .= wf_delimiter(0);
  554. }
  555. //basic download control
  556. $result .= wf_Link($fileUrl, wf_img('skins/icon_download.png', __('Download') . ' ' . __('as is')) . ' ' . __('Download') . ' ' . __('as is'), false, 'ubButton');
  557. }
  558. $result .= wf_delimiter();
  559. $result .= wf_BackLink($backLink);
  560. return ($result);
  561. }
  562. }