Changeset 3796384
- Timestamp:
- 11/11/2026 01:20:18 PM (less than one hour ago)
- Location:
- members-for-kofi
- Files:
-
- 8 added
- 24 edited
- 1 copied
-
tags/1.7.0 (copied) (copied from members-for-kofi/trunk )
-
tags/1.7.0/assets/css (added)
-
tags/1.7.0/assets/css/admin.css (added)
-
tags/1.7.0/assets/js/admin-settings.js (modified) (1 diff)
-
tags/1.7.0/languages/members-for-kofi-da_DK.mo (modified) ( previous)
-
tags/1.7.0/languages/members-for-kofi-da_DK.po (modified) (5 diffs)
-
tags/1.7.0/languages/members-for-kofi.pot (modified) (6 diffs)
-
tags/1.7.0/members-for-kofi.php (modified) (1 diff)
-
tags/1.7.0/readme.txt (modified) (3 diffs)
-
tags/1.7.0/src/Admin/AdminSettings.php (modified) (6 diffs)
-
tags/1.7.0/src/Admin/LogsTab.php (added)
-
tags/1.7.0/src/Admin/MembersTab.php (added)
-
tags/1.7.0/src/Logging/RequestLogger.php (modified) (1 diff)
-
tags/1.7.0/src/Logging/UserLogger.php (modified) (1 diff)
-
tags/1.7.0/src/Privacy/PersonalData.php (modified) (1 diff)
-
tags/1.7.0/vendor/composer/autoload_classmap.php (modified) (1 diff)
-
tags/1.7.0/vendor/composer/autoload_static.php (modified) (1 diff)
-
trunk/assets/css (added)
-
trunk/assets/css/admin.css (added)
-
trunk/assets/js/admin-settings.js (modified) (1 diff)
-
trunk/languages/members-for-kofi-da_DK.mo (modified) ( previous)
-
trunk/languages/members-for-kofi-da_DK.po (modified) (5 diffs)
-
trunk/languages/members-for-kofi.pot (modified) (6 diffs)
-
trunk/members-for-kofi.php (modified) (1 diff)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/src/Admin/AdminSettings.php (modified) (6 diffs)
-
trunk/src/Admin/LogsTab.php (added)
-
trunk/src/Admin/MembersTab.php (added)
-
trunk/src/Logging/RequestLogger.php (modified) (1 diff)
-
trunk/src/Logging/UserLogger.php (modified) (1 diff)
-
trunk/src/Privacy/PersonalData.php (modified) (1 diff)
-
trunk/vendor/composer/autoload_classmap.php (modified) (1 diff)
-
trunk/vendor/composer/autoload_static.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
members-for-kofi/tags/1.7.0/assets/js/admin-settings.js
r3703946 r3796384 1 function membersForKofiInit() { 2 const tabs = document.querySelectorAll('.nav-tab'); 3 const tabContents = document.querySelectorAll('.members-for-kofi-tab'); 4 const activeTabInput = document.getElementById('active_tab'); 5 // Save button now lives only inside the visible general tab container; no JS hide needed. 1 /** 2 * Members for Ko-fi admin page. 3 * 4 * The tabs, filters and paging are plain links and forms; this only adds 5 * what needs a script: copying the webhook URL, adding and removing tier 6 * rows, and confirming a log deletion. 7 */ 8 (function () { 9 'use strict'; 6 10 7 tabs.forEach(tab => { 8 tab.addEventListener('click', function (e) { 9 e.preventDefault(); 10 const activeTabId = this.dataset.tab; 11 if (!activeTabId) return; 11 function init() { 12 const copyButton = document.getElementById('copy-webhook-url'); 13 const urlInput = document.getElementById('webhook-kofi-url'); 14 const copyStatus = document.getElementById('mfk-copy-status'); 12 15 13 // Update active classes 14 tabs.forEach(t => { 15 t.classList.remove('nav-tab-active'); 16 t.setAttribute('aria-selected', 'false'); 17 }); 18 this.classList.add('nav-tab-active'); 19 this.setAttribute('aria-selected', 'true'); 20 21 // Toggle content visibility 22 tabContents.forEach(content => { 23 if (content.id === `members-for-kofi-tab-${activeTabId}`) { 24 content.style.display = ''; 16 if (copyButton && urlInput) { 17 copyButton.addEventListener('click', function () { 18 const done = function () { 19 if (copyStatus) { 20 copyStatus.textContent = (window.kofiMembers && window.kofiMembers.copied) || 'Copied'; 21 window.setTimeout(function () { copyStatus.textContent = ''; }, 3000); 22 } 23 }; 24 urlInput.select(); 25 if (navigator.clipboard && navigator.clipboard.writeText) { 26 navigator.clipboard.writeText(urlInput.value).then(done).catch(function () { 27 document.execCommand('copy'); 28 done(); 29 }); 25 30 } else { 26 content.style.display = 'none'; 27 } 28 }); 29 30 // Hidden input (used only if form submission needs to know last active tab) 31 if (activeTabInput) activeTabInput.value = activeTabId; 32 33 // Optionally reflect tab in URL without reload 34 if (window.history && window.history.replaceState) { 35 const url = new URL(window.location.href); 36 url.searchParams.set('tab', activeTabId); 37 window.history.replaceState({}, '', url.toString()); 38 } 39 }); 40 }); 41 42 // Handle log type selector dropdown 43 function handleLogTypeChange() { 44 const logTypeSelector = document.getElementById('log-type-selector'); 45 if (logTypeSelector && !logTypeSelector.dataset.bound) { 46 logTypeSelector.dataset.bound = '1'; 47 logTypeSelector.addEventListener('change', function() { 48 const logType = this.value; 49 const searchInput = document.getElementById('kofi-members-log-search'); 50 const rowsPerPage = document.getElementById('rows_per_page')?.value || 10; 51 52 // Update the search label based on log type 53 const searchLabel = document.querySelector('label[for="kofi-members-log-search"]'); 54 if (searchLabel) { 55 if (logType === 'request') { 56 searchLabel.textContent = kofiMembers.searchLabelRequest || 'Search (email / tier / status)'; 57 } else { 58 searchLabel.textContent = kofiMembers.searchLabelUser || 'Search (email / action / role)'; 59 } 60 } 61 62 // Clear search input 63 if (searchInput) searchInput.value = ''; 64 65 // Reload the logs table with the new log type 66 loadLogsWithType(logType, '', 1, rowsPerPage); 67 }); 68 } 69 } 70 71 function loadLogsWithType(logType, search, paged, rowsPerPage) { 72 fetch(kofiMembers.ajaxurl, { 73 method: 'POST', 74 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 75 body: new URLSearchParams({ 76 action: 'members_for_kofi_switch_log_type', 77 log_type: logType, 78 search: search || '', 79 paged: paged || 1, 80 rows_per_page: rowsPerPage || 10, 81 _ajax_nonce: kofiMembers.switchLogTypeNonce 82 }), 83 }) 84 .then(r => r.json()) 85 .then(data => { 86 if (data.success) { 87 const tableContainer = document.querySelector('#logs-table-container'); 88 if (tableContainer) { 89 tableContainer.innerHTML = data.data; 90 tableContainer.dataset.logType = logType; 91 } 92 reapplyEventListeners(); 93 handleLogTypeChange(); // Re-bind after content update 94 } else { 95 alert(data.data || kofiMembers.errorMessage); 96 } 97 }) 98 .catch(err => console.error('Log type switch error:', err)); 99 } 100 101 // No need to toggle save button; it is only present within the general tab container. 102 103 function reapplyEventListeners() { 104 // Handle Clear Logs button click 105 const clearLogsButton = document.querySelector('button[name="clear_logs"]'); 106 if (clearLogsButton && !clearLogsButton.dataset.bound) { 107 clearLogsButton.dataset.bound = '1'; 108 clearLogsButton.addEventListener('click', function (e) { 109 e.preventDefault(); 110 const currentSearch = document.getElementById('kofi-members-current-search')?.value || ''; 111 const logType = document.querySelector('#log-type-selector')?.value || 'user'; 112 113 if (confirm(kofiMembers.clearLogsConfirm)) { 114 fetch(kofiMembers.ajaxurl, { 115 method: 'POST', 116 headers: { 117 'Content-Type': 'application/x-www-form-urlencoded', 118 }, 119 body: new URLSearchParams({ 120 action: 'members_for_kofi_clear_logs', 121 _ajax_nonce: kofiMembers.clearLogsNonce, 122 search: currentSearch, 123 log_type: logType, 124 }), 125 }) 126 .then(response => response.json()) 127 .then(data => { 128 if (data.success) { 129 document.querySelector('#logs-table-container').innerHTML = data.data; 130 reapplyEventListeners(); // Reapply listeners after DOM update 131 // Don't call handleLogTypeChange() here - dropdown is outside the container 132 } else { 133 alert(data.data || kofiMembers.errorMessage); 134 } 135 }) 136 .catch(error => console.error('Error clearing logs:', error)); 31 document.execCommand('copy'); 32 done(); 137 33 } 138 34 }); 139 35 } 140 36 141 // Handle Rows Per Page dropdown change 142 const rowsPerPageDropdown = document.querySelector('#rows_per_page'); 143 if (rowsPerPageDropdown && !rowsPerPageDropdown.dataset.bound) { 144 rowsPerPageDropdown.dataset.bound = '1'; 145 rowsPerPageDropdown.addEventListener('change', function (e) { 146 e.preventDefault(); 37 const tbody = document.querySelector('#tier-role-map-table tbody'); 38 const template = document.getElementById('tier-role-row-template'); 147 39 148 const rowsPerPage = rowsPerPageDropdown.value; 149 const currentSearch = document.getElementById('kofi-members-current-search')?.value || ''; 150 const logType = document.querySelector('#log-type-selector')?.value || 'user'; 40 function addRow(tierName) { 41 if (!tbody || !template) { 42 return null; 43 } 44 const row = template.content.firstElementChild.cloneNode(true); 45 tbody.appendChild(row); 46 const input = row.querySelector('.tier-name-input'); 47 if (input) { 48 if (tierName) { 49 input.value = tierName; 50 const select = row.querySelector('.tier-role-select'); 51 if (select) { 52 select.focus(); 53 } 54 } else { 55 input.focus(); 56 } 57 } 58 return row; 59 } 151 60 152 fetch(kofiMembers.ajaxurl, { 153 method: 'POST', 154 headers: { 155 'Content-Type': 'application/x-www-form-urlencoded', 156 }, 157 body: new URLSearchParams({ 158 action: 'members_for_kofi_update_rows_per_page', 159 rows_per_page: rowsPerPage, 160 search: currentSearch, 161 log_type: logType, 162 _ajax_nonce: kofiMembers.rowsPerPageNonce, 163 }), 164 }) 165 .then(response => response.json()) 166 .then(data => { 167 if (data.success) { 168 document.querySelector('#logs-table-container').innerHTML = data.data; 169 reapplyEventListeners(); // Reapply listeners after DOM update 170 // Don't call handleLogTypeChange() here - dropdown is outside the container and doesn't need rebinding 171 } else { 172 alert(data.data || kofiMembers.errorMessage); 173 } 174 }) 175 .catch(error => console.error('Error updating rows per page:', error)); 61 const addButton = document.getElementById('add-tier-role-row'); 62 if (addButton) { 63 addButton.addEventListener('click', function () { 64 addRow(''); 176 65 }); 177 66 } 178 67 179 // Search / Reset / Refresh buttons (bind after lazy load and after table updates) 180 const searchBtn = document.getElementById('kofi-members-log-search-btn'); 181 const resetBtn = document.getElementById('kofi-members-log-reset-btn'); 182 const refreshBtn = document.getElementById('kofi-members-log-refresh-btn'); 183 const searchInput = document.getElementById('kofi-members-log-search'); 68 document.addEventListener('click', function (event) { 69 const remove = event.target.closest('.remove-tier-role-row'); 70 if (remove && tbody && tbody.contains(remove)) { 71 const row = remove.closest('tr'); 72 if (row) { 73 row.remove(); 74 } 75 if (addButton) { 76 addButton.focus(); 77 } 78 return; 79 } 184 80 185 function loadLogs(params) { 186 fetch(kofiMembers.ajaxurl, { 187 method: 'POST', 188 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 189 body: new URLSearchParams(params), 190 }) 191 .then(r => r.json()) 192 .then(data => { 193 if (data.success) { 194 const tableContainer = document.querySelector('#logs-table-container'); 195 if (tableContainer) tableContainer.innerHTML = data.data; 196 const hiddenSearch = document.getElementById('kofi-members-current-search'); 197 if (hiddenSearch) hiddenSearch.value = params.search || ''; 198 reapplyEventListeners(); 199 handleLogTypeChange(); 200 } else { 201 alert(data.data || kofiMembers.errorMessage); 202 } 203 }) 204 .catch(err => console.error('Log load error:', err)); 205 } 81 const unmapped = event.target.closest('.mfk-add-unmapped'); 82 if (unmapped) { 83 addRow(unmapped.getAttribute('data-tier') || ''); 84 const box = unmapped.closest('.mfk-unmapped'); 85 unmapped.remove(); 86 if (box && !box.querySelector('.mfk-add-unmapped')) { 87 box.remove(); 88 } 89 } 90 }); 206 91 207 if (searchBtn && !searchBtn.dataset.bound) { 208 searchBtn.dataset.bound = '1'; 209 searchBtn.addEventListener('click', function (e) { 210 e.preventDefault(); 211 loadLogs({ 212 action: 'members_for_kofi_filter_logs', 213 search: (searchInput?.value || '').trim(), 214 paged: 1, 215 log_type: document.querySelector('#log-type-selector')?.value || 'user', 216 rows_per_page: document.getElementById('rows_per_page')?.value || 10, 217 _ajax_nonce: kofiMembers.filterNonce, 218 }); 219 }); 220 } 221 if (resetBtn && !resetBtn.dataset.bound) { 222 resetBtn.dataset.bound = '1'; 223 resetBtn.addEventListener('click', function (e) { 224 e.preventDefault(); 225 if (searchInput) searchInput.value = ''; 226 loadLogs({ 227 action: 'members_for_kofi_filter_logs', 228 search: '', 229 paged: 1, 230 log_type: document.querySelector('#log-type-selector')?.value || 'user', 231 rows_per_page: document.getElementById('rows_per_page')?.value || 10, 232 _ajax_nonce: kofiMembers.filterNonce, 233 }); 234 }); 235 } 236 if (refreshBtn && !refreshBtn.dataset.bound) { 237 refreshBtn.dataset.bound = '1'; 238 refreshBtn.addEventListener('click', function (e) { 239 e.preventDefault(); 240 const currentSearch = document.getElementById('kofi-members-current-search')?.value || ''; 241 loadLogs({ 242 action: 'members_for_kofi_refresh_logs', 243 search: currentSearch, 244 paged: 1, 245 rows_per_page: document.getElementById('rows_per_page')?.value || 10, 246 _ajax_nonce: kofiMembers.refreshNonce, 247 }); 248 }); 249 } 250 } 251 252 reapplyEventListeners(); 253 handleLogTypeChange(); 254 255 // Handle Pagination link clicks 256 document.addEventListener('click', function (e) { 257 const btn = e.target.closest('.members-for-kofi-page-btn'); 258 if (!btn) return; 259 e.preventDefault(); 260 const page = btn.getAttribute('data-page'); 261 const currentSearch = document.getElementById('kofi-members-current-search')?.value || ''; 262 const logType = document.querySelector('#log-type-selector')?.value || 'user'; 263 fetch(kofiMembers.ajaxurl, { 264 method: 'POST', 265 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 266 body: new URLSearchParams({ 267 action: 'members_for_kofi_pagination', 268 paged: page, 269 search: currentSearch, 270 log_type: logType, 271 _ajax_nonce: kofiMembers.paginationNonce, 272 }), 273 }) 274 .then(r => r.json()) 275 .then(data => { 276 if (data.success) { 277 document.querySelector('#logs-table-container').innerHTML = data.data; 278 reapplyEventListeners(); 279 handleLogTypeChange(); 280 } else { 281 console.error('Error fetching logs:', data.data); 92 document.querySelectorAll('form[data-mfk-confirm]').forEach(function (form) { 93 form.addEventListener('submit', function (event) { 94 if (!window.confirm(form.getAttribute('data-mfk-confirm'))) { 95 event.preventDefault(); 282 96 } 283 })284 .catch(err => console.error('Error fetching logs:', err));285 });286 287 const copyButton = document.getElementById('copy-webhook-url');288 const webhookInput = document.getElementById('webhook-kofi-url');289 290 if (copyButton && webhookInput) {291 copyButton.addEventListener('click', function () {292 // Select the text in the input field293 webhookInput.select();294 webhookInput.setSelectionRange(0, 99999); // For mobile devices295 296 // Copy the text to the clipboard297 navigator.clipboard.writeText(webhookInput.value).then(() => {298 alert('Webhook URL copied to clipboard!');299 }).catch(err => {300 console.error('Failed to copy webhook URL:', err);301 97 }); 302 98 }); 303 99 } 304 100 305 // Tier to Role Mapping dynamic rows 306 const addTierRoleBtn = document.getElementById('add-tier-role-row'); 307 const tierRoleTableBody = document.querySelector('#tier-role-map-table tbody'); 308 const tierRowTemplate = document.getElementById('tier-role-row-template'); 309 310 function addTierRoleRow() { 311 if (!tierRowTemplate || !tierRoleTableBody) return; 312 const html = tierRowTemplate.innerHTML.trim(); 313 const temp = document.createElement('tbody'); 314 temp.innerHTML = html; 315 const row = temp.firstElementChild; 316 if (row) { 317 tierRoleTableBody.appendChild(row); 318 } 101 if (document.readyState === 'loading') { 102 document.addEventListener('DOMContentLoaded', init); 103 } else { 104 init(); 319 105 } 320 321 if (addTierRoleBtn) { 322 addTierRoleBtn.addEventListener('click', function (e) { 323 e.preventDefault(); 324 addTierRoleRow(); 325 }); 326 } 327 328 // Delegate remove button clicks 329 document.addEventListener('click', function (e) { 330 const removeBtn = e.target.closest('.remove-tier-role-row'); 331 if (removeBtn && tierRoleTableBody && tierRoleTableBody.contains(removeBtn)) { 332 e.preventDefault(); 333 const row = removeBtn.closest('tr'); 334 if (row) { 335 row.remove(); 336 } 337 } 338 }); 339 340 } 341 342 if (document.readyState === 'loading') { 343 document.addEventListener('DOMContentLoaded', membersForKofiInit); 344 } else { 345 // DOM already ready (script loaded late in footer) -> initialize immediately. 346 membersForKofiInit(); 347 } 348 349 // Auto-activate user logs tab if URL param present (after init attaches listeners) 350 (() => { 351 const params = new URLSearchParams(window.location.search); 352 if (params.get('tab') === 'user_logs') { 353 const userLogsTab = document.querySelector('.nav-tab[data-tab="user_logs"]'); 354 if (userLogsTab) { 355 // Defer to allow initial init to finish. 356 setTimeout(() => userLogsTab.click(), 0); 357 } 358 } 359 })(); 106 }()); -
members-for-kofi/tags/1.7.0/languages/members-for-kofi-da_DK.po
r3796045 r3796384 4 4 msgid "" 5 5 msgstr "" 6 "Project-Id-Version: Members for Ko-fi 1. 4.0\n"6 "Project-Id-Version: Members for Ko-fi 1.7.0\n" 7 7 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/members-for-kofi\n" 8 8 "Language: da_DK\n" … … 15 15 #. Plugin Name of the plugin 16 16 #: members-for-kofi.php 17 #: src/Plugin.php:434 18 #: src/Plugin.php:435 19 #: src/Privacy/PersonalData.php:333 17 #: src/Admin/AdminSettings.php:162 18 #: src/Plugin.php:436 19 #: src/Plugin.php:437 20 #: src/Privacy/PersonalData.php:360 20 21 msgid "Members for Ko-fi" 21 22 msgstr "Members for Ko-fi" … … 41 42 msgstr "https://foodgeek.io" 42 43 43 #: src/Admin/AdminSettings.php:107 44 #: src/Admin/AdminSettings.php:667 45 msgid "Ko-fi Settings" 46 msgstr "Ko-fi-indstillinger" 47 48 #: src/Admin/AdminSettings.php:108 49 msgid "Verification Token" 50 msgstr "Verifikationstoken" 51 52 #: src/Admin/AdminSettings.php:111 53 #: src/Admin/AdminSettings.php:671 54 msgid "Role Assignment" 55 msgstr "Tildeling af roller" 56 57 #: src/Admin/AdminSettings.php:112 58 msgid "Only Accept Subscriptions" 59 msgstr "Accepter kun abonnementer" 60 61 #: src/Admin/AdminSettings.php:113 62 msgid "Tier to Role Mapping" 63 msgstr "Tilknytning af niveauer til roller" 64 65 #: src/Admin/AdminSettings.php:114 66 msgid "Default Role (if no match)" 67 msgstr "Standardrolle (hvis intet matcher)" 68 69 #: src/Admin/AdminSettings.php:115 70 msgid "Enable Expiry" 71 msgstr "Slå udløb til" 72 73 #: src/Admin/AdminSettings.php:116 74 msgid "Role Expiry (days)" 75 msgstr "Rollens gyldighed (dage)" 76 77 #: src/Admin/AdminSettings.php:119 78 #: src/Admin/AdminSettings.php:675 79 msgid "Logging" 80 msgstr "Logning" 81 82 #: src/Admin/AdminSettings.php:120 83 msgid "Automatically Clear Logs" 84 msgstr "Ryd logs automatisk" 85 86 #: src/Admin/AdminSettings.php:121 44 #: src/Admin/AdminSettings.php:110 45 msgid "Copied" 46 msgstr "Kopieret" 47 48 #: src/Admin/AdminSettings.php:156 49 msgid "Settings" 50 msgstr "Indstillinger" 51 52 #: src/Admin/AdminSettings.php:157 53 msgid "Members" 54 msgstr "Medlemmer" 55 56 #: src/Admin/AdminSettings.php:158 57 msgid "Logs" 58 msgstr "Logs" 59 60 #: src/Admin/AdminSettings.php:171 61 msgid "Members for Ko-fi sections" 62 msgstr "Sektioner i Members for Ko-fi" 63 64 #: src/Admin/AdminSettings.php:201 65 msgid "Ko-fi connection" 66 msgstr "Forbindelse til Ko-fi" 67 68 #: src/Admin/AdminSettings.php:208 69 msgid "Press \"Send test\" on Ko-fi" 70 msgstr "Tryk på \"Send test\" hos Ko-fi" 71 72 #. translators: %s is a link to the request log. 73 #: src/Admin/AdminSettings.php:213 74 #, php-format 75 msgid "It shows up in the status line above and in %s. A test never creates an account." 76 msgstr "Den vises i statuslinjen ovenfor og i %s. En test opretter aldrig en konto." 77 78 #: src/Admin/AdminSettings.php:214 79 msgid "the request log" 80 msgstr "forespørgselsloggen" 81 82 #: src/Admin/AdminSettings.php:223 83 msgid "Who gets a role" 84 msgstr "Hvem får en rolle" 85 86 #: src/Admin/AdminSettings.php:232 87 msgid "When membership ends" 88 msgstr "Når medlemskabet ophører" 89 90 #: src/Admin/AdminSettings.php:239 91 msgid "Ko-fi subscriptions renew monthly, so 35 days leaves a few days' grace. A role the user held before paying is never removed." 92 msgstr "Ko-fi-abonnementer fornyes hver måned, så 35 dage giver et par dages ekstra frist. En rolle, som brugeren havde før betalingen, fjernes aldrig." 93 94 #: src/Admin/AdminSettings.php:243 95 msgid "Log history" 96 msgstr "Loghistorik" 97 98 #: src/Admin/AdminSettings.php:250 99 msgid "Untick to keep everything, for example when another tool reads the request log." 100 msgstr "Fjern fluebenet for at beholde alt, for eksempel hvis et andet værktøj læser forespørgselsloggen." 101 102 #: src/Admin/AdminSettings.php:272 103 msgid "Not connected: no token is saved, so Ko-fi's requests are refused." 104 msgstr "Ikke forbundet: der er ikke gemt et token, så forespørgsler fra Ko-fi afvises." 105 106 #: src/Admin/AdminSettings.php:277 107 msgid "Waiting for Ko-fi: no request has arrived yet." 108 msgstr "Venter på Ko-fi: der er ikke kommet nogen forespørgsel endnu." 109 110 #. translators: %s is a time span, e.g. "2 hours". 111 #: src/Admin/AdminSettings.php:283 112 #, php-format 113 msgid "Working: the last request from Ko-fi, %s ago, was accepted." 114 msgstr "Forbindelsen virker: den seneste forespørgsel fra Ko-fi for %s siden blev accepteret." 115 116 #. translators: 1: a time span, e.g. "2 hours", 2: why it was refused, e.g. "the token doesn't match". 117 #: src/Admin/AdminSettings.php:287 118 #, php-format 119 msgid "The last request, %1$s ago, was refused: %2$s" 120 msgstr "Den seneste forespørgsel for %1$s siden blev afvist: %2$s" 121 122 #: src/Admin/AdminSettings.php:302 123 msgid "Ko-fi's webhook settings" 124 msgstr "Ko-fis webhook-indstillinger" 125 126 #: src/Admin/AdminSettings.php:303 127 msgid "(opens in a new tab)" 128 msgstr "(åbner i en ny fane)" 129 130 #: src/Admin/AdminSettings.php:305 131 msgid "Give Ko-fi this webhook URL" 132 msgstr "Giv Ko-fi denne webhook-URL" 133 134 #: src/Admin/AdminSettings.php:308 135 msgid "Copy URL" 136 msgstr "Kopiér URL" 137 138 #. translators: %s is a link to Ko-fi's webhook settings. 139 #: src/Admin/AdminSettings.php:314 140 #, php-format 141 msgid "Paste it into %s." 142 msgstr "Indsæt den i %s." 143 144 #: src/Admin/AdminSettings.php:329 145 msgid "Paste Ko-fi's verification token" 146 msgstr "Indsæt Ko-fis verifikationstoken" 147 148 #: src/Admin/AdminSettings.php:332 149 msgid "A token is saved" 150 msgstr "Der er gemt et token" 151 152 #: src/Admin/AdminSettings.php:334 153 msgid "fingerprint, to compare with another site" 154 msgstr "fingeraftryk til sammenligning med et andet websted" 155 156 #: src/Admin/AdminSettings.php:337 157 msgid "No token is saved yet." 158 msgstr "Der er ikke gemt noget token endnu." 159 160 #: src/Admin/AdminSettings.php:343 161 msgid "Leave blank to keep the saved token" 162 msgstr "Lad feltet stå tomt for at beholde det gemte token" 163 164 #: src/Admin/AdminSettings.php:344 165 msgid "On Ko-fi's webhook page, open Advanced and copy the token. Only a hash is stored, so it can't be shown again." 166 msgstr "Åbn Advanced på Ko-fis webhook-side, og kopiér tokenet. Der gemmes kun en hashværdi, så det kan ikke vises igen." 167 168 #: src/Admin/AdminSettings.php:358 169 msgid "Payments that grant a role" 170 msgstr "Betalinger, der giver en rolle" 171 172 #: src/Admin/AdminSettings.php:359 173 msgid "Donations and subscriptions" 174 msgstr "Donationer og abonnementer" 175 176 #: src/Admin/AdminSettings.php:360 177 msgid "Subscriptions only" 178 msgstr "Kun abonnementer" 179 180 #: src/Admin/AdminSettings.php:360 181 msgid "(one-off donations are logged and ignored)" 182 msgstr "(enkeltdonationer logges og ignoreres)" 183 184 #: src/Admin/AdminSettings.php:393 185 #: src/Admin/AdminSettings.php:421 186 msgid "Tier name on Ko-fi" 187 msgstr "Niveaunavn hos Ko-fi" 188 189 #: src/Admin/AdminSettings.php:394 190 msgid "Role for this tier" 191 msgstr "Rolle for dette niveau" 192 193 #: src/Admin/AdminSettings.php:395 194 msgid "Remove this tier" 195 msgstr "Fjern dette niveau" 196 197 #: src/Admin/AdminSettings.php:395 198 msgid "Remove" 199 msgstr "Fjern" 200 201 #: src/Admin/AdminSettings.php:416 202 msgid "Ko-fi tiers" 203 msgstr "Ko-fi-niveauer" 204 205 #: src/Admin/AdminSettings.php:422 206 msgid "Role it grants" 207 msgstr "Rolle, der tildeles" 208 209 #: src/Admin/AdminSettings.php:423 210 msgid "Actions" 211 msgstr "Handlinger" 212 213 #: src/Admin/AdminSettings.php:433 214 msgid "Add tier" 215 msgstr "Tilføj niveau" 216 217 #: src/Admin/AdminSettings.php:434 218 msgid "Names match whatever their capitalisation." 219 msgstr "Navne matcher uanset store og små bogstaver." 220 221 #: src/Admin/AdminSettings.php:438 222 msgid "Tiers Ko-fi sent that aren't listed:" 223 msgstr "Niveauer fra Ko-fi, der ikke er på listen:" 224 225 #. translators: 1: a tier name, 2: how many payments named it. 226 #: src/Admin/AdminSettings.php:445 227 #, php-format 228 msgid "Add \"%1$s\" (%2$d payment)" 229 msgid_plural "Add \"%1$s\" (%2$d payments)" 230 msgstr[0] "Tilføj \"%1$s\" (%2$d betaling)" 231 msgstr[1] "Tilføj \"%1$s\" (%2$d betalinger)" 232 233 #: src/Admin/AdminSettings.php:469 234 msgid "Any other tier, or no tier" 235 msgstr "Andre niveauer eller intet niveau" 236 237 #: src/Admin/AdminSettings.php:471 238 msgid "— No role —" 239 msgstr "— Ingen rolle —" 240 241 #: src/Admin/AdminSettings.php:474 242 msgid "With \"No role\", those payers still get an account, but no role. Administrator and other roles that can run the site are never offered." 243 msgstr "Med \"Ingen rolle\" får disse betalere stadig en konto, men ingen rolle. Administrator og andre roller, der kan styre webstedet, tilbydes aldrig." 244 245 #: src/Admin/AdminSettings.php:489 246 msgid "Remove the role" 247 msgstr "Fjern rollen" 248 249 #: src/Admin/AdminSettings.php:500 250 msgid "Days after the last payment" 251 msgstr "Dage efter seneste betaling" 252 253 #: src/Admin/AdminSettings.php:502 254 msgid "days after the member's last payment" 255 msgstr "dage efter medlemmets seneste betaling" 256 257 #: src/Admin/AdminSettings.php:518 258 msgid "Delete log entries older than" 259 msgstr "Slet logposter, der er ældre end" 260 261 #: src/Admin/AdminSettings.php:529 87 262 msgid "Number of Days to Keep Logs" 88 263 msgstr "Antal dage, logs gemmes" 89 264 90 #: src/Admin/AdminSettings.php:150 91 msgid "Are you sure you want to clear all logs? This action cannot be undone." 92 msgstr "Er du sikker på, at du vil rydde alle logs? Det kan ikke fortrydes." 93 94 #: src/Admin/AdminSettings.php:151 95 msgid "An error occurred. Please try again." 96 msgstr "Der opstod en fejl. Prøv igen." 97 98 #: src/Admin/AdminSettings.php:152 99 #: src/Admin/AdminSettings.php:748 100 msgid "Search (email / action / role)" 101 msgstr "Søg (e-mail / handling / rolle)" 102 103 #: src/Admin/AdminSettings.php:153 104 #: src/Admin/AdminSettings.php:746 105 msgid "Search (email / tier / status)" 106 msgstr "Søg (e-mail / niveau / status)" 107 108 #: src/Admin/AdminSettings.php:188 265 #: src/Admin/AdminSettings.php:531 266 msgid "days" 267 msgstr "dage" 268 269 #. translators: %s is a human-readable time span, e.g. "3 days". 270 #: src/Admin/AdminSettings.php:575 271 #, php-format 272 msgid "Scheduled tasks are not running: role expiry and log cleanup are %s overdue. Make sure WP-Cron runs, or that a system cron calls wp-cron.php." 273 msgstr "Planlagte opgaver kører ikke: udløb af roller og oprydning i logs er %s forsinket. Sørg for, at WP-Cron kører, eller at et system-cronjob kalder wp-cron.php." 274 275 #. translators: 1: email address, 2: comma-separated role names, or "no role". 276 #: src/Admin/AdminSettings.php:627 277 #, php-format 278 msgid "Members for Ko-fi: an account was created by Ko-fi's \"Send test\" button (%1$s, role: %2$s). It is not a real supporter; earlier versions treated tests as payments. You can safely delete it." 279 msgstr "Members for Ko-fi: Ko-fi's \"Send test\"-knap har oprettet en konto (%1$s, rolle: %2$s). Det er ikke en rigtig supporter; tidligere versioner behandlede test som betalinger. Du kan roligt slette den." 280 281 #: src/Admin/AdminSettings.php:629 282 msgid "none" 283 msgstr "ingen" 284 285 #: src/Admin/AdminSettings.php:633 286 msgid "Review the account" 287 msgstr "Se kontoen" 288 289 #: src/Admin/AdminSettings.php:643 290 msgid "Members for Ko-fi: two different verification tokens are stored, and webhooks are checked against the hashed one. Re-enter your Ko-fi verification token in the settings to resolve this." 291 msgstr "Members for Ko-fi: der er gemt to forskellige verifikationstokens, og webhooks tjekkes mod det hashede. Indtast dit Ko-fi-verifikationstoken igen i indstillingerne for at løse det." 292 293 #: src/Admin/AdminSettings.php:668 294 msgid "Open settings" 295 msgstr "Åbn indstillinger" 296 297 #: src/Admin/AdminSettings.php:701 109 298 msgid "Verification Token is required." 110 299 msgstr "Verifikationstoken skal udfyldes." 111 300 112 #: src/Admin/AdminSettings.php: 193301 #: src/Admin/AdminSettings.php:706 113 302 msgid "Role Expiry Days is required and must be a number when expiry is enabled." 114 303 msgstr "Når udløb er slået til, skal du angive rollens gyldighed i dage." 115 304 116 305 #. translators: %s is the role name that was rejected. 117 #: src/Admin/AdminSettings.php: 213118 #: src/Admin/AdminSettings.php: 229306 #: src/Admin/AdminSettings.php:726 307 #: src/Admin/AdminSettings.php:742 119 308 #, php-format 120 309 msgid "Security: Role \"%s\" cannot be assigned via webhook for security reasons." … … 122 311 123 312 #. translators: %s is the role name that was rejected. 124 #: src/Admin/AdminSettings.php: 241313 #: src/Admin/AdminSettings.php:754 125 314 #, php-format 126 315 msgid "Security: Role \"%s\" cannot be used as default role for security reasons." 127 316 msgstr "Sikkerhed: Rollen \"%s\" kan af sikkerhedshensyn ikke bruges som standardrolle." 128 317 129 #: src/Admin/AdminSettings.php:290 130 msgid "A token is saved. Leave this blank to keep it, or paste a new one to replace it." 131 msgstr "Der er gemt et token. Lad feltet stå tomt for at beholde det, eller indsæt et nyt for at erstatte det." 132 133 #. translators: %s is the first eight characters of the saved token's SHA-256 hash. 134 #: src/Admin/AdminSettings.php:293 135 #, php-format 136 msgid "Saved token fingerprint: %s" 137 msgstr "Fingeraftryk af det gemte token: %s" 138 139 #: src/Admin/AdminSettings.php:297 140 msgid "No token set yet. Webhooks are rejected until one is saved." 141 msgstr "Der er endnu ikke gemt et token. Webhook-forespørgsler afvises, indtil du gemmer et." 142 143 #. translators: %s is a link to the Ko-fi Webhooks Management page. 144 #: src/Admin/AdminSettings.php:307 145 #, php-format 146 msgid "This token is used to verify incoming webhook requests from Ko-fi. Paste the verification token from this page: %s and open the Advanced box. Only a hash of it is stored, so it cannot be shown again; to change it, paste the new one." 147 msgstr "Dette token bruges til at bekræfte, at webhook-forespørgsler kommer fra Ko-fi. Åbn %s, find sektionen Advanced, og kopiér verifikationstokenet. Der gemmes kun en hashværdi af tokenet, så det kan ikke vises igen. Indsæt et nyt token for at erstatte det." 148 149 #: src/Admin/AdminSettings.php:308 150 msgid "Ko-fi Webhooks Management" 151 msgstr "Ko-fi Webhooks Management" 152 153 #: src/Admin/AdminSettings.php:322 154 msgid "Webhook URL:" 155 msgstr "Webhook-URL:" 156 157 #: src/Admin/AdminSettings.php:325 158 msgid "Copy to Clipboard" 159 msgstr "Kopiér til udklipsholder" 160 161 #: src/Admin/AdminSettings.php:327 162 msgid "Use this URL to configure your Ko-fi webhook." 163 msgstr "Brug denne URL, når du opsætter din webhook i Ko-fi." 164 165 #. translators: %s is a human-readable time span, e.g. "3 days". 166 #: src/Admin/AdminSettings.php:372 167 #, php-format 168 msgid "Scheduled tasks are not running: role expiry and log cleanup are %s overdue. Make sure WP-Cron runs, or that a system cron calls wp-cron.php." 169 msgstr "Planlagte opgaver kører ikke: udløb af roller og oprydning i logs er %s forsinket. Sørg for, at WP-Cron kører, eller at et system-cronjob kalder wp-cron.php." 170 171 #. translators: 1: email address, 2: comma-separated role names, or "no role". 172 #: src/Admin/AdminSettings.php:424 173 #, php-format 174 msgid "Members for Ko-fi: an account was created by Ko-fi's \"Send test\" button (%1$s, role: %2$s). It is not a real supporter; earlier versions treated tests as payments. You can safely delete it." 175 msgstr "Members for Ko-fi: Ko-fi's \"Send test\"-knap har oprettet en konto (%1$s, rolle: %2$s). Det er ikke en rigtig supporter; tidligere versioner behandlede test som betalinger. Du kan roligt slette den." 176 177 #: src/Admin/AdminSettings.php:426 178 msgid "none" 179 msgstr "ingen" 180 181 #: src/Admin/AdminSettings.php:430 182 msgid "Review the account" 183 msgstr "Se kontoen" 184 185 #: src/Admin/AdminSettings.php:440 186 msgid "Members for Ko-fi: two different verification tokens are stored, and webhooks are checked against the hashed one. Re-enter your Ko-fi verification token in the settings to resolve this." 187 msgstr "Members for Ko-fi: der er gemt to forskellige verifikationstokens, og webhooks tjekkes mod det hashede. Indtast dit Ko-fi-verifikationstoken igen i indstillingerne for at løse det." 188 189 #: src/Admin/AdminSettings.php:465 190 msgid "Open settings" 191 msgstr "Åbn indstillinger" 192 193 #: src/Admin/AdminSettings.php:481 194 msgid "When enabled, users must be active subscribers to receive a role." 195 msgstr "Når det er slået til, skal brugere have et aktivt abonnement for at få en rolle." 196 197 #: src/Admin/AdminSettings.php:498 198 msgid "Ko-fi Tier" 199 msgstr "Ko-fi-niveau" 200 201 #: src/Admin/AdminSettings.php:498 202 #: src/Admin/AdminSettings.php:1097 318 #: src/Admin/LogsTab.php:245 319 msgid "the token doesn't match" 320 msgstr "tokenet passer ikke" 321 322 #: src/Admin/LogsTab.php:247 323 msgid "no token is saved" 324 msgstr "der er ikke gemt noget token" 325 326 #: src/Admin/LogsTab.php:252 327 msgid "the request was malformed" 328 msgstr "forespørgslen var ugyldig" 329 330 #: src/Admin/LogsTab.php:254 331 msgid "the account could not be created" 332 msgstr "kontoen kunne ikke oprettes" 333 334 #. translators: %s is a time of day. 335 #: src/Admin/LogsTab.php:292 336 #, php-format 337 msgid "Today, %s" 338 msgstr "I dag, %s" 339 340 #. translators: %s is a time of day. 341 #: src/Admin/LogsTab.php:297 342 #, php-format 343 msgid "Yesterday, %s" 344 msgstr "I går, %s" 345 346 #: src/Admin/LogsTab.php:365 347 msgid "The log was deleted." 348 msgstr "Loggen blev ryddet." 349 350 #: src/Admin/LogsTab.php:383 351 msgid "Member activity" 352 msgstr "Medlemsaktivitet" 353 354 #: src/Admin/LogsTab.php:396 355 msgid "Requests from Ko-fi" 356 msgstr "Forespørgsler fra Ko-fi" 357 358 #. translators: %s is the site's time zone, e.g. "Europe/Copenhagen". 359 #: src/Admin/LogsTab.php:401 360 #, php-format 361 msgid "Times are in the site's time zone (%s)." 362 msgstr "Tidspunkter er i webstedets tidszone (%s)." 363 364 #: src/Admin/LogsTab.php:412 365 #: src/Admin/LogsTab.php:477 366 msgid "Outcome" 367 msgstr "Resultat" 368 369 #: src/Admin/LogsTab.php:414 370 msgid "All outcomes" 371 msgstr "Alle resultater" 372 373 #: src/Admin/LogsTab.php:415 374 #: src/Admin/LogsTab.php:547 375 msgid "Accepted" 376 msgstr "Accepteret" 377 378 #: src/Admin/LogsTab.php:416 379 msgid "Refused" 380 msgstr "Afvist" 381 382 #: src/Admin/LogsTab.php:424 383 msgid "Search email, tier or reason" 384 msgstr "Søg i e-mail, niveau eller årsag" 385 386 #: src/Admin/LogsTab.php:426 387 msgid "Search email, action or role" 388 msgstr "Søg i e-mail, handling eller rolle" 389 390 #: src/Admin/LogsTab.php:433 391 msgid "Per page" 392 msgstr "Pr. side" 393 394 #: src/Admin/LogsTab.php:440 395 msgid "Filter" 396 msgstr "Filtrér" 397 398 #: src/Admin/LogsTab.php:473 399 msgid "Received" 400 msgstr "Modtaget" 401 402 #: src/Admin/LogsTab.php:474 403 #: src/Privacy/PersonalData.php:321 404 #: src/Webhook/Webhook.php:911 405 #: src/Webhook/Webhook.php:931 406 msgid "Supporter" 407 msgstr "Supporter" 408 409 #: src/Admin/LogsTab.php:475 410 msgid "Tier" 411 msgstr "Niveau" 412 413 #: src/Admin/LogsTab.php:476 414 msgid "Payment" 415 msgstr "Betaling" 416 417 #: src/Admin/LogsTab.php:478 418 msgid "Details" 419 msgstr "Detaljer" 420 421 #: src/Admin/LogsTab.php:483 422 #: src/Admin/LogsTab.php:610 423 msgid "No entries match." 424 msgstr "Ingen poster matcher." 425 426 #: src/Admin/LogsTab.php:483 427 #: src/Admin/LogsTab.php:610 428 msgid "No entries yet." 429 msgstr "Ingen poster endnu." 430 431 #: src/Admin/LogsTab.php:504 432 msgid "Ko-fi test" 433 msgstr "Ko-fi-test" 434 435 #: src/Admin/LogsTab.php:506 436 msgid "Not stored" 437 msgstr "Ikke gemt" 438 439 #: src/Admin/LogsTab.php:522 440 msgid "not mapped" 441 msgstr "ikke tilknyttet" 442 443 #: src/Admin/LogsTab.php:530 444 msgid "Test, no account made" 445 msgstr "Test, ingen konto oprettet" 446 447 #. translators: %s is an amount with its currency, e.g. "5.00 EUR". 448 #: src/Admin/LogsTab.php:536 449 #, php-format 450 msgid "%s · subscription" 451 msgstr "%s · abonnement" 452 453 #. translators: %s is an amount with its currency, e.g. "5.00 EUR". 454 #: src/Admin/LogsTab.php:538 455 #, php-format 456 msgid "%s · one-off" 457 msgstr "%s · enkeltbetaling" 458 459 #. translators: %s is why the request was refused, e.g. "the token doesn't match". 460 #: src/Admin/LogsTab.php:552 461 #, php-format 462 msgid "Refused: %s" 463 msgstr "Afvist: %s" 464 465 #: src/Admin/LogsTab.php:560 466 msgid "Payload" 467 msgstr "Indhold" 468 469 #: src/Admin/LogsTab.php:602 470 msgid "When" 471 msgstr "Tidspunkt" 472 473 #: src/Admin/LogsTab.php:603 474 #: src/Admin/MembersTab.php:264 475 msgid "Member" 476 msgstr "Medlem" 477 478 #: src/Admin/LogsTab.php:604 479 msgid "What happened" 480 msgstr "Hændelse" 481 482 #: src/Admin/LogsTab.php:605 483 #: src/Admin/MembersTab.php:265 203 484 msgid "Role" 204 485 msgstr "Rolle" 205 486 206 #: src/Admin/AdminSettings.php:536 207 msgid "Add Tier Mapping" 208 msgstr "Tilføj niveautilknytning" 209 210 #: src/Admin/AdminSettings.php:537 211 msgid "Map Ko-fi tiers to WordPress roles. Only exact tier names will be matched." 212 msgstr "Knyt Ko-fi-niveauer til WordPress-roller. Kun niveaunavne, der er helt ens, matcher." 213 214 #: src/Admin/AdminSettings.php:554 215 msgid "— No default —" 216 msgstr "— Ingen standard —" 217 218 #: src/Admin/AdminSettings.php:563 219 msgid "If no tier matches, or if you are not using tiers, this role will be assigned to the user." 220 msgstr "Hvis intet niveau matcher, eller hvis du ikke bruger niveauer, får brugeren denne rolle." 221 222 #: src/Admin/AdminSettings.php:594 223 msgid "Number of days the role remains active (only used if expiry is enabled)." 224 msgstr "Antal dage rollen er aktiv (bruges kun, når udløb er slået til)." 225 226 #: src/Admin/AdminSettings.php:606 227 msgid "Enable automatic deletion of old logs based on retention period." 228 msgstr "Slet automatisk logs, der er ældre end opbevaringsperioden." 229 230 #: src/Admin/AdminSettings.php:618 231 msgid "Number of days to keep logs before automatic deletion (minimum 1 day)." 232 msgstr "Antal dage, logs gemmes, før de slettes automatisk (mindst 1 dag)." 233 234 #: src/Admin/AdminSettings.php:646 235 msgid "Members for Ko-fi Settings" 236 msgstr "Indstillinger for Members for Ko-fi" 237 238 #: src/Admin/AdminSettings.php:657 239 msgid "Settings" 240 msgstr "Indstillinger" 241 242 #: src/Admin/AdminSettings.php:660 243 #: src/Admin/AdminSettings.php:683 244 msgid "Logs" 245 msgstr "Logs" 246 247 #: src/Admin/AdminSettings.php:728 248 msgid "Log Type:" 249 msgstr "Logtype:" 250 251 #: src/Admin/AdminSettings.php:732 252 msgid "User Logs" 253 msgstr "Brugerlog" 254 255 #: src/Admin/AdminSettings.php:735 256 msgid "Request Logs" 257 msgstr "Forespørgselslog" 258 259 #: src/Admin/AdminSettings.php:755 260 msgid "Search" 261 msgstr "Søg" 262 263 #: src/Admin/AdminSettings.php:756 264 msgid "Reset" 265 msgstr "Nulstil" 266 267 #: src/Admin/AdminSettings.php:759 268 msgid "Refresh" 269 msgstr "Opdater" 270 271 #: src/Admin/AdminSettings.php:790 272 #: src/Admin/AdminSettings.php:826 273 #: src/Admin/AdminSettings.php:900 274 #: src/Admin/AdminSettings.php:929 275 #: src/Admin/AdminSettings.php:960 276 #: src/Admin/AdminSettings.php:983 487 #: src/Admin/LogsTab.php:651 488 msgid "First page" 489 msgstr "Første side" 490 491 #: src/Admin/LogsTab.php:652 492 #: src/Admin/MembersTab.php:351 493 msgid "Previous page" 494 msgstr "Forrige side" 495 496 #: src/Admin/LogsTab.php:653 497 #: src/Admin/MembersTab.php:362 498 msgid "Next page" 499 msgstr "Næste side" 500 501 #: src/Admin/LogsTab.php:654 502 msgid "Last page" 503 msgstr "Sidste side" 504 505 #: src/Admin/LogsTab.php:665 506 msgid "Current page" 507 msgstr "Aktuel side" 508 509 #: src/Admin/LogsTab.php:676 510 #: src/Admin/MembersTab.php:349 511 msgid "Pages" 512 msgstr "Sider" 513 514 #. translators: 1: an input holding the current page number, 2: the number of pages. 515 #. translators: 1: the current page number, 2: the number of pages. 516 #: src/Admin/LogsTab.php:683 517 #: src/Admin/MembersTab.php:356 518 #, php-format 519 msgid "Page %1$s of %2$s" 520 msgstr "Side %1$s af %2$s" 521 522 #. translators: %s is the number of entries. 523 #: src/Admin/LogsTab.php:715 524 #, php-format 525 msgid "Removes %s request entry. Member activity is kept." 526 msgid_plural "Removes all %s request entries. Member activity is kept." 527 msgstr[0] "Fjerner %s post i forespørgselsloggen. Medlemsaktiviteten bevares." 528 msgstr[1] "Fjerner alle %s poster i forespørgselsloggen. Medlemsaktiviteten bevares." 529 530 #. translators: %s is the number of entries. 531 #: src/Admin/LogsTab.php:717 532 #, php-format 533 msgid "Removes %s activity entry. The request log is kept." 534 msgid_plural "Removes all %s activity entries. The request log is kept." 535 msgstr[0] "Fjerner %s post i medlemsaktiviteten. Forespørgselsloggen bevares." 536 msgstr[1] "Fjerner alle %s poster i medlemsaktiviteten. Forespørgselsloggen bevares." 537 538 #. translators: %s is the number of entries. 539 #: src/Admin/LogsTab.php:720 540 #, php-format 541 msgid "Delete %s entry in this log? This can't be undone." 542 msgid_plural "Delete all %s entries in this log? This can't be undone." 543 msgstr[0] "Ryd %s post i denne log? Det kan ikke fortrydes." 544 msgstr[1] "Ryd alle %s poster i denne log? Det kan ikke fortrydes." 545 546 #: src/Admin/LogsTab.php:726 547 msgid "Delete this log" 548 msgstr "Ryd denne log" 549 550 #. translators: %d is a number of days. 551 #: src/Admin/LogsTab.php:734 552 #, php-format 553 msgid "Entries older than %d day are deleted automatically." 554 msgid_plural "Entries older than %d days are deleted automatically." 555 msgstr[0] "Poster ældre end %d dag slettes automatisk." 556 msgstr[1] "Poster ældre end %d dage slettes automatisk." 557 558 #: src/Admin/LogsTab.php:747 559 msgid "Delete request log…" 560 msgstr "Ryd forespørgselsloggen…" 561 562 #: src/Admin/LogsTab.php:747 563 msgid "Delete activity log…" 564 msgstr "Ryd aktivitetsloggen…" 565 566 #: src/Admin/LogsTab.php:761 277 567 msgid "You do not have permission to access this resource." 278 568 msgstr "Du har ikke adgang til denne ressource." 279 569 280 #: src/Admin/AdminSettings.php:799 281 msgid "Invalid page number." 282 msgstr "Ugyldigt sidetal." 283 284 #: src/Admin/AdminSettings.php:859 285 msgid "Logs table does not exist." 286 msgstr "Logtabellen findes ikke." 287 288 #: src/Admin/AdminSettings.php:1093 289 #: src/Admin/AdminSettings.php:1235 290 msgid "Timestamp" 291 msgstr "Tidspunkt" 292 293 #: src/Admin/AdminSettings.php:1094 294 msgid "User ID" 295 msgstr "Bruger-ID" 296 297 #: src/Admin/AdminSettings.php:1095 298 #: src/Admin/AdminSettings.php:1236 299 msgid "Email" 300 msgstr "E-mail" 301 302 #: src/Admin/AdminSettings.php:1096 303 msgid "Action" 304 msgstr "Handling" 305 306 #: src/Admin/AdminSettings.php:1103 307 #: src/Admin/AdminSettings.php:1248 308 msgid "No logs available." 309 msgstr "Der er ingen logs." 310 311 #: src/Admin/AdminSettings.php:1125 312 #: src/Admin/AdminSettings.php:1273 313 msgid "Clear Logs" 314 msgstr "Ryd logs" 315 316 #: src/Admin/AdminSettings.php:1133 317 #: src/Admin/AdminSettings.php:1281 318 msgid "Rows per page:" 319 msgstr "Rækker pr. side:" 320 321 #: src/Admin/AdminSettings.php:1148 322 #: src/Admin/AdminSettings.php:1296 323 msgid "Previous" 324 msgstr "Forrige" 325 326 #: src/Admin/AdminSettings.php:1155 327 #: src/Admin/AdminSettings.php:1303 328 msgid "Next" 329 msgstr "Næste" 330 331 #: src/Admin/AdminSettings.php:1237 332 msgid "Tier Name" 333 msgstr "Niveaunavn" 334 335 #: src/Admin/AdminSettings.php:1238 336 msgid "Amount" 337 msgstr "Beløb" 338 339 #: src/Admin/AdminSettings.php:1239 340 msgid "Subscription" 341 msgstr "Abonnement" 342 343 #: src/Admin/AdminSettings.php:1240 570 #: src/Admin/MembersTab.php:195 571 msgid "Unknown" 572 msgstr "Ukendt" 573 574 #: src/Admin/MembersTab.php:214 575 msgid "All" 576 msgstr "Alle" 577 578 #: src/Admin/MembersTab.php:215 579 #: src/Admin/MembersTab.php:316 580 msgid "Active" 581 msgstr "Aktive" 582 583 #. translators: The members whose access ends within the next seven days. 584 #: src/Admin/MembersTab.php:217 585 msgid "Ending within 7 days" 586 msgstr "Ophører inden for 7 dage" 587 588 #: src/Admin/MembersTab.php:218 589 #: src/Admin/MembersTab.php:305 590 msgid "Ended" 591 msgstr "Ophørt" 592 593 #: src/Admin/MembersTab.php:254 594 #: src/Admin/MembersTab.php:256 595 msgid "Search members" 596 msgstr "Søg efter medlemmer" 597 598 #: src/Admin/MembersTab.php:255 599 msgid "Name or email" 600 msgstr "Navn eller e-mail" 601 602 #: src/Admin/MembersTab.php:266 344 603 msgid "Status" 345 604 msgstr "Status" 346 605 347 #: src/Admin/AdminSettings.php:1241 348 msgid "Success" 349 msgstr "Gennemført" 350 351 #: src/Admin/AdminSettings.php:1242 352 msgid "Error" 353 msgstr "Fejl" 354 355 #: src/Admin/AdminSettings.php:1257 356 msgid "Yes" 357 msgstr "Ja" 358 359 #: src/Admin/AdminSettings.php:1257 360 msgid "No" 361 msgstr "Nej" 606 #: src/Admin/MembersTab.php:267 607 msgid "Member since" 608 msgstr "Medlem siden" 609 610 #: src/Admin/MembersTab.php:268 611 msgid "Last payment" 612 msgstr "Seneste betaling" 613 614 #: src/Admin/MembersTab.php:269 615 msgid "Access until" 616 msgstr "Adgang indtil" 617 618 #: src/Admin/MembersTab.php:274 619 msgid "Nobody has become a member through Ko-fi yet." 620 msgstr "Ingen er endnu blevet medlem via Ko-fi." 621 622 #: src/Admin/MembersTab.php:274 623 msgid "No members match." 624 msgstr "Ingen medlemmer matcher." 625 626 #. translators: %s is the name of a role that was removed. 627 #: src/Admin/MembersTab.php:293 628 #, php-format 629 msgid "%s, removed" 630 msgstr "%s, fjernet" 631 632 #: src/Admin/MembersTab.php:297 633 msgid "Granted before payment; never removed" 634 msgstr "Tildelt før betaling; fjernes aldrig" 635 636 #. translators: %d is a number of days. 637 #: src/Admin/MembersTab.php:311 638 #, php-format 639 msgid "Ends in %d day" 640 msgid_plural "Ends in %d days" 641 msgstr[0] "Udløber om %d dag" 642 msgstr[1] "Udløber om %d dage" 643 644 #: src/Admin/MembersTab.php:325 645 msgid "subscription" 646 msgstr "abonnement" 647 648 #: src/Admin/MembersTab.php:325 649 msgid "one-off" 650 msgstr "enkeltbetaling" 651 652 #: src/Admin/MembersTab.php:332 653 msgid "No expiry" 654 msgstr "Udløber ikke" 655 656 #. translators: %s is a date. 657 #: src/Admin/MembersTab.php:335 658 #, php-format 659 msgid "Ended %s" 660 msgstr "Ophørte %s" 661 662 #: src/Admin/MembersTab.php:367 663 msgid "Cancelling happens on Ko-fi, not here." 664 msgstr "Opsigelse sker hos Ko-fi, ikke her." 362 665 363 666 #: src/Membership.php:198 … … 381 684 msgstr "Log ind på Ko-fi, og gå til Account settings og Subscriptions. Betaler du med PayPal, kan du også opsige det i PayPal." 382 685 383 #: src/Privacy/PersonalData.php:6 4384 #: src/Privacy/PersonalData.php: 79686 #: src/Privacy/PersonalData.php:65 687 #: src/Privacy/PersonalData.php:80 385 688 msgid "Members for Ko-fi logs" 386 689 msgstr "Logs fra Members for Ko-fi" 387 690 388 #: src/Privacy/PersonalData.php:11 5691 #: src/Privacy/PersonalData.php:119 389 692 msgid "Ko-fi membership" 390 693 msgstr "Ko-fi-medlemskab" 391 694 392 #: src/Privacy/PersonalData.php:14 5695 #: src/Privacy/PersonalData.php:148 393 696 msgid "Ko-fi membership activity" 394 697 msgstr "Aktivitet knyttet til Ko-fi-medlemskab" 395 698 396 #: src/Privacy/PersonalData.php:16 5699 #: src/Privacy/PersonalData.php:166 397 700 msgid "Ko-fi webhook requests" 398 701 msgstr "Ko-fi-webhook-forespørgsler" 399 702 400 #: src/Privacy/PersonalData.php:2 27703 #: src/Privacy/PersonalData.php:242 401 704 msgid "An active Ko-fi membership was kept, so the person keeps the access they pay for. It is removed when the membership ends." 402 705 msgstr "Et aktivt Ko-fi-medlemskab blev bevaret, så personen fortsat har den adgang, vedkommende betaler for. Adgangen fjernes, når medlemskabet ophører." 403 706 404 #: src/Privacy/PersonalData.php:294 405 #: src/Webhook/Webhook.php:753 406 #: src/Webhook/Webhook.php:772 407 msgid "Supporter" 408 msgstr "Supporter" 409 410 #: src/Privacy/PersonalData.php:335 707 #: src/Privacy/PersonalData.php:362 411 708 msgid "When you support us on Ko-fi, Ko-fi sends this site your email address, the name and message you entered, the amount, and your membership tier. We use your email address to create or find your account here and give it the access your support pays for." 412 709 msgstr "Når du støtter os på Ko-fi, sender Ko-fi din e-mailadresse, det navn og den besked, du skrev, beløbet og dit medlemsniveau til denne side. Vi bruger din e-mailadresse til at oprette eller finde din konto her og give dig den adgang, din støtte betaler for." 413 710 414 #: src/Privacy/PersonalData.php:3 36711 #: src/Privacy/PersonalData.php:363 415 712 msgid "A record of each payment and of the access granted or removed is kept for the number of days set by the site owner, then deleted automatically when log cleanup is enabled. You can ask for a copy of this data, or for it to be erased, through the site's personal data request process." 416 713 msgstr "Hver betaling og hver tildelt eller fjernet adgang registreres og gemmes i det antal dage, sidens ejer har valgt, og slettes derefter automatisk, når oprydning i logs er slået til. Du kan bede om en kopi af disse data eller om at få dem slettet via sidens procedure for anmodninger om persondata." 417 418 #: src/Admin/AdminSettings.php419 msgid "Only assign roles to subscribers."420 msgstr "Tildel kun roller til abonnenter."421 422 #: src/Admin/AdminSettings.php423 msgid "If checked, role will be removed after the set number of days."424 msgstr "Hvis markeret, fjernes rollen efter det angivne antal dage." -
members-for-kofi/tags/1.7.0/languages/members-for-kofi.pot
r3796045 r3796384 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Members for Ko-fi 1. 6.1\n"5 "Project-Id-Version: Members for Ko-fi 1.7.0\n" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/members-for-kofi\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-11-11T 09:51:50+00:00\n"12 "POT-Creation-Date: 2026-11-11T11:11:06+00:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 14 "X-Generator: WP-CLI 2.12.0\n" … … 17 17 #. Plugin Name of the plugin 18 18 #: members-for-kofi.php 19 #: src/Admin/AdminSettings.php:162 19 20 #: src/Plugin.php:436 20 21 #: src/Plugin.php:437 21 #: src/Privacy/PersonalData.php:36 322 #: src/Privacy/PersonalData.php:360 22 23 msgid "Members for Ko-fi" 23 24 msgstr "" … … 43 44 msgstr "" 44 45 45 #: src/Admin/AdminSettings.php:107 46 #: src/Admin/AdminSettings.php:671 47 msgid "Ko-fi Settings" 48 msgstr "" 49 50 #: src/Admin/AdminSettings.php:108 51 msgid "Verification Token" 52 msgstr "" 53 54 #: src/Admin/AdminSettings.php:111 55 #: src/Admin/AdminSettings.php:675 56 msgid "Role Assignment" 57 msgstr "" 58 59 #: src/Admin/AdminSettings.php:112 60 msgid "Only Accept Subscriptions" 61 msgstr "" 62 63 #: src/Admin/AdminSettings.php:113 64 msgid "Tier to Role Mapping" 65 msgstr "" 66 67 #: src/Admin/AdminSettings.php:114 68 msgid "Default Role (if no match)" 69 msgstr "" 70 71 #: src/Admin/AdminSettings.php:115 72 msgid "Enable Expiry" 73 msgstr "" 74 75 #: src/Admin/AdminSettings.php:116 76 msgid "Role Expiry (days)" 77 msgstr "" 78 79 #: src/Admin/AdminSettings.php:119 80 #: src/Admin/AdminSettings.php:679 81 msgid "Logging" 82 msgstr "" 83 84 #: src/Admin/AdminSettings.php:120 85 msgid "Automatically Clear Logs" 86 msgstr "" 87 88 #: src/Admin/AdminSettings.php:121 46 #: src/Admin/AdminSettings.php:110 47 msgid "Copied" 48 msgstr "" 49 50 #: src/Admin/AdminSettings.php:156 51 msgid "Settings" 52 msgstr "" 53 54 #: src/Admin/AdminSettings.php:157 55 msgid "Members" 56 msgstr "" 57 58 #: src/Admin/AdminSettings.php:158 59 msgid "Logs" 60 msgstr "" 61 62 #: src/Admin/AdminSettings.php:171 63 msgid "Members for Ko-fi sections" 64 msgstr "" 65 66 #: src/Admin/AdminSettings.php:201 67 msgid "Ko-fi connection" 68 msgstr "" 69 70 #: src/Admin/AdminSettings.php:208 71 msgid "Press \"Send test\" on Ko-fi" 72 msgstr "" 73 74 #. translators: %s is a link to the request log. 75 #: src/Admin/AdminSettings.php:213 76 #, php-format 77 msgid "It shows up in the status line above and in %s. A test never creates an account." 78 msgstr "" 79 80 #: src/Admin/AdminSettings.php:214 81 msgid "the request log" 82 msgstr "" 83 84 #: src/Admin/AdminSettings.php:223 85 msgid "Who gets a role" 86 msgstr "" 87 88 #: src/Admin/AdminSettings.php:232 89 msgid "When membership ends" 90 msgstr "" 91 92 #: src/Admin/AdminSettings.php:239 93 msgid "Ko-fi subscriptions renew monthly, so 35 days leaves a few days' grace. A role the user held before paying is never removed." 94 msgstr "" 95 96 #: src/Admin/AdminSettings.php:243 97 msgid "Log history" 98 msgstr "" 99 100 #: src/Admin/AdminSettings.php:250 101 msgid "Untick to keep everything, for example when another tool reads the request log." 102 msgstr "" 103 104 #: src/Admin/AdminSettings.php:272 105 msgid "Not connected: no token is saved, so Ko-fi's requests are refused." 106 msgstr "" 107 108 #: src/Admin/AdminSettings.php:277 109 msgid "Waiting for Ko-fi: no request has arrived yet." 110 msgstr "" 111 112 #. translators: %s is a time span, e.g. "2 hours". 113 #: src/Admin/AdminSettings.php:283 114 #, php-format 115 msgid "Working: the last request from Ko-fi, %s ago, was accepted." 116 msgstr "" 117 118 #. translators: 1: a time span, e.g. "2 hours", 2: why it was refused, e.g. "the token doesn't match". 119 #: src/Admin/AdminSettings.php:287 120 #, php-format 121 msgid "The last request, %1$s ago, was refused: %2$s" 122 msgstr "" 123 124 #: src/Admin/AdminSettings.php:302 125 msgid "Ko-fi's webhook settings" 126 msgstr "" 127 128 #: src/Admin/AdminSettings.php:303 129 msgid "(opens in a new tab)" 130 msgstr "" 131 132 #: src/Admin/AdminSettings.php:305 133 msgid "Give Ko-fi this webhook URL" 134 msgstr "" 135 136 #: src/Admin/AdminSettings.php:308 137 msgid "Copy URL" 138 msgstr "" 139 140 #. translators: %s is a link to Ko-fi's webhook settings. 141 #: src/Admin/AdminSettings.php:314 142 #, php-format 143 msgid "Paste it into %s." 144 msgstr "" 145 146 #: src/Admin/AdminSettings.php:329 147 msgid "Paste Ko-fi's verification token" 148 msgstr "" 149 150 #: src/Admin/AdminSettings.php:332 151 msgid "A token is saved" 152 msgstr "" 153 154 #: src/Admin/AdminSettings.php:334 155 msgid "fingerprint, to compare with another site" 156 msgstr "" 157 158 #: src/Admin/AdminSettings.php:337 159 msgid "No token is saved yet." 160 msgstr "" 161 162 #: src/Admin/AdminSettings.php:343 163 msgid "Leave blank to keep the saved token" 164 msgstr "" 165 166 #: src/Admin/AdminSettings.php:344 167 msgid "On Ko-fi's webhook page, open Advanced and copy the token. Only a hash is stored, so it can't be shown again." 168 msgstr "" 169 170 #: src/Admin/AdminSettings.php:358 171 msgid "Payments that grant a role" 172 msgstr "" 173 174 #: src/Admin/AdminSettings.php:359 175 msgid "Donations and subscriptions" 176 msgstr "" 177 178 #: src/Admin/AdminSettings.php:360 179 msgid "Subscriptions only" 180 msgstr "" 181 182 #: src/Admin/AdminSettings.php:360 183 msgid "(one-off donations are logged and ignored)" 184 msgstr "" 185 186 #: src/Admin/AdminSettings.php:393 187 #: src/Admin/AdminSettings.php:421 188 msgid "Tier name on Ko-fi" 189 msgstr "" 190 191 #: src/Admin/AdminSettings.php:394 192 msgid "Role for this tier" 193 msgstr "" 194 195 #: src/Admin/AdminSettings.php:395 196 msgid "Remove this tier" 197 msgstr "" 198 199 #: src/Admin/AdminSettings.php:395 200 msgid "Remove" 201 msgstr "" 202 203 #: src/Admin/AdminSettings.php:416 204 msgid "Ko-fi tiers" 205 msgstr "" 206 207 #: src/Admin/AdminSettings.php:422 208 msgid "Role it grants" 209 msgstr "" 210 211 #: src/Admin/AdminSettings.php:423 212 msgid "Actions" 213 msgstr "" 214 215 #: src/Admin/AdminSettings.php:433 216 msgid "Add tier" 217 msgstr "" 218 219 #: src/Admin/AdminSettings.php:434 220 msgid "Names match whatever their capitalisation." 221 msgstr "" 222 223 #: src/Admin/AdminSettings.php:438 224 msgid "Tiers Ko-fi sent that aren't listed:" 225 msgstr "" 226 227 #. translators: 1: a tier name, 2: how many payments named it. 228 #: src/Admin/AdminSettings.php:445 229 #, php-format 230 msgid "Add \"%1$s\" (%2$d payment)" 231 msgid_plural "Add \"%1$s\" (%2$d payments)" 232 msgstr[0] "" 233 msgstr[1] "" 234 235 #: src/Admin/AdminSettings.php:469 236 msgid "Any other tier, or no tier" 237 msgstr "" 238 239 #: src/Admin/AdminSettings.php:471 240 msgid "— No role —" 241 msgstr "" 242 243 #: src/Admin/AdminSettings.php:474 244 msgid "With \"No role\", those payers still get an account, but no role. Administrator and other roles that can run the site are never offered." 245 msgstr "" 246 247 #: src/Admin/AdminSettings.php:489 248 msgid "Remove the role" 249 msgstr "" 250 251 #: src/Admin/AdminSettings.php:500 252 msgid "Days after the last payment" 253 msgstr "" 254 255 #: src/Admin/AdminSettings.php:502 256 msgid "days after the member's last payment" 257 msgstr "" 258 259 #: src/Admin/AdminSettings.php:518 260 msgid "Delete log entries older than" 261 msgstr "" 262 263 #: src/Admin/AdminSettings.php:529 89 264 msgid "Number of Days to Keep Logs" 90 265 msgstr "" 91 266 92 #: src/Admin/AdminSettings.php:150 93 msgid "Are you sure you want to clear all logs? This action cannot be undone." 94 msgstr "" 95 96 #: src/Admin/AdminSettings.php:151 97 msgid "An error occurred. Please try again." 98 msgstr "" 99 100 #: src/Admin/AdminSettings.php:152 101 #: src/Admin/AdminSettings.php:752 102 msgid "Search (email / action / role)" 103 msgstr "" 104 105 #: src/Admin/AdminSettings.php:153 106 #: src/Admin/AdminSettings.php:750 107 msgid "Search (email / tier / status)" 108 msgstr "" 109 110 #: src/Admin/AdminSettings.php:188 267 #: src/Admin/AdminSettings.php:531 268 msgid "days" 269 msgstr "" 270 271 #. translators: %s is a human-readable time span, e.g. "3 days". 272 #: src/Admin/AdminSettings.php:575 273 #, php-format 274 msgid "Scheduled tasks are not running: role expiry and log cleanup are %s overdue. Make sure WP-Cron runs, or that a system cron calls wp-cron.php." 275 msgstr "" 276 277 #. translators: 1: email address, 2: comma-separated role names, or "no role". 278 #: src/Admin/AdminSettings.php:627 279 #, php-format 280 msgid "Members for Ko-fi: an account was created by Ko-fi's \"Send test\" button (%1$s, role: %2$s). It is not a real supporter; earlier versions treated tests as payments. You can safely delete it." 281 msgstr "" 282 283 #: src/Admin/AdminSettings.php:629 284 msgid "none" 285 msgstr "" 286 287 #: src/Admin/AdminSettings.php:633 288 msgid "Review the account" 289 msgstr "" 290 291 #: src/Admin/AdminSettings.php:643 292 msgid "Members for Ko-fi: two different verification tokens are stored, and webhooks are checked against the hashed one. Re-enter your Ko-fi verification token in the settings to resolve this." 293 msgstr "" 294 295 #: src/Admin/AdminSettings.php:668 296 msgid "Open settings" 297 msgstr "" 298 299 #: src/Admin/AdminSettings.php:701 111 300 msgid "Verification Token is required." 112 301 msgstr "" 113 302 114 #: src/Admin/AdminSettings.php: 193303 #: src/Admin/AdminSettings.php:706 115 304 msgid "Role Expiry Days is required and must be a number when expiry is enabled." 116 305 msgstr "" 117 306 118 307 #. translators: %s is the role name that was rejected. 119 #: src/Admin/AdminSettings.php: 213120 #: src/Admin/AdminSettings.php: 229308 #: src/Admin/AdminSettings.php:726 309 #: src/Admin/AdminSettings.php:742 121 310 #, php-format 122 311 msgid "Security: Role \"%s\" cannot be assigned via webhook for security reasons." … … 124 313 125 314 #. translators: %s is the role name that was rejected. 126 #: src/Admin/AdminSettings.php: 241315 #: src/Admin/AdminSettings.php:754 127 316 #, php-format 128 317 msgid "Security: Role \"%s\" cannot be used as default role for security reasons." 129 318 msgstr "" 130 319 131 #: src/Admin/AdminSettings.php:290 132 msgid "A token is saved. Leave this blank to keep it, or paste a new one to replace it." 133 msgstr "" 134 135 #. translators: %s is the first eight characters of the saved token's SHA-256 hash. 136 #: src/Admin/AdminSettings.php:293 137 #, php-format 138 msgid "Saved token fingerprint: %s" 139 msgstr "" 140 141 #: src/Admin/AdminSettings.php:297 142 msgid "No token set yet. Webhooks are rejected until one is saved." 143 msgstr "" 144 145 #. translators: %s is a link to the Ko-fi Webhooks Management page. 146 #: src/Admin/AdminSettings.php:307 147 #, php-format 148 msgid "This token is used to verify incoming webhook requests from Ko-fi. Paste the verification token from this page: %s and open the Advanced box. Only a hash of it is stored, so it cannot be shown again; to change it, paste the new one." 149 msgstr "" 150 151 #: src/Admin/AdminSettings.php:308 152 msgid "Ko-fi Webhooks Management" 153 msgstr "" 154 155 #: src/Admin/AdminSettings.php:322 156 msgid "Webhook URL:" 157 msgstr "" 158 159 #: src/Admin/AdminSettings.php:325 160 msgid "Copy to Clipboard" 161 msgstr "" 162 163 #: src/Admin/AdminSettings.php:327 164 msgid "Use this URL to configure your Ko-fi webhook." 165 msgstr "" 166 167 #. translators: %s is a human-readable time span, e.g. "3 days". 168 #: src/Admin/AdminSettings.php:372 169 #, php-format 170 msgid "Scheduled tasks are not running: role expiry and log cleanup are %s overdue. Make sure WP-Cron runs, or that a system cron calls wp-cron.php." 171 msgstr "" 172 173 #. translators: 1: email address, 2: comma-separated role names, or "no role". 174 #: src/Admin/AdminSettings.php:424 175 #, php-format 176 msgid "Members for Ko-fi: an account was created by Ko-fi's \"Send test\" button (%1$s, role: %2$s). It is not a real supporter; earlier versions treated tests as payments. You can safely delete it." 177 msgstr "" 178 179 #: src/Admin/AdminSettings.php:426 180 msgid "none" 181 msgstr "" 182 183 #: src/Admin/AdminSettings.php:430 184 msgid "Review the account" 185 msgstr "" 186 187 #: src/Admin/AdminSettings.php:440 188 msgid "Members for Ko-fi: two different verification tokens are stored, and webhooks are checked against the hashed one. Re-enter your Ko-fi verification token in the settings to resolve this." 189 msgstr "" 190 191 #: src/Admin/AdminSettings.php:465 192 msgid "Open settings" 193 msgstr "" 194 195 #: src/Admin/AdminSettings.php:480 196 msgid "Only assign roles to subscribers." 197 msgstr "" 198 199 #: src/Admin/AdminSettings.php:481 200 msgid "When enabled, users must be active subscribers to receive a role." 201 msgstr "" 202 203 #: src/Admin/AdminSettings.php:498 204 msgid "Ko-fi Tier" 205 msgstr "" 206 207 #: src/Admin/AdminSettings.php:498 208 #: src/Admin/AdminSettings.php:1101 320 #: src/Admin/LogsTab.php:245 321 msgid "the token doesn't match" 322 msgstr "" 323 324 #: src/Admin/LogsTab.php:247 325 msgid "no token is saved" 326 msgstr "" 327 328 #: src/Admin/LogsTab.php:252 329 msgid "the request was malformed" 330 msgstr "" 331 332 #: src/Admin/LogsTab.php:254 333 msgid "the account could not be created" 334 msgstr "" 335 336 #. translators: %s is a time of day. 337 #: src/Admin/LogsTab.php:292 338 #, php-format 339 msgid "Today, %s" 340 msgstr "" 341 342 #. translators: %s is a time of day. 343 #: src/Admin/LogsTab.php:297 344 #, php-format 345 msgid "Yesterday, %s" 346 msgstr "" 347 348 #: src/Admin/LogsTab.php:365 349 msgid "The log was deleted." 350 msgstr "" 351 352 #: src/Admin/LogsTab.php:383 353 msgid "Member activity" 354 msgstr "" 355 356 #: src/Admin/LogsTab.php:396 357 msgid "Requests from Ko-fi" 358 msgstr "" 359 360 #. translators: %s is the site's time zone, e.g. "Europe/Copenhagen". 361 #: src/Admin/LogsTab.php:401 362 #, php-format 363 msgid "Times are in the site's time zone (%s)." 364 msgstr "" 365 366 #: src/Admin/LogsTab.php:412 367 #: src/Admin/LogsTab.php:477 368 msgid "Outcome" 369 msgstr "" 370 371 #: src/Admin/LogsTab.php:414 372 msgid "All outcomes" 373 msgstr "" 374 375 #: src/Admin/LogsTab.php:415 376 #: src/Admin/LogsTab.php:547 377 msgid "Accepted" 378 msgstr "" 379 380 #: src/Admin/LogsTab.php:416 381 msgid "Refused" 382 msgstr "" 383 384 #: src/Admin/LogsTab.php:424 385 msgid "Search email, tier or reason" 386 msgstr "" 387 388 #: src/Admin/LogsTab.php:426 389 msgid "Search email, action or role" 390 msgstr "" 391 392 #: src/Admin/LogsTab.php:433 393 msgid "Per page" 394 msgstr "" 395 396 #: src/Admin/LogsTab.php:440 397 msgid "Filter" 398 msgstr "" 399 400 #: src/Admin/LogsTab.php:473 401 msgid "Received" 402 msgstr "" 403 404 #: src/Admin/LogsTab.php:474 405 #: src/Privacy/PersonalData.php:321 406 #: src/Webhook/Webhook.php:911 407 #: src/Webhook/Webhook.php:931 408 msgid "Supporter" 409 msgstr "" 410 411 #: src/Admin/LogsTab.php:475 412 msgid "Tier" 413 msgstr "" 414 415 #: src/Admin/LogsTab.php:476 416 msgid "Payment" 417 msgstr "" 418 419 #: src/Admin/LogsTab.php:478 420 msgid "Details" 421 msgstr "" 422 423 #: src/Admin/LogsTab.php:483 424 #: src/Admin/LogsTab.php:610 425 msgid "No entries match." 426 msgstr "" 427 428 #: src/Admin/LogsTab.php:483 429 #: src/Admin/LogsTab.php:610 430 msgid "No entries yet." 431 msgstr "" 432 433 #: src/Admin/LogsTab.php:504 434 msgid "Ko-fi test" 435 msgstr "" 436 437 #: src/Admin/LogsTab.php:506 438 msgid "Not stored" 439 msgstr "" 440 441 #: src/Admin/LogsTab.php:522 442 msgid "not mapped" 443 msgstr "" 444 445 #: src/Admin/LogsTab.php:530 446 msgid "Test, no account made" 447 msgstr "" 448 449 #. translators: %s is an amount with its currency, e.g. "5.00 EUR". 450 #: src/Admin/LogsTab.php:536 451 #, php-format 452 msgid "%s · subscription" 453 msgstr "" 454 455 #. translators: %s is an amount with its currency, e.g. "5.00 EUR". 456 #: src/Admin/LogsTab.php:538 457 #, php-format 458 msgid "%s · one-off" 459 msgstr "" 460 461 #. translators: %s is why the request was refused, e.g. "the token doesn't match". 462 #: src/Admin/LogsTab.php:552 463 #, php-format 464 msgid "Refused: %s" 465 msgstr "" 466 467 #: src/Admin/LogsTab.php:560 468 msgid "Payload" 469 msgstr "" 470 471 #: src/Admin/LogsTab.php:602 472 msgid "When" 473 msgstr "" 474 475 #: src/Admin/LogsTab.php:603 476 #: src/Admin/MembersTab.php:264 477 msgid "Member" 478 msgstr "" 479 480 #: src/Admin/LogsTab.php:604 481 msgid "What happened" 482 msgstr "" 483 484 #: src/Admin/LogsTab.php:605 485 #: src/Admin/MembersTab.php:265 209 486 msgid "Role" 210 487 msgstr "" 211 488 212 #: src/Admin/AdminSettings.php:536 213 msgid "Add Tier Mapping" 214 msgstr "" 215 216 #: src/Admin/AdminSettings.php:537 217 msgid "Map Ko-fi tiers to WordPress roles. Only exact tier names will be matched." 218 msgstr "" 219 220 #: src/Admin/AdminSettings.php:554 221 msgid "— No default —" 222 msgstr "" 223 224 #: src/Admin/AdminSettings.php:563 225 msgid "If no tier matches, or if you are not using tiers, this role will be assigned to the user." 226 msgstr "" 227 228 #: src/Admin/AdminSettings.php:578 229 msgid "If checked, role will be removed after the set number of days." 230 msgstr "" 231 232 #: src/Admin/AdminSettings.php:594 233 msgid "Number of days the role remains active (only used if expiry is enabled)." 234 msgstr "" 235 236 #: src/Admin/AdminSettings.php:610 237 msgid "Enable automatic deletion of old logs based on retention period." 238 msgstr "" 239 240 #: src/Admin/AdminSettings.php:622 241 msgid "Number of days to keep logs before automatic deletion (minimum 1 day)." 242 msgstr "" 243 244 #: src/Admin/AdminSettings.php:650 245 msgid "Members for Ko-fi Settings" 246 msgstr "" 247 248 #: src/Admin/AdminSettings.php:661 249 msgid "Settings" 250 msgstr "" 251 252 #: src/Admin/AdminSettings.php:664 253 #: src/Admin/AdminSettings.php:687 254 msgid "Logs" 255 msgstr "" 256 257 #: src/Admin/AdminSettings.php:732 258 msgid "Log Type:" 259 msgstr "" 260 261 #: src/Admin/AdminSettings.php:736 262 msgid "User Logs" 263 msgstr "" 264 265 #: src/Admin/AdminSettings.php:739 266 msgid "Request Logs" 267 msgstr "" 268 269 #: src/Admin/AdminSettings.php:759 270 msgid "Search" 271 msgstr "" 272 273 #: src/Admin/AdminSettings.php:760 274 msgid "Reset" 275 msgstr "" 276 277 #: src/Admin/AdminSettings.php:763 278 msgid "Refresh" 279 msgstr "" 280 281 #: src/Admin/AdminSettings.php:794 282 #: src/Admin/AdminSettings.php:830 283 #: src/Admin/AdminSettings.php:904 284 #: src/Admin/AdminSettings.php:933 285 #: src/Admin/AdminSettings.php:964 286 #: src/Admin/AdminSettings.php:987 489 #: src/Admin/LogsTab.php:651 490 msgid "First page" 491 msgstr "" 492 493 #: src/Admin/LogsTab.php:652 494 #: src/Admin/MembersTab.php:351 495 msgid "Previous page" 496 msgstr "" 497 498 #: src/Admin/LogsTab.php:653 499 #: src/Admin/MembersTab.php:362 500 msgid "Next page" 501 msgstr "" 502 503 #: src/Admin/LogsTab.php:654 504 msgid "Last page" 505 msgstr "" 506 507 #: src/Admin/LogsTab.php:665 508 msgid "Current page" 509 msgstr "" 510 511 #: src/Admin/LogsTab.php:676 512 #: src/Admin/MembersTab.php:349 513 msgid "Pages" 514 msgstr "" 515 516 #. translators: 1: an input holding the current page number, 2: the number of pages. 517 #. translators: 1: the current page number, 2: the number of pages. 518 #: src/Admin/LogsTab.php:683 519 #: src/Admin/MembersTab.php:356 520 #, php-format 521 msgid "Page %1$s of %2$s" 522 msgstr "" 523 524 #. translators: %s is the number of entries. 525 #: src/Admin/LogsTab.php:715 526 #, php-format 527 msgid "Removes %s request entry. Member activity is kept." 528 msgid_plural "Removes all %s request entries. Member activity is kept." 529 msgstr[0] "" 530 msgstr[1] "" 531 532 #. translators: %s is the number of entries. 533 #: src/Admin/LogsTab.php:717 534 #, php-format 535 msgid "Removes %s activity entry. The request log is kept." 536 msgid_plural "Removes all %s activity entries. The request log is kept." 537 msgstr[0] "" 538 msgstr[1] "" 539 540 #. translators: %s is the number of entries. 541 #: src/Admin/LogsTab.php:720 542 #, php-format 543 msgid "Delete %s entry in this log? This can't be undone." 544 msgid_plural "Delete all %s entries in this log? This can't be undone." 545 msgstr[0] "" 546 msgstr[1] "" 547 548 #: src/Admin/LogsTab.php:726 549 msgid "Delete this log" 550 msgstr "" 551 552 #. translators: %d is a number of days. 553 #: src/Admin/LogsTab.php:734 554 #, php-format 555 msgid "Entries older than %d day are deleted automatically." 556 msgid_plural "Entries older than %d days are deleted automatically." 557 msgstr[0] "" 558 msgstr[1] "" 559 560 #: src/Admin/LogsTab.php:747 561 msgid "Delete request log…" 562 msgstr "" 563 564 #: src/Admin/LogsTab.php:747 565 msgid "Delete activity log…" 566 msgstr "" 567 568 #: src/Admin/LogsTab.php:761 287 569 msgid "You do not have permission to access this resource." 288 570 msgstr "" 289 571 290 #: src/Admin/AdminSettings.php:803 291 msgid "Invalid page number." 292 msgstr "" 293 294 #: src/Admin/AdminSettings.php:863 295 msgid "Logs table does not exist." 296 msgstr "" 297 298 #: src/Admin/AdminSettings.php:1097 299 #: src/Admin/AdminSettings.php:1239 300 msgid "Timestamp" 301 msgstr "" 302 303 #: src/Admin/AdminSettings.php:1098 304 msgid "User ID" 305 msgstr "" 306 307 #: src/Admin/AdminSettings.php:1099 308 #: src/Admin/AdminSettings.php:1240 309 msgid "Email" 310 msgstr "" 311 312 #: src/Admin/AdminSettings.php:1100 313 msgid "Action" 314 msgstr "" 315 316 #: src/Admin/AdminSettings.php:1107 317 #: src/Admin/AdminSettings.php:1252 318 msgid "No logs available." 319 msgstr "" 320 321 #: src/Admin/AdminSettings.php:1129 322 #: src/Admin/AdminSettings.php:1277 323 msgid "Clear Logs" 324 msgstr "" 325 326 #: src/Admin/AdminSettings.php:1137 327 #: src/Admin/AdminSettings.php:1285 328 msgid "Rows per page:" 329 msgstr "" 330 331 #: src/Admin/AdminSettings.php:1152 332 #: src/Admin/AdminSettings.php:1300 333 msgid "Previous" 334 msgstr "" 335 336 #: src/Admin/AdminSettings.php:1159 337 #: src/Admin/AdminSettings.php:1307 338 msgid "Next" 339 msgstr "" 340 341 #: src/Admin/AdminSettings.php:1241 342 msgid "Tier Name" 343 msgstr "" 344 345 #: src/Admin/AdminSettings.php:1242 346 msgid "Amount" 347 msgstr "" 348 349 #: src/Admin/AdminSettings.php:1243 350 msgid "Subscription" 351 msgstr "" 352 353 #: src/Admin/AdminSettings.php:1244 572 #: src/Admin/MembersTab.php:195 573 msgid "Unknown" 574 msgstr "" 575 576 #: src/Admin/MembersTab.php:214 577 msgid "All" 578 msgstr "" 579 580 #: src/Admin/MembersTab.php:215 581 #: src/Admin/MembersTab.php:316 582 msgid "Active" 583 msgstr "" 584 585 #. translators: The members whose access ends within the next seven days. 586 #: src/Admin/MembersTab.php:217 587 msgid "Ending within 7 days" 588 msgstr "" 589 590 #: src/Admin/MembersTab.php:218 591 #: src/Admin/MembersTab.php:305 592 msgid "Ended" 593 msgstr "" 594 595 #: src/Admin/MembersTab.php:254 596 #: src/Admin/MembersTab.php:256 597 msgid "Search members" 598 msgstr "" 599 600 #: src/Admin/MembersTab.php:255 601 msgid "Name or email" 602 msgstr "" 603 604 #: src/Admin/MembersTab.php:266 354 605 msgid "Status" 355 606 msgstr "" 356 607 357 #: src/Admin/AdminSettings.php:1245 358 msgid "Success" 359 msgstr "" 360 361 #: src/Admin/AdminSettings.php:1246 362 msgid "Error" 363 msgstr "" 364 365 #: src/Admin/AdminSettings.php:1261 366 msgid "Yes" 367 msgstr "" 368 369 #: src/Admin/AdminSettings.php:1261 370 msgid "No" 608 #: src/Admin/MembersTab.php:267 609 msgid "Member since" 610 msgstr "" 611 612 #: src/Admin/MembersTab.php:268 613 msgid "Last payment" 614 msgstr "" 615 616 #: src/Admin/MembersTab.php:269 617 msgid "Access until" 618 msgstr "" 619 620 #: src/Admin/MembersTab.php:274 621 msgid "Nobody has become a member through Ko-fi yet." 622 msgstr "" 623 624 #: src/Admin/MembersTab.php:274 625 msgid "No members match." 626 msgstr "" 627 628 #. translators: %s is the name of a role that was removed. 629 #: src/Admin/MembersTab.php:293 630 #, php-format 631 msgid "%s, removed" 632 msgstr "" 633 634 #: src/Admin/MembersTab.php:297 635 msgid "Granted before payment; never removed" 636 msgstr "" 637 638 #. translators: %d is a number of days. 639 #: src/Admin/MembersTab.php:311 640 #, php-format 641 msgid "Ends in %d day" 642 msgid_plural "Ends in %d days" 643 msgstr[0] "" 644 msgstr[1] "" 645 646 #: src/Admin/MembersTab.php:325 647 msgid "subscription" 648 msgstr "" 649 650 #: src/Admin/MembersTab.php:325 651 msgid "one-off" 652 msgstr "" 653 654 #: src/Admin/MembersTab.php:332 655 msgid "No expiry" 656 msgstr "" 657 658 #. translators: %s is a date. 659 #: src/Admin/MembersTab.php:335 660 #, php-format 661 msgid "Ended %s" 662 msgstr "" 663 664 #: src/Admin/MembersTab.php:367 665 msgid "Cancelling happens on Ko-fi, not here." 371 666 msgstr "" 372 667 … … 408 703 msgstr "" 409 704 410 #: src/Privacy/PersonalData.php:24 5705 #: src/Privacy/PersonalData.php:242 411 706 msgid "An active Ko-fi membership was kept, so the person keeps the access they pay for. It is removed when the membership ends." 412 707 msgstr "" 413 708 414 #: src/Privacy/PersonalData.php:324 415 #: src/Webhook/Webhook.php:911 416 #: src/Webhook/Webhook.php:931 417 msgid "Supporter" 418 msgstr "" 419 420 #: src/Privacy/PersonalData.php:365 709 #: src/Privacy/PersonalData.php:362 421 710 msgid "When you support us on Ko-fi, Ko-fi sends this site your email address, the name and message you entered, the amount, and your membership tier. We use your email address to create or find your account here and give it the access your support pays for." 422 711 msgstr "" 423 712 424 #: src/Privacy/PersonalData.php:36 6713 #: src/Privacy/PersonalData.php:363 425 714 msgid "A record of each payment and of the access granted or removed is kept for the number of days set by the site owner, then deleted automatically when log cleanup is enabled. You can ask for a copy of this data, or for it to be erased, through the site's personal data request process." 426 715 msgstr "" -
members-for-kofi/tags/1.7.0/members-for-kofi.php
r3796045 r3796384 25 25 * Plugin URI: https://github.com/trudslev/members-for-kofi 26 26 * Description: Integrate with Ko-fi to manage WordPress users or roles via webhook. 27 * Version: 1. 6.127 * Version: 1.7.0 28 28 * Author: Sune Adelowo Trudslev 29 29 * Author URI: https://foodgeek.io -
members-for-kofi/tags/1.7.0/readme.txt
r3796045 r3796384 6 6 Tested up to: 7.1 7 7 Requires PHP: 7.4 8 Stable tag: 1. 6.18 Stable tag: 1.7.0 9 9 License: GPLv3 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 140 140 141 141 == Changelog == 142 143 = 1.7.0 = 144 * Redesign: The settings page is reorganised around how you set it up. "Ko-fi connection" walks through the three steps in Ko-fi's own order (copy the webhook URL, paste the token, press Send test) and shows whether Ko-fi's requests are arriving and being accepted. 145 * Feature: Tiers Ko-fi has sent that aren't mapped to a role are listed, and can be added with one click. 146 * Feature: A new Members tab lists everyone who has or had a membership through Ko-fi, with their role, status, member-since date, last payment and when access ends. It can show only active members, those ending within a week, or those that ended. 147 * Logs: The request log can show only refused requests, says in words why a request was refused, shows times in the site's time zone, links to the member's profile, and shows each request's payload. Paging no longer prints a button for every page. 148 * Logs: Deleting a log now says which log it empties and leaves the other one alone. 149 * Accessibility: Every checkbox and button has a label, and the copy button confirms in place instead of with a pop-up. 150 * Fix: The descriptions of "Subscriptions only" and tier matching were inaccurate; they now say what the settings do. 151 * Security: The log viewer no longer runs on AJAX endpoints, so the many low-privilege accounts this plugin creates have nothing of it to call. Viewing and deleting logs still require an administrator. 152 * Translation: Complete Danish translation of the new screens. 142 153 143 154 = 1.6.1 = … … 223 234 == Upgrade Notice == 224 235 236 = 1.7.0 = 237 A redesigned admin page: a connection status, a Members tab, and a log viewer that can show only refused requests. No settings change. 238 225 239 = 1.6.1 = 226 240 "Automatically Clear Logs" can now be switched off. If you unticked it before, untick it again and save after updating. -
members-for-kofi/tags/1.7.0/src/Admin/AdminSettings.php
r3796045 r3796384 26 26 use MembersForKofi\Webhook\VerificationToken; 27 27 28 use function add_settings_section;29 use function add_settings_field;30 28 use function register_setting; 31 29 use function add_settings_error; … … 33 31 34 32 /** 35 * Handles the admin settings for the Members for Ko-fi plugin.33 * The plugin's admin page: Settings, Members and Logs tabs. 36 34 * 37 * This class is responsible for registering and rendering the settings 38 * for the plugin in the WordPress admin dashboard. 35 * Each tab is its own URL (`?page=members-for-kofi&tab=…`), so a tab or a 36 * filtered log can be linked and reloaded. The Members and Logs tabs live in 37 * MembersTab and LogsTab; this class renders the page, the Settings tab and 38 * the admin notices, and sanitizes the settings. 39 39 * 40 40 * @package MembersForKofi … … 43 43 44 44 /** 45 * Page sizes the log viewer offers. 46 * 47 * @var array<int> 48 */ 49 public const ROWS_PER_PAGE_OPTIONS = array( 10, 25, 50, 100 ); 50 51 /** 52 * Page size used when none is given, or when an invalid one is. 53 * 54 * @var int 55 */ 56 public const DEFAULT_ROWS_PER_PAGE = 10; 57 /** 58 * Resolve the current logs table name, handling legacy name mismatch. 59 * 60 * @return string Fully qualified table name with prefix. 61 */ 62 private function get_logs_table_name(): string { 63 global $wpdb; 64 return $wpdb->prefix . 'members_for_kofi_user_logs'; 65 } 66 67 /** 68 * Constructor for the AdminSettings class. 69 * 70 * Initializes the class by adding the necessary WordPress actions. 45 * The page's tabs, in order. 46 * 47 * @var array<int,string> 48 */ 49 public const TABS = array( 'settings', 'members', 'logs' ); 50 51 /** 52 * Registers the page's handlers and assets. 71 53 */ 72 54 public function __construct() { 73 // Register settings.74 add_action( 'admin_init', array( $this, 'register_settings' ) );75 76 // Enqueue admin scripts.77 55 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); 78 79 // Register AJAX handlers. 80 add_action( 'wp_ajax_members_for_kofi_pagination', array( $this, 'handle_pagination' ) ); 81 add_action( 'wp_ajax_members_for_kofi_clear_logs', array( $this, 'handle_clear_logs' ) ); 82 add_action( 'wp_ajax_members_for_kofi_update_rows_per_page', array( $this, 'handle_update_rows_per_page' ) ); 83 add_action( 'wp_ajax_members_for_kofi_filter_logs', array( $this, 'handle_filter_logs' ) ); 84 add_action( 'wp_ajax_members_for_kofi_refresh_logs', array( $this, 'handle_refresh_logs' ) ); 85 add_action( 'wp_ajax_members_for_kofi_switch_log_type', array( $this, 'handle_switch_log_type' ) ); 86 } 87 88 89 /** 90 * Registers the settings for the Members for Ko-fi plugin. 91 * 92 * This function sets up the settings sections and fields for the plugin 93 * in the WordPress admin dashboard. 56 LogsTab::register(); 57 } 58 59 /** 60 * Registers the plugin's setting and its sanitizer. 61 * 62 * The form is rendered by render_settings_tab(), not by Settings API 63 * sections, so only the setting itself is registered here. 94 64 * 95 65 * @return void … … 103 73 ) 104 74 ); 105 106 // Ko-fi Settings section. 107 add_settings_section( 'members_for_kofi_general', __( 'Ko-fi Settings', 'members-for-kofi' ), '__return_null', 'members-for-kofi' ); 108 add_settings_field( 'verification_token', __( 'Verification Token', 'members-for-kofi' ), array( $this, 'render_verification_token_field' ), 'members-for-kofi', 'members_for_kofi_general' ); 109 110 // Role Assignment section. 111 add_settings_section( 'members_for_kofi_role_assignment', __( 'Role Assignment', 'members-for-kofi' ), '__return_null', 'members-for-kofi' ); 112 add_settings_field( 'only_subscriptions', __( 'Only Accept Subscriptions', 'members-for-kofi' ), array( $this, 'render_only_subscriptions_field' ), 'members-for-kofi', 'members_for_kofi_role_assignment' ); 113 add_settings_field( 'tier_role_map', __( 'Tier to Role Mapping', 'members-for-kofi' ), array( $this, 'render_tier_role_map_field' ), 'members-for-kofi', 'members_for_kofi_role_assignment' ); 114 add_settings_field( 'default_role', __( 'Default Role (if no match)', 'members-for-kofi' ), array( $this, 'render_default_role_field' ), 'members-for-kofi', 'members_for_kofi_role_assignment' ); 115 add_settings_field( 'enable_expiry', __( 'Enable Expiry', 'members-for-kofi' ), array( $this, 'render_expiry_toggle_field' ), 'members-for-kofi', 'members_for_kofi_role_assignment' ); 116 add_settings_field( 'role_expiry_days', __( 'Role Expiry (days)', 'members-for-kofi' ), array( $this, 'render_role_expiry_field' ), 'members-for-kofi', 'members_for_kofi_role_assignment' ); 117 118 // Logging section. 119 add_settings_section( 'members_for_kofi_logging', __( 'Logging', 'members-for-kofi' ), '__return_null', 'members-for-kofi' ); 120 add_settings_field( 'auto_clear_logs', __( 'Automatically Clear Logs', 'members-for-kofi' ), array( $this, 'render_auto_clear_logs_field' ), 'members-for-kofi', 'members_for_kofi_logging' ); 121 add_settings_field( 'log_retention_days', __( 'Number of Days to Keep Logs', 'members-for-kofi' ), array( $this, 'render_log_retention_days_field' ), 'members-for-kofi', 'members_for_kofi_logging' ); 122 } /** 123 * Enqueues the admin JavaScript file for the Members for Ko-fi settings page. 124 * 125 * @return void 126 */ 75 } 76 77 /** 78 * Enqueues the page's script and stylesheet, on this page only. 79 * 80 * @return void 81 */ 127 82 public function enqueue_admin_scripts(): void { 128 83 $screen = get_current_screen(); 129 84 130 if ( isset( $screen->id ) && 'toplevel_page_members-for-kofi' === $screen->id ) { 131 wp_enqueue_script( 132 'members-for-kofi-admin-settings', 133 plugins_url( 'assets/js/admin-settings.js', dirname( __DIR__ ) ), 134 array(), 135 '1.0.6', 136 true 137 ); 138 139 wp_localize_script( 140 'members-for-kofi-admin-settings', 141 'kofiMembers', 142 array( 143 'ajaxurl' => admin_url( 'admin-ajax.php' ), 144 'paginationNonce' => wp_create_nonce( 'members_for_kofi_pagination' ), 145 'clearLogsNonce' => wp_create_nonce( 'members_for_kofi_clear_logs' ), 146 'rowsPerPageNonce' => wp_create_nonce( 'members_for_kofi_update_rows_per_page' ), 147 'filterNonce' => wp_create_nonce( 'members_for_kofi_filter_logs' ), 148 'refreshNonce' => wp_create_nonce( 'members_for_kofi_refresh_logs' ), 149 'switchLogTypeNonce' => wp_create_nonce( 'members_for_kofi_switch_log_type' ), 150 'clearLogsConfirm' => __( 'Are you sure you want to clear all logs? This action cannot be undone.', 'members-for-kofi' ), 151 'errorMessage' => __( 'An error occurred. Please try again.', 'members-for-kofi' ), 152 'searchLabelUser' => __( 'Search (email / action / role)', 'members-for-kofi' ), 153 'searchLabelRequest' => __( 'Search (email / tier / status)', 'members-for-kofi' ), 85 if ( ! isset( $screen->id ) || 'toplevel_page_members-for-kofi' !== $screen->id ) { 86 return; 87 } 88 89 $version = get_file_data( MEMBERS_FOR_KOFI_PLUGIN_FILE, array( 'Version' => 'Version' ) )['Version']; 90 91 wp_enqueue_style( 92 'members-for-kofi-admin', 93 plugins_url( 'assets/css/admin.css', dirname( __DIR__ ) ), 94 array(), 95 $version 96 ); 97 98 wp_enqueue_script( 99 'members-for-kofi-admin-settings', 100 plugins_url( 'assets/js/admin-settings.js', dirname( __DIR__ ) ), 101 array(), 102 $version, 103 true 104 ); 105 106 wp_localize_script( 107 'members-for-kofi-admin-settings', 108 'kofiMembers', 109 array( 110 'copied' => __( 'Copied', 'members-for-kofi' ), 111 ) 112 ); 113 } 114 115 /** 116 * The tab the URL asks for. 117 * 118 * @return string 119 */ 120 public static function current_tab(): string { 121 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Choosing which tab to display. 122 $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'settings'; 123 124 // The log viewer used to link to its own tab name. 125 if ( 'user_logs' === $tab ) { 126 return 'logs'; 127 } 128 129 return in_array( $tab, self::TABS, true ) ? $tab : 'settings'; 130 } 131 132 /** 133 * URL of one of the page's tabs. 134 * 135 * @param string $tab Tab key. 136 * @return string 137 */ 138 public static function tab_url( string $tab ): string { 139 $args = array( 'page' => 'members-for-kofi' ); 140 141 if ( 'settings' !== $tab ) { 142 $args['tab'] = $tab; 143 } 144 145 return add_query_arg( $args, admin_url( 'admin.php' ) ); 146 } 147 148 /** 149 * Renders the admin page. 150 * 151 * @return void 152 */ 153 public function render_settings_page(): void { 154 $active = self::current_tab(); 155 $labels = array( 156 'settings' => __( 'Settings', 'members-for-kofi' ), 157 'members' => __( 'Members', 'members-for-kofi' ), 158 'logs' => __( 'Logs', 'members-for-kofi' ), 159 ); 160 ?> 161 <div class="wrap mfk-wrap"> 162 <h1><?php esc_html_e( 'Members for Ko-fi', 'members-for-kofi' ); ?></h1> 163 <?php 164 // WordPress prints Settings API messages by itself only on pages 165 // under Settings. This is a top-level page, so without this call a 166 // rejected save ("Verification Token is required", a disallowed 167 // role) was silently dropped, and a good one got no confirmation. 168 settings_errors(); 169 $this->render_cron_warning(); 170 ?> 171 <nav class="nav-tab-wrapper" aria-label="<?php esc_attr_e( 'Members for Ko-fi sections', 'members-for-kofi' ); ?>"> 172 <?php foreach ( $labels as $tab => $label ) : ?> 173 <a href="<?php echo esc_url( self::tab_url( $tab ) ); ?>" class="nav-tab<?php echo $tab === $active ? ' nav-tab-active' : ''; ?>"<?php echo $tab === $active ? ' aria-current="page"' : ''; ?>><?php echo esc_html( $label ); ?></a> 174 <?php endforeach; ?> 175 </nav> 176 <?php 177 if ( 'members' === $active ) { 178 MembersTab::render(); 179 } elseif ( 'logs' === $active ) { 180 LogsTab::render(); 181 } else { 182 $this->render_settings_tab(); 183 } 184 ?> 185 </div> 186 <?php 187 } 188 189 /** 190 * Renders the Settings tab. 191 * 192 * @return void 193 */ 194 public function render_settings_tab(): void { 195 ?> 196 <form method="post" action="options.php" id="members-for-kofi-settings-form" class="mfk-settings"> 197 <?php settings_fields( 'members_for_kofi_options' ); ?> 198 199 <section class="mfk-card" aria-labelledby="mfk-connection"> 200 <div class="mfk-card-head"> 201 <h2 id="mfk-connection"><?php esc_html_e( 'Ko-fi connection', 'members-for-kofi' ); ?></h2> 202 <?php $this->render_connection_status(); ?> 203 </div> 204 <ol class="mfk-steps"> 205 <li><?php $this->render_webhook_url_field(); ?></li> 206 <li><?php $this->render_verification_token_field(); ?></li> 207 <li> 208 <p class="mfk-step-title"><?php esc_html_e( 'Press "Send test" on Ko-fi', 'members-for-kofi' ); ?></p> 209 <p class="description"> 210 <?php 211 printf( 212 // translators: %s is a link to the request log. 213 esc_html__( 'It shows up in the status line above and in %s. A test never creates an account.', 'members-for-kofi' ), 214 '<a href="' . esc_url( add_query_arg( 'log', 'requests', self::tab_url( 'logs' ) ) ) . '">' . esc_html__( 'the request log', 'members-for-kofi' ) . '</a>' 215 ); 216 ?> 217 </p> 218 </li> 219 </ol> 220 </section> 221 222 <section class="mfk-card mfk-card-body" aria-labelledby="mfk-roles"> 223 <h2 id="mfk-roles"><?php esc_html_e( 'Who gets a role', 'members-for-kofi' ); ?></h2> 224 <?php 225 $this->render_only_subscriptions_field(); 226 $this->render_tier_role_map_field(); 227 $this->render_default_role_field(); 228 ?> 229 </section> 230 231 <section class="mfk-card mfk-card-body" aria-labelledby="mfk-expiry"> 232 <h2 id="mfk-expiry"><?php esc_html_e( 'When membership ends', 'members-for-kofi' ); ?></h2> 233 <p class="mfk-sentence"> 234 <?php 235 $this->render_expiry_toggle_field(); 236 $this->render_role_expiry_field(); 237 ?> 238 </p> 239 <p class="description"><?php esc_html_e( 'Ko-fi subscriptions renew monthly, so 35 days leaves a few days\' grace. A role the user held before paying is never removed.', 'members-for-kofi' ); ?></p> 240 </section> 241 242 <section class="mfk-card mfk-card-body" aria-labelledby="mfk-log-history"> 243 <h2 id="mfk-log-history"><?php esc_html_e( 'Log history', 'members-for-kofi' ); ?></h2> 244 <p class="mfk-sentence"> 245 <?php 246 $this->render_auto_clear_logs_field(); 247 $this->render_log_retention_days_field(); 248 ?> 249 </p> 250 <p class="description"><?php esc_html_e( 'Untick to keep everything, for example when another tool reads the request log.', 'members-for-kofi' ); ?></p> 251 </section> 252 253 <?php submit_button(); ?> 254 </form> 255 <?php 256 } 257 258 /** 259 * Says whether Ko-fi's requests are arriving and being accepted. 260 * 261 * Based on the newest row of the request log, which also holds refused 262 * requests from anyone who finds the URL: a refusal says "the last 263 * request", not "Ko-fi". 264 * 265 * @return void 266 */ 267 public function render_connection_status(): void { 268 $options = get_option( VerificationToken::OPTION ); 269 270 if ( '' === VerificationToken::stored_hash( $options ) ) { 271 $class = 'mfk-status-error'; 272 $text = __( 'Not connected: no token is saved, so Ko-fi\'s requests are refused.', 'members-for-kofi' ); 273 } else { 274 $latest = LogsTab::latest_request(); 275 if ( null === $latest ) { 276 $class = 'mfk-status-waiting'; 277 $text = __( 'Waiting for Ko-fi: no request has arrived yet.', 'members-for-kofi' ); 278 } else { 279 $ago = human_time_diff( LogsTab::to_unix( (string) $latest['timestamp'] ) ); 280 if ( '1' === (string) $latest['success'] ) { 281 $class = 'mfk-status-ok'; 282 // translators: %s is a time span, e.g. "2 hours". 283 $text = sprintf( __( 'Working: the last request from Ko-fi, %s ago, was accepted.', 'members-for-kofi' ), $ago ); 284 } else { 285 $class = 'mfk-status-error'; 286 // translators: 1: a time span, e.g. "2 hours", 2: why it was refused, e.g. "the token doesn't match". 287 $text = sprintf( __( 'The last request, %1$s ago, was refused: %2$s', 'members-for-kofi' ), $ago, LogsTab::refusal_reason( (string) $latest['error'] ) ); 288 } 289 } 290 } 291 292 echo '<p class="mfk-status ' . esc_attr( $class ) . '">' . esc_html( $text ) . '</p>'; 293 } 294 295 /** 296 * Step 1: the webhook URL to give Ko-fi, with a copy button. 297 * 298 * @return void 299 */ 300 public function render_webhook_url_field(): void { 301 $link = '<a href="' . esc_url( 'https://ko-fi.com/manage/webhooks?src=sidemenu' ) . '" target="_blank" rel="noopener noreferrer">' 302 . esc_html__( 'Ko-fi\'s webhook settings', 'members-for-kofi' ) 303 . '<span class="screen-reader-text"> ' . esc_html__( '(opens in a new tab)', 'members-for-kofi' ) . '</span></a>'; 304 ?> 305 <label for="webhook-kofi-url" class="mfk-step-title"><?php esc_html_e( 'Give Ko-fi this webhook URL', 'members-for-kofi' ); ?></label> 306 <div class="mfk-copy-row"> 307 <input type="text" id="webhook-kofi-url" value="<?php echo esc_url( self::webhook_url() ); ?>" readonly class="regular-text code"> 308 <button type="button" id="copy-webhook-url" class="button"><?php esc_html_e( 'Copy URL', 'members-for-kofi' ); ?></button> 309 <span id="mfk-copy-status" class="mfk-copy-status" role="status"></span> 310 </div> 311 <p class="description"> 312 <?php 313 // translators: %s is a link to Ko-fi's webhook settings. 314 printf( esc_html__( 'Paste it into %s.', 'members-for-kofi' ), $link ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built from escaped parts above. 315 ?> 316 </p> 317 <?php 318 } 319 320 /** 321 * Step 2: the verification token. Write-only: the saved one is never shown. 322 * 323 * @return void 324 */ 325 public function render_verification_token_field(): void { 326 $options = get_option( 'members_for_kofi_options' ); 327 $fingerprint = VerificationToken::fingerprint( $options ); 328 ?> 329 <label for="mfk-token" class="mfk-step-title"><?php esc_html_e( 'Paste Ko-fi\'s verification token', 'members-for-kofi' ); ?></label> 330 <?php if ( '' !== $fingerprint ) : ?> 331 <p class="mfk-token-state"> 332 <?php esc_html_e( 'A token is saved', 'members-for-kofi' ); ?> 333 <code><?php echo esc_html( $fingerprint ); ?></code> 334 <span class="mfk-muted"><?php esc_html_e( 'fingerprint, to compare with another site', 'members-for-kofi' ); ?></span> 335 </p> 336 <?php else : ?> 337 <p class="mfk-token-state mfk-status-error"><?php esc_html_e( 'No token is saved yet.', 'members-for-kofi' ); ?></p> 338 <?php endif; ?> 339 <?php if ( VerificationToken::has_conflict( $options ) ) : ?> 340 <p class="mfk-status-error"><?php echo esc_html( self::token_conflict_message() ); ?></p> 341 <?php endif; ?> 342 <?php // Write-only: the saved token is never rendered back into the page, and only its hash is stored, so the value is always empty. ?> 343 <input type="password" id="mfk-token" name="members_for_kofi_options[verification_token]" value="" class="regular-text" autocomplete="new-password"<?php echo '' !== $fingerprint ? ' placeholder="' . esc_attr__( 'Leave blank to keep the saved token', 'members-for-kofi' ) . '"' : ''; ?>> 344 <p class="description"><?php esc_html_e( 'On Ko-fi\'s webhook page, open Advanced and copy the token. Only a hash is stored, so it can\'t be shown again.', 'members-for-kofi' ); ?></p> 345 <?php 346 } 347 348 /** 349 * Which payments grant a role. Radios, so both answers are always sent. 350 * 351 * @return void 352 */ 353 public static function render_only_subscriptions_field(): void { 354 $options = get_option( 'members_for_kofi_options', array() ); 355 $only = ! empty( $options['only_subscriptions'] ); 356 ?> 357 <fieldset class="mfk-field"> 358 <legend class="mfk-field-title"><?php esc_html_e( 'Payments that grant a role', 'members-for-kofi' ); ?></legend> 359 <label><input type="radio" name="members_for_kofi_options[only_subscriptions]" value="0"<?php checked( ! $only ); ?>> <?php esc_html_e( 'Donations and subscriptions', 'members-for-kofi' ); ?></label><br> 360 <label><input type="radio" name="members_for_kofi_options[only_subscriptions]" value="1"<?php checked( $only ); ?>> <?php esc_html_e( 'Subscriptions only', 'members-for-kofi' ); ?> <span class="mfk-muted"><?php esc_html_e( '(one-off donations are logged and ignored)', 'members-for-kofi' ); ?></span></label> 361 </fieldset> 362 <?php 363 } 364 365 /** 366 * Options for a role select: assignable roles only. 367 * 368 * @param string $selected Selected role slug. 369 * @return string Escaped `<option>` elements. 370 */ 371 private function role_options( string $selected ): string { 372 $html = ''; 373 374 foreach ( get_editable_roles() as $key => $details ) { 375 if ( ! Webhook::is_assignable_role( $key ) ) { 376 continue; 377 } 378 $html .= '<option value="' . esc_attr( $key ) . '"' . selected( $selected, $key, false ) . '>' . esc_html( translate_user_role( $details['name'] ) ) . '</option>'; 379 } 380 381 return $html; 382 } 383 384 /** 385 * One row of the tier table. 386 * 387 * @param string $tier Tier name. 388 * @param string $role Role slug. 389 * @return string 390 */ 391 private function tier_row( string $tier, string $role ): string { 392 return '<tr>' 393 . '<td><input type="text" name="members_for_kofi_options[tier_role_map][tier][]" value="' . esc_attr( $tier ) . '" class="regular-text tier-name-input" aria-label="' . esc_attr__( 'Tier name on Ko-fi', 'members-for-kofi' ) . '"></td>' 394 . '<td><select name="members_for_kofi_options[tier_role_map][role][]" class="tier-role-select" aria-label="' . esc_attr__( 'Role for this tier', 'members-for-kofi' ) . '">' . $this->role_options( $role ) . '</select></td>' 395 . '<td class="mfk-actions"><button type="button" class="button-link button-link-delete remove-tier-role-row" aria-label="' . esc_attr__( 'Remove this tier', 'members-for-kofi' ) . '">' . esc_html__( 'Remove', 'members-for-kofi' ) . '</button></td>' 396 . '</tr>'; 397 } 398 399 /** 400 * The tier table, with tiers Ko-fi sent that are not mapped yet. 401 * 402 * @return void 403 */ 404 public function render_tier_role_map_field(): void { 405 $options = get_option( 'members_for_kofi_options', array() ); 406 $map = isset( $options['tier_role_map'] ) && is_array( $options['tier_role_map'] ) ? $options['tier_role_map'] : array(); 407 408 $rows = ''; 409 foreach ( $map as $tier => $role ) { 410 $rows .= $this->tier_row( (string) $tier, (string) $role ); 411 } 412 413 $unmapped = LogsTab::unmapped_tiers( array_map( 'strval', array_keys( $map ) ) ); 414 ?> 415 <div class="mfk-field"> 416 <p class="mfk-field-title"><?php esc_html_e( 'Ko-fi tiers', 'members-for-kofi' ); ?></p> 417 <div class="mfk-table-wrap"> 418 <table id="tier-role-map-table" class="widefat mfk-table mfk-tier-table"> 419 <thead> 420 <tr> 421 <th scope="col"><?php esc_html_e( 'Tier name on Ko-fi', 'members-for-kofi' ); ?></th> 422 <th scope="col"><?php esc_html_e( 'Role it grants', 'members-for-kofi' ); ?></th> 423 <th scope="col"><span class="screen-reader-text"><?php esc_html_e( 'Actions', 'members-for-kofi' ); ?></span></th> 424 </tr> 425 </thead> 426 <tbody> 427 <?php echo $rows; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built by tier_row(), which escapes every value. ?> 428 </tbody> 429 </table> 430 </div> 431 <template id="tier-role-row-template"><?php echo $this->tier_row( '', '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in tier_row(). ?></template> 432 <p class="mfk-inline"> 433 <button type="button" class="button" id="add-tier-role-row"><?php esc_html_e( 'Add tier', 'members-for-kofi' ); ?></button> 434 <span class="mfk-muted"><?php esc_html_e( 'Names match whatever their capitalisation.', 'members-for-kofi' ); ?></span> 435 </p> 436 <?php if ( array() !== $unmapped ) : ?> 437 <div class="mfk-unmapped"> 438 <span><?php esc_html_e( 'Tiers Ko-fi sent that aren\'t listed:', 'members-for-kofi' ); ?></span> 439 <?php foreach ( $unmapped as $tier => $payments ) : ?> 440 <button type="button" class="button mfk-add-unmapped" data-tier="<?php echo esc_attr( $tier ); ?>"> 441 <?php 442 echo esc_html( 443 sprintf( 444 // translators: 1: a tier name, 2: how many payments named it. 445 _n( 'Add "%1$s" (%2$d payment)', 'Add "%1$s" (%2$d payments)', $payments, 'members-for-kofi' ), 446 $tier, 447 $payments 448 ) 449 ); 450 ?> 451 </button> 452 <?php endforeach; ?> 453 </div> 454 <?php endif; ?> 455 </div> 456 <?php 457 } 458 459 /** 460 * The role for payments no tier matches. 461 * 462 * @return void 463 */ 464 public function render_default_role_field(): void { 465 $options = get_option( 'members_for_kofi_options', array() ); 466 $selected = isset( $options['default_role'] ) && is_string( $options['default_role'] ) ? $options['default_role'] : ''; 467 ?> 468 <div class="mfk-field"> 469 <label for="mfk-default-role" class="mfk-field-title"><?php esc_html_e( 'Any other tier, or no tier', 'members-for-kofi' ); ?></label> 470 <select id="mfk-default-role" name="members_for_kofi_options[default_role]"> 471 <option value=""><?php esc_html_e( '— No role —', 'members-for-kofi' ); ?></option> 472 <?php echo $this->role_options( $selected ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in role_options(). ?> 473 </select> 474 <p class="description"><?php esc_html_e( 'With "No role", those payers still get an account, but no role. Administrator and other roles that can run the site are never offered.', 'members-for-kofi' ); ?></p> 475 </div> 476 <?php 477 } 478 479 /** 480 * "Remove the role" checkbox: the start of the expiry sentence. 481 * 482 * @return void 483 */ 484 public static function render_expiry_toggle_field(): void { 485 $options = get_option( 'members_for_kofi_options', array() ); 486 $checked = ! empty( $options['enable_expiry'] ); 487 echo '<input type="hidden" name="members_for_kofi_options[enable_expiry]" value="0">'; 488 echo '<input type="checkbox" id="members_for_kofi_enable_expiry" name="members_for_kofi_options[enable_expiry]" value="1"' . checked( $checked, true, false ) . '> '; 489 echo '<label for="members_for_kofi_enable_expiry">' . esc_html__( 'Remove the role', 'members-for-kofi' ) . '</label> '; 490 } 491 492 /** 493 * "[35] days after the member's last payment": the end of the sentence. 494 * 495 * @return void 496 */ 497 public static function render_role_expiry_field(): void { 498 $options = get_option( 'members_for_kofi_options', array() ); 499 $value = isset( $options['role_expiry_days'] ) ? intval( $options['role_expiry_days'] ) : 35; 500 echo '<label for="members_for_kofi_expiry_days" class="screen-reader-text">' . esc_html__( 'Days after the last payment', 'members-for-kofi' ) . '</label>'; 501 echo '<input type="number" id="members_for_kofi_expiry_days" name="members_for_kofi_options[role_expiry_days]" value="' . esc_attr( (string) $value ) . '" min="1" class="small-text"> '; 502 echo '<span>' . esc_html__( 'days after the member\'s last payment', 'members-for-kofi' ) . '</span>'; 503 } 504 505 /** 506 * "Delete log entries older than" checkbox. 507 * 508 * @return void 509 */ 510 public static function render_auto_clear_logs_field(): void { 511 $options = get_option( 'members_for_kofi_options', array() ); 512 $checked = isset( $options['auto_clear_logs'] ) ? (bool) $options['auto_clear_logs'] : true; 513 // A browser sends nothing for an unchecked box, and the sanitizer reads a 514 // missing value as on (the default for installs that predate the 515 // setting). Without this hidden "0", unticking the box never stuck. 516 echo '<input type="hidden" name="members_for_kofi_options[auto_clear_logs]" value="0">'; 517 echo '<input type="checkbox" id="members_for_kofi_auto_clear_logs" name="members_for_kofi_options[auto_clear_logs]" value="1"' . checked( $checked, true, false ) . '> '; 518 echo '<label for="members_for_kofi_auto_clear_logs">' . esc_html__( 'Delete log entries older than', 'members-for-kofi' ) . '</label> '; 519 } 520 521 /** 522 * "[30] days": the end of the log sentence. 523 * 524 * @return void 525 */ 526 public static function render_log_retention_days_field(): void { 527 $options = get_option( 'members_for_kofi_options', array() ); 528 $value = isset( $options['log_retention_days'] ) ? intval( $options['log_retention_days'] ) : 30; 529 echo '<label for="members_for_kofi_log_retention_days" class="screen-reader-text">' . esc_html__( 'Number of Days to Keep Logs', 'members-for-kofi' ) . '</label>'; 530 echo '<input type="number" id="members_for_kofi_log_retention_days" name="members_for_kofi_options[log_retention_days]" value="' . esc_attr( (string) $value ) . '" min="1" class="small-text"> '; 531 echo '<span>' . esc_html__( 'days', 'members-for-kofi' ) . '</span>'; 532 } 533 534 /** 535 * The webhook address to give Ko-fi. 536 * 537 * `/webhook-kofi/` needs pretty permalinks to reach WordPress at all on 538 * some servers; the query form works everywhere, so it is what a site on 539 * plain permalinks is shown. 540 * 541 * @return string 542 */ 543 public static function webhook_url(): string { 544 if ( '' === (string) get_option( 'permalink_structure' ) ) { 545 return add_query_arg( 'kofi_webhook', '1', home_url( '/' ) ); 546 } 547 548 return home_url( '/webhook-kofi/' ); 549 } 550 551 /** 552 * Warns when the plugin's scheduled tasks are not running. 553 * 554 * With WP-Cron disabled and no system cron, roles never expire -- free 555 * access for anyone who ever paid -- and logs are never pruned, with 556 * nothing to say so. An event more than a day overdue is that evidence. 557 * 558 * @return void 559 */ 560 public function render_cron_warning(): void { 561 $options = get_option( 'members_for_kofi_options', array() ); 562 $hooks = array( 'kofi_members_cleanup_logs' ); 563 564 if ( ! empty( $options['enable_expiry'] ?? true ) ) { 565 $hooks[] = 'kofi_members_check_expired_roles'; 566 } 567 568 foreach ( $hooks as $hook ) { 569 $next = wp_next_scheduled( $hook ); 570 571 if ( false !== $next && $next < time() - DAY_IN_SECONDS ) { 572 echo '<div class="notice notice-warning"><p>' . esc_html( 573 sprintf( 574 // translators: %s is a human-readable time span, e.g. "3 days". 575 __( 'Scheduled tasks are not running: role expiry and log cleanup are %s overdue. Make sure WP-Cron runs, or that a system cron calls wp-cron.php.', 'members-for-kofi' ), 576 human_time_diff( $next ) 577 ) 578 ) . '</p></div>'; 579 return; 580 } 581 } 582 } 583 584 /** 585 * Points administrators at an account left behind by Ko-fi's test button. 586 * 587 * Before 1.3.0 a "Send test" webhook was processed like a payment, so a 588 * site that ever pressed it has a "Jo Example" account, usually holding the 589 * membership role. Shown where an administrator would see and act on it. 590 * 591 * @param string|null $screen_id Screen to check; defaults to the current one. 592 * @return void 593 */ 594 public static function render_test_account_notice( $screen_id = null ): void { 595 if ( ! current_user_can( 'delete_users' ) ) { 596 return; 597 } 598 599 // do_action( 'admin_notices' ) hands every callback an empty string, so 600 // '' has to mean "the current screen" just like no argument does. 601 if ( ! is_string( $screen_id ) || '' === $screen_id ) { 602 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; 603 $screen_id = $screen ? (string) $screen->id : ''; 604 } 605 606 if ( ! in_array( $screen_id, array( 'dashboard', 'users', 'toplevel_page_members-for-kofi' ), true ) ) { 607 return; 608 } 609 610 $user = get_user_by( 'email', Webhook::KOFI_TEST_EMAIL ); 611 612 if ( ! $user ) { 613 return; 614 } 615 616 $names = wp_roles()->get_names(); 617 $roles = array(); 618 foreach ( $user->roles as $slug ) { 619 $roles[] = isset( $names[ $slug ] ) ? translate_user_role( $names[ $slug ] ) : $slug; 620 } 621 622 printf( 623 '<div class="notice notice-warning"><p>%s <a href="%s">%s</a></p></div>', 624 esc_html( 625 sprintf( 626 // translators: 1: email address, 2: comma-separated role names, or "no role". 627 __( 'Members for Ko-fi: an account was created by Ko-fi\'s "Send test" button (%1$s, role: %2$s). It is not a real supporter; earlier versions treated tests as payments. You can safely delete it.', 'members-for-kofi' ), 628 Webhook::KOFI_TEST_EMAIL, 629 array() !== $roles ? implode( ', ', $roles ) : __( 'none', 'members-for-kofi' ) 154 630 ) 155 ); 156 } 631 ), 632 esc_url( get_edit_user_link( $user->ID ) ), 633 esc_html__( 'Review the account', 'members-for-kofi' ) 634 ); 635 } 636 637 /** 638 * Explains a stored hash and plaintext token that disagree. 639 * 640 * @return string 641 */ 642 private static function token_conflict_message(): string { 643 return __( 'Members for Ko-fi: two different verification tokens are stored, and webhooks are checked against the hashed one. Re-enter your Ko-fi verification token in the settings to resolve this.', 'members-for-kofi' ); 644 } 645 646 /** 647 * Warns administrators, on every admin screen, about conflicting tokens. 648 * 649 * The site is verifying against a hash nobody can inspect, and the debug 650 * log is off on most sites, so this is the only place it will be noticed. 651 * Saving a token clears it: the settings never store the plaintext key. 652 * 653 * @return void 654 */ 655 public static function render_token_conflict_notice(): void { 656 if ( ! current_user_can( 'manage_options' ) ) { 657 return; 658 } 659 660 if ( ! VerificationToken::has_conflict( get_option( VerificationToken::OPTION ) ) ) { 661 return; 662 } 663 664 printf( 665 '<div class="notice notice-error"><p>%s <a href="%s">%s</a></p></div>', 666 esc_html( self::token_conflict_message() ), 667 esc_url( admin_url( 'admin.php?page=members-for-kofi' ) ), 668 esc_html__( 'Open settings', 'members-for-kofi' ) 669 ); 157 670 } 158 671 … … 194 707 } 195 708 196 if ( ! $enable_expiry ) {197 // Preserve existing stored value if expiry disabled, fallback to default.709 if ( ! $enable_expiry && empty( $role_expiry_days ) ) { 710 // Expiry is off and no usable number was sent: keep the stored one. 198 711 $role_expiry_days = $stored['role_expiry_days'] ?? 35; 199 712 } … … 269 782 'log_retention_days' => $log_retention_days, 270 783 ); 271 } /**272 * Renders the verification token field in the settings page.273 *274 * This function outputs the HTML for the verification token input field,275 * allowing users to enter the token required for webhook verification.276 *277 * @return void278 */279 public function render_verification_token_field(): void {280 $options = get_option( 'members_for_kofi_options' );281 $webhook_url = self::webhook_url();282 283 $fingerprint = VerificationToken::fingerprint( $options );284 285 // Write-only: the saved token is never rendered back into the page, and286 // only its hash is stored, so the value is always empty.287 echo '<input type="password" name="members_for_kofi_options[verification_token]" value="" class="regular-text" autocomplete="new-password">';288 289 if ( '' !== $fingerprint ) {290 echo '<p class="description">' . esc_html__( 'A token is saved. Leave this blank to keep it, or paste a new one to replace it.', 'members-for-kofi' ) . '</p>';291 echo '<p class="description">' . sprintf(292 // translators: %s is the first eight characters of the saved token's SHA-256 hash.293 esc_html__( 'Saved token fingerprint: %s', 'members-for-kofi' ),294 '<code>' . esc_html( $fingerprint ) . '</code>'295 ) . '</p>';296 } else {297 echo '<p class="description">' . esc_html__( 'No token set yet. Webhooks are rejected until one is saved.', 'members-for-kofi' ) . '</p>';298 }299 300 if ( VerificationToken::has_conflict( $options ) ) {301 echo '<p class="description" style="color: #b32d2e;">' . esc_html( self::token_conflict_message() ) . '</p>';302 }303 304 // Description for Verification Token.305 $description = sprintf(306 // translators: %s is a link to the Ko-fi Webhooks Management page.307 esc_html__( 'This token is used to verify incoming webhook requests from Ko-fi. Paste the verification token from this page: %s and open the Advanced box. Only a hash of it is stored, so it cannot be shown again; to change it, paste the new one.', 'members-for-kofi' ),308 '<a href="' . esc_url( 'https://ko-fi.com/manage/webhooks?src=sidemenu' ) . '" target="_blank">' . esc_html__( 'Ko-fi Webhooks Management', 'members-for-kofi' ) . '</a>'309 );310 echo '<p class="description">' . wp_kses(311 $description,312 array(313 'a' => array(314 'href' => array(),315 'target' => array(),316 ),317 )318 ) . '</p>';319 320 // Webhook URL Textbox and Copy Button.321 echo '<div style="margin-top: 20px;">';322 echo '<label for="webhook-kofi-url" style="font-weight: bold; display: block; margin-bottom: 5px;">' . esc_html__( 'Webhook URL:', 'members-for-kofi' ) . '</label>';323 echo '<div style="display: flex; align-items: center; width: 500px;">';324 echo '<input type="text" id="webhook-kofi-url" value="' . esc_url( $webhook_url ) . '" readonly style="flex: 1; margin-right: 10px;" class="regular-text">';325 echo '<button type="button" id="copy-webhook-url" class="button button-secondary">' . esc_html__( 'Copy to Clipboard', 'members-for-kofi' ) . '</button>';326 echo '</div>';327 echo '<p class="description">' . esc_html__( 'Use this URL to configure your Ko-fi webhook.', 'members-for-kofi' ) . '</p>';328 echo '</div>';329 }330 331 /**332 * The webhook address to give Ko-fi.333 *334 * `/webhook-kofi/` needs pretty permalinks to reach WordPress at all on335 * some servers; the query form works everywhere, so it is what a site on336 * plain permalinks is shown.337 *338 * @return string339 */340 public static function webhook_url(): string {341 if ( '' === (string) get_option( 'permalink_structure' ) ) {342 return add_query_arg( 'kofi_webhook', '1', home_url( '/' ) );343 }344 345 return home_url( '/webhook-kofi/' );346 }347 348 /**349 * Warns when the plugin's scheduled tasks are not running.350 *351 * With WP-Cron disabled and no system cron, roles never expire -- free352 * access for anyone who ever paid -- and logs are never pruned, with353 * nothing to say so. An event more than a day overdue is that evidence.354 *355 * @return void356 */357 public function render_cron_warning(): void {358 $options = get_option( 'members_for_kofi_options', array() );359 $hooks = array( 'kofi_members_cleanup_logs' );360 361 if ( ! empty( $options['enable_expiry'] ?? true ) ) {362 $hooks[] = 'kofi_members_check_expired_roles';363 }364 365 foreach ( $hooks as $hook ) {366 $next = wp_next_scheduled( $hook );367 368 if ( false !== $next && $next < time() - DAY_IN_SECONDS ) {369 echo '<div class="notice notice-warning"><p>' . esc_html(370 sprintf(371 // translators: %s is a human-readable time span, e.g. "3 days".372 __( 'Scheduled tasks are not running: role expiry and log cleanup are %s overdue. Make sure WP-Cron runs, or that a system cron calls wp-cron.php.', 'members-for-kofi' ),373 human_time_diff( $next )374 )375 ) . '</p></div>';376 return;377 }378 }379 }380 381 /**382 * Points administrators at an account left behind by Ko-fi's test button.383 *384 * Before 1.3.0 a "Send test" webhook was processed like a payment, so a385 * site that ever pressed it has a "Jo Example" account, usually holding the386 * membership role. Shown where an administrator would see and act on it.387 *388 * @param string|null $screen_id Screen to check; defaults to the current one.389 * @return void390 */391 public static function render_test_account_notice( $screen_id = null ): void {392 if ( ! current_user_can( 'delete_users' ) ) {393 return;394 }395 396 // do_action( 'admin_notices' ) hands every callback an empty string, so397 // '' has to mean "the current screen" just like no argument does.398 if ( ! is_string( $screen_id ) || '' === $screen_id ) {399 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;400 $screen_id = $screen ? (string) $screen->id : '';401 }402 403 if ( ! in_array( $screen_id, array( 'dashboard', 'users', 'toplevel_page_members-for-kofi' ), true ) ) {404 return;405 }406 407 $user = get_user_by( 'email', Webhook::KOFI_TEST_EMAIL );408 409 if ( ! $user ) {410 return;411 }412 413 $names = wp_roles()->get_names();414 $roles = array();415 foreach ( $user->roles as $slug ) {416 $roles[] = isset( $names[ $slug ] ) ? translate_user_role( $names[ $slug ] ) : $slug;417 }418 419 printf(420 '<div class="notice notice-warning"><p>%s <a href="%s">%s</a></p></div>',421 esc_html(422 sprintf(423 // translators: 1: email address, 2: comma-separated role names, or "no role".424 __( 'Members for Ko-fi: an account was created by Ko-fi\'s "Send test" button (%1$s, role: %2$s). It is not a real supporter; earlier versions treated tests as payments. You can safely delete it.', 'members-for-kofi' ),425 Webhook::KOFI_TEST_EMAIL,426 array() !== $roles ? implode( ', ', $roles ) : __( 'none', 'members-for-kofi' )427 )428 ),429 esc_url( get_edit_user_link( $user->ID ) ),430 esc_html__( 'Review the account', 'members-for-kofi' )431 );432 }433 434 /**435 * Explains a stored hash and plaintext token that disagree.436 *437 * @return string438 */439 private static function token_conflict_message(): string {440 return __( 'Members for Ko-fi: two different verification tokens are stored, and webhooks are checked against the hashed one. Re-enter your Ko-fi verification token in the settings to resolve this.', 'members-for-kofi' );441 }442 443 /**444 * Warns administrators, on every admin screen, about conflicting tokens.445 *446 * The site is verifying against a hash nobody can inspect, and the debug447 * log is off on most sites, so this is the only place it will be noticed.448 * Saving a token clears it: the settings never store the plaintext key.449 *450 * @return void451 */452 public static function render_token_conflict_notice(): void {453 if ( ! current_user_can( 'manage_options' ) ) {454 return;455 }456 457 if ( ! VerificationToken::has_conflict( get_option( VerificationToken::OPTION ) ) ) {458 return;459 }460 461 printf(462 '<div class="notice notice-error"><p>%s <a href="%s">%s</a></p></div>',463 esc_html( self::token_conflict_message() ),464 esc_url( admin_url( 'admin.php?page=members-for-kofi' ) ),465 esc_html__( 'Open settings', 'members-for-kofi' )466 );467 }468 469 /**470 * Renders the "Only Accept Subscriptions" field in the settings page.471 *472 * This function outputs the HTML for the checkbox that allows users473 * to enable or disable role assignment only for active subscribers.474 *475 * @return void476 */477 public static function render_only_subscriptions_field(): void {478 $options = get_option( 'members_for_kofi_options', array() );479 $checked = ! empty( $options['only_subscriptions'] );480 echo '<label><input type="checkbox" name="members_for_kofi_options[only_subscriptions]" value="1" ' . checked( $checked, true, false ) . '> ' . esc_html__( 'Only assign roles to subscribers.', 'members-for-kofi' ) . '</label>';481 echo '<p class="description">' . esc_html__( 'When enabled, users must be active subscribers to receive a role.', 'members-for-kofi' ) . '</p>';482 }483 484 /**485 * Renders the Tier to Role Mapping field in the settings page.486 *487 * This function outputs the HTML for mapping Ko-fi tiers to WordPress roles,488 * allowing users to assign roles based on Ko-fi tier names.489 *490 * @return void491 */492 public function render_tier_role_map_field(): void {493 $options = get_option( 'members_for_kofi_options', array() );494 $map = isset( $options['tier_role_map'] ) && is_array( $options['tier_role_map'] ) ? $options['tier_role_map'] : array();495 $roles = get_editable_roles();496 497 echo '<table id="tier-role-map-table" class="form-table" style="margin-top:0">';498 echo '<thead><tr><th>' . esc_html__( 'Ko-fi Tier', 'members-for-kofi' ) . '</th><th>' . esc_html__( 'Role', 'members-for-kofi' ) . '</th><th></th></tr></thead><tbody>';499 500 if ( ! empty( $map ) ) {501 foreach ( $map as $tier => $role ) {502 $tier_val = esc_attr( $tier );503 $role_val = esc_attr( $role );504 echo '<tr>';505 echo '<td><input type="text" name="members_for_kofi_options[tier_role_map][tier][]" value="' . esc_attr( $tier_val ) . '" class="regular-text"></td>';506 echo '<td><select name="members_for_kofi_options[tier_role_map][role][]" class="regular-text">';507 foreach ( $roles as $key => $details ) {508 if ( ! Webhook::is_assignable_role( $key ) ) {509 continue;510 }511 echo '<option value="' . esc_attr( $key ) . '"' . selected( $role_val, $key, false ) . '>' . esc_html( $details['name'] ) . '</option>';512 }513 echo '</select></td>';514 echo '<td><button type="button" class="button remove-tier-role-row">×</button></td>';515 echo '</tr>';516 }517 }518 519 echo '</tbody></table>';520 521 echo '<template id="tier-role-row-template">';522 echo '<tr>';523 echo '<td><input type="text" name="members_for_kofi_options[tier_role_map][tier][]" class="regular-text tier-name-input"></td>';524 echo '<td><select name="members_for_kofi_options[tier_role_map][role][]" class="regular-text tier-role-select">';525 foreach ( $roles as $key => $details ) {526 if ( ! Webhook::is_assignable_role( $key ) ) {527 continue;528 }529 echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $details['name'] ) . '</option>';530 }531 echo '</select></td>';532 echo '<td><button type="button" class="button remove-tier-role-row">×</button></td>';533 echo '</tr>';534 echo '</template>';535 536 echo '<p><button type="button" class="button" id="add-tier-role-row">' . esc_html__( 'Add Tier Mapping', 'members-for-kofi' ) . '</button></p>';537 echo '<p><em>' . esc_html__( 'Map Ko-fi tiers to WordPress roles. Only exact tier names will be matched.', 'members-for-kofi' ) . '</em></p>';538 }539 540 /**541 * Renders the default role field in the settings page.542 *543 * This function outputs the HTML for selecting a default WordPress role544 * to assign to users when no tier matches or tiers are not used.545 *546 * @return void547 */548 public function render_default_role_field(): void {549 $options = get_option( 'members_for_kofi_options', array() );550 $roles = get_editable_roles();551 $selected = $options['default_role'] ?? array();552 553 echo '<select name="members_for_kofi_options[default_role]">';554 echo '<option value="">' . esc_html__( '— No default —', 'members-for-kofi' ) . '</option>';555 foreach ( $roles as $role_key => $role_details ) {556 // Skip disallowed roles (e.g., administrator) for security.557 if ( ! Webhook::is_assignable_role( $role_key ) ) {558 continue;559 }560 echo '<option value="' . esc_attr( $role_key ) . '"' . selected( $role_key, $selected, false ) . '>' . esc_html( $role_details['name'] ) . '</option>';561 }562 echo '</select>';563 echo '<p class="description">' . esc_html__( 'If no tier matches, or if you are not using tiers, this role will be assigned to the user.', 'members-for-kofi' ) . '</p>';564 }565 566 /**567 * Renders the "Enable Expiry" toggle field in the settings page.568 *569 * This function outputs the HTML for the checkbox that allows users570 * to enable or disable role expiry after a set number of days.571 *572 * @return void573 */574 public static function render_expiry_toggle_field(): void {575 $options = get_option( 'members_for_kofi_options', array() );576 $checked = ! empty( $options['enable_expiry'] );577 echo '<label><input type="checkbox" name="members_for_kofi_options[enable_expiry]" value="1" ' . checked( $checked, true, false ) . '></label>';578 echo '<p class="description">' . esc_html__( 'If checked, role will be removed after the set number of days.', 'members-for-kofi' ) . '</p>';579 }580 581 /**582 * Renders the role expiry field in the settings page.583 *584 * This function outputs the HTML for the input field that allows users585 * to specify the number of days a role remains active for non-subscribers.586 *587 * @return void588 */589 public static function render_role_expiry_field(): void {590 $options = get_option( 'members_for_kofi_options', array() );591 $value = isset( $options['role_expiry_days'] ) ? intval( $options['role_expiry_days'] ) : 35;592 $expiry_enable = ! empty( $options['enable_expiry'] );593 echo '<input type="number" id="members_for_kofi_expiry_days" name="members_for_kofi_options[role_expiry_days]" value="' . esc_attr( $value ) . '" min="1" ' . disabled( ! $expiry_enable, true, false ) . ' class="small-text">';594 echo '<p class="description">' . esc_html__( 'Number of days the role remains active (only used if expiry is enabled).', 'members-for-kofi' ) . '</p>';595 }596 597 /**598 * Renders the auto clear logs checkbox field.599 *600 * @return void601 */602 public static function render_auto_clear_logs_field(): void {603 $options = get_option( 'members_for_kofi_options', array() );604 $checked = isset( $options['auto_clear_logs'] ) ? (bool) $options['auto_clear_logs'] : true;605 // A browser sends nothing for an unchecked box, and the sanitizer reads a606 // missing value as on (the default for installs that predate the607 // setting). Without this hidden "0", unticking the box never stuck.608 echo '<input type="hidden" name="members_for_kofi_options[auto_clear_logs]" value="0">';609 echo '<input type="checkbox" id="members_for_kofi_auto_clear_logs" name="members_for_kofi_options[auto_clear_logs]" value="1" ' . checked( $checked, true, false ) . '>';610 echo '<p class="description">' . esc_html__( 'Enable automatic deletion of old logs based on retention period.', 'members-for-kofi' ) . '</p>';611 }612 613 /**614 * Renders the log retention days field.615 *616 * @return void617 */618 public static function render_log_retention_days_field(): void {619 $options = get_option( 'members_for_kofi_options', array() );620 $value = isset( $options['log_retention_days'] ) ? intval( $options['log_retention_days'] ) : 30;621 echo '<input type="number" id="members_for_kofi_log_retention_days" name="members_for_kofi_options[log_retention_days]" value="' . esc_attr( $value ) . '" min="1" class="small-text">';622 echo '<p class="description">' . esc_html__( 'Number of days to keep logs before automatic deletion (minimum 1 day).', 'members-for-kofi' ) . '</p>';623 }624 625 /**626 * Renders the log level field in the settings page.627 *628 * This function outputs the HTML for selecting the minimum severity629 * of log messages to record in the plugin's log file.630 *631 * @return void632 */633 // Deprecated: log level field removed with lightweight logger; method retained intentionally removed.634 635 /**636 * Renders the settings page for the Members for Ko-fi plugin.637 * This function outputs the HTML for the settings page in the WordPress admin dashboard,638 * allowing users to configure the plugin's settings.639 *640 * @return void641 */642 public function render_settings_page(): void {643 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading tab from URL for display only644 $active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'settings';645 if ( ! in_array( $active_tab, array( 'settings', 'logs' ), true ) ) {646 $active_tab = 'settings';647 }648 ?>649 <div class="wrap">650 <h1><?php esc_html_e( 'Members for Ko-fi Settings', 'members-for-kofi' ); ?></h1>651 <?php652 // WordPress prints Settings API messages by itself only on pages653 // under Settings. This is a top-level page, so without this call a654 // rejected save ("Verification Token is required", a disallowed655 // role) was silently dropped, and a good one got no confirmation.656 settings_errors();657 $this->render_cron_warning();658 ?>659 <h2 class="nav-tab-wrapper" role="tablist">660 <a href="#" role="tab" data-tab="settings" aria-selected="<?php echo 'settings' === $active_tab ? 'true' : 'false'; ?>" class="nav-tab <?php echo 'settings' === $active_tab ? 'nav-tab-active' : ''; ?>">661 <?php esc_html_e( 'Settings', 'members-for-kofi' ); ?>662 </a>663 <a href="#" role="tab" data-tab="logs" aria-selected="<?php echo 'logs' === $active_tab ? 'true' : 'false'; ?>" class="nav-tab <?php echo 'logs' === $active_tab ? 'nav-tab-active' : ''; ?>">664 <?php esc_html_e( 'Logs', 'members-for-kofi' ); ?>665 </a>666 </h2>667 <input type="hidden" id="active_tab" value="<?php echo esc_attr( $active_tab ); ?>" />668 <form method="post" action="options.php" id="members-for-kofi-settings-form">669 <?php settings_fields( 'members_for_kofi_options' ); ?>670 <div id="members-for-kofi-tab-settings" class="members-for-kofi-tab" style="<?php echo 'settings' === $active_tab ? '' : 'display:none;'; ?>">671 <h2><?php esc_html_e( 'Ko-fi Settings', 'members-for-kofi' ); ?></h2>672 <table class="form-table">673 <?php do_settings_fields( 'members-for-kofi', 'members_for_kofi_general' ); ?>674 </table>675 <h2><?php esc_html_e( 'Role Assignment', 'members-for-kofi' ); ?></h2>676 <table class="form-table">677 <?php do_settings_fields( 'members-for-kofi', 'members_for_kofi_role_assignment' ); ?>678 </table>679 <h2><?php esc_html_e( 'Logging', 'members-for-kofi' ); ?></h2>680 <table class="form-table">681 <?php do_settings_fields( 'members-for-kofi', 'members_for_kofi_logging' ); ?>682 </table>683 <?php submit_button(); ?>684 </div>685 </form>686 <div id="members-for-kofi-tab-logs" class="members-for-kofi-tab" style="<?php echo 'logs' === $active_tab ? '' : 'display:none;'; ?>">687 <h2><?php esc_html_e( 'Logs', 'members-for-kofi' ); ?></h2>688 <?php $this->render_logs_tab(); ?>689 </div>690 </div>691 <?php692 }693 694 /**695 * Renders the "Logs" tab in the settings page.696 *697 * This function outputs the HTML for displaying logs with a dropdown to switch between user and request logs.698 *699 * @param int|null $paged The current page number, or null to auto-determine.700 * @param int $rows_per_page The number of rows to display per page.701 * @param string $log_type The type of logs to display ('user' or 'request').702 * @return void703 */704 public function render_logs_tab( ?int $paged = null, int $rows_per_page = 10, string $log_type = 'user' ): void {705 $rows_per_page = $this->sanitize_rows_per_page( $rows_per_page );706 global $wpdb;707 708 // Determine which table to query based on log type.709 if ( 'request' === $log_type ) {710 $table_name = $wpdb->prefix . 'members_for_kofi_request_logs';711 $transient_key = 'members_for_kofi_total_request_logs';712 } else {713 $table_name = $this->get_logs_table_name();714 $transient_key = 'members_for_kofi_total_logs';715 }716 717 $total_logs = get_transient( $transient_key );718 719 if ( false === $total_logs || 0 === (int) $total_logs ) {720 // Recalculate if not cached or cached as zero (could be stale from prior table mismatch).721 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared -- Counting rows in plugin-owned table.722 $total_logs = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . esc_sql( $table_name ) . '`' );723 set_transient( $transient_key, $total_logs, MINUTE_IN_SECONDS );724 }725 726 $total_pages = ceil( $total_logs / $rows_per_page );727 728 ?>729 <!-- Log Type Selector -->730 <div style="margin-bottom:15px;">731 <label for="log-type-selector" style="font-weight:bold; margin-right:10px;">732 <?php esc_html_e( 'Log Type:', 'members-for-kofi' ); ?>733 </label>734 <select id="log-type-selector" style="min-width:150px;">735 <option value="user" <?php selected( $log_type, 'user' ); ?>>736 <?php esc_html_e( 'User Logs', 'members-for-kofi' ); ?>737 </option>738 <option value="request" <?php selected( $log_type, 'request' ); ?>>739 <?php esc_html_e( 'Request Logs', 'members-for-kofi' ); ?>740 </option>741 </select>742 </div>743 744 <!-- Logs Table Container -->745 <div id="logs-controls" style="display:flex; gap:10px; align-items:flex-end; margin-bottom:10px;">746 <div>747 <label for="kofi-members-log-search" style="display:block;">748 <?php749 if ( 'request' === $log_type ) {750 esc_html_e( 'Search (email / tier / status)', 'members-for-kofi' );751 } else {752 esc_html_e( 'Search (email / action / role)', 'members-for-kofi' );753 }754 ?>755 </label>756 <input type="search" id="kofi-members-log-search" style="min-width:280px;" />757 </div>758 <div>759 <button type="button" class="button" id="kofi-members-log-search-btn"><?php esc_html_e( 'Search', 'members-for-kofi' ); ?></button>760 <button type="button" class="button" id="kofi-members-log-reset-btn"><?php esc_html_e( 'Reset', 'members-for-kofi' ); ?></button>761 </div>762 <div>763 <button type="button" class="button" id="kofi-members-log-refresh-btn"><?php esc_html_e( 'Refresh', 'members-for-kofi' ); ?></button>764 </div>765 </div>766 <div id="logs-table-container" data-current-search="" data-log-type="<?php echo esc_attr( $log_type ); ?>">767 <?php768 if ( 'request' === $log_type ) {769 $this->render_request_logs_table( $paged, $rows_per_page );770 } else {771 $this->render_user_logs_table( $paged, $rows_per_page );772 }773 ?>774 </div>775 776 <?php777 }778 779 /**780 * Handles the AJAX request for pagination in the user logs tab.781 *782 * This function verifies the user's permissions, processes the pagination request,783 * and returns the rendered logs table for the requested page.784 *785 * @return void786 */787 public function handle_pagination(): void {788 // Verify the nonce for security.789 check_ajax_referer( 'members_for_kofi_pagination', '_ajax_nonce' );790 \MembersForKofi\Logging\DebugLogger::debug( 'AJAX pagination request' );791 792 // Verify the current user has permission to view logs.793 if ( ! current_user_can( 'manage_options' ) ) {794 wp_send_json_error( __( 'You do not have permission to access this resource.', 'members-for-kofi' ) );795 }796 797 $paged = isset( $_POST['paged'] ) ? absint( $_POST['paged'] ) : 1;798 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';799 $log_type = isset( $_POST['log_type'] ) ? sanitize_text_field( wp_unslash( $_POST['log_type'] ) ) : 'user';800 801 // Ensure the paged parameter is valid.802 if ( $paged < 1 ) {803 wp_send_json_error( __( 'Invalid page number.', 'members-for-kofi' ) );804 }805 806 // Render only the logs table for the requested page.807 ob_start();808 if ( 'request' === $log_type ) {809 $this->render_request_logs_table( $paged, 10, $search );810 } else {811 $this->render_user_logs_table( $paged, 10, $search );812 }813 $content = ob_get_clean();814 815 wp_send_json_success( $content );816 }817 818 /**819 * Handles the AJAX request to clear logs.820 *821 * @return void822 */823 public function handle_clear_logs(): void {824 // Verify the nonce for security.825 check_ajax_referer( 'members_for_kofi_clear_logs', '_ajax_nonce' );826 \MembersForKofi\Logging\DebugLogger::debug( 'AJAX clear logs request' );827 828 // Verify the current user has permission to clear logs.829 if ( ! current_user_can( 'manage_options' ) ) {830 wp_send_json_error( __( 'You do not have permission to access this resource.', 'members-for-kofi' ) );831 }832 833 global $wpdb;834 $log_type = isset( $_POST['log_type'] ) ? sanitize_text_field( wp_unslash( $_POST['log_type'] ) ) : 'user';835 836 // Determine which table to clear based on log type.837 if ( 'request' === $log_type ) {838 $table_name = $wpdb->prefix . 'members_for_kofi_request_logs';839 $transient_key = 'members_for_kofi_total_request_logs';840 } else {841 $table_name = $this->get_logs_table_name();842 $transient_key = 'members_for_kofi_total_logs';843 }844 845 // Check if the logs table exists and clear it.846 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Checking for the plugin's own table; a cached answer could be wrong.847 if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) ) {848 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Clearing the plugin's own log table on admin request; the cached count is deleted below.849 $wpdb->query( 'DELETE FROM `' . esc_sql( $table_name ) . '`' );850 delete_transient( $transient_key ); // Clear the cached total logs count.851 852 // Render the updated logs table (which will now show "No logs available").853 ob_start();854 if ( 'request' === $log_type ) {855 $this->render_request_logs_table();856 } else {857 $this->render_user_logs_table();858 }859 $content = ob_get_clean();860 861 wp_send_json_success( $content );862 } else {863 wp_send_json_error( __( 'Logs table does not exist.', 'members-for-kofi' ) );864 }865 }866 867 /**868 * Handles the AJAX request to update rows per page.869 *870 * @return void871 */872 /**873 * Clamps a requested page size to one the log viewer actually offers.874 *875 * Anything the picker cannot produce is rejected rather than squeezed876 * into range: 0 reached `ceil( $total_logs / 0 )` and killed the page877 * with a DivisionByZeroError, and a huge value went straight into LIMIT878 * and loaded the entire table into memory.879 *880 * @param mixed $raw Untrusted page size, typically straight from $_POST.881 * @return int One of ROWS_PER_PAGE_OPTIONS.882 */883 public function sanitize_rows_per_page( $raw ): int {884 $value = absint( $raw );885 886 if ( ! in_array( $value, self::ROWS_PER_PAGE_OPTIONS, true ) ) {887 return self::DEFAULT_ROWS_PER_PAGE;888 }889 890 return $value;891 }892 893 /**894 * Handles the AJAX request that changes the log table's page size.895 *896 * @return void897 */898 public function handle_update_rows_per_page(): void {899 check_ajax_referer( 'members_for_kofi_update_rows_per_page', '_ajax_nonce' );900 \MembersForKofi\Logging\DebugLogger::debug( 'AJAX rows per page update request' );901 902 // Verify the current user has permission to update settings.903 if ( ! current_user_can( 'manage_options' ) ) {904 wp_send_json_error( __( 'You do not have permission to access this resource.', 'members-for-kofi' ) );905 }906 907 $rows_per_page = $this->sanitize_rows_per_page(908 isset( $_POST['rows_per_page'] ) ? absint( wp_unslash( $_POST['rows_per_page'] ) ) : self::DEFAULT_ROWS_PER_PAGE909 );910 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';911 $log_type = isset( $_POST['log_type'] ) ? sanitize_text_field( wp_unslash( $_POST['log_type'] ) ) : 'user';912 913 ob_start();914 if ( 'request' === $log_type ) {915 $this->render_request_logs_table( null, $rows_per_page, $search );916 } else {917 $this->render_user_logs_table( null, $rows_per_page, $search );918 }919 $content = ob_get_clean();920 921 wp_send_json_success( $content );922 }923 924 /**925 * Handles AJAX log filtering.926 */927 public function handle_filter_logs(): void {928 check_ajax_referer( 'members_for_kofi_filter_logs', '_ajax_nonce' );929 \MembersForKofi\Logging\DebugLogger::debug( 'AJAX filter logs request' );930 931 // Verify the current user has permission to view logs.932 if ( ! current_user_can( 'manage_options' ) ) {933 wp_send_json_error( __( 'You do not have permission to access this resource.', 'members-for-kofi' ) );934 }935 936 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';937 $paged = isset( $_POST['paged'] ) ? absint( $_POST['paged'] ) : 1;938 $log_type = isset( $_POST['log_type'] ) ? sanitize_text_field( wp_unslash( $_POST['log_type'] ) ) : 'user';939 $rows_per_page = $this->sanitize_rows_per_page(940 isset( $_POST['rows_per_page'] ) ? absint( wp_unslash( $_POST['rows_per_page'] ) ) : self::DEFAULT_ROWS_PER_PAGE941 );942 943 ob_start();944 // Every other log handler honours log_type; this one always rendered the945 // user log, so searching from the Request tab answered with user rows.946 if ( 'request' === $log_type ) {947 $this->render_request_logs_table( $paged, $rows_per_page, $search );948 } else {949 $this->render_user_logs_table( $paged, $rows_per_page, $search );950 }951 $content = ob_get_clean();952 wp_send_json_success( $content );953 }954 955 /**956 * Handles AJAX refresh (no search alteration; just reload current page).957 */958 public function handle_refresh_logs(): void {959 check_ajax_referer( 'members_for_kofi_refresh_logs', '_ajax_nonce' );960 \MembersForKofi\Logging\DebugLogger::debug( 'AJAX refresh logs request' );961 962 // Verify the current user has permission to view logs.963 if ( ! current_user_can( 'manage_options' ) ) {964 wp_send_json_error( __( 'You do not have permission to access this resource.', 'members-for-kofi' ) );965 }966 967 $paged = isset( $_POST['paged'] ) ? absint( $_POST['paged'] ) : 1;968 $rows_per_page = $this->sanitize_rows_per_page(969 isset( $_POST['rows_per_page'] ) ? absint( wp_unslash( $_POST['rows_per_page'] ) ) : self::DEFAULT_ROWS_PER_PAGE970 );971 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';972 ob_start();973 $this->render_user_logs_table( $paged, $rows_per_page, $search );974 $content = ob_get_clean();975 wp_send_json_success( $content );976 }977 978 /**979 * Handles AJAX log type switch (User vs Request logs).980 */981 public function handle_switch_log_type(): void {982 check_ajax_referer( 'members_for_kofi_switch_log_type', '_ajax_nonce' );983 \MembersForKofi\Logging\DebugLogger::debug( 'AJAX switch log type request' );984 985 // Verify the current user has permission to view logs.986 if ( ! current_user_can( 'manage_options' ) ) {987 wp_send_json_error( __( 'You do not have permission to access this resource.', 'members-for-kofi' ) );988 }989 990 $log_type = isset( $_POST['log_type'] ) ? sanitize_text_field( wp_unslash( $_POST['log_type'] ) ) : 'user';991 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';992 $paged = isset( $_POST['paged'] ) ? absint( $_POST['paged'] ) : 1;993 $rows_per_page = $this->sanitize_rows_per_page(994 isset( $_POST['rows_per_page'] ) ? absint( wp_unslash( $_POST['rows_per_page'] ) ) : self::DEFAULT_ROWS_PER_PAGE995 );996 997 ob_start();998 if ( 'request' === $log_type ) {999 $this->render_request_logs_table( $paged, $rows_per_page, $search );1000 } else {1001 $this->render_user_logs_table( $paged, $rows_per_page, $search );1002 }1003 $content = ob_get_clean();1004 wp_send_json_success( $content );1005 }1006 1007 /**1008 * Renders the user logs table.1009 *1010 * This function outputs the HTML for displaying the user logs in a table,1011 * including pagination links.1012 *1013 * @param int|null $paged The current page number, or null to auto-determine.1014 * @param int $rows_per_page The number of rows to display per page.1015 * @param string $search Search filter.1016 * @return void1017 */1018 public function render_user_logs_table( ?int $paged = null, int $rows_per_page = 10, string $search = '' ): void {1019 $rows_per_page = $this->sanitize_rows_per_page( $rows_per_page );1020 global $wpdb;1021 1022 $table_name = $this->get_logs_table_name();1023 $current_page = $paged ?? 1; // Simplified; no $_GET fallback (avoids nonce sniff concerns).1024 $offset = ( $current_page - 1 ) * $rows_per_page;1025 1026 $use_cache = ( '' === $search );1027 $total_logs = $use_cache ? get_transient( 'members_for_kofi_total_logs' ) : false;1028 $where_sql = 'WHERE 1=1';1029 $params = array();1030 $like = '';1031 if ( '' !== $search ) {1032 $like = '%' . $wpdb->esc_like( $search ) . '%';1033 $where_sql .= ' AND (email LIKE %s OR action LIKE %s OR role LIKE %s)';1034 $params = array( $like, $like, $like );1035 }1036 1037 if ( false === $total_logs ) {1038 if ( empty( $params ) ) {1039 // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Counting plugin-owned table.1040 $total_logs = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . esc_sql( $table_name ) . '`' );1041 } else {1042 // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared -- $query is literal SQL with %s placeholders; the search term is escaped into $params.1043 $query = 'SELECT COUNT(*) FROM `' . esc_sql( $table_name ) . '` ' . $where_sql;1044 $total_logs = (int) $wpdb->get_var( $wpdb->prepare( $query, $params ) );1045 // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared1046 }1047 if ( ! empty( $wpdb->last_error ) ) {1048 \MembersForKofi\Logging\DebugLogger::error(1049 'DB error counting logs',1050 array(1051 'error' => $wpdb->last_error,1052 'last_query' => $wpdb->last_query,1053 )1054 );1055 }1056 if ( $use_cache ) {1057 set_transient( 'members_for_kofi_total_logs', $total_logs, MINUTE_IN_SECONDS );1058 }1059 }1060 1061 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Controlled table name.1062 // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Plugin-owned table query.1063 if ( empty( $params ) ) {1064 // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Unfiltered query.1065 $logs = $wpdb->get_results(1066 $wpdb->prepare(1067 'SELECT `timestamp`, `user_id`, `email`, `action`, `role` FROM `' . esc_sql( $table_name ) . '` ORDER BY `timestamp` DESC LIMIT %d OFFSET %d',1068 $rows_per_page,1069 $offset1070 ),1071 ARRAY_A1072 );1073 // Debug removed (noise).1074 } else {1075 // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared -- $query is literal SQL with %s placeholders; the search term is escaped into $params.1076 $query = 'SELECT `timestamp`, `user_id`, `email`, `action`, `role` FROM `' . esc_sql( $table_name ) . '` ' . $where_sql . ' ORDER BY `timestamp` DESC LIMIT %d OFFSET %d';1077 $logs = $wpdb->get_results(1078 $wpdb->prepare(1079 $query,1080 array_merge( $params, array( $rows_per_page, $offset ) )1081 ),1082 ARRAY_A1083 );1084 // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared1085 // Debug removed (noise).1086 }1087 if ( empty( $logs ) && (int) $total_logs > 0 ) {1088 \MembersForKofi\Logging\DebugLogger::warning( 'No rows returned despite positive total' );1089 }1090 1091 $total_pages = ceil( $total_logs / $rows_per_page );1092 1093 ?>1094 <table class="widefat fixed striped">1095 <thead>1096 <tr>1097 <th><?php esc_html_e( 'Timestamp', 'members-for-kofi' ); ?></th>1098 <th><?php esc_html_e( 'User ID', 'members-for-kofi' ); ?></th>1099 <th><?php esc_html_e( 'Email', 'members-for-kofi' ); ?></th>1100 <th><?php esc_html_e( 'Action', 'members-for-kofi' ); ?></th>1101 <th><?php esc_html_e( 'Role', 'members-for-kofi' ); ?></th>1102 </tr>1103 </thead>1104 <tbody>1105 <?php if ( empty( $logs ) ) : ?>1106 <tr>1107 <td colspan="5"><?php esc_html_e( 'No logs available.', 'members-for-kofi' ); ?></td>1108 </tr>1109 <?php else : ?>1110 <?php foreach ( $logs as $log ) : ?>1111 <tr>1112 <td><?php echo esc_html( $log['timestamp'] ); ?></td>1113 <td><?php echo esc_html( $log['user_id'] ); ?></td>1114 <td><?php echo esc_html( $log['email'] ); ?></td>1115 <td><?php echo esc_html( $log['action'] ); ?></td>1116 <td><?php echo esc_html( $log['role'] ); ?></td>1117 </tr>1118 <?php endforeach; ?>1119 <?php endif; ?>1120 </tbody>1121 </table>1122 1123 <div style="display: flex; align-items: center; justify-content: space-between; margin-top: 15px;">1124 <input type="hidden" id="kofi-members-current-search" value="<?php echo esc_attr( $search ); ?>" />1125 <!-- Clear Logs Button -->1126 <form method="post" action="<?php echo esc_url( add_query_arg( 'tab', 'user_logs', admin_url( 'admin.php?page=members-for-kofi' ) ) ); ?>" style="margin-right: 15px;">1127 <?php wp_nonce_field( 'clear_user_logs_action', 'clear_user_logs_nonce' ); ?>1128 <button type="button" name="clear_logs" class="button button-secondary">1129 <?php esc_html_e( 'Clear Logs', 'members-for-kofi' ); ?>1130 </button>1131 </form>1132 1133 <!-- Rows Per Page Dropdown -->1134 <form method="get" style="margin-right: 15px;">1135 <input type="hidden" name="page" value="members-for-kofi">1136 <input type="hidden" name="tab" value="user_logs">1137 <label for="rows_per_page" style="margin-right: 10px;"><?php esc_html_e( 'Rows per page:', 'members-for-kofi' ); ?></label>1138 <select name="rows_per_page" id="rows_per_page" style="width: auto;">1139 <?php foreach ( self::ROWS_PER_PAGE_OPTIONS as $option ) : ?>1140 <option value="<?php echo esc_attr( $option ); ?>" <?php selected( $rows_per_page, $option ); ?>>1141 <?php echo esc_html( $option ); ?>1142 </option>1143 <?php endforeach; ?>1144 </select>1145 </form>1146 <div class="pagination-links" style="display:flex; gap:4px; flex-wrap:wrap;">1147 <?php if ( $total_pages > 1 ) : ?>1148 <?php1149 $prev_page = max( 1, $current_page - 1 );1150 $next_page = min( $total_pages, $current_page + 1 );1151 if ( $current_page > 1 ) {1152 echo '<button type="button" class="button members-for-kofi-page-btn" data-page="' . esc_attr( $prev_page ) . '">« ' . esc_html__( 'Previous', 'members-for-kofi' ) . '</button>';1153 }1154 for ( $p = 1; $p <= $total_pages; $p++ ) {1155 $cls = 'button members-for-kofi-page-btn' . ( $p === $current_page ? ' button-primary' : '' );1156 echo '<button type="button" class="' . esc_attr( $cls ) . '" data-page="' . esc_attr( $p ) . '">' . esc_html( (string) $p ) . '</button>';1157 }1158 if ( $current_page < $total_pages ) {1159 echo '<button type="button" class="button members-for-kofi-page-btn" data-page="' . esc_attr( $next_page ) . '">' . esc_html__( 'Next', 'members-for-kofi' ) . ' »</button>';1160 }1161 ?>1162 <?php endif; ?>1163 </div>1164 </div>1165 <?php1166 }1167 1168 /**1169 * Renders the request logs table.1170 *1171 * @param int|null $paged The current page number.1172 * @param int $rows_per_page The number of rows to display per page.1173 * @param string $search Search filter.1174 * @return void1175 */1176 public function render_request_logs_table( ?int $paged = null, int $rows_per_page = 10, string $search = '' ): void {1177 $rows_per_page = $this->sanitize_rows_per_page( $rows_per_page );1178 global $wpdb;1179 1180 $table_name = $wpdb->prefix . 'members_for_kofi_request_logs';1181 $current_page = $paged ?? 1;1182 $offset = ( $current_page - 1 ) * $rows_per_page;1183 1184 $use_cache = ( '' === $search );1185 $total_logs = $use_cache ? get_transient( 'members_for_kofi_total_request_logs' ) : false;1186 $where_sql = 'WHERE 1=1';1187 $params = array();1188 1189 if ( '' !== $search ) {1190 $like = '%' . $wpdb->esc_like( $search ) . '%';1191 $where_sql .= ' AND (email LIKE %s OR tier_name LIKE %s OR CAST(status_code AS CHAR) LIKE %s OR error LIKE %s)';1192 $params = array( $like, $like, $like, $like );1193 }1194 1195 if ( false === $total_logs ) {1196 if ( empty( $params ) ) {1197 // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Counting plugin-owned table.1198 $total_logs = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . esc_sql( $table_name ) . '`' );1199 } else {1200 // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared -- $query is literal SQL with %s placeholders; the search term is escaped into $params.1201 $query = 'SELECT COUNT(*) FROM `' . esc_sql( $table_name ) . '` ' . $where_sql;1202 $total_logs = (int) $wpdb->get_var( $wpdb->prepare( $query, $params ) );1203 // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared1204 }1205 if ( $use_cache ) {1206 set_transient( 'members_for_kofi_total_request_logs', $total_logs, MINUTE_IN_SECONDS );1207 }1208 }1209 1210 if ( empty( $params ) ) {1211 // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Unfiltered query on a plugin-owned table.1212 $logs = $wpdb->get_results(1213 $wpdb->prepare(1214 'SELECT `timestamp`, `email`, `tier_name`, `amount`, `currency`, `is_subscription`, `status_code`, `success`, `error` FROM `' . esc_sql( $table_name ) . '` ORDER BY `timestamp` DESC LIMIT %d OFFSET %d',1215 $rows_per_page,1216 $offset1217 ),1218 ARRAY_A1219 );1220 } else {1221 // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared -- $query is literal SQL with %s placeholders; the search term is escaped into $params.1222 $query = 'SELECT `timestamp`, `email`, `tier_name`, `amount`, `currency`, `is_subscription`, `status_code`, `success`, `error` FROM `' . esc_sql( $table_name ) . '` ' . $where_sql . ' ORDER BY `timestamp` DESC LIMIT %d OFFSET %d';1223 $logs = $wpdb->get_results(1224 $wpdb->prepare(1225 $query,1226 array_merge( $params, array( $rows_per_page, $offset ) )1227 ),1228 ARRAY_A1229 );1230 // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared1231 }1232 1233 $total_pages = ceil( $total_logs / $rows_per_page );1234 1235 ?>1236 <table class="widefat fixed striped">1237 <thead>1238 <tr>1239 <th><?php esc_html_e( 'Timestamp', 'members-for-kofi' ); ?></th>1240 <th><?php esc_html_e( 'Email', 'members-for-kofi' ); ?></th>1241 <th><?php esc_html_e( 'Tier Name', 'members-for-kofi' ); ?></th>1242 <th><?php esc_html_e( 'Amount', 'members-for-kofi' ); ?></th>1243 <th><?php esc_html_e( 'Subscription', 'members-for-kofi' ); ?></th>1244 <th><?php esc_html_e( 'Status', 'members-for-kofi' ); ?></th>1245 <th><?php esc_html_e( 'Success', 'members-for-kofi' ); ?></th>1246 <th><?php esc_html_e( 'Error', 'members-for-kofi' ); ?></th>1247 </tr>1248 </thead>1249 <tbody>1250 <?php if ( empty( $logs ) ) : ?>1251 <tr>1252 <td colspan="8"><?php esc_html_e( 'No logs available.', 'members-for-kofi' ); ?></td>1253 </tr>1254 <?php else : ?>1255 <?php foreach ( $logs as $log ) : ?>1256 <tr>1257 <td><?php echo esc_html( $log['timestamp'] ); ?></td>1258 <td><?php echo esc_html( $log['email'] ); ?></td>1259 <td><?php echo esc_html( $log['tier_name'] ); ?></td>1260 <td><?php echo esc_html( $log['amount'] ? esc_html( $log['amount'] . ' ' . $log['currency'] ) : '-' ); ?></td>1261 <td><?php echo $log['is_subscription'] ? esc_html__( 'Yes', 'members-for-kofi' ) : esc_html__( 'No', 'members-for-kofi' ); ?></td>1262 <td><?php echo esc_html( $log['status_code'] ); ?></td>1263 <td><?php echo $log['success'] ? '✓' : '✗'; ?></td>1264 <td><?php echo esc_html( $log['error'] ); ?></td>1265 </tr>1266 <?php endforeach; ?>1267 <?php endif; ?>1268 </tbody>1269 </table>1270 1271 <div style="display: flex; align-items: center; justify-content: space-between; margin-top: 15px;">1272 <input type="hidden" id="kofi-members-current-search" value="<?php echo esc_attr( $search ); ?>" />1273 <!-- Clear Logs Button -->1274 <form method="post" action="<?php echo esc_url( add_query_arg( 'tab', 'logs', admin_url( 'admin.php?page=members-for-kofi' ) ) ); ?>" style="margin-right: 15px;">1275 <?php wp_nonce_field( 'clear_request_logs_action', 'clear_request_logs_nonce' ); ?>1276 <button type="button" name="clear_logs" class="button button-secondary">1277 <?php esc_html_e( 'Clear Logs', 'members-for-kofi' ); ?>1278 </button>1279 </form>1280 1281 <!-- Rows Per Page Dropdown -->1282 <form method="get" style="margin-right: 15px;">1283 <input type="hidden" name="page" value="members-for-kofi">1284 <input type="hidden" name="tab" value="logs">1285 <label for="rows_per_page" style="margin-right: 10px;"><?php esc_html_e( 'Rows per page:', 'members-for-kofi' ); ?></label>1286 <select name="rows_per_page" id="rows_per_page" style="width: auto;">1287 <?php foreach ( self::ROWS_PER_PAGE_OPTIONS as $option ) : ?>1288 <option value="<?php echo esc_attr( $option ); ?>" <?php selected( $rows_per_page, $option ); ?>>1289 <?php echo esc_html( $option ); ?>1290 </option>1291 <?php endforeach; ?>1292 </select>1293 </form>1294 <div class="pagination-links" style="display:flex; gap:4px; flex-wrap:wrap;">1295 <?php if ( $total_pages > 1 ) : ?>1296 <?php1297 $prev_page = max( 1, $current_page - 1 );1298 $next_page = min( $total_pages, $current_page + 1 );1299 if ( $current_page > 1 ) {1300 echo '<button type="button" class="button members-for-kofi-page-btn" data-page="' . esc_attr( $prev_page ) . '">« ' . esc_html__( 'Previous', 'members-for-kofi' ) . '</button>';1301 }1302 for ( $p = 1; $p <= $total_pages; $p++ ) {1303 $cls = 'button members-for-kofi-page-btn' . ( $p === $current_page ? ' button-primary' : '' );1304 echo '<button type="button" class="' . esc_attr( $cls ) . '" data-page="' . esc_attr( $p ) . '">' . esc_html( (string) $p ) . '</button>';1305 }1306 if ( $current_page < $total_pages ) {1307 echo '<button type="button" class="button members-for-kofi-page-btn" data-page="' . esc_attr( $next_page ) . '">' . esc_html__( 'Next', 'members-for-kofi' ) . ' »</button>';1308 }1309 ?>1310 <?php endif; ?>1311 </div>1312 </div>1313 <?php1314 784 } 1315 785 } -
members-for-kofi/tags/1.7.0/src/Logging/RequestLogger.php
r3728327 r3796384 96 96 ); 97 97 98 // Invalidate cached total logs count so UI refresh reflects new entries.99 delete_transient( 'members_for_kofi_total_request_logs' );100 98 } 101 99 -
members-for-kofi/tags/1.7.0/src/Logging/UserLogger.php
r3703946 r3796384 59 59 ); 60 60 61 // Invalidate cached total logs count so UI refresh reflects new entries.62 delete_transient( 'members_for_kofi_total_logs' );63 61 } 64 62 -
members-for-kofi/tags/1.7.0/src/Privacy/PersonalData.php
r3777494 r3796384 212 212 } 213 213 214 delete_transient( 'members_for_kofi_total_logs' );215 delete_transient( 'members_for_kofi_total_request_logs' );216 217 214 // The record of a membership that ended is history, and goes. A 218 215 // membership still being tracked is kept: deleting it would end access -
members-for-kofi/tags/1.7.0/vendor/composer/autoload_classmap.php
r3787989 r3796384 9 9 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 10 10 'MembersForKofi\\Admin\\AdminSettings' => $baseDir . '/src/Admin/AdminSettings.php', 11 'MembersForKofi\\Admin\\LogsTab' => $baseDir . '/src/Admin/LogsTab.php', 12 'MembersForKofi\\Admin\\MembersTab' => $baseDir . '/src/Admin/MembersTab.php', 11 13 'MembersForKofi\\Cli\\MemberSinceCommand' => $baseDir . '/src/Cli/MemberSinceCommand.php', 12 14 'MembersForKofi\\Cron\\LogCleanup' => $baseDir . '/src/Cron/LogCleanup.php', -
members-for-kofi/tags/1.7.0/vendor/composer/autoload_static.php
r3787989 r3796384 24 24 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 25 25 'MembersForKofi\\Admin\\AdminSettings' => __DIR__ . '/../..' . '/src/Admin/AdminSettings.php', 26 'MembersForKofi\\Admin\\LogsTab' => __DIR__ . '/../..' . '/src/Admin/LogsTab.php', 27 'MembersForKofi\\Admin\\MembersTab' => __DIR__ . '/../..' . '/src/Admin/MembersTab.php', 26 28 'MembersForKofi\\Cli\\MemberSinceCommand' => __DIR__ . '/../..' . '/src/Cli/MemberSinceCommand.php', 27 29 'MembersForKofi\\Cron\\LogCleanup' => __DIR__ . '/../..' . '/src/Cron/LogCleanup.php', -
members-for-kofi/trunk/assets/js/admin-settings.js
r3703946 r3796384 1 function membersForKofiInit() { 2 const tabs = document.querySelectorAll('.nav-tab'); 3 const tabContents = document.querySelectorAll('.members-for-kofi-tab'); 4 const activeTabInput = document.getElementById('active_tab'); 5 // Save button now lives only inside the visible general tab container; no JS hide needed. 1 /** 2 * Members for Ko-fi admin page. 3 * 4 * The tabs, filters and paging are plain links and forms; this only adds 5 * what needs a script: copying the webhook URL, adding and removing tier 6 * rows, and confirming a log deletion. 7 */ 8 (function () { 9 'use strict'; 6 10 7 tabs.forEach(tab => { 8 tab.addEventListener('click', function (e) { 9 e.preventDefault(); 10 const activeTabId = this.dataset.tab; 11 if (!activeTabId) return; 11 function init() { 12 const copyButton = document.getElementById('copy-webhook-url'); 13 const urlInput = document.getElementById('webhook-kofi-url'); 14 const copyStatus = document.getElementById('mfk-copy-status'); 12 15 13 // Update active classes 14 tabs.forEach(t => { 15 t.classList.remove('nav-tab-active'); 16 t.setAttribute('aria-selected', 'false'); 17 }); 18 this.classList.add('nav-tab-active'); 19 this.setAttribute('aria-selected', 'true'); 20 21 // Toggle content visibility 22 tabContents.forEach(content => { 23 if (content.id === `members-for-kofi-tab-${activeTabId}`) { 24 content.style.display = ''; 16 if (copyButton && urlInput) { 17 copyButton.addEventListener('click', function () { 18 const done = function () { 19 if (copyStatus) { 20 copyStatus.textContent = (window.kofiMembers && window.kofiMembers.copied) || 'Copied'; 21 window.setTimeout(function () { copyStatus.textContent = ''; }, 3000); 22 } 23 }; 24 urlInput.select(); 25 if (navigator.clipboard && navigator.clipboard.writeText) { 26 navigator.clipboard.writeText(urlInput.value).then(done).catch(function () { 27 document.execCommand('copy'); 28 done(); 29 }); 25 30 } else { 26 content.style.display = 'none'; 27 } 28 }); 29 30 // Hidden input (used only if form submission needs to know last active tab) 31 if (activeTabInput) activeTabInput.value = activeTabId; 32 33 // Optionally reflect tab in URL without reload 34 if (window.history && window.history.replaceState) { 35 const url = new URL(window.location.href); 36 url.searchParams.set('tab', activeTabId); 37 window.history.replaceState({}, '', url.toString()); 38 } 39 }); 40 }); 41 42 // Handle log type selector dropdown 43 function handleLogTypeChange() { 44 const logTypeSelector = document.getElementById('log-type-selector'); 45 if (logTypeSelector && !logTypeSelector.dataset.bound) { 46 logTypeSelector.dataset.bound = '1'; 47 logTypeSelector.addEventListener('change', function() { 48 const logType = this.value; 49 const searchInput = document.getElementById('kofi-members-log-search'); 50 const rowsPerPage = document.getElementById('rows_per_page')?.value || 10; 51 52 // Update the search label based on log type 53 const searchLabel = document.querySelector('label[for="kofi-members-log-search"]'); 54 if (searchLabel) { 55 if (logType === 'request') { 56 searchLabel.textContent = kofiMembers.searchLabelRequest || 'Search (email / tier / status)'; 57 } else { 58 searchLabel.textContent = kofiMembers.searchLabelUser || 'Search (email / action / role)'; 59 } 60 } 61 62 // Clear search input 63 if (searchInput) searchInput.value = ''; 64 65 // Reload the logs table with the new log type 66 loadLogsWithType(logType, '', 1, rowsPerPage); 67 }); 68 } 69 } 70 71 function loadLogsWithType(logType, search, paged, rowsPerPage) { 72 fetch(kofiMembers.ajaxurl, { 73 method: 'POST', 74 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 75 body: new URLSearchParams({ 76 action: 'members_for_kofi_switch_log_type', 77 log_type: logType, 78 search: search || '', 79 paged: paged || 1, 80 rows_per_page: rowsPerPage || 10, 81 _ajax_nonce: kofiMembers.switchLogTypeNonce 82 }), 83 }) 84 .then(r => r.json()) 85 .then(data => { 86 if (data.success) { 87 const tableContainer = document.querySelector('#logs-table-container'); 88 if (tableContainer) { 89 tableContainer.innerHTML = data.data; 90 tableContainer.dataset.logType = logType; 91 } 92 reapplyEventListeners(); 93 handleLogTypeChange(); // Re-bind after content update 94 } else { 95 alert(data.data || kofiMembers.errorMessage); 96 } 97 }) 98 .catch(err => console.error('Log type switch error:', err)); 99 } 100 101 // No need to toggle save button; it is only present within the general tab container. 102 103 function reapplyEventListeners() { 104 // Handle Clear Logs button click 105 const clearLogsButton = document.querySelector('button[name="clear_logs"]'); 106 if (clearLogsButton && !clearLogsButton.dataset.bound) { 107 clearLogsButton.dataset.bound = '1'; 108 clearLogsButton.addEventListener('click', function (e) { 109 e.preventDefault(); 110 const currentSearch = document.getElementById('kofi-members-current-search')?.value || ''; 111 const logType = document.querySelector('#log-type-selector')?.value || 'user'; 112 113 if (confirm(kofiMembers.clearLogsConfirm)) { 114 fetch(kofiMembers.ajaxurl, { 115 method: 'POST', 116 headers: { 117 'Content-Type': 'application/x-www-form-urlencoded', 118 }, 119 body: new URLSearchParams({ 120 action: 'members_for_kofi_clear_logs', 121 _ajax_nonce: kofiMembers.clearLogsNonce, 122 search: currentSearch, 123 log_type: logType, 124 }), 125 }) 126 .then(response => response.json()) 127 .then(data => { 128 if (data.success) { 129 document.querySelector('#logs-table-container').innerHTML = data.data; 130 reapplyEventListeners(); // Reapply listeners after DOM update 131 // Don't call handleLogTypeChange() here - dropdown is outside the container 132 } else { 133 alert(data.data || kofiMembers.errorMessage); 134 } 135 }) 136 .catch(error => console.error('Error clearing logs:', error)); 31 document.execCommand('copy'); 32 done(); 137 33 } 138 34 }); 139 35 } 140 36 141 // Handle Rows Per Page dropdown change 142 const rowsPerPageDropdown = document.querySelector('#rows_per_page'); 143 if (rowsPerPageDropdown && !rowsPerPageDropdown.dataset.bound) { 144 rowsPerPageDropdown.dataset.bound = '1'; 145 rowsPerPageDropdown.addEventListener('change', function (e) { 146 e.preventDefault(); 37 const tbody = document.querySelector('#tier-role-map-table tbody'); 38 const template = document.getElementById('tier-role-row-template'); 147 39 148 const rowsPerPage = rowsPerPageDropdown.value; 149 const currentSearch = document.getElementById('kofi-members-current-search')?.value || ''; 150 const logType = document.querySelector('#log-type-selector')?.value || 'user'; 40 function addRow(tierName) { 41 if (!tbody || !template) { 42 return null; 43 } 44 const row = template.content.firstElementChild.cloneNode(true); 45 tbody.appendChild(row); 46 const input = row.querySelector('.tier-name-input'); 47 if (input) { 48 if (tierName) { 49 input.value = tierName; 50 const select = row.querySelector('.tier-role-select'); 51 if (select) { 52 select.focus(); 53 } 54 } else { 55 input.focus(); 56 } 57 } 58 return row; 59 } 151 60 152 fetch(kofiMembers.ajaxurl, { 153 method: 'POST', 154 headers: { 155 'Content-Type': 'application/x-www-form-urlencoded', 156 }, 157 body: new URLSearchParams({ 158 action: 'members_for_kofi_update_rows_per_page', 159 rows_per_page: rowsPerPage, 160 search: currentSearch, 161 log_type: logType, 162 _ajax_nonce: kofiMembers.rowsPerPageNonce, 163 }), 164 }) 165 .then(response => response.json()) 166 .then(data => { 167 if (data.success) { 168 document.querySelector('#logs-table-container').innerHTML = data.data; 169 reapplyEventListeners(); // Reapply listeners after DOM update 170 // Don't call handleLogTypeChange() here - dropdown is outside the container and doesn't need rebinding 171 } else { 172 alert(data.data || kofiMembers.errorMessage); 173 } 174 }) 175 .catch(error => console.error('Error updating rows per page:', error)); 61 const addButton = document.getElementById('add-tier-role-row'); 62 if (addButton) { 63 addButton.addEventListener('click', function () { 64 addRow(''); 176 65 }); 177 66 } 178 67 179 // Search / Reset / Refresh buttons (bind after lazy load and after table updates) 180 const searchBtn = document.getElementById('kofi-members-log-search-btn'); 181 const resetBtn = document.getElementById('kofi-members-log-reset-btn'); 182 const refreshBtn = document.getElementById('kofi-members-log-refresh-btn'); 183 const searchInput = document.getElementById('kofi-members-log-search'); 68 document.addEventListener('click', function (event) { 69 const remove = event.target.closest('.remove-tier-role-row'); 70 if (remove && tbody && tbody.contains(remove)) { 71 const row = remove.closest('tr'); 72 if (row) { 73 row.remove(); 74 } 75 if (addButton) { 76 addButton.focus(); 77 } 78 return; 79 } 184 80 185 function loadLogs(params) { 186 fetch(kofiMembers.ajaxurl, { 187 method: 'POST', 188 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 189 body: new URLSearchParams(params), 190 }) 191 .then(r => r.json()) 192 .then(data => { 193 if (data.success) { 194 const tableContainer = document.querySelector('#logs-table-container'); 195 if (tableContainer) tableContainer.innerHTML = data.data; 196 const hiddenSearch = document.getElementById('kofi-members-current-search'); 197 if (hiddenSearch) hiddenSearch.value = params.search || ''; 198 reapplyEventListeners(); 199 handleLogTypeChange(); 200 } else { 201 alert(data.data || kofiMembers.errorMessage); 202 } 203 }) 204 .catch(err => console.error('Log load error:', err)); 205 } 81 const unmapped = event.target.closest('.mfk-add-unmapped'); 82 if (unmapped) { 83 addRow(unmapped.getAttribute('data-tier') || ''); 84 const box = unmapped.closest('.mfk-unmapped'); 85 unmapped.remove(); 86 if (box && !box.querySelector('.mfk-add-unmapped')) { 87 box.remove(); 88 } 89 } 90 }); 206 91 207 if (searchBtn && !searchBtn.dataset.bound) { 208 searchBtn.dataset.bound = '1'; 209 searchBtn.addEventListener('click', function (e) { 210 e.preventDefault(); 211 loadLogs({ 212 action: 'members_for_kofi_filter_logs', 213 search: (searchInput?.value || '').trim(), 214 paged: 1, 215 log_type: document.querySelector('#log-type-selector')?.value || 'user', 216 rows_per_page: document.getElementById('rows_per_page')?.value || 10, 217 _ajax_nonce: kofiMembers.filterNonce, 218 }); 219 }); 220 } 221 if (resetBtn && !resetBtn.dataset.bound) { 222 resetBtn.dataset.bound = '1'; 223 resetBtn.addEventListener('click', function (e) { 224 e.preventDefault(); 225 if (searchInput) searchInput.value = ''; 226 loadLogs({ 227 action: 'members_for_kofi_filter_logs', 228 search: '', 229 paged: 1, 230 log_type: document.querySelector('#log-type-selector')?.value || 'user', 231 rows_per_page: document.getElementById('rows_per_page')?.value || 10, 232 _ajax_nonce: kofiMembers.filterNonce, 233 }); 234 }); 235 } 236 if (refreshBtn && !refreshBtn.dataset.bound) { 237 refreshBtn.dataset.bound = '1'; 238 refreshBtn.addEventListener('click', function (e) { 239 e.preventDefault(); 240 const currentSearch = document.getElementById('kofi-members-current-search')?.value || ''; 241 loadLogs({ 242 action: 'members_for_kofi_refresh_logs', 243 search: currentSearch, 244 paged: 1, 245 rows_per_page: document.getElementById('rows_per_page')?.value || 10, 246 _ajax_nonce: kofiMembers.refreshNonce, 247 }); 248 }); 249 } 250 } 251 252 reapplyEventListeners(); 253 handleLogTypeChange(); 254 255 // Handle Pagination link clicks 256 document.addEventListener('click', function (e) { 257 const btn = e.target.closest('.members-for-kofi-page-btn'); 258 if (!btn) return; 259 e.preventDefault(); 260 const page = btn.getAttribute('data-page'); 261 const currentSearch = document.getElementById('kofi-members-current-search')?.value || ''; 262 const logType = document.querySelector('#log-type-selector')?.value || 'user'; 263 fetch(kofiMembers.ajaxurl, { 264 method: 'POST', 265 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 266 body: new URLSearchParams({ 267 action: 'members_for_kofi_pagination', 268 paged: page, 269 search: currentSearch, 270 log_type: logType, 271 _ajax_nonce: kofiMembers.paginationNonce, 272 }), 273 }) 274 .then(r => r.json()) 275 .then(data => { 276 if (data.success) { 277 document.querySelector('#logs-table-container').innerHTML = data.data; 278 reapplyEventListeners(); 279 handleLogTypeChange(); 280 } else { 281 console.error('Error fetching logs:', data.data); 92 document.querySelectorAll('form[data-mfk-confirm]').forEach(function (form) { 93 form.addEventListener('submit', function (event) { 94 if (!window.confirm(form.getAttribute('data-mfk-confirm'))) { 95 event.preventDefault(); 282 96 } 283 })284 .catch(err => console.error('Error fetching logs:', err));285 });286 287 const copyButton = document.getElementById('copy-webhook-url');288 const webhookInput = document.getElementById('webhook-kofi-url');289 290 if (copyButton && webhookInput) {291 copyButton.addEventListener('click', function () {292 // Select the text in the input field293 webhookInput.select();294 webhookInput.setSelectionRange(0, 99999); // For mobile devices295 296 // Copy the text to the clipboard297 navigator.clipboard.writeText(webhookInput.value).then(() => {298 alert('Webhook URL copied to clipboard!');299 }).catch(err => {300 console.error('Failed to copy webhook URL:', err);301 97 }); 302 98 }); 303 99 } 304 100 305 // Tier to Role Mapping dynamic rows 306 const addTierRoleBtn = document.getElementById('add-tier-role-row'); 307 const tierRoleTableBody = document.querySelector('#tier-role-map-table tbody'); 308 const tierRowTemplate = document.getElementById('tier-role-row-template'); 309 310 function addTierRoleRow() { 311 if (!tierRowTemplate || !tierRoleTableBody) return; 312 const html = tierRowTemplate.innerHTML.trim(); 313 const temp = document.createElement('tbody'); 314 temp.innerHTML = html; 315 const row = temp.firstElementChild; 316 if (row) { 317 tierRoleTableBody.appendChild(row); 318 } 101 if (document.readyState === 'loading') { 102 document.addEventListener('DOMContentLoaded', init); 103 } else { 104 init(); 319 105 } 320 321 if (addTierRoleBtn) { 322 addTierRoleBtn.addEventListener('click', function (e) { 323 e.preventDefault(); 324 addTierRoleRow(); 325 }); 326 } 327 328 // Delegate remove button clicks 329 document.addEventListener('click', function (e) { 330 const removeBtn = e.target.closest('.remove-tier-role-row'); 331 if (removeBtn && tierRoleTableBody && tierRoleTableBody.contains(removeBtn)) { 332 e.preventDefault(); 333 const row = removeBtn.closest('tr'); 334 if (row) { 335 row.remove(); 336 } 337 } 338 }); 339 340 } 341 342 if (document.readyState === 'loading') { 343 document.addEventListener('DOMContentLoaded', membersForKofiInit); 344 } else { 345 // DOM already ready (script loaded late in footer) -> initialize immediately. 346 membersForKofiInit(); 347 } 348 349 // Auto-activate user logs tab if URL param present (after init attaches listeners) 350 (() => { 351 const params = new URLSearchParams(window.location.search); 352 if (params.get('tab') === 'user_logs') { 353 const userLogsTab = document.querySelector('.nav-tab[data-tab="user_logs"]'); 354 if (userLogsTab) { 355 // Defer to allow initial init to finish. 356 setTimeout(() => userLogsTab.click(), 0); 357 } 358 } 359 })(); 106 }()); -
members-for-kofi/trunk/languages/members-for-kofi-da_DK.po
r3796045 r3796384 4 4 msgid "" 5 5 msgstr "" 6 "Project-Id-Version: Members for Ko-fi 1. 4.0\n"6 "Project-Id-Version: Members for Ko-fi 1.7.0\n" 7 7 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/members-for-kofi\n" 8 8 "Language: da_DK\n" … … 15 15 #. Plugin Name of the plugin 16 16 #: members-for-kofi.php 17 #: src/Plugin.php:434 18 #: src/Plugin.php:435 19 #: src/Privacy/PersonalData.php:333 17 #: src/Admin/AdminSettings.php:162 18 #: src/Plugin.php:436 19 #: src/Plugin.php:437 20 #: src/Privacy/PersonalData.php:360 20 21 msgid "Members for Ko-fi" 21 22 msgstr "Members for Ko-fi" … … 41 42 msgstr "https://foodgeek.io" 42 43 43 #: src/Admin/AdminSettings.php:107 44 #: src/Admin/AdminSettings.php:667 45 msgid "Ko-fi Settings" 46 msgstr "Ko-fi-indstillinger" 47 48 #: src/Admin/AdminSettings.php:108 49 msgid "Verification Token" 50 msgstr "Verifikationstoken" 51 52 #: src/Admin/AdminSettings.php:111 53 #: src/Admin/AdminSettings.php:671 54 msgid "Role Assignment" 55 msgstr "Tildeling af roller" 56 57 #: src/Admin/AdminSettings.php:112 58 msgid "Only Accept Subscriptions" 59 msgstr "Accepter kun abonnementer" 60 61 #: src/Admin/AdminSettings.php:113 62 msgid "Tier to Role Mapping" 63 msgstr "Tilknytning af niveauer til roller" 64 65 #: src/Admin/AdminSettings.php:114 66 msgid "Default Role (if no match)" 67 msgstr "Standardrolle (hvis intet matcher)" 68 69 #: src/Admin/AdminSettings.php:115 70 msgid "Enable Expiry" 71 msgstr "Slå udløb til" 72 73 #: src/Admin/AdminSettings.php:116 74 msgid "Role Expiry (days)" 75 msgstr "Rollens gyldighed (dage)" 76 77 #: src/Admin/AdminSettings.php:119 78 #: src/Admin/AdminSettings.php:675 79 msgid "Logging" 80 msgstr "Logning" 81 82 #: src/Admin/AdminSettings.php:120 83 msgid "Automatically Clear Logs" 84 msgstr "Ryd logs automatisk" 85 86 #: src/Admin/AdminSettings.php:121 44 #: src/Admin/AdminSettings.php:110 45 msgid "Copied" 46 msgstr "Kopieret" 47 48 #: src/Admin/AdminSettings.php:156 49 msgid "Settings" 50 msgstr "Indstillinger" 51 52 #: src/Admin/AdminSettings.php:157 53 msgid "Members" 54 msgstr "Medlemmer" 55 56 #: src/Admin/AdminSettings.php:158 57 msgid "Logs" 58 msgstr "Logs" 59 60 #: src/Admin/AdminSettings.php:171 61 msgid "Members for Ko-fi sections" 62 msgstr "Sektioner i Members for Ko-fi" 63 64 #: src/Admin/AdminSettings.php:201 65 msgid "Ko-fi connection" 66 msgstr "Forbindelse til Ko-fi" 67 68 #: src/Admin/AdminSettings.php:208 69 msgid "Press \"Send test\" on Ko-fi" 70 msgstr "Tryk på \"Send test\" hos Ko-fi" 71 72 #. translators: %s is a link to the request log. 73 #: src/Admin/AdminSettings.php:213 74 #, php-format 75 msgid "It shows up in the status line above and in %s. A test never creates an account." 76 msgstr "Den vises i statuslinjen ovenfor og i %s. En test opretter aldrig en konto." 77 78 #: src/Admin/AdminSettings.php:214 79 msgid "the request log" 80 msgstr "forespørgselsloggen" 81 82 #: src/Admin/AdminSettings.php:223 83 msgid "Who gets a role" 84 msgstr "Hvem får en rolle" 85 86 #: src/Admin/AdminSettings.php:232 87 msgid "When membership ends" 88 msgstr "Når medlemskabet ophører" 89 90 #: src/Admin/AdminSettings.php:239 91 msgid "Ko-fi subscriptions renew monthly, so 35 days leaves a few days' grace. A role the user held before paying is never removed." 92 msgstr "Ko-fi-abonnementer fornyes hver måned, så 35 dage giver et par dages ekstra frist. En rolle, som brugeren havde før betalingen, fjernes aldrig." 93 94 #: src/Admin/AdminSettings.php:243 95 msgid "Log history" 96 msgstr "Loghistorik" 97 98 #: src/Admin/AdminSettings.php:250 99 msgid "Untick to keep everything, for example when another tool reads the request log." 100 msgstr "Fjern fluebenet for at beholde alt, for eksempel hvis et andet værktøj læser forespørgselsloggen." 101 102 #: src/Admin/AdminSettings.php:272 103 msgid "Not connected: no token is saved, so Ko-fi's requests are refused." 104 msgstr "Ikke forbundet: der er ikke gemt et token, så forespørgsler fra Ko-fi afvises." 105 106 #: src/Admin/AdminSettings.php:277 107 msgid "Waiting for Ko-fi: no request has arrived yet." 108 msgstr "Venter på Ko-fi: der er ikke kommet nogen forespørgsel endnu." 109 110 #. translators: %s is a time span, e.g. "2 hours". 111 #: src/Admin/AdminSettings.php:283 112 #, php-format 113 msgid "Working: the last request from Ko-fi, %s ago, was accepted." 114 msgstr "Forbindelsen virker: den seneste forespørgsel fra Ko-fi for %s siden blev accepteret." 115 116 #. translators: 1: a time span, e.g. "2 hours", 2: why it was refused, e.g. "the token doesn't match". 117 #: src/Admin/AdminSettings.php:287 118 #, php-format 119 msgid "The last request, %1$s ago, was refused: %2$s" 120 msgstr "Den seneste forespørgsel for %1$s siden blev afvist: %2$s" 121 122 #: src/Admin/AdminSettings.php:302 123 msgid "Ko-fi's webhook settings" 124 msgstr "Ko-fis webhook-indstillinger" 125 126 #: src/Admin/AdminSettings.php:303 127 msgid "(opens in a new tab)" 128 msgstr "(åbner i en ny fane)" 129 130 #: src/Admin/AdminSettings.php:305 131 msgid "Give Ko-fi this webhook URL" 132 msgstr "Giv Ko-fi denne webhook-URL" 133 134 #: src/Admin/AdminSettings.php:308 135 msgid "Copy URL" 136 msgstr "Kopiér URL" 137 138 #. translators: %s is a link to Ko-fi's webhook settings. 139 #: src/Admin/AdminSettings.php:314 140 #, php-format 141 msgid "Paste it into %s." 142 msgstr "Indsæt den i %s." 143 144 #: src/Admin/AdminSettings.php:329 145 msgid "Paste Ko-fi's verification token" 146 msgstr "Indsæt Ko-fis verifikationstoken" 147 148 #: src/Admin/AdminSettings.php:332 149 msgid "A token is saved" 150 msgstr "Der er gemt et token" 151 152 #: src/Admin/AdminSettings.php:334 153 msgid "fingerprint, to compare with another site" 154 msgstr "fingeraftryk til sammenligning med et andet websted" 155 156 #: src/Admin/AdminSettings.php:337 157 msgid "No token is saved yet." 158 msgstr "Der er ikke gemt noget token endnu." 159 160 #: src/Admin/AdminSettings.php:343 161 msgid "Leave blank to keep the saved token" 162 msgstr "Lad feltet stå tomt for at beholde det gemte token" 163 164 #: src/Admin/AdminSettings.php:344 165 msgid "On Ko-fi's webhook page, open Advanced and copy the token. Only a hash is stored, so it can't be shown again." 166 msgstr "Åbn Advanced på Ko-fis webhook-side, og kopiér tokenet. Der gemmes kun en hashværdi, så det kan ikke vises igen." 167 168 #: src/Admin/AdminSettings.php:358 169 msgid "Payments that grant a role" 170 msgstr "Betalinger, der giver en rolle" 171 172 #: src/Admin/AdminSettings.php:359 173 msgid "Donations and subscriptions" 174 msgstr "Donationer og abonnementer" 175 176 #: src/Admin/AdminSettings.php:360 177 msgid "Subscriptions only" 178 msgstr "Kun abonnementer" 179 180 #: src/Admin/AdminSettings.php:360 181 msgid "(one-off donations are logged and ignored)" 182 msgstr "(enkeltdonationer logges og ignoreres)" 183 184 #: src/Admin/AdminSettings.php:393 185 #: src/Admin/AdminSettings.php:421 186 msgid "Tier name on Ko-fi" 187 msgstr "Niveaunavn hos Ko-fi" 188 189 #: src/Admin/AdminSettings.php:394 190 msgid "Role for this tier" 191 msgstr "Rolle for dette niveau" 192 193 #: src/Admin/AdminSettings.php:395 194 msgid "Remove this tier" 195 msgstr "Fjern dette niveau" 196 197 #: src/Admin/AdminSettings.php:395 198 msgid "Remove" 199 msgstr "Fjern" 200 201 #: src/Admin/AdminSettings.php:416 202 msgid "Ko-fi tiers" 203 msgstr "Ko-fi-niveauer" 204 205 #: src/Admin/AdminSettings.php:422 206 msgid "Role it grants" 207 msgstr "Rolle, der tildeles" 208 209 #: src/Admin/AdminSettings.php:423 210 msgid "Actions" 211 msgstr "Handlinger" 212 213 #: src/Admin/AdminSettings.php:433 214 msgid "Add tier" 215 msgstr "Tilføj niveau" 216 217 #: src/Admin/AdminSettings.php:434 218 msgid "Names match whatever their capitalisation." 219 msgstr "Navne matcher uanset store og små bogstaver." 220 221 #: src/Admin/AdminSettings.php:438 222 msgid "Tiers Ko-fi sent that aren't listed:" 223 msgstr "Niveauer fra Ko-fi, der ikke er på listen:" 224 225 #. translators: 1: a tier name, 2: how many payments named it. 226 #: src/Admin/AdminSettings.php:445 227 #, php-format 228 msgid "Add \"%1$s\" (%2$d payment)" 229 msgid_plural "Add \"%1$s\" (%2$d payments)" 230 msgstr[0] "Tilføj \"%1$s\" (%2$d betaling)" 231 msgstr[1] "Tilføj \"%1$s\" (%2$d betalinger)" 232 233 #: src/Admin/AdminSettings.php:469 234 msgid "Any other tier, or no tier" 235 msgstr "Andre niveauer eller intet niveau" 236 237 #: src/Admin/AdminSettings.php:471 238 msgid "— No role —" 239 msgstr "— Ingen rolle —" 240 241 #: src/Admin/AdminSettings.php:474 242 msgid "With \"No role\", those payers still get an account, but no role. Administrator and other roles that can run the site are never offered." 243 msgstr "Med \"Ingen rolle\" får disse betalere stadig en konto, men ingen rolle. Administrator og andre roller, der kan styre webstedet, tilbydes aldrig." 244 245 #: src/Admin/AdminSettings.php:489 246 msgid "Remove the role" 247 msgstr "Fjern rollen" 248 249 #: src/Admin/AdminSettings.php:500 250 msgid "Days after the last payment" 251 msgstr "Dage efter seneste betaling" 252 253 #: src/Admin/AdminSettings.php:502 254 msgid "days after the member's last payment" 255 msgstr "dage efter medlemmets seneste betaling" 256 257 #: src/Admin/AdminSettings.php:518 258 msgid "Delete log entries older than" 259 msgstr "Slet logposter, der er ældre end" 260 261 #: src/Admin/AdminSettings.php:529 87 262 msgid "Number of Days to Keep Logs" 88 263 msgstr "Antal dage, logs gemmes" 89 264 90 #: src/Admin/AdminSettings.php:150 91 msgid "Are you sure you want to clear all logs? This action cannot be undone." 92 msgstr "Er du sikker på, at du vil rydde alle logs? Det kan ikke fortrydes." 93 94 #: src/Admin/AdminSettings.php:151 95 msgid "An error occurred. Please try again." 96 msgstr "Der opstod en fejl. Prøv igen." 97 98 #: src/Admin/AdminSettings.php:152 99 #: src/Admin/AdminSettings.php:748 100 msgid "Search (email / action / role)" 101 msgstr "Søg (e-mail / handling / rolle)" 102 103 #: src/Admin/AdminSettings.php:153 104 #: src/Admin/AdminSettings.php:746 105 msgid "Search (email / tier / status)" 106 msgstr "Søg (e-mail / niveau / status)" 107 108 #: src/Admin/AdminSettings.php:188 265 #: src/Admin/AdminSettings.php:531 266 msgid "days" 267 msgstr "dage" 268 269 #. translators: %s is a human-readable time span, e.g. "3 days". 270 #: src/Admin/AdminSettings.php:575 271 #, php-format 272 msgid "Scheduled tasks are not running: role expiry and log cleanup are %s overdue. Make sure WP-Cron runs, or that a system cron calls wp-cron.php." 273 msgstr "Planlagte opgaver kører ikke: udløb af roller og oprydning i logs er %s forsinket. Sørg for, at WP-Cron kører, eller at et system-cronjob kalder wp-cron.php." 274 275 #. translators: 1: email address, 2: comma-separated role names, or "no role". 276 #: src/Admin/AdminSettings.php:627 277 #, php-format 278 msgid "Members for Ko-fi: an account was created by Ko-fi's \"Send test\" button (%1$s, role: %2$s). It is not a real supporter; earlier versions treated tests as payments. You can safely delete it." 279 msgstr "Members for Ko-fi: Ko-fi's \"Send test\"-knap har oprettet en konto (%1$s, rolle: %2$s). Det er ikke en rigtig supporter; tidligere versioner behandlede test som betalinger. Du kan roligt slette den." 280 281 #: src/Admin/AdminSettings.php:629 282 msgid "none" 283 msgstr "ingen" 284 285 #: src/Admin/AdminSettings.php:633 286 msgid "Review the account" 287 msgstr "Se kontoen" 288 289 #: src/Admin/AdminSettings.php:643 290 msgid "Members for Ko-fi: two different verification tokens are stored, and webhooks are checked against the hashed one. Re-enter your Ko-fi verification token in the settings to resolve this." 291 msgstr "Members for Ko-fi: der er gemt to forskellige verifikationstokens, og webhooks tjekkes mod det hashede. Indtast dit Ko-fi-verifikationstoken igen i indstillingerne for at løse det." 292 293 #: src/Admin/AdminSettings.php:668 294 msgid "Open settings" 295 msgstr "Åbn indstillinger" 296 297 #: src/Admin/AdminSettings.php:701 109 298 msgid "Verification Token is required." 110 299 msgstr "Verifikationstoken skal udfyldes." 111 300 112 #: src/Admin/AdminSettings.php: 193301 #: src/Admin/AdminSettings.php:706 113 302 msgid "Role Expiry Days is required and must be a number when expiry is enabled." 114 303 msgstr "Når udløb er slået til, skal du angive rollens gyldighed i dage." 115 304 116 305 #. translators: %s is the role name that was rejected. 117 #: src/Admin/AdminSettings.php: 213118 #: src/Admin/AdminSettings.php: 229306 #: src/Admin/AdminSettings.php:726 307 #: src/Admin/AdminSettings.php:742 119 308 #, php-format 120 309 msgid "Security: Role \"%s\" cannot be assigned via webhook for security reasons." … … 122 311 123 312 #. translators: %s is the role name that was rejected. 124 #: src/Admin/AdminSettings.php: 241313 #: src/Admin/AdminSettings.php:754 125 314 #, php-format 126 315 msgid "Security: Role \"%s\" cannot be used as default role for security reasons." 127 316 msgstr "Sikkerhed: Rollen \"%s\" kan af sikkerhedshensyn ikke bruges som standardrolle." 128 317 129 #: src/Admin/AdminSettings.php:290 130 msgid "A token is saved. Leave this blank to keep it, or paste a new one to replace it." 131 msgstr "Der er gemt et token. Lad feltet stå tomt for at beholde det, eller indsæt et nyt for at erstatte det." 132 133 #. translators: %s is the first eight characters of the saved token's SHA-256 hash. 134 #: src/Admin/AdminSettings.php:293 135 #, php-format 136 msgid "Saved token fingerprint: %s" 137 msgstr "Fingeraftryk af det gemte token: %s" 138 139 #: src/Admin/AdminSettings.php:297 140 msgid "No token set yet. Webhooks are rejected until one is saved." 141 msgstr "Der er endnu ikke gemt et token. Webhook-forespørgsler afvises, indtil du gemmer et." 142 143 #. translators: %s is a link to the Ko-fi Webhooks Management page. 144 #: src/Admin/AdminSettings.php:307 145 #, php-format 146 msgid "This token is used to verify incoming webhook requests from Ko-fi. Paste the verification token from this page: %s and open the Advanced box. Only a hash of it is stored, so it cannot be shown again; to change it, paste the new one." 147 msgstr "Dette token bruges til at bekræfte, at webhook-forespørgsler kommer fra Ko-fi. Åbn %s, find sektionen Advanced, og kopiér verifikationstokenet. Der gemmes kun en hashværdi af tokenet, så det kan ikke vises igen. Indsæt et nyt token for at erstatte det." 148 149 #: src/Admin/AdminSettings.php:308 150 msgid "Ko-fi Webhooks Management" 151 msgstr "Ko-fi Webhooks Management" 152 153 #: src/Admin/AdminSettings.php:322 154 msgid "Webhook URL:" 155 msgstr "Webhook-URL:" 156 157 #: src/Admin/AdminSettings.php:325 158 msgid "Copy to Clipboard" 159 msgstr "Kopiér til udklipsholder" 160 161 #: src/Admin/AdminSettings.php:327 162 msgid "Use this URL to configure your Ko-fi webhook." 163 msgstr "Brug denne URL, når du opsætter din webhook i Ko-fi." 164 165 #. translators: %s is a human-readable time span, e.g. "3 days". 166 #: src/Admin/AdminSettings.php:372 167 #, php-format 168 msgid "Scheduled tasks are not running: role expiry and log cleanup are %s overdue. Make sure WP-Cron runs, or that a system cron calls wp-cron.php." 169 msgstr "Planlagte opgaver kører ikke: udløb af roller og oprydning i logs er %s forsinket. Sørg for, at WP-Cron kører, eller at et system-cronjob kalder wp-cron.php." 170 171 #. translators: 1: email address, 2: comma-separated role names, or "no role". 172 #: src/Admin/AdminSettings.php:424 173 #, php-format 174 msgid "Members for Ko-fi: an account was created by Ko-fi's \"Send test\" button (%1$s, role: %2$s). It is not a real supporter; earlier versions treated tests as payments. You can safely delete it." 175 msgstr "Members for Ko-fi: Ko-fi's \"Send test\"-knap har oprettet en konto (%1$s, rolle: %2$s). Det er ikke en rigtig supporter; tidligere versioner behandlede test som betalinger. Du kan roligt slette den." 176 177 #: src/Admin/AdminSettings.php:426 178 msgid "none" 179 msgstr "ingen" 180 181 #: src/Admin/AdminSettings.php:430 182 msgid "Review the account" 183 msgstr "Se kontoen" 184 185 #: src/Admin/AdminSettings.php:440 186 msgid "Members for Ko-fi: two different verification tokens are stored, and webhooks are checked against the hashed one. Re-enter your Ko-fi verification token in the settings to resolve this." 187 msgstr "Members for Ko-fi: der er gemt to forskellige verifikationstokens, og webhooks tjekkes mod det hashede. Indtast dit Ko-fi-verifikationstoken igen i indstillingerne for at løse det." 188 189 #: src/Admin/AdminSettings.php:465 190 msgid "Open settings" 191 msgstr "Åbn indstillinger" 192 193 #: src/Admin/AdminSettings.php:481 194 msgid "When enabled, users must be active subscribers to receive a role." 195 msgstr "Når det er slået til, skal brugere have et aktivt abonnement for at få en rolle." 196 197 #: src/Admin/AdminSettings.php:498 198 msgid "Ko-fi Tier" 199 msgstr "Ko-fi-niveau" 200 201 #: src/Admin/AdminSettings.php:498 202 #: src/Admin/AdminSettings.php:1097 318 #: src/Admin/LogsTab.php:245 319 msgid "the token doesn't match" 320 msgstr "tokenet passer ikke" 321 322 #: src/Admin/LogsTab.php:247 323 msgid "no token is saved" 324 msgstr "der er ikke gemt noget token" 325 326 #: src/Admin/LogsTab.php:252 327 msgid "the request was malformed" 328 msgstr "forespørgslen var ugyldig" 329 330 #: src/Admin/LogsTab.php:254 331 msgid "the account could not be created" 332 msgstr "kontoen kunne ikke oprettes" 333 334 #. translators: %s is a time of day. 335 #: src/Admin/LogsTab.php:292 336 #, php-format 337 msgid "Today, %s" 338 msgstr "I dag, %s" 339 340 #. translators: %s is a time of day. 341 #: src/Admin/LogsTab.php:297 342 #, php-format 343 msgid "Yesterday, %s" 344 msgstr "I går, %s" 345 346 #: src/Admin/LogsTab.php:365 347 msgid "The log was deleted." 348 msgstr "Loggen blev ryddet." 349 350 #: src/Admin/LogsTab.php:383 351 msgid "Member activity" 352 msgstr "Medlemsaktivitet" 353 354 #: src/Admin/LogsTab.php:396 355 msgid "Requests from Ko-fi" 356 msgstr "Forespørgsler fra Ko-fi" 357 358 #. translators: %s is the site's time zone, e.g. "Europe/Copenhagen". 359 #: src/Admin/LogsTab.php:401 360 #, php-format 361 msgid "Times are in the site's time zone (%s)." 362 msgstr "Tidspunkter er i webstedets tidszone (%s)." 363 364 #: src/Admin/LogsTab.php:412 365 #: src/Admin/LogsTab.php:477 366 msgid "Outcome" 367 msgstr "Resultat" 368 369 #: src/Admin/LogsTab.php:414 370 msgid "All outcomes" 371 msgstr "Alle resultater" 372 373 #: src/Admin/LogsTab.php:415 374 #: src/Admin/LogsTab.php:547 375 msgid "Accepted" 376 msgstr "Accepteret" 377 378 #: src/Admin/LogsTab.php:416 379 msgid "Refused" 380 msgstr "Afvist" 381 382 #: src/Admin/LogsTab.php:424 383 msgid "Search email, tier or reason" 384 msgstr "Søg i e-mail, niveau eller årsag" 385 386 #: src/Admin/LogsTab.php:426 387 msgid "Search email, action or role" 388 msgstr "Søg i e-mail, handling eller rolle" 389 390 #: src/Admin/LogsTab.php:433 391 msgid "Per page" 392 msgstr "Pr. side" 393 394 #: src/Admin/LogsTab.php:440 395 msgid "Filter" 396 msgstr "Filtrér" 397 398 #: src/Admin/LogsTab.php:473 399 msgid "Received" 400 msgstr "Modtaget" 401 402 #: src/Admin/LogsTab.php:474 403 #: src/Privacy/PersonalData.php:321 404 #: src/Webhook/Webhook.php:911 405 #: src/Webhook/Webhook.php:931 406 msgid "Supporter" 407 msgstr "Supporter" 408 409 #: src/Admin/LogsTab.php:475 410 msgid "Tier" 411 msgstr "Niveau" 412 413 #: src/Admin/LogsTab.php:476 414 msgid "Payment" 415 msgstr "Betaling" 416 417 #: src/Admin/LogsTab.php:478 418 msgid "Details" 419 msgstr "Detaljer" 420 421 #: src/Admin/LogsTab.php:483 422 #: src/Admin/LogsTab.php:610 423 msgid "No entries match." 424 msgstr "Ingen poster matcher." 425 426 #: src/Admin/LogsTab.php:483 427 #: src/Admin/LogsTab.php:610 428 msgid "No entries yet." 429 msgstr "Ingen poster endnu." 430 431 #: src/Admin/LogsTab.php:504 432 msgid "Ko-fi test" 433 msgstr "Ko-fi-test" 434 435 #: src/Admin/LogsTab.php:506 436 msgid "Not stored" 437 msgstr "Ikke gemt" 438 439 #: src/Admin/LogsTab.php:522 440 msgid "not mapped" 441 msgstr "ikke tilknyttet" 442 443 #: src/Admin/LogsTab.php:530 444 msgid "Test, no account made" 445 msgstr "Test, ingen konto oprettet" 446 447 #. translators: %s is an amount with its currency, e.g. "5.00 EUR". 448 #: src/Admin/LogsTab.php:536 449 #, php-format 450 msgid "%s · subscription" 451 msgstr "%s · abonnement" 452 453 #. translators: %s is an amount with its currency, e.g. "5.00 EUR". 454 #: src/Admin/LogsTab.php:538 455 #, php-format 456 msgid "%s · one-off" 457 msgstr "%s · enkeltbetaling" 458 459 #. translators: %s is why the request was refused, e.g. "the token doesn't match". 460 #: src/Admin/LogsTab.php:552 461 #, php-format 462 msgid "Refused: %s" 463 msgstr "Afvist: %s" 464 465 #: src/Admin/LogsTab.php:560 466 msgid "Payload" 467 msgstr "Indhold" 468 469 #: src/Admin/LogsTab.php:602 470 msgid "When" 471 msgstr "Tidspunkt" 472 473 #: src/Admin/LogsTab.php:603 474 #: src/Admin/MembersTab.php:264 475 msgid "Member" 476 msgstr "Medlem" 477 478 #: src/Admin/LogsTab.php:604 479 msgid "What happened" 480 msgstr "Hændelse" 481 482 #: src/Admin/LogsTab.php:605 483 #: src/Admin/MembersTab.php:265 203 484 msgid "Role" 204 485 msgstr "Rolle" 205 486 206 #: src/Admin/AdminSettings.php:536 207 msgid "Add Tier Mapping" 208 msgstr "Tilføj niveautilknytning" 209 210 #: src/Admin/AdminSettings.php:537 211 msgid "Map Ko-fi tiers to WordPress roles. Only exact tier names will be matched." 212 msgstr "Knyt Ko-fi-niveauer til WordPress-roller. Kun niveaunavne, der er helt ens, matcher." 213 214 #: src/Admin/AdminSettings.php:554 215 msgid "— No default —" 216 msgstr "— Ingen standard —" 217 218 #: src/Admin/AdminSettings.php:563 219 msgid "If no tier matches, or if you are not using tiers, this role will be assigned to the user." 220 msgstr "Hvis intet niveau matcher, eller hvis du ikke bruger niveauer, får brugeren denne rolle." 221 222 #: src/Admin/AdminSettings.php:594 223 msgid "Number of days the role remains active (only used if expiry is enabled)." 224 msgstr "Antal dage rollen er aktiv (bruges kun, når udløb er slået til)." 225 226 #: src/Admin/AdminSettings.php:606 227 msgid "Enable automatic deletion of old logs based on retention period." 228 msgstr "Slet automatisk logs, der er ældre end opbevaringsperioden." 229 230 #: src/Admin/AdminSettings.php:618 231 msgid "Number of days to keep logs before automatic deletion (minimum 1 day)." 232 msgstr "Antal dage, logs gemmes, før de slettes automatisk (mindst 1 dag)." 233 234 #: src/Admin/AdminSettings.php:646 235 msgid "Members for Ko-fi Settings" 236 msgstr "Indstillinger for Members for Ko-fi" 237 238 #: src/Admin/AdminSettings.php:657 239 msgid "Settings" 240 msgstr "Indstillinger" 241 242 #: src/Admin/AdminSettings.php:660 243 #: src/Admin/AdminSettings.php:683 244 msgid "Logs" 245 msgstr "Logs" 246 247 #: src/Admin/AdminSettings.php:728 248 msgid "Log Type:" 249 msgstr "Logtype:" 250 251 #: src/Admin/AdminSettings.php:732 252 msgid "User Logs" 253 msgstr "Brugerlog" 254 255 #: src/Admin/AdminSettings.php:735 256 msgid "Request Logs" 257 msgstr "Forespørgselslog" 258 259 #: src/Admin/AdminSettings.php:755 260 msgid "Search" 261 msgstr "Søg" 262 263 #: src/Admin/AdminSettings.php:756 264 msgid "Reset" 265 msgstr "Nulstil" 266 267 #: src/Admin/AdminSettings.php:759 268 msgid "Refresh" 269 msgstr "Opdater" 270 271 #: src/Admin/AdminSettings.php:790 272 #: src/Admin/AdminSettings.php:826 273 #: src/Admin/AdminSettings.php:900 274 #: src/Admin/AdminSettings.php:929 275 #: src/Admin/AdminSettings.php:960 276 #: src/Admin/AdminSettings.php:983 487 #: src/Admin/LogsTab.php:651 488 msgid "First page" 489 msgstr "Første side" 490 491 #: src/Admin/LogsTab.php:652 492 #: src/Admin/MembersTab.php:351 493 msgid "Previous page" 494 msgstr "Forrige side" 495 496 #: src/Admin/LogsTab.php:653 497 #: src/Admin/MembersTab.php:362 498 msgid "Next page" 499 msgstr "Næste side" 500 501 #: src/Admin/LogsTab.php:654 502 msgid "Last page" 503 msgstr "Sidste side" 504 505 #: src/Admin/LogsTab.php:665 506 msgid "Current page" 507 msgstr "Aktuel side" 508 509 #: src/Admin/LogsTab.php:676 510 #: src/Admin/MembersTab.php:349 511 msgid "Pages" 512 msgstr "Sider" 513 514 #. translators: 1: an input holding the current page number, 2: the number of pages. 515 #. translators: 1: the current page number, 2: the number of pages. 516 #: src/Admin/LogsTab.php:683 517 #: src/Admin/MembersTab.php:356 518 #, php-format 519 msgid "Page %1$s of %2$s" 520 msgstr "Side %1$s af %2$s" 521 522 #. translators: %s is the number of entries. 523 #: src/Admin/LogsTab.php:715 524 #, php-format 525 msgid "Removes %s request entry. Member activity is kept." 526 msgid_plural "Removes all %s request entries. Member activity is kept." 527 msgstr[0] "Fjerner %s post i forespørgselsloggen. Medlemsaktiviteten bevares." 528 msgstr[1] "Fjerner alle %s poster i forespørgselsloggen. Medlemsaktiviteten bevares." 529 530 #. translators: %s is the number of entries. 531 #: src/Admin/LogsTab.php:717 532 #, php-format 533 msgid "Removes %s activity entry. The request log is kept." 534 msgid_plural "Removes all %s activity entries. The request log is kept." 535 msgstr[0] "Fjerner %s post i medlemsaktiviteten. Forespørgselsloggen bevares." 536 msgstr[1] "Fjerner alle %s poster i medlemsaktiviteten. Forespørgselsloggen bevares." 537 538 #. translators: %s is the number of entries. 539 #: src/Admin/LogsTab.php:720 540 #, php-format 541 msgid "Delete %s entry in this log? This can't be undone." 542 msgid_plural "Delete all %s entries in this log? This can't be undone." 543 msgstr[0] "Ryd %s post i denne log? Det kan ikke fortrydes." 544 msgstr[1] "Ryd alle %s poster i denne log? Det kan ikke fortrydes." 545 546 #: src/Admin/LogsTab.php:726 547 msgid "Delete this log" 548 msgstr "Ryd denne log" 549 550 #. translators: %d is a number of days. 551 #: src/Admin/LogsTab.php:734 552 #, php-format 553 msgid "Entries older than %d day are deleted automatically." 554 msgid_plural "Entries older than %d days are deleted automatically." 555 msgstr[0] "Poster ældre end %d dag slettes automatisk." 556 msgstr[1] "Poster ældre end %d dage slettes automatisk." 557 558 #: src/Admin/LogsTab.php:747 559 msgid "Delete request log…" 560 msgstr "Ryd forespørgselsloggen…" 561 562 #: src/Admin/LogsTab.php:747 563 msgid "Delete activity log…" 564 msgstr "Ryd aktivitetsloggen…" 565 566 #: src/Admin/LogsTab.php:761 277 567 msgid "You do not have permission to access this resource." 278 568 msgstr "Du har ikke adgang til denne ressource." 279 569 280 #: src/Admin/AdminSettings.php:799 281 msgid "Invalid page number." 282 msgstr "Ugyldigt sidetal." 283 284 #: src/Admin/AdminSettings.php:859 285 msgid "Logs table does not exist." 286 msgstr "Logtabellen findes ikke." 287 288 #: src/Admin/AdminSettings.php:1093 289 #: src/Admin/AdminSettings.php:1235 290 msgid "Timestamp" 291 msgstr "Tidspunkt" 292 293 #: src/Admin/AdminSettings.php:1094 294 msgid "User ID" 295 msgstr "Bruger-ID" 296 297 #: src/Admin/AdminSettings.php:1095 298 #: src/Admin/AdminSettings.php:1236 299 msgid "Email" 300 msgstr "E-mail" 301 302 #: src/Admin/AdminSettings.php:1096 303 msgid "Action" 304 msgstr "Handling" 305 306 #: src/Admin/AdminSettings.php:1103 307 #: src/Admin/AdminSettings.php:1248 308 msgid "No logs available." 309 msgstr "Der er ingen logs." 310 311 #: src/Admin/AdminSettings.php:1125 312 #: src/Admin/AdminSettings.php:1273 313 msgid "Clear Logs" 314 msgstr "Ryd logs" 315 316 #: src/Admin/AdminSettings.php:1133 317 #: src/Admin/AdminSettings.php:1281 318 msgid "Rows per page:" 319 msgstr "Rækker pr. side:" 320 321 #: src/Admin/AdminSettings.php:1148 322 #: src/Admin/AdminSettings.php:1296 323 msgid "Previous" 324 msgstr "Forrige" 325 326 #: src/Admin/AdminSettings.php:1155 327 #: src/Admin/AdminSettings.php:1303 328 msgid "Next" 329 msgstr "Næste" 330 331 #: src/Admin/AdminSettings.php:1237 332 msgid "Tier Name" 333 msgstr "Niveaunavn" 334 335 #: src/Admin/AdminSettings.php:1238 336 msgid "Amount" 337 msgstr "Beløb" 338 339 #: src/Admin/AdminSettings.php:1239 340 msgid "Subscription" 341 msgstr "Abonnement" 342 343 #: src/Admin/AdminSettings.php:1240 570 #: src/Admin/MembersTab.php:195 571 msgid "Unknown" 572 msgstr "Ukendt" 573 574 #: src/Admin/MembersTab.php:214 575 msgid "All" 576 msgstr "Alle" 577 578 #: src/Admin/MembersTab.php:215 579 #: src/Admin/MembersTab.php:316 580 msgid "Active" 581 msgstr "Aktive" 582 583 #. translators: The members whose access ends within the next seven days. 584 #: src/Admin/MembersTab.php:217 585 msgid "Ending within 7 days" 586 msgstr "Ophører inden for 7 dage" 587 588 #: src/Admin/MembersTab.php:218 589 #: src/Admin/MembersTab.php:305 590 msgid "Ended" 591 msgstr "Ophørt" 592 593 #: src/Admin/MembersTab.php:254 594 #: src/Admin/MembersTab.php:256 595 msgid "Search members" 596 msgstr "Søg efter medlemmer" 597 598 #: src/Admin/MembersTab.php:255 599 msgid "Name or email" 600 msgstr "Navn eller e-mail" 601 602 #: src/Admin/MembersTab.php:266 344 603 msgid "Status" 345 604 msgstr "Status" 346 605 347 #: src/Admin/AdminSettings.php:1241 348 msgid "Success" 349 msgstr "Gennemført" 350 351 #: src/Admin/AdminSettings.php:1242 352 msgid "Error" 353 msgstr "Fejl" 354 355 #: src/Admin/AdminSettings.php:1257 356 msgid "Yes" 357 msgstr "Ja" 358 359 #: src/Admin/AdminSettings.php:1257 360 msgid "No" 361 msgstr "Nej" 606 #: src/Admin/MembersTab.php:267 607 msgid "Member since" 608 msgstr "Medlem siden" 609 610 #: src/Admin/MembersTab.php:268 611 msgid "Last payment" 612 msgstr "Seneste betaling" 613 614 #: src/Admin/MembersTab.php:269 615 msgid "Access until" 616 msgstr "Adgang indtil" 617 618 #: src/Admin/MembersTab.php:274 619 msgid "Nobody has become a member through Ko-fi yet." 620 msgstr "Ingen er endnu blevet medlem via Ko-fi." 621 622 #: src/Admin/MembersTab.php:274 623 msgid "No members match." 624 msgstr "Ingen medlemmer matcher." 625 626 #. translators: %s is the name of a role that was removed. 627 #: src/Admin/MembersTab.php:293 628 #, php-format 629 msgid "%s, removed" 630 msgstr "%s, fjernet" 631 632 #: src/Admin/MembersTab.php:297 633 msgid "Granted before payment; never removed" 634 msgstr "Tildelt før betaling; fjernes aldrig" 635 636 #. translators: %d is a number of days. 637 #: src/Admin/MembersTab.php:311 638 #, php-format 639 msgid "Ends in %d day" 640 msgid_plural "Ends in %d days" 641 msgstr[0] "Udløber om %d dag" 642 msgstr[1] "Udløber om %d dage" 643 644 #: src/Admin/MembersTab.php:325 645 msgid "subscription" 646 msgstr "abonnement" 647 648 #: src/Admin/MembersTab.php:325 649 msgid "one-off" 650 msgstr "enkeltbetaling" 651 652 #: src/Admin/MembersTab.php:332 653 msgid "No expiry" 654 msgstr "Udløber ikke" 655 656 #. translators: %s is a date. 657 #: src/Admin/MembersTab.php:335 658 #, php-format 659 msgid "Ended %s" 660 msgstr "Ophørte %s" 661 662 #: src/Admin/MembersTab.php:367 663 msgid "Cancelling happens on Ko-fi, not here." 664 msgstr "Opsigelse sker hos Ko-fi, ikke her." 362 665 363 666 #: src/Membership.php:198 … … 381 684 msgstr "Log ind på Ko-fi, og gå til Account settings og Subscriptions. Betaler du med PayPal, kan du også opsige det i PayPal." 382 685 383 #: src/Privacy/PersonalData.php:6 4384 #: src/Privacy/PersonalData.php: 79686 #: src/Privacy/PersonalData.php:65 687 #: src/Privacy/PersonalData.php:80 385 688 msgid "Members for Ko-fi logs" 386 689 msgstr "Logs fra Members for Ko-fi" 387 690 388 #: src/Privacy/PersonalData.php:11 5691 #: src/Privacy/PersonalData.php:119 389 692 msgid "Ko-fi membership" 390 693 msgstr "Ko-fi-medlemskab" 391 694 392 #: src/Privacy/PersonalData.php:14 5695 #: src/Privacy/PersonalData.php:148 393 696 msgid "Ko-fi membership activity" 394 697 msgstr "Aktivitet knyttet til Ko-fi-medlemskab" 395 698 396 #: src/Privacy/PersonalData.php:16 5699 #: src/Privacy/PersonalData.php:166 397 700 msgid "Ko-fi webhook requests" 398 701 msgstr "Ko-fi-webhook-forespørgsler" 399 702 400 #: src/Privacy/PersonalData.php:2 27703 #: src/Privacy/PersonalData.php:242 401 704 msgid "An active Ko-fi membership was kept, so the person keeps the access they pay for. It is removed when the membership ends." 402 705 msgstr "Et aktivt Ko-fi-medlemskab blev bevaret, så personen fortsat har den adgang, vedkommende betaler for. Adgangen fjernes, når medlemskabet ophører." 403 706 404 #: src/Privacy/PersonalData.php:294 405 #: src/Webhook/Webhook.php:753 406 #: src/Webhook/Webhook.php:772 407 msgid "Supporter" 408 msgstr "Supporter" 409 410 #: src/Privacy/PersonalData.php:335 707 #: src/Privacy/PersonalData.php:362 411 708 msgid "When you support us on Ko-fi, Ko-fi sends this site your email address, the name and message you entered, the amount, and your membership tier. We use your email address to create or find your account here and give it the access your support pays for." 412 709 msgstr "Når du støtter os på Ko-fi, sender Ko-fi din e-mailadresse, det navn og den besked, du skrev, beløbet og dit medlemsniveau til denne side. Vi bruger din e-mailadresse til at oprette eller finde din konto her og give dig den adgang, din støtte betaler for." 413 710 414 #: src/Privacy/PersonalData.php:3 36711 #: src/Privacy/PersonalData.php:363 415 712 msgid "A record of each payment and of the access granted or removed is kept for the number of days set by the site owner, then deleted automatically when log cleanup is enabled. You can ask for a copy of this data, or for it to be erased, through the site's personal data request process." 416 713 msgstr "Hver betaling og hver tildelt eller fjernet adgang registreres og gemmes i det antal dage, sidens ejer har valgt, og slettes derefter automatisk, når oprydning i logs er slået til. Du kan bede om en kopi af disse data eller om at få dem slettet via sidens procedure for anmodninger om persondata." 417 418 #: src/Admin/AdminSettings.php419 msgid "Only assign roles to subscribers."420 msgstr "Tildel kun roller til abonnenter."421 422 #: src/Admin/AdminSettings.php423 msgid "If checked, role will be removed after the set number of days."424 msgstr "Hvis markeret, fjernes rollen efter det angivne antal dage." -
members-for-kofi/trunk/languages/members-for-kofi.pot
r3796045 r3796384 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Members for Ko-fi 1. 6.1\n"5 "Project-Id-Version: Members for Ko-fi 1.7.0\n" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/members-for-kofi\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-11-11T 09:51:50+00:00\n"12 "POT-Creation-Date: 2026-11-11T11:11:06+00:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 14 "X-Generator: WP-CLI 2.12.0\n" … … 17 17 #. Plugin Name of the plugin 18 18 #: members-for-kofi.php 19 #: src/Admin/AdminSettings.php:162 19 20 #: src/Plugin.php:436 20 21 #: src/Plugin.php:437 21 #: src/Privacy/PersonalData.php:36 322 #: src/Privacy/PersonalData.php:360 22 23 msgid "Members for Ko-fi" 23 24 msgstr "" … … 43 44 msgstr "" 44 45 45 #: src/Admin/AdminSettings.php:107 46 #: src/Admin/AdminSettings.php:671 47 msgid "Ko-fi Settings" 48 msgstr "" 49 50 #: src/Admin/AdminSettings.php:108 51 msgid "Verification Token" 52 msgstr "" 53 54 #: src/Admin/AdminSettings.php:111 55 #: src/Admin/AdminSettings.php:675 56 msgid "Role Assignment" 57 msgstr "" 58 59 #: src/Admin/AdminSettings.php:112 60 msgid "Only Accept Subscriptions" 61 msgstr "" 62 63 #: src/Admin/AdminSettings.php:113 64 msgid "Tier to Role Mapping" 65 msgstr "" 66 67 #: src/Admin/AdminSettings.php:114 68 msgid "Default Role (if no match)" 69 msgstr "" 70 71 #: src/Admin/AdminSettings.php:115 72 msgid "Enable Expiry" 73 msgstr "" 74 75 #: src/Admin/AdminSettings.php:116 76 msgid "Role Expiry (days)" 77 msgstr "" 78 79 #: src/Admin/AdminSettings.php:119 80 #: src/Admin/AdminSettings.php:679 81 msgid "Logging" 82 msgstr "" 83 84 #: src/Admin/AdminSettings.php:120 85 msgid "Automatically Clear Logs" 86 msgstr "" 87 88 #: src/Admin/AdminSettings.php:121 46 #: src/Admin/AdminSettings.php:110 47 msgid "Copied" 48 msgstr "" 49 50 #: src/Admin/AdminSettings.php:156 51 msgid "Settings" 52 msgstr "" 53 54 #: src/Admin/AdminSettings.php:157 55 msgid "Members" 56 msgstr "" 57 58 #: src/Admin/AdminSettings.php:158 59 msgid "Logs" 60 msgstr "" 61 62 #: src/Admin/AdminSettings.php:171 63 msgid "Members for Ko-fi sections" 64 msgstr "" 65 66 #: src/Admin/AdminSettings.php:201 67 msgid "Ko-fi connection" 68 msgstr "" 69 70 #: src/Admin/AdminSettings.php:208 71 msgid "Press \"Send test\" on Ko-fi" 72 msgstr "" 73 74 #. translators: %s is a link to the request log. 75 #: src/Admin/AdminSettings.php:213 76 #, php-format 77 msgid "It shows up in the status line above and in %s. A test never creates an account." 78 msgstr "" 79 80 #: src/Admin/AdminSettings.php:214 81 msgid "the request log" 82 msgstr "" 83 84 #: src/Admin/AdminSettings.php:223 85 msgid "Who gets a role" 86 msgstr "" 87 88 #: src/Admin/AdminSettings.php:232 89 msgid "When membership ends" 90 msgstr "" 91 92 #: src/Admin/AdminSettings.php:239 93 msgid "Ko-fi subscriptions renew monthly, so 35 days leaves a few days' grace. A role the user held before paying is never removed." 94 msgstr "" 95 96 #: src/Admin/AdminSettings.php:243 97 msgid "Log history" 98 msgstr "" 99 100 #: src/Admin/AdminSettings.php:250 101 msgid "Untick to keep everything, for example when another tool reads the request log." 102 msgstr "" 103 104 #: src/Admin/AdminSettings.php:272 105 msgid "Not connected: no token is saved, so Ko-fi's requests are refused." 106 msgstr "" 107 108 #: src/Admin/AdminSettings.php:277 109 msgid "Waiting for Ko-fi: no request has arrived yet." 110 msgstr "" 111 112 #. translators: %s is a time span, e.g. "2 hours". 113 #: src/Admin/AdminSettings.php:283 114 #, php-format 115 msgid "Working: the last request from Ko-fi, %s ago, was accepted." 116 msgstr "" 117 118 #. translators: 1: a time span, e.g. "2 hours", 2: why it was refused, e.g. "the token doesn't match". 119 #: src/Admin/AdminSettings.php:287 120 #, php-format 121 msgid "The last request, %1$s ago, was refused: %2$s" 122 msgstr "" 123 124 #: src/Admin/AdminSettings.php:302 125 msgid "Ko-fi's webhook settings" 126 msgstr "" 127 128 #: src/Admin/AdminSettings.php:303 129 msgid "(opens in a new tab)" 130 msgstr "" 131 132 #: src/Admin/AdminSettings.php:305 133 msgid "Give Ko-fi this webhook URL" 134 msgstr "" 135 136 #: src/Admin/AdminSettings.php:308 137 msgid "Copy URL" 138 msgstr "" 139 140 #. translators: %s is a link to Ko-fi's webhook settings. 141 #: src/Admin/AdminSettings.php:314 142 #, php-format 143 msgid "Paste it into %s." 144 msgstr "" 145 146 #: src/Admin/AdminSettings.php:329 147 msgid "Paste Ko-fi's verification token" 148 msgstr "" 149 150 #: src/Admin/AdminSettings.php:332 151 msgid "A token is saved" 152 msgstr "" 153 154 #: src/Admin/AdminSettings.php:334 155 msgid "fingerprint, to compare with another site" 156 msgstr "" 157 158 #: src/Admin/AdminSettings.php:337 159 msgid "No token is saved yet." 160 msgstr "" 161 162 #: src/Admin/AdminSettings.php:343 163 msgid "Leave blank to keep the saved token" 164 msgstr "" 165 166 #: src/Admin/AdminSettings.php:344 167 msgid "On Ko-fi's webhook page, open Advanced and copy the token. Only a hash is stored, so it can't be shown again." 168 msgstr "" 169 170 #: src/Admin/AdminSettings.php:358 171 msgid "Payments that grant a role" 172 msgstr "" 173 174 #: src/Admin/AdminSettings.php:359 175 msgid "Donations and subscriptions" 176 msgstr "" 177 178 #: src/Admin/AdminSettings.php:360 179 msgid "Subscriptions only" 180 msgstr "" 181 182 #: src/Admin/AdminSettings.php:360 183 msgid "(one-off donations are logged and ignored)" 184 msgstr "" 185 186 #: src/Admin/AdminSettings.php:393 187 #: src/Admin/AdminSettings.php:421 188 msgid "Tier name on Ko-fi" 189 msgstr "" 190 191 #: src/Admin/AdminSettings.php:394 192 msgid "Role for this tier" 193 msgstr "" 194 195 #: src/Admin/AdminSettings.php:395 196 msgid "Remove this tier" 197 msgstr "" 198 199 #: src/Admin/AdminSettings.php:395 200 msgid "Remove" 201 msgstr "" 202 203 #: src/Admin/AdminSettings.php:416 204 msgid "Ko-fi tiers" 205 msgstr "" 206 207 #: src/Admin/AdminSettings.php:422 208 msgid "Role it grants" 209 msgstr "" 210 211 #: src/Admin/AdminSettings.php:423 212 msgid "Actions" 213 msgstr "" 214 215 #: src/Admin/AdminSettings.php:433 216 msgid "Add tier" 217 msgstr "" 218 219 #: src/Admin/AdminSettings.php:434 220 msgid "Names match whatever their capitalisation." 221 msgstr "" 222 223 #: src/Admin/AdminSettings.php:438 224 msgid "Tiers Ko-fi sent that aren't listed:" 225 msgstr "" 226 227 #. translators: 1: a tier name, 2: how many payments named it. 228 #: src/Admin/AdminSettings.php:445 229 #, php-format 230 msgid "Add \"%1$s\" (%2$d payment)" 231 msgid_plural "Add \"%1$s\" (%2$d payments)" 232 msgstr[0] "" 233 msgstr[1] "" 234 235 #: src/Admin/AdminSettings.php:469 236 msgid "Any other tier, or no tier" 237 msgstr "" 238 239 #: src/Admin/AdminSettings.php:471 240 msgid "— No role —" 241 msgstr "" 242 243 #: src/Admin/AdminSettings.php:474 244 msgid "With \"No role\", those payers still get an account, but no role. Administrator and other roles that can run the site are never offered." 245 msgstr "" 246 247 #: src/Admin/AdminSettings.php:489 248 msgid "Remove the role" 249 msgstr "" 250 251 #: src/Admin/AdminSettings.php:500 252 msgid "Days after the last payment" 253 msgstr "" 254 255 #: src/Admin/AdminSettings.php:502 256 msgid "days after the member's last payment" 257 msgstr "" 258 259 #: src/Admin/AdminSettings.php:518 260 msgid "Delete log entries older than" 261 msgstr "" 262 263 #: src/Admin/AdminSettings.php:529 89 264 msgid "Number of Days to Keep Logs" 90 265 msgstr "" 91 266 92 #: src/Admin/AdminSettings.php:150 93 msgid "Are you sure you want to clear all logs? This action cannot be undone." 94 msgstr "" 95 96 #: src/Admin/AdminSettings.php:151 97 msgid "An error occurred. Please try again." 98 msgstr "" 99 100 #: src/Admin/AdminSettings.php:152 101 #: src/Admin/AdminSettings.php:752 102 msgid "Search (email / action / role)" 103 msgstr "" 104 105 #: src/Admin/AdminSettings.php:153 106 #: src/Admin/AdminSettings.php:750 107 msgid "Search (email / tier / status)" 108 msgstr "" 109 110 #: src/Admin/AdminSettings.php:188 267 #: src/Admin/AdminSettings.php:531 268 msgid "days" 269 msgstr "" 270 271 #. translators: %s is a human-readable time span, e.g. "3 days". 272 #: src/Admin/AdminSettings.php:575 273 #, php-format 274 msgid "Scheduled tasks are not running: role expiry and log cleanup are %s overdue. Make sure WP-Cron runs, or that a system cron calls wp-cron.php." 275 msgstr "" 276 277 #. translators: 1: email address, 2: comma-separated role names, or "no role". 278 #: src/Admin/AdminSettings.php:627 279 #, php-format 280 msgid "Members for Ko-fi: an account was created by Ko-fi's \"Send test\" button (%1$s, role: %2$s). It is not a real supporter; earlier versions treated tests as payments. You can safely delete it." 281 msgstr "" 282 283 #: src/Admin/AdminSettings.php:629 284 msgid "none" 285 msgstr "" 286 287 #: src/Admin/AdminSettings.php:633 288 msgid "Review the account" 289 msgstr "" 290 291 #: src/Admin/AdminSettings.php:643 292 msgid "Members for Ko-fi: two different verification tokens are stored, and webhooks are checked against the hashed one. Re-enter your Ko-fi verification token in the settings to resolve this." 293 msgstr "" 294 295 #: src/Admin/AdminSettings.php:668 296 msgid "Open settings" 297 msgstr "" 298 299 #: src/Admin/AdminSettings.php:701 111 300 msgid "Verification Token is required." 112 301 msgstr "" 113 302 114 #: src/Admin/AdminSettings.php: 193303 #: src/Admin/AdminSettings.php:706 115 304 msgid "Role Expiry Days is required and must be a number when expiry is enabled." 116 305 msgstr "" 117 306 118 307 #. translators: %s is the role name that was rejected. 119 #: src/Admin/AdminSettings.php: 213120 #: src/Admin/AdminSettings.php: 229308 #: src/Admin/AdminSettings.php:726 309 #: src/Admin/AdminSettings.php:742 121 310 #, php-format 122 311 msgid "Security: Role \"%s\" cannot be assigned via webhook for security reasons." … … 124 313 125 314 #. translators: %s is the role name that was rejected. 126 #: src/Admin/AdminSettings.php: 241315 #: src/Admin/AdminSettings.php:754 127 316 #, php-format 128 317 msgid "Security: Role \"%s\" cannot be used as default role for security reasons." 129 318 msgstr "" 130 319 131 #: src/Admin/AdminSettings.php:290 132 msgid "A token is saved. Leave this blank to keep it, or paste a new one to replace it." 133 msgstr "" 134 135 #. translators: %s is the first eight characters of the saved token's SHA-256 hash. 136 #: src/Admin/AdminSettings.php:293 137 #, php-format 138 msgid "Saved token fingerprint: %s" 139 msgstr "" 140 141 #: src/Admin/AdminSettings.php:297 142 msgid "No token set yet. Webhooks are rejected until one is saved." 143 msgstr "" 144 145 #. translators: %s is a link to the Ko-fi Webhooks Management page. 146 #: src/Admin/AdminSettings.php:307 147 #, php-format 148 msgid "This token is used to verify incoming webhook requests from Ko-fi. Paste the verification token from this page: %s and open the Advanced box. Only a hash of it is stored, so it cannot be shown again; to change it, paste the new one." 149 msgstr "" 150 151 #: src/Admin/AdminSettings.php:308 152 msgid "Ko-fi Webhooks Management" 153 msgstr "" 154 155 #: src/Admin/AdminSettings.php:322 156 msgid "Webhook URL:" 157 msgstr "" 158 159 #: src/Admin/AdminSettings.php:325 160 msgid "Copy to Clipboard" 161 msgstr "" 162 163 #: src/Admin/AdminSettings.php:327 164 msgid "Use this URL to configure your Ko-fi webhook." 165 msgstr "" 166 167 #. translators: %s is a human-readable time span, e.g. "3 days". 168 #: src/Admin/AdminSettings.php:372 169 #, php-format 170 msgid "Scheduled tasks are not running: role expiry and log cleanup are %s overdue. Make sure WP-Cron runs, or that a system cron calls wp-cron.php." 171 msgstr "" 172 173 #. translators: 1: email address, 2: comma-separated role names, or "no role". 174 #: src/Admin/AdminSettings.php:424 175 #, php-format 176 msgid "Members for Ko-fi: an account was created by Ko-fi's \"Send test\" button (%1$s, role: %2$s). It is not a real supporter; earlier versions treated tests as payments. You can safely delete it." 177 msgstr "" 178 179 #: src/Admin/AdminSettings.php:426 180 msgid "none" 181 msgstr "" 182 183 #: src/Admin/AdminSettings.php:430 184 msgid "Review the account" 185 msgstr "" 186 187 #: src/Admin/AdminSettings.php:440 188 msgid "Members for Ko-fi: two different verification tokens are stored, and webhooks are checked against the hashed one. Re-enter your Ko-fi verification token in the settings to resolve this." 189 msgstr "" 190 191 #: src/Admin/AdminSettings.php:465 192 msgid "Open settings" 193 msgstr "" 194 195 #: src/Admin/AdminSettings.php:480 196 msgid "Only assign roles to subscribers." 197 msgstr "" 198 199 #: src/Admin/AdminSettings.php:481 200 msgid "When enabled, users must be active subscribers to receive a role." 201 msgstr "" 202 203 #: src/Admin/AdminSettings.php:498 204 msgid "Ko-fi Tier" 205 msgstr "" 206 207 #: src/Admin/AdminSettings.php:498 208 #: src/Admin/AdminSettings.php:1101 320 #: src/Admin/LogsTab.php:245 321 msgid "the token doesn't match" 322 msgstr "" 323 324 #: src/Admin/LogsTab.php:247 325 msgid "no token is saved" 326 msgstr "" 327 328 #: src/Admin/LogsTab.php:252 329 msgid "the request was malformed" 330 msgstr "" 331 332 #: src/Admin/LogsTab.php:254 333 msgid "the account could not be created" 334 msgstr "" 335 336 #. translators: %s is a time of day. 337 #: src/Admin/LogsTab.php:292 338 #, php-format 339 msgid "Today, %s" 340 msgstr "" 341 342 #. translators: %s is a time of day. 343 #: src/Admin/LogsTab.php:297 344 #, php-format 345 msgid "Yesterday, %s" 346 msgstr "" 347 348 #: src/Admin/LogsTab.php:365 349 msgid "The log was deleted." 350 msgstr "" 351 352 #: src/Admin/LogsTab.php:383 353 msgid "Member activity" 354 msgstr "" 355 356 #: src/Admin/LogsTab.php:396 357 msgid "Requests from Ko-fi" 358 msgstr "" 359 360 #. translators: %s is the site's time zone, e.g. "Europe/Copenhagen". 361 #: src/Admin/LogsTab.php:401 362 #, php-format 363 msgid "Times are in the site's time zone (%s)." 364 msgstr "" 365 366 #: src/Admin/LogsTab.php:412 367 #: src/Admin/LogsTab.php:477 368 msgid "Outcome" 369 msgstr "" 370 371 #: src/Admin/LogsTab.php:414 372 msgid "All outcomes" 373 msgstr "" 374 375 #: src/Admin/LogsTab.php:415 376 #: src/Admin/LogsTab.php:547 377 msgid "Accepted" 378 msgstr "" 379 380 #: src/Admin/LogsTab.php:416 381 msgid "Refused" 382 msgstr "" 383 384 #: src/Admin/LogsTab.php:424 385 msgid "Search email, tier or reason" 386 msgstr "" 387 388 #: src/Admin/LogsTab.php:426 389 msgid "Search email, action or role" 390 msgstr "" 391 392 #: src/Admin/LogsTab.php:433 393 msgid "Per page" 394 msgstr "" 395 396 #: src/Admin/LogsTab.php:440 397 msgid "Filter" 398 msgstr "" 399 400 #: src/Admin/LogsTab.php:473 401 msgid "Received" 402 msgstr "" 403 404 #: src/Admin/LogsTab.php:474 405 #: src/Privacy/PersonalData.php:321 406 #: src/Webhook/Webhook.php:911 407 #: src/Webhook/Webhook.php:931 408 msgid "Supporter" 409 msgstr "" 410 411 #: src/Admin/LogsTab.php:475 412 msgid "Tier" 413 msgstr "" 414 415 #: src/Admin/LogsTab.php:476 416 msgid "Payment" 417 msgstr "" 418 419 #: src/Admin/LogsTab.php:478 420 msgid "Details" 421 msgstr "" 422 423 #: src/Admin/LogsTab.php:483 424 #: src/Admin/LogsTab.php:610 425 msgid "No entries match." 426 msgstr "" 427 428 #: src/Admin/LogsTab.php:483 429 #: src/Admin/LogsTab.php:610 430 msgid "No entries yet." 431 msgstr "" 432 433 #: src/Admin/LogsTab.php:504 434 msgid "Ko-fi test" 435 msgstr "" 436 437 #: src/Admin/LogsTab.php:506 438 msgid "Not stored" 439 msgstr "" 440 441 #: src/Admin/LogsTab.php:522 442 msgid "not mapped" 443 msgstr "" 444 445 #: src/Admin/LogsTab.php:530 446 msgid "Test, no account made" 447 msgstr "" 448 449 #. translators: %s is an amount with its currency, e.g. "5.00 EUR". 450 #: src/Admin/LogsTab.php:536 451 #, php-format 452 msgid "%s · subscription" 453 msgstr "" 454 455 #. translators: %s is an amount with its currency, e.g. "5.00 EUR". 456 #: src/Admin/LogsTab.php:538 457 #, php-format 458 msgid "%s · one-off" 459 msgstr "" 460 461 #. translators: %s is why the request was refused, e.g. "the token doesn't match". 462 #: src/Admin/LogsTab.php:552 463 #, php-format 464 msgid "Refused: %s" 465 msgstr "" 466 467 #: src/Admin/LogsTab.php:560 468 msgid "Payload" 469 msgstr "" 470 471 #: src/Admin/LogsTab.php:602 472 msgid "When" 473 msgstr "" 474 475 #: src/Admin/LogsTab.php:603 476 #: src/Admin/MembersTab.php:264 477 msgid "Member" 478 msgstr "" 479 480 #: src/Admin/LogsTab.php:604 481 msgid "What happened" 482 msgstr "" 483 484 #: src/Admin/LogsTab.php:605 485 #: src/Admin/MembersTab.php:265 209 486 msgid "Role" 210 487 msgstr "" 211 488 212 #: src/Admin/AdminSettings.php:536 213 msgid "Add Tier Mapping" 214 msgstr "" 215 216 #: src/Admin/AdminSettings.php:537 217 msgid "Map Ko-fi tiers to WordPress roles. Only exact tier names will be matched." 218 msgstr "" 219 220 #: src/Admin/AdminSettings.php:554 221 msgid "— No default —" 222 msgstr "" 223 224 #: src/Admin/AdminSettings.php:563 225 msgid "If no tier matches, or if you are not using tiers, this role will be assigned to the user." 226 msgstr "" 227 228 #: src/Admin/AdminSettings.php:578 229 msgid "If checked, role will be removed after the set number of days." 230 msgstr "" 231 232 #: src/Admin/AdminSettings.php:594 233 msgid "Number of days the role remains active (only used if expiry is enabled)." 234 msgstr "" 235 236 #: src/Admin/AdminSettings.php:610 237 msgid "Enable automatic deletion of old logs based on retention period." 238 msgstr "" 239 240 #: src/Admin/AdminSettings.php:622 241 msgid "Number of days to keep logs before automatic deletion (minimum 1 day)." 242 msgstr "" 243 244 #: src/Admin/AdminSettings.php:650 245 msgid "Members for Ko-fi Settings" 246 msgstr "" 247 248 #: src/Admin/AdminSettings.php:661 249 msgid "Settings" 250 msgstr "" 251 252 #: src/Admin/AdminSettings.php:664 253 #: src/Admin/AdminSettings.php:687 254 msgid "Logs" 255 msgstr "" 256 257 #: src/Admin/AdminSettings.php:732 258 msgid "Log Type:" 259 msgstr "" 260 261 #: src/Admin/AdminSettings.php:736 262 msgid "User Logs" 263 msgstr "" 264 265 #: src/Admin/AdminSettings.php:739 266 msgid "Request Logs" 267 msgstr "" 268 269 #: src/Admin/AdminSettings.php:759 270 msgid "Search" 271 msgstr "" 272 273 #: src/Admin/AdminSettings.php:760 274 msgid "Reset" 275 msgstr "" 276 277 #: src/Admin/AdminSettings.php:763 278 msgid "Refresh" 279 msgstr "" 280 281 #: src/Admin/AdminSettings.php:794 282 #: src/Admin/AdminSettings.php:830 283 #: src/Admin/AdminSettings.php:904 284 #: src/Admin/AdminSettings.php:933 285 #: src/Admin/AdminSettings.php:964 286 #: src/Admin/AdminSettings.php:987 489 #: src/Admin/LogsTab.php:651 490 msgid "First page" 491 msgstr "" 492 493 #: src/Admin/LogsTab.php:652 494 #: src/Admin/MembersTab.php:351 495 msgid "Previous page" 496 msgstr "" 497 498 #: src/Admin/LogsTab.php:653 499 #: src/Admin/MembersTab.php:362 500 msgid "Next page" 501 msgstr "" 502 503 #: src/Admin/LogsTab.php:654 504 msgid "Last page" 505 msgstr "" 506 507 #: src/Admin/LogsTab.php:665 508 msgid "Current page" 509 msgstr "" 510 511 #: src/Admin/LogsTab.php:676 512 #: src/Admin/MembersTab.php:349 513 msgid "Pages" 514 msgstr "" 515 516 #. translators: 1: an input holding the current page number, 2: the number of pages. 517 #. translators: 1: the current page number, 2: the number of pages. 518 #: src/Admin/LogsTab.php:683 519 #: src/Admin/MembersTab.php:356 520 #, php-format 521 msgid "Page %1$s of %2$s" 522 msgstr "" 523 524 #. translators: %s is the number of entries. 525 #: src/Admin/LogsTab.php:715 526 #, php-format 527 msgid "Removes %s request entry. Member activity is kept." 528 msgid_plural "Removes all %s request entries. Member activity is kept." 529 msgstr[0] "" 530 msgstr[1] "" 531 532 #. translators: %s is the number of entries. 533 #: src/Admin/LogsTab.php:717 534 #, php-format 535 msgid "Removes %s activity entry. The request log is kept." 536 msgid_plural "Removes all %s activity entries. The request log is kept." 537 msgstr[0] "" 538 msgstr[1] "" 539 540 #. translators: %s is the number of entries. 541 #: src/Admin/LogsTab.php:720 542 #, php-format 543 msgid "Delete %s entry in this log? This can't be undone." 544 msgid_plural "Delete all %s entries in this log? This can't be undone." 545 msgstr[0] "" 546 msgstr[1] "" 547 548 #: src/Admin/LogsTab.php:726 549 msgid "Delete this log" 550 msgstr "" 551 552 #. translators: %d is a number of days. 553 #: src/Admin/LogsTab.php:734 554 #, php-format 555 msgid "Entries older than %d day are deleted automatically." 556 msgid_plural "Entries older than %d days are deleted automatically." 557 msgstr[0] "" 558 msgstr[1] "" 559 560 #: src/Admin/LogsTab.php:747 561 msgid "Delete request log…" 562 msgstr "" 563 564 #: src/Admin/LogsTab.php:747 565 msgid "Delete activity log…" 566 msgstr "" 567 568 #: src/Admin/LogsTab.php:761 287 569 msgid "You do not have permission to access this resource." 288 570 msgstr "" 289 571 290 #: src/Admin/AdminSettings.php:803 291 msgid "Invalid page number." 292 msgstr "" 293 294 #: src/Admin/AdminSettings.php:863 295 msgid "Logs table does not exist." 296 msgstr "" 297 298 #: src/Admin/AdminSettings.php:1097 299 #: src/Admin/AdminSettings.php:1239 300 msgid "Timestamp" 301 msgstr "" 302 303 #: src/Admin/AdminSettings.php:1098 304 msgid "User ID" 305 msgstr "" 306 307 #: src/Admin/AdminSettings.php:1099 308 #: src/Admin/AdminSettings.php:1240 309 msgid "Email" 310 msgstr "" 311 312 #: src/Admin/AdminSettings.php:1100 313 msgid "Action" 314 msgstr "" 315 316 #: src/Admin/AdminSettings.php:1107 317 #: src/Admin/AdminSettings.php:1252 318 msgid "No logs available." 319 msgstr "" 320 321 #: src/Admin/AdminSettings.php:1129 322 #: src/Admin/AdminSettings.php:1277 323 msgid "Clear Logs" 324 msgstr "" 325 326 #: src/Admin/AdminSettings.php:1137 327 #: src/Admin/AdminSettings.php:1285 328 msgid "Rows per page:" 329 msgstr "" 330 331 #: src/Admin/AdminSettings.php:1152 332 #: src/Admin/AdminSettings.php:1300 333 msgid "Previous" 334 msgstr "" 335 336 #: src/Admin/AdminSettings.php:1159 337 #: src/Admin/AdminSettings.php:1307 338 msgid "Next" 339 msgstr "" 340 341 #: src/Admin/AdminSettings.php:1241 342 msgid "Tier Name" 343 msgstr "" 344 345 #: src/Admin/AdminSettings.php:1242 346 msgid "Amount" 347 msgstr "" 348 349 #: src/Admin/AdminSettings.php:1243 350 msgid "Subscription" 351 msgstr "" 352 353 #: src/Admin/AdminSettings.php:1244 572 #: src/Admin/MembersTab.php:195 573 msgid "Unknown" 574 msgstr "" 575 576 #: src/Admin/MembersTab.php:214 577 msgid "All" 578 msgstr "" 579 580 #: src/Admin/MembersTab.php:215 581 #: src/Admin/MembersTab.php:316 582 msgid "Active" 583 msgstr "" 584 585 #. translators: The members whose access ends within the next seven days. 586 #: src/Admin/MembersTab.php:217 587 msgid "Ending within 7 days" 588 msgstr "" 589 590 #: src/Admin/MembersTab.php:218 591 #: src/Admin/MembersTab.php:305 592 msgid "Ended" 593 msgstr "" 594 595 #: src/Admin/MembersTab.php:254 596 #: src/Admin/MembersTab.php:256 597 msgid "Search members" 598 msgstr "" 599 600 #: src/Admin/MembersTab.php:255 601 msgid "Name or email" 602 msgstr "" 603 604 #: src/Admin/MembersTab.php:266 354 605 msgid "Status" 355 606 msgstr "" 356 607 357 #: src/Admin/AdminSettings.php:1245 358 msgid "Success" 359 msgstr "" 360 361 #: src/Admin/AdminSettings.php:1246 362 msgid "Error" 363 msgstr "" 364 365 #: src/Admin/AdminSettings.php:1261 366 msgid "Yes" 367 msgstr "" 368 369 #: src/Admin/AdminSettings.php:1261 370 msgid "No" 608 #: src/Admin/MembersTab.php:267 609 msgid "Member since" 610 msgstr "" 611 612 #: src/Admin/MembersTab.php:268 613 msgid "Last payment" 614 msgstr "" 615 616 #: src/Admin/MembersTab.php:269 617 msgid "Access until" 618 msgstr "" 619 620 #: src/Admin/MembersTab.php:274 621 msgid "Nobody has become a member through Ko-fi yet." 622 msgstr "" 623 624 #: src/Admin/MembersTab.php:274 625 msgid "No members match." 626 msgstr "" 627 628 #. translators: %s is the name of a role that was removed. 629 #: src/Admin/MembersTab.php:293 630 #, php-format 631 msgid "%s, removed" 632 msgstr "" 633 634 #: src/Admin/MembersTab.php:297 635 msgid "Granted before payment; never removed" 636 msgstr "" 637 638 #. translators: %d is a number of days. 639 #: src/Admin/MembersTab.php:311 640 #, php-format 641 msgid "Ends in %d day" 642 msgid_plural "Ends in %d days" 643 msgstr[0] "" 644 msgstr[1] "" 645 646 #: src/Admin/MembersTab.php:325 647 msgid "subscription" 648 msgstr "" 649 650 #: src/Admin/MembersTab.php:325 651 msgid "one-off" 652 msgstr "" 653 654 #: src/Admin/MembersTab.php:332 655 msgid "No expiry" 656 msgstr "" 657 658 #. translators: %s is a date. 659 #: src/Admin/MembersTab.php:335 660 #, php-format 661 msgid "Ended %s" 662 msgstr "" 663 664 #: src/Admin/MembersTab.php:367 665 msgid "Cancelling happens on Ko-fi, not here." 371 666 msgstr "" 372 667 … … 408 703 msgstr "" 409 704 410 #: src/Privacy/PersonalData.php:24 5705 #: src/Privacy/PersonalData.php:242 411 706 msgid "An active Ko-fi membership was kept, so the person keeps the access they pay for. It is removed when the membership ends." 412 707 msgstr "" 413 708 414 #: src/Privacy/PersonalData.php:324 415 #: src/Webhook/Webhook.php:911 416 #: src/Webhook/Webhook.php:931 417 msgid "Supporter" 418 msgstr "" 419 420 #: src/Privacy/PersonalData.php:365 709 #: src/Privacy/PersonalData.php:362 421 710 msgid "When you support us on Ko-fi, Ko-fi sends this site your email address, the name and message you entered, the amount, and your membership tier. We use your email address to create or find your account here and give it the access your support pays for." 422 711 msgstr "" 423 712 424 #: src/Privacy/PersonalData.php:36 6713 #: src/Privacy/PersonalData.php:363 425 714 msgid "A record of each payment and of the access granted or removed is kept for the number of days set by the site owner, then deleted automatically when log cleanup is enabled. You can ask for a copy of this data, or for it to be erased, through the site's personal data request process." 426 715 msgstr "" -
members-for-kofi/trunk/members-for-kofi.php
r3796045 r3796384 25 25 * Plugin URI: https://github.com/trudslev/members-for-kofi 26 26 * Description: Integrate with Ko-fi to manage WordPress users or roles via webhook. 27 * Version: 1. 6.127 * Version: 1.7.0 28 28 * Author: Sune Adelowo Trudslev 29 29 * Author URI: https://foodgeek.io -
members-for-kofi/trunk/readme.txt
r3796045 r3796384 6 6 Tested up to: 7.1 7 7 Requires PHP: 7.4 8 Stable tag: 1. 6.18 Stable tag: 1.7.0 9 9 License: GPLv3 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 140 140 141 141 == Changelog == 142 143 = 1.7.0 = 144 * Redesign: The settings page is reorganised around how you set it up. "Ko-fi connection" walks through the three steps in Ko-fi's own order (copy the webhook URL, paste the token, press Send test) and shows whether Ko-fi's requests are arriving and being accepted. 145 * Feature: Tiers Ko-fi has sent that aren't mapped to a role are listed, and can be added with one click. 146 * Feature: A new Members tab lists everyone who has or had a membership through Ko-fi, with their role, status, member-since date, last payment and when access ends. It can show only active members, those ending within a week, or those that ended. 147 * Logs: The request log can show only refused requests, says in words why a request was refused, shows times in the site's time zone, links to the member's profile, and shows each request's payload. Paging no longer prints a button for every page. 148 * Logs: Deleting a log now says which log it empties and leaves the other one alone. 149 * Accessibility: Every checkbox and button has a label, and the copy button confirms in place instead of with a pop-up. 150 * Fix: The descriptions of "Subscriptions only" and tier matching were inaccurate; they now say what the settings do. 151 * Security: The log viewer no longer runs on AJAX endpoints, so the many low-privilege accounts this plugin creates have nothing of it to call. Viewing and deleting logs still require an administrator. 152 * Translation: Complete Danish translation of the new screens. 142 153 143 154 = 1.6.1 = … … 223 234 == Upgrade Notice == 224 235 236 = 1.7.0 = 237 A redesigned admin page: a connection status, a Members tab, and a log viewer that can show only refused requests. No settings change. 238 225 239 = 1.6.1 = 226 240 "Automatically Clear Logs" can now be switched off. If you unticked it before, untick it again and save after updating. -
members-for-kofi/trunk/src/Admin/AdminSettings.php
r3796045 r3796384 26 26 use MembersForKofi\Webhook\VerificationToken; 27 27 28 use function add_settings_section;29 use function add_settings_field;30 28 use function register_setting; 31 29 use function add_settings_error; … … 33 31 34 32 /** 35 * Handles the admin settings for the Members for Ko-fi plugin.33 * The plugin's admin page: Settings, Members and Logs tabs. 36 34 * 37 * This class is responsible for registering and rendering the settings 38 * for the plugin in the WordPress admin dashboard. 35 * Each tab is its own URL (`?page=members-for-kofi&tab=…`), so a tab or a 36 * filtered log can be linked and reloaded. The Members and Logs tabs live in 37 * MembersTab and LogsTab; this class renders the page, the Settings tab and 38 * the admin notices, and sanitizes the settings. 39 39 * 40 40 * @package MembersForKofi … … 43 43 44 44 /** 45 * Page sizes the log viewer offers. 46 * 47 * @var array<int> 48 */ 49 public const ROWS_PER_PAGE_OPTIONS = array( 10, 25, 50, 100 ); 50 51 /** 52 * Page size used when none is given, or when an invalid one is. 53 * 54 * @var int 55 */ 56 public const DEFAULT_ROWS_PER_PAGE = 10; 57 /** 58 * Resolve the current logs table name, handling legacy name mismatch. 59 * 60 * @return string Fully qualified table name with prefix. 61 */ 62 private function get_logs_table_name(): string { 63 global $wpdb; 64 return $wpdb->prefix . 'members_for_kofi_user_logs'; 65 } 66 67 /** 68 * Constructor for the AdminSettings class. 69 * 70 * Initializes the class by adding the necessary WordPress actions. 45 * The page's tabs, in order. 46 * 47 * @var array<int,string> 48 */ 49 public const TABS = array( 'settings', 'members', 'logs' ); 50 51 /** 52 * Registers the page's handlers and assets. 71 53 */ 72 54 public function __construct() { 73 // Register settings.74 add_action( 'admin_init', array( $this, 'register_settings' ) );75 76 // Enqueue admin scripts.77 55 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); 78 79 // Register AJAX handlers. 80 add_action( 'wp_ajax_members_for_kofi_pagination', array( $this, 'handle_pagination' ) ); 81 add_action( 'wp_ajax_members_for_kofi_clear_logs', array( $this, 'handle_clear_logs' ) ); 82 add_action( 'wp_ajax_members_for_kofi_update_rows_per_page', array( $this, 'handle_update_rows_per_page' ) ); 83 add_action( 'wp_ajax_members_for_kofi_filter_logs', array( $this, 'handle_filter_logs' ) ); 84 add_action( 'wp_ajax_members_for_kofi_refresh_logs', array( $this, 'handle_refresh_logs' ) ); 85 add_action( 'wp_ajax_members_for_kofi_switch_log_type', array( $this, 'handle_switch_log_type' ) ); 86 } 87 88 89 /** 90 * Registers the settings for the Members for Ko-fi plugin. 91 * 92 * This function sets up the settings sections and fields for the plugin 93 * in the WordPress admin dashboard. 56 LogsTab::register(); 57 } 58 59 /** 60 * Registers the plugin's setting and its sanitizer. 61 * 62 * The form is rendered by render_settings_tab(), not by Settings API 63 * sections, so only the setting itself is registered here. 94 64 * 95 65 * @return void … … 103 73 ) 104 74 ); 105 106 // Ko-fi Settings section. 107 add_settings_section( 'members_for_kofi_general', __( 'Ko-fi Settings', 'members-for-kofi' ), '__return_null', 'members-for-kofi' ); 108 add_settings_field( 'verification_token', __( 'Verification Token', 'members-for-kofi' ), array( $this, 'render_verification_token_field' ), 'members-for-kofi', 'members_for_kofi_general' ); 109 110 // Role Assignment section. 111 add_settings_section( 'members_for_kofi_role_assignment', __( 'Role Assignment', 'members-for-kofi' ), '__return_null', 'members-for-kofi' ); 112 add_settings_field( 'only_subscriptions', __( 'Only Accept Subscriptions', 'members-for-kofi' ), array( $this, 'render_only_subscriptions_field' ), 'members-for-kofi', 'members_for_kofi_role_assignment' ); 113 add_settings_field( 'tier_role_map', __( 'Tier to Role Mapping', 'members-for-kofi' ), array( $this, 'render_tier_role_map_field' ), 'members-for-kofi', 'members_for_kofi_role_assignment' ); 114 add_settings_field( 'default_role', __( 'Default Role (if no match)', 'members-for-kofi' ), array( $this, 'render_default_role_field' ), 'members-for-kofi', 'members_for_kofi_role_assignment' ); 115 add_settings_field( 'enable_expiry', __( 'Enable Expiry', 'members-for-kofi' ), array( $this, 'render_expiry_toggle_field' ), 'members-for-kofi', 'members_for_kofi_role_assignment' ); 116 add_settings_field( 'role_expiry_days', __( 'Role Expiry (days)', 'members-for-kofi' ), array( $this, 'render_role_expiry_field' ), 'members-for-kofi', 'members_for_kofi_role_assignment' ); 117 118 // Logging section. 119 add_settings_section( 'members_for_kofi_logging', __( 'Logging', 'members-for-kofi' ), '__return_null', 'members-for-kofi' ); 120 add_settings_field( 'auto_clear_logs', __( 'Automatically Clear Logs', 'members-for-kofi' ), array( $this, 'render_auto_clear_logs_field' ), 'members-for-kofi', 'members_for_kofi_logging' ); 121 add_settings_field( 'log_retention_days', __( 'Number of Days to Keep Logs', 'members-for-kofi' ), array( $this, 'render_log_retention_days_field' ), 'members-for-kofi', 'members_for_kofi_logging' ); 122 } /** 123 * Enqueues the admin JavaScript file for the Members for Ko-fi settings page. 124 * 125 * @return void 126 */ 75 } 76 77 /** 78 * Enqueues the page's script and stylesheet, on this page only. 79 * 80 * @return void 81 */ 127 82 public function enqueue_admin_scripts(): void { 128 83 $screen = get_current_screen(); 129 84 130 if ( isset( $screen->id ) && 'toplevel_page_members-for-kofi' === $screen->id ) { 131 wp_enqueue_script( 132 'members-for-kofi-admin-settings', 133 plugins_url( 'assets/js/admin-settings.js', dirname( __DIR__ ) ), 134 array(), 135 '1.0.6', 136 true 137 ); 138 139 wp_localize_script( 140 'members-for-kofi-admin-settings', 141 'kofiMembers', 142 array( 143 'ajaxurl' => admin_url( 'admin-ajax.php' ), 144 'paginationNonce' => wp_create_nonce( 'members_for_kofi_pagination' ), 145 'clearLogsNonce' => wp_create_nonce( 'members_for_kofi_clear_logs' ), 146 'rowsPerPageNonce' => wp_create_nonce( 'members_for_kofi_update_rows_per_page' ), 147 'filterNonce' => wp_create_nonce( 'members_for_kofi_filter_logs' ), 148 'refreshNonce' => wp_create_nonce( 'members_for_kofi_refresh_logs' ), 149 'switchLogTypeNonce' => wp_create_nonce( 'members_for_kofi_switch_log_type' ), 150 'clearLogsConfirm' => __( 'Are you sure you want to clear all logs? This action cannot be undone.', 'members-for-kofi' ), 151 'errorMessage' => __( 'An error occurred. Please try again.', 'members-for-kofi' ), 152 'searchLabelUser' => __( 'Search (email / action / role)', 'members-for-kofi' ), 153 'searchLabelRequest' => __( 'Search (email / tier / status)', 'members-for-kofi' ), 85 if ( ! isset( $screen->id ) || 'toplevel_page_members-for-kofi' !== $screen->id ) { 86 return; 87 } 88 89 $version = get_file_data( MEMBERS_FOR_KOFI_PLUGIN_FILE, array( 'Version' => 'Version' ) )['Version']; 90 91 wp_enqueue_style( 92 'members-for-kofi-admin', 93 plugins_url( 'assets/css/admin.css', dirname( __DIR__ ) ), 94 array(), 95 $version 96 ); 97 98 wp_enqueue_script( 99 'members-for-kofi-admin-settings', 100 plugins_url( 'assets/js/admin-settings.js', dirname( __DIR__ ) ), 101 array(), 102 $version, 103 true 104 ); 105 106 wp_localize_script( 107 'members-for-kofi-admin-settings', 108 'kofiMembers', 109 array( 110 'copied' => __( 'Copied', 'members-for-kofi' ), 111 ) 112 ); 113 } 114 115 /** 116 * The tab the URL asks for. 117 * 118 * @return string 119 */ 120 public static function current_tab(): string { 121 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Choosing which tab to display. 122 $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'settings'; 123 124 // The log viewer used to link to its own tab name. 125 if ( 'user_logs' === $tab ) { 126 return 'logs'; 127 } 128 129 return in_array( $tab, self::TABS, true ) ? $tab : 'settings'; 130 } 131 132 /** 133 * URL of one of the page's tabs. 134 * 135 * @param string $tab Tab key. 136 * @return string 137 */ 138 public static function tab_url( string $tab ): string { 139 $args = array( 'page' => 'members-for-kofi' ); 140 141 if ( 'settings' !== $tab ) { 142 $args['tab'] = $tab; 143 } 144 145 return add_query_arg( $args, admin_url( 'admin.php' ) ); 146 } 147 148 /** 149 * Renders the admin page. 150 * 151 * @return void 152 */ 153 public function render_settings_page(): void { 154 $active = self::current_tab(); 155 $labels = array( 156 'settings' => __( 'Settings', 'members-for-kofi' ), 157 'members' => __( 'Members', 'members-for-kofi' ), 158 'logs' => __( 'Logs', 'members-for-kofi' ), 159 ); 160 ?> 161 <div class="wrap mfk-wrap"> 162 <h1><?php esc_html_e( 'Members for Ko-fi', 'members-for-kofi' ); ?></h1> 163 <?php 164 // WordPress prints Settings API messages by itself only on pages 165 // under Settings. This is a top-level page, so without this call a 166 // rejected save ("Verification Token is required", a disallowed 167 // role) was silently dropped, and a good one got no confirmation. 168 settings_errors(); 169 $this->render_cron_warning(); 170 ?> 171 <nav class="nav-tab-wrapper" aria-label="<?php esc_attr_e( 'Members for Ko-fi sections', 'members-for-kofi' ); ?>"> 172 <?php foreach ( $labels as $tab => $label ) : ?> 173 <a href="<?php echo esc_url( self::tab_url( $tab ) ); ?>" class="nav-tab<?php echo $tab === $active ? ' nav-tab-active' : ''; ?>"<?php echo $tab === $active ? ' aria-current="page"' : ''; ?>><?php echo esc_html( $label ); ?></a> 174 <?php endforeach; ?> 175 </nav> 176 <?php 177 if ( 'members' === $active ) { 178 MembersTab::render(); 179 } elseif ( 'logs' === $active ) { 180 LogsTab::render(); 181 } else { 182 $this->render_settings_tab(); 183 } 184 ?> 185 </div> 186 <?php 187 } 188 189 /** 190 * Renders the Settings tab. 191 * 192 * @return void 193 */ 194 public function render_settings_tab(): void { 195 ?> 196 <form method="post" action="options.php" id="members-for-kofi-settings-form" class="mfk-settings"> 197 <?php settings_fields( 'members_for_kofi_options' ); ?> 198 199 <section class="mfk-card" aria-labelledby="mfk-connection"> 200 <div class="mfk-card-head"> 201 <h2 id="mfk-connection"><?php esc_html_e( 'Ko-fi connection', 'members-for-kofi' ); ?></h2> 202 <?php $this->render_connection_status(); ?> 203 </div> 204 <ol class="mfk-steps"> 205 <li><?php $this->render_webhook_url_field(); ?></li> 206 <li><?php $this->render_verification_token_field(); ?></li> 207 <li> 208 <p class="mfk-step-title"><?php esc_html_e( 'Press "Send test" on Ko-fi', 'members-for-kofi' ); ?></p> 209 <p class="description"> 210 <?php 211 printf( 212 // translators: %s is a link to the request log. 213 esc_html__( 'It shows up in the status line above and in %s. A test never creates an account.', 'members-for-kofi' ), 214 '<a href="' . esc_url( add_query_arg( 'log', 'requests', self::tab_url( 'logs' ) ) ) . '">' . esc_html__( 'the request log', 'members-for-kofi' ) . '</a>' 215 ); 216 ?> 217 </p> 218 </li> 219 </ol> 220 </section> 221 222 <section class="mfk-card mfk-card-body" aria-labelledby="mfk-roles"> 223 <h2 id="mfk-roles"><?php esc_html_e( 'Who gets a role', 'members-for-kofi' ); ?></h2> 224 <?php 225 $this->render_only_subscriptions_field(); 226 $this->render_tier_role_map_field(); 227 $this->render_default_role_field(); 228 ?> 229 </section> 230 231 <section class="mfk-card mfk-card-body" aria-labelledby="mfk-expiry"> 232 <h2 id="mfk-expiry"><?php esc_html_e( 'When membership ends', 'members-for-kofi' ); ?></h2> 233 <p class="mfk-sentence"> 234 <?php 235 $this->render_expiry_toggle_field(); 236 $this->render_role_expiry_field(); 237 ?> 238 </p> 239 <p class="description"><?php esc_html_e( 'Ko-fi subscriptions renew monthly, so 35 days leaves a few days\' grace. A role the user held before paying is never removed.', 'members-for-kofi' ); ?></p> 240 </section> 241 242 <section class="mfk-card mfk-card-body" aria-labelledby="mfk-log-history"> 243 <h2 id="mfk-log-history"><?php esc_html_e( 'Log history', 'members-for-kofi' ); ?></h2> 244 <p class="mfk-sentence"> 245 <?php 246 $this->render_auto_clear_logs_field(); 247 $this->render_log_retention_days_field(); 248 ?> 249 </p> 250 <p class="description"><?php esc_html_e( 'Untick to keep everything, for example when another tool reads the request log.', 'members-for-kofi' ); ?></p> 251 </section> 252 253 <?php submit_button(); ?> 254 </form> 255 <?php 256 } 257 258 /** 259 * Says whether Ko-fi's requests are arriving and being accepted. 260 * 261 * Based on the newest row of the request log, which also holds refused 262 * requests from anyone who finds the URL: a refusal says "the last 263 * request", not "Ko-fi". 264 * 265 * @return void 266 */ 267 public function render_connection_status(): void { 268 $options = get_option( VerificationToken::OPTION ); 269 270 if ( '' === VerificationToken::stored_hash( $options ) ) { 271 $class = 'mfk-status-error'; 272 $text = __( 'Not connected: no token is saved, so Ko-fi\'s requests are refused.', 'members-for-kofi' ); 273 } else { 274 $latest = LogsTab::latest_request(); 275 if ( null === $latest ) { 276 $class = 'mfk-status-waiting'; 277 $text = __( 'Waiting for Ko-fi: no request has arrived yet.', 'members-for-kofi' ); 278 } else { 279 $ago = human_time_diff( LogsTab::to_unix( (string) $latest['timestamp'] ) ); 280 if ( '1' === (string) $latest['success'] ) { 281 $class = 'mfk-status-ok'; 282 // translators: %s is a time span, e.g. "2 hours". 283 $text = sprintf( __( 'Working: the last request from Ko-fi, %s ago, was accepted.', 'members-for-kofi' ), $ago ); 284 } else { 285 $class = 'mfk-status-error'; 286 // translators: 1: a time span, e.g. "2 hours", 2: why it was refused, e.g. "the token doesn't match". 287 $text = sprintf( __( 'The last request, %1$s ago, was refused: %2$s', 'members-for-kofi' ), $ago, LogsTab::refusal_reason( (string) $latest['error'] ) ); 288 } 289 } 290 } 291 292 echo '<p class="mfk-status ' . esc_attr( $class ) . '">' . esc_html( $text ) . '</p>'; 293 } 294 295 /** 296 * Step 1: the webhook URL to give Ko-fi, with a copy button. 297 * 298 * @return void 299 */ 300 public function render_webhook_url_field(): void { 301 $link = '<a href="' . esc_url( 'https://ko-fi.com/manage/webhooks?src=sidemenu' ) . '" target="_blank" rel="noopener noreferrer">' 302 . esc_html__( 'Ko-fi\'s webhook settings', 'members-for-kofi' ) 303 . '<span class="screen-reader-text"> ' . esc_html__( '(opens in a new tab)', 'members-for-kofi' ) . '</span></a>'; 304 ?> 305 <label for="webhook-kofi-url" class="mfk-step-title"><?php esc_html_e( 'Give Ko-fi this webhook URL', 'members-for-kofi' ); ?></label> 306 <div class="mfk-copy-row"> 307 <input type="text" id="webhook-kofi-url" value="<?php echo esc_url( self::webhook_url() ); ?>" readonly class="regular-text code"> 308 <button type="button" id="copy-webhook-url" class="button"><?php esc_html_e( 'Copy URL', 'members-for-kofi' ); ?></button> 309 <span id="mfk-copy-status" class="mfk-copy-status" role="status"></span> 310 </div> 311 <p class="description"> 312 <?php 313 // translators: %s is a link to Ko-fi's webhook settings. 314 printf( esc_html__( 'Paste it into %s.', 'members-for-kofi' ), $link ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built from escaped parts above. 315 ?> 316 </p> 317 <?php 318 } 319 320 /** 321 * Step 2: the verification token. Write-only: the saved one is never shown. 322 * 323 * @return void 324 */ 325 public function render_verification_token_field(): void { 326 $options = get_option( 'members_for_kofi_options' ); 327 $fingerprint = VerificationToken::fingerprint( $options ); 328 ?> 329 <label for="mfk-token" class="mfk-step-title"><?php esc_html_e( 'Paste Ko-fi\'s verification token', 'members-for-kofi' ); ?></label> 330 <?php if ( '' !== $fingerprint ) : ?> 331 <p class="mfk-token-state"> 332 <?php esc_html_e( 'A token is saved', 'members-for-kofi' ); ?> 333 <code><?php echo esc_html( $fingerprint ); ?></code> 334 <span class="mfk-muted"><?php esc_html_e( 'fingerprint, to compare with another site', 'members-for-kofi' ); ?></span> 335 </p> 336 <?php else : ?> 337 <p class="mfk-token-state mfk-status-error"><?php esc_html_e( 'No token is saved yet.', 'members-for-kofi' ); ?></p> 338 <?php endif; ?> 339 <?php if ( VerificationToken::has_conflict( $options ) ) : ?> 340 <p class="mfk-status-error"><?php echo esc_html( self::token_conflict_message() ); ?></p> 341 <?php endif; ?> 342 <?php // Write-only: the saved token is never rendered back into the page, and only its hash is stored, so the value is always empty. ?> 343 <input type="password" id="mfk-token" name="members_for_kofi_options[verification_token]" value="" class="regular-text" autocomplete="new-password"<?php echo '' !== $fingerprint ? ' placeholder="' . esc_attr__( 'Leave blank to keep the saved token', 'members-for-kofi' ) . '"' : ''; ?>> 344 <p class="description"><?php esc_html_e( 'On Ko-fi\'s webhook page, open Advanced and copy the token. Only a hash is stored, so it can\'t be shown again.', 'members-for-kofi' ); ?></p> 345 <?php 346 } 347 348 /** 349 * Which payments grant a role. Radios, so both answers are always sent. 350 * 351 * @return void 352 */ 353 public static function render_only_subscriptions_field(): void { 354 $options = get_option( 'members_for_kofi_options', array() ); 355 $only = ! empty( $options['only_subscriptions'] ); 356 ?> 357 <fieldset class="mfk-field"> 358 <legend class="mfk-field-title"><?php esc_html_e( 'Payments that grant a role', 'members-for-kofi' ); ?></legend> 359 <label><input type="radio" name="members_for_kofi_options[only_subscriptions]" value="0"<?php checked( ! $only ); ?>> <?php esc_html_e( 'Donations and subscriptions', 'members-for-kofi' ); ?></label><br> 360 <label><input type="radio" name="members_for_kofi_options[only_subscriptions]" value="1"<?php checked( $only ); ?>> <?php esc_html_e( 'Subscriptions only', 'members-for-kofi' ); ?> <span class="mfk-muted"><?php esc_html_e( '(one-off donations are logged and ignored)', 'members-for-kofi' ); ?></span></label> 361 </fieldset> 362 <?php 363 } 364 365 /** 366 * Options for a role select: assignable roles only. 367 * 368 * @param string $selected Selected role slug. 369 * @return string Escaped `<option>` elements. 370 */ 371 private function role_options( string $selected ): string { 372 $html = ''; 373 374 foreach ( get_editable_roles() as $key => $details ) { 375 if ( ! Webhook::is_assignable_role( $key ) ) { 376 continue; 377 } 378 $html .= '<option value="' . esc_attr( $key ) . '"' . selected( $selected, $key, false ) . '>' . esc_html( translate_user_role( $details['name'] ) ) . '</option>'; 379 } 380 381 return $html; 382 } 383 384 /** 385 * One row of the tier table. 386 * 387 * @param string $tier Tier name. 388 * @param string $role Role slug. 389 * @return string 390 */ 391 private function tier_row( string $tier, string $role ): string { 392 return '<tr>' 393 . '<td><input type="text" name="members_for_kofi_options[tier_role_map][tier][]" value="' . esc_attr( $tier ) . '" class="regular-text tier-name-input" aria-label="' . esc_attr__( 'Tier name on Ko-fi', 'members-for-kofi' ) . '"></td>' 394 . '<td><select name="members_for_kofi_options[tier_role_map][role][]" class="tier-role-select" aria-label="' . esc_attr__( 'Role for this tier', 'members-for-kofi' ) . '">' . $this->role_options( $role ) . '</select></td>' 395 . '<td class="mfk-actions"><button type="button" class="button-link button-link-delete remove-tier-role-row" aria-label="' . esc_attr__( 'Remove this tier', 'members-for-kofi' ) . '">' . esc_html__( 'Remove', 'members-for-kofi' ) . '</button></td>' 396 . '</tr>'; 397 } 398 399 /** 400 * The tier table, with tiers Ko-fi sent that are not mapped yet. 401 * 402 * @return void 403 */ 404 public function render_tier_role_map_field(): void { 405 $options = get_option( 'members_for_kofi_options', array() ); 406 $map = isset( $options['tier_role_map'] ) && is_array( $options['tier_role_map'] ) ? $options['tier_role_map'] : array(); 407 408 $rows = ''; 409 foreach ( $map as $tier => $role ) { 410 $rows .= $this->tier_row( (string) $tier, (string) $role ); 411 } 412 413 $unmapped = LogsTab::unmapped_tiers( array_map( 'strval', array_keys( $map ) ) ); 414 ?> 415 <div class="mfk-field"> 416 <p class="mfk-field-title"><?php esc_html_e( 'Ko-fi tiers', 'members-for-kofi' ); ?></p> 417 <div class="mfk-table-wrap"> 418 <table id="tier-role-map-table" class="widefat mfk-table mfk-tier-table"> 419 <thead> 420 <tr> 421 <th scope="col"><?php esc_html_e( 'Tier name on Ko-fi', 'members-for-kofi' ); ?></th> 422 <th scope="col"><?php esc_html_e( 'Role it grants', 'members-for-kofi' ); ?></th> 423 <th scope="col"><span class="screen-reader-text"><?php esc_html_e( 'Actions', 'members-for-kofi' ); ?></span></th> 424 </tr> 425 </thead> 426 <tbody> 427 <?php echo $rows; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built by tier_row(), which escapes every value. ?> 428 </tbody> 429 </table> 430 </div> 431 <template id="tier-role-row-template"><?php echo $this->tier_row( '', '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in tier_row(). ?></template> 432 <p class="mfk-inline"> 433 <button type="button" class="button" id="add-tier-role-row"><?php esc_html_e( 'Add tier', 'members-for-kofi' ); ?></button> 434 <span class="mfk-muted"><?php esc_html_e( 'Names match whatever their capitalisation.', 'members-for-kofi' ); ?></span> 435 </p> 436 <?php if ( array() !== $unmapped ) : ?> 437 <div class="mfk-unmapped"> 438 <span><?php esc_html_e( 'Tiers Ko-fi sent that aren\'t listed:', 'members-for-kofi' ); ?></span> 439 <?php foreach ( $unmapped as $tier => $payments ) : ?> 440 <button type="button" class="button mfk-add-unmapped" data-tier="<?php echo esc_attr( $tier ); ?>"> 441 <?php 442 echo esc_html( 443 sprintf( 444 // translators: 1: a tier name, 2: how many payments named it. 445 _n( 'Add "%1$s" (%2$d payment)', 'Add "%1$s" (%2$d payments)', $payments, 'members-for-kofi' ), 446 $tier, 447 $payments 448 ) 449 ); 450 ?> 451 </button> 452 <?php endforeach; ?> 453 </div> 454 <?php endif; ?> 455 </div> 456 <?php 457 } 458 459 /** 460 * The role for payments no tier matches. 461 * 462 * @return void 463 */ 464 public function render_default_role_field(): void { 465 $options = get_option( 'members_for_kofi_options', array() ); 466 $selected = isset( $options['default_role'] ) && is_string( $options['default_role'] ) ? $options['default_role'] : ''; 467 ?> 468 <div class="mfk-field"> 469 <label for="mfk-default-role" class="mfk-field-title"><?php esc_html_e( 'Any other tier, or no tier', 'members-for-kofi' ); ?></label> 470 <select id="mfk-default-role" name="members_for_kofi_options[default_role]"> 471 <option value=""><?php esc_html_e( '— No role —', 'members-for-kofi' ); ?></option> 472 <?php echo $this->role_options( $selected ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped in role_options(). ?> 473 </select> 474 <p class="description"><?php esc_html_e( 'With "No role", those payers still get an account, but no role. Administrator and other roles that can run the site are never offered.', 'members-for-kofi' ); ?></p> 475 </div> 476 <?php 477 } 478 479 /** 480 * "Remove the role" checkbox: the start of the expiry sentence. 481 * 482 * @return void 483 */ 484 public static function render_expiry_toggle_field(): void { 485 $options = get_option( 'members_for_kofi_options', array() ); 486 $checked = ! empty( $options['enable_expiry'] ); 487 echo '<input type="hidden" name="members_for_kofi_options[enable_expiry]" value="0">'; 488 echo '<input type="checkbox" id="members_for_kofi_enable_expiry" name="members_for_kofi_options[enable_expiry]" value="1"' . checked( $checked, true, false ) . '> '; 489 echo '<label for="members_for_kofi_enable_expiry">' . esc_html__( 'Remove the role', 'members-for-kofi' ) . '</label> '; 490 } 491 492 /** 493 * "[35] days after the member's last payment": the end of the sentence. 494 * 495 * @return void 496 */ 497 public static function render_role_expiry_field(): void { 498 $options = get_option( 'members_for_kofi_options', array() ); 499 $value = isset( $options['role_expiry_days'] ) ? intval( $options['role_expiry_days'] ) : 35; 500 echo '<label for="members_for_kofi_expiry_days" class="screen-reader-text">' . esc_html__( 'Days after the last payment', 'members-for-kofi' ) . '</label>'; 501 echo '<input type="number" id="members_for_kofi_expiry_days" name="members_for_kofi_options[role_expiry_days]" value="' . esc_attr( (string) $value ) . '" min="1" class="small-text"> '; 502 echo '<span>' . esc_html__( 'days after the member\'s last payment', 'members-for-kofi' ) . '</span>'; 503 } 504 505 /** 506 * "Delete log entries older than" checkbox. 507 * 508 * @return void 509 */ 510 public static function render_auto_clear_logs_field(): void { 511 $options = get_option( 'members_for_kofi_options', array() ); 512 $checked = isset( $options['auto_clear_logs'] ) ? (bool) $options['auto_clear_logs'] : true; 513 // A browser sends nothing for an unchecked box, and the sanitizer reads a 514 // missing value as on (the default for installs that predate the 515 // setting). Without this hidden "0", unticking the box never stuck. 516 echo '<input type="hidden" name="members_for_kofi_options[auto_clear_logs]" value="0">'; 517 echo '<input type="checkbox" id="members_for_kofi_auto_clear_logs" name="members_for_kofi_options[auto_clear_logs]" value="1"' . checked( $checked, true, false ) . '> '; 518 echo '<label for="members_for_kofi_auto_clear_logs">' . esc_html__( 'Delete log entries older than', 'members-for-kofi' ) . '</label> '; 519 } 520 521 /** 522 * "[30] days": the end of the log sentence. 523 * 524 * @return void 525 */ 526 public static function render_log_retention_days_field(): void { 527 $options = get_option( 'members_for_kofi_options', array() ); 528 $value = isset( $options['log_retention_days'] ) ? intval( $options['log_retention_days'] ) : 30; 529 echo '<label for="members_for_kofi_log_retention_days" class="screen-reader-text">' . esc_html__( 'Number of Days to Keep Logs', 'members-for-kofi' ) . '</label>'; 530 echo '<input type="number" id="members_for_kofi_log_retention_days" name="members_for_kofi_options[log_retention_days]" value="' . esc_attr( (string) $value ) . '" min="1" class="small-text"> '; 531 echo '<span>' . esc_html__( 'days', 'members-for-kofi' ) . '</span>'; 532 } 533 534 /** 535 * The webhook address to give Ko-fi. 536 * 537 * `/webhook-kofi/` needs pretty permalinks to reach WordPress at all on 538 * some servers; the query form works everywhere, so it is what a site on 539 * plain permalinks is shown. 540 * 541 * @return string 542 */ 543 public static function webhook_url(): string { 544 if ( '' === (string) get_option( 'permalink_structure' ) ) { 545 return add_query_arg( 'kofi_webhook', '1', home_url( '/' ) ); 546 } 547 548 return home_url( '/webhook-kofi/' ); 549 } 550 551 /** 552 * Warns when the plugin's scheduled tasks are not running. 553 * 554 * With WP-Cron disabled and no system cron, roles never expire -- free 555 * access for anyone who ever paid -- and logs are never pruned, with 556 * nothing to say so. An event more than a day overdue is that evidence. 557 * 558 * @return void 559 */ 560 public function render_cron_warning(): void { 561 $options = get_option( 'members_for_kofi_options', array() ); 562 $hooks = array( 'kofi_members_cleanup_logs' ); 563 564 if ( ! empty( $options['enable_expiry'] ?? true ) ) { 565 $hooks[] = 'kofi_members_check_expired_roles'; 566 } 567 568 foreach ( $hooks as $hook ) { 569 $next = wp_next_scheduled( $hook ); 570 571 if ( false !== $next && $next < time() - DAY_IN_SECONDS ) { 572 echo '<div class="notice notice-warning"><p>' . esc_html( 573 sprintf( 574 // translators: %s is a human-readable time span, e.g. "3 days". 575 __( 'Scheduled tasks are not running: role expiry and log cleanup are %s overdue. Make sure WP-Cron runs, or that a system cron calls wp-cron.php.', 'members-for-kofi' ), 576 human_time_diff( $next ) 577 ) 578 ) . '</p></div>'; 579 return; 580 } 581 } 582 } 583 584 /** 585 * Points administrators at an account left behind by Ko-fi's test button. 586 * 587 * Before 1.3.0 a "Send test" webhook was processed like a payment, so a 588 * site that ever pressed it has a "Jo Example" account, usually holding the 589 * membership role. Shown where an administrator would see and act on it. 590 * 591 * @param string|null $screen_id Screen to check; defaults to the current one. 592 * @return void 593 */ 594 public static function render_test_account_notice( $screen_id = null ): void { 595 if ( ! current_user_can( 'delete_users' ) ) { 596 return; 597 } 598 599 // do_action( 'admin_notices' ) hands every callback an empty string, so 600 // '' has to mean "the current screen" just like no argument does. 601 if ( ! is_string( $screen_id ) || '' === $screen_id ) { 602 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; 603 $screen_id = $screen ? (string) $screen->id : ''; 604 } 605 606 if ( ! in_array( $screen_id, array( 'dashboard', 'users', 'toplevel_page_members-for-kofi' ), true ) ) { 607 return; 608 } 609 610 $user = get_user_by( 'email', Webhook::KOFI_TEST_EMAIL ); 611 612 if ( ! $user ) { 613 return; 614 } 615 616 $names = wp_roles()->get_names(); 617 $roles = array(); 618 foreach ( $user->roles as $slug ) { 619 $roles[] = isset( $names[ $slug ] ) ? translate_user_role( $names[ $slug ] ) : $slug; 620 } 621 622 printf( 623 '<div class="notice notice-warning"><p>%s <a href="%s">%s</a></p></div>', 624 esc_html( 625 sprintf( 626 // translators: 1: email address, 2: comma-separated role names, or "no role". 627 __( 'Members for Ko-fi: an account was created by Ko-fi\'s "Send test" button (%1$s, role: %2$s). It is not a real supporter; earlier versions treated tests as payments. You can safely delete it.', 'members-for-kofi' ), 628 Webhook::KOFI_TEST_EMAIL, 629 array() !== $roles ? implode( ', ', $roles ) : __( 'none', 'members-for-kofi' ) 154 630 ) 155 ); 156 } 631 ), 632 esc_url( get_edit_user_link( $user->ID ) ), 633 esc_html__( 'Review the account', 'members-for-kofi' ) 634 ); 635 } 636 637 /** 638 * Explains a stored hash and plaintext token that disagree. 639 * 640 * @return string 641 */ 642 private static function token_conflict_message(): string { 643 return __( 'Members for Ko-fi: two different verification tokens are stored, and webhooks are checked against the hashed one. Re-enter your Ko-fi verification token in the settings to resolve this.', 'members-for-kofi' ); 644 } 645 646 /** 647 * Warns administrators, on every admin screen, about conflicting tokens. 648 * 649 * The site is verifying against a hash nobody can inspect, and the debug 650 * log is off on most sites, so this is the only place it will be noticed. 651 * Saving a token clears it: the settings never store the plaintext key. 652 * 653 * @return void 654 */ 655 public static function render_token_conflict_notice(): void { 656 if ( ! current_user_can( 'manage_options' ) ) { 657 return; 658 } 659 660 if ( ! VerificationToken::has_conflict( get_option( VerificationToken::OPTION ) ) ) { 661 return; 662 } 663 664 printf( 665 '<div class="notice notice-error"><p>%s <a href="%s">%s</a></p></div>', 666 esc_html( self::token_conflict_message() ), 667 esc_url( admin_url( 'admin.php?page=members-for-kofi' ) ), 668 esc_html__( 'Open settings', 'members-for-kofi' ) 669 ); 157 670 } 158 671 … … 194 707 } 195 708 196 if ( ! $enable_expiry ) {197 // Preserve existing stored value if expiry disabled, fallback to default.709 if ( ! $enable_expiry && empty( $role_expiry_days ) ) { 710 // Expiry is off and no usable number was sent: keep the stored one. 198 711 $role_expiry_days = $stored['role_expiry_days'] ?? 35; 199 712 } … … 269 782 'log_retention_days' => $log_retention_days, 270 783 ); 271 } /**272 * Renders the verification token field in the settings page.273 *274 * This function outputs the HTML for the verification token input field,275 * allowing users to enter the token required for webhook verification.276 *277 * @return void278 */279 public function render_verification_token_field(): void {280 $options = get_option( 'members_for_kofi_options' );281 $webhook_url = self::webhook_url();282 283 $fingerprint = VerificationToken::fingerprint( $options );284 285 // Write-only: the saved token is never rendered back into the page, and286 // only its hash is stored, so the value is always empty.287 echo '<input type="password" name="members_for_kofi_options[verification_token]" value="" class="regular-text" autocomplete="new-password">';288 289 if ( '' !== $fingerprint ) {290 echo '<p class="description">' . esc_html__( 'A token is saved. Leave this blank to keep it, or paste a new one to replace it.', 'members-for-kofi' ) . '</p>';291 echo '<p class="description">' . sprintf(292 // translators: %s is the first eight characters of the saved token's SHA-256 hash.293 esc_html__( 'Saved token fingerprint: %s', 'members-for-kofi' ),294 '<code>' . esc_html( $fingerprint ) . '</code>'295 ) . '</p>';296 } else {297 echo '<p class="description">' . esc_html__( 'No token set yet. Webhooks are rejected until one is saved.', 'members-for-kofi' ) . '</p>';298 }299 300 if ( VerificationToken::has_conflict( $options ) ) {301 echo '<p class="description" style="color: #b32d2e;">' . esc_html( self::token_conflict_message() ) . '</p>';302 }303 304 // Description for Verification Token.305 $description = sprintf(306 // translators: %s is a link to the Ko-fi Webhooks Management page.307 esc_html__( 'This token is used to verify incoming webhook requests from Ko-fi. Paste the verification token from this page: %s and open the Advanced box. Only a hash of it is stored, so it cannot be shown again; to change it, paste the new one.', 'members-for-kofi' ),308 '<a href="' . esc_url( 'https://ko-fi.com/manage/webhooks?src=sidemenu' ) . '" target="_blank">' . esc_html__( 'Ko-fi Webhooks Management', 'members-for-kofi' ) . '</a>'309 );310 echo '<p class="description">' . wp_kses(311 $description,312 array(313 'a' => array(314 'href' => array(),315 'target' => array(),316 ),317 )318 ) . '</p>';319 320 // Webhook URL Textbox and Copy Button.321 echo '<div style="margin-top: 20px;">';322 echo '<label for="webhook-kofi-url" style="font-weight: bold; display: block; margin-bottom: 5px;">' . esc_html__( 'Webhook URL:', 'members-for-kofi' ) . '</label>';323 echo '<div style="display: flex; align-items: center; width: 500px;">';324 echo '<input type="text" id="webhook-kofi-url" value="' . esc_url( $webhook_url ) . '" readonly style="flex: 1; margin-right: 10px;" class="regular-text">';325 echo '<button type="button" id="copy-webhook-url" class="button button-secondary">' . esc_html__( 'Copy to Clipboard', 'members-for-kofi' ) . '</button>';326 echo '</div>';327 echo '<p class="description">' . esc_html__( 'Use this URL to configure your Ko-fi webhook.', 'members-for-kofi' ) . '</p>';328 echo '</div>';329 }330 331 /**332 * The webhook address to give Ko-fi.333 *334 * `/webhook-kofi/` needs pretty permalinks to reach WordPress at all on335 * some servers; the query form works everywhere, so it is what a site on336 * plain permalinks is shown.337 *338 * @return string339 */340 public static function webhook_url(): string {341 if ( '' === (string) get_option( 'permalink_structure' ) ) {342 return add_query_arg( 'kofi_webhook', '1', home_url( '/' ) );343 }344 345 return home_url( '/webhook-kofi/' );346 }347 348 /**349 * Warns when the plugin's scheduled tasks are not running.350 *351 * With WP-Cron disabled and no system cron, roles never expire -- free352 * access for anyone who ever paid -- and logs are never pruned, with353 * nothing to say so. An event more than a day overdue is that evidence.354 *355 * @return void356 */357 public function render_cron_warning(): void {358 $options = get_option( 'members_for_kofi_options', array() );359 $hooks = array( 'kofi_members_cleanup_logs' );360 361 if ( ! empty( $options['enable_expiry'] ?? true ) ) {362 $hooks[] = 'kofi_members_check_expired_roles';363 }364 365 foreach ( $hooks as $hook ) {366 $next = wp_next_scheduled( $hook );367 368 if ( false !== $next && $next < time() - DAY_IN_SECONDS ) {369 echo '<div class="notice notice-warning"><p>' . esc_html(370 sprintf(371 // translators: %s is a human-readable time span, e.g. "3 days".372 __( 'Scheduled tasks are not running: role expiry and log cleanup are %s overdue. Make sure WP-Cron runs, or that a system cron calls wp-cron.php.', 'members-for-kofi' ),373 human_time_diff( $next )374 )375 ) . '</p></div>';376 return;377 }378 }379 }380 381 /**382 * Points administrators at an account left behind by Ko-fi's test button.383 *384 * Before 1.3.0 a "Send test" webhook was processed like a payment, so a385 * site that ever pressed it has a "Jo Example" account, usually holding the386 * membership role. Shown where an administrator would see and act on it.387 *388 * @param string|null $screen_id Screen to check; defaults to the current one.389 * @return void390 */391 public static function render_test_account_notice( $screen_id = null ): void {392 if ( ! current_user_can( 'delete_users' ) ) {393 return;394 }395 396 // do_action( 'admin_notices' ) hands every callback an empty string, so397 // '' has to mean "the current screen" just like no argument does.398 if ( ! is_string( $screen_id ) || '' === $screen_id ) {399 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;400 $screen_id = $screen ? (string) $screen->id : '';401 }402 403 if ( ! in_array( $screen_id, array( 'dashboard', 'users', 'toplevel_page_members-for-kofi' ), true ) ) {404 return;405 }406 407 $user = get_user_by( 'email', Webhook::KOFI_TEST_EMAIL );408 409 if ( ! $user ) {410 return;411 }412 413 $names = wp_roles()->get_names();414 $roles = array();415 foreach ( $user->roles as $slug ) {416 $roles[] = isset( $names[ $slug ] ) ? translate_user_role( $names[ $slug ] ) : $slug;417 }418 419 printf(420 '<div class="notice notice-warning"><p>%s <a href="%s">%s</a></p></div>',421 esc_html(422 sprintf(423 // translators: 1: email address, 2: comma-separated role names, or "no role".424 __( 'Members for Ko-fi: an account was created by Ko-fi\'s "Send test" button (%1$s, role: %2$s). It is not a real supporter; earlier versions treated tests as payments. You can safely delete it.', 'members-for-kofi' ),425 Webhook::KOFI_TEST_EMAIL,426 array() !== $roles ? implode( ', ', $roles ) : __( 'none', 'members-for-kofi' )427 )428 ),429 esc_url( get_edit_user_link( $user->ID ) ),430 esc_html__( 'Review the account', 'members-for-kofi' )431 );432 }433 434 /**435 * Explains a stored hash and plaintext token that disagree.436 *437 * @return string438 */439 private static function token_conflict_message(): string {440 return __( 'Members for Ko-fi: two different verification tokens are stored, and webhooks are checked against the hashed one. Re-enter your Ko-fi verification token in the settings to resolve this.', 'members-for-kofi' );441 }442 443 /**444 * Warns administrators, on every admin screen, about conflicting tokens.445 *446 * The site is verifying against a hash nobody can inspect, and the debug447 * log is off on most sites, so this is the only place it will be noticed.448 * Saving a token clears it: the settings never store the plaintext key.449 *450 * @return void451 */452 public static function render_token_conflict_notice(): void {453 if ( ! current_user_can( 'manage_options' ) ) {454 return;455 }456 457 if ( ! VerificationToken::has_conflict( get_option( VerificationToken::OPTION ) ) ) {458 return;459 }460 461 printf(462 '<div class="notice notice-error"><p>%s <a href="%s">%s</a></p></div>',463 esc_html( self::token_conflict_message() ),464 esc_url( admin_url( 'admin.php?page=members-for-kofi' ) ),465 esc_html__( 'Open settings', 'members-for-kofi' )466 );467 }468 469 /**470 * Renders the "Only Accept Subscriptions" field in the settings page.471 *472 * This function outputs the HTML for the checkbox that allows users473 * to enable or disable role assignment only for active subscribers.474 *475 * @return void476 */477 public static function render_only_subscriptions_field(): void {478 $options = get_option( 'members_for_kofi_options', array() );479 $checked = ! empty( $options['only_subscriptions'] );480 echo '<label><input type="checkbox" name="members_for_kofi_options[only_subscriptions]" value="1" ' . checked( $checked, true, false ) . '> ' . esc_html__( 'Only assign roles to subscribers.', 'members-for-kofi' ) . '</label>';481 echo '<p class="description">' . esc_html__( 'When enabled, users must be active subscribers to receive a role.', 'members-for-kofi' ) . '</p>';482 }483 484 /**485 * Renders the Tier to Role Mapping field in the settings page.486 *487 * This function outputs the HTML for mapping Ko-fi tiers to WordPress roles,488 * allowing users to assign roles based on Ko-fi tier names.489 *490 * @return void491 */492 public function render_tier_role_map_field(): void {493 $options = get_option( 'members_for_kofi_options', array() );494 $map = isset( $options['tier_role_map'] ) && is_array( $options['tier_role_map'] ) ? $options['tier_role_map'] : array();495 $roles = get_editable_roles();496 497 echo '<table id="tier-role-map-table" class="form-table" style="margin-top:0">';498 echo '<thead><tr><th>' . esc_html__( 'Ko-fi Tier', 'members-for-kofi' ) . '</th><th>' . esc_html__( 'Role', 'members-for-kofi' ) . '</th><th></th></tr></thead><tbody>';499 500 if ( ! empty( $map ) ) {501 foreach ( $map as $tier => $role ) {502 $tier_val = esc_attr( $tier );503 $role_val = esc_attr( $role );504 echo '<tr>';505 echo '<td><input type="text" name="members_for_kofi_options[tier_role_map][tier][]" value="' . esc_attr( $tier_val ) . '" class="regular-text"></td>';506 echo '<td><select name="members_for_kofi_options[tier_role_map][role][]" class="regular-text">';507 foreach ( $roles as $key => $details ) {508 if ( ! Webhook::is_assignable_role( $key ) ) {509 continue;510 }511 echo '<option value="' . esc_attr( $key ) . '"' . selected( $role_val, $key, false ) . '>' . esc_html( $details['name'] ) . '</option>';512 }513 echo '</select></td>';514 echo '<td><button type="button" class="button remove-tier-role-row">×</button></td>';515 echo '</tr>';516 }517 }518 519 echo '</tbody></table>';520 521 echo '<template id="tier-role-row-template">';522 echo '<tr>';523 echo '<td><input type="text" name="members_for_kofi_options[tier_role_map][tier][]" class="regular-text tier-name-input"></td>';524 echo '<td><select name="members_for_kofi_options[tier_role_map][role][]" class="regular-text tier-role-select">';525 foreach ( $roles as $key => $details ) {526 if ( ! Webhook::is_assignable_role( $key ) ) {527 continue;528 }529 echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $details['name'] ) . '</option>';530 }531 echo '</select></td>';532 echo '<td><button type="button" class="button remove-tier-role-row">×</button></td>';533 echo '</tr>';534 echo '</template>';535 536 echo '<p><button type="button" class="button" id="add-tier-role-row">' . esc_html__( 'Add Tier Mapping', 'members-for-kofi' ) . '</button></p>';537 echo '<p><em>' . esc_html__( 'Map Ko-fi tiers to WordPress roles. Only exact tier names will be matched.', 'members-for-kofi' ) . '</em></p>';538 }539 540 /**541 * Renders the default role field in the settings page.542 *543 * This function outputs the HTML for selecting a default WordPress role544 * to assign to users when no tier matches or tiers are not used.545 *546 * @return void547 */548 public function render_default_role_field(): void {549 $options = get_option( 'members_for_kofi_options', array() );550 $roles = get_editable_roles();551 $selected = $options['default_role'] ?? array();552 553 echo '<select name="members_for_kofi_options[default_role]">';554 echo '<option value="">' . esc_html__( '— No default —', 'members-for-kofi' ) . '</option>';555 foreach ( $roles as $role_key => $role_details ) {556 // Skip disallowed roles (e.g., administrator) for security.557 if ( ! Webhook::is_assignable_role( $role_key ) ) {558 continue;559 }560 echo '<option value="' . esc_attr( $role_key ) . '"' . selected( $role_key, $selected, false ) . '>' . esc_html( $role_details['name'] ) . '</option>';561 }562 echo '</select>';563 echo '<p class="description">' . esc_html__( 'If no tier matches, or if you are not using tiers, this role will be assigned to the user.', 'members-for-kofi' ) . '</p>';564 }565 566 /**567 * Renders the "Enable Expiry" toggle field in the settings page.568 *569 * This function outputs the HTML for the checkbox that allows users570 * to enable or disable role expiry after a set number of days.571 *572 * @return void573 */574 public static function render_expiry_toggle_field(): void {575 $options = get_option( 'members_for_kofi_options', array() );576 $checked = ! empty( $options['enable_expiry'] );577 echo '<label><input type="checkbox" name="members_for_kofi_options[enable_expiry]" value="1" ' . checked( $checked, true, false ) . '></label>';578 echo '<p class="description">' . esc_html__( 'If checked, role will be removed after the set number of days.', 'members-for-kofi' ) . '</p>';579 }580 581 /**582 * Renders the role expiry field in the settings page.583 *584 * This function outputs the HTML for the input field that allows users585 * to specify the number of days a role remains active for non-subscribers.586 *587 * @return void588 */589 public static function render_role_expiry_field(): void {590 $options = get_option( 'members_for_kofi_options', array() );591 $value = isset( $options['role_expiry_days'] ) ? intval( $options['role_expiry_days'] ) : 35;592 $expiry_enable = ! empty( $options['enable_expiry'] );593 echo '<input type="number" id="members_for_kofi_expiry_days" name="members_for_kofi_options[role_expiry_days]" value="' . esc_attr( $value ) . '" min="1" ' . disabled( ! $expiry_enable, true, false ) . ' class="small-text">';594 echo '<p class="description">' . esc_html__( 'Number of days the role remains active (only used if expiry is enabled).', 'members-for-kofi' ) . '</p>';595 }596 597 /**598 * Renders the auto clear logs checkbox field.599 *600 * @return void601 */602 public static function render_auto_clear_logs_field(): void {603 $options = get_option( 'members_for_kofi_options', array() );604 $checked = isset( $options['auto_clear_logs'] ) ? (bool) $options['auto_clear_logs'] : true;605 // A browser sends nothing for an unchecked box, and the sanitizer reads a606 // missing value as on (the default for installs that predate the607 // setting). Without this hidden "0", unticking the box never stuck.608 echo '<input type="hidden" name="members_for_kofi_options[auto_clear_logs]" value="0">';609 echo '<input type="checkbox" id="members_for_kofi_auto_clear_logs" name="members_for_kofi_options[auto_clear_logs]" value="1" ' . checked( $checked, true, false ) . '>';610 echo '<p class="description">' . esc_html__( 'Enable automatic deletion of old logs based on retention period.', 'members-for-kofi' ) . '</p>';611 }612 613 /**614 * Renders the log retention days field.615 *616 * @return void617 */618 public static function render_log_retention_days_field(): void {619 $options = get_option( 'members_for_kofi_options', array() );620 $value = isset( $options['log_retention_days'] ) ? intval( $options['log_retention_days'] ) : 30;621 echo '<input type="number" id="members_for_kofi_log_retention_days" name="members_for_kofi_options[log_retention_days]" value="' . esc_attr( $value ) . '" min="1" class="small-text">';622 echo '<p class="description">' . esc_html__( 'Number of days to keep logs before automatic deletion (minimum 1 day).', 'members-for-kofi' ) . '</p>';623 }624 625 /**626 * Renders the log level field in the settings page.627 *628 * This function outputs the HTML for selecting the minimum severity629 * of log messages to record in the plugin's log file.630 *631 * @return void632 */633 // Deprecated: log level field removed with lightweight logger; method retained intentionally removed.634 635 /**636 * Renders the settings page for the Members for Ko-fi plugin.637 * This function outputs the HTML for the settings page in the WordPress admin dashboard,638 * allowing users to configure the plugin's settings.639 *640 * @return void641 */642 public function render_settings_page(): void {643 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading tab from URL for display only644 $active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'settings';645 if ( ! in_array( $active_tab, array( 'settings', 'logs' ), true ) ) {646 $active_tab = 'settings';647 }648 ?>649 <div class="wrap">650 <h1><?php esc_html_e( 'Members for Ko-fi Settings', 'members-for-kofi' ); ?></h1>651 <?php652 // WordPress prints Settings API messages by itself only on pages653 // under Settings. This is a top-level page, so without this call a654 // rejected save ("Verification Token is required", a disallowed655 // role) was silently dropped, and a good one got no confirmation.656 settings_errors();657 $this->render_cron_warning();658 ?>659 <h2 class="nav-tab-wrapper" role="tablist">660 <a href="#" role="tab" data-tab="settings" aria-selected="<?php echo 'settings' === $active_tab ? 'true' : 'false'; ?>" class="nav-tab <?php echo 'settings' === $active_tab ? 'nav-tab-active' : ''; ?>">661 <?php esc_html_e( 'Settings', 'members-for-kofi' ); ?>662 </a>663 <a href="#" role="tab" data-tab="logs" aria-selected="<?php echo 'logs' === $active_tab ? 'true' : 'false'; ?>" class="nav-tab <?php echo 'logs' === $active_tab ? 'nav-tab-active' : ''; ?>">664 <?php esc_html_e( 'Logs', 'members-for-kofi' ); ?>665 </a>666 </h2>667 <input type="hidden" id="active_tab" value="<?php echo esc_attr( $active_tab ); ?>" />668 <form method="post" action="options.php" id="members-for-kofi-settings-form">669 <?php settings_fields( 'members_for_kofi_options' ); ?>670 <div id="members-for-kofi-tab-settings" class="members-for-kofi-tab" style="<?php echo 'settings' === $active_tab ? '' : 'display:none;'; ?>">671 <h2><?php esc_html_e( 'Ko-fi Settings', 'members-for-kofi' ); ?></h2>672 <table class="form-table">673 <?php do_settings_fields( 'members-for-kofi', 'members_for_kofi_general' ); ?>674 </table>675 <h2><?php esc_html_e( 'Role Assignment', 'members-for-kofi' ); ?></h2>676 <table class="form-table">677 <?php do_settings_fields( 'members-for-kofi', 'members_for_kofi_role_assignment' ); ?>678 </table>679 <h2><?php esc_html_e( 'Logging', 'members-for-kofi' ); ?></h2>680 <table class="form-table">681 <?php do_settings_fields( 'members-for-kofi', 'members_for_kofi_logging' ); ?>682 </table>683 <?php submit_button(); ?>684 </div>685 </form>686 <div id="members-for-kofi-tab-logs" class="members-for-kofi-tab" style="<?php echo 'logs' === $active_tab ? '' : 'display:none;'; ?>">687 <h2><?php esc_html_e( 'Logs', 'members-for-kofi' ); ?></h2>688 <?php $this->render_logs_tab(); ?>689 </div>690 </div>691 <?php692 }693 694 /**695 * Renders the "Logs" tab in the settings page.696 *697 * This function outputs the HTML for displaying logs with a dropdown to switch between user and request logs.698 *699 * @param int|null $paged The current page number, or null to auto-determine.700 * @param int $rows_per_page The number of rows to display per page.701 * @param string $log_type The type of logs to display ('user' or 'request').702 * @return void703 */704 public function render_logs_tab( ?int $paged = null, int $rows_per_page = 10, string $log_type = 'user' ): void {705 $rows_per_page = $this->sanitize_rows_per_page( $rows_per_page );706 global $wpdb;707 708 // Determine which table to query based on log type.709 if ( 'request' === $log_type ) {710 $table_name = $wpdb->prefix . 'members_for_kofi_request_logs';711 $transient_key = 'members_for_kofi_total_request_logs';712 } else {713 $table_name = $this->get_logs_table_name();714 $transient_key = 'members_for_kofi_total_logs';715 }716 717 $total_logs = get_transient( $transient_key );718 719 if ( false === $total_logs || 0 === (int) $total_logs ) {720 // Recalculate if not cached or cached as zero (could be stale from prior table mismatch).721 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared -- Counting rows in plugin-owned table.722 $total_logs = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . esc_sql( $table_name ) . '`' );723 set_transient( $transient_key, $total_logs, MINUTE_IN_SECONDS );724 }725 726 $total_pages = ceil( $total_logs / $rows_per_page );727 728 ?>729 <!-- Log Type Selector -->730 <div style="margin-bottom:15px;">731 <label for="log-type-selector" style="font-weight:bold; margin-right:10px;">732 <?php esc_html_e( 'Log Type:', 'members-for-kofi' ); ?>733 </label>734 <select id="log-type-selector" style="min-width:150px;">735 <option value="user" <?php selected( $log_type, 'user' ); ?>>736 <?php esc_html_e( 'User Logs', 'members-for-kofi' ); ?>737 </option>738 <option value="request" <?php selected( $log_type, 'request' ); ?>>739 <?php esc_html_e( 'Request Logs', 'members-for-kofi' ); ?>740 </option>741 </select>742 </div>743 744 <!-- Logs Table Container -->745 <div id="logs-controls" style="display:flex; gap:10px; align-items:flex-end; margin-bottom:10px;">746 <div>747 <label for="kofi-members-log-search" style="display:block;">748 <?php749 if ( 'request' === $log_type ) {750 esc_html_e( 'Search (email / tier / status)', 'members-for-kofi' );751 } else {752 esc_html_e( 'Search (email / action / role)', 'members-for-kofi' );753 }754 ?>755 </label>756 <input type="search" id="kofi-members-log-search" style="min-width:280px;" />757 </div>758 <div>759 <button type="button" class="button" id="kofi-members-log-search-btn"><?php esc_html_e( 'Search', 'members-for-kofi' ); ?></button>760 <button type="button" class="button" id="kofi-members-log-reset-btn"><?php esc_html_e( 'Reset', 'members-for-kofi' ); ?></button>761 </div>762 <div>763 <button type="button" class="button" id="kofi-members-log-refresh-btn"><?php esc_html_e( 'Refresh', 'members-for-kofi' ); ?></button>764 </div>765 </div>766 <div id="logs-table-container" data-current-search="" data-log-type="<?php echo esc_attr( $log_type ); ?>">767 <?php768 if ( 'request' === $log_type ) {769 $this->render_request_logs_table( $paged, $rows_per_page );770 } else {771 $this->render_user_logs_table( $paged, $rows_per_page );772 }773 ?>774 </div>775 776 <?php777 }778 779 /**780 * Handles the AJAX request for pagination in the user logs tab.781 *782 * This function verifies the user's permissions, processes the pagination request,783 * and returns the rendered logs table for the requested page.784 *785 * @return void786 */787 public function handle_pagination(): void {788 // Verify the nonce for security.789 check_ajax_referer( 'members_for_kofi_pagination', '_ajax_nonce' );790 \MembersForKofi\Logging\DebugLogger::debug( 'AJAX pagination request' );791 792 // Verify the current user has permission to view logs.793 if ( ! current_user_can( 'manage_options' ) ) {794 wp_send_json_error( __( 'You do not have permission to access this resource.', 'members-for-kofi' ) );795 }796 797 $paged = isset( $_POST['paged'] ) ? absint( $_POST['paged'] ) : 1;798 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';799 $log_type = isset( $_POST['log_type'] ) ? sanitize_text_field( wp_unslash( $_POST['log_type'] ) ) : 'user';800 801 // Ensure the paged parameter is valid.802 if ( $paged < 1 ) {803 wp_send_json_error( __( 'Invalid page number.', 'members-for-kofi' ) );804 }805 806 // Render only the logs table for the requested page.807 ob_start();808 if ( 'request' === $log_type ) {809 $this->render_request_logs_table( $paged, 10, $search );810 } else {811 $this->render_user_logs_table( $paged, 10, $search );812 }813 $content = ob_get_clean();814 815 wp_send_json_success( $content );816 }817 818 /**819 * Handles the AJAX request to clear logs.820 *821 * @return void822 */823 public function handle_clear_logs(): void {824 // Verify the nonce for security.825 check_ajax_referer( 'members_for_kofi_clear_logs', '_ajax_nonce' );826 \MembersForKofi\Logging\DebugLogger::debug( 'AJAX clear logs request' );827 828 // Verify the current user has permission to clear logs.829 if ( ! current_user_can( 'manage_options' ) ) {830 wp_send_json_error( __( 'You do not have permission to access this resource.', 'members-for-kofi' ) );831 }832 833 global $wpdb;834 $log_type = isset( $_POST['log_type'] ) ? sanitize_text_field( wp_unslash( $_POST['log_type'] ) ) : 'user';835 836 // Determine which table to clear based on log type.837 if ( 'request' === $log_type ) {838 $table_name = $wpdb->prefix . 'members_for_kofi_request_logs';839 $transient_key = 'members_for_kofi_total_request_logs';840 } else {841 $table_name = $this->get_logs_table_name();842 $transient_key = 'members_for_kofi_total_logs';843 }844 845 // Check if the logs table exists and clear it.846 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Checking for the plugin's own table; a cached answer could be wrong.847 if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) ) {848 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Clearing the plugin's own log table on admin request; the cached count is deleted below.849 $wpdb->query( 'DELETE FROM `' . esc_sql( $table_name ) . '`' );850 delete_transient( $transient_key ); // Clear the cached total logs count.851 852 // Render the updated logs table (which will now show "No logs available").853 ob_start();854 if ( 'request' === $log_type ) {855 $this->render_request_logs_table();856 } else {857 $this->render_user_logs_table();858 }859 $content = ob_get_clean();860 861 wp_send_json_success( $content );862 } else {863 wp_send_json_error( __( 'Logs table does not exist.', 'members-for-kofi' ) );864 }865 }866 867 /**868 * Handles the AJAX request to update rows per page.869 *870 * @return void871 */872 /**873 * Clamps a requested page size to one the log viewer actually offers.874 *875 * Anything the picker cannot produce is rejected rather than squeezed876 * into range: 0 reached `ceil( $total_logs / 0 )` and killed the page877 * with a DivisionByZeroError, and a huge value went straight into LIMIT878 * and loaded the entire table into memory.879 *880 * @param mixed $raw Untrusted page size, typically straight from $_POST.881 * @return int One of ROWS_PER_PAGE_OPTIONS.882 */883 public function sanitize_rows_per_page( $raw ): int {884 $value = absint( $raw );885 886 if ( ! in_array( $value, self::ROWS_PER_PAGE_OPTIONS, true ) ) {887 return self::DEFAULT_ROWS_PER_PAGE;888 }889 890 return $value;891 }892 893 /**894 * Handles the AJAX request that changes the log table's page size.895 *896 * @return void897 */898 public function handle_update_rows_per_page(): void {899 check_ajax_referer( 'members_for_kofi_update_rows_per_page', '_ajax_nonce' );900 \MembersForKofi\Logging\DebugLogger::debug( 'AJAX rows per page update request' );901 902 // Verify the current user has permission to update settings.903 if ( ! current_user_can( 'manage_options' ) ) {904 wp_send_json_error( __( 'You do not have permission to access this resource.', 'members-for-kofi' ) );905 }906 907 $rows_per_page = $this->sanitize_rows_per_page(908 isset( $_POST['rows_per_page'] ) ? absint( wp_unslash( $_POST['rows_per_page'] ) ) : self::DEFAULT_ROWS_PER_PAGE909 );910 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';911 $log_type = isset( $_POST['log_type'] ) ? sanitize_text_field( wp_unslash( $_POST['log_type'] ) ) : 'user';912 913 ob_start();914 if ( 'request' === $log_type ) {915 $this->render_request_logs_table( null, $rows_per_page, $search );916 } else {917 $this->render_user_logs_table( null, $rows_per_page, $search );918 }919 $content = ob_get_clean();920 921 wp_send_json_success( $content );922 }923 924 /**925 * Handles AJAX log filtering.926 */927 public function handle_filter_logs(): void {928 check_ajax_referer( 'members_for_kofi_filter_logs', '_ajax_nonce' );929 \MembersForKofi\Logging\DebugLogger::debug( 'AJAX filter logs request' );930 931 // Verify the current user has permission to view logs.932 if ( ! current_user_can( 'manage_options' ) ) {933 wp_send_json_error( __( 'You do not have permission to access this resource.', 'members-for-kofi' ) );934 }935 936 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';937 $paged = isset( $_POST['paged'] ) ? absint( $_POST['paged'] ) : 1;938 $log_type = isset( $_POST['log_type'] ) ? sanitize_text_field( wp_unslash( $_POST['log_type'] ) ) : 'user';939 $rows_per_page = $this->sanitize_rows_per_page(940 isset( $_POST['rows_per_page'] ) ? absint( wp_unslash( $_POST['rows_per_page'] ) ) : self::DEFAULT_ROWS_PER_PAGE941 );942 943 ob_start();944 // Every other log handler honours log_type; this one always rendered the945 // user log, so searching from the Request tab answered with user rows.946 if ( 'request' === $log_type ) {947 $this->render_request_logs_table( $paged, $rows_per_page, $search );948 } else {949 $this->render_user_logs_table( $paged, $rows_per_page, $search );950 }951 $content = ob_get_clean();952 wp_send_json_success( $content );953 }954 955 /**956 * Handles AJAX refresh (no search alteration; just reload current page).957 */958 public function handle_refresh_logs(): void {959 check_ajax_referer( 'members_for_kofi_refresh_logs', '_ajax_nonce' );960 \MembersForKofi\Logging\DebugLogger::debug( 'AJAX refresh logs request' );961 962 // Verify the current user has permission to view logs.963 if ( ! current_user_can( 'manage_options' ) ) {964 wp_send_json_error( __( 'You do not have permission to access this resource.', 'members-for-kofi' ) );965 }966 967 $paged = isset( $_POST['paged'] ) ? absint( $_POST['paged'] ) : 1;968 $rows_per_page = $this->sanitize_rows_per_page(969 isset( $_POST['rows_per_page'] ) ? absint( wp_unslash( $_POST['rows_per_page'] ) ) : self::DEFAULT_ROWS_PER_PAGE970 );971 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';972 ob_start();973 $this->render_user_logs_table( $paged, $rows_per_page, $search );974 $content = ob_get_clean();975 wp_send_json_success( $content );976 }977 978 /**979 * Handles AJAX log type switch (User vs Request logs).980 */981 public function handle_switch_log_type(): void {982 check_ajax_referer( 'members_for_kofi_switch_log_type', '_ajax_nonce' );983 \MembersForKofi\Logging\DebugLogger::debug( 'AJAX switch log type request' );984 985 // Verify the current user has permission to view logs.986 if ( ! current_user_can( 'manage_options' ) ) {987 wp_send_json_error( __( 'You do not have permission to access this resource.', 'members-for-kofi' ) );988 }989 990 $log_type = isset( $_POST['log_type'] ) ? sanitize_text_field( wp_unslash( $_POST['log_type'] ) ) : 'user';991 $search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';992 $paged = isset( $_POST['paged'] ) ? absint( $_POST['paged'] ) : 1;993 $rows_per_page = $this->sanitize_rows_per_page(994 isset( $_POST['rows_per_page'] ) ? absint( wp_unslash( $_POST['rows_per_page'] ) ) : self::DEFAULT_ROWS_PER_PAGE995 );996 997 ob_start();998 if ( 'request' === $log_type ) {999 $this->render_request_logs_table( $paged, $rows_per_page, $search );1000 } else {1001 $this->render_user_logs_table( $paged, $rows_per_page, $search );1002 }1003 $content = ob_get_clean();1004 wp_send_json_success( $content );1005 }1006 1007 /**1008 * Renders the user logs table.1009 *1010 * This function outputs the HTML for displaying the user logs in a table,1011 * including pagination links.1012 *1013 * @param int|null $paged The current page number, or null to auto-determine.1014 * @param int $rows_per_page The number of rows to display per page.1015 * @param string $search Search filter.1016 * @return void1017 */1018 public function render_user_logs_table( ?int $paged = null, int $rows_per_page = 10, string $search = '' ): void {1019 $rows_per_page = $this->sanitize_rows_per_page( $rows_per_page );1020 global $wpdb;1021 1022 $table_name = $this->get_logs_table_name();1023 $current_page = $paged ?? 1; // Simplified; no $_GET fallback (avoids nonce sniff concerns).1024 $offset = ( $current_page - 1 ) * $rows_per_page;1025 1026 $use_cache = ( '' === $search );1027 $total_logs = $use_cache ? get_transient( 'members_for_kofi_total_logs' ) : false;1028 $where_sql = 'WHERE 1=1';1029 $params = array();1030 $like = '';1031 if ( '' !== $search ) {1032 $like = '%' . $wpdb->esc_like( $search ) . '%';1033 $where_sql .= ' AND (email LIKE %s OR action LIKE %s OR role LIKE %s)';1034 $params = array( $like, $like, $like );1035 }1036 1037 if ( false === $total_logs ) {1038 if ( empty( $params ) ) {1039 // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Counting plugin-owned table.1040 $total_logs = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . esc_sql( $table_name ) . '`' );1041 } else {1042 // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared -- $query is literal SQL with %s placeholders; the search term is escaped into $params.1043 $query = 'SELECT COUNT(*) FROM `' . esc_sql( $table_name ) . '` ' . $where_sql;1044 $total_logs = (int) $wpdb->get_var( $wpdb->prepare( $query, $params ) );1045 // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared1046 }1047 if ( ! empty( $wpdb->last_error ) ) {1048 \MembersForKofi\Logging\DebugLogger::error(1049 'DB error counting logs',1050 array(1051 'error' => $wpdb->last_error,1052 'last_query' => $wpdb->last_query,1053 )1054 );1055 }1056 if ( $use_cache ) {1057 set_transient( 'members_for_kofi_total_logs', $total_logs, MINUTE_IN_SECONDS );1058 }1059 }1060 1061 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Controlled table name.1062 // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Plugin-owned table query.1063 if ( empty( $params ) ) {1064 // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Unfiltered query.1065 $logs = $wpdb->get_results(1066 $wpdb->prepare(1067 'SELECT `timestamp`, `user_id`, `email`, `action`, `role` FROM `' . esc_sql( $table_name ) . '` ORDER BY `timestamp` DESC LIMIT %d OFFSET %d',1068 $rows_per_page,1069 $offset1070 ),1071 ARRAY_A1072 );1073 // Debug removed (noise).1074 } else {1075 // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared -- $query is literal SQL with %s placeholders; the search term is escaped into $params.1076 $query = 'SELECT `timestamp`, `user_id`, `email`, `action`, `role` FROM `' . esc_sql( $table_name ) . '` ' . $where_sql . ' ORDER BY `timestamp` DESC LIMIT %d OFFSET %d';1077 $logs = $wpdb->get_results(1078 $wpdb->prepare(1079 $query,1080 array_merge( $params, array( $rows_per_page, $offset ) )1081 ),1082 ARRAY_A1083 );1084 // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared1085 // Debug removed (noise).1086 }1087 if ( empty( $logs ) && (int) $total_logs > 0 ) {1088 \MembersForKofi\Logging\DebugLogger::warning( 'No rows returned despite positive total' );1089 }1090 1091 $total_pages = ceil( $total_logs / $rows_per_page );1092 1093 ?>1094 <table class="widefat fixed striped">1095 <thead>1096 <tr>1097 <th><?php esc_html_e( 'Timestamp', 'members-for-kofi' ); ?></th>1098 <th><?php esc_html_e( 'User ID', 'members-for-kofi' ); ?></th>1099 <th><?php esc_html_e( 'Email', 'members-for-kofi' ); ?></th>1100 <th><?php esc_html_e( 'Action', 'members-for-kofi' ); ?></th>1101 <th><?php esc_html_e( 'Role', 'members-for-kofi' ); ?></th>1102 </tr>1103 </thead>1104 <tbody>1105 <?php if ( empty( $logs ) ) : ?>1106 <tr>1107 <td colspan="5"><?php esc_html_e( 'No logs available.', 'members-for-kofi' ); ?></td>1108 </tr>1109 <?php else : ?>1110 <?php foreach ( $logs as $log ) : ?>1111 <tr>1112 <td><?php echo esc_html( $log['timestamp'] ); ?></td>1113 <td><?php echo esc_html( $log['user_id'] ); ?></td>1114 <td><?php echo esc_html( $log['email'] ); ?></td>1115 <td><?php echo esc_html( $log['action'] ); ?></td>1116 <td><?php echo esc_html( $log['role'] ); ?></td>1117 </tr>1118 <?php endforeach; ?>1119 <?php endif; ?>1120 </tbody>1121 </table>1122 1123 <div style="display: flex; align-items: center; justify-content: space-between; margin-top: 15px;">1124 <input type="hidden" id="kofi-members-current-search" value="<?php echo esc_attr( $search ); ?>" />1125 <!-- Clear Logs Button -->1126 <form method="post" action="<?php echo esc_url( add_query_arg( 'tab', 'user_logs', admin_url( 'admin.php?page=members-for-kofi' ) ) ); ?>" style="margin-right: 15px;">1127 <?php wp_nonce_field( 'clear_user_logs_action', 'clear_user_logs_nonce' ); ?>1128 <button type="button" name="clear_logs" class="button button-secondary">1129 <?php esc_html_e( 'Clear Logs', 'members-for-kofi' ); ?>1130 </button>1131 </form>1132 1133 <!-- Rows Per Page Dropdown -->1134 <form method="get" style="margin-right: 15px;">1135 <input type="hidden" name="page" value="members-for-kofi">1136 <input type="hidden" name="tab" value="user_logs">1137 <label for="rows_per_page" style="margin-right: 10px;"><?php esc_html_e( 'Rows per page:', 'members-for-kofi' ); ?></label>1138 <select name="rows_per_page" id="rows_per_page" style="width: auto;">1139 <?php foreach ( self::ROWS_PER_PAGE_OPTIONS as $option ) : ?>1140 <option value="<?php echo esc_attr( $option ); ?>" <?php selected( $rows_per_page, $option ); ?>>1141 <?php echo esc_html( $option ); ?>1142 </option>1143 <?php endforeach; ?>1144 </select>1145 </form>1146 <div class="pagination-links" style="display:flex; gap:4px; flex-wrap:wrap;">1147 <?php if ( $total_pages > 1 ) : ?>1148 <?php1149 $prev_page = max( 1, $current_page - 1 );1150 $next_page = min( $total_pages, $current_page + 1 );1151 if ( $current_page > 1 ) {1152 echo '<button type="button" class="button members-for-kofi-page-btn" data-page="' . esc_attr( $prev_page ) . '">« ' . esc_html__( 'Previous', 'members-for-kofi' ) . '</button>';1153 }1154 for ( $p = 1; $p <= $total_pages; $p++ ) {1155 $cls = 'button members-for-kofi-page-btn' . ( $p === $current_page ? ' button-primary' : '' );1156 echo '<button type="button" class="' . esc_attr( $cls ) . '" data-page="' . esc_attr( $p ) . '">' . esc_html( (string) $p ) . '</button>';1157 }1158 if ( $current_page < $total_pages ) {1159 echo '<button type="button" class="button members-for-kofi-page-btn" data-page="' . esc_attr( $next_page ) . '">' . esc_html__( 'Next', 'members-for-kofi' ) . ' »</button>';1160 }1161 ?>1162 <?php endif; ?>1163 </div>1164 </div>1165 <?php1166 }1167 1168 /**1169 * Renders the request logs table.1170 *1171 * @param int|null $paged The current page number.1172 * @param int $rows_per_page The number of rows to display per page.1173 * @param string $search Search filter.1174 * @return void1175 */1176 public function render_request_logs_table( ?int $paged = null, int $rows_per_page = 10, string $search = '' ): void {1177 $rows_per_page = $this->sanitize_rows_per_page( $rows_per_page );1178 global $wpdb;1179 1180 $table_name = $wpdb->prefix . 'members_for_kofi_request_logs';1181 $current_page = $paged ?? 1;1182 $offset = ( $current_page - 1 ) * $rows_per_page;1183 1184 $use_cache = ( '' === $search );1185 $total_logs = $use_cache ? get_transient( 'members_for_kofi_total_request_logs' ) : false;1186 $where_sql = 'WHERE 1=1';1187 $params = array();1188 1189 if ( '' !== $search ) {1190 $like = '%' . $wpdb->esc_like( $search ) . '%';1191 $where_sql .= ' AND (email LIKE %s OR tier_name LIKE %s OR CAST(status_code AS CHAR) LIKE %s OR error LIKE %s)';1192 $params = array( $like, $like, $like, $like );1193 }1194 1195 if ( false === $total_logs ) {1196 if ( empty( $params ) ) {1197 // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Counting plugin-owned table.1198 $total_logs = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . esc_sql( $table_name ) . '`' );1199 } else {1200 // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared -- $query is literal SQL with %s placeholders; the search term is escaped into $params.1201 $query = 'SELECT COUNT(*) FROM `' . esc_sql( $table_name ) . '` ' . $where_sql;1202 $total_logs = (int) $wpdb->get_var( $wpdb->prepare( $query, $params ) );1203 // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared1204 }1205 if ( $use_cache ) {1206 set_transient( 'members_for_kofi_total_request_logs', $total_logs, MINUTE_IN_SECONDS );1207 }1208 }1209 1210 if ( empty( $params ) ) {1211 // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Unfiltered query on a plugin-owned table.1212 $logs = $wpdb->get_results(1213 $wpdb->prepare(1214 'SELECT `timestamp`, `email`, `tier_name`, `amount`, `currency`, `is_subscription`, `status_code`, `success`, `error` FROM `' . esc_sql( $table_name ) . '` ORDER BY `timestamp` DESC LIMIT %d OFFSET %d',1215 $rows_per_page,1216 $offset1217 ),1218 ARRAY_A1219 );1220 } else {1221 // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared -- $query is literal SQL with %s placeholders; the search term is escaped into $params.1222 $query = 'SELECT `timestamp`, `email`, `tier_name`, `amount`, `currency`, `is_subscription`, `status_code`, `success`, `error` FROM `' . esc_sql( $table_name ) . '` ' . $where_sql . ' ORDER BY `timestamp` DESC LIMIT %d OFFSET %d';1223 $logs = $wpdb->get_results(1224 $wpdb->prepare(1225 $query,1226 array_merge( $params, array( $rows_per_page, $offset ) )1227 ),1228 ARRAY_A1229 );1230 // phpcs:enable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared1231 }1232 1233 $total_pages = ceil( $total_logs / $rows_per_page );1234 1235 ?>1236 <table class="widefat fixed striped">1237 <thead>1238 <tr>1239 <th><?php esc_html_e( 'Timestamp', 'members-for-kofi' ); ?></th>1240 <th><?php esc_html_e( 'Email', 'members-for-kofi' ); ?></th>1241 <th><?php esc_html_e( 'Tier Name', 'members-for-kofi' ); ?></th>1242 <th><?php esc_html_e( 'Amount', 'members-for-kofi' ); ?></th>1243 <th><?php esc_html_e( 'Subscription', 'members-for-kofi' ); ?></th>1244 <th><?php esc_html_e( 'Status', 'members-for-kofi' ); ?></th>1245 <th><?php esc_html_e( 'Success', 'members-for-kofi' ); ?></th>1246 <th><?php esc_html_e( 'Error', 'members-for-kofi' ); ?></th>1247 </tr>1248 </thead>1249 <tbody>1250 <?php if ( empty( $logs ) ) : ?>1251 <tr>1252 <td colspan="8"><?php esc_html_e( 'No logs available.', 'members-for-kofi' ); ?></td>1253 </tr>1254 <?php else : ?>1255 <?php foreach ( $logs as $log ) : ?>1256 <tr>1257 <td><?php echo esc_html( $log['timestamp'] ); ?></td>1258 <td><?php echo esc_html( $log['email'] ); ?></td>1259 <td><?php echo esc_html( $log['tier_name'] ); ?></td>1260 <td><?php echo esc_html( $log['amount'] ? esc_html( $log['amount'] . ' ' . $log['currency'] ) : '-' ); ?></td>1261 <td><?php echo $log['is_subscription'] ? esc_html__( 'Yes', 'members-for-kofi' ) : esc_html__( 'No', 'members-for-kofi' ); ?></td>1262 <td><?php echo esc_html( $log['status_code'] ); ?></td>1263 <td><?php echo $log['success'] ? '✓' : '✗'; ?></td>1264 <td><?php echo esc_html( $log['error'] ); ?></td>1265 </tr>1266 <?php endforeach; ?>1267 <?php endif; ?>1268 </tbody>1269 </table>1270 1271 <div style="display: flex; align-items: center; justify-content: space-between; margin-top: 15px;">1272 <input type="hidden" id="kofi-members-current-search" value="<?php echo esc_attr( $search ); ?>" />1273 <!-- Clear Logs Button -->1274 <form method="post" action="<?php echo esc_url( add_query_arg( 'tab', 'logs', admin_url( 'admin.php?page=members-for-kofi' ) ) ); ?>" style="margin-right: 15px;">1275 <?php wp_nonce_field( 'clear_request_logs_action', 'clear_request_logs_nonce' ); ?>1276 <button type="button" name="clear_logs" class="button button-secondary">1277 <?php esc_html_e( 'Clear Logs', 'members-for-kofi' ); ?>1278 </button>1279 </form>1280 1281 <!-- Rows Per Page Dropdown -->1282 <form method="get" style="margin-right: 15px;">1283 <input type="hidden" name="page" value="members-for-kofi">1284 <input type="hidden" name="tab" value="logs">1285 <label for="rows_per_page" style="margin-right: 10px;"><?php esc_html_e( 'Rows per page:', 'members-for-kofi' ); ?></label>1286 <select name="rows_per_page" id="rows_per_page" style="width: auto;">1287 <?php foreach ( self::ROWS_PER_PAGE_OPTIONS as $option ) : ?>1288 <option value="<?php echo esc_attr( $option ); ?>" <?php selected( $rows_per_page, $option ); ?>>1289 <?php echo esc_html( $option ); ?>1290 </option>1291 <?php endforeach; ?>1292 </select>1293 </form>1294 <div class="pagination-links" style="display:flex; gap:4px; flex-wrap:wrap;">1295 <?php if ( $total_pages > 1 ) : ?>1296 <?php1297 $prev_page = max( 1, $current_page - 1 );1298 $next_page = min( $total_pages, $current_page + 1 );1299 if ( $current_page > 1 ) {1300 echo '<button type="button" class="button members-for-kofi-page-btn" data-page="' . esc_attr( $prev_page ) . '">« ' . esc_html__( 'Previous', 'members-for-kofi' ) . '</button>';1301 }1302 for ( $p = 1; $p <= $total_pages; $p++ ) {1303 $cls = 'button members-for-kofi-page-btn' . ( $p === $current_page ? ' button-primary' : '' );1304 echo '<button type="button" class="' . esc_attr( $cls ) . '" data-page="' . esc_attr( $p ) . '">' . esc_html( (string) $p ) . '</button>';1305 }1306 if ( $current_page < $total_pages ) {1307 echo '<button type="button" class="button members-for-kofi-page-btn" data-page="' . esc_attr( $next_page ) . '">' . esc_html__( 'Next', 'members-for-kofi' ) . ' »</button>';1308 }1309 ?>1310 <?php endif; ?>1311 </div>1312 </div>1313 <?php1314 784 } 1315 785 } -
members-for-kofi/trunk/src/Logging/RequestLogger.php
r3728327 r3796384 96 96 ); 97 97 98 // Invalidate cached total logs count so UI refresh reflects new entries.99 delete_transient( 'members_for_kofi_total_request_logs' );100 98 } 101 99 -
members-for-kofi/trunk/src/Logging/UserLogger.php
r3703946 r3796384 59 59 ); 60 60 61 // Invalidate cached total logs count so UI refresh reflects new entries.62 delete_transient( 'members_for_kofi_total_logs' );63 61 } 64 62 -
members-for-kofi/trunk/src/Privacy/PersonalData.php
r3777494 r3796384 212 212 } 213 213 214 delete_transient( 'members_for_kofi_total_logs' );215 delete_transient( 'members_for_kofi_total_request_logs' );216 217 214 // The record of a membership that ended is history, and goes. A 218 215 // membership still being tracked is kept: deleting it would end access -
members-for-kofi/trunk/vendor/composer/autoload_classmap.php
r3787989 r3796384 9 9 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 10 10 'MembersForKofi\\Admin\\AdminSettings' => $baseDir . '/src/Admin/AdminSettings.php', 11 'MembersForKofi\\Admin\\LogsTab' => $baseDir . '/src/Admin/LogsTab.php', 12 'MembersForKofi\\Admin\\MembersTab' => $baseDir . '/src/Admin/MembersTab.php', 11 13 'MembersForKofi\\Cli\\MemberSinceCommand' => $baseDir . '/src/Cli/MemberSinceCommand.php', 12 14 'MembersForKofi\\Cron\\LogCleanup' => $baseDir . '/src/Cron/LogCleanup.php', -
members-for-kofi/trunk/vendor/composer/autoload_static.php
r3787989 r3796384 24 24 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 25 25 'MembersForKofi\\Admin\\AdminSettings' => __DIR__ . '/../..' . '/src/Admin/AdminSettings.php', 26 'MembersForKofi\\Admin\\LogsTab' => __DIR__ . '/../..' . '/src/Admin/LogsTab.php', 27 'MembersForKofi\\Admin\\MembersTab' => __DIR__ . '/../..' . '/src/Admin/MembersTab.php', 26 28 'MembersForKofi\\Cli\\MemberSinceCommand' => __DIR__ . '/../..' . '/src/Cli/MemberSinceCommand.php', 27 29 'MembersForKofi\\Cron\\LogCleanup' => __DIR__ . '/../..' . '/src/Cron/LogCleanup.php',
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)