From 276f9c44d47711aff23c0922de08027a70b88441 Mon Sep 17 00:00:00 2001 From: pupi1985 Date: Sun, 17 Jul 2022 15:07:07 -0300 Subject: [PATCH 1/2] Update recalc logic to be resumable --- qa-content/qa-admin.js | 150 ++-- .../Admin/Recalc/AbstractProcessManager.php | 123 +++ qa-include/Q2A/Admin/Recalc/AbstractStep.php | 84 ++ .../Admin/Recalc/BlobsToDb/ProcessManager.php | 30 + .../Admin/Recalc/BlobsToDb/StepBlobsMove.php | 64 ++ .../Recalc/BlobsToDisk/ProcessManager.php | 30 + .../Recalc/BlobsToDisk/StepBlobsMove.php | 64 ++ .../Recalc/Caching/AbstractStepCaching.php | 59 ++ .../Caching/CacheClear/ProcessManager.php | 30 + .../Caching/CacheClear/StepCacheClear.php | 28 + .../Caching/CacheTrim/ProcessManager.php | 30 + .../Caching/CacheTrim/StepCacheTrim.php | 28 + .../AbstractStepDeleteHiddenPosts.php | 60 ++ .../DeleteHiddenPosts/ProcessManager.php | 32 + .../StepDeleteHiddenAnswers.php | 33 + .../StepDeleteHiddenComments.php | 33 + .../StepDeleteHiddenQuestions.php | 33 + .../RecalcCategories/ProcessManager.php | 32 + .../RecalcCategories/StepBackpathsRecalc.php | 60 ++ .../RecalcCategories/StepCategoriesCount.php | 62 ++ .../RecalcCategories/StepPostsUpdate.php | 65 ++ .../Recalc/RecountPosts/ProcessManager.php | 32 + .../Recalc/RecountPosts/StepAnswersCount.php | 60 ++ .../Recalc/RecountPosts/StepPostsCount.php | 47 ++ .../Recalc/RecountPosts/StepVotesCount.php | 56 ++ .../Recalc/RefillEvents/ProcessManager.php | 30 + .../Recalc/RefillEvents/StepRefillEvents.php | 141 ++++ .../Recalc/ReindexContent/ProcessManager.php | 33 + .../ReindexContent/StepPagesReindex.php | 74 ++ .../Recalc/ReindexContent/StepPostsCount.php | 45 + .../ReindexContent/StepPostsReindex.php | 71 ++ .../Recalc/ReindexContent/StepWordsCount.php | 61 ++ .../Recalc/UsersPoints/ProcessManager.php | 31 + .../Recalc/UsersPoints/StepRecalcPoints.php | 63 ++ .../Recalc/UsersPoints/StepUsersCount.php | 44 + qa-include/ajax/recalc.php | 35 +- qa-include/app/page.php | 1 - qa-include/app/recalc.php | 770 +----------------- qa-include/db/admin.php | 8 +- qa-include/db/install.php | 55 +- qa-include/db/recalc.php | 110 ++- qa-include/db/users.php | 2 + qa-include/lang/qa-lang-admin.php | 90 +- qa-include/pages/admin/admin-categories.php | 42 +- qa-include/pages/admin/admin-default.php | 115 ++- qa-include/pages/admin/admin-points.php | 33 +- qa-include/pages/admin/admin-recalc.php | 132 --- qa-include/pages/admin/admin-stats.php | 173 ++-- qa-include/qa-page-admin-recalc.php | 15 - qa-theme/Snow/qa-styles.css | 4 +- qa-theme/SnowFlat/qa-styles.css | 3 +- 51 files changed, 2320 insertions(+), 1186 deletions(-) create mode 100644 qa-include/Q2A/Admin/Recalc/AbstractProcessManager.php create mode 100644 qa-include/Q2A/Admin/Recalc/AbstractStep.php create mode 100644 qa-include/Q2A/Admin/Recalc/BlobsToDb/ProcessManager.php create mode 100644 qa-include/Q2A/Admin/Recalc/BlobsToDb/StepBlobsMove.php create mode 100644 qa-include/Q2A/Admin/Recalc/BlobsToDisk/ProcessManager.php create mode 100644 qa-include/Q2A/Admin/Recalc/BlobsToDisk/StepBlobsMove.php create mode 100644 qa-include/Q2A/Admin/Recalc/Caching/AbstractStepCaching.php create mode 100644 qa-include/Q2A/Admin/Recalc/Caching/CacheClear/ProcessManager.php create mode 100644 qa-include/Q2A/Admin/Recalc/Caching/CacheClear/StepCacheClear.php create mode 100644 qa-include/Q2A/Admin/Recalc/Caching/CacheTrim/ProcessManager.php create mode 100644 qa-include/Q2A/Admin/Recalc/Caching/CacheTrim/StepCacheTrim.php create mode 100644 qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/AbstractStepDeleteHiddenPosts.php create mode 100644 qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/ProcessManager.php create mode 100644 qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/StepDeleteHiddenAnswers.php create mode 100644 qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/StepDeleteHiddenComments.php create mode 100644 qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/StepDeleteHiddenQuestions.php create mode 100644 qa-include/Q2A/Admin/Recalc/RecalcCategories/ProcessManager.php create mode 100644 qa-include/Q2A/Admin/Recalc/RecalcCategories/StepBackpathsRecalc.php create mode 100644 qa-include/Q2A/Admin/Recalc/RecalcCategories/StepCategoriesCount.php create mode 100644 qa-include/Q2A/Admin/Recalc/RecalcCategories/StepPostsUpdate.php create mode 100644 qa-include/Q2A/Admin/Recalc/RecountPosts/ProcessManager.php create mode 100644 qa-include/Q2A/Admin/Recalc/RecountPosts/StepAnswersCount.php create mode 100644 qa-include/Q2A/Admin/Recalc/RecountPosts/StepPostsCount.php create mode 100644 qa-include/Q2A/Admin/Recalc/RecountPosts/StepVotesCount.php create mode 100644 qa-include/Q2A/Admin/Recalc/RefillEvents/ProcessManager.php create mode 100644 qa-include/Q2A/Admin/Recalc/RefillEvents/StepRefillEvents.php create mode 100644 qa-include/Q2A/Admin/Recalc/ReindexContent/ProcessManager.php create mode 100644 qa-include/Q2A/Admin/Recalc/ReindexContent/StepPagesReindex.php create mode 100644 qa-include/Q2A/Admin/Recalc/ReindexContent/StepPostsCount.php create mode 100644 qa-include/Q2A/Admin/Recalc/ReindexContent/StepPostsReindex.php create mode 100644 qa-include/Q2A/Admin/Recalc/ReindexContent/StepWordsCount.php create mode 100644 qa-include/Q2A/Admin/Recalc/UsersPoints/ProcessManager.php create mode 100644 qa-include/Q2A/Admin/Recalc/UsersPoints/StepRecalcPoints.php create mode 100644 qa-include/Q2A/Admin/Recalc/UsersPoints/StepUsersCount.php delete mode 100644 qa-include/pages/admin/admin-recalc.php delete mode 100644 qa-include/qa-page-admin-recalc.php diff --git a/qa-content/qa-admin.js b/qa-content/qa-admin.js index 618fdccfe..734eaacc7 100644 --- a/qa-content/qa-admin.js +++ b/qa-content/qa-admin.js @@ -19,76 +19,124 @@ More about this license: http://www.question2answer.org/license.php */ -var qa_recalc_running = 0; +const qa_recalcProcesses = new Map(); -window.onbeforeunload = function(event) -{ - if (qa_recalc_running > 0) { - event = event || window.event; - var message = qa_warning_recalc; - event.returnValue = message; - return message; +window.onbeforeunload = event => { + for (let [processKey, process] of qa_recalcProcesses.entries()) { + if (process.clientRunning) { + event.preventDefault(); + event.returnValue = true; + } } }; -function qa_recalc_click(state, elem, value, noteid) +/** + * @param {String} processKey + * @param {object} options - See keys and default values below + * @returns {boolean} + */ +function qa_recalc_click(processKey, options = {}) { - if (elem.qa_recalc_running) { - elem.qa_recalc_stopped = true; - + options = { + forceRestart: false, + requiresServerTracking: true, + callbackStart: process => {}, + callbackStop: hasFinished => {}, + ...options, + }; + + let process = qa_recalcProcesses.get(processKey) ?? {processKey: processKey}; + + const startButton = document.getElementById(processKey); + const continueButton = document.getElementById(processKey + '_continue'); + const statusLabel = document.getElementById(processKey + '_status'); + + if (process.clientRunning) { + process.stopRequest = true; } else { - elem.qa_recalc_running = true; - elem.qa_recalc_stopped = false; - qa_recalc_running++; + process = { + ...process, + "startButton": startButton, + "continueButton": continueButton, + "statusLabel": statusLabel, + "clientRunning": true, + "stopRequest": false, + "options": options + }; + + qa_recalcProcesses.set(processKey, process); - document.getElementById(noteid).innerHTML = ''; - elem.qa_original_value = elem.value; - if (value) - elem.value = value; + qa_conceal(process.continueButton); - qa_recalc_update(elem, state, noteid); + statusLabel.innerHTML = qa_langs.please_wait; + startButton.value = qa_langs.process_stop; + + process.options.callbackStart(process); + + qa_recalc_update(process); } return false; } -function qa_recalc_update(elem, state, noteid) +function qa_recalc_update(process) { - if (state) { - var recalcCode = elem.form.elements.code_recalc ? elem.form.elements.code_recalc.value : elem.form.elements.code.value; - qa_ajax_post( - 'recalc', - {state: state, code: recalcCode}, - function(lines) { - if (lines[0] == '1') { - if (lines[2]) - document.getElementById(noteid).innerHTML = lines[2]; - - if (elem.qa_recalc_stopped) - qa_recalc_cleanup(elem); - else - qa_recalc_update(elem, lines[1], noteid); - - } else if (lines[0] == '0') { - document.getElementById(noteid).innerHTML = lines[1]; - qa_recalc_cleanup(elem); - - } else { + const recalcCode = process.startButton.form.elements.code.value; + + qa_ajax_post( + 'recalc', + { + process: process.processKey, + forceRestart: process.options.forceRestart, + code: recalcCode + }, + function (lines) { + const result = lines[0] ?? null; + const message = lines[1] ?? null; + const hasFinished = (lines[2] ?? '0') === '1'; + + switch (result) { + case '1': + if (message !== null) { + process.statusLabel.innerHTML = message; + } + + process.serverProcessPending = process.options.requiresServerTracking ? !hasFinished : false; + if (hasFinished || process.stopRequest) { + qa_recalc_cleanup(process, hasFinished); + } else { + process.options.forceRestart = false; + qa_recalc_update(process); + } + break; + case '0': + process.statusLabel.innerHTML = message; + process.serverProcessPending = true; + qa_recalc_cleanup(process, false, message); + break; + default: + process.serverProcessPending = true; + qa_recalc_cleanup(process); qa_ajax_error(); - qa_recalc_cleanup(elem); - } } - ); - } else { - qa_recalc_cleanup(elem); - } + } + ); } -function qa_recalc_cleanup(elem) +function qa_recalc_cleanup(process, hasFinished = false, message = null) { - elem.value = elem.qa_original_value; - elem.qa_recalc_running = null; - qa_recalc_running--; + process.clientRunning = false; + + process.options.callbackStop(hasFinished); + + if (process.options.requiresServerTracking && process.serverProcessPending) { + process.startButton.value = qa_langs.process_restart; + process.statusLabel.innerHTML = message ?? qa_langs.process_unfinished; + qa_reveal(process.continueButton); + } else { + process.startButton.value = qa_langs.process_start; + qa_conceal(process.continueButton); + } } function qa_mailing_start(noteid, pauseid) diff --git a/qa-include/Q2A/Admin/Recalc/AbstractProcessManager.php b/qa-include/Q2A/Admin/Recalc/AbstractProcessManager.php new file mode 100644 index 000000000..7161c328d --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/AbstractProcessManager.php @@ -0,0 +1,123 @@ +loadState(); + + if ($step->isFinished()) { + $this->currentStepIndex++; + + if ($this->currentStepIndex >= count($this->steps)) { + $this->clearState(); + + return [ + 'process_finished' => true, + 'message' => qa_lang('admin/process_complete'), + ]; + } + + $newStep = true; + $step = $this->getCurrentStepInstance(); + } + } catch (Exception $e) { + $step = $this->getCurrentStepInstance(); + $newStep = true; + } + + if ($newStep) { + $step->setup(); + } else { + $step->execute(); + } + + $result = [ + 'step_state' => $step->asArray(), + 'step_index' => $this->currentStepIndex, + 'process_finished' => false, + ]; + $this->saveState($result); + + $result['message'] = $step->getMessage(); + + return $result; + } + + /** + * @throws Exception + */ + private function loadState() + { + $state = qa_opt($this->stateOption); + $state = json_decode($state, true); + + if (!isset($state['step_index'])) { + throw new Exception('Nothing to load'); + } + + $this->currentStepIndex = $state['step_index']; + + $step = $this->getCurrentStepInstance(); + $step->loadFromJson($state['step_state']); + + return $step; + } + + private function saveState($state) + { + qa_opt($this->stateOption, json_encode($state)); + } + + private function clearState() + { + qa_opt($this->stateOption, ''); + } + + /** + * @return Q2A_Admin_Recalc_AbstractStep + */ + protected function getCurrentStepInstance() + { + // Make sure the step index to instantiate is valid + $this->currentStepIndex = min(max(0, $this->currentStepIndex), count($this->steps) - 1); + + return new $this->steps[$this->currentStepIndex]; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/AbstractStep.php b/qa-include/Q2A/Admin/Recalc/AbstractStep.php new file mode 100644 index 000000000..de3d7a14c --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/AbstractStep.php @@ -0,0 +1,84 @@ + $this->isFinished, + 'processed_items' => $this->processedItems, + 'total_items' => $this->totalItems, + 'next_item_id' => $this->nextItemId, + 'last_processed_item_id' => $this->lastProcessedItemId, + ]; + } + + public function loadFromJson($state) + { + $this->isFinished = $state['is_finished']; + $this->processedItems = $state['processed_items']; + $this->totalItems = $state['total_items']; + $this->nextItemId = $state['next_item_id']; + $this->lastProcessedItemId = $state['last_processed_item_id']; + } + + /** + * @return bool + */ + public function isFinished() + { + return $this->isFinished; + } + + public function getMessage() + { + require_once QA_INCLUDE_DIR . 'app/format.php'; + + return strtr(qa_lang($this->messageLangId), array( + '^1' => qa_format_number($this->processedItems), + '^2' => qa_format_number($this->totalItems), + )); + } +} diff --git a/qa-include/Q2A/Admin/Recalc/BlobsToDb/ProcessManager.php b/qa-include/Q2A/Admin/Recalc/BlobsToDb/ProcessManager.php new file mode 100644 index 000000000..a4c647ba7 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/BlobsToDb/ProcessManager.php @@ -0,0 +1,30 @@ +stateOption = 'qa_recalc_blobs_to_db_state'; + + $this->steps = [ + Q2A_Admin_Recalc_BlobsToDb_StepBlobsMove::class, + ]; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/BlobsToDb/StepBlobsMove.php b/qa-include/Q2A/Admin/Recalc/BlobsToDb/StepBlobsMove.php new file mode 100644 index 000000000..2a1c7b679 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/BlobsToDb/StepBlobsMove.php @@ -0,0 +1,64 @@ +messageLangId = 'admin/blobs_move_moved'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $this->totalItems = qa_db_count_blobs_on_disk(); + $this->lastProcessedItemId = ''; + } + + public function execute() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $blobs = qa_db_get_next_blobs_on_disk($this->lastProcessedItemId, self::BATCH_AMOUNT); + + if (!empty($blobs)) { + end($blobs); + $this->lastProcessedItemId = key($blobs); + + require_once QA_INCLUDE_DIR . 'app/blobs.php'; + require_once QA_INCLUDE_DIR . 'db/blobs.php'; + + foreach ($blobs as $blob) { + $content = qa_read_blob_file($blob['blobid'], $blob['format']); + qa_db_blob_set_content($blob['blobid'], $content); + qa_delete_blob_file($blob['blobid'], $blob['format']); + } + + $this->processedItems += count($blobs); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($blobs) < self::BATCH_AMOUNT) { + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/BlobsToDisk/ProcessManager.php b/qa-include/Q2A/Admin/Recalc/BlobsToDisk/ProcessManager.php new file mode 100644 index 000000000..2b1a50cac --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/BlobsToDisk/ProcessManager.php @@ -0,0 +1,30 @@ +stateOption = 'qa_recalc_blobs_to_disk_state'; + + $this->steps = [ + Q2A_Admin_Recalc_BlobsToDisk_StepBlobsMove::class, + ]; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/BlobsToDisk/StepBlobsMove.php b/qa-include/Q2A/Admin/Recalc/BlobsToDisk/StepBlobsMove.php new file mode 100644 index 000000000..a29b8a087 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/BlobsToDisk/StepBlobsMove.php @@ -0,0 +1,64 @@ +messageLangId = 'admin/blobs_move_moved'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $this->totalItems = qa_db_count_blobs_in_db(); + $this->lastProcessedItemId = ''; + } + + public function execute() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $blobs = qa_db_get_next_blobs_in_db($this->lastProcessedItemId, self::BATCH_AMOUNT); + + if (!empty($blobs)) { + end($blobs); + $this->lastProcessedItemId = key($blobs); + + require_once QA_INCLUDE_DIR . 'app/blobs.php'; + require_once QA_INCLUDE_DIR . 'db/blobs.php'; + + foreach ($blobs as $blob) { + if (qa_write_blob_file($blob['blobid'], $blob['content'], $blob['format'])) { + qa_db_blob_set_content($blob['blobid'], null); + } + } + + $this->processedItems += count($blobs); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($blobs) < self::BATCH_AMOUNT) { + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/Caching/AbstractStepCaching.php b/qa-include/Q2A/Admin/Recalc/Caching/AbstractStepCaching.php new file mode 100644 index 000000000..c6d4102f7 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/Caching/AbstractStepCaching.php @@ -0,0 +1,59 @@ +messageLangId = 'admin/caching_delete_progress'; + } + + public function setup() + { + $cacheDriver = Q2A_Storage_CacheFactory::getCacheDriver(); + $cacheStats = $cacheDriver->getStats(); + $this->totalItems = $cacheStats['files']; + } + + public function execute() + { + $cacheDriver = Q2A_Storage_CacheFactory::getCacheDriver(); + $cacheStats = $cacheDriver->getStats(); + $remaining = $cacheStats['files'] - $this->nextItemId; + $limit = min($remaining, self::BATCH_AMOUNT); + + if ($remaining > 0) { + $expiredOnly = $this->onlyProcessExpiredItems(); + + $deleted = $cacheDriver->clear($limit, $this->nextItemId, $expiredOnly); + + $this->nextItemId += $limit - $deleted; // skip files that weren't deleted on next iteration + $this->processedItems += $limit; + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if ($limit < self::BATCH_AMOUNT) { + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/Caching/CacheClear/ProcessManager.php b/qa-include/Q2A/Admin/Recalc/Caching/CacheClear/ProcessManager.php new file mode 100644 index 000000000..4f55f11d0 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/Caching/CacheClear/ProcessManager.php @@ -0,0 +1,30 @@ +stateOption = 'qa_recalc_cache_clear_state'; + + $this->steps = [ + Q2A_Admin_Recalc_Caching_CacheClear_StepCacheClear::class, + ]; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/Caching/CacheClear/StepCacheClear.php b/qa-include/Q2A/Admin/Recalc/Caching/CacheClear/StepCacheClear.php new file mode 100644 index 000000000..7ce6d63df --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/Caching/CacheClear/StepCacheClear.php @@ -0,0 +1,28 @@ +stateOption = 'qa_recalc_cache_trim_state'; + + $this->steps = [ + Q2A_Admin_Recalc_Caching_CacheTrim_StepCacheTrim::class, + ]; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/Caching/CacheTrim/StepCacheTrim.php b/qa-include/Q2A/Admin/Recalc/Caching/CacheTrim/StepCacheTrim.php new file mode 100644 index 000000000..3d29d1fb6 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/Caching/CacheTrim/StepCacheTrim.php @@ -0,0 +1,28 @@ +getPostType(); + $this->totalItems = qa_db_posts_count_for_deleting($postType); + } + + public function execute() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $postType = $this->getPostType(); + + $postids = qa_db_posts_get_for_deleting($postType, $this->nextItemId, self::BATCH_AMOUNT); + + if (!empty($postids)) { + require_once QA_INCLUDE_DIR . 'app/posts.php'; + + $lastpostid = max($postids); + + foreach ($postids as $postid) { + qa_post_delete($postid); + } + + $this->nextItemId = $lastpostid + 1; + $this->processedItems += count($postids); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($postids) < self::BATCH_AMOUNT) { + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/ProcessManager.php b/qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/ProcessManager.php new file mode 100644 index 000000000..54d072a6c --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/ProcessManager.php @@ -0,0 +1,32 @@ +stateOption = 'qa_recalc_delete_hidden_posts_state'; + + $this->steps = [ + Q2A_Admin_Recalc_DeleteHiddenPosts_StepDeleteHiddenComments::class, + Q2A_Admin_Recalc_DeleteHiddenPosts_StepDeleteHiddenAnswers::class, + Q2A_Admin_Recalc_DeleteHiddenPosts_StepDeleteHiddenQuestions::class, + ]; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/StepDeleteHiddenAnswers.php b/qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/StepDeleteHiddenAnswers.php new file mode 100644 index 000000000..b6a9c2a03 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/StepDeleteHiddenAnswers.php @@ -0,0 +1,33 @@ +messageLangId = 'admin/hidden_answers_deleted'; + } + + protected function getPostType() + { + return 'A'; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/StepDeleteHiddenComments.php b/qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/StepDeleteHiddenComments.php new file mode 100644 index 000000000..d353f8563 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/StepDeleteHiddenComments.php @@ -0,0 +1,33 @@ +messageLangId = 'admin/hidden_comments_deleted'; + } + + protected function getPostType() + { + return 'C'; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/StepDeleteHiddenQuestions.php b/qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/StepDeleteHiddenQuestions.php new file mode 100644 index 000000000..0715ff775 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/DeleteHiddenPosts/StepDeleteHiddenQuestions.php @@ -0,0 +1,33 @@ +messageLangId = 'admin/hidden_questions_deleted'; + } + + protected function getPostType() + { + return 'Q'; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/RecalcCategories/ProcessManager.php b/qa-include/Q2A/Admin/Recalc/RecalcCategories/ProcessManager.php new file mode 100644 index 000000000..fca11d3d7 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/RecalcCategories/ProcessManager.php @@ -0,0 +1,32 @@ +stateOption = 'qa_recalc_recalc_categories_state'; + + $this->steps = [ + Q2A_Admin_Recalc_RecalcCategories_StepPostsUpdate::class, + Q2A_Admin_Recalc_RecalcCategories_StepCategoriesCount::class, + Q2A_Admin_Recalc_RecalcCategories_StepBackpathsRecalc::class, + ]; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/RecalcCategories/StepBackpathsRecalc.php b/qa-include/Q2A/Admin/Recalc/RecalcCategories/StepBackpathsRecalc.php new file mode 100644 index 000000000..868f3e308 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/RecalcCategories/StepBackpathsRecalc.php @@ -0,0 +1,60 @@ +messageLangId = 'admin/recalc_categories_backpaths'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/admin.php'; + + $this->totalItems = qa_db_count_categories(); + } + + public function execute() + { + // For qa_db_categories_get_for_recalcs() + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + // For qa_db_categories_recalc_backpaths() + require_once QA_INCLUDE_DIR . 'db/admin.php'; + + $categoryids = qa_db_categories_get_for_recalcs($this->nextItemId, self::BATCH_AMOUNT); + + if (!empty($categoryids)) { + $lastcategoryid = max($categoryids); + + qa_db_categories_recalc_backpaths($this->nextItemId, $lastcategoryid); + + $this->nextItemId = $lastcategoryid + 1; + $this->processedItems += count($categoryids); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($categoryids) < self::BATCH_AMOUNT) { + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/RecalcCategories/StepCategoriesCount.php b/qa-include/Q2A/Admin/Recalc/RecalcCategories/StepCategoriesCount.php new file mode 100644 index 000000000..b3634101c --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/RecalcCategories/StepCategoriesCount.php @@ -0,0 +1,62 @@ +messageLangId = 'admin/recalc_categories_recounting'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/admin.php'; + + $this->totalItems = qa_db_count_categories(); + } + + public function execute() + { + // For qa_db_categories_get_for_recalcs() + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + // For qa_db_ifcategory_qcount_update() + require_once QA_INCLUDE_DIR . 'db/post-create.php'; + + $categoryids = qa_db_categories_get_for_recalcs($this->nextItemId, self::BATCH_AMOUNT); + + if (!empty($categoryids)) { + $lastcategoryid = max($categoryids); + + foreach ($categoryids as $categoryid) { + qa_db_ifcategory_qcount_update($categoryid); + } + + $this->nextItemId = $lastcategoryid + 1; + $this->processedItems += count($categoryids); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($categoryids) < self::BATCH_AMOUNT) { + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/RecalcCategories/StepPostsUpdate.php b/qa-include/Q2A/Admin/Recalc/RecalcCategories/StepPostsUpdate.php new file mode 100644 index 000000000..53675c0bf --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/RecalcCategories/StepPostsUpdate.php @@ -0,0 +1,65 @@ +messageLangId = 'admin/recalc_categories_updated'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/post-create.php'; + require_once QA_INCLUDE_DIR . 'db/admin.php'; + + qa_db_acount_update(); + qa_db_ccount_update(); + + $this->totalItems = qa_db_count_posts(); + } + + public function execute() + { + // For qa_db_posts_get_for_recategorizing() + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + // For qa_db_posts_calc_category_path() + require_once QA_INCLUDE_DIR . 'db/post-create.php'; + + $postids = qa_db_posts_get_for_recategorizing($this->nextItemId, self::BATCH_AMOUNT); + + if (!empty($postids)) { + $lastpostid = max($postids); + + qa_db_posts_recalc_categoryid($this->nextItemId, $lastpostid); + qa_db_posts_calc_category_path($this->nextItemId, $lastpostid); + + $this->nextItemId = $lastpostid + 1; + $this->processedItems += count($postids); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($postids) < self::BATCH_AMOUNT) { + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/RecountPosts/ProcessManager.php b/qa-include/Q2A/Admin/Recalc/RecountPosts/ProcessManager.php new file mode 100644 index 000000000..15ad55c28 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/RecountPosts/ProcessManager.php @@ -0,0 +1,32 @@ +stateOption = 'qa_recalc_recount_posts_state'; + + $this->steps = [ + Q2A_Admin_Recalc_RecountPosts_StepPostsCount::class, + Q2A_Admin_Recalc_RecountPosts_StepVotesCount::class, + Q2A_Admin_Recalc_RecountPosts_StepAnswersCount::class, + ]; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/RecountPosts/StepAnswersCount.php b/qa-include/Q2A/Admin/Recalc/RecountPosts/StepAnswersCount.php new file mode 100644 index 000000000..bb910bc95 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/RecountPosts/StepAnswersCount.php @@ -0,0 +1,60 @@ +messageLangId = 'admin/recount_posts_as_recounted'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/admin.php'; + + $this->totalItems = qa_db_count_posts(); + } + + public function execute() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $postids = qa_db_posts_get_for_recounting($this->nextItemId, self::BATCH_AMOUNT); + + if (!empty($postids)) { + $lastpostid = max($postids); + + qa_db_posts_answers_recount($this->nextItemId, $lastpostid); + + $this->nextItemId = $lastpostid + 1; + $this->processedItems += count($postids); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($postids) < self::BATCH_AMOUNT) { + require_once QA_INCLUDE_DIR . 'db/post-create.php'; + + qa_db_unupaqcount_update(); + + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/RecountPosts/StepPostsCount.php b/qa-include/Q2A/Admin/Recalc/RecountPosts/StepPostsCount.php new file mode 100644 index 000000000..d186c6598 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/RecountPosts/StepPostsCount.php @@ -0,0 +1,47 @@ +messageLangId = 'admin/recalc_posts_count'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/admin.php'; + + $this->totalItems = qa_db_count_posts(); + } + + public function execute() + { + require_once QA_INCLUDE_DIR . 'db/post-create.php'; + + qa_db_qcount_update(); + qa_db_acount_update(); + qa_db_ccount_update(); + qa_db_unaqcount_update(); + qa_db_unselqcount_update(); + + $this->processedItems = $this->totalItems; + $this->isFinished = true; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/RecountPosts/StepVotesCount.php b/qa-include/Q2A/Admin/Recalc/RecountPosts/StepVotesCount.php new file mode 100644 index 000000000..80df0af81 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/RecountPosts/StepVotesCount.php @@ -0,0 +1,56 @@ +messageLangId = 'admin/recount_posts_votes_recounted'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/admin.php'; + + $this->totalItems = qa_db_count_posts(); + } + + public function execute() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $postids = qa_db_posts_get_for_recounting($this->nextItemId, self::BATCH_AMOUNT); + + if (!empty($postids)) { + $lastpostid = max($postids); + + qa_db_posts_votes_recount($this->nextItemId, $lastpostid); + + $this->nextItemId = $lastpostid + 1; + $this->processedItems += count($postids); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($postids) < self::BATCH_AMOUNT) { + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/RefillEvents/ProcessManager.php b/qa-include/Q2A/Admin/Recalc/RefillEvents/ProcessManager.php new file mode 100644 index 000000000..26551a33d --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/RefillEvents/ProcessManager.php @@ -0,0 +1,30 @@ +stateOption = 'qa_recalc_refill_events_state'; + + $this->steps = [ + Q2A_Admin_Recalc_RefillEvents_StepRefillEvents::class, + ]; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/RefillEvents/StepRefillEvents.php b/qa-include/Q2A/Admin/Recalc/RefillEvents/StepRefillEvents.php new file mode 100644 index 000000000..406bfde2d --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/RefillEvents/StepRefillEvents.php @@ -0,0 +1,141 @@ +messageLangId = 'admin/refill_events_refilled'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/admin.php'; + + $this->totalItems = qa_db_count_posts(['Q', 'Q_HIDDEN', 'Q_QUEUED']); + } + + public function execute() + { + // For qa_db_select_with_pending() + require_once QA_INCLUDE_DIR . 'db/selects.php'; + + // For qa_db_qs_get_for_event_refilling() + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $questionids = qa_db_qs_get_for_event_refilling($this->nextItemId, self::BATCH_AMOUNT); + + if (!empty($questionids)) { + require_once QA_INCLUDE_DIR . 'app/events.php'; + require_once QA_INCLUDE_DIR . 'app/updates.php'; + require_once QA_INCLUDE_DIR . 'util/sort.php'; + + $lastquestionid = max($questionids); + + foreach ($questionids as $questionid) { + // Retrieve all posts relating to this question + + list($question, $childposts, $achildposts) = qa_db_select_with_pending( + qa_db_full_post_selectspec(null, $questionid), + qa_db_full_child_posts_selectspec(null, $questionid), + qa_db_full_a_child_posts_selectspec(null, $questionid) + ); + + // Merge all posts while preserving keys as postids + + $posts = array($questionid => $question); + + foreach ($childposts as $postid => $post) { + $posts[$postid] = $post; + } + + foreach ($achildposts as $postid => $post) { + $posts[$postid] = $post; + } + + // Creation and editing of each post + + foreach ($posts as $postid => $post) { + $followonq = ($post['basetype'] == 'Q') && ($postid != $questionid); + + if ($followonq) { + $updatetype = QA_UPDATE_FOLLOWS; + } elseif ($post['basetype'] == 'C' && @$posts[$post['parentid']]['basetype'] == 'Q') { + $updatetype = QA_UPDATE_C_FOR_Q; + } elseif ($post['basetype'] == 'C' && @$posts[$post['parentid']]['basetype'] == 'A') { + $updatetype = QA_UPDATE_C_FOR_A; + } else { + $updatetype = null; + } + + qa_create_event_for_q_user($questionid, $postid, $updatetype, $post['userid'], @$posts[$post['parentid']]['userid'], $post['created']); + + if (isset($post['updated']) && !$followonq) { + qa_create_event_for_q_user($questionid, $postid, $post['updatetype'], $post['lastuserid'], $post['userid'], $post['updated']); + } + } + + // Tags and categories of question + + qa_create_event_for_tags($question['tags'], $questionid, null, $question['userid'], $question['created']); + qa_create_event_for_category($question['categoryid'], $questionid, null, $question['userid'], $question['created']); + + // Collect comment threads + + $parentidcomments = array(); + + foreach ($posts as $postid => $post) { + if ($post['basetype'] == 'C') { + $parentidcomments[$post['parentid']][$postid] = $post; + } + } + + // For each comment thread, notify all previous comment authors of each comment in the thread (could get slow) + + foreach ($parentidcomments as $parentid => $comments) { + $keyuserids = array(); + + qa_sort_by($comments, 'created'); + + foreach ($comments as $comment) { + foreach ($keyuserids as $keyuserid => $dummy) { + if ($keyuserid != $comment['userid'] && $keyuserid != @$posts[$parentid]['userid']) { + qa_db_event_create_not_entity($keyuserid, $questionid, $comment['postid'], QA_UPDATE_FOLLOWS, $comment['userid'], $comment['created']); + } + } + + if (isset($comment['userid'])) { + $keyuserids[$comment['userid']] = true; + } + } + } + } + + $this->nextItemId = $lastquestionid + 1; + $this->processedItems += count($questionids); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($questionids) < self::BATCH_AMOUNT) { + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/ReindexContent/ProcessManager.php b/qa-include/Q2A/Admin/Recalc/ReindexContent/ProcessManager.php new file mode 100644 index 000000000..657449b39 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/ReindexContent/ProcessManager.php @@ -0,0 +1,33 @@ +stateOption = 'qa_recalc_reindex_content_state'; + + $this->steps = [ + Q2A_Admin_Recalc_ReindexContent_StepPagesReindex::class, + Q2A_Admin_Recalc_ReindexContent_StepPostsCount::class, + Q2A_Admin_Recalc_ReindexContent_StepPostsReindex::class, + Q2A_Admin_Recalc_ReindexContent_StepWordsCount::class, + ]; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/ReindexContent/StepPagesReindex.php b/qa-include/Q2A/Admin/Recalc/ReindexContent/StepPagesReindex.php new file mode 100644 index 000000000..7659ff847 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/ReindexContent/StepPagesReindex.php @@ -0,0 +1,74 @@ +messageLangId = 'admin/reindex_pages_reindexed'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $this->totalItems = qa_db_count_pages(); + } + + public function execute() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $pages = qa_db_pages_get_for_reindexing($this->nextItemId, self::BATCH_AMOUNT); + + if (!empty($pages)) { + require_once QA_INCLUDE_DIR . 'app/format.php'; + + $lastpageid = max(array_keys($pages)); + + foreach ($pages as $pageid => $page) { + if (!($page['flags'] & QA_PAGE_FLAGS_EXTERNAL)) { + $searchmodules = qa_load_modules_with('search', 'unindex_page'); + foreach ($searchmodules as $searchmodule) { + $searchmodule->unindex_page($pageid); + } + + $searchmodules = qa_load_modules_with('search', 'index_page'); + if (count($searchmodules)) { + $indextext = qa_viewer_text($page['content'], 'html'); + + foreach ($searchmodules as $searchmodule) { + $searchmodule->index_page($pageid, $page['tags'], $page['heading'], $page['content'], 'html', $indextext); + } + } + } + } + + $this->nextItemId = $lastpageid + 1; + $this->processedItems += count($pages); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($pages) < self::BATCH_AMOUNT) { + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/ReindexContent/StepPostsCount.php b/qa-include/Q2A/Admin/Recalc/ReindexContent/StepPostsCount.php new file mode 100644 index 000000000..567cab229 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/ReindexContent/StepPostsCount.php @@ -0,0 +1,45 @@ +messageLangId = 'admin/recalc_posts_count'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/admin.php'; + + $this->totalItems = qa_db_count_posts(); + } + + public function execute() + { + require_once QA_INCLUDE_DIR . 'db/post-create.php'; + + qa_db_qcount_update(); + qa_db_acount_update(); + qa_db_ccount_update(); + + $this->processedItems = $this->totalItems; + $this->isFinished = true; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/ReindexContent/StepPostsReindex.php b/qa-include/Q2A/Admin/Recalc/ReindexContent/StepPostsReindex.php new file mode 100644 index 000000000..936c9fedb --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/ReindexContent/StepPostsReindex.php @@ -0,0 +1,71 @@ +messageLangId = 'admin/reindex_posts_reindexed'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $this->totalItems = qa_db_posts_count_for_reindexing(); + } + + public function execute() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $posts = qa_db_posts_get_for_reindexing($this->nextItemId, self::BATCH_AMOUNT); + + if (!empty($posts)) { + + require_once QA_INCLUDE_DIR . 'app/post-update.php'; + require_once QA_INCLUDE_DIR . 'app/post-update.php'; + require_once QA_INCLUDE_DIR . 'app/format.php'; + + $lastpostid = max(array_keys($posts)); + + qa_db_prepare_for_reindexing($this->nextItemId, $lastpostid); + qa_suspend_update_counts(); + + foreach ($posts as $postid => $post) { + qa_post_unindex($postid); + qa_post_index($postid, $post['type'], $post['questionid'], $post['parentid'], $post['title'], $post['content'], + $post['format'], qa_viewer_text($post['content'], $post['format']), $post['tags'], $post['categoryid']); + } + + $this->nextItemId = $lastpostid + 1; + $this->processedItems += count($posts); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($posts) < self::BATCH_AMOUNT) { + + qa_db_truncate_indexes($this->nextItemId); + + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/ReindexContent/StepWordsCount.php b/qa-include/Q2A/Admin/Recalc/ReindexContent/StepWordsCount.php new file mode 100644 index 000000000..433f51918 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/ReindexContent/StepWordsCount.php @@ -0,0 +1,61 @@ +messageLangId = 'admin/reindex_posts_wordcounted'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $this->totalItems = qa_db_count_words(); + } + + public function execute() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + require_once QA_INCLUDE_DIR . 'db/post-create.php'; + + $wordids = qa_db_words_prepare_for_recounting($this->nextItemId, self::BATCH_AMOUNT); + + if (!empty($wordids)) { + $lastpostid = max($wordids); + + qa_db_words_recount($this->nextItemId, $lastpostid); + + $this->nextItemId = $lastpostid + 1; + $this->processedItems += count($wordids); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($wordids) < self::BATCH_AMOUNT) { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + qa_db_tagcount_update(); // this is quick so just do it here + + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/UsersPoints/ProcessManager.php b/qa-include/Q2A/Admin/Recalc/UsersPoints/ProcessManager.php new file mode 100644 index 000000000..1bed0ddc1 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/UsersPoints/ProcessManager.php @@ -0,0 +1,31 @@ +stateOption = 'qa_recalc_users_points_state'; + + $this->steps = [ + Q2A_Admin_Recalc_UsersPoints_StepUsersCount::class, + Q2A_Admin_Recalc_UsersPoints_StepRecalcPoints::class, + ]; + } +} diff --git a/qa-include/Q2A/Admin/Recalc/UsersPoints/StepRecalcPoints.php b/qa-include/Q2A/Admin/Recalc/UsersPoints/StepRecalcPoints.php new file mode 100644 index 000000000..9c2eec664 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/UsersPoints/StepRecalcPoints.php @@ -0,0 +1,63 @@ +messageLangId = 'admin/recalc_users_points_recalced'; + } + + public function setup() + { + require_once QA_INCLUDE_DIR . 'db/admin.php'; + + $this->totalItems = qa_opt('cache_userpointscount'); + + $this->lastProcessedItemId = QA_FINAL_EXTERNAL_USERS ? '' : 0; + } + + public function execute() + { + require_once QA_INCLUDE_DIR . 'db/recalc.php'; + + $userids = qa_db_users_get_for_recalc_points($this->lastProcessedItemId, self::BATCH_AMOUNT); + + if (!empty($userids)) { + $firstUserId = reset($userids); + $this->lastProcessedItemId = end($userids); + + qa_db_users_recalc_points($firstUserId, $this->lastProcessedItemId); + + $this->processedItems += count($userids); + $this->totalItems = max($this->totalItems, $this->processedItems); + } + + if (count($userids) < self::BATCH_AMOUNT) { + require_once QA_INCLUDE_DIR . 'db/points.php'; + + qa_db_truncate_userpoints($this->lastProcessedItemId); + qa_db_userpointscount_update(); // quick so just do it here + + $this->isFinished = true; + } + } +} diff --git a/qa-include/Q2A/Admin/Recalc/UsersPoints/StepUsersCount.php b/qa-include/Q2A/Admin/Recalc/UsersPoints/StepUsersCount.php new file mode 100644 index 000000000..dc36a3237 --- /dev/null +++ b/qa-include/Q2A/Admin/Recalc/UsersPoints/StepUsersCount.php @@ -0,0 +1,44 @@ +messageLangId = 'admin/recalc_users_points_usercount'; + } + + public function setup() + { + // Rough approximation of the amount of work to perform + $this->totalItems = qa_opt('cache_userpointscount'); + } + + public function execute() + { + require_once QA_INCLUDE_DIR . 'db/points.php'; + require_once QA_INCLUDE_DIR . 'db/users.php'; + + qa_db_userpointscount_update(); // for progress update - not necessarily accurate + qa_db_uapprovecount_update(); // needs to be somewhere and this is the most appropriate place + + $this->processedItems = $this->totalItems; + $this->isFinished = true; + } +} diff --git a/qa-include/ajax/recalc.php b/qa-include/ajax/recalc.php index 54fe0a2c8..dc4f2cc3b 100644 --- a/qa-include/ajax/recalc.php +++ b/qa-include/ajax/recalc.php @@ -22,27 +22,38 @@ require_once QA_INCLUDE_DIR . 'app/users.php'; require_once QA_INCLUDE_DIR . 'app/recalc.php'; +$response = "QA_AJAX_RESPONSE\n1\n"; if (qa_get_logged_in_level() >= QA_USER_LEVEL_ADMIN) { if (!qa_check_form_security_code('admin/recalc', qa_post_text('code'))) { - $state = ''; - $message = qa_lang('misc/form_security_reload'); - + $response .= qa_lang('misc/form_security_reload'); } else { - $state = qa_post_text('state'); + $process = qa_post_text('process'); + $forceRestart = qa_post_text('forceRestart') === 'true'; $stoptime = time() + 3; - while (qa_recalc_perform_step($state) && time() < $stoptime) { - // wait + do { + $result = qa_recalc_get_process_manager($process)->execute($forceRestart); + $stepFinished = $result['step_state']['is_finished'] ?? false; + $processedItems = $result['step_state']['processed_items'] ?? 0; + $shouldShowProgress = $result['process_finished'] || $stepFinished || $processedItems === 0; + } while (!$shouldShowProgress && time() < $stoptime); + + // Remove reminder to run pending processes + if ($result['process_finished']) { + $pendingRecalcs = json_decode(qa_opt('recalc_pending_processes'), true) ?? []; + $processKey = array_search($process, $pendingRecalcs); + if ($processKey !== false) { + unset($pendingRecalcs[$processKey]); + qa_opt('recalc_pending_processes', json_encode($pendingRecalcs)); + } } - $message = qa_recalc_get_message($state); + $response .= $result['message'] . "\n"; + $response .= (int)$result['process_finished']; } - } else { - $state = ''; - $message = qa_lang('admin/no_privileges'); + $response .= qa_lang('admin/no_privileges'); } - -echo "QA_AJAX_RESPONSE\n1\n" . $state . "\n" . qa_html($message); +echo $response; diff --git a/qa-include/app/page.php b/qa-include/app/page.php index bcddc4c06..8c96d5517 100644 --- a/qa-include/app/page.php +++ b/qa-include/app/page.php @@ -407,7 +407,6 @@ function qa_page_routing() 'admin/pages' => 'pages/admin/admin-pages.php', 'admin/plugins' => 'pages/admin/admin-plugins.php', 'admin/points' => 'pages/admin/admin-points.php', - 'admin/recalc' => 'pages/admin/admin-recalc.php', 'admin/stats' => 'pages/admin/admin-stats.php', 'admin/userfields' => 'pages/admin/admin-userfields.php', 'admin/usertitles' => 'pages/admin/admin-usertitles.php', diff --git a/qa-include/app/recalc.php b/qa-include/app/recalc.php index d94405d15..42d8e66eb 100644 --- a/qa-include/app/recalc.php +++ b/qa-include/app/recalc.php @@ -22,7 +22,7 @@ /* A full list of redundant (non-normal) information in the database that can be recalculated: - Recalculated in doreindexcontent: + Recalculated in reindex_content: ================================ ^titlewords (all): index of words in titles of posts ^contentwords (all): index of words in content of posts @@ -31,23 +31,23 @@ ^words (all): list of words used for indexes ^options (title=cache_*): cached values for various things (e.g. counting questions) - Recalculated in dorecountposts: + Recalculated in recount_posts: ============================== ^posts (upvotes, downvotes, netvotes, hotness, acount, amaxvotes, flagcount): number of votes, hotness, answers, answer votes, flags - Recalculated in dorecalcpoints: + Recalculated in users_points: =============================== ^userpoints (all except bonus): points calculation for all users ^options (title=cache_userpointscount): - Recalculated in dorecalccategories: + Recalculated in recalc_categories: =================================== ^posts (categoryid): assign to answers and comments based on their antecedent question ^posts (catidpath1, catidpath2, catidpath3): hierarchical path to category ids (requires QA_CATEGORY_DEPTH=4) ^categories (qcount): number of (visible) questions in each category ^categories (backpath): full (backwards) path of slugs to that category - Recalculated in dorebuildupdates: + Recalculated in refill_events: ================================= ^sharedevents (all): per-entity event streams (see big comment in /qa-include/db/favorites.php) ^userevents (all): per-subscriber event streams @@ -60,748 +60,32 @@ exit; } -require_once QA_INCLUDE_DIR . 'db/recalc.php'; -require_once QA_INCLUDE_DIR . 'db/post-create.php'; -require_once QA_INCLUDE_DIR . 'db/points.php'; -require_once QA_INCLUDE_DIR . 'db/selects.php'; -require_once QA_INCLUDE_DIR . 'db/admin.php'; -require_once QA_INCLUDE_DIR . 'db/users.php'; -require_once QA_INCLUDE_DIR . 'app/options.php'; -require_once QA_INCLUDE_DIR . 'app/post-create.php'; -require_once QA_INCLUDE_DIR . 'app/post-update.php'; - - /** - * Advance the recalculation operation represented by $state by a single step. - * $state can also be the name of a recalculation operation on its own. - * @param $state - * @return bool + * Return the process manager for the given process string. + * @param string $process + * @return Q2A_Admin_Recalc_AbstractProcessManager */ -function qa_recalc_perform_step(&$state) +function qa_recalc_get_process_manager($process) { - $continue = false; - - @list($operation, $length, $next, $done) = explode("\t", $state); - - switch ($operation) { - case 'doreindexcontent': - qa_recalc_transition($state, 'doreindexcontent_pagereindex'); - break; - - case 'doreindexcontent_pagereindex': - $pages = qa_db_pages_get_for_reindexing($next, 10); - - if (count($pages)) { - require_once QA_INCLUDE_DIR . 'app/format.php'; - - $lastpageid = max(array_keys($pages)); - - foreach ($pages as $pageid => $page) { - if (!($page['flags'] & QA_PAGE_FLAGS_EXTERNAL)) { - $searchmodules = qa_load_modules_with('search', 'unindex_page'); - foreach ($searchmodules as $searchmodule) { - $searchmodule->unindex_page($pageid); - } - - $searchmodules = qa_load_modules_with('search', 'index_page'); - if (count($searchmodules)) { - $indextext = qa_viewer_text($page['content'], 'html'); - - foreach ($searchmodules as $searchmodule) - $searchmodule->index_page($pageid, $page['tags'], $page['heading'], $page['content'], 'html', $indextext); - } - } - } - - $next = 1 + $lastpageid; - $done += count($pages); - $continue = true; - - } else { - qa_recalc_transition($state, 'doreindexcontent_postcount'); - } - break; - - case 'doreindexcontent_postcount': - qa_db_qcount_update(); - qa_db_acount_update(); - qa_db_ccount_update(); - - qa_recalc_transition($state, 'doreindexcontent_postreindex'); - break; - - case 'doreindexcontent_postreindex': - $posts = qa_db_posts_get_for_reindexing($next, 10); - - if (count($posts)) { - require_once QA_INCLUDE_DIR . 'app/format.php'; - - $lastpostid = max(array_keys($posts)); - - qa_db_prepare_for_reindexing($next, $lastpostid); - qa_suspend_update_counts(); - - foreach ($posts as $postid => $post) { - qa_post_unindex($postid); - qa_post_index($postid, $post['type'], $post['questionid'], $post['parentid'], $post['title'], $post['content'], - $post['format'], qa_viewer_text($post['content'], $post['format']), $post['tags'], $post['categoryid']); - } - - $next = 1 + $lastpostid; - $done += count($posts); - $continue = true; - - } else { - qa_db_truncate_indexes($next); - qa_recalc_transition($state, 'doreindexposts_wordcount'); - } - break; - - case 'doreindexposts_wordcount': - $wordids = qa_db_words_prepare_for_recounting($next, 1000); - - if (count($wordids)) { - $lastwordid = max($wordids); - - qa_db_words_recount($next, $lastwordid); - - $next = 1 + $lastwordid; - $done += count($wordids); - $continue = true; - - } else { - qa_db_tagcount_update(); // this is quick so just do it here - qa_recalc_transition($state, 'doreindexposts_complete'); - } - break; - - case 'dorecountposts': - qa_recalc_transition($state, 'dorecountposts_postcount'); - break; - - case 'dorecountposts_postcount': - qa_db_qcount_update(); - qa_db_acount_update(); - qa_db_ccount_update(); - qa_db_unaqcount_update(); - qa_db_unselqcount_update(); - - qa_recalc_transition($state, 'dorecountposts_votecount'); - break; - - case 'dorecountposts_votecount': - $postids = qa_db_posts_get_for_recounting($next, 1000); - - if (count($postids)) { - $lastpostid = max($postids); - - qa_db_posts_votes_recount($next, $lastpostid); - - $next = 1 + $lastpostid; - $done += count($postids); - $continue = true; - - } else { - qa_recalc_transition($state, 'dorecountposts_acount'); - } - break; - - case 'dorecountposts_acount': - $postids = qa_db_posts_get_for_recounting($next, 1000); - - if (count($postids)) { - $lastpostid = max($postids); - - qa_db_posts_answers_recount($next, $lastpostid); - - $next = 1 + $lastpostid; - $done += count($postids); - $continue = true; - - } else { - qa_db_unupaqcount_update(); - qa_recalc_transition($state, 'dorecountposts_complete'); - } - break; - - case 'dorecalcpoints': - qa_recalc_transition($state, 'dorecalcpoints_usercount'); - break; - - case 'dorecalcpoints_usercount': - qa_db_userpointscount_update(); // for progress update - not necessarily accurate - qa_db_uapprovecount_update(); // needs to be somewhere and this is the most appropriate place - qa_recalc_transition($state, 'dorecalcpoints_recalc'); - break; - - case 'dorecalcpoints_recalc': - $recalccount = 10; - $userids = qa_db_users_get_for_recalc_points($next, $recalccount + 1); // get one extra so we know where to start from next - $gotcount = count($userids); - $recalccount = min($recalccount, $gotcount); // can't recalc more than we got - - if ($recalccount > 0) { - $lastuserid = $userids[$recalccount - 1]; - qa_db_users_recalc_points($next, $lastuserid); - $done += $recalccount; - - } else { - $lastuserid = $next; // for truncation - } - - if ($gotcount > $recalccount) { // more left to do - $next = $userids[$recalccount]; // start next round at first one not recalculated - $continue = true; - } else { - qa_db_truncate_userpoints($lastuserid); - qa_db_userpointscount_update(); // quick so just do it here - qa_recalc_transition($state, 'dorecalcpoints_complete'); - } - break; - - case 'dorefillevents': - qa_recalc_transition($state, 'dorefillevents_qcount'); - break; - - case 'dorefillevents_qcount': - qa_db_qcount_update(); - qa_recalc_transition($state, 'dorefillevents_refill'); - break; - - case 'dorefillevents_refill': - $questionids = qa_db_qs_get_for_event_refilling($next, 1); - - if (count($questionids)) { - require_once QA_INCLUDE_DIR . 'app/events.php'; - require_once QA_INCLUDE_DIR . 'app/updates.php'; - require_once QA_INCLUDE_DIR . 'util/sort.php'; - - $lastquestionid = max($questionids); - - foreach ($questionids as $questionid) { - // Retrieve all posts relating to this question - - list($question, $childposts, $achildposts) = qa_db_select_with_pending( - qa_db_full_post_selectspec(null, $questionid), - qa_db_full_child_posts_selectspec(null, $questionid), - qa_db_full_a_child_posts_selectspec(null, $questionid) - ); - - // Merge all posts while preserving keys as postids - - $posts = array($questionid => $question); - - foreach ($childposts as $postid => $post) { - $posts[$postid] = $post; - } - - foreach ($achildposts as $postid => $post) { - $posts[$postid] = $post; - } - - // Creation and editing of each post - - foreach ($posts as $postid => $post) { - $followonq = ($post['basetype'] == 'Q') && ($postid != $questionid); - - if ($followonq) { - $updatetype = QA_UPDATE_FOLLOWS; - } elseif ($post['basetype'] == 'C' && @$posts[$post['parentid']]['basetype'] == 'Q') { - $updatetype = QA_UPDATE_C_FOR_Q; - } elseif ($post['basetype'] == 'C' && @$posts[$post['parentid']]['basetype'] == 'A') { - $updatetype = QA_UPDATE_C_FOR_A; - } else { - $updatetype = null; - } - - qa_create_event_for_q_user($questionid, $postid, $updatetype, $post['userid'], @$posts[$post['parentid']]['userid'], $post['created']); - - if (isset($post['updated']) && !$followonq) { - qa_create_event_for_q_user($questionid, $postid, $post['updatetype'], $post['lastuserid'], $post['userid'], $post['updated']); - } - } - - // Tags and categories of question - - qa_create_event_for_tags($question['tags'], $questionid, null, $question['userid'], $question['created']); - qa_create_event_for_category($question['categoryid'], $questionid, null, $question['userid'], $question['created']); - - // Collect comment threads - - $parentidcomments = array(); - - foreach ($posts as $postid => $post) { - if ($post['basetype'] == 'C') { - $parentidcomments[$post['parentid']][$postid] = $post; - } - } - - // For each comment thread, notify all previous comment authors of each comment in the thread (could get slow) - - foreach ($parentidcomments as $parentid => $comments) { - $keyuserids = array(); - - qa_sort_by($comments, 'created'); - - foreach ($comments as $comment) { - foreach ($keyuserids as $keyuserid => $dummy) { - if ($keyuserid != $comment['userid'] && $keyuserid != @$posts[$parentid]['userid']) { - qa_db_event_create_not_entity($keyuserid, $questionid, $comment['postid'], QA_UPDATE_FOLLOWS, $comment['userid'], $comment['created']); - } - } - - if (isset($comment['userid'])) { - $keyuserids[$comment['userid']] = true; - } - } - } - } - - $next = 1 + $lastquestionid; - $done += count($questionids); - $continue = true; - - } else { - qa_recalc_transition($state, 'dorefillevents_complete'); - } - break; - - case 'dorecalccategories': - qa_recalc_transition($state, 'dorecalccategories_postcount'); - break; - - case 'dorecalccategories_postcount': - qa_db_acount_update(); - qa_db_ccount_update(); - - qa_recalc_transition($state, 'dorecalccategories_postupdate'); - break; - - case 'dorecalccategories_postupdate': - $postids = qa_db_posts_get_for_recategorizing($next, 100); - - if (count($postids)) { - $lastpostid = max($postids); - - qa_db_posts_recalc_categoryid($next, $lastpostid); - qa_db_posts_calc_category_path($next, $lastpostid); - - $next = 1 + $lastpostid; - $done += count($postids); - $continue = true; - } else { - qa_recalc_transition($state, 'dorecalccategories_recount'); - } - break; - - case 'dorecalccategories_recount': - $categoryids = qa_db_categories_get_for_recalcs($next, 10); - - if (count($categoryids)) { - $lastcategoryid = max($categoryids); - - foreach ($categoryids as $categoryid) { - qa_db_ifcategory_qcount_update($categoryid); - } - - $next = 1 + $lastcategoryid; - $done += count($categoryids); - $continue = true; - } else { - qa_recalc_transition($state, 'dorecalccategories_backpaths'); - } - break; - - case 'dorecalccategories_backpaths': - $categoryids = qa_db_categories_get_for_recalcs($next, 10); - - if (count($categoryids)) { - $lastcategoryid = max($categoryids); - - qa_db_categories_recalc_backpaths($next, $lastcategoryid); - - $next = 1 + $lastcategoryid; - $done += count($categoryids); - $continue = true; - - } else { - qa_recalc_transition($state, 'dorecalccategories_complete'); - } - break; - - case 'dodeletehidden': - qa_recalc_transition($state, 'dodeletehidden_comments'); - break; - - case 'dodeletehidden_comments': - $posts = qa_db_posts_get_for_deleting('C', $next, 1); - - if (count($posts)) { - require_once QA_INCLUDE_DIR . 'app/posts.php'; - - $postid = $posts[0]; - qa_post_delete($postid); - - $next = 1 + $postid; - $done++; - $continue = true; - } else { - qa_recalc_transition($state, 'dodeletehidden_answers'); - } - break; - - case 'dodeletehidden_answers': - $posts = qa_db_posts_get_for_deleting('A', $next, 1); - - if (count($posts)) { - require_once QA_INCLUDE_DIR . 'app/posts.php'; - - $postid = $posts[0]; - qa_post_delete($postid); - - $next = 1 + $postid; - $done++; - $continue = true; - - } else { - qa_recalc_transition($state, 'dodeletehidden_questions'); - } - break; - - case 'dodeletehidden_questions': - $posts = qa_db_posts_get_for_deleting('Q', $next, 1); - - if (count($posts)) { - require_once QA_INCLUDE_DIR . 'app/posts.php'; - - $postid = $posts[0]; - qa_post_delete($postid); - - $next = 1 + $postid; - $done++; - $continue = true; - - } else { - qa_recalc_transition($state, 'dodeletehidden_complete'); - } - break; - - case 'doblobstodisk': - qa_recalc_transition($state, 'doblobstodisk_move'); - break; - - case 'doblobstodisk_move': - $blob = qa_db_get_next_blob_in_db($next); - - if (isset($blob)) { - require_once QA_INCLUDE_DIR . 'app/blobs.php'; - require_once QA_INCLUDE_DIR . 'db/blobs.php'; - - if (qa_write_blob_file($blob['blobid'], $blob['content'], $blob['format'])) { - qa_db_blob_set_content($blob['blobid'], null); - } - - $next = 1 + $blob['blobid']; - $done++; - $continue = true; - } else { - qa_recalc_transition($state, 'doblobstodisk_complete'); - } - break; - - case 'doblobstodb': - qa_recalc_transition($state, 'doblobstodb_move'); - break; - - case 'doblobstodb_move': - $blob = qa_db_get_next_blob_on_disk($next); - - if (isset($blob)) { - require_once QA_INCLUDE_DIR . 'app/blobs.php'; - require_once QA_INCLUDE_DIR . 'db/blobs.php'; - - $content = qa_read_blob_file($blob['blobid'], $blob['format']); - qa_db_blob_set_content($blob['blobid'], $content); - qa_delete_blob_file($blob['blobid'], $blob['format']); - - $next = 1 + $blob['blobid']; - $done++; - $continue = true; - } else { - qa_recalc_transition($state, 'doblobstodb_complete'); - } - break; - - case 'docachetrim': - qa_recalc_transition($state, 'docachetrim_process'); - break; - case 'docacheclear': - qa_recalc_transition($state, 'docacheclear_process'); - break; - - case 'docachetrim_process': - case 'docacheclear_process': - $cacheDriver = Q2A_Storage_CacheFactory::getCacheDriver(); - $cacheStats = $cacheDriver->getStats(); - $limit = min($cacheStats['files'], 500); - - if ($cacheStats['files'] > 0 && $next <= $length) { - $deleted = $cacheDriver->clear($limit, $next, ($operation === 'docachetrim_process')); - $done += $deleted; - $next += $limit - $deleted; // skip files that weren't deleted on next iteration - $continue = true; - } else { - qa_recalc_transition($state, 'docacheclear_complete'); - } - break; - - default: - $state = ''; - break; - } - - if ($continue) { - $state = $operation . "\t" . $length . "\t" . $next . "\t" . $done; - } - - return $continue && $done < $length; -} - - -/** - * Change the $state to represent the beginning of a new $operation - * @param $state - * @param $operation - */ -function qa_recalc_transition(&$state, $operation) -{ - $length = qa_recalc_stage_length($operation); - $next = (QA_FINAL_EXTERNAL_USERS && ($operation == 'dorecalcpoints_recalc')) ? '' : 0; - $done = 0; - - $state = $operation . "\t" . $length . "\t" . $next . "\t" . $done; -} - - -/** - * Return how many steps there will be in recalculation $operation - * @param $operation - * @return int - */ -function qa_recalc_stage_length($operation) -{ - switch ($operation) { - case 'doreindexcontent_pagereindex': - $length = qa_db_count_pages(); - break; - - case 'doreindexcontent_postreindex': - $length = qa_opt('cache_qcount') + qa_opt('cache_acount') + qa_opt('cache_ccount'); - break; - - case 'doreindexposts_wordcount': - $length = qa_db_count_words(); - break; - - case 'dorecalcpoints_recalc': - $length = qa_opt('cache_userpointscount'); - break; - - case 'dorecountposts_votecount': - case 'dorecountposts_acount': - case 'dorecalccategories_postupdate': - $length = qa_db_count_posts(); - break; - - case 'dorefillevents_refill': - $length = qa_opt('cache_qcount') + qa_db_count_posts('Q_HIDDEN'); - break; - - case 'dorecalccategories_recount': - case 'dorecalccategories_backpaths': - $length = qa_db_count_categories(); - break; - - case 'dodeletehidden_comments': - $length = count(qa_db_posts_get_for_deleting('C')); - break; - - case 'dodeletehidden_answers': - $length = count(qa_db_posts_get_for_deleting('A')); - break; - - case 'dodeletehidden_questions': - $length = count(qa_db_posts_get_for_deleting('Q')); - break; - - case 'doblobstodisk_move': - $length = qa_db_count_blobs_in_db(); - break; - - case 'doblobstodb_move': - $length = qa_db_count_blobs_on_disk(); - break; - - case 'docachetrim_process': - case 'docacheclear_process': - $cacheDriver = Q2A_Storage_CacheFactory::getCacheDriver(); - $cacheStats = $cacheDriver->getStats(); - $length = $cacheStats['files']; - break; - - default: - $length = 0; - break; + $processes = [ + 'recount_posts' => Q2A_Admin_Recalc_RecountPosts_ProcessManager::class, + 'reindex_content' => Q2A_Admin_Recalc_ReindexContent_ProcessManager::class, + 'users_points' => Q2A_Admin_Recalc_UsersPoints_ProcessManager::class, + 'refill_events' => Q2A_Admin_Recalc_RefillEvents_ProcessManager::class, + 'recalc_categories' => Q2A_Admin_Recalc_RecalcCategories_ProcessManager::class, + 'delete_hidden_posts' => Q2A_Admin_Recalc_DeleteHiddenPosts_ProcessManager::class, + 'blobs_to_disk' => Q2A_Admin_Recalc_BlobsToDisk_ProcessManager::class, + 'blobs_to_db' => Q2A_Admin_Recalc_BlobsToDb_ProcessManager::class, + 'cache_trim' => Q2A_Admin_Recalc_Caching_CacheTrim_ProcessManager::class, + 'cache_clear' => Q2A_Admin_Recalc_Caching_CacheClear_ProcessManager::class, + ]; + + // Make sure something is run and avoid the error handling for such an unlikely case + if (!isset($processes[$process])) { + $process = key($processes); } - return $length; -} - - -/** - * Return the translated language ID string replacing the progress and total in it. - * @access private - * @param string $langId Language string ID that contains 2 placeholders (^1 and ^2) - * @param int $progress Amount of processed elements - * @param int $total Total amount of elements - * - * @return string Returns the language string ID with their placeholders replaced with - * the formatted progress and total numbers - */ -function qa_recalc_progress_lang($langId, $progress, $total) -{ - return strtr(qa_lang($langId), array( - '^1' => qa_format_number($progress), - '^2' => qa_format_number($total), - )); -} - - -/** - * Return a string which gives a user-viewable version of $state - * @param $state - * @return string - */ -function qa_recalc_get_message($state) -{ - require_once QA_INCLUDE_DIR . 'app/format.php'; - - @list($operation, $length, $next, $done) = explode("\t", $state); - - $done = (int) $done; - $length = (int) $length; - - switch ($operation) { - case 'doreindexcontent_postcount': - case 'dorecountposts_postcount': - case 'dorecalccategories_postcount': - case 'dorefillevents_qcount': - $message = qa_lang('admin/recalc_posts_count'); - break; - - case 'doreindexcontent_pagereindex': - $message = qa_recalc_progress_lang('admin/reindex_pages_reindexed', $done, $length); - break; - - case 'doreindexcontent_postreindex': - $message = qa_recalc_progress_lang('admin/reindex_posts_reindexed', $done, $length); - break; - - case 'doreindexposts_complete': - $message = qa_lang('admin/reindex_posts_complete'); - break; - - case 'doreindexposts_wordcount': - $message = qa_recalc_progress_lang('admin/reindex_posts_wordcounted', $done, $length); - break; - - case 'dorecountposts_votecount': - $message = qa_recalc_progress_lang('admin/recount_posts_votes_recounted', $done, $length); - break; - - case 'dorecountposts_acount': - $message = qa_recalc_progress_lang('admin/recount_posts_as_recounted', $done, $length); - break; - - case 'dorecountposts_complete': - $message = qa_lang('admin/recount_posts_complete'); - break; - - case 'dorecalcpoints_usercount': - $message = qa_lang('admin/recalc_points_usercount'); - break; - - case 'dorecalcpoints_recalc': - $message = qa_recalc_progress_lang('admin/recalc_points_recalced', $done, $length); - break; - - case 'dorecalcpoints_complete': - $message = qa_lang('admin/recalc_points_complete'); - break; - - case 'dorefillevents_refill': - $message = qa_recalc_progress_lang('admin/refill_events_refilled', $done, $length); - break; - - case 'dorefillevents_complete': - $message = qa_lang('admin/refill_events_complete'); - break; - - case 'dorecalccategories_postupdate': - $message = qa_recalc_progress_lang('admin/recalc_categories_updated', $done, $length); - break; - - case 'dorecalccategories_recount': - $message = qa_recalc_progress_lang('admin/recalc_categories_recounting', $done, $length); - break; - - case 'dorecalccategories_backpaths': - $message = qa_recalc_progress_lang('admin/recalc_categories_backpaths', $done, $length); - break; - - case 'dorecalccategories_complete': - $message = qa_lang('admin/recalc_categories_complete'); - break; - - case 'dodeletehidden_comments': - $message = qa_recalc_progress_lang('admin/hidden_comments_deleted', $done, $length); - break; - - case 'dodeletehidden_answers': - $message = qa_recalc_progress_lang('admin/hidden_answers_deleted', $done, $length); - break; - - case 'dodeletehidden_questions': - $message = qa_recalc_progress_lang('admin/hidden_questions_deleted', $done, $length); - break; - - case 'dodeletehidden_complete': - $message = qa_lang('admin/delete_hidden_complete'); - break; - - case 'doblobstodisk_move': - case 'doblobstodb_move': - $message = qa_recalc_progress_lang('admin/blobs_move_moved', $done, $length); - break; - - case 'doblobstodisk_complete': - case 'doblobstodb_complete': - $message = qa_lang('admin/blobs_move_complete'); - break; - - case 'docachetrim_process': - case 'docacheclear_process': - $message = qa_recalc_progress_lang('admin/caching_delete_progress', $done, $length); - break; - - case 'docacheclear_complete': - $message = qa_lang('admin/caching_delete_complete'); - break; - - default: - $message = ''; - break; - } + $managerClassName = $processes[$process]; - return $message; + return new $managerClassName(); } diff --git a/qa-include/db/admin.php b/qa-include/db/admin.php index ab5df36e3..7a2519e00 100644 --- a/qa-include/db/admin.php +++ b/qa-include/db/admin.php @@ -75,8 +75,12 @@ function qa_db_count_posts($type = null, $fromuser = null) { $wheresql = ''; - if (isset($type)) - $wheresql .= ' WHERE type=' . qa_db_argument_to_mysql($type, true); + if (isset($type)) { + if (!is_array($type)) { + $type = [$type]; + } + $wheresql .= ' WHERE type IN ' . qa_db_argument_to_mysql($type, true, true); + } if (isset($fromuser)) $wheresql .= (strlen($wheresql) ? ' AND' : ' WHERE') . ' userid ' . ($fromuser ? 'IS NOT' : 'IS') . ' NULL'; diff --git a/qa-include/db/install.php b/qa-include/db/install.php index f7bd7d81b..b46a7801c 100644 --- a/qa-include/db/install.php +++ b/qa-include/db/install.php @@ -808,14 +808,14 @@ function qa_db_upgrade_tables() qa_db_upgrade_query('ALTER TABLE ^posts DROP COLUMN votes, ADD COLUMN upvotes ' . $definitions['posts']['upvotes'] . ' AFTER cookieid, ADD COLUMN downvotes ' . $definitions['posts']['downvotes'] . ' AFTER upvotes'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorecountposts'] = true; + $keyrecalc['recount_posts'] = true; break; case 3: qa_db_upgrade_query('ALTER TABLE ^userpoints ADD COLUMN upvoteds ' . $definitions['userpoints']['upvoteds'] . ' AFTER avoteds, ADD COLUMN downvoteds ' . $definitions['userpoints']['downvoteds'] . ' AFTER upvoteds'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorecalcpoints'] = true; + $keyrecalc['recount_posts'] = true; break; case 4: @@ -827,7 +827,7 @@ function qa_db_upgrade_tables() case 5: qa_db_upgrade_query('ALTER TABLE ^contentwords ADD COLUMN type ' . $definitions['contentwords']['type'] . ' AFTER count, ADD COLUMN questionid ' . $definitions['contentwords']['questionid'] . ' AFTER type'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['doreindexcontent'] = true; + $keyrecalc['reindex_content'] = true; break; // Up to here: Version 1.0 beta 2 @@ -835,7 +835,7 @@ function qa_db_upgrade_tables() case 6: qa_db_upgrade_query('ALTER TABLE ^userpoints ADD COLUMN cposts ' . $definitions['userpoints']['cposts'] . ' AFTER aposts'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorecalcpoints'] = true; + $keyrecalc['users_points'] = true; break; case 7: @@ -848,7 +848,7 @@ function qa_db_upgrade_tables() case 8: qa_db_upgrade_query('ALTER TABLE ^posts ADD KEY (type, acount, created)'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorecountposts'] = true; // for unanswered question count + $keyrecalc['recount_posts'] = true; // for unanswered question count break; // Up to here: Version 1.0 beta 3, 1.0, 1.0.1 beta, 1.0.1 @@ -911,7 +911,7 @@ function qa_db_upgrade_tables() case 14: qa_db_upgrade_query('ALTER TABLE ^userpoints DROP COLUMN qvotes, DROP COLUMN avotes, ADD COLUMN qupvotes ' . $definitions['userpoints']['qupvotes'] . ' AFTER aselecteds, ADD COLUMN qdownvotes ' . $definitions['userpoints']['qdownvotes'] . ' AFTER qupvotes, ADD COLUMN aupvotes ' . $definitions['userpoints']['aupvotes'] . ' AFTER qdownvotes, ADD COLUMN adownvotes ' . $definitions['userpoints']['adownvotes'] . ' AFTER aupvotes'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorecalcpoints'] = true; + $keyrecalc['users_points'] = true; break; // Up to here: Version 1.2 beta 1 @@ -933,7 +933,7 @@ function qa_db_upgrade_tables() case 16: qa_db_upgrade_table_columns($definitions, 'posts', array('format')); qa_db_upgrade_query($locktablesquery); - $keyrecalc['doreindexcontent'] = true; // because of new treatment of apostrophes in words + $keyrecalc['reindex_content'] = true; // because of new treatment of apostrophes in words break; case 17: @@ -1061,7 +1061,7 @@ function qa_db_upgrade_tables() qa_db_upgrade_query('ALTER TABLE ^words ADD COLUMN tagwordcount ' . $definitions['words']['tagwordcount'] . ' AFTER contentcount'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['doreindexcontent'] = true; + $keyrecalc['reindex_content'] = true; break; // Up to here: Version 1.4 developer preview @@ -1086,21 +1086,21 @@ function qa_db_upgrade_tables() qa_db_upgrade_query('ALTER TABLE ^posts ADD COLUMN flagcount ' . $definitions['posts']['flagcount'] . ' AFTER downvotes, ADD KEY type_3 (type, flagcount, created)'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorecountposts'] = true; + $keyrecalc['recount_posts'] = true; break; case 27: qa_db_upgrade_query('ALTER TABLE ^posts ADD COLUMN netvotes ' . $definitions['posts']['netvotes'] . ' AFTER downvotes, ADD KEY type_4 (type, netvotes, created)'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorecountposts'] = true; + $keyrecalc['recount_posts'] = true; break; case 28: qa_db_upgrade_query('ALTER TABLE ^posts ADD COLUMN views ' . $definitions['posts']['views'] . ' AFTER netvotes, ADD COLUMN hotness ' . $definitions['posts']['hotness'] . ' AFTER views, ADD KEY type_5 (type, views, created), ADD KEY type_6 (type, hotness)'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorecountposts'] = true; + $keyrecalc['recount_posts'] = true; break; case 29: @@ -1127,7 +1127,7 @@ function qa_db_upgrade_tables() qa_db_upgrade_query('ALTER TABLE ^posts ADD CONSTRAINT ^posts_ibfk_3 FOREIGN KEY (categoryid) REFERENCES ^categories(categoryid) ON DELETE SET NULL'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorecalccategories'] = true; + $keyrecalc['recalc_categories'] = true; break; // Up to here: Version 1.4 betas and release @@ -1212,7 +1212,7 @@ function qa_db_upgrade_tables() $locktablesquery .= ', ^userevents WRITE'; qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorefillevents'] = true; + $keyrecalc['refill_events'] = true; break; case 37: @@ -1233,7 +1233,7 @@ function qa_db_upgrade_tables() $locktablesquery .= ', ^sharedevents WRITE'; qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorefillevents'] = true; + $keyrecalc['refill_events'] = true; break; case 38: @@ -1319,7 +1319,7 @@ function qa_db_upgrade_tables() qa_db_upgrade_query('ALTER TABLE ^posts DROP KEY selchildid, ADD KEY selchildid (selchildid, type, created), ADD COLUMN amaxvote SMALLINT UNSIGNED NOT NULL DEFAULT 0 AFTER acount, ADD KEY type_7 (type, amaxvote, created)'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorecountposts'] = true; + $keyrecalc['recount_posts'] = true; break; case 47: @@ -1472,8 +1472,8 @@ function qa_db_upgrade_tables() qa_db_upgrade_query('DELETE ^uservotes FROM ^uservotes JOIN ^posts ON ^uservotes.postid=^posts.postid AND ^uservotes.userid=^posts.userid'); qa_db_upgrade_query($locktablesquery); - $keyrecalc['dorecountposts'] = true; - $keyrecalc['dorecalcpoints'] = true; + $keyrecalc['recount_posts'] = true; + $keyrecalc['users_points'] = true; break; // Up to here: Version 1.7 @@ -1613,17 +1613,22 @@ function qa_db_upgrade_tables() // Perform any necessary recalculations, as determined by upgrade steps - foreach ($keyrecalc as $state => $dummy) { - while ($state) { - set_time_limit(60); + foreach ($keyrecalc as $process => $dummy) { + set_time_limit(60); - $stoptime = time() + 2; + $stoptime = time() + 3; - while (qa_recalc_perform_step($state) && (time() < $stoptime)) - ; + $processManager = qa_recalc_get_process_manager($process); + do { + do { + $result = $processManager->execute(true); + $stepFinished = $result['step_state']['is_finished'] ?? false; + $processedItems = $result['step_state']['processed_items'] ?? 0; + $shouldShowProgress = $result['process_finished'] || $stepFinished || $processedItems === 0; + } while (!$shouldShowProgress && time() < $stoptime); - qa_db_upgrade_progress(qa_recalc_get_message($state)); - } + qa_db_upgrade_progress(qa_html($result['message'])); + } while (!$result['process_finished']); } } diff --git a/qa-include/db/recalc.php b/qa-include/db/recalc.php index 351cd751b..3c8e0714b 100644 --- a/qa-include/db/recalc.php +++ b/qa-include/db/recalc.php @@ -66,11 +66,37 @@ function qa_db_pages_get_for_reindexing($startpageid, $count) function qa_db_posts_get_for_reindexing($startpostid, $count) { return qa_db_read_all_assoc(qa_db_query_sub( - "SELECT ^posts.postid, ^posts.title, ^posts.content, ^posts.format, ^posts.tags, ^posts.categoryid, ^posts.type, IF (^posts.type='Q', ^posts.postid, IF(parent.type='Q', parent.postid, grandparent.postid)) AS questionid, ^posts.parentid FROM ^posts LEFT JOIN ^posts AS parent ON ^posts.parentid=parent.postid LEFT JOIN ^posts as grandparent ON parent.parentid=grandparent.postid WHERE ^posts.postid>=# AND ( (^posts.type='Q') OR (^posts.type='A' AND parent.type<=>'Q') OR (^posts.type='C' AND parent.type<=>'Q') OR (^posts.type='C' AND parent.type<=>'A' AND grandparent.type<=>'Q') ) ORDER BY postid LIMIT #", + 'SELECT ^posts.postid, ^posts.title, ^posts.content, ^posts.format, ^posts.tags, ^posts.categoryid, ' . + '^posts.type, IF (^posts.type = "Q", ^posts.postid, IF(parent.type = "Q", parent.postid, grandparent.postid)) AS questionid, ' . + '^posts.parentid FROM ^posts ' . + 'LEFT JOIN ^posts AS parent ON ^posts.parentid = parent.postid ' . + 'LEFT JOIN ^posts AS grandparent ON parent.parentid = grandparent.postid ' . + 'WHERE ^posts.postid >= # AND (' . + '^posts.type = "Q" OR ' . + '(^posts.type = "A" AND parent.type <=> "Q") OR ' . + '(^posts.type = "C" AND parent.type <=> "Q") OR ' . + '(^posts.type = "C" AND parent.type <=> "A" AND grandparent.type <=> "Q")' . + ') ' . + 'ORDER BY postid ' . + 'LIMIT #', $startpostid, $count ), 'postid'); } +function qa_db_posts_count_for_reindexing() +{ + return (int)qa_db_read_one_value(qa_db_query_sub( + 'SELECT COUNT(*) FROM ^posts ' . + 'LEFT JOIN ^posts AS parent ON ^posts.parentid = parent.postid ' . + 'LEFT JOIN ^posts AS grandparent ON parent.parentid = grandparent.postid ' . + 'WHERE ^posts.type = "Q" OR ' . + '(^posts.type = "A" AND parent.type <=> "Q") OR ' . + '(^posts.type = "C" AND parent.type <=> "Q") OR ' . + '(^posts.type = "C" AND parent.type <=> "A" AND grandparent.type <=> "Q")' + )); +} + + /** * Prepare posts $firstpostid to $lastpostid for reindexing in the database by removing their prior index entries @@ -213,6 +239,8 @@ function qa_db_posts_get_for_recounting($startpostid, $count) */ function qa_db_posts_votes_recount($firstpostid, $lastpostid) { + require_once QA_INCLUDE_DIR . 'db/hotness.php'; + qa_db_query_sub( 'UPDATE ^posts AS x, (SELECT ^posts.postid, COALESCE(SUM(GREATEST(0,^uservotes.vote)),0) AS upvotes, -COALESCE(SUM(LEAST(0,^uservotes.vote)),0) AS downvotes, COALESCE(SUM(IF(^uservotes.flag, 1, 0)),0) AS flagcount FROM ^posts LEFT JOIN ^uservotes ON ^uservotes.postid=^posts.postid WHERE ^posts.postid>=# AND ^posts.postid<=# GROUP BY postid) AS a SET x.upvotes=a.upvotes, x.downvotes=a.downvotes, x.netvotes=a.upvotes-a.downvotes, x.flagcount=a.flagcount WHERE x.postid=a.postid', $firstpostid, $lastpostid @@ -244,22 +272,25 @@ function qa_db_posts_answers_recount($firstpostid, $lastpostid) /** * Return the ids of up to $count users in the database starting from $startuserid - * If using single sign-on integration, base this on user activity rather than the users table which we don't have - * @param $startuserid + * When using external users, users are fetched based on their activity rather than the users table which we don't have. + * @param $lastProcessedUserId * @param $count * @return array */ -function qa_db_users_get_for_recalc_points($startuserid, $count) +function qa_db_users_get_for_recalc_points($lastProcessedUserId, $count) { if (QA_FINAL_EXTERNAL_USERS) { return qa_db_read_all_values(qa_db_query_sub( - 'SELECT userid FROM ((SELECT DISTINCT userid FROM ^posts WHERE userid>=# ORDER BY userid LIMIT #) UNION (SELECT DISTINCT userid FROM ^uservotes WHERE userid>=# ORDER BY userid LIMIT #)) x ORDER BY userid LIMIT #', - $startuserid, $count, $startuserid, $count, $count + 'SELECT userid FROM (' . + '(SELECT DISTINCT userid FROM ^posts WHERE userid > # ORDER BY userid LIMIT #) UNION ' . + '(SELECT DISTINCT userid FROM ^uservotes WHERE userid > # ORDER BY userid LIMIT #)' . + ') x ORDER BY userid LIMIT #', + $lastProcessedUserId, $count, $lastProcessedUserId, $count, $count )); } else { return qa_db_read_all_values(qa_db_query_sub( - 'SELECT DISTINCT userid FROM ^users WHERE userid>=# ORDER BY userid LIMIT #', - $startuserid, $count + 'SELECT userid FROM ^users WHERE userid > # ORDER BY userid LIMIT #', + $lastProcessedUserId, $count )); } } @@ -413,11 +444,31 @@ function qa_db_posts_get_for_deleting($type, $startpostid = 0, $limit = null) $limitsql = isset($limit) ? (' ORDER BY ^posts.postid LIMIT ' . (int)$limit) : ''; return qa_db_read_all_values(qa_db_query_sub( - "SELECT ^posts.postid FROM ^posts LEFT JOIN ^posts AS child ON child.parentid=^posts.postid LEFT JOIN ^posts AS dupe ON dupe.closedbyid=^posts.postid WHERE ^posts.type=$ AND ^posts.postid>=# AND child.postid IS NULL AND dupe.postid IS NULL" . $limitsql, + 'SELECT ^posts.postid FROM ^posts ' . + 'LEFT JOIN ^posts AS child ON child.parentid = ^posts.postid ' . + 'LEFT JOIN ^posts AS dupe ON dupe.closedbyid = ^posts.postid ' . + 'WHERE ^posts.type = $ AND ^posts.postid >= # AND child.postid IS NULL AND dupe.postid IS NULL' . + $limitsql, $type . '_HIDDEN', $startpostid )); } +/** + * Return the count of $type posts that can be deleted from the database (i.e. have no dependents) + * @param $type + * @return array + */ +function qa_db_posts_count_for_deleting($type) +{ + return qa_db_read_one_value(qa_db_query_sub( + 'SELECT COUNT(*) FROM ^posts ' . + 'LEFT JOIN ^posts AS child ON child.parentid = ^posts.postid ' . + 'LEFT JOIN ^posts AS dupe ON dupe.closedbyid = ^posts.postid ' . + 'WHERE ^posts.type = $ AND child.postid IS NULL AND dupe.postid IS NULL', + $type . '_HIDDEN' + )); +} + // For moving blobs between database and disk... @@ -431,16 +482,20 @@ function qa_db_count_blobs_in_db() /** - * Return the id, content and format of the first blob whose content is stored in the database starting from $startblobid - * @param $startblobid - * @return array|null + * Return the id, content and format of the blobs whose content are stored in the database immediately + * after $lastBlobId (excluding it), returning the amount defined in $count + * @param $lastBlobId + * @param $count + * @return array */ -function qa_db_get_next_blob_in_db($startblobid) +function qa_db_get_next_blobs_in_db($lastBlobId, $count) { - return qa_db_read_one_assoc(qa_db_query_sub( - 'SELECT blobid, content, format FROM ^blobs WHERE blobid>=# AND content IS NOT NULL LIMIT 1', - $startblobid - ), true); + return qa_db_read_all_assoc(qa_db_query_sub( + 'SELECT blobid, content, format FROM ^blobs ' . + 'WHERE blobid > # AND content IS NOT NULL ' . + 'LIMIT #', + $lastBlobId, $count + ), 'blobid'); } @@ -452,16 +507,19 @@ function qa_db_count_blobs_on_disk() return qa_db_read_one_value(qa_db_query_sub('SELECT COUNT(*) FROM ^blobs WHERE content IS NULL')); } - /** - * Return the id and format of the first blob whose content is stored on disk starting from $startblobid - * @param $startblobid - * @return array|null + * Return the id and format of the blobs whose content are stored on disk immediately after $lastBlobId + * (excluding it), returning the amount defined in $count + * @param $lastBlobId + * @param $count + * @return array */ -function qa_db_get_next_blob_on_disk($startblobid) +function qa_db_get_next_blobs_on_disk($lastBlobId, $count) { - return qa_db_read_one_assoc(qa_db_query_sub( - 'SELECT blobid, format FROM ^blobs WHERE blobid>=# AND content IS NULL LIMIT 1', - $startblobid - ), true); + return qa_db_read_all_assoc(qa_db_query_sub( + 'SELECT blobid, format FROM ^blobs ' . + 'WHERE blobid > # AND content IS NULL ' . + 'LIMIT #', + $lastBlobId, $count + ), 'blobid'); } diff --git a/qa-include/db/users.php b/qa-include/db/users.php index 62844176b..4bd994ea3 100644 --- a/qa-include/db/users.php +++ b/qa-include/db/users.php @@ -400,6 +400,8 @@ function qa_db_users_get_mailing_next($lastuserid, $count) */ function qa_db_uapprovecount_update($increment = null) { + require_once QA_INCLUDE_DIR . 'app/users.php'; + if (QA_FINAL_EXTERNAL_USERS) { return; } diff --git a/qa-include/lang/qa-lang-admin.php b/qa-include/lang/qa-lang-admin.php index d1ea83f4a..d05a2b131 100644 --- a/qa-include/lang/qa-lang-admin.php +++ b/qa-include/lang/qa-lang-admin.php @@ -41,22 +41,22 @@ 'basic_editor' => 'Basic Editor', 'before_main_menu' => 'Before tabs at top', 'blobs_directory_error' => 'The directory ^ defined as QA_BLOBS_DIRECTORY is not writable by the web server.', - 'blobs_move_complete' => 'Migration of uploaded images and documents completed.', - 'blobs_move_moved' => 'Migration ^1 of ^2 uploaded images and documents', - 'blobs_stop' => 'Stop migrating', - 'blobs_to_db' => 'Blobs to database', - 'blobs_to_db_note' => '- migrate all uploaded images and documents from disk files to the database', - 'blobs_to_disk' => 'Blobs to disk', - 'blobs_to_disk_note' => '- migrate all uploaded images and documents from the database to disk files', + 'blobs_move_complete' => 'Migration of uploaded images and documents completed.', // @deprecated + 'blobs_move_moved' => 'Migration ^1 of ^2 uploaded images and documents', // @deprecated + 'blobs_stop' => 'Stop migrating', // @deprecated + 'blobs_to_db' => 'Blobs to database', // @deprecated + 'blobs_to_db_note' => '- migrate all uploaded images and documents from disk files to the database', // @deprecated + 'blobs_to_disk' => 'Blobs to disk', // @deprecated + 'blobs_to_disk_note' => '- migrate all uploaded images and documents from the database to disk files', // @deprecated 'block_button' => 'block', 'block_ips_note' => 'Use a hyphen for ranges or * to match any number. Examples: 192.168.0.4 , 192.168.0.0-192.168.0.31 , 192.168.0.*', 'block_user_popup' => 'Block user', 'block_words_note' => 'Use a * to match any letters. Examples: doh (will only match exact word doh) , doh* (will match doh or dohno) , do*h (will match doh, dooh, dough).', 'caching_cleanup' => 'Caching clean-up operations', 'caching_delete_all' => 'Delete entire cache', - 'caching_delete_complete' => 'Cache successfully deleted', + 'caching_delete_complete' => 'Cache successfully deleted', // @deprecated 'caching_delete_expired' => 'Delete expired cache', - 'caching_delete_progress' => 'Deleted ^1 of ^2 cache files...', + 'caching_delete_progress' => 'Processed ^1 of ^2 cache items...', 'caching_dir_error' => 'The directory ^ defined as QA_CACHE_DIRECTORY is not writable by the web server.', 'caching_dir_missing' => 'Cache directory has not been defined.', 'caching_dir_public' => 'The directory ^ defined as QA_CACHE_DIRECTORY must be outside the public root.', @@ -96,9 +96,9 @@ 'delete_category' => 'Delete this category', 'delete_category_reassign' => 'Delete this category and reassign its questions to:', 'delete_field' => 'Delete this field', - 'delete_hidden' => 'Delete hidden posts', - 'delete_hidden_complete' => 'All hidden posts without dependents have been deleted', - 'delete_hidden_note' => ' - all hidden questions, answer and comments without dependents', + 'delete_hidden' => 'Delete hidden posts', // @deprecated + 'delete_hidden_complete' => 'All hidden posts without dependents have been deleted', // @deprecated + 'delete_hidden_note' => ' - all hidden questions, answer and comments without dependents', // @deprecated 'delete_link' => 'Delete this link', 'delete_page' => 'Delete this page', 'delete_stop' => 'Stop deleting', @@ -182,6 +182,7 @@ 'permit_to_view' => 'Visible for:', 'php_version' => 'PHP version:', 'pixels' => 'pixels', + 'please_wait' => 'Please, wait...', // @since 1.8.9 'plugin_module' => ' (plugin module: ^)', 'plugin_pages_explanation' => 'Pages available via plugins:', 'plugins_title' => 'Plugins', @@ -191,6 +192,12 @@ 'points_title' => 'Points', 'position' => 'Position:', 'posting_title' => 'Posting', + 'process_complete' => 'Process complete.', // @since 1.8.9 + 'process_continue' => 'Continue process', // @since 1.8.9 + 'process_restart' => 'Restart process', // @since 1.8.9 + 'process_start' => 'Start process', // @since 1.8.9 + 'process_stop' => 'Stop process', // @since 1.8.9 + 'process_unfinished' => 'This process is unfinished', // @since 1.8.9 'profile_fields' => 'Extra fields on user pages or registration form:', 'q2a_build_date' => 'Build date:', 'q2a_db_size' => 'Database size:', @@ -199,38 +206,57 @@ 'q2a_version' => 'Question2Answer version:', 'question_lists' => 'Question lists', 'question_pages' => 'Question pages', - 'recalc_categories' => 'Recalculate categories', + 'recalc_blobs_to_db_note' => 'Migrate all uploaded images and documents from disk files to the database', // @since 1.8.9 + 'recalc_blobs_to_db_title' => 'Blobs to database', // @since 1.8.9 + 'recalc_blobs_to_disk_note' => 'Migrate all uploaded images and documents from the database to disk files', // @since 1.8.9 + 'recalc_blobs_to_disk_title' => 'Blobs to disk', // @since 1.8.9 + 'recalc_categories' => 'Recalculate categories', // @deprecated 'recalc_categories_backpaths' => 'Recalculating URL paths for ^1 of ^2 categories...', - 'recalc_categories_complete' => 'All categories were successfully recalculated.', - 'recalc_categories_note' => ' - for post categories and category counts', + 'recalc_categories_complete' => 'All categories were successfully recalculated.', // @deprecated + 'recalc_categories_note' => ' - for post categories and category counts', // @deprecated 'recalc_categories_recounting' => 'Recounting questions for ^1 of ^2 categories...', 'recalc_categories_updated' => 'Recalculated for ^1 of ^2 posts...', + 'recalc_delete_hidden_posts_note' => 'All hidden questions, answer and comments without dependents', // @since 1.8.9 + 'recalc_delete_hidden_posts_title' => 'Delete hidden posts', // @since 1.8.9 'recalc_hotness_q_view_note' => 'May slightly improve page speed if disabled, but hotness values will become out of date if views are included in hotness settings', - 'recalc_points' => 'Recalculate user points', - 'recalc_points_complete' => 'All user points were successfully recalculated.', - 'recalc_points_note' => ' - for user ranking and points displays', - 'recalc_points_recalced' => 'Recalculated for ^1 of ^2 users...', - 'recalc_points_usercount' => 'Estimating total number of users...', - 'recalc_posts_count' => 'Getting total number of questions, answers and comments...', - 'recalc_stop' => 'Stop recalculating', + 'recalc_needed' => 'The changes made require to manually run: ^1^2^3', // @since 1.8.9 + 'recalc_points' => 'Recalculate user points', // @deprecated + 'recalc_points_complete' => 'All user points were successfully recalculated.', // @deprecated + 'recalc_points_note' => ' - for user ranking and points displays', // @deprecated + 'recalc_points_recalced' => 'Recalculated for ^1 of ^2 users...', // @deprecated + 'recalc_points_usercount' => 'Estimating total number of users...', // @deprecated + 'recalc_posts_count' => 'Counted ^1 of ^2 posts...', // @since 1.8.9 it receives 2 parameters + 'recalc_recalc_categories_note' => 'For post categories and category counts', // @since 1.8.9 + 'recalc_recalc_categories_title' => 'Recalculate categories', // @since 1.8.9 + 'recalc_recount_posts_note' => 'The number of answers, votes, flags and hotness for each post', // @since 1.8.9 + 'recalc_recount_posts_title' => 'Recount posts', // @since 1.8.9 + 'recalc_refill_events_note' => 'For each user\'s list of updates', // @since 1.8.9 + 'recalc_refill_events_title' => 'Refill event streams', // @since 1.8.9 + 'recalc_reindex_content_note' => 'For searching and related question suggestions', // @since 1.8.9 + 'recalc_reindex_content_title' => 'Reindex content', // @since 1.8.9 + 'recalc_stop' => 'Stop recalculating', // @deprecated + 'recalc_users_points_note' => 'For user ranking and points displays', // @since 1.8.9 + 'recalc_users_points_recalced' => 'Recalculated for ^1 of ^2 users...', // @since 1.8.9 + 'recalc_users_points_title' => 'Recalculate user points', // @since 1.8.9 + 'recalc_users_points_usercount' => 'Counted ^1 of ^2 users...', // @since 1.8.9 'recent_approve_title' => 'Recent content waiting for approval', 'recent_hidden_title' => 'Recent hidden content', - 'recount_posts' => 'Recount posts', + 'recount_posts' => 'Recount posts', // @deprecated 'recount_posts_as_recounted' => 'Recounted answers and hotness for ^1 of ^2 posts...', - 'recount_posts_complete' => 'All posts were successfully recounted.', - 'recount_posts_note' => ' - the number of answers, votes, flags and hotness for each post', + 'recount_posts_complete' => 'All posts were successfully recounted.', // @deprecated + 'recount_posts_note' => ' - the number of answers, votes, flags and hotness for each post', // @deprecated 'recount_posts_stop' => 'Stop recounting', 'recount_posts_votes_recounted' => 'Recounted votes and flags for ^1 of ^2 posts...', - 'refill_events' => 'Refill event streams', + 'refill_events' => 'Refill event streams', // @deprecated 'refill_events_complete' => 'All events streams were successfully refilled', - 'refill_events_note' => ' - for each user\'s list of updates', + 'refill_events_note' => ' - for each user\'s list of updates', // @deprecated 'refill_events_refilled' => 'Refilled for ^1 of ^2 questions...', 'registration_fields' => 'add registration fields', - 'reindex_content' => 'Reindex content', - 'reindex_content_note' => ' - for searching and related question suggestions', + 'reindex_content' => 'Reindex content', // @deprecated + 'reindex_content_note' => ' - for searching and related question suggestions', // @deprecated 'reindex_content_stop' => 'Stop reindexing', 'reindex_pages_reindexed' => 'Reindexed ^1 of ^2 pages...', - 'reindex_posts_complete' => 'All posts were successfully reindexed.', + 'reindex_posts_complete' => 'All posts were successfully reindexed.', // @deprecated 'reindex_posts_reindexed' => 'Reindexed ^1 of ^2 posts...', 'reindex_posts_wordcounted' => 'Recounted ^1 of ^2 words...', 'requires_php_version' => 'Disabled - requires PHP ^ or later', @@ -239,7 +265,7 @@ 'reset_options_confirm' => 'Are you sure you want to reset all options on this page to their defaults?', 'resume_mailing_button' => 'Resume Mailing', 'save_options_button' => 'Save Options', - 'save_recalc_button' => 'Save and Recalculate', + 'save_recalc_button' => 'Save and Recalculate', // @deprecated 'save_view_button' => 'Save and View', 'send_test_button' => 'Send Test to Me', 'show_defaults_button' => 'Show Defaults', @@ -249,7 +275,7 @@ 'spam_title' => 'Spam', 'start_mailing_button' => 'Start Mailing', 'stats_title' => 'Stats', - 'stop_recalc_warning' => 'A database clean-up operation is running. If you close this page now, the operation will be interrupted.', + 'stop_recalc_warning' => 'A database clean-up operation is running. If you close this page now, the operation will be interrupted.', // @deprecated 'tag_pages' => 'Tag pages', 'tags' => 'Tags', 'tags_and_categories' => 'Tags and Categories', diff --git a/qa-include/pages/admin/admin-categories.php b/qa-include/pages/admin/admin-categories.php index 3c9bed43e..aadb8fcc9 100644 --- a/qa-include/pages/admin/admin-categories.php +++ b/qa-include/pages/admin/admin-categories.php @@ -93,6 +93,8 @@ } } +$pendingRecalcs = json_decode(qa_opt('recalc_pending_processes'), true) ?? []; + $errors = array(); // Process saving an old or new category @@ -112,7 +114,11 @@ else { $inreassign = qa_get_category_field_value('reassign'); qa_db_category_reassign($editcategory['categoryid'], $inreassign); - qa_redirect(qa_request(), array('recalc' => 1, 'edit' => $editcategory['categoryid'])); + + $pendingRecalcs[] = 'recalc_categories'; + qa_opt('recalc_pending_processes', json_encode($pendingRecalcs)); + + qa_redirect(qa_request(), array('edit' => $editcategory['categoryid'])); } } elseif (qa_clicked('dosavecategory')) { @@ -124,7 +130,11 @@ $inreassign = qa_get_category_field_value('reassign'); qa_db_category_reassign($editcategory['categoryid'], $inreassign); qa_db_category_delete($editcategory['categoryid']); - qa_redirect(qa_request(), array('recalc' => 1, 'edit' => $editcategory['parentid'])); + + $pendingRecalcs[] = 'recalc_categories'; + qa_opt('recalc_pending_processes', json_encode($pendingRecalcs)); + + qa_redirect(qa_request(), array('edit' => $editcategory['parentid'])); } } else { @@ -207,8 +217,6 @@ if (isset($editcategory['categoryid'])) { // changing existing category qa_db_category_rename($editcategory['categoryid'], $inname, $inslug); - $recalc = false; - if ($setparent) { qa_db_category_set_parent($editcategory['categoryid'], $inparentid); $recalc = true; @@ -218,7 +226,12 @@ $recalc = $hassubcategory && $inslug !== $editcategory['tags']; } - qa_redirect(qa_request(), array('edit' => $editcategory['categoryid'], 'saved' => true, 'recalc' => (int)$recalc)); + if ($recalc) { + $pendingRecalcs[] = 'recalc_categories'; + qa_opt('recalc_pending_processes', json_encode($pendingRecalcs)); + } + + qa_redirect(qa_request(), array('edit' => $editcategory['categoryid'], 'saved' => true)); } else { // creating a new one $categoryid = qa_db_category_create($inparentid, $inname, $inslug); @@ -259,7 +272,6 @@ 'buttons' => array( 'save' => array( - 'tags' => 'id="dosaveoptions"', // just used for qa_recalc_click() 'label' => qa_lang_html('main/save_button'), ), @@ -324,7 +336,6 @@ 'buttons' => array( 'save' => array( - 'tags' => 'id="dosaveoptions"', // just used for qa_recalc_click 'label' => qa_lang_html(isset($editcategory['categoryid']) ? 'main/save_button' : 'admin/add_category_button'), ), @@ -543,7 +554,7 @@ 'buttons' => array( 'save' => array( - 'tags' => 'name="dosaveoptions" id="dosaveoptions"', + 'tags' => 'name="dosaveoptions"', 'label' => qa_lang_html('main/save_button'), ), @@ -613,16 +624,13 @@ unset($qa_content['form']['buttons']['save']); } -if (qa_get('recalc')) { - $qa_content['form']['ok'] = '' . qa_lang_html('admin/recalc_categories') . ''; - $qa_content['form']['hidden']['code_recalc'] = qa_get_form_security_code('admin/recalc'); - $qa_content['script_rel'][] = 'qa-content/qa-admin.js?' . QA_VERSION; - $qa_content['script_var']['qa_warning_recalc'] = qa_lang('admin/stop_recalc_warning'); - - $qa_content['script_onloads'][] = array( - "qa_recalc_click('dorecalccategories', document.getElementById('dosaveoptions'), null, 'recalc_ok');" - ); +if (in_array('recalc_categories', $pendingRecalcs)) { + $qa_content['error'] = strtr(qa_lang_html('admin/recalc_needed'), [ + '^1' => sprintf('', qa_path_html('admin/stats', null, null, null, 'form_recalc_categories')), + '^2' => qa_lang_html('admin/recalc_recalc_categories_title'), + '^3' => '', + ]); } $qa_content['navigation']['sub'] = qa_admin_sub_navigation(); diff --git a/qa-include/pages/admin/admin-default.php b/qa-include/pages/admin/admin-default.php index 0e898a089..9e58e7a6b 100644 --- a/qa-include/pages/admin/admin-default.php +++ b/qa-include/pages/admin/admin-default.php @@ -672,7 +672,6 @@ } return include QA_INCLUDE_DIR . 'qa-page-not-found.php'; - break; } @@ -689,7 +688,9 @@ $errors = array(); -$recalchotness = false; +$pendingRecalcs = json_decode(qa_opt('recalc_pending_processes'), true) ?? []; +$recalchotness = in_array('recount_posts', $pendingRecalcs); + $startmailing = false; $securityexpired = false; @@ -737,8 +738,12 @@ case 'hot_weight_votes': case 'hot_weight_q_age': case 'hot_weight_a_age': - if (qa_opt($optionname) != $optionvalue) + if (qa_opt($optionname) != $optionvalue && !$recalchotness) { $recalchotness = true; + + $pendingRecalcs[] = 'recount_posts'; + qa_opt('recalc_pending_processes', json_encode($pendingRecalcs)); + } break; case 'block_ips_write': @@ -866,6 +871,14 @@ $qa_content['title'] = qa_lang_html('admin/admin_title') . ' - ' . qa_lang_html($subtitle); $qa_content['error'] = $securityexpired ? qa_lang_html('admin/form_security_expired') : qa_admin_page_error(); +if (empty($qa_content['error']) && $recalchotness && $adminsection === 'lists') { + $qa_content['error'] = strtr(qa_lang_html('admin/recalc_needed'), [ + '^1' => sprintf('', qa_path_html('admin/stats', null, null, null, 'form_recount_posts')), + '^2' => qa_lang_html('admin/recalc_recount_posts_title'), + '^3' => '', + ]); +} + $qa_content['script_rel'][] = 'qa-content/qa-admin.js?' . QA_VERSION; $qa_content['form'] = array( @@ -896,17 +909,7 @@ ), ); -if ($recalchotness) { - $qa_content['form']['ok'] = ''; - $qa_content['form']['hidden']['code_recalc'] = qa_get_form_security_code('admin/recalc'); - - $qa_content['script_var']['qa_warning_recalc'] = qa_lang('admin/stop_recalc_warning'); - - $qa_content['script_onloads'][] = array( - "qa_recalc_click('dorecountposts', document.getElementById('dosaveoptions'), null, 'recalc_ok');" - ); - -} elseif ($startmailing) { +if ($startmailing) { if (qa_post_text('has_js')) { $qa_content['form']['ok'] = '' . qa_html($mailingprogress) . ''; @@ -1815,43 +1818,77 @@ function qa_optionfield_make_select(&$optionfield, $options, $value, $default) $qa_content['error'] = $cacheDriver->getError(); $cacheStats = $cacheDriver->getStats(); - $qa_content['form_2'] = array( - 'tags' => 'method="post" action="' . qa_path_html('admin/recalc') . '"', + $qa_lang_keys = ['please_wait', 'process_start', 'process_stop']; + + $qa_langs = []; + foreach ($qa_lang_keys as $key) { + $qa_langs[$key] = qa_lang('admin/' . $key); + } + + $qa_content['script_lines'][] = [ + sprintf('const qa_langs = %s;', json_encode($qa_langs)), + 'const cachingProcessOptions = {', + ' forceRestart: true,', + ' requiresServerTracking: false,', + ' callbackStart: process => document.getElementById(\'process_type_select\').disabled = true,', + ' callbackStop: hasFinished => document.getElementById(\'process_type_select\').disabled = false', + '};', + ]; + + $qa_content['script_onloads'][] = [ + 'const processTypeSelect = document.getElementById(\'process_type_select\');', + 'processTypeSelect.addEventListener(\'change\', event => {', + ' document.querySelector(\'[data-caching_button_id="caching_button"]\').id = event.target.value;' . + ' document.querySelector(\'[data-caching_status_id="caching_status"]\').id = event.target.value + \'_status\';' . + '});', + ]; + + $qa_content['form_2'] = [ + 'tags' => sprintf('method="post" action="%s"', qa_path_html('admin/recalc')), 'title' => qa_lang_html('admin/caching_cleanup'), 'style' => 'wide', - 'fields' => array( - 'cache_files' => array( + 'fields' => [ + 'cache_files' => [ 'type' => 'static', 'label' => qa_lang_html('admin/caching_num_items'), 'value' => qa_html(qa_format_number($cacheStats['files'])), - ), - 'cache_size' => array( + ], + 'cache_size' => [ 'type' => 'static', 'label' => qa_lang_html('admin/caching_space_used'), 'value' => qa_html(qa_format_number($cacheStats['size'] / 1048576, 1) . ' MB'), - ), - ), - - 'buttons' => array( - 'delete_expired' => array( - 'label' => qa_lang_html('admin/caching_delete_expired'), - 'tags' => 'name="docachetrim" onclick="return qa_recalc_click(this.name, this, ' . qa_js(qa_lang_html('admin/delete_stop')) . ', \'cachetrim_note\');"', - 'note' => '', - ), - 'delete_all' => array( - 'label' => qa_lang_html('admin/caching_delete_all'), - 'tags' => 'name="docacheclear" onclick="return qa_recalc_click(this.name, this, ' . qa_js(qa_lang_html('admin/delete_stop')) . ', \'cacheclear_note\');"', - 'note' => '', - ), - ), - - 'hidden' => array( + ], + 'process_type' => [ + 'type' => 'select', + 'style' => 'tall', + 'tags' => 'id="process_type_select"', + 'options' => [ + 'cache_trim' => qa_lang_html('admin/caching_delete_expired'), + 'cache_clear' => qa_lang_html('admin/caching_delete_all'), + ], + ], + 'status' => [ + 'type' => 'custom', + 'style' => 'tall', + 'html' => '', + ], + ], + + 'buttons' => [ + 'caching_process' => [ + 'label' => qa_lang_html('admin/process_start'), + 'tags' => 'id="cache_trim" data-caching_button_id="caching_button" onclick="return qa_recalc_click(this.id, cachingProcessOptions);"', + ], + ], + + 'hidden' => [ 'code' => qa_get_form_security_code('admin/recalc'), - ), - ); + ], + ]; + break; } diff --git a/qa-include/pages/admin/admin-points.php b/qa-include/pages/admin/admin-points.php index 671982cb8..7390a21bd 100644 --- a/qa-include/pages/admin/admin-points.php +++ b/qa-include/pages/admin/admin-points.php @@ -40,8 +40,10 @@ // Process user actions +$pendingRecalcs = json_decode(qa_opt('recalc_pending_processes'), true) ?? []; +$recalculate = in_array('users_points', $pendingRecalcs); + $securityexpired = false; -$recalculate = false; $optionnames = qa_db_points_option_names(); if (qa_clicked('doshowdefaults')) { @@ -51,7 +53,7 @@ $options[$optionname] = qa_default_option($optionname); } } else { - if (qa_clicked('dosaverecalc')) { + if (qa_clicked('dosave')) { if (!qa_check_form_security_code('admin/points', qa_post_text('code'))) { $securityexpired = true; } else { @@ -59,10 +61,10 @@ qa_set_option($optionname, (int)qa_post_text('option_' . $optionname)); } - if (!qa_post_text('has_js')) { - qa_redirect('admin/recalc', array('dorecalcpoints' => 1)); - } else { + if (!$recalculate) { $recalculate = true; + $pendingRecalcs[] = 'users_points'; + qa_opt('recalc_pending_processes', json_encode($pendingRecalcs)); } } } @@ -84,14 +86,13 @@ 'style' => 'wide', 'buttons' => array( - 'saverecalc' => array( - 'tags' => 'id="dosaverecalc"', - 'label' => qa_lang_html('admin/save_recalc_button'), + 'save' => array( + 'label' => qa_lang_html('admin/save_options_button'), ), ), 'hidden' => array( - 'dosaverecalc' => '1', + 'dosave' => '1', 'has_js' => '0', 'code' => qa_get_form_security_code('admin/points'), ), @@ -107,15 +108,11 @@ ); } else { if ($recalculate) { - $qa_content['form']['ok'] = ''; - $qa_content['form']['hidden']['code_recalc'] = qa_get_form_security_code('admin/recalc'); - - $qa_content['script_rel'][] = 'qa-content/qa-admin.js?' . QA_VERSION; - $qa_content['script_var']['qa_warning_recalc'] = qa_lang('admin/stop_recalc_warning'); - - $qa_content['script_onloads'][] = array( - "qa_recalc_click('dorecalcpoints', document.getElementById('dosaverecalc'), null, 'recalc_ok');" - ); + $qa_content['error'] = strtr(qa_lang_html('admin/recalc_needed'), [ + '^1' => sprintf('', qa_path_html('admin/stats', null, null, null, 'form_users_points')), + '^2' => qa_lang_html('admin/recalc_users_points_title'), + '^3' => '', + ]); } $qa_content['form']['buttons']['showdefaults'] = array( diff --git a/qa-include/pages/admin/admin-recalc.php b/qa-include/pages/admin/admin-recalc.php deleted file mode 100644 index 0eff6f0f7..000000000 --- a/qa-include/pages/admin/admin-recalc.php +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - \n"; - - flush(); - sleep(1); // ... then rest for one - } - - ?> - - - - - - - 'method="post" action="' . qa_self_html() . '"', - - 'style' => 'wide', - - 'buttons' => array( - 'recalc' => array( - 'tags' => 'name="' . qa_html($state) . '"', - 'label' => qa_lang_html('misc/form_security_again'), - ), - ), - - 'hidden' => array( - 'code' => qa_get_form_security_code('admin/recalc'), - ), - ); - - return $qa_content; - -} else { - require_once QA_INCLUDE_DIR . 'app/format.php'; - - $qa_content = qa_content_prepare(); - - $qa_content['title'] = qa_lang_html('admin/admin_title'); - $qa_content['error'] = qa_lang_html('main/page_not_found'); - - return $qa_content; -} diff --git a/qa-include/pages/admin/admin-stats.php b/qa-include/pages/admin/admin-stats.php index f4ab043a1..9dc3fdf17 100644 --- a/qa-include/pages/admin/admin-stats.php +++ b/qa-include/pages/admin/admin-stats.php @@ -29,12 +29,11 @@ require_once QA_INCLUDE_DIR . 'db/admin.php'; require_once QA_INCLUDE_DIR . 'app/format.php'; - // Check admin privileges (do late to allow one DB query) -if (!qa_admin_check_privileges($qa_content)) +if (!qa_admin_check_privileges($qa_content)) { return $qa_content; - +} // Get the information to display @@ -48,7 +47,6 @@ $ccount = (int)qa_opt('cache_ccount'); $ccount_anon = qa_db_count_posts('C', false); - // Prepare content for theme $qa_content = qa_content_prepare(); @@ -193,90 +191,128 @@ ), ); -if (QA_FINAL_EXTERNAL_USERS) +if (QA_FINAL_EXTERNAL_USERS) { unset($qa_content['form']['fields']['users']); -else +} else { unset($qa_content['form']['fields']['users_active']); +} foreach ($qa_content['form']['fields'] as $index => $field) { - if (empty($field['type'])) + if (empty($field['type'])) { $qa_content['form']['fields'][$index]['type'] = 'static'; + } } -$qa_content['form_2'] = array( - 'tags' => 'method="post" action="' . qa_path_html('admin/recalc') . '"', - - 'title' => qa_lang_html('admin/database_cleanup'), - - 'style' => 'basic', - - 'buttons' => array( - 'recount_posts' => array( - 'label' => qa_lang_html('admin/recount_posts'), - 'tags' => 'name="dorecountposts" onclick="return qa_recalc_click(this.name, this, ' . qa_js(qa_lang_html('admin/recount_posts_stop')) . ', \'recount_posts_note\');"', - 'note' => '' . qa_lang_html('admin/recount_posts_note') . '', - ), - - 'reindex_content' => array( - 'label' => qa_lang_html('admin/reindex_content'), - 'tags' => 'name="doreindexcontent" onclick="return qa_recalc_click(this.name, this, ' . qa_js(qa_lang_html('admin/reindex_content_stop')) . ', \'reindex_content_note\');"', - 'note' => '' . qa_lang_html('admin/reindex_content_note') . '', - ), - - 'recalc_points' => array( - 'label' => qa_lang_html('admin/recalc_points'), - 'tags' => 'name="dorecalcpoints" onclick="return qa_recalc_click(this.name, this, ' . qa_js(qa_lang_html('admin/recalc_stop')) . ', \'recalc_points_note\');"', - 'note' => '' . qa_lang_html('admin/recalc_points_note') . '', - ), - - 'refill_events' => array( - 'label' => qa_lang_html('admin/refill_events'), - 'tags' => 'name="dorefillevents" onclick="return qa_recalc_click(this.name, this, ' . qa_js(qa_lang_html('admin/recalc_stop')) . ', \'refill_events_note\');"', - 'note' => '' . qa_lang_html('admin/refill_events_note') . '', - ), - - 'recalc_categories' => array( - 'label' => qa_lang_html('admin/recalc_categories'), - 'tags' => 'name="dorecalccategories" onclick="return qa_recalc_click(this.name, this, ' . qa_js(qa_lang_html('admin/recalc_stop')) . ', \'recalc_categories_note\');"', - 'note' => '' . qa_lang_html('admin/recalc_categories_note') . '', - ), - - 'delete_hidden' => array( - 'label' => qa_lang_html('admin/delete_hidden'), - 'tags' => 'name="dodeletehidden" onclick="return qa_recalc_click(this.name, this, ' . qa_js(qa_lang_html('admin/delete_stop')) . ', \'delete_hidden_note\');"', - 'note' => '' . qa_lang_html('admin/delete_hidden_note') . '', - ), - ), +$qa_lang_keys = ['please_wait', 'process_start', 'process_stop', 'process_restart', 'process_unfinished']; - 'hidden' => array( - 'code' => qa_get_form_security_code('admin/recalc'), - ), -); +$qa_langs = []; +foreach ($qa_lang_keys as $key) { + $qa_langs[$key] = qa_lang('admin/' . $key); +} -if (!qa_using_categories()) - unset($qa_content['form_2']['buttons']['recalc_categories']); +$allProcessesKeys = [ + 'recount_posts', + 'reindex_content', + 'users_points', + 'refill_events', + 'delete_hidden_posts', + 'recalc_categories', +]; + +if (qa_using_categories()) { + $allProcessesKeys[] = 'recalc_categories'; +} if (defined('QA_BLOBS_DIRECTORY')) { if (qa_db_has_blobs_in_db()) { - $qa_content['form_2']['buttons']['blobs_to_disk'] = array( - 'label' => qa_lang_html('admin/blobs_to_disk'), - 'tags' => 'name="doblobstodisk" onclick="return qa_recalc_click(this.name, this, ' . qa_js(qa_lang_html('admin/blobs_stop')) . ', \'blobs_to_disk_note\');"', - 'note' => '' . qa_lang_html('admin/blobs_to_disk_note') . '', - ); + $allProcessesKeys[] = 'blobs_to_disk'; } if (qa_db_has_blobs_on_disk()) { - $qa_content['form_2']['buttons']['blobs_to_db'] = array( - 'label' => qa_lang_html('admin/blobs_to_db'), - 'tags' => 'name="doblobstodb" onclick="return qa_recalc_click(this.name, this, ' . qa_js(qa_lang_html('admin/blobs_stop')) . ', \'blobs_to_db_note\');"', - 'note' => '' . qa_lang_html('admin/blobs_to_db_note') . '', - ); + $allProcessesKeys[] = 'blobs_to_db'; } } +$allProcesses = []; +foreach ($allProcessesKeys as $processKey) { + // One of: qa_recalc_recount_posts_state, qa_recalc_reindex_content_state, qa_recalc_users_points_state, + // qa_recalc_refill_events_state, qa_recalc_recalc_categories_state, qa_recalc_delete_hidden_posts_state + $stateOption = 'qa_recalc_' . $processKey . '_state'; + $allProcesses[$processKey] = [ + 'serverProcessPending' => !empty(qa_opt($stateOption)), + ]; + + $qa_content['form_' . $processKey] = [ + 'tags' => sprintf('method="post" action="%s", id="form_%s"', qa_path_html("admin/recalc"), $processKey), + + 'style' => 'tall', + + 'fields' => [ + 'process_title' => [ + 'type' => 'static', + // One of: recalc_recount_posts_title, recalc_reindex_content_title, recalc_users_points_title, + // recalc_refill_events_title, recalc_recalc_categories_title, recalc_delete_hidden_posts_title + 'value' => qa_lang_html(sprintf('admin/recalc_%s_title', $processKey)), + // One of: recalc_recount_posts_note, recalc_reindex_content_note, recalc_users_points_note, + // recalc_refill_events_note, recalc_recalc_categories_note, recalc_delete_hidden_posts_note + 'note' => qa_lang_html(sprintf('admin/recalc_%s_note', $processKey)), + ], + 'status' => [ + 'type' => 'custom', + 'html' => sprintf( + '%s', + $processKey, + $allProcesses[$processKey]['serverProcessPending'] ? qa_html($qa_langs['process_unfinished']) : '', + ), + ], + ], + + 'buttons' => [ + $processKey . '_restart' => [ + 'label' => $allProcesses[$processKey]['serverProcessPending'] ? qa_html($qa_langs['process_restart']) : qa_html($qa_langs['process_start']), + 'tags' => sprintf('id="%s" name="%s" onclick="return qa_recalc_click(this.name, {forceRestart: true})"', $processKey, $processKey), + ], + $processKey . '_continue' => [ + 'label' => qa_lang_html('admin/process_continue'), + 'tags' => sprintf( + 'id="%s_continue" name="%s_continue" data-process="%s" onclick="return qa_recalc_click(this.dataset.process)"%s', + $processKey, + $processKey, + $processKey, + empty(qa_opt($stateOption)) ? ' style="display: none"' : '' + ), + ], + ], + + 'hidden' => [ + 'code' => qa_get_form_security_code('admin/recalc'), + ], + ]; +} + +$qa_content['script_lines'][] = [ + sprintf('const qa_langs = %s;', json_encode($qa_langs)), + sprintf('const qa_serverProcessesInfo = %s;', json_encode($allProcesses)), + 'window.onbeforeunload = event => {', + ' for (let [processKey, process] of qa_recalcProcesses.entries()) {', + ' if (process.clientRunning) {', + ' event.preventDefault();', + ' event.returnValue = true;', + ' }', + ' }', + '};', +]; + +$qa_content['script_onloads'][] = [ + 'for (const processKey in qa_serverProcessesInfo) {', + ' qa_recalcProcesses.set(processKey, {', + ' "processKey": processKey,', + ' "serverProcessPending": qa_serverProcessesInfo[processKey]["serverProcessPending"]', + ' });', + '}', +]; $qa_content['script_rel'][] = 'qa-content/qa-admin.js?' . QA_VERSION; -$qa_content['script_var']['qa_warning_recalc'] = qa_lang('admin/stop_recalc_warning'); $qa_content['script_onloads'][] = array( "qa_version_check('https://raw.githubusercontent.com/q2a/question2answer/master/VERSION.txt', " . qa_js(qa_html(QA_VERSION), true) . ", 'q2a-version', true);" @@ -284,5 +320,4 @@ $qa_content['navigation']['sub'] = qa_admin_sub_navigation(); - return $qa_content; diff --git a/qa-include/qa-page-admin-recalc.php b/qa-include/qa-page-admin-recalc.php deleted file mode 100644 index 863e82ed2..000000000 --- a/qa-include/qa-page-admin-recalc.php +++ /dev/null @@ -1,15 +0,0 @@ - Date: Thu, 22 Aug 2024 01:43:10 -0300 Subject: [PATCH 2/2] Give theme developers a chance to add listeners to update a custom UI --- qa-content/qa-admin.js | 18 +++++++++++------- qa-include/pages/admin/admin-default.php | 8 ++++---- qa-include/pages/admin/admin-stats.php | 6 ++++-- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/qa-content/qa-admin.js b/qa-content/qa-admin.js index 734eaacc7..5cb7cf225 100644 --- a/qa-content/qa-admin.js +++ b/qa-content/qa-admin.js @@ -22,7 +22,7 @@ const qa_recalcProcesses = new Map(); window.onbeforeunload = event => { - for (let [processKey, process] of qa_recalcProcesses.entries()) { + for (const [processKey, process] of qa_recalcProcesses.entries()) { if (process.clientRunning) { event.preventDefault(); event.returnValue = true; @@ -31,8 +31,12 @@ window.onbeforeunload = event => { }; /** - * @param {String} processKey - * @param {object} options - See keys and default values below + * @param {string} processKey + * @param {Object} options - Object used to configure the process + * @param {boolean} [options.forceRestart = false] - Whether the click has to trigger a restart of the process + * @param {boolean} [options.requiresServerTracking = true] - Whether the process is expected to be stopped and resumed + * @param {(process: object) => void} [options.callbackStart] - Callback run when the process is started + * @param {(process: object, hasFinished: boolean) => void} [options.callbackStop] - Callback run when the process is stopped * @returns {boolean} */ function qa_recalc_click(processKey, options = {}) @@ -40,8 +44,6 @@ function qa_recalc_click(processKey, options = {}) options = { forceRestart: false, requiresServerTracking: true, - callbackStart: process => {}, - callbackStop: hasFinished => {}, ...options, }; @@ -61,6 +63,8 @@ function qa_recalc_click(processKey, options = {}) "statusLabel": statusLabel, "clientRunning": true, "stopRequest": false, + "startListeners": options.callbackStart ? [options.callbackStart] : [], + "stopListeners": options.callbackStop ? [options.callbackStop] : [], "options": options }; @@ -71,7 +75,7 @@ function qa_recalc_click(processKey, options = {}) statusLabel.innerHTML = qa_langs.please_wait; startButton.value = qa_langs.process_stop; - process.options.callbackStart(process); + process.startListeners.forEach(listener => listener(process)); qa_recalc_update(process); } @@ -127,7 +131,7 @@ function qa_recalc_cleanup(process, hasFinished = false, message = null) { process.clientRunning = false; - process.options.callbackStop(hasFinished); + process.stopListeners.forEach(listener => listener(process, hasFinished)); if (process.options.requiresServerTracking && process.serverProcessPending) { process.startButton.value = qa_langs.process_restart; diff --git a/qa-include/pages/admin/admin-default.php b/qa-include/pages/admin/admin-default.php index 9e58e7a6b..f941da2b0 100644 --- a/qa-include/pages/admin/admin-default.php +++ b/qa-include/pages/admin/admin-default.php @@ -1827,7 +1827,7 @@ function qa_optionfield_make_select(&$optionfield, $options, $value, $default) $qa_content['script_lines'][] = [ sprintf('const qa_langs = %s;', json_encode($qa_langs)), - 'const cachingProcessOptions = {', + 'const processOptions = {', ' forceRestart: true,', ' requiresServerTracking: false,', ' callbackStart: process => document.getElementById(\'process_type_select\').disabled = true,', @@ -1838,8 +1838,8 @@ function qa_optionfield_make_select(&$optionfield, $options, $value, $default) $qa_content['script_onloads'][] = [ 'const processTypeSelect = document.getElementById(\'process_type_select\');', 'processTypeSelect.addEventListener(\'change\', event => {', - ' document.querySelector(\'[data-caching_button_id="caching_button"]\').id = event.target.value;' . - ' document.querySelector(\'[data-caching_status_id="caching_status"]\').id = event.target.value + \'_status\';' . + ' document.querySelector(\'[data-caching_button_id="caching_button"]\').id = event.target.value;', + ' document.querySelector(\'[data-caching_status_id="caching_status"]\').id = event.target.value + \'_status\';', '});', ]; @@ -1880,7 +1880,7 @@ function qa_optionfield_make_select(&$optionfield, $options, $value, $default) 'buttons' => [ 'caching_process' => [ 'label' => qa_lang_html('admin/process_start'), - 'tags' => 'id="cache_trim" data-caching_button_id="caching_button" onclick="return qa_recalc_click(this.id, cachingProcessOptions);"', + 'tags' => 'id="cache_trim" data-caching_button_id="caching_button" onclick="return qa_recalc_click(this.id, processOptions);"', ], ], diff --git a/qa-include/pages/admin/admin-stats.php b/qa-include/pages/admin/admin-stats.php index 9dc3fdf17..26c3323d3 100644 --- a/qa-include/pages/admin/admin-stats.php +++ b/qa-include/pages/admin/admin-stats.php @@ -270,12 +270,12 @@ 'buttons' => [ $processKey . '_restart' => [ 'label' => $allProcesses[$processKey]['serverProcessPending'] ? qa_html($qa_langs['process_restart']) : qa_html($qa_langs['process_start']), - 'tags' => sprintf('id="%s" name="%s" onclick="return qa_recalc_click(this.name, {forceRestart: true})"', $processKey, $processKey), + 'tags' => sprintf('id="%s" name="%s" onclick="return qa_recalc_click(this.name, processOptionsRestart)"', $processKey, $processKey), ], $processKey . '_continue' => [ 'label' => qa_lang_html('admin/process_continue'), 'tags' => sprintf( - 'id="%s_continue" name="%s_continue" data-process="%s" onclick="return qa_recalc_click(this.dataset.process)"%s', + 'id="%s_continue" name="%s_continue" data-process="%s" onclick="return qa_recalc_click(this.dataset.process, processOptionsContinue)"%s', $processKey, $processKey, $processKey, @@ -301,6 +301,8 @@ ' }', ' }', '};', + 'const processOptionsRestart = { forceRestart: true };', + 'const processOptionsContinue = {};', ]; $qa_content['script_onloads'][] = [