diff --git a/debian/control b/debian/control index 739347f482..a5c074b294 100644 --- a/debian/control +++ b/debian/control @@ -36,6 +36,7 @@ Build-Depends: debhelper (>= 8), libdata-printer-perl, libxml-mini-perl, libgd-gd2-perl, + libhtml-parser-perl, ngcp-schema Standards-Version: 3.9.4 Homepage: http://sipwise.com/ @@ -76,6 +77,7 @@ Depends: libcatalyst-actionrole-acl-perl, libdata-printer-perl, libxml-mini-perl, libgd-gd2-perl, + libhtml-parser-perl, ngcp-schema, ${misc:Depends}, ${perl:Depends} diff --git a/lib/NGCP/Panel/Controller/Subscriber.pm b/lib/NGCP/Panel/Controller/Subscriber.pm index be652dfd2e..8f15549056 100644 --- a/lib/NGCP/Panel/Controller/Subscriber.pm +++ b/lib/NGCP/Panel/Controller/Subscriber.pm @@ -1,7 +1,7 @@ package NGCP::Panel::Controller::Subscriber; use Sipwise::Base; -use namespace::sweep; BEGIN { extends 'Catalyst::Controller'; } +use HTML::Entities; use NGCP::Panel::Utils; use NGCP::Panel::Utils::Navigation; use NGCP::Panel::Utils::Contract; @@ -1380,6 +1380,11 @@ sub master :Chained('base') :PathPart('details') :CaptureArgs(0) { { name => "contact", search => 1, title => "Contact" }, { name => "expires", search => 1, title => "Expires" }, ]); + $c->stash->{capture_dt_columns} = NGCP::Panel::Utils::Datatables::set_columns($c, [ + { name => "timestamp", search => 1, title => "Timestamp" }, + { name => "call_id", search => 1, title => "Call-ID" }, + { name => "method", search => 1, title => "Method" }, + ]); $c->stash( template => 'subscriber/master.tt', @@ -1930,6 +1935,25 @@ sub ajax_voicemails :Chained('master') :PathPart('voicemails/ajax') :Args(0) { $c->detach( $c->view("JSON") ); } +sub ajax_captured_calls :Chained('master') :PathPart('callflow/ajax') :Args(0) { + my ($self, $c) = @_; + + my $caller_rs = $c->model('DB')->resultset('messages')->search({ + 'me.caller_uuid' => $c->stash->{subscriber}->uuid, + }); + my $callee_rs = $c->model('DB')->resultset('messages')->search({ + 'me.callee_uuid' => $c->stash->{subscriber}->uuid, + }); + + # TODO: group_by or distinct on call_id! + my $rs = $caller_rs->union($callee_rs)->search(undef, { + order_by => { -asc => 'me.timestamp' }, + }); + + NGCP::Panel::Utils::Datatables::process($c, $rs, $c->stash->{capture_dt_columns}); + $c->detach( $c->view("JSON") ); +} + sub voicemail :Chained('master') :PathPart('voicemail') :CaptureArgs(1) { my ($self, $c, $vm_id) = @_; @@ -2380,7 +2404,6 @@ sub get_pcap :Chained('callflow_base') :PathPart('pcap') :Args(0) { $c->response->header ('Content-Disposition' => 'attachment; filename="' . $cid . '.pcap"'); $c->response->content_type('application/octet-stream'); $c->response->body($pcap); - } sub get_png :Chained('callflow_base') :PathPart('png') :Args(0) { @@ -2399,6 +2422,60 @@ sub get_png :Chained('callflow_base') :PathPart('png') :Args(0) { $c->response->header ('Content-Disposition' => 'attachment; filename="' . $cid . '.png"'); $c->response->content_type('image/png'); $c->response->body($png); +} + +sub get_callmap :Chained('callflow_base') :PathPart('callmap') :Args(0) { + my ($self, $c) = @_; + my $cid = $c->stash->{callid}; + + my $calls_rs = $c->model('DB')->resultset('messages')->search({ + 'me.call_id' => { -in => [ $cid, $cid.'_b2b-1' ] }, + }, { + order_by => { -asc => 'timestamp' }, + }); + + my $calls = [ $calls_rs->all ]; + my $map = NGCP::Panel::Utils::Callflow::generate_callmap($c, $calls); + + $c->stash( + canvas => $map, + template => 'subscriber/callmap.tt', + ); +} + +sub get_packet :Chained('callflow_base') :PathPart('packet') :Args() { + my ($self, $c, $packet_id) = @_; + my $cid = $c->stash->{callid}; + + my $packet = $c->model('DB')->resultset('messages')->find({ + 'me.call_id' => { -in => [ $cid, $cid.'_b2b-1' ] }, + 'me.id' => $packet_id, + }, { + order_by => { -asc => 'timestamp' }, + }); + + return unless($packet); + + my $pkg = { $packet->get_inflated_columns }; + + my $t = DateTime->from_epoch( + epoch => $pkg->{timestamp}, + time_zone => DateTime::TimeZone->new(name => 'local'), + ); + my $tstamp = $t->ymd('-') . ' ' . $t->hms(':') . '.' . $t->millisecond; + + $pkg->{payload} = encode_entities($pkg->{payload}); + $pkg->{payload} =~ s/\r//g; + $pkg->{payload} =~ s/([^\n]{120})/$1/g; + $pkg->{payload} =~ s/^([^\n]+)\n/$1<\/b>\n/; + $pkg->{payload} = $tstamp .' ('.$pkg->{timestamp}.')
'. + $pkg->{src_ip}.':'.$pkg->{src_port}.' → '. $pkg->{dst_ip}.':'.$pkg->{dst_port}.'

'. + $pkg->{payload}; + $pkg->{payload} =~ s/\n([a-zA-Z0-9\-_]+\:)/\n$1<\/b>/g; + $pkg->{payload} =~ s/\n//g; + + $c->response->content_type('text/html'); + $c->response->body($pkg->{payload}); } diff --git a/share/static/js/libs/jquery.popup.js b/share/static/js/libs/jquery.popup.js new file mode 100644 index 0000000000..3ae1dc5add --- /dev/null +++ b/share/static/js/libs/jquery.popup.js @@ -0,0 +1,222 @@ +function modalPopup(align, top, width, padding, disableColor, disableOpacity, backgroundColor, borderColor, borderWeight, borderRadius, fadeOutTime, url, loadingImage){ + + var containerid = "innerModalPopupDiv"; + + var popupDiv = document.createElement('div'); + var popupMessage = document.createElement('div'); + var blockDiv = document.createElement('div'); + + popupDiv.setAttribute('id', 'outerModalPopupDiv'); + popupDiv.setAttribute('class', 'outerModalPopupDiv'); + + popupMessage.setAttribute('id', 'innerModalPopupDiv'); + popupMessage.setAttribute('class', 'innerModalPopupDiv'); + + blockDiv.setAttribute('id', 'blockModalPopupDiv'); + blockDiv.setAttribute('class', 'blockModalPopupDiv'); + blockDiv.setAttribute('onClick', 'closePopup(' + fadeOutTime + ')'); + + document.body.appendChild(popupDiv); + popupDiv.appendChild(popupMessage); + document.body.appendChild(blockDiv); + + if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)){ //test for MSIE x.x; + var ieversion=new Number(RegExp.$1) // capture x.x portion and store as a number + if(ieversion>6) { + getScrollHeight(top); + } + } else { + getScrollHeight(top); + } + + document.getElementById('outerModalPopupDiv').style.display='block'; + document.getElementById('outerModalPopupDiv').style.width = width + 'px'; + document.getElementById('outerModalPopupDiv').style.padding = borderWeight + 'px'; + document.getElementById('outerModalPopupDiv').style.background = borderColor; + document.getElementById('outerModalPopupDiv').style.borderRadius = borderRadius + 'px'; + document.getElementById('outerModalPopupDiv').style.MozBorderRadius = borderRadius + 'px'; + document.getElementById('outerModalPopupDiv').style.WebkitBorderRadius = borderRadius + 'px'; + document.getElementById('outerModalPopupDiv').style.borderWidth = 0 + 'px'; + document.getElementById('outerModalPopupDiv').style.position = 'absolute'; + document.getElementById('outerModalPopupDiv').style.zIndex = 100; + + document.getElementById('innerModalPopupDiv').style.padding = padding + 'px'; + document.getElementById('innerModalPopupDiv').style.background = backgroundColor; + document.getElementById('innerModalPopupDiv').style.borderRadius = (borderRadius - 3) + 'px'; + document.getElementById('innerModalPopupDiv').style.MozBorderRadius = (borderRadius - 3) + 'px'; + document.getElementById('innerModalPopupDiv').style.WebkitBorderRadius = (borderRadius - 3) + 'px'; + + document.getElementById('blockModalPopupDiv').style.width = 100 + '%'; + document.getElementById('blockModalPopupDiv').style.border = 0 + 'px'; + document.getElementById('blockModalPopupDiv').style.padding = 0 + 'px'; + document.getElementById('blockModalPopupDiv').style.margin = 0 + 'px'; + document.getElementById('blockModalPopupDiv').style.background = disableColor; + document.getElementById('blockModalPopupDiv').style.opacity = (disableOpacity / 100); + document.getElementById('blockModalPopupDiv').style.filter = 'alpha(Opacity=' + disableOpacity + ')'; + document.getElementById('blockModalPopupDiv').style.zIndex = 99; + document.getElementById('blockModalPopupDiv').style.position = 'fixed'; + document.getElementById('blockModalPopupDiv').style.top = 0 + 'px'; + document.getElementById('blockModalPopupDiv').style.left = 0 + 'px'; + + if(align=="center") { + document.getElementById('outerModalPopupDiv').style.marginLeft = (-1 * (width / 2)) + 'px'; + document.getElementById('outerModalPopupDiv').style.left = 50 + '%'; + } else if(align=="left") { + document.getElementById('outerModalPopupDiv').style.marginLeft = 0 + 'px'; + document.getElementById('outerModalPopupDiv').style.left = 10 + 'px'; + } else if(align=="right") { + document.getElementById('outerModalPopupDiv').style.marginRight = 0 + 'px'; + document.getElementById('outerModalPopupDiv').style.right = 10 + 'px'; + } else { + document.getElementById('outerModalPopupDiv').style.marginLeft = (-1 * (width / 2)) + 'px'; + document.getElementById('outerModalPopupDiv').style.left = 50 + '%'; + } + + blockPage(); + + var page_request = false; + if (window.XMLHttpRequest) { + page_request = new XMLHttpRequest(); + } else if (window.ActiveXObject) { + try { + page_request = new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) { + try { + page_request = new ActiveXObject("Microsoft.XMLHTTP"); + } catch (e) { } + } + } else { + return false; + } + + + page_request.onreadystatechange=function(){ + if((url.search(/.jpg/i)==-1) && (url.search(/.jpeg/i)==-1) && (url.search(/.gif/i)==-1) && (url.search(/.png/i)==-1) && (url.search(/.bmp/i)==-1)) { + pageloader(page_request, containerid, loadingImage); + } else { + imageloader(url, containerid, loadingImage); + } + } + + page_request.open('GET', url, true); + page_request.send(null); + +} + +function pageloader(page_request, containerid, loadingImage){ + + document.getElementById(containerid).innerHTML = '
'; + + if (page_request.readyState == 4 && (page_request.status==200 || window.location.href.indexOf("http")==-1)) { + document.getElementById(containerid).innerHTML=page_request.responseText; + } + +} + +function imageloader(url, containerid, loadingImage) { + + document.getElementById(containerid).innerHTML = '
'; + document.getElementById(containerid).innerHTML='
'; + +} + +function blockPage() { + + var blockdiv = document.getElementById('blockModalPopupDiv'); + var height = screen.height; + + blockdiv.style.height = height + 'px'; + blockdiv.style.display = 'block'; + +} + +function getScrollHeight(top) { + + var h = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop; + + if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)) { + + var ieversion=new Number(RegExp.$1); + + if(ieversion>6) { + document.getElementById('outerModalPopupDiv').style.top = h + top + 'px'; + } else { + document.getElementById('outerModalPopupDiv').style.top = top + 'px'; + } + + } else { + document.getElementById('outerModalPopupDiv').style.top = h + top + 'px'; + } + +} + +function closePopup(fadeOutTime) { + + fade('outerModalPopupDiv', fadeOutTime); + document.getElementById('blockModalPopupDiv').style.display='none'; + +} + +function fade(id, fadeOutTime) { + + var el = document.getElementById(id); + + if(el == null) { + return; + } + + if(el.FadeState == null) { + + if(el.style.opacity == null || el.style.opacity == '' || el.style.opacity == '1') { + el.FadeState = 2; + } else { + el.FadeState = -2; + } + + } + + if(el.FadeState == 1 || el.FadeState == -1) { + + el.FadeState = el.FadeState == 1 ? -1 : 1; + el.fadeTimeLeft = fadeOutTime - el.fadeTimeLeft; + + } else { + + el.FadeState = el.FadeState == 2 ? -1 : 1; + el.fadeTimeLeft = fadeOutTime; + setTimeout("animateFade(" + new Date().getTime() + ",'" + id + "','" + fadeOutTime + "')", 33); + + } + +} + +function animateFade(lastTick, id, fadeOutTime) { + + var currentTick = new Date().getTime(); + var totalTicks = currentTick - lastTick; + + var el = document.getElementById(id); + + if(el.fadeTimeLeft <= totalTicks) { + + el.style.opacity = el.FadeState == 1 ? '1' : '0'; + el.style.filter = 'alpha(opacity = ' + (el.FadeState == 1 ? '100' : '0') + ')'; + el.FadeState = el.FadeState == 1 ? 2 : -2; + document.body.removeChild(el); + return; + + } + + el.fadeTimeLeft -= totalTicks; + var newOpVal = el.fadeTimeLeft / fadeOutTime; + + if(el.FadeState == 1) { + newOpVal = 1 - newOpVal; + } + + el.style.opacity = newOpVal; + el.style.filter = 'alpha(opacity = ' + (newOpVal*100) + ')'; + + setTimeout("animateFade(" + currentTick + ",'" + id + "','" + fadeOutTime + "')", 33); + +} \ No newline at end of file diff --git a/share/templates/subscriber/callmap.tt b/share/templates/subscriber/callmap.tt new file mode 100644 index 0000000000..0cf76e9b38 --- /dev/null +++ b/share/templates/subscriber/callmap.tt @@ -0,0 +1,37 @@ +[% site_config.title = 'Call Flow for Call-ID ' _ callid -%] + + + + + +[% back_created = 1 -%] + +
+ [% FOREACH m IN messages -%] +
[% m.text %]
+ [% END -%] +
+ +
+ + + + [% FOREACH area IN canvas.areas -%] + + [% END -%] + + +[% # vim: set tabstop=4 syntax=html expandtab: -%] diff --git a/share/templates/subscriber/master.tt b/share/templates/subscriber/master.tt index aad02bcac3..c14306a450 100644 --- a/share/templates/subscriber/master.tt +++ b/share/templates/subscriber/master.tt @@ -161,6 +161,34 @@ +
+ +
+
+ +[% + helper.name = 'Captured Dialogs'; + #helper.column_sort = 'origtime'; + helper.dt_columns = capture_dt_columns; + + helper.close_target = close_target; + helper.create_flag = create_flag; + helper.edit_flag = edit_flag; + helper.form_object = form; + helper.ajax_uri = c.uri_for_action('/subscriber/ajax_captured_calls', [c.req.captures.0]); + + helper.dt_buttons = [ + { name = 'Call Flow', uri = "callflow/'+encodeURIComponent(full.call_id)+'/callmap", class = 'btn-small btn-primary', icon = 'icon-random' }, + ]; + + + PROCESS 'helpers/datatables.tt'; +%] +
+
+