[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 /** Highest positive signed 32-bit float value */ 2 3 const maxInt = 2147483647; // aka. 0x7FFFFFFF or 2^31-1 4 5 /** Bootstring parameters */ 6 7 const base = 36; 8 const tMin = 1; 9 const tMax = 26; 10 const skew = 38; 11 const damp = 700; 12 const initialBias = 72; 13 const initialN = 128; // 0x80 14 15 const delimiter = '-'; // '\x2D' 16 17 /** Regular expressions */ 18 19 const regexPunycode = /^xn--/; 20 const regexNonASCII = /[^\0-\x7E]/; // non-ASCII chars 21 22 const regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g; // RFC 3490 separators 23 24 /** Error messages */ 25 26 const errors = { 27 'overflow': 'Overflow: input needs wider integers to process', 28 'not-basic': 'Illegal input >= 0x80 (not a basic code point)', 29 'invalid-input': 'Invalid input' 30 }; 31 /** Convenience shortcuts */ 32 33 const baseMinusTMin = base - tMin; 34 const floor = Math.floor; 35 const stringFromCharCode = String.fromCharCode; 36 /*--------------------------------------------------------------------------*/ 37 38 /** 39 * A generic error utility function. 40 * @private 41 * @param {String} type The error type. 42 * @returns {Error} Throws a `RangeError` with the applicable error message. 43 */ 44 45 function error(type) { 46 throw new RangeError(errors[type]); 47 } 48 /** 49 * A generic `Array#map` utility function. 50 * @private 51 * @param {Array} array The array to iterate over. 52 * @param {Function} callback The function that gets called for every array 53 * item. 54 * @returns {Array} A new array of values returned by the callback function. 55 */ 56 57 58 function map(array, fn) { 59 const result = []; 60 let length = array.length; 61 62 while (length--) { 63 result[length] = fn(array[length]); 64 } 65 66 return result; 67 } 68 /** 69 * A simple `Array#map`-like wrapper to work with domain name strings or email 70 * addresses. 71 * @private 72 * @param {String} domain The domain name or email address. 73 * @param {Function} callback The function that gets called for every 74 * character. 75 * @returns {Array} A new string of characters returned by the callback 76 * function. 77 */ 78 79 80 function mapDomain(string, fn) { 81 const parts = string.split('@'); 82 let result = ''; 83 84 if (parts.length > 1) { 85 // In email addresses, only the domain name should be punycoded. Leave 86 // the local part (i.e. everything up to `@`) intact. 87 result = parts[0] + '@'; 88 string = parts[1]; 89 } // Avoid `split(regex)` for IE8 compatibility. See #17. 90 91 92 string = string.replace(regexSeparators, '\x2E'); 93 const labels = string.split('.'); 94 const encoded = map(labels, fn).join('.'); 95 return result + encoded; 96 } 97 /** 98 * Creates an array containing the numeric code points of each Unicode 99 * character in the string. While JavaScript uses UCS-2 internally, 100 * this function will convert a pair of surrogate halves (each of which 101 * UCS-2 exposes as separate characters) into a single code point, 102 * matching UTF-16. 103 * @see `punycode.ucs2.encode` 104 * @see <https://mathiasbynens.be/notes/javascript-encoding> 105 * @memberOf punycode.ucs2 106 * @name decode 107 * @param {String} string The Unicode input string (UCS-2). 108 * @returns {Array} The new array of code points. 109 */ 110 111 112 function ucs2decode(string) { 113 const output = []; 114 let counter = 0; 115 const length = string.length; 116 117 while (counter < length) { 118 const value = string.charCodeAt(counter++); 119 120 if (value >= 0xD800 && value <= 0xDBFF && counter < length) { 121 // It's a high surrogate, and there is a next character. 122 const extra = string.charCodeAt(counter++); 123 124 if ((extra & 0xFC00) == 0xDC00) { 125 // Low surrogate. 126 output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); 127 } else { 128 // It's an unmatched surrogate; only append this code unit, in case the 129 // next code unit is the high surrogate of a surrogate pair. 130 output.push(value); 131 counter--; 132 } 133 } else { 134 output.push(value); 135 } 136 } 137 138 return output; 139 } 140 /** 141 * Creates a string based on an array of numeric code points. 142 * @see `punycode.ucs2.decode` 143 * @memberOf punycode.ucs2 144 * @name encode 145 * @param {Array} codePoints The array of numeric code points. 146 * @returns {String} The new Unicode string (UCS-2). 147 */ 148 149 150 const ucs2encode = array => String.fromCodePoint(...array); 151 /** 152 * Converts a basic code point into a digit/integer. 153 * @see `digitToBasic()` 154 * @private 155 * @param {Number} codePoint The basic numeric code point value. 156 * @returns {Number} The numeric value of a basic code point (for use in 157 * representing integers) in the range `0` to `base - 1`, or `base` if 158 * the code point does not represent a value. 159 */ 160 161 162 const basicToDigit = function (codePoint) { 163 if (codePoint - 0x30 < 0x0A) { 164 return codePoint - 0x16; 165 } 166 167 if (codePoint - 0x41 < 0x1A) { 168 return codePoint - 0x41; 169 } 170 171 if (codePoint - 0x61 < 0x1A) { 172 return codePoint - 0x61; 173 } 174 175 return base; 176 }; 177 /** 178 * Converts a digit/integer into a basic code point. 179 * @see `basicToDigit()` 180 * @private 181 * @param {Number} digit The numeric value of a basic code point. 182 * @returns {Number} The basic code point whose value (when used for 183 * representing integers) is `digit`, which needs to be in the range 184 * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is 185 * used; else, the lowercase form is used. The behavior is undefined 186 * if `flag` is non-zero and `digit` has no uppercase form. 187 */ 188 189 190 const digitToBasic = function (digit, flag) { 191 // 0..25 map to ASCII a..z or A..Z 192 // 26..35 map to ASCII 0..9 193 return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5); 194 }; 195 /** 196 * Bias adaptation function as per section 3.4 of RFC 3492. 197 * https://tools.ietf.org/html/rfc3492#section-3.4 198 * @private 199 */ 200 201 202 const adapt = function (delta, numPoints, firstTime) { 203 let k = 0; 204 delta = firstTime ? floor(delta / damp) : delta >> 1; 205 delta += floor(delta / numPoints); 206 207 for (; delta > baseMinusTMin * tMax >> 1; k += base) { 208 delta = floor(delta / baseMinusTMin); 209 } 210 211 return floor(k + (baseMinusTMin + 1) * delta / (delta + skew)); 212 }; 213 /** 214 * Converts a Punycode string of ASCII-only symbols to a string of Unicode 215 * symbols. 216 * @memberOf punycode 217 * @param {String} input The Punycode string of ASCII-only symbols. 218 * @returns {String} The resulting string of Unicode symbols. 219 */ 220 221 222 const decode = function (input) { 223 // Don't use UCS-2. 224 const output = []; 225 const inputLength = input.length; 226 let i = 0; 227 let n = initialN; 228 let bias = initialBias; // Handle the basic code points: let `basic` be the number of input code 229 // points before the last delimiter, or `0` if there is none, then copy 230 // the first basic code points to the output. 231 232 let basic = input.lastIndexOf(delimiter); 233 234 if (basic < 0) { 235 basic = 0; 236 } 237 238 for (let j = 0; j < basic; ++j) { 239 // if it's not a basic code point 240 if (input.charCodeAt(j) >= 0x80) { 241 error('not-basic'); 242 } 243 244 output.push(input.charCodeAt(j)); 245 } // Main decoding loop: start just after the last delimiter if any basic code 246 // points were copied; start at the beginning otherwise. 247 248 249 for (let index = basic > 0 ? basic + 1 : 0; index < inputLength;) { 250 // `index` is the index of the next character to be consumed. 251 // Decode a generalized variable-length integer into `delta`, 252 // which gets added to `i`. The overflow checking is easier 253 // if we increase `i` as we go, then subtract off its starting 254 // value at the end to obtain `delta`. 255 let oldi = i; 256 257 for (let w = 1, k = base;; k += base) { 258 if (index >= inputLength) { 259 error('invalid-input'); 260 } 261 262 const digit = basicToDigit(input.charCodeAt(index++)); 263 264 if (digit >= base || digit > floor((maxInt - i) / w)) { 265 error('overflow'); 266 } 267 268 i += digit * w; 269 const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias; 270 271 if (digit < t) { 272 break; 273 } 274 275 const baseMinusT = base - t; 276 277 if (w > floor(maxInt / baseMinusT)) { 278 error('overflow'); 279 } 280 281 w *= baseMinusT; 282 } 283 284 const out = output.length + 1; 285 bias = adapt(i - oldi, out, oldi == 0); // `i` was supposed to wrap around from `out` to `0`, 286 // incrementing `n` each time, so we'll fix that now: 287 288 if (floor(i / out) > maxInt - n) { 289 error('overflow'); 290 } 291 292 n += floor(i / out); 293 i %= out; // Insert `n` at position `i` of the output. 294 295 output.splice(i++, 0, n); 296 } 297 298 return String.fromCodePoint(...output); 299 }; 300 /** 301 * Converts a string of Unicode symbols (e.g. a domain name label) to a 302 * Punycode string of ASCII-only symbols. 303 * @memberOf punycode 304 * @param {String} input The string of Unicode symbols. 305 * @returns {String} The resulting Punycode string of ASCII-only symbols. 306 */ 307 308 309 const encode = function (input) { 310 const output = []; // Convert the input in UCS-2 to an array of Unicode code points. 311 312 input = ucs2decode(input); // Cache the length. 313 314 let inputLength = input.length; // Initialize the state. 315 316 let n = initialN; 317 let delta = 0; 318 let bias = initialBias; // Handle the basic code points. 319 320 for (const currentValue of input) { 321 if (currentValue < 0x80) { 322 output.push(stringFromCharCode(currentValue)); 323 } 324 } 325 326 let basicLength = output.length; 327 let handledCPCount = basicLength; // `handledCPCount` is the number of code points that have been handled; 328 // `basicLength` is the number of basic code points. 329 // Finish the basic string with a delimiter unless it's empty. 330 331 if (basicLength) { 332 output.push(delimiter); 333 } // Main encoding loop: 334 335 336 while (handledCPCount < inputLength) { 337 // All non-basic code points < n have been handled already. Find the next 338 // larger one: 339 let m = maxInt; 340 341 for (const currentValue of input) { 342 if (currentValue >= n && currentValue < m) { 343 m = currentValue; 344 } 345 } // Increase `delta` enough to advance the decoder's <n,i> state to <m,0>, 346 // but guard against overflow. 347 348 349 const handledCPCountPlusOne = handledCPCount + 1; 350 351 if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) { 352 error('overflow'); 353 } 354 355 delta += (m - n) * handledCPCountPlusOne; 356 n = m; 357 358 for (const currentValue of input) { 359 if (currentValue < n && ++delta > maxInt) { 360 error('overflow'); 361 } 362 363 if (currentValue == n) { 364 // Represent delta as a generalized variable-length integer. 365 let q = delta; 366 367 for (let k = base;; k += base) { 368 const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias; 369 370 if (q < t) { 371 break; 372 } 373 374 const qMinusT = q - t; 375 const baseMinusT = base - t; 376 output.push(stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0))); 377 q = floor(qMinusT / baseMinusT); 378 } 379 380 output.push(stringFromCharCode(digitToBasic(q, 0))); 381 bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength); 382 delta = 0; 383 ++handledCPCount; 384 } 385 } 386 387 ++delta; 388 ++n; 389 } 390 391 return output.join(''); 392 }; 393 /** 394 * Converts a Punycode string representing a domain name or an email address 395 * to Unicode. Only the Punycoded parts of the input will be converted, i.e. 396 * it doesn't matter if you call it on a string that has already been 397 * converted to Unicode. 398 * @memberOf punycode 399 * @param {String} input The Punycoded domain name or email address to 400 * convert to Unicode. 401 * @returns {String} The Unicode representation of the given Punycode 402 * string. 403 */ 404 405 406 const toUnicode = function (input) { 407 return mapDomain(input, function (string) { 408 return regexPunycode.test(string) ? decode(string.slice(4).toLowerCase()) : string; 409 }); 410 }; 411 /** 412 * Converts a Unicode string representing a domain name or an email address to 413 * Punycode. Only the non-ASCII parts of the domain name will be converted, 414 * i.e. it doesn't matter if you call it with a domain that's already in 415 * ASCII. 416 * @memberOf punycode 417 * @param {String} input The domain name or email address to convert, as a 418 * Unicode string. 419 * @returns {String} The Punycode representation of the given domain name or 420 * email address. 421 */ 422 423 424 const toASCII = function (input) { 425 return mapDomain(input, function (string) { 426 return regexNonASCII.test(string) ? 'xn--' + encode(string) : string; 427 }); 428 }; 429 /*--------------------------------------------------------------------------*/ 430 431 /** Define the public API */ 432 433 434 const punycode = { 435 /** 436 * A string representing the current Punycode.js version number. 437 * @memberOf punycode 438 * @type String 439 */ 440 'version': '2.1.0', 441 442 /** 443 * An object of methods to convert from JavaScript's internal character 444 * representation (UCS-2) to Unicode code points, and back. 445 * @see <https://mathiasbynens.be/notes/javascript-encoding> 446 * @memberOf punycode 447 * @type Object 448 */ 449 'ucs2': { 450 'decode': ucs2decode, 451 'encode': ucs2encode 452 }, 453 'decode': decode, 454 'encode': encode, 455 'toASCII': toASCII, 456 'toUnicode': toUnicode 457 }; 458 459 /** 460 * @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org> 461 * @license GNU General Public License version 2 or later; see LICENSE.txt 462 */ 463 464 class JFormValidator { 465 constructor() { 466 this.customValidators = {}; 467 this.handlers = []; 468 this.handlers = {}; 469 this.removeMarking = this.removeMarking.bind(this); 470 471 this.inputEmail = () => { 472 const input = document.createElement('input'); 473 input.setAttribute('type', 'email'); 474 return input.type !== 'text'; 475 }; // Default handlers 476 477 478 this.setHandler('username', value => { 479 const regex = /[<|>|"|'|%|;|(|)|&]/i; 480 return !regex.test(value); 481 }); 482 this.setHandler('password', value => { 483 const regex = /^\S[\S ]{2,98}\S$/; 484 return regex.test(value); 485 }); 486 this.setHandler('numeric', value => { 487 const regex = /^(\d|-)?(\d|,)*\.?\d*$/; 488 return regex.test(value); 489 }); 490 this.setHandler('email', value => { 491 const newValue = punycode.toASCII(value); 492 const regex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; 493 return regex.test(newValue); 494 }); // Attach all forms with a class 'form-validate' 495 496 const forms = [].slice.call(document.querySelectorAll('form')); 497 forms.forEach(form => { 498 if (form.classList.contains('form-validate')) { 499 this.attachToForm(form); 500 } 501 }); 502 } 503 504 get custom() { 505 return this.customValidators; 506 } 507 508 set custom(value) { 509 this.customValidators = value; 510 } 511 512 setHandler(name, func, en) { 513 const isEnabled = en === '' ? true : en; 514 this.handlers[name] = { 515 enabled: isEnabled, 516 exec: func 517 }; 518 } // eslint-disable-next-line class-methods-use-this 519 520 521 markValid(element) { 522 // Get a label 523 const label = element.form.querySelector(`label[for="$element.id}"]`); 524 let message; 525 526 if (element.classList.contains('required') || element.getAttribute('required')) { 527 if (label) { 528 message = label.querySelector('span.form-control-feedback'); 529 } 530 } 531 532 element.classList.remove('form-control-danger'); 533 element.classList.remove('invalid'); 534 element.classList.add('form-control-success'); 535 element.parentNode.classList.remove('has-danger'); 536 element.parentNode.classList.add('has-success'); 537 element.setAttribute('aria-invalid', 'false'); // Remove message 538 539 if (message) { 540 message.parentNode.removeChild(message); 541 } // Restore Label 542 543 544 if (label) { 545 label.classList.remove('invalid'); 546 } 547 } // eslint-disable-next-line class-methods-use-this 548 549 550 markInvalid(element, empty) { 551 // Get a label 552 const label = element.form.querySelector(`label[for="$element.id}"]`); 553 element.classList.remove('form-control-success'); 554 element.classList.remove('valid'); 555 element.classList.add('form-control-danger'); 556 element.classList.add('invalid'); 557 element.parentNode.classList.remove('has-success'); 558 element.parentNode.classList.add('has-danger'); 559 element.setAttribute('aria-invalid', 'true'); // Display custom message 560 561 let mesgCont; 562 const message = element.getAttribute('data-validation-text'); 563 564 if (label) { 565 mesgCont = label.querySelector('span.form-control-feedback'); 566 } 567 568 if (!mesgCont) { 569 const elMsg = document.createElement('span'); 570 elMsg.classList.add('form-control-feedback'); 571 572 if (empty && empty === 'checkbox') { 573 elMsg.innerHTML = message !== null ? Joomla.sanitizeHtml(message) : Joomla.sanitizeHtml(Joomla.Text._('JLIB_FORM_FIELD_REQUIRED_CHECK')); 574 } else if (empty && empty === 'value') { 575 elMsg.innerHTML = message !== null ? Joomla.sanitizeHtml(message) : Joomla.sanitizeHtml(Joomla.Text._('JLIB_FORM_FIELD_REQUIRED_VALUE')); 576 } else { 577 elMsg.innerHTML = message !== null ? Joomla.sanitizeHtml(message) : Joomla.sanitizeHtml(Joomla.Text._('JLIB_FORM_FIELD_INVALID_VALUE')); 578 } 579 580 if (label) { 581 label.appendChild(elMsg); 582 } 583 } // Mark the Label as well 584 585 586 if (label) { 587 label.classList.add('invalid'); 588 } 589 } // eslint-disable-next-line class-methods-use-this 590 591 592 removeMarking(element) { 593 // Get the associated label 594 let message; 595 const label = element.form.querySelector(`label[for="$element.id}"]`); 596 597 if (label) { 598 message = label.querySelector('span.form-control-feedback'); 599 } 600 601 element.classList.remove('form-control-danger'); 602 element.classList.remove('form-control-success'); 603 element.classList.remove('invalid'); 604 element.classList.add('valid'); 605 element.parentNode.classList.remove('has-danger'); 606 element.parentNode.classList.remove('has-success'); // Remove message 607 608 if (message) { 609 if (label) { 610 label.removeChild(message); 611 } 612 } // Restore Label 613 614 615 if (label) { 616 label.classList.remove('invalid'); 617 } 618 } 619 620 handleResponse(state, element, empty) { 621 const tagName = element.tagName.toLowerCase(); // Set the element and its label (if exists) invalid state 622 623 if (tagName !== 'button' && element.value !== undefined || tagName === 'fieldset') { 624 if (state === false) { 625 this.markInvalid(element, empty); 626 } else { 627 this.markValid(element); 628 } 629 } 630 } 631 632 validate(element) { 633 let tagName; // Ignore the element if its currently disabled, 634 // because are not submitted for the http-request. 635 // For those case return always true. 636 637 if (element.getAttribute('disabled') === 'disabled' || element.getAttribute('display') === 'none') { 638 this.handleResponse(true, element); 639 return true; 640 } // If the field is required make sure it has a value 641 642 643 if (element.getAttribute('required') || element.classList.contains('required')) { 644 tagName = element.tagName.toLowerCase(); 645 646 if (tagName === 'fieldset' && (element.classList.contains('radio') || element.classList.contains('checkboxes'))) { 647 // No options are checked. 648 if (element.querySelector('input:checked') === null) { 649 this.handleResponse(false, element, 'checkbox'); 650 return false; 651 } 652 } else if (element.getAttribute('type') === 'checkbox' && element.checked !== true || tagName === 'select' && !element.value.length) { 653 this.handleResponse(false, element, 'checkbox'); 654 return false; 655 } else if (!element.value || element.classList.contains('placeholder')) { 656 // If element has class placeholder that means it is empty. 657 this.handleResponse(false, element, 'value'); 658 return false; 659 } 660 } // Only validate the field if the validate class is set 661 662 663 const handler = element.getAttribute('class') && element.getAttribute('class').match(/validate-([a-zA-Z0-9_-]+)/) ? element.getAttribute('class').match(/validate-([a-zA-Z0-9_-]+)/)[1] : ''; 664 665 if (element.getAttribute('pattern') && element.getAttribute('pattern') !== '') { 666 if (element.value.length) { 667 const isValid = new RegExp(`^$element.getAttribute('pattern')}$`).test(element.value); 668 this.handleResponse(isValid, element, 'empty'); 669 return isValid; 670 } 671 672 if (element.hasAttribute('required') || element.classList.contains('required')) { 673 this.handleResponse(false, element, 'empty'); 674 return false; 675 } 676 677 this.handleResponse(true, element); 678 return true; 679 } 680 681 if (handler === '') { 682 this.handleResponse(true, element); 683 return true; 684 } // Check the additional validation types 685 686 687 if (handler && handler !== 'none' && this.handlers[handler] && element.value) { 688 // Execute the validation handler and return result 689 if (this.handlers[handler].exec(element.value, element) !== true) { 690 this.handleResponse(false, element, 'invalid_value'); 691 return false; 692 } 693 } // Return validation state 694 695 696 this.handleResponse(true, element); 697 return true; 698 } 699 700 isValid(form) { 701 let valid = true; 702 let message; 703 let error; 704 const invalid = []; // Validate form fields 705 706 const fields = [].slice.call(form.querySelectorAll('input, textarea, select, button, fieldset')); 707 fields.forEach(field => { 708 if (this.validate(field) === false) { 709 valid = false; 710 invalid.push(field); 711 } 712 }); // Run custom form validators if present 713 714 if (Object.keys(this.customValidators).length) { 715 Object.keys(this.customValidators).foreach(key => { 716 if (this.customValidators[key].exec() !== true) { 717 valid = false; 718 } 719 }); 720 } 721 722 if (!valid && invalid.length > 0) { 723 if (form.getAttribute('data-validation-text')) { 724 message = form.getAttribute('data-validation-text'); 725 } else { 726 message = Joomla.Text._('JLIB_FORM_CONTAINS_INVALID_FIELDS'); 727 } 728 729 error = { 730 error: [message] 731 }; 732 Joomla.renderMessages(error); 733 } 734 735 return valid; 736 } 737 738 attachToForm(form) { 739 const elements = [].slice.call(form.querySelectorAll('input, textarea, select, button, fieldset')); // Iterate through the form object and attach the validate method to all input fields. 740 741 elements.forEach(element => { 742 const tagName = element.tagName.toLowerCase(); 743 744 if (['input', 'textarea', 'select', 'fieldset'].indexOf(tagName) > -1 && element.classList.contains('required')) { 745 element.setAttribute('required', ''); 746 } // Attach isValid method to submit button 747 748 749 if ((tagName === 'input' || tagName === 'button') && (element.getAttribute('type') === 'submit' || element.getAttribute('type') === 'image')) { 750 if (element.classList.contains('validate')) { 751 element.addEventListener('click', () => this.isValid(form)); 752 } 753 } else if (tagName !== 'button' && !(tagName === 'input' && element.getAttribute('type') === 'button')) { 754 // Attach validate method only to fields 755 if (tagName !== 'fieldset') { 756 element.addEventListener('blur', ({ 757 target 758 }) => this.validate(target)); 759 element.addEventListener('focus', ({ 760 target 761 }) => this.removeMarking(target)); 762 763 if (element.classList.contains('validate-email') && this.inputEmail) { 764 element.setAttribute('type', 'email'); 765 } 766 } 767 } 768 }); 769 } 770 771 } 772 773 const initialize = () => { 774 document.formvalidator = new JFormValidator(); // Cleanup 775 776 document.removeEventListener('DOMContentLoaded', initialize); 777 }; 778 779 document.addEventListener('DOMContentLoaded', initialize);
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |