├── static-html ├── ModSecurityLogo.png ├── modsec-status-spider.png ├── images │ ├── animated-overlay.gif │ ├── ui-icons_222222_256x240.png │ ├── ui-icons_2e83ff_256x240.png │ ├── ui-icons_454545_256x240.png │ ├── ui-icons_888888_256x240.png │ ├── ui-icons_cd0a0a_256x240.png │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ ├── ui-bg_flat_75_ffffff_40x100.png │ ├── ui-bg_glass_55_fbf9ee_1x400.png │ ├── ui-bg_glass_65_ffffff_1x400.png │ ├── ui-bg_glass_75_dadada_1x400.png │ ├── ui-bg_glass_75_e6e6e6_1x400.png │ ├── ui-bg_glass_95_fef1ec_1x400.png │ └── ui-bg_highlight-soft_75_cccccc_1x100.png ├── modsec-status-loading.gif ├── modsec-status.html ├── modsec-status.css ├── modsec-status.js ├── jquery.balloon.js └── jquery-ui.css ├── api ├── modsec-status-config.pl └── modsec-status.pl ├── collector ├── modsec-tinydns-parser-config.pl └── modsec-tinydns-parser.pl ├── database-structure.sql └── README /static-html/ModSecurityLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/ModSecurityLogo.png -------------------------------------------------------------------------------- /static-html/modsec-status-spider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/modsec-status-spider.png -------------------------------------------------------------------------------- /static-html/images/animated-overlay.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/animated-overlay.gif -------------------------------------------------------------------------------- /static-html/modsec-status-loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/modsec-status-loading.gif -------------------------------------------------------------------------------- /static-html/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /static-html/images/ui-icons_2e83ff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-icons_2e83ff_256x240.png -------------------------------------------------------------------------------- /static-html/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /static-html/images/ui-icons_888888_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-icons_888888_256x240.png -------------------------------------------------------------------------------- /static-html/images/ui-icons_cd0a0a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-icons_cd0a0a_256x240.png -------------------------------------------------------------------------------- /static-html/images/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /static-html/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /static-html/images/ui-bg_glass_55_fbf9ee_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-bg_glass_55_fbf9ee_1x400.png -------------------------------------------------------------------------------- /static-html/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /static-html/images/ui-bg_glass_75_dadada_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-bg_glass_75_dadada_1x400.png -------------------------------------------------------------------------------- /static-html/images/ui-bg_glass_75_e6e6e6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-bg_glass_75_e6e6e6_1x400.png -------------------------------------------------------------------------------- /static-html/images/ui-bg_glass_95_fef1ec_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-bg_glass_95_fef1ec_1x400.png -------------------------------------------------------------------------------- /static-html/images/ui-bg_highlight-soft_75_cccccc_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpiderLabs/ModSecurity-status/HEAD/static-html/images/ui-bg_highlight-soft_75_cccccc_1x100.png -------------------------------------------------------------------------------- /api/modsec-status-config.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | 4 | our $mysql_dbi, $mysql_username, $mysql_password; 5 | 6 | $mysql_dbi = "dbi goes here"; 7 | $mysql_username = "username goes here"; 8 | $mysql_password = "password goes here"; 9 | 10 | 1; 11 | -------------------------------------------------------------------------------- /collector/modsec-tinydns-parser-config.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -p 2 | 3 | our $tail, $geo_lite_db, $mysql_dbi, $mysql_username, $mysql_password, $log_uri; 4 | 5 | $tail = "status.modsecurity.org"; 6 | $geo_lite_db = "/usr/local/apache/conf/GeoLiteCity.dat"; 7 | 8 | $mysql_dbi = "dbi:mysql:modsecurity_db_goes_here"; 9 | $mysql_username = "db_username_goes_here"; 10 | $mysql_password = "db_password_goes_here"; 11 | 12 | $log_uri = "/tmp/tinydns-parser-collector.txt"; 13 | 14 | 1; 15 | 16 | -------------------------------------------------------------------------------- /database-structure.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- Table structure for table `status` 3 | -- 4 | 5 | DROP TABLE IF EXISTS `status`; 6 | CREATE TABLE `status` ( 7 | `ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 8 | `payload` varchar(400) DEFAULT NULL, 9 | `host` varchar(15) DEFAULT NULL, 10 | `modsec` varchar(15) DEFAULT NULL, 11 | `apache` varchar(15) DEFAULT NULL, 12 | `apr` varchar(15) DEFAULT NULL, 13 | `apr_loaded` varchar(15) DEFAULT NULL, 14 | `pcre` varchar(15) DEFAULT NULL, 15 | `pcre_loaded` varchar(15) DEFAULT NULL, 16 | `lua` varchar(15) DEFAULT NULL, 17 | `lua_loaded` varchar(15) DEFAULT NULL, 18 | `libxml` varchar(15) DEFAULT NULL, 19 | `libxml_loaded` varchar(15) DEFAULT NULL, 20 | `well_formated` int(1) DEFAULT '1', 21 | `latitude` varchar(14) DEFAULT NULL, 22 | `longitude` varchar(14) DEFAULT NULL, 23 | `country` varchar(20) DEFAULT NULL, 24 | `city` varchar(20) DEFAULT NULL, 25 | `id` varchar(40) DEFAULT NULL 26 | ) ENGINE=MyISAM DEFAULT CHARSET=latin1; 27 | 28 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | ModSecurity status 3 | 4 | 5 | ModSecurity status is a mechanism to collect and display usage statistics of 6 | ModSecurity. It is divided into four main components: 7 | 8 | - WebSite: A rich html page that is responsable to show the utilization 9 | status in a user friendly way. 10 | 11 | - API: An api which is utilized by the WebSite component to retrieve 12 | data from our database. 13 | 14 | - Collector: Set of scripts responsable to parser tinydns log files and 15 | populate a database that can be comsumed by the API. 16 | 17 | - ModSecurity: Responsible to provide information about every instance of 18 | itself, using DNS queries. 19 | 20 | ModSecurity is not part of this repository, it is available under its own 21 | repository at: https://github.com/SpiderLabs/ModSecurity. The other three 22 | compoents lies on the same repository as that file. 23 | 24 | The WebSite and API componets are deployed under the official ModSecurity 25 | status page, public available at: 26 | 27 | http://status.modsecurity.org 28 | 29 | For more information about the stats mechanism, check ModSecurity wiki. 30 | FIXME: feed the wiki and place the url. 31 | 32 | -------------------------------------------------------------------------------- /static-html/modsec-status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |
21 | 0
46 |
'
100 | );
101 | $("#consoleLoading")
102 | .fadeTo("slow", 1, function () {});
103 | });
104 | }
105 |
106 | function hideLoading()
107 | {
108 | var data = new Date();
109 | $("#consoleLoading")
110 | .fadeTo("fast", 0, function ()
111 | {
112 | $("#consoleLoading")
113 | .empty();
114 | $("#consoleLoading")
115 | .append('Updated at ' + data.getHours() + ':' + data.getMinutes() +
116 | ':' + data.getSeconds());
117 | $("#consoleLoading")
118 | .fadeTo("fast", 1, function () {});
119 | });
120 | $("#legend")
121 | .fadeTo("slow", 1, function () {});
122 | }
123 |
124 | function fetchJsonData(from, to)
125 | {
126 | lastFetchedEpoch = to;
127 | showLoading();
128 | var unique = "";
129 |
130 | //console.log("fetch: " + from + " to: " + to);
131 | //console.log(api_uri + "/" + from + "/" + to + "");
132 | if (uniqueIds == true)
133 | {
134 | unique = "/unique/";
135 | }
136 | else
137 | {
138 | unique = "/";
139 | }
140 |
141 | var apiData = $.getJSON(api_uri + unique + "/" + from + "/" + to + "", function ()
142 | {
143 | console.log("success");
144 | })
145 | .done(function (json)
146 | {
147 | processJson(json);
148 | })
149 | .fail(function (jqxhr, textStatus, error)
150 | {
151 | var err = textStatus + ", " + error;
152 | console.log("Request Failed: " + err);
153 | })
154 | .always(function ()
155 | {
156 | console.log("complete");
157 | hideLoading();
158 | });
159 | }
160 |
161 | function redrawMap()
162 | {
163 | clearTimeout(redrawMapTimeout);
164 | redrawMapTimeout = setTimeout(function ()
165 | {
166 | redrawMapNow();
167 | }, 500);
168 | }
169 |
170 | function redrawMapNow(amount)
171 | {
172 | //console.log("Paiting map.");
173 | var pointArray = new google.maps.MVCArray();
174 | if (preciseOnly)
175 | {
176 | for (var i = 0; i < heatmapData.length; i++)
177 | if (heatmapData[i][1] != '')
178 | pointArray.push(heatmapData[i][0]);
179 | }
180 | else
181 | {
182 | for (var i = 0; i < heatmapData.length; i++)
183 | pointArray.push(heatmapData[i][0]);
184 | }
185 | if (!heatmap)
186 | {
187 | heatmap = new google.maps.visualization.HeatmapLayer(
188 | {
189 | data: pointArray,
190 | map: map
191 | });
192 | changeGradient();
193 | changeRadius();
194 | }
195 | else
196 | {
197 | heatmap.setData(pointArray);
198 | }
199 | $("#amountOfData")
200 | .fadeTo("slow", 0, function ()
201 | {
202 | $("#amountOfData")
203 | .empty();
204 | $("#amountOfData")
205 | .append("" + pointArray.getLength() +
206 | " initialization records");
207 | $("#amountOfData")
208 | .fadeTo("slow", 1, function () {});
209 | });
210 |
211 | uiValues[0] = lastFetchedEpoch - 1 * 24 * 60 * 60;
212 | uiValues[1] = lastFetchedEpoch;
213 |
214 | $("#slider-range")
215 | .slider(
216 | {
217 | values: [uiValues[0], uiValues[1]]
218 | })
219 | $("#range-first-date")
220 | .empty();
221 | f = new Date((lastFetchedEpoch - 1 * 24 * 60 * 60) * 1000);
222 | d = f.getFullYear() + "-" + f.getMonth() + "-" + f.getDay() + "' + item.ts + ' ModSecurity version ' + item.version + 252 | ' started at ' + item.dns_server.country + ' using ' + item.apache) 253 | var elem = document.getElementById('consoleContent'); 254 | elem.scrollTop = elem.scrollHeight; 255 | } 256 | 257 | function draw_on_map(item) 258 | { 259 | heatmapData.push([new google.maps.LatLng(item.dns_server.latitude, item.dns_server 260 | .longitude), item.dns_server.city]); 261 | } 262 | 263 | function showB(content) 264 | { 265 | if (content == null || content == '') 266 | { 267 | content = balloonDate; 268 | } 269 | $('#slider-handle') 270 | .showBalloon( 271 | { 272 | position: "top", 273 | contents: content, 274 | tipSize: 24 275 | }); 276 | } 277 | 278 | function hideB() 279 | { 280 | $('#slider-handle') 281 | .hideBalloon(); 282 | } 283 | 284 | function initialize() 285 | { 286 | // Style from: http://stackoverflow.com/questions/4003578/google-maps-in-grayscale 287 | var stylez = [ 288 | { 289 | featureType: "all", 290 | elementType: "all", 291 | stylers: [ 292 | { 293 | saturation: -70 294 | }] 295 | }]; 296 | var mapOptions = { 297 | zoom: 2, 298 | minZoom: 2, 299 | center: new google.maps.LatLng(0, 0), 300 | mapTypeIds: [google.maps.MapTypeId.ROADMAP, 'tehgrayz'] 301 | //disableDefaultUI: true 302 | }; 303 | map = new google.maps.Map(document.getElementById('map-canvas'), 304 | mapOptions); 305 | var mapType = new google.maps.StyledMapType(stylez, 306 | { 307 | name: "Grayscale" 308 | }); 309 | map.mapTypes.set('tehgrayz', mapType); 310 | map.setMapTypeId('tehgrayz'); 311 | $("#slider-range") 312 | .slider( 313 | { 314 | range: true, 315 | min: getEpoch() - (15 * 24 * 60 * 60), 316 | max: getEpoch(), 317 | values: [getEpoch() - (5 * 24 * 60 * 60), getEpoch()], 318 | step: 1, 319 | disabled: false, 320 | animate: "slow", 321 | start: function (event, ui) 322 | { 323 | hideB(); 324 | }, 325 | stop: function (event, ui) 326 | { 327 | $("#slider-range") 328 | .slider( 329 | { 330 | disabled: true 331 | }); 332 | 333 | showB("Time selection is not implemented yet..."); 334 | 335 | setTimeout(function () 336 | { 337 | hideB(); 338 | 339 | setTimeout(function () { 340 | var a = $("#slider-range") 341 | .slider( 342 | { 343 | values: [uiValues[0], uiValues[1]] 344 | }) 345 | }, 500); 346 | 347 | setTimeout(function () { 348 | showB(null); 349 | $("#slider-range") 350 | .slider( 351 | { 352 | disabled: false 353 | }); 354 | }, 900); 355 | }, 3000) 356 | return false; 357 | } 358 | }); 359 | 360 | /* Legend div */ 361 | var gradientPrefix = getCssValuePrefix('backgroundImage', 362 | 'linear-gradient(left, #fff, #fff)'); 363 | var back = gradientPrefix + 'linear-gradient(' + 'left'; 364 | for (var i = 0; i < gradient.length; i++) 365 | { 366 | back = back + "," + gradient[i] + " " + (100 * (i + 1)) / gradient.length + 367 | "%"; 368 | } 369 | back = back + ")"; 370 | 371 | document.getElementById('colorBar') 372 | .style.backgroundImage = back; 373 | 374 | f = new Date(firstQueryEver * 1000); 375 | d = f.getFullYear() + "-" + f.getMonth() + "-" + f.getDay(); 376 | $('#first-register') 377 | .empty(); 378 | $('#first-register') 379 | .append(d); 380 | fetchJsonData(0, getEpoch()); 381 | $('#max') 382 | .append('??'); 383 | 384 | 385 | $('#ignore').click(function () { 386 | setPreciseOnly(); 387 | }); 388 | $('#uniq').click(function () { 389 | setUniqueIds(); 390 | }); 391 | 392 | } 393 | 394 | function getEpoch() 395 | { 396 | return Math.round(new Date() 397 | .getTime() / 1000); 398 | } 399 | 400 | function setPreciseOnly() 401 | { 402 | showLoading(); 403 | preciseOnly = preciseOnly ? false : true; 404 | redrawMap(); 405 | } 406 | 407 | function setUniqueIds() 408 | { 409 | clearTimeout(updateTimeOut); 410 | showLoading(); 411 | heatmapData = []; 412 | uniqueIds = uniqueIds ? false : true; 413 | fetchJsonData(firstQueryEver, getEpoch()); 414 | } 415 | 416 | function toggleHeatmap() 417 | { 418 | heatmap.setMap(heatmap.getMap() ? null : map); 419 | } 420 | 421 | function changeGradient() 422 | { 423 | heatmap.set('gradient', heatmap.get('gradient') ? null : gradient); 424 | } 425 | 426 | function changeRadius() 427 | { 428 | heatmap.set('radius', heatmap.get('radius') ? null : 10); 429 | } 430 | 431 | function changeOpacity() 432 | { 433 | heatmap.set('opacity', heatmap.get('opacity') ? null : 0.2); 434 | } 435 | 436 | 437 | google.maps.event.addDomListener(window, 'load', initialize); 438 | -------------------------------------------------------------------------------- /static-html/jquery.balloon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hover balloon on elements without css and images. 3 | * 4 | * Copyright (c) 2011 Hayato Takenaka 5 | * Dual licensed under the MIT and GPL licenses: 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * http://www.gnu.org/licenses/gpl.html 8 | * @author: Hayato Takenaka (http://urin.take-uma.net) 9 | * @version: 0.3.0 - 2012/02/25 10 | **/ 11 | ;(function($) { 12 | //----------------------------------------------------------------------------- 13 | // Private 14 | //----------------------------------------------------------------------------- 15 | // Helper for meta programming 16 | var Meta = {}; 17 | Meta.pos = $.extend(["top", "bottom", "left", "right"], {camel: ["Top", "Bottom", "Left", "Right"]}); 18 | Meta.size = $.extend(["height", "width"], {camel: ["Height", "Width"]}); 19 | Meta.getRelativeNames = function(position) { 20 | var idx = { 21 | pos: { 22 | o: position, // origin 23 | f: (position % 2 == 0) ? position + 1 : position - 1, // faced 24 | p1: (position % 2 == 0) ? position : position - 1, 25 | p2: (position % 2 == 0) ? position + 1 : position, 26 | c1: (position < 2) ? 2 : 0, 27 | c2: (position < 2) ? 3 : 1 28 | }, 29 | size: { 30 | p: (position < 2) ? 0 : 1, // parallel 31 | c: (position < 2) ? 1 : 0 // cross 32 | } 33 | }; 34 | var names = {}; 35 | for(var m1 in idx) { 36 | if(!names[m1]) names[m1] = {}; 37 | for(var m2 in idx[m1]) { 38 | names[m1][m2] = Meta[m1][idx[m1][m2]]; 39 | if(!names.camel) names.camel = {}; 40 | if(!names.camel[m1]) names.camel[m1] = {}; 41 | names.camel[m1][m2] = Meta[m1].camel[idx[m1][m2]]; 42 | } 43 | } 44 | names.isTopLeft = (names.pos.o == names.pos.p1); 45 | return names; 46 | }; 47 | 48 | // Helper class to handle position and size as numerical pixels. 49 | function NumericalBoxElement() { this.initialize.apply(this, arguments); } 50 | (function() { 51 | // Method factories 52 | var Methods = { 53 | setBorder: function(pos, isVertical) { 54 | return function(value) { 55 | this.$.css("border-" + pos.toLowerCase() + "-width", value + "px"); 56 | this["border" + pos] = value; 57 | return (this.isActive) ? digitalize(this, isVertical) : this; 58 | } 59 | }, 60 | setPosition: function(pos, isVertical) { 61 | return function(value) { 62 | this.$.css(pos.toLowerCase(), value + "px"); 63 | this[pos.toLowerCase()] = value; 64 | return (this.isActive) ? digitalize(this, isVertical) : this; 65 | } 66 | } 67 | }; 68 | 69 | NumericalBoxElement.prototype = { 70 | initialize: function($element) { 71 | this.$ = $element; 72 | $.extend(true, this, this.$.offset(), {center: {}, inner: {center: {}}}); 73 | for(var i = 0; i < Meta.pos.length; i++) { 74 | this["border" + Meta.pos.camel[i]] = parseInt(this.$.css("border-" + Meta.pos[i] + "-width")) || 0; 75 | } 76 | this.active(); 77 | }, 78 | active: function() { this.isActive = true; digitalize(this); return this; }, 79 | inactive: function() { this.isActive = false; return this; } 80 | }; 81 | for(var i = 0; i < Meta.pos.length; i++) { 82 | NumericalBoxElement.prototype["setBorder" + Meta.pos.camel[i]] = Methods.setBorder(Meta.pos.camel[i], (i < 2)); 83 | if(i % 2 == 0) 84 | NumericalBoxElement.prototype["set" + Meta.pos.camel[i]] = Methods.setPosition(Meta.pos.camel[i], (i < 2)); 85 | } 86 | 87 | function digitalize(box, isVertical) { 88 | if(isVertical == undefined) { digitalize(box, true); return digitalize(box, false); } 89 | var m = Meta.getRelativeNames((isVertical) ? 0 : 2); 90 | box[m.size.p] = box.$["outer" + m.camel.size.p](); 91 | box[m.pos.f] = box[m.pos.o] + box[m.size.p]; 92 | box.center[m.pos.o] = box[m.pos.o] + box[m.size.p] / 2; 93 | box.inner[m.pos.o] = box[m.pos.o] + box["border" + m.camel.pos.o]; 94 | box.inner[m.size.p] = box.$["inner" + m.camel.size.p](); 95 | box.inner[m.pos.f] = box.inner[m.pos.o] + box.inner[m.size.p]; 96 | box.inner.center[m.pos.o] = box.inner[m.pos.f] + box.inner[m.size.p] / 2; 97 | return box; 98 | } 99 | })(); 100 | 101 | // Adjust position of balloon body 102 | function makeupBalloon($target, $balloon, options) { 103 | $balloon.stop(true, true); 104 | var outerTip, innerTip, 105 | initTipStyle = {position: "absolute", height: "0", width: "0", border: "solid 0 transparent"}, 106 | target = new NumericalBoxElement($target), 107 | balloon = new NumericalBoxElement($balloon); 108 | balloon.setTop(-options.offsetY 109 | + ((options.position && options.position.indexOf("top") >= 0) ? target.top - balloon.height 110 | : ((options.position && options.position.indexOf("bottom") >= 0) ? target.bottom 111 | : target.center.top - balloon.height / 2))); 112 | balloon.setLeft(options.offsetX 113 | + ((options.position && options.position.indexOf("left") >= 0) ? target.left - balloon.width 114 | : ((options.position && options.position.indexOf("right") >= 0) ? target.right 115 | : target.center.left - balloon.width / 2))); 116 | if(options.tipSize > 0) { 117 | // Add hidden balloon tips into balloon body. 118 | if($balloon.data("outerTip")) { $balloon.data("outerTip").remove(); $balloon.removeData("outerTip"); } 119 | if($balloon.data("innerTip")) { $balloon.data("innerTip").remove(); $balloon.removeData("innerTip"); } 120 | outerTip = new NumericalBoxElement($("