[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 /** 2 * @copyright (C) 2012 Open Source Matters, Inc. <https://www.joomla.org> 3 * @license GNU General Public License version 2 or later; see LICENSE.txt 4 */ 5 Joomla = window.Joomla || {}; 6 7 ((Joomla, document) => { 8 9 Joomla.submitbuttonUpload = () => { 10 const form = document.getElementById('uploadForm'); 11 const confirmBackup = document.getElementById('joomlaupdate-confirm-backup'); // do field validation 12 13 if (form.install_package.value === '') { 14 alert(Joomla.Text._('COM_INSTALLER_MSG_INSTALL_PLEASE_SELECT_A_PACKAGE'), true); 15 } else if (form.install_package.files[0].size > form.max_upload_size.value) { 16 alert(Joomla.Text._('COM_INSTALLER_MSG_WARNINGS_UPLOADFILETOOBIG'), true); 17 } else if (confirmBackup && confirmBackup.checked) { 18 form.submit(); 19 } 20 }; 21 22 Joomla.installpackageChange = () => { 23 const form = document.getElementById('uploadForm'); 24 const fileSize = form.install_package.files[0].size; 25 const fileSizeMB = fileSize * 1.0 / 1024.0 / 1024.0; 26 const fileSizeElement = document.getElementById('file_size'); 27 const warningElement = document.getElementById('max_upload_size_warn'); 28 29 if (form.install_package.value === '') { 30 fileSizeElement.classList.add('hidden'); 31 warningElement.classList.add('hidden'); 32 } else if (fileSize) { 33 fileSizeElement.classList.remove('hidden'); 34 fileSizeElement.innerHTML = Joomla.sanitizeHtml(Joomla.Text._('JGLOBAL_SELECTED_UPLOAD_FILE_SIZE').replace('%s', `$fileSizeMB.toFixed(2)} MB`)); 35 36 if (fileSize > form.max_upload_size.value) { 37 warningElement.classList.remove('hidden'); 38 } else { 39 warningElement.classList.add('hidden'); 40 } 41 } 42 }; 43 44 document.addEventListener('DOMContentLoaded', () => { 45 const confirmButton = document.getElementById('confirmButton'); 46 const uploadForm = document.getElementById('uploadForm'); 47 const uploadButton = document.getElementById('uploadButton'); 48 const uploadField = document.getElementById('install_package'); 49 const installButton = document.querySelector('.emptystate-btnadd', document.getElementById('joomlaupdate-wrapper')); 50 const updateCheck = document.getElementById('joomlaupdate-confirm-backup'); 51 const form = installButton ? installButton.closest('form') : null; 52 const task = form ? form.querySelector('[name=task]', form) : null; 53 54 if (uploadButton) { 55 uploadButton.addEventListener('click', Joomla.submitbuttonUpload); 56 uploadButton.disabled = updateCheck && !updateCheck.checked; 57 58 if (updateCheck) { 59 updateCheck.addEventListener('change', () => { 60 uploadButton.disabled = !updateCheck.checked; 61 }); 62 } 63 } 64 65 if (confirmButton && updateCheck && !updateCheck.checked) { 66 confirmButton.classList.add('disabled'); 67 } 68 69 if (confirmButton && updateCheck) { 70 updateCheck.addEventListener('change', () => { 71 if (updateCheck.checked) { 72 confirmButton.classList.remove('disabled'); 73 } else { 74 confirmButton.classList.add('disabled'); 75 } 76 }); 77 } 78 79 if (uploadField) { 80 uploadField.addEventListener('change', Joomla.installpackageChange); 81 82 if (updateCheck) { 83 uploadField.addEventListener('change', () => { 84 const fileSize = uploadForm.install_package.files[0].size; 85 const allowedSize = uploadForm.max_upload_size.value; 86 87 if (fileSize <= allowedSize && updateCheck.disabled) { 88 updateCheck.disabled = !updateCheck.disabled; 89 } else if (fileSize <= allowedSize && !updateCheck.disabled && !updateCheck.checked) { 90 updateCheck.disabled = false; 91 } else if (fileSize <= allowedSize && updateCheck.checked) { 92 updateCheck.checked = updateCheck.classList.contains('d-none'); 93 uploadButton.disabled = true; 94 } else if (fileSize > allowedSize && !updateCheck.disabled) { 95 updateCheck.disabled = !updateCheck.disabled; 96 updateCheck.checked = updateCheck.classList.contains('d-none'); 97 uploadButton.disabled = true; 98 } 99 }); 100 } 101 } // Trigger (re-) install (including checkbox confirm if we update) 102 103 104 if (installButton && installButton.getAttribute('href') === '#' && task) { 105 installButton.addEventListener('click', e => { 106 e.preventDefault(); 107 108 if (updateCheck && !updateCheck.checked) { 109 return; 110 } 111 112 task.value = 'update.download'; 113 form.submit(); 114 }); 115 } 116 }); 117 })(Joomla, document); 118 119 ((Joomla, document) => { 120 /** 121 * PreUpdateChecker 122 * 123 * @type {Object} 124 */ 125 const PreUpdateChecker = {}; 126 /** 127 * Config object 128 * 129 * @type {{serverUrl: string, selector: string}} 130 */ 131 132 PreUpdateChecker.config = { 133 serverUrl: 'index.php?option=com_joomlaupdate&task=update.fetchextensioncompatibility', 134 batchUrl: 'index.php?option=com_joomlaupdate&task=update.batchextensioncompatibility', 135 selector: '.extension-check' 136 }; 137 /** 138 * Extension compatibility states returned by the server. 139 * 140 * @type {{ 141 * INCOMPATIBLE: number, 142 * COMPATIBLE: number, 143 * MISSING_COMPATIBILITY_TAG: number, 144 * SERVER_ERROR: number}} 145 */ 146 147 PreUpdateChecker.STATE = { 148 INCOMPATIBLE: 0, 149 COMPATIBLE: 1, 150 MISSING_COMPATIBILITY_TAG: 2, 151 SERVER_ERROR: 3 152 }; 153 154 PreUpdateChecker.cleanup = status => { 155 // Set the icon in the nav-tab 156 const infoIcon = document.querySelector('#joomlaupdate-precheck-extensions-tab .fa-spinner'); 157 let iconColor = 'success'; 158 let iconClass = 'check'; 159 160 switch (status) { 161 case 'danger': 162 iconColor = 'danger'; 163 iconClass = 'times'; 164 break; 165 166 case 'warning': 167 iconColor = 'warning'; 168 iconClass = 'exclamation-triangle'; 169 break; 170 } 171 172 if (infoIcon) { 173 infoIcon.classList.remove('fa-spinner', 'fa-spin'); 174 infoIcon.classList.add(`fa-$iconClass}`, `text-$iconColor}`, 'bg-white'); 175 } // Hide table of addons to load 176 177 178 const checkedExtensions = document.querySelector('#compatibilityTable0'); 179 const preupdateCheckWarning = document.querySelector('#preupdateCheckWarning'); 180 181 if (checkedExtensions) { 182 checkedExtensions.classList.add('hidden'); 183 } 184 185 if (preupdateCheckWarning) { 186 preupdateCheckWarning.classList.add('hidden'); 187 } 188 }; 189 /** 190 * Run the PreUpdateChecker. 191 * Called by document ready, setup below. 192 */ 193 194 195 PreUpdateChecker.run = () => { 196 // eslint-disable-next-line no-undef 197 PreUpdateChecker.nonCoreCriticalPlugins = Joomla.getOptions('nonCoreCriticalPlugins', []); // Grab all extensions based on the selector set in the config object 198 199 const extensions = document.querySelectorAll(PreUpdateChecker.config.selector); // If there are no extensions to be checked we can exit here 200 201 if (extensions.length === 0) { 202 if (document.getElementById('preupdatecheckbox') !== null) { 203 document.getElementById('preupdatecheckbox').style.display = 'none'; 204 } 205 206 if (document.getElementById('noncoreplugins') !== null) { 207 document.getElementById('noncoreplugins').checked = true; 208 } 209 210 [].slice.call(document.querySelectorAll('button.submitupdate')).forEach(el => { 211 el.classList.remove('disabled'); 212 el.removeAttribute('disabled'); 213 }); 214 PreUpdateChecker.cleanup(); 215 return; 216 } // Let the user make an update although there *could* be dangerous plugins in the wild 217 218 219 const onChangeEvent = () => { 220 const nonCorePluginCheckbox = document.getElementById('noncoreplugins'); 221 222 if (nonCorePluginCheckbox.checked) { 223 if (window.confirm(Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_CONFIRM_MESSAGE'))) { 224 [].slice.call(document.querySelectorAll('button.submitupdate')).forEach(el => { 225 el.classList.remove('disabled'); 226 el.removeAttribute('disabled'); 227 }); 228 } else { 229 nonCorePluginCheckbox.checked = false; 230 } 231 } else { 232 [].slice.call(document.querySelectorAll('button.submitupdate')).forEach(el => { 233 el.classList.add('disabled'); 234 el.setAttribute('disabled', ''); 235 }); 236 } 237 }; 238 239 if (document.getElementById('noncoreplugins') !== null) { 240 document.getElementById('noncoreplugins').addEventListener('change', onChangeEvent); 241 } // Get version of the available joomla update 242 243 244 const joomlaUpdateWrapper = document.getElementById('joomlaupdate-wrapper'); 245 PreUpdateChecker.joomlaTargetVersion = joomlaUpdateWrapper.getAttribute('data-joomla-target-version'); 246 PreUpdateChecker.joomlaCurrentVersion = joomlaUpdateWrapper.getAttribute('data-joomla-current-version'); 247 [].slice.call(document.querySelectorAll('.compatibilitytoggle')).forEach(el => { 248 el.addEventListener('click', () => { 249 const compatibilityTable = el.closest('.compatibilityTable'); 250 251 if (el.dataset.state === 'closed') { 252 el.dataset.state = 'open'; 253 el.innerHTML = Joomla.sanitizeHtml(Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_SHOW_LESS_COMPATIBILITY_INFORMATION')); 254 [].slice.call(compatibilityTable.querySelectorAll('table .hidden')).forEach(elem => { 255 elem.classList.remove('hidden'); 256 }); 257 } else { 258 el.dataset.state = 'closed'; 259 el.innerHTML = Joomla.sanitizeHtml(Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_SHOW_MORE_COMPATIBILITY_INFORMATION')); 260 [].slice.call(compatibilityTable.querySelectorAll('table .instver, table .upcomp, table .currcomp')).forEach(elem => { 261 elem.classList.add('hidden'); 262 }); 263 } 264 }); 265 }); // Grab all extensions based on the selector set in the config object 266 267 const extensionsInformation = []; 268 [].slice.call(extensions).forEach(extension => { 269 const thisInfo = { 270 eid: extension.getAttribute('data-extension-id'), 271 version: extension.getAttribute('data-extension-current-version') 272 }; 273 extensionsInformation.push(thisInfo); 274 }); 275 PreUpdateChecker.checkNextChunk(extensionsInformation); 276 }; 277 /** 278 * Converts a simple object containing query string parameters to a single, escaped query string 279 * 280 * @param data {object|string} A plain object containing the query parameters to pass 281 * @param prefix {string} Prefix for array-type parameters 282 * 283 * @returns {string} 284 */ 285 286 287 PreUpdateChecker.interpolateParameters = (data, prefix) => { 288 let encodedString = ''; 289 290 if (typeof data !== 'object' || data === null || !data) { 291 return ''; 292 } 293 294 Object.keys(data).forEach(prop => { 295 const item = data[prop]; 296 297 if (encodedString.length > 0) { 298 encodedString += '&'; 299 } // Scalar values 300 301 302 if (typeof item === 'object') { 303 const newPrefix = prefix.length ? `$prefix}[$prop}]` : prop; 304 encodedString += PreUpdateChecker.interpolateParameters(item, newPrefix); 305 return; 306 } 307 308 if (prefix === '') { 309 encodedString += `$encodeURIComponent(prop)}=$encodeURIComponent(item)}`; 310 return; 311 } 312 313 encodedString += `$encodeURIComponent(prefix)}[$encodeURIComponent(prop)}]=$encodeURIComponent(item)}`; 314 }); 315 return encodedString; 316 }; 317 /** 318 * Check the compatibility of several extensions. 319 * 320 * Asks the server to check the compatibility of as many extensions as possible. The server 321 * returns these results and the remainder of the extensions not already checked. 322 * 323 * @param {Array} extensionsArray 324 */ 325 326 327 PreUpdateChecker.checkNextChunk = extensionsArray => { 328 if (extensionsArray.length === 0) { 329 return; 330 } 331 332 Joomla.request({ 333 url: PreUpdateChecker.config.batchUrl, 334 method: 'POST', 335 data: PreUpdateChecker.interpolateParameters({ 336 'joomla-target-version': PreUpdateChecker.joomlaTargetVersion, 337 'joomla-current-version': PreUpdateChecker.joomlaCurrentVersion, 338 extensions: extensionsArray 339 }, ''), 340 341 onSuccess(data) { 342 const response = JSON.parse(data); 343 344 if (response.messages) { 345 Joomla.renderMessages(response.messages); 346 } 347 348 const extensions = response.data.extensions || []; 349 response.data.compatibility.forEach(record => { 350 const node = document.getElementById(`preUpdateCheck_$record.id}`); 351 352 if (!node) { 353 return; 354 } 355 356 PreUpdateChecker.setResultView({ 357 element: node, 358 compatibleVersion: 0, 359 serverError: 0, 360 compatibilityData: record 361 }); 362 }); 363 PreUpdateChecker.checkNextChunk(extensions); 364 }, 365 366 onError(xhr) { 367 // Report the XHR error 368 Joomla.renderMessages(Joomla.ajaxErrorsMessages(xhr)); // Mark all pending extensions as errored out on the server side 369 370 extensionsArray.forEach(info => { 371 const node = document.getElementById(`preUpdateCheck_$info.eid}`); 372 373 if (!node) { 374 return; 375 } 376 377 PreUpdateChecker.setResultView({ 378 element: node, 379 compatibleVersion: 0, 380 serverError: 1 381 }); 382 }); 383 } 384 385 }); 386 }; 387 /** 388 * Check the compatibility for a single extension. 389 * Requests the server checking the compatibility based 390 * on the data set in the element's data attributes. 391 * 392 * @param {Object} extension 393 */ 394 395 396 PreUpdateChecker.checkCompatibility = node => { 397 // Result object passed to the callback 398 // Set to server error by default 399 const extension = { 400 element: node, 401 compatibleVersion: 0, 402 serverError: 1 403 }; // Request the server to check the compatibility for the passed extension and joomla version 404 405 Joomla.request({ 406 url: `$PreUpdateChecker.config.serverUrl}&joomla-target-version=$encodeURIComponent(PreUpdateChecker.joomlaTargetVersion)}&joomla-current-version=$PreUpdateChecker.joomlaCurrentVersion}&extension-version=$node.getAttribute('data-extension-current-version')}&extension-id=$encodeURIComponent(node.getAttribute('data-extension-id'))}`, 407 408 onSuccess(data) { 409 const response = JSON.parse(data); // Extract the data from the JResponseJson object 410 411 extension.serverError = 0; 412 extension.compatibilityData = response.data; // Pass the retrieved data to the callback 413 414 PreUpdateChecker.setResultView(extension); 415 }, 416 417 onError() { 418 extension.serverError = 1; // Pass the retrieved data to the callback 419 420 PreUpdateChecker.setResultView(extension); 421 } 422 423 }); 424 }; 425 /** 426 * Set the result for a passed extensionData object containing state compatible version 427 * 428 * @param {Object} extensionData 429 */ 430 431 432 PreUpdateChecker.setResultView = extensionData => { 433 let html = ''; // const direction = (document.dir !== undefined) ? document.dir : document.getElementsByTagName('html')[0].getAttribute('dir'); 434 // Process Target Version Extension Compatibility 435 436 if (extensionData.serverError) { 437 // An error occurred -> show unknown error note 438 html = Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_SERVER_ERROR'); // Force result into group 4 = Pre update checks failed 439 440 extensionData.compatibilityData = { 441 resultGroup: 4 442 }; 443 } else { 444 // Switch the compatibility state 445 switch (extensionData.compatibilityData.upgradeCompatibilityStatus.state) { 446 case PreUpdateChecker.STATE.COMPATIBLE: 447 if (extensionData.compatibilityData.upgradeWarning) { 448 const compatibleVersion = Joomla.sanitizeHtml(extensionData.compatibilityData.upgradeCompatibilityStatus.compatibleVersion); 449 html = `<span class="label label-warning">$compatibleVersion}</span>`; // @TODO activate when language strings are correct 450 451 /* if (compatibilitytypes.querySelector('#updateorangewarning')) { 452 compatibilitytypes.querySelector('#updateorangewarning').classList.remove('hidden'); 453 } */ 454 } else { 455 html = extensionData.compatibilityData.upgradeCompatibilityStatus.compatibleVersion === false ? Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_NO_COMPATIBILITY_INFORMATION') : Joomla.sanitizeHtml(extensionData.compatibilityData.upgradeCompatibilityStatus.compatibleVersion); 456 } 457 458 break; 459 460 case PreUpdateChecker.STATE.INCOMPATIBLE: 461 // No compatible version found -> display error label 462 html = Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_NO_COMPATIBILITY_INFORMATION'); // @TODO activate when language strings are correct 463 464 /* if (document.querySelector('#updateyellowwarning')) { 465 document.querySelector('#updateyellowwarning').classList.remove('hidden'); 466 } */ 467 468 break; 469 470 case PreUpdateChecker.STATE.MISSING_COMPATIBILITY_TAG: 471 // Could not check compatibility state -> display warning 472 html = Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_NO_COMPATIBILITY_INFORMATION'); // @TODO activate when language strings are correct 473 474 /* if (document.querySelector('#updateyellowwarning')) { 475 document.querySelector('#updateyellowwarning').classList.remove('hidden'); 476 } */ 477 478 break; 479 480 default: 481 // An error occurred -> show unknown error note 482 html = Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_WARNING_UNKNOWN'); 483 } 484 } // Insert the generated html 485 486 487 extensionData.element.innerHTML = html; // Process Current Version Extension Compatibility 488 489 html = ''; 490 491 if (extensionData.serverError) { 492 // An error occurred -> show unknown error note 493 html = Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_SERVER_ERROR'); 494 } else { 495 // Switch the compatibility state 496 switch (extensionData.compatibilityData.currentCompatibilityStatus.state) { 497 case PreUpdateChecker.STATE.COMPATIBLE: 498 html = extensionData.compatibilityData.currentCompatibilityStatus.compatibleVersion === false ? Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_NO_COMPATIBILITY_INFORMATION') : extensionData.compatibilityData.currentCompatibilityStatus.compatibleVersion; 499 break; 500 501 case PreUpdateChecker.STATE.INCOMPATIBLE: 502 // No compatible version found -> display error label 503 html = Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_NO_COMPATIBILITY_INFORMATION'); 504 break; 505 506 case PreUpdateChecker.STATE.MISSING_COMPATIBILITY_TAG: 507 // Could not check compatibility state -> display warning 508 html = Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_NO_COMPATIBILITY_INFORMATION'); 509 break; 510 511 default: 512 // An error occurred -> show unknown error note 513 html = Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSION_WARNING_UNKNOWN'); 514 } 515 } // Insert the generated html 516 517 518 const extensionId = extensionData.element.getAttribute('data-extension-id'); 519 document.getElementById(`available-version-$extensionId}`).innerText = html; 520 const compatType = document.querySelector(`#compatibilityTable$extensionData.compatibilityData.resultGroup} tbody`); 521 522 if (compatType) { 523 compatType.appendChild(extensionData.element.closest('tr')); 524 } // Show the table 525 526 527 document.getElementById(`compatibilityTable$extensionData.compatibilityData.resultGroup}`).classList.remove('hidden'); // Process the nonCoreCriticalPlugin list 528 529 if (extensionData.compatibilityData.resultGroup === 3) { 530 PreUpdateChecker.nonCoreCriticalPlugins = PreUpdateChecker.nonCoreCriticalPlugins // eslint-disable-next-line max-len 531 .filter(ext => !(ext.package_id.toString() === extensionId || ext.extension_id.toString() === extensionId)); 532 } // Have we finished? 533 534 535 if (!document.querySelector('#compatibilityTable0 tbody td')) { 536 document.getElementById('compatibilityTable0').classList.add('hidden'); 537 let status = 'success'; 538 PreUpdateChecker.nonCoreCriticalPlugins.forEach(plugin => { 539 let problemPluginRow = document.querySelector(`td[data-extension-id="$plugin.extension_id}"]`); 540 541 if (!problemPluginRow) { 542 problemPluginRow = document.querySelector(`td[data-extension-id="$plugin.package_id}"]`); 543 } 544 545 if (problemPluginRow) { 546 const tableRow = problemPluginRow.closest('tr'); 547 tableRow.classList.add('error'); 548 const pluginTitleTableCell = tableRow.querySelector('.exname'); 549 pluginTitleTableCell.innerHTML = `$Joomla.sanitizeHtml(pluginTitleTableCell.innerHTML)} 550 <div class="small"> 551 <span class="badge bg-warning"> 552 <span class="icon-warning"></span> 553 $Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN')} 554 </span> 555 556 <button type="button" class="btn btn-sm btn-link hasPopover" 557 title="$Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN')} " 558 data-bs-content="$Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_POTENTIALLY_DANGEROUS_PLUGIN_DESC')} " 559 > 560 $Joomla.Text._('COM_JOOMLAUPDATE_VIEW_DEFAULT_HELP')} 561 </button> 562 </div>`; 563 const popoverElement = pluginTitleTableCell.querySelector('.hasPopover'); 564 565 if (popoverElement) { 566 popoverElement.style.cursor = 'pointer'; // eslint-disable-next-line no-new 567 568 new bootstrap.Popover(popoverElement, { 569 placement: 'top', 570 html: true, 571 trigger: 'focus' 572 }); 573 } 574 575 status = 'danger'; 576 } 577 }); // Updates required 578 579 if (document.querySelector('#compatibilityTable2 tbody td')) { 580 status = 'danger'; 581 } else if (status !== 'danger' && document.querySelector('#compatibilityTable1 tbody td')) { 582 status = 'warning'; 583 } 584 585 if (PreUpdateChecker.nonCoreCriticalPlugins.length === 0 && status === 'success') { 586 document.getElementById('preupdatecheckbox').style.display = 'none'; 587 document.getElementById('noncoreplugins').checked = true; 588 [].slice.call(document.querySelectorAll('button.submitupdate')).forEach(el => { 589 el.classList.remove('disabled'); 590 el.removeAttribute('disabled'); 591 }); 592 } else if (PreUpdateChecker.nonCoreCriticalPlugins.length > 0) { 593 document.getElementById('preupdateCheckCompleteProblems').classList.remove('hidden'); 594 } 595 596 PreUpdateChecker.cleanup(status); 597 } 598 }; 599 600 if (document.getElementById('preupdatecheck') !== null) { 601 // Run PreUpdateChecker on document ready 602 document.addEventListener('DOMContentLoaded', PreUpdateChecker.run, false); 603 } 604 })(Joomla, document);
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 |