Changeset 3799692
- Timestamp:
- 11/13/2026 05:12:46 AM (2 hours ago)
- Location:
- site-add-on-watchdog/trunk
- Files:
-
- 2 added
- 18 edited
-
CHANGELOG.md (modified) (1 diff)
-
assets/css/admin.css (modified) (1 diff)
-
assets/js/admin-table.js (modified) (1 diff)
-
languages/site-add-on-watchdog.pot (modified) (15 diffs)
-
readme.txt (modified) (8 diffs)
-
site-add-on-watchdog.php (modified) (2 diffs)
-
src/Admin/RiskSorter.php (modified) (6 diffs)
-
src/AdminPage.php (modified) (12 diffs)
-
src/Bootstrap.php (modified) (1 diff)
-
src/Plugin.php (modified) (10 diffs)
-
src/Repository/MigratesLegacyOptions.php (added)
-
src/Repository/RiskRepository.php (modified) (4 diffs)
-
src/Repository/SettingsRepository.php (modified) (3 diffs)
-
src/Scanner.php (modified) (4 diffs)
-
src/Services/NotificationQueue.php (modified) (2 diffs)
-
src/Services/WPScanClient.php (modified) (3 diffs)
-
src/Version.php (modified) (1 diff)
-
templates/admin-page.php (modified) (6 diffs)
-
templates/history.php (modified) (2 diffs)
-
uninstall.php (added)
Legend:
- Unmodified
- Added
- Removed
-
site-add-on-watchdog/trunk/CHANGELOG.md
r3770137 r3799692 5 5 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 6 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 8 ## [1.10.0] - 2026-11-13 9 10 ### Added 11 12 - Split the dashboard into Risks, History, Settings, and Delivery tabs. The overview now shows when the last scan ran and when the next one is due. 13 - Check plugins that have no WordPress.org directory entry against WPScan when an API key is configured. The "not in the directory" answer is cached for six hours. 14 - Delete the plugin's settings, saved risks, ignore list, scan history, notification queue, and scheduled events when the plugin is deleted from the Plugins screen. 15 - Add copy buttons for the server cron endpoint, secret, and command. 16 17 ### Changed 18 19 - Take the dashboard accent color from the WordPress admin, so it matches the admin color scheme on WordPress 6 and 7. 20 - Declare `Tested up to` only in `readme.txt`, as the current Plugin Check requires. 21 - Verify the packaged plugin against WordPress 7.1.4. 22 - Hide a notification channel's fields while the channel is switched off. Saved values are kept. 23 - Show the site-wide WP-Cron notice only after the scheduled scan is overdue. Sites that disable WP-Cron and run it from a system cron no longer see a permanent error. 24 - Remove the "Retry notifications" button that appeared when WP-Cron was disabled. It did the same thing as "Send notifications now". 25 26 ### Fixed 27 28 - Turn email alerts on by default, as the readme has always described. A new install saved the setting as off. Sites that never saved the Watchdog settings will start emailing risk alerts to their administrators after this update; sites that saved the settings keep their choice. 29 - Stop writing the cron status option and running two option deletes on every page load. A front-end request now makes one plugin query instead of five. 30 - Stop looking up administrator accounts before `plugins_loaded` on a new install, which logged a WordPress notice in debug mode. 31 - Leave the recipient field empty when the site owner empties it. It was refilled with the administrator addresses, at the cost of a user query on every request. Administrators are still included when an alert is sent. 32 - Remove a plugin from the risk table as soon as it is ignored. It used to stay listed until the next scan. 33 - Show every column of the risk table on narrow screens. Only the plugin name was visible. 34 - Send the report again when a REST cron scan is requested with `force`, even if nothing changed. The parameter was previously ignored for scans. 35 - Stop repeating an earlier notice after a later action or a page reload. 36 - Keep the "Download payload" button usable after the first download. 37 - Stop recording a diagnostic failure on every scan for plugins that have no WordPress.org directory entry. 7 38 8 39 ## [1.9.0] - 2026-10-28 -
site-add-on-watchdog/trunk/assets/css/admin.css
r3690952 r3799692 1 1 .wp-watchdog-admin { 2 --watchdog-brand: #135e96; 3 --watchdog-brand-strong: #0a4b78; 4 --watchdog-ink: #1d2327; 5 --watchdog-muted: #50575e; 6 --watchdog-border: #dcdcde; 7 --watchdog-canvas: #f6f7f7; 8 max-width: 1440px; 9 } 10 11 .wp-watchdog-admin * { 12 box-sizing: border-box; 13 } 14 15 .wp-watchdog-admin .wp-watchdog-hero { 16 display: flex; 17 align-items: center; 18 justify-content: space-between; 19 gap: 24px; 20 margin: 20px 0 0; 21 padding: 24px 28px; 2 --watchdog-accent: #2271b1; 3 --watchdog-accent-strong: #135e96; 4 --watchdog-accent-soft: #f0f6fc; 5 --watchdog-ink: #1d2327; 6 --watchdog-muted: #50575e; 7 --watchdog-border: #dcdcde; 8 --watchdog-divider: #f0f0f1; 9 --watchdog-canvas: #f6f7f7; 10 --watchdog-ok: #007017; 11 --watchdog-ok-soft: #edfaef; 12 --watchdog-warn: #7a5a00; 13 --watchdog-warn-soft: #fcf5dc; 14 --watchdog-danger: #b32d2e; 15 --watchdog-danger-soft: #fcf0f1; 16 --watchdog-radius: 12px; 17 max-width: 1200px; 18 color: var(--watchdog-ink); 19 } 20 21 /* Keep the tint in step when the script swaps in the admin accent color. */ 22 @supports (color: color-mix(in srgb, red, blue)) { 23 .wp-watchdog-admin { 24 --watchdog-accent-soft: color-mix(in srgb, var(--watchdog-accent) 9%, #fff); 25 } 26 } 27 28 .wp-watchdog-admin *, 29 .wp-watchdog-admin *::before, 30 .wp-watchdog-admin *::after { 31 box-sizing: border-box; 32 } 33 34 /* Buttons with an icon */ 35 .wp-watchdog-admin .button { 36 display: inline-flex; 37 align-items: center; 38 gap: 6px; 39 } 40 41 .wp-watchdog-admin .button .dashicons { 42 width: 16px; 43 height: 16px; 44 font-size: 16px; 45 } 46 47 .wp-watchdog-actions { 48 display: flex; 49 align-items: center; 50 flex-wrap: wrap; 51 gap: 8px; 52 } 53 54 /* Header */ 55 .wp-watchdog-header { 56 display: flex; 57 align-items: center; 58 justify-content: space-between; 59 flex-wrap: wrap; 60 gap: 16px 24px; 61 margin: 20px 0 16px; 62 padding: 20px 24px; 63 background: #fff; 64 border: 1px solid var(--watchdog-border); 65 border-radius: var(--watchdog-radius); 66 } 67 68 .wp-watchdog-header__brand { 69 display: flex; 70 align-items: center; 71 gap: 16px; 72 min-width: 0; 73 } 74 75 .wp-watchdog-header__icon { 76 display: flex; 77 flex: none; 78 align-items: center; 79 justify-content: center; 80 width: 48px; 81 height: 48px; 82 color: #fff; 83 background: var(--watchdog-accent); 84 border-radius: var(--watchdog-radius); 85 font-size: 28px; 86 } 87 88 .wp-watchdog-admin .wp-watchdog-header__title { 89 display: flex; 90 align-items: center; 91 flex-wrap: wrap; 92 gap: 4px 10px; 93 margin: 0; 94 padding: 0; 95 font-size: 22px; 96 font-weight: 600; 97 line-height: 1.3; 98 } 99 100 .wp-watchdog-header__version { 101 padding: 2px 8px; 102 color: var(--watchdog-muted); 103 background: var(--watchdog-canvas); 104 border: 1px solid var(--watchdog-border); 105 border-radius: 999px; 106 font-size: 12px; 107 font-weight: 500; 108 line-height: 1.5; 109 } 110 111 .wp-watchdog-header__summary { 112 margin: 4px 0 0; 113 color: var(--watchdog-muted); 114 font-size: 14px; 115 } 116 117 .wp-watchdog-header .button { 118 min-height: 36px; 119 padding: 0 14px; 120 } 121 122 /* Overview tiles */ 123 .wp-watchdog-overview { 124 display: grid; 125 grid-template-columns: repeat(4, minmax(0, 1fr)); 126 gap: 12px; 127 margin: 16px 0 0; 128 } 129 130 .wp-watchdog-stat { 131 display: grid; 132 grid-template-columns: auto minmax(0, 1fr); 133 align-content: start; 134 gap: 2px 12px; 135 padding: 16px; 136 color: var(--watchdog-ink); 137 background: #fff; 138 border: 1px solid var(--watchdog-border); 139 border-radius: var(--watchdog-radius); 140 text-decoration: none; 141 transition: 142 border-color 120ms ease, 143 box-shadow 120ms ease; 144 } 145 146 .wp-watchdog-stat:hover, 147 .wp-watchdog-stat:focus { 148 color: var(--watchdog-ink); 149 border-color: var(--watchdog-accent); 150 box-shadow: 0 0 0 1px var(--watchdog-accent); 151 outline: none; 152 } 153 154 .wp-watchdog-stat__icon { 155 display: flex; 156 grid-row: 1 / span 3; 157 align-items: center; 158 justify-content: center; 159 width: 36px; 160 height: 36px; 161 color: var(--watchdog-accent); 162 background: var(--watchdog-accent-soft); 163 border-radius: 10px; 164 font-size: 20px; 165 } 166 167 .wp-watchdog-stat__label { 168 color: var(--watchdog-muted); 169 font-size: 12px; 170 font-weight: 600; 171 } 172 173 .wp-watchdog-stat__value { 174 font-size: 24px; 175 font-weight: 600; 176 line-height: 1.25; 177 } 178 179 .wp-watchdog-stat__value--text { 180 font-size: 18px; 181 line-height: 1.65; 182 } 183 184 .wp-watchdog-stat__hint { 185 overflow: hidden; 186 color: var(--watchdog-muted); 187 font-size: 12px; 188 text-overflow: ellipsis; 189 } 190 191 .wp-watchdog-stat--ok .wp-watchdog-stat__icon { 192 color: var(--watchdog-ok); 193 background: var(--watchdog-ok-soft); 194 } 195 196 .wp-watchdog-stat--warn .wp-watchdog-stat__icon { 197 color: var(--watchdog-warn); 198 background: var(--watchdog-warn-soft); 199 } 200 201 .wp-watchdog-stat--danger .wp-watchdog-stat__icon { 202 color: var(--watchdog-danger); 203 background: var(--watchdog-danger-soft); 204 } 205 206 .wp-watchdog-stat--danger .wp-watchdog-stat__hint { 207 color: var(--watchdog-danger); 208 font-weight: 600; 209 } 210 211 /* Section tabs */ 212 .wp-watchdog-tabs { 213 display: flex; 214 gap: 4px; 215 margin: 20px 0 16px; 216 overflow-x: auto; 217 border-bottom: 1px solid var(--watchdog-border); 218 } 219 220 .wp-watchdog-tabs__tab { 221 display: inline-flex; 222 align-items: center; 223 gap: 8px; 224 margin-bottom: -1px; 225 padding: 10px 14px; 226 color: var(--watchdog-muted); 227 border-bottom: 2px solid transparent; 228 font-size: 14px; 229 font-weight: 600; 230 text-decoration: none; 231 white-space: nowrap; 232 } 233 234 .wp-watchdog-tabs__tab:hover { 235 color: var(--watchdog-ink); 236 } 237 238 .wp-watchdog-tabs__tab:focus { 239 color: var(--watchdog-ink); 240 box-shadow: inset 0 0 0 2px var(--watchdog-accent); 241 outline: none; 242 } 243 244 .wp-watchdog-tabs__tab[aria-current="true"] { 245 color: var(--watchdog-accent); 246 border-bottom-color: var(--watchdog-accent); 247 } 248 249 .wp-watchdog-tabs__count { 250 min-width: 20px; 251 padding: 0 6px; 252 color: #fff; 253 background: var(--watchdog-danger); 254 border-radius: 999px; 255 font-size: 11px; 256 line-height: 20px; 257 text-align: center; 258 } 259 260 .wp-watchdog-tabs__dot { 261 width: 8px; 262 height: 8px; 263 background: var(--watchdog-danger); 264 border-radius: 50%; 265 } 266 267 .wp-watchdog-panel { 268 scroll-margin-top: 48px; 269 } 270 271 .wp-watchdog-panel[hidden] { 272 display: none; 273 } 274 275 .wp-watchdog-admin--js .wp-watchdog-no-js { 276 display: none; 277 } 278 279 /* Cards */ 280 .wp-watchdog-card { 281 margin: 0 0 16px; 282 padding: 20px 24px; 283 background: #fff; 284 border: 1px solid var(--watchdog-border); 285 border-radius: var(--watchdog-radius); 286 } 287 288 .wp-watchdog-card__header { 289 display: flex; 290 align-items: flex-start; 291 justify-content: space-between; 292 flex-wrap: wrap; 293 gap: 8px 16px; 294 } 295 296 .wp-watchdog-admin .wp-watchdog-card__title { 297 margin: 0; 298 padding: 0; 299 color: var(--watchdog-ink); 300 font-size: 16px; 301 font-weight: 600; 302 line-height: 1.4; 303 } 304 305 .wp-watchdog-admin .wp-watchdog-card__description { 306 max-width: 76ch; 307 margin: 4px 0 0; 308 color: var(--watchdog-muted); 309 font-size: 13px; 310 } 311 312 .wp-watchdog-card-grid { 313 display: grid; 314 grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); 315 gap: 16px; 316 } 317 318 .wp-watchdog-card-grid .wp-watchdog-card { 319 margin: 0; 320 } 321 322 .wp-watchdog-card .wp-watchdog-actions { 323 margin-top: 16px; 324 } 325 326 .wp-watchdog-error-text { 327 margin: 8px 0 0; 328 padding: 8px 12px; 329 overflow-wrap: anywhere; 330 color: var(--watchdog-danger); 331 background: var(--watchdog-danger-soft); 332 border-radius: 6px; 333 font-size: 13px; 334 } 335 336 /* Badges */ 337 .wp-watchdog-badge { 338 display: inline-flex; 339 flex: none; 340 align-items: center; 341 padding: 2px 10px; 342 color: var(--watchdog-muted); 343 background: var(--watchdog-canvas); 344 border-radius: 999px; 345 font-size: 12px; 346 font-weight: 600; 347 line-height: 1.6; 348 white-space: nowrap; 349 } 350 351 .wp-watchdog-badge--ok { 352 color: var(--watchdog-ok); 353 background: var(--watchdog-ok-soft); 354 } 355 356 .wp-watchdog-badge--warn { 357 color: var(--watchdog-warn); 358 background: var(--watchdog-warn-soft); 359 } 360 361 .wp-watchdog-badge--danger { 362 color: var(--watchdog-danger); 363 background: var(--watchdog-danger-soft); 364 } 365 366 /* Empty state */ 367 .wp-watchdog-empty { 368 padding: 32px 16px 16px; 369 text-align: center; 370 } 371 372 .wp-watchdog-empty__icon { 373 display: inline-flex; 374 align-items: center; 375 justify-content: center; 376 width: 56px; 377 height: 56px; 378 color: var(--watchdog-ok); 379 background: var(--watchdog-ok-soft); 380 border-radius: 50%; 381 font-size: 28px; 382 } 383 384 .wp-watchdog-empty__icon.dashicons-search { 385 color: var(--watchdog-accent); 386 background: var(--watchdog-accent-soft); 387 } 388 389 .wp-watchdog-admin .wp-watchdog-empty__title { 390 margin: 12px 0 0; 391 font-size: 15px; 392 font-weight: 600; 393 } 394 395 .wp-watchdog-admin .wp-watchdog-empty__text { 396 margin: 4px 0 0; 397 color: var(--watchdog-muted); 398 } 399 400 /* Risk table */ 401 .wp-watchdog-risk-table { 402 margin-top: 16px; 403 } 404 405 .wp-watchdog-risk-table__controls { 406 display: flex; 407 align-items: center; 408 justify-content: space-between; 409 flex-wrap: wrap; 410 gap: 8px 16px; 411 margin-bottom: 12px; 412 } 413 414 .wp-watchdog-risk-table__filter-form { 415 display: flex; 416 align-items: center; 417 flex-wrap: wrap; 418 gap: 8px; 419 } 420 421 .wp-watchdog-risk-table__search input[type="search"] { 422 width: 260px; 423 max-width: 100%; 424 } 425 426 .wp-watchdog-risk-table__pagination { 427 display: flex; 428 align-items: center; 429 gap: 8px; 430 } 431 432 .wp-watchdog-risk-table__pagination .button { 433 justify-content: center; 434 min-width: 32px; 435 } 436 437 .wp-watchdog-risk-table__page-status { 438 color: var(--watchdog-muted); 439 font-size: 13px; 440 } 441 442 .wp-watchdog-risk-table__scroll { 443 overflow-x: auto; 444 border: 1px solid var(--watchdog-border); 445 border-radius: 8px; 446 } 447 448 .wp-watchdog-risk-table__scroll table.widefat { 449 margin: 0; 450 border: 0; 451 box-shadow: none; 452 } 453 454 .wp-watchdog-risk-table table.widefat th, 455 .wp-watchdog-risk-table table.widefat td { 456 padding: 12px; 457 vertical-align: top; 458 } 459 460 .wp-watchdog-risk-table__col-plugin { 461 width: 24%; 462 } 463 464 .wp-watchdog-risk-table__col-local, 465 .wp-watchdog-risk-table__col-remote { 466 width: 13%; 467 } 468 469 .wp-watchdog-risk-table__col-actions { 470 width: 90px; 471 } 472 473 .wp-watchdog-risk-table__sort { 474 display: inline-flex; 475 align-items: center; 476 gap: 6px; 477 padding: 0; 478 color: inherit; 479 background: none; 480 border: 0; 481 font: inherit; 482 font-weight: 600; 483 text-align: left; 484 cursor: pointer; 485 } 486 487 .wp-watchdog-risk-table__sort:focus-visible { 488 border-radius: 2px; 489 outline: 2px solid var(--watchdog-accent); 490 outline-offset: 2px; 491 } 492 493 .wp-watchdog-risk-table__sort-indicator { 494 width: 0; 495 height: 0; 496 border-top: 6px solid currentColor; 497 border-right: 4px solid transparent; 498 border-left: 4px solid transparent; 499 opacity: 0.3; 500 transition: 501 transform 0.2s ease, 502 opacity 0.2s ease; 503 } 504 505 .wp-watchdog-risk-table__sort[aria-sort="ascending"] 506 .wp-watchdog-risk-table__sort-indicator { 507 opacity: 1; 508 transform: rotate(180deg); 509 } 510 511 .wp-watchdog-risk-table__sort[aria-sort="descending"] 512 .wp-watchdog-risk-table__sort-indicator { 513 opacity: 1; 514 } 515 516 .wp-watchdog-admin .wp-watchdog-reasons { 517 margin: 0; 518 padding-left: 18px; 519 list-style: disc; 520 } 521 522 .wp-watchdog-admin .wp-watchdog-reasons li { 523 margin-bottom: 4px; 524 } 525 526 .wp-watchdog-admin .wp-watchdog-vulnerabilities { 527 display: grid; 528 gap: 6px; 529 margin: 8px 0 0; 530 padding: 10px 12px; 531 background: var(--watchdog-canvas); 532 border-radius: 6px; 533 list-style: none; 534 } 535 536 .wp-watchdog-admin .wp-watchdog-vulnerabilities li { 537 display: flex; 538 align-items: baseline; 539 flex-wrap: wrap; 540 gap: 4px 8px; 541 margin: 0; 542 } 543 544 .wp-watchdog-vulnerability__meta { 545 color: var(--watchdog-muted); 546 font-size: 12px; 547 } 548 549 .wp-watchdog-severity { 550 display: inline-flex; 551 align-items: center; 552 padding: 1px 8px; 553 border-radius: 999px; 554 font-size: 11px; 555 font-weight: 600; 556 letter-spacing: 0.04em; 557 text-transform: uppercase; 558 } 559 560 .wp-watchdog-severity--low { 561 color: #1c5f3a; 562 background: #e7f7ed; 563 } 564 565 .wp-watchdog-severity--medium { 566 color: #7a5a00; 567 background: #fff4d6; 568 } 569 570 .wp-watchdog-severity--high { 571 color: #922424; 572 background: #fde4df; 573 } 574 575 .wp-watchdog-severity--severe { 576 color: #fff; 577 background: #b32d2e; 578 } 579 580 /* Ignored plugins */ 581 .wp-watchdog-admin .wp-watchdog-chips { 582 display: flex; 583 flex-wrap: wrap; 584 gap: 8px; 585 margin: 12px 0 0; 586 padding: 0; 587 list-style: none; 588 } 589 590 .wp-watchdog-admin .wp-watchdog-chips li { 591 margin: 0; 592 } 593 594 .wp-watchdog-chip { 595 display: inline-flex; 596 align-items: center; 597 gap: 4px; 598 padding: 4px 6px 4px 12px; 599 color: var(--watchdog-ink); 600 background: var(--watchdog-canvas); 601 border: 1px solid var(--watchdog-border); 602 border-radius: 999px; 603 font-size: 13px; 604 line-height: 1.5; 605 cursor: pointer; 606 } 607 608 .wp-watchdog-chip:hover, 609 .wp-watchdog-chip:focus-visible { 610 color: var(--watchdog-danger); 611 border-color: var(--watchdog-danger); 612 outline: none; 613 } 614 615 .wp-watchdog-chip .dashicons { 616 width: 16px; 617 height: 16px; 618 font-size: 16px; 619 } 620 621 /* History */ 622 .wp-watchdog-admin .wp-watchdog-history { 623 margin: 16px 0 0; 624 padding: 0; 625 border: 1px solid var(--watchdog-border); 626 border-radius: 8px; 627 list-style: none; 628 } 629 630 .wp-watchdog-admin .wp-watchdog-history__item { 631 display: flex; 632 align-items: center; 633 flex-wrap: wrap; 634 gap: 8px 16px; 635 margin: 0; 636 padding: 10px 14px; 637 border-top: 1px solid var(--watchdog-divider); 638 } 639 640 .wp-watchdog-admin .wp-watchdog-history__item:first-child { 641 border-top: 0; 642 } 643 644 .wp-watchdog-history__date { 645 display: inline-flex; 646 flex: 1 1 220px; 647 align-items: center; 648 gap: 8px; 649 font-weight: 600; 650 } 651 652 .wp-watchdog-history__date .dashicons { 653 color: var(--watchdog-muted); 654 } 655 656 .wp-watchdog-history__downloads { 657 display: flex; 658 flex-wrap: wrap; 659 gap: 8px; 660 } 661 662 /* Settings */ 663 .wp-watchdog-field { 664 display: grid; 665 grid-template-columns: 220px minmax(0, 1fr); 666 gap: 8px 24px; 667 padding: 16px 0; 668 border-top: 1px solid var(--watchdog-divider); 669 } 670 671 .wp-watchdog-card__title + .wp-watchdog-field, 672 .wp-watchdog-card__title 673 + [data-watchdog-frequency-options] 674 > .wp-watchdog-field:first-child { 675 margin-top: 12px; 676 } 677 678 .wp-watchdog-card > .wp-watchdog-field:last-child { 679 padding-bottom: 0; 680 } 681 682 .wp-watchdog-field__label { 683 padding-top: 5px; 684 font-weight: 600; 685 } 686 687 .wp-watchdog-field__inline { 688 display: flex; 689 flex-wrap: wrap; 690 gap: 8px; 691 } 692 693 .wp-watchdog-admin .wp-watchdog-field .description, 694 .wp-watchdog-admin .wp-watchdog-channel .description { 695 max-width: 76ch; 696 margin: 6px 0 0; 697 } 698 699 .wp-watchdog-settings-form input.regular-text { 700 width: 100%; 701 max-width: 560px; 702 } 703 704 .wp-watchdog-admin .wp-watchdog-frequency-description--testing { 705 color: var(--watchdog-danger); 706 font-weight: 600; 707 } 708 709 .wp-watchdog-channels { 710 margin-top: 16px; 711 border: 1px solid var(--watchdog-border); 712 border-radius: 8px; 713 } 714 715 .wp-watchdog-channel + .wp-watchdog-channel { 716 border-top: 1px solid var(--watchdog-border); 717 } 718 719 .wp-watchdog-channel__header { 720 display: flex; 721 align-items: center; 722 gap: 12px; 723 padding: 12px 16px; 724 } 725 726 .wp-watchdog-channel__icon { 727 display: flex; 728 flex: none; 729 align-items: center; 730 justify-content: center; 731 width: 32px; 732 height: 32px; 733 color: var(--watchdog-accent); 734 background: var(--watchdog-accent-soft); 735 border-radius: 8px; 736 } 737 738 .wp-watchdog-admin .wp-watchdog-channel__title { 739 flex: 1 1 auto; 740 margin: 0; 741 font-size: 14px; 742 font-weight: 600; 743 } 744 745 .wp-watchdog-channel__body { 746 padding: 0 16px 16px 60px; 747 } 748 749 .wp-watchdog-channel__body[hidden] { 750 display: none; 751 } 752 753 .wp-watchdog-admin .wp-watchdog-channel__field { 754 margin: 0 0 12px; 755 } 756 757 .wp-watchdog-channel__field label { 758 display: block; 759 margin-bottom: 4px; 760 font-weight: 600; 761 } 762 763 .wp-watchdog-admin .wp-watchdog-channel__actions { 764 margin: 12px 0 0; 765 } 766 767 /* Switch */ 768 .wp-watchdog-switch { 769 display: inline-flex; 770 flex: none; 771 align-items: center; 772 gap: 8px; 773 cursor: pointer; 774 } 775 776 .wp-watchdog-admin .wp-watchdog-switch input[type="checkbox"] { 777 position: relative; 778 width: 36px; 779 min-width: 36px; 780 height: 20px; 781 margin: 0; 782 background: #8c8f94; 783 border: 0; 784 border-radius: 999px; 785 box-shadow: none; 786 transition: background-color 120ms ease; 787 appearance: none; 788 } 789 790 .wp-watchdog-admin .wp-watchdog-switch input[type="checkbox"]::before { 791 position: absolute; 792 top: 2px; 793 left: 2px; 794 float: none; 795 width: 16px; 796 height: 16px; 797 margin: 0; 798 background: #fff; 799 border-radius: 50%; 800 content: ""; 801 transition: transform 120ms ease; 802 } 803 804 .wp-watchdog-admin .wp-watchdog-switch input[type="checkbox"]:checked { 805 background: var(--watchdog-accent); 806 } 807 808 .wp-watchdog-admin .wp-watchdog-switch input[type="checkbox"]:checked::before { 809 content: ""; 810 transform: translateX(16px); 811 } 812 813 .wp-watchdog-admin .wp-watchdog-switch input[type="checkbox"]:focus-visible { 814 outline: 2px solid var(--watchdog-accent); 815 outline-offset: 2px; 816 } 817 818 .wp-watchdog-switch__state { 819 min-width: 24px; 820 color: var(--watchdog-muted); 821 font-size: 12px; 822 font-weight: 600; 823 } 824 825 .wp-watchdog-switch__state::after { 826 content: attr(data-off); 827 } 828 829 .wp-watchdog-switch input:checked + .wp-watchdog-switch__state { 830 color: var(--watchdog-accent); 831 } 832 833 .wp-watchdog-switch input:checked + .wp-watchdog-switch__state::after { 834 content: attr(data-on); 835 } 836 837 .wp-watchdog-admin .wp-watchdog-settings-form__submit { 838 position: sticky; 839 z-index: 2; 840 bottom: 0; 841 margin: 0; 842 padding: 12px 0; 843 background: #f0f0f1; 844 border-top: 1px solid var(--watchdog-border); 845 } 846 847 /* Server cron details */ 848 .wp-watchdog-details { 849 margin-top: 16px; 850 padding-top: 12px; 851 border-top: 1px solid var(--watchdog-divider); 852 } 853 854 .wp-watchdog-details summary { 855 color: var(--watchdog-accent); 856 font-weight: 600; 857 cursor: pointer; 858 } 859 860 .wp-watchdog-details__body { 861 margin-top: 12px; 862 } 863 864 .wp-watchdog-copy { 865 display: grid; 866 grid-template-columns: minmax(0, 1fr) auto; 867 align-items: center; 868 gap: 4px 8px; 869 margin-bottom: 12px; 870 } 871 872 .wp-watchdog-copy__label { 873 grid-column: 1 / -1; 874 font-weight: 600; 875 } 876 877 .wp-watchdog-copy code { 878 display: block; 879 padding: 8px 10px; 880 overflow-wrap: anywhere; 881 background: var(--watchdog-canvas); 882 border: 1px solid var(--watchdog-border); 883 border-radius: 6px; 884 font-size: 12px; 885 } 886 887 /* Submitting state */ 888 .wp-watchdog-admin form.is-busy { 889 opacity: 0.72; 890 pointer-events: none; 891 } 892 893 .wp-watchdog-admin form.is-busy button[type="submit"] .dashicons { 894 animation: watchdog-spin 0.8s linear infinite; 895 } 896 897 @keyframes watchdog-spin { 898 to { 899 transform: rotate(360deg); 900 } 901 } 902 903 @media (prefers-reduced-motion: reduce) { 904 .wp-watchdog-admin *, 905 .wp-watchdog-admin *::before { 906 transition: none !important; 907 animation: none !important; 908 } 909 } 910 911 @media (max-width: 960px) { 912 .wp-watchdog-overview { 913 grid-template-columns: repeat(2, minmax(0, 1fr)); 914 } 915 } 916 917 @media (max-width: 782px) { 918 .wp-watchdog-admin { 919 margin-right: 10px; 920 } 921 922 .wp-watchdog-header, 923 .wp-watchdog-card { 924 padding: 16px; 925 } 926 927 .wp-watchdog-header .wp-watchdog-actions, 928 .wp-watchdog-header .wp-watchdog-actions form { 929 flex: 1 1 100%; 930 } 931 932 .wp-watchdog-header .wp-watchdog-actions .button { 933 justify-content: center; 934 width: 100%; 935 } 936 937 .wp-watchdog-field { 938 grid-template-columns: minmax(0, 1fr); 939 } 940 941 .wp-watchdog-field__label { 942 padding-top: 0; 943 } 944 945 .wp-watchdog-channel__body { 946 padding-left: 16px; 947 } 948 949 .wp-watchdog-admin .wp-watchdog-switch input[type="checkbox"] { 950 width: 44px; 951 min-width: 44px; 952 height: 24px; 953 } 954 955 .wp-watchdog-admin .wp-watchdog-switch input[type="checkbox"]::before { 956 width: 20px; 957 height: 20px; 958 } 959 960 .wp-watchdog-admin 961 .wp-watchdog-switch 962 input[type="checkbox"]:checked::before { 963 margin: 0; 964 transform: translateX(20px); 965 } 966 967 .wp-watchdog-risk-table__search, 968 .wp-watchdog-risk-table__search input[type="search"] { 969 width: 100%; 970 } 971 972 /* Each risk becomes a card. */ 973 .wp-watchdog-risk-table__scroll { 974 border: 0; 975 } 976 977 .wp-watchdog-risk-table__scroll table, 978 .wp-watchdog-risk-table__scroll tbody, 979 .wp-watchdog-risk-table__scroll td { 980 display: block; 981 } 982 983 .wp-watchdog-risk-table__scroll thead { 984 position: absolute; 985 width: 1px; 986 height: 1px; 987 margin: -1px; 988 padding: 0; 22 989 overflow: hidden; 23 color: #fff; 24 background: 25 radial-gradient(circle at 88% 10%, rgba(255, 255, 255, 0.2), transparent 28%), 26 linear-gradient(135deg, var(--watchdog-brand-strong), var(--watchdog-brand)); 27 border-radius: 14px; 28 box-shadow: 0 8px 24px rgba(10, 75, 120, 0.16); 29 } 30 31 .wp-watchdog-hero__content { 32 max-width: 760px; 33 } 34 35 .wp-watchdog-hero__eyebrow { 36 display: flex; 37 align-items: center; 38 gap: 8px; 39 margin-bottom: 8px; 40 color: rgba(255, 255, 255, 0.78); 41 font-size: 12px; 42 font-weight: 600; 43 letter-spacing: 0.05em; 44 text-transform: uppercase; 45 } 46 47 .wp-watchdog-admin .wp-watchdog-hero__title { 48 display: flex; 49 align-items: center; 50 gap: 10px; 51 margin: 0; 52 color: #fff; 53 font-size: clamp(24px, 3vw, 34px); 54 line-height: 1.2; 55 } 56 57 .wp-watchdog-hero__title .dashicons { 58 width: 36px; 59 height: 36px; 60 font-size: 36px; 61 } 62 63 .wp-watchdog-hero__description { 64 margin: 10px 0 0; 65 color: rgba(255, 255, 255, 0.88); 66 font-size: 15px; 67 line-height: 1.6; 68 } 69 70 .wp-watchdog-actions { 71 display: flex; 72 align-items: center; 73 flex-wrap: wrap; 74 gap: 8px; 75 } 76 77 .wp-watchdog-hero .button { 78 min-height: 40px; 79 border-color: rgba(255, 255, 255, 0.85); 80 } 81 82 .wp-watchdog-hero .button.button-primary { 83 color: var(--watchdog-brand-strong); 84 background: #fff; 85 } 86 87 .wp-watchdog-hero .button.button-secondary { 88 color: #fff; 89 background: rgba(255, 255, 255, 0.1); 90 } 91 92 .wp-watchdog-nav { 93 position: sticky; 94 z-index: 5; 95 top: 32px; 96 display: flex; 97 gap: 4px; 98 margin: 16px 0; 99 padding: 6px; 100 overflow-x: auto; 101 background: rgba(255, 255, 255, 0.96); 102 border: 1px solid var(--watchdog-border); 103 border-radius: 10px; 104 } 105 106 .wp-watchdog-nav a { 107 padding: 8px 14px; 108 color: var(--watchdog-ink); 109 border-radius: 7px; 110 font-weight: 600; 111 text-decoration: none; 112 white-space: nowrap; 113 } 114 115 .wp-watchdog-nav a:hover, 116 .wp-watchdog-nav a:focus-visible { 117 color: var(--watchdog-brand); 118 background: #f0f6fc; 119 outline: none; 120 box-shadow: 0 0 0 2px var(--watchdog-brand); 121 } 122 123 .wp-watchdog-overview { 990 clip: rect(0, 0, 0, 0); 991 border: 0; 992 } 993 994 .wp-watchdog-risk-table__scroll tbody tr { 124 995 display: grid; 125 grid-template-columns: repeat(4, minmax(0, 1fr)); 126 gap: 12px; 127 margin: 16px 0; 128 } 129 130 .wp-watchdog-stat { 131 display: grid; 132 grid-template-columns: auto 1fr; 133 gap: 2px 12px; 134 min-height: 96px; 135 padding: 16px; 136 color: var(--watchdog-ink); 996 grid-template-columns: repeat(2, minmax(0, 1fr)); 997 margin-bottom: 12px; 998 padding: 8px 0; 137 999 background: #fff; 138 1000 border: 1px solid var(--watchdog-border); 139 border-radius: 10px;140 text-decoration: none;141 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);142 transition: border-color 120ms ease, box-shadow 120ms ease, transform 120ms ease;143 }144 145 .wp-watchdog-stat:hover,146 .wp-watchdog-stat:focus-visible {147 color: var(--watchdog-ink);148 border-color: var(--watchdog-brand);149 outline: none;150 box-shadow: 0 4px 12px rgba(19, 94, 150, 0.12);151 transform: translateY(-1px);152 }153 154 .wp-watchdog-stat__icon {155 grid-row: 1 / span 2;156 width: 34px;157 height: 34px;158 padding: 7px;159 color: var(--watchdog-brand);160 background: #f0f6fc;161 1001 border-radius: 8px; 162 font-size: 20px; 163 } 164 165 .wp-watchdog-stat__value { 166 align-self: end; 167 font-size: 26px; 168 font-weight: 700; 169 line-height: 1.1; 170 } 171 172 .wp-watchdog-stat__value--text { 173 font-size: 18px; 174 } 175 176 .wp-watchdog-stat__label { 1002 } 1003 1004 .wp-watchdog-risk-table__scroll tbody tr:last-child { 1005 margin-bottom: 0; 1006 } 1007 1008 .wp-watchdog-risk-table table.widefat td { 1009 padding: 6px 16px; 1010 border: 0; 1011 } 1012 1013 .wp-watchdog-risk-table table.widefat td[data-column="plugin"], 1014 .wp-watchdog-risk-table table.widefat td[data-column="reasons"], 1015 .wp-watchdog-risk-table table.widefat td[data-column="actions"] { 1016 grid-column: 1 / -1; 1017 } 1018 1019 .wp-watchdog-risk-table table.widefat td[data-column="plugin"] { 1020 font-size: 15px; 1021 } 1022 1023 .wp-watchdog-risk-table table.widefat td::before { 1024 display: block; 1025 margin-bottom: 2px; 177 1026 color: var(--watchdog-muted); 178 font-size: 13px; 179 } 180 181 .wp-watchdog-risk-table { 182 margin-top: 1.5rem; 183 } 184 185 .wp-watchdog-notification-toggle { 186 display: inline-flex; 187 align-items: center; 188 gap: 0.5rem; 1027 font-size: 11px; 189 1028 font-weight: 600; 190 } 191 192 .wp-watchdog-notification-fields { 193 margin-top: 0.5rem; 194 } 195 196 .wp-watchdog-notification-fields--disabled { 197 opacity: 0.65; 198 } 199 200 .wp-watchdog-notification-fields--disabled input, 201 .wp-watchdog-notification-fields--disabled textarea, 202 .wp-watchdog-notification-fields--disabled select { 203 background-color: #f6f7f7; 204 cursor: not-allowed; 205 } 206 207 .wp-watchdog-frequency-description--testing { 208 color: #b32d2e; 209 font-weight: 600; 210 } 211 212 .wp-watchdog-frequency-options { 213 margin-top: 0.5rem; 214 display: grid; 215 gap: 0.75rem; 216 } 217 218 .wp-watchdog-frequency-options__row { 219 padding: 0.75rem; 220 border: 1px solid #c3c4c7; 221 border-radius: 4px; 222 background: #fff; 223 } 224 225 .wp-watchdog-frequency-label { 226 display: block; 227 font-weight: 600; 228 margin-bottom: 0.35rem; 229 } 230 231 .wp-watchdog-frequency-weekly { 232 display: flex; 233 align-items: center; 234 gap: 0.5rem; 235 margin-bottom: 0.35rem; 236 flex-wrap: wrap; 237 } 238 239 .wp-watchdog-risk-table__controls { 240 display: flex; 241 align-items: center; 242 justify-content: space-between; 243 gap: 1rem; 244 margin-bottom: 0.75rem; 245 flex-wrap: wrap; 246 } 247 248 .wp-watchdog-risk-table__filter-form { 249 display: flex; 250 align-items: center; 251 gap: 0.75rem; 252 flex-wrap: wrap; 253 } 254 255 .wp-watchdog-risk-table__search input[type="search"] { 256 min-width: 220px; 257 } 258 259 .wp-watchdog-risk-table__sort-select { 260 display: flex; 261 align-items: center; 262 gap: 0.5rem; 263 flex-wrap: wrap; 264 } 265 266 .wp-watchdog-risk-table__pagination { 267 display: flex; 268 align-items: center; 269 gap: 0.5rem; 270 } 271 272 .wp-watchdog-risk-table__pagination button.button { 273 min-width: 2.5rem; 274 text-align: center; 275 } 276 277 .wp-watchdog-risk-table__page-status { 278 font-weight: 600; 279 } 280 281 .wp-watchdog-risk-table__scroll { 282 overflow-x: auto; 283 border: 1px solid #c3c4c7; 284 border-radius: 4px; 285 background: #fff; 286 } 287 288 .wp-watchdog-risk-table__scroll table { 289 margin: 0; 290 } 291 292 .wp-watchdog-risk-table table.wp-list-table th { 293 vertical-align: middle; 294 } 295 296 .wp-watchdog-risk-table__sort { 297 background: none; 298 border: 0; 299 padding: 0; 300 font: inherit; 301 color: inherit; 302 cursor: pointer; 303 display: inline-flex; 304 align-items: center; 305 gap: 0.25rem; 306 } 307 308 .wp-watchdog-risk-table__sort:focus { 309 outline: 2px solid #2271b1; 310 outline-offset: 2px; 311 border-radius: 2px; 312 } 313 314 .wp-watchdog-risk-table__sort-indicator { 315 width: 0; 316 height: 0; 317 border-left: 4px solid transparent; 318 border-right: 4px solid transparent; 319 border-top: 6px solid currentColor; 320 transform: rotate(0deg); 321 opacity: 0.3; 322 transition: transform 0.2s ease, opacity 0.2s ease; 323 } 324 325 .wp-watchdog-risk-table__sort[aria-sort="ascending"] .wp-watchdog-risk-table__sort-indicator { 326 transform: rotate(180deg); 327 opacity: 1; 328 } 329 330 .wp-watchdog-risk-table__sort[aria-sort="descending"] .wp-watchdog-risk-table__sort-indicator { 331 transform: rotate(0deg); 332 opacity: 1; 333 } 334 335 .wp-watchdog-risk-table__sort[aria-sort="none"] .wp-watchdog-risk-table__sort-indicator { 336 opacity: 0.3; 337 } 338 339 .wp-watchdog-risk-table table.wp-list-table tbody td ul { 340 margin: 0; 341 padding-left: 1.2rem; 342 } 343 344 .wp-watchdog-risk-table table.wp-list-table tbody td ul ul { 345 margin-top: 0.5rem; 346 } 347 348 .wp-watchdog-risk-table table.wp-list-table tbody td ul li { 349 margin-bottom: 0.25rem; 350 } 351 352 .wp-watchdog-severity { 353 display: inline-flex; 354 align-items: center; 355 gap: 0.25rem; 356 padding: 0.125rem 0.5rem; 357 border-radius: 999px; 358 font-size: 0.75rem; 359 font-weight: 600; 1029 letter-spacing: 0.04em; 360 1030 text-transform: uppercase; 361 letter-spacing: 0.04em; 362 margin-right: 0.5rem; 363 } 364 365 .wp-watchdog-severity--low { 366 background: #e7f7ed; 367 color: #1c5f3a; 368 } 369 370 .wp-watchdog-severity--medium { 371 background: #fff4d6; 372 color: #7a5a00; 373 } 374 375 .wp-watchdog-severity--high { 376 background: #fde4df; 377 color: #922424; 378 } 379 380 .wp-watchdog-severity--severe { 381 background: #fbe0e6; 382 color: #80102a; 383 } 384 385 .wp-watchdog-vulnerability__title, 386 .wp-watchdog-vulnerability__cve, 387 .wp-watchdog-vulnerability__fixed { 388 display: inline-block; 389 margin-right: 0.25rem; 390 } 391 392 .wp-watchdog-admin .wp-watchdog-grid { 393 display: grid; 394 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); 395 gap: 16px; 396 margin: 16px 0; 397 } 398 399 .wp-watchdog-admin .wp-watchdog-surface { 400 background: #fff; 401 border: 1px solid #dcdcde; 402 border-radius: 10px; 403 padding: 16px; 404 box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); 405 } 406 407 .wp-watchdog-admin .wp-watchdog-surface[id] { 408 scroll-margin-top: 92px; 409 } 410 411 .wp-watchdog-admin .wp-watchdog-surface--wide { 412 grid-column: 1 / -1; 413 } 414 415 .wp-watchdog-admin .wp-watchdog-section-title { 416 display: flex; 417 align-items: center; 418 gap: 8px; 419 margin-bottom: 12px; 420 font-size: 16px; 421 font-weight: 600; 422 } 423 424 .wp-watchdog-admin .wp-watchdog-badge { 425 display: inline-flex; 426 align-items: center; 427 gap: 6px; 428 padding: 4px 10px; 429 border-radius: 999px; 430 font-weight: 600; 431 font-size: 12px; 432 text-transform: uppercase; 433 letter-spacing: 0.02em; 434 } 435 436 .wp-watchdog-admin .wp-watchdog-badge--success { 437 background: #e7f7ed; 438 color: #1c5f3a; 439 } 440 441 .wp-watchdog-admin .wp-watchdog-badge--warning { 442 background: #fff4d6; 443 color: #7a5a00; 444 } 445 446 .wp-watchdog-admin .wp-watchdog-badge--muted { 447 background: #eef1f3; 448 color: #1d2327; 449 } 450 451 .wp-watchdog-admin .wp-watchdog-section-header { 452 display: flex; 453 justify-content: space-between; 454 align-items: center; 455 flex-wrap: wrap; 456 gap: 12px; 457 } 458 459 .wp-watchdog-admin .button-hero { 460 display: flex; 461 align-items: center; 462 gap: 8px; 463 } 464 465 .wp-watchdog-admin .wp-watchdog-inline-list { 466 display: flex; 467 gap: 8px; 468 flex-wrap: wrap; 469 margin: 8px 0 0; 470 padding: 0; 471 list-style: none; 472 } 473 474 .wp-watchdog-admin .wp-watchdog-inline-list li { 475 margin: 0; 476 } 477 478 .wp-watchdog-admin .wp-watchdog-summary { 479 display: flex; 480 align-items: center; 481 gap: 10px; 482 } 483 484 .wp-watchdog-admin .wp-watchdog-summary__count { 485 font-size: 32px; 486 font-weight: 700; 487 } 488 489 .wp-watchdog-admin .wp-watchdog-summary__label { 490 color: #4b5563; 491 } 492 493 .wp-watchdog-admin .wp-watchdog-card-stack { 494 display: flex; 495 flex-direction: column; 496 gap: 12px; 497 } 498 499 .wp-watchdog-admin .wp-watchdog-muted { 500 color: #4b5563; 501 margin: 0; 502 } 503 504 .wp-watchdog-endpoint { 505 overflow-wrap: anywhere; 506 } 507 508 .wp-watchdog-subheading { 509 margin-bottom: 8px !important; 510 } 511 512 .wp-watchdog-block-action { 513 margin: 12px 0; 514 } 515 516 .wp-watchdog-channel-summary { 517 margin-top: 12px !important; 518 } 519 520 .wp-watchdog-inline-form { 521 display: inline; 522 } 523 524 .wp-watchdog-settings-form .form-table { 525 margin-top: 8px; 526 } 527 528 .wp-watchdog-settings-form .form-table > tbody > tr > th, 529 .wp-watchdog-settings-form .form-table > tbody > tr > td { 530 padding-top: 18px; 531 padding-bottom: 18px; 532 border-top: 1px solid #f0f0f1; 533 } 534 535 .wp-watchdog-settings-form .form-table > tbody > tr:first-child > th, 536 .wp-watchdog-settings-form .form-table > tbody > tr:first-child > td { 537 border-top: 0; 538 } 539 540 .wp-watchdog-settings-form input[type="url"], 541 .wp-watchdog-settings-form input[type="text"], 542 .wp-watchdog-settings-form input[type="password"] { 543 width: min(100%, 620px); 544 } 545 546 .wp-watchdog-settings-form .description { 547 max-width: 720px; 548 } 549 550 .wp-watchdog-admin form.is-busy { 551 pointer-events: none; 552 opacity: 0.72; 553 } 554 555 .wp-watchdog-admin form.is-busy button[type="submit"] .dashicons { 556 animation: watchdog-spin 0.8s linear infinite; 557 } 558 559 @keyframes watchdog-spin { 560 to { 561 transform: rotate(360deg); 562 } 563 } 564 565 .wp-watchdog-admin .wp-watchdog-divider { 566 border-top: 1px solid #dcdcde; 567 margin: 16px 0; 568 } 569 570 .wp-watchdog-admin .wp-watchdog-history-table { 571 margin-top: 12px; 572 } 573 574 .wp-watchdog-history-grid { 575 display: grid; 576 grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); 577 gap: 12px; 578 } 579 580 .wp-watchdog-history-card { 581 background: #f6f7f7; 582 border: 1px solid #dcdcde; 583 border-radius: 8px; 584 padding: 12px 14px; 585 box-shadow: 0 1px 1px rgba(0, 0, 0, 0.02); 586 } 587 588 .wp-watchdog-history-card__header { 589 display: flex; 590 justify-content: space-between; 591 align-items: flex-start; 592 gap: 8px; 593 margin-bottom: 8px; 594 flex-wrap: wrap; 595 } 596 597 .wp-watchdog-history-card__title { 598 margin: 0; 599 font-weight: 600; 600 font-size: 14px; 601 display: flex; 602 align-items: center; 603 gap: 6px; 604 flex: 1 1 auto; 605 min-width: 0; 606 } 607 608 .wp-watchdog-history-card__meta { 609 color: #4b5563; 610 font-size: 12px; 611 margin: 0; 612 } 613 614 .wp-watchdog-history-badge { 615 display: inline-flex; 616 align-items: center; 617 gap: 6px; 618 padding: 4px 8px; 619 border-radius: 999px; 620 font-weight: 600; 621 font-size: 12px; 622 text-transform: uppercase; 623 align-self: flex-start; 624 } 625 626 .wp-watchdog-history-badge--safe { 627 background: #e7f7ed; 628 color: #1c5f3a; 629 } 630 631 .wp-watchdog-history-badge--risk { 632 background: #fff4d6; 633 color: #7a5a00; 634 } 635 636 .wp-watchdog-history-card__downloads { 637 display: flex; 638 flex-wrap: wrap; 639 gap: 8px; 640 margin-top: 10px; 641 } 642 643 @media (max-width: 960px) { 644 .wp-watchdog-overview { 645 grid-template-columns: repeat(2, minmax(0, 1fr)); 646 } 647 648 .wp-watchdog-risk-table__controls { 649 flex-direction: column; 650 align-items: flex-start; 651 } 652 } 653 654 @media (max-width: 782px) { 655 .wp-watchdog-admin { 656 margin-right: 10px; 657 } 658 659 .wp-watchdog-admin .wp-watchdog-hero { 660 align-items: flex-start; 661 flex-direction: column; 662 padding: 20px; 663 } 664 665 .wp-watchdog-hero .wp-watchdog-actions, 666 .wp-watchdog-hero .wp-watchdog-actions form, 667 .wp-watchdog-hero .wp-watchdog-actions .button { 668 width: 100%; 669 } 670 671 .wp-watchdog-hero .wp-watchdog-actions .button { 672 justify-content: center; 673 } 674 675 .wp-watchdog-nav { 676 top: 46px; 677 } 678 679 .wp-watchdog-overview { 680 grid-template-columns: 1fr; 681 } 682 683 .wp-watchdog-settings-form .form-table th, 684 .wp-watchdog-settings-form .form-table td { 685 display: block; 686 width: 100%; 687 } 688 689 .wp-watchdog-settings-form .form-table th { 690 padding-bottom: 6px; 691 } 692 693 .wp-watchdog-risk-table__scroll { 694 border: none; 695 } 696 697 .wp-watchdog-risk-table__scroll table, 698 .wp-watchdog-risk-table__scroll thead, 699 .wp-watchdog-risk-table__scroll tbody, 700 .wp-watchdog-risk-table__scroll th, 701 .wp-watchdog-risk-table__scroll td, 702 .wp-watchdog-risk-table__scroll tr { 703 display: block; 704 } 705 706 .wp-watchdog-risk-table__scroll thead tr { 707 position: absolute; 708 width: 1px; 709 height: 1px; 710 padding: 0; 711 margin: -1px; 712 overflow: hidden; 713 clip: rect(0, 0, 0, 0); 714 border: 0; 715 } 716 717 .wp-watchdog-risk-table__scroll tbody tr { 718 border: 1px solid #c3c4c7; 719 border-radius: 4px; 720 margin-bottom: 1rem; 721 padding: 0.75rem 0; 722 background: #fff; 723 } 724 725 .wp-watchdog-risk-table__scroll tbody tr:last-child { 726 margin-bottom: 0; 727 } 728 729 .wp-watchdog-risk-table__scroll td { 730 border: none; 731 position: relative; 732 padding: 0.5rem 1rem 0.5rem 45%; 733 min-height: 2.5rem; 734 } 735 736 .wp-watchdog-risk-table__scroll td::before { 737 content: attr(data-column-label); 738 position: absolute; 739 left: 1rem; 740 top: 0.5rem; 741 font-weight: 600; 742 width: 40%; 743 padding-right: 0.5rem; 744 white-space: normal; 745 } 746 747 .wp-watchdog-risk-table table.wp-list-table tbody td ul { 748 padding-left: 1rem; 749 } 750 } 1031 content: attr(data-column-label); 1032 } 1033 1034 .wp-watchdog-risk-table table.widefat td[data-column="plugin"]::before, 1035 .wp-watchdog-risk-table table.widefat td[data-column="actions"]::before { 1036 content: none; 1037 } 1038 } 1039 1040 @media (max-width: 480px) { 1041 .wp-watchdog-overview { 1042 grid-template-columns: minmax(0, 1fr); 1043 } 1044 } -
site-add-on-watchdog/trunk/assets/js/admin-table.js
r3690952 r3799692 1 1 (function () { 2 'use strict'; 3 4 const i18n = window.siteAddOnWatchdogTable || {}; 5 6 /** 7 * @param {HTMLElement} tableWrapper 8 */ 9 function initTable(tableWrapper) { 10 const tbody = tableWrapper.querySelector('tbody'); 11 if (!tbody) { 2 "use strict"; 3 4 const i18n = window.siteAddOnWatchdogTable || {}; 5 6 /** 7 * @param {HTMLElement} tableWrapper 8 */ 9 function initTable(tableWrapper) { 10 const tbody = tableWrapper.querySelector("tbody"); 11 if (!tbody) { 12 return; 13 } 14 15 const perPage = parseInt(tableWrapper.dataset.perPage || "10", 10); 16 const baseRows = Array.from(tbody.querySelectorAll("tr")); 17 let rows = baseRows.slice(); 18 const pagination = tableWrapper.querySelector("[data-pagination]"); 19 const statusEl = tableWrapper.querySelector("[data-page-status]"); 20 const prevBtn = pagination 21 ? pagination.querySelector('[data-action="prev"]') 22 : null; 23 const nextBtn = pagination 24 ? pagination.querySelector('[data-action="next"]') 25 : null; 26 const headers = Array.from( 27 tableWrapper.querySelectorAll("[data-sort-key]"), 28 ); 29 const searchInput = tableWrapper.querySelector("[data-risk-search]"); 30 const sortSelect = tableWrapper.querySelector("[data-risk-sort]"); 31 const orderSelect = tableWrapper.querySelector("[data-risk-order]"); 32 33 const state = { 34 page: 1, 35 perPage: Number.isFinite(perPage) && perPage > 0 ? perPage : rows.length, 36 sortKey: null, 37 sortOrder: "asc", 38 filterQuery: searchInput ? searchInput.value.trim() : "", 39 }; 40 41 const initialHeader = headers.find((header) => 42 header.hasAttribute("data-sort-initial"), 43 ); 44 if (initialHeader) { 45 state.sortKey = initialHeader.getAttribute("data-sort-key"); 46 state.sortOrder = 47 initialHeader.getAttribute("data-sort-default") || "asc"; 48 } 49 50 const VERSION_SORT_KEYS = new Set(["sortLocal", "sortRemote"]); 51 const NUMERIC_SORT_KEYS = new Set(["sortRiskCount", "sortVersionGap"]); 52 const NUMERIC_TOKEN_PATTERN = /^\d+$/; 53 54 if (sortSelect) { 55 const selectedOption = sortSelect.selectedOptions[0]; 56 if (selectedOption && selectedOption.dataset.sortKey) { 57 state.sortKey = selectedOption.dataset.sortKey; 58 state.sortOrder = orderSelect 59 ? orderSelect.value 60 : selectedOption.dataset.sortDefault || "asc"; 61 } 62 } 63 64 function getSortValue(row, key, options = {}) { 65 const { raw = false } = options; 66 67 if (NUMERIC_SORT_KEYS.has(key)) { 68 const datasetKey = key.replace(/-([a-z])/g, (_, char) => 69 char.toUpperCase(), 70 ); 71 const value = row.dataset[datasetKey]; 72 const parsed = value ? parseFloat(value) : 0; 73 return Number.isFinite(parsed) ? parsed : 0; 74 } 75 76 if (VERSION_SORT_KEYS.has(key)) { 77 const columnKey = key === "sortLocal" ? "local" : "remote"; 78 const text = ( 79 row.querySelector(`[data-column="${columnKey}"]`)?.textContent || "" 80 ).trim(); 81 if (!text) { 82 return raw ? "" : null; 83 } 84 if (text.toLowerCase() === "n/a") { 85 return raw ? "N/A" : null; 86 } 87 return raw ? text : text.toLowerCase(); 88 } 89 90 const datasetKey = key.replace(/-([a-z])/g, (_, char) => 91 char.toUpperCase(), 92 ); 93 const value = row.dataset[datasetKey]; 94 if (value !== undefined) { 95 return raw ? value : value.toLowerCase(); 96 } 97 const fallback = ( 98 row.querySelector(`[data-column="${key}"]`)?.textContent || "" 99 ).trim(); 100 return raw ? fallback : fallback.toLowerCase(); 101 } 102 103 function parseVersionForSort(value) { 104 if (typeof value !== "string") { 105 return { tokens: [], isMissing: true }; 106 } 107 108 const trimmed = value.trim(); 109 if (!trimmed || trimmed.toLowerCase() === "n/a") { 110 return { tokens: [], isMissing: true }; 111 } 112 113 const tokens = trimmed.match(/[0-9]+|[a-zA-Z]+/g) || []; 114 return { tokens, isMissing: tokens.length === 0 }; 115 } 116 117 function compareVersionTokens(a, b) { 118 if (a.isMissing && b.isMissing) { 119 return 0; 120 } 121 122 if (a.isMissing) { 123 return 1; 124 } 125 126 if (b.isMissing) { 127 return -1; 128 } 129 130 const maxLength = Math.max(a.tokens.length, b.tokens.length); 131 for (let index = 0; index < maxLength; index += 1) { 132 const tokenA = a.tokens[index]; 133 const tokenB = b.tokens[index]; 134 135 if (tokenA === undefined && tokenB === undefined) { 136 return 0; 137 } 138 139 if (tokenA === undefined) { 140 if (tokenB === undefined) { 141 return 0; 142 } 143 const bIsNumeric = NUMERIC_TOKEN_PATTERN.test(tokenB); 144 return bIsNumeric ? -1 : 1; 145 } 146 147 if (tokenB === undefined) { 148 const aIsNumeric = NUMERIC_TOKEN_PATTERN.test(tokenA); 149 return aIsNumeric ? 1 : -1; 150 } 151 152 const aIsNumeric = NUMERIC_TOKEN_PATTERN.test(tokenA); 153 const bIsNumeric = NUMERIC_TOKEN_PATTERN.test(tokenB); 154 155 if (aIsNumeric && bIsNumeric) { 156 const diff = parseInt(tokenA, 10) - parseInt(tokenB, 10); 157 if (diff !== 0) { 158 return diff; 159 } 160 continue; 161 } 162 163 if (aIsNumeric && !bIsNumeric) { 164 return 1; 165 } 166 167 if (!aIsNumeric && bIsNumeric) { 168 return -1; 169 } 170 171 const tokenALower = tokenA.toLowerCase(); 172 const tokenBLower = tokenB.toLowerCase(); 173 if (tokenALower === tokenBLower) { 174 continue; 175 } 176 177 return tokenALower > tokenBLower ? 1 : -1; 178 } 179 180 return 0; 181 } 182 183 function updateSortIndicators() { 184 headers.forEach((header) => { 185 const sortKey = header.getAttribute("data-sort-key"); 186 if (!sortKey) { 187 return; 188 } 189 if (sortKey === state.sortKey) { 190 header.setAttribute( 191 "aria-sort", 192 state.sortOrder === "asc" ? "ascending" : "descending", 193 ); 194 } else { 195 header.setAttribute("aria-sort", "none"); 196 } 197 }); 198 } 199 200 function applyPagination() { 201 const totalPages = Math.max(1, Math.ceil(rows.length / state.perPage)); 202 if (state.page > totalPages) { 203 state.page = totalPages; 204 } 205 206 const start = (state.page - 1) * state.perPage; 207 const end = start + state.perPage; 208 209 baseRows.forEach((row) => { 210 row.style.display = "none"; 211 }); 212 213 if (statusEl) { 214 if (rows.length === 0) { 215 statusEl.textContent = i18n.noResults || "No results."; 216 } else { 217 const template = i18n.pageStatus || "Page %1$d of %2$d"; 218 statusEl.textContent = template 219 .replace("%1$d", String(state.page)) 220 .replace("%2$d", String(totalPages)); 221 } 222 } 223 224 if (prevBtn) { 225 prevBtn.disabled = state.page <= 1 || rows.length === 0; 226 } 227 228 if (nextBtn) { 229 nextBtn.disabled = state.page >= totalPages || rows.length === 0; 230 } 231 232 rows.forEach((row, index) => { 233 row.style.display = index >= start && index < end ? "" : "none"; 234 }); 235 } 236 237 function applySort() { 238 if (!state.sortKey) { 239 return; 240 } 241 242 const isVersionSort = VERSION_SORT_KEYS.has(state.sortKey); 243 244 const sortedRows = rows.slice().sort((a, b) => { 245 let comparison = 0; 246 247 if (NUMERIC_SORT_KEYS.has(state.sortKey)) { 248 const aValue = getSortValue(a, state.sortKey); 249 const bValue = getSortValue(b, state.sortKey); 250 comparison = aValue - bValue; 251 } else if (isVersionSort) { 252 const aVersion = parseVersionForSort( 253 getSortValue(a, state.sortKey, { raw: true }), 254 ); 255 const bVersion = parseVersionForSort( 256 getSortValue(b, state.sortKey, { raw: true }), 257 ); 258 comparison = compareVersionTokens(aVersion, bVersion); 259 } else { 260 const aValue = getSortValue(a, state.sortKey); 261 const bValue = getSortValue(b, state.sortKey); 262 263 if (aValue !== bValue) { 264 comparison = aValue > bValue ? 1 : -1; 265 } 266 } 267 268 if (state.sortOrder === "asc") { 269 return comparison; 270 } 271 272 return -comparison; 273 }); 274 275 rows = sortedRows; 276 277 sortedRows.forEach((row) => tbody.appendChild(row)); 278 } 279 280 function syncSortControls() { 281 if (sortSelect) { 282 const option = sortSelect.querySelector( 283 `[data-sort-key="${state.sortKey}"]`, 284 ); 285 if (option) { 286 sortSelect.value = option.value; 287 } 288 } 289 290 if (orderSelect) { 291 orderSelect.value = state.sortOrder; 292 } 293 } 294 295 function getFilterText(row) { 296 return (row.dataset.filterText || row.textContent || "").toLowerCase(); 297 } 298 299 function applyFilter() { 300 const query = state.filterQuery.trim().toLowerCase(); 301 if (!query) { 302 rows = baseRows.slice(); 303 } else { 304 rows = baseRows.filter((row) => getFilterText(row).includes(query)); 305 } 306 307 applySort(); 308 updateSortIndicators(); 309 syncSortControls(); 310 state.page = 1; 311 applyPagination(); 312 } 313 314 headers.forEach((header) => { 315 header.addEventListener("click", () => { 316 const sortKey = header.getAttribute("data-sort-key"); 317 if (!sortKey) { 318 return; 319 } 320 321 if (state.sortKey === sortKey) { 322 state.sortOrder = state.sortOrder === "asc" ? "desc" : "asc"; 323 } else { 324 state.sortKey = sortKey; 325 state.sortOrder = header.getAttribute("data-sort-default") || "asc"; 326 } 327 328 applySort(); 329 updateSortIndicators(); 330 syncSortControls(); 331 state.page = 1; 332 applyPagination(); 333 }); 334 335 header.setAttribute("aria-sort", "none"); 336 }); 337 338 if (searchInput) { 339 searchInput.addEventListener("input", () => { 340 state.filterQuery = searchInput.value; 341 applyFilter(); 342 }); 343 } 344 345 if (sortSelect) { 346 sortSelect.addEventListener("change", () => { 347 const option = sortSelect.selectedOptions[0]; 348 if (!option || !option.dataset.sortKey) { 349 return; 350 } 351 state.sortKey = option.dataset.sortKey; 352 state.sortOrder = orderSelect 353 ? orderSelect.value 354 : option.dataset.sortDefault || "asc"; 355 applySort(); 356 updateSortIndicators(); 357 syncSortControls(); 358 state.page = 1; 359 applyPagination(); 360 }); 361 } 362 363 if (orderSelect) { 364 orderSelect.addEventListener("change", () => { 365 state.sortOrder = orderSelect.value; 366 applySort(); 367 updateSortIndicators(); 368 syncSortControls(); 369 state.page = 1; 370 applyPagination(); 371 }); 372 } 373 374 if (prevBtn) { 375 prevBtn.addEventListener("click", () => { 376 if (state.page > 1) { 377 state.page -= 1; 378 applyPagination(); 379 } 380 }); 381 } 382 383 if (nextBtn) { 384 nextBtn.addEventListener("click", () => { 385 const totalPages = Math.max(1, Math.ceil(rows.length / state.perPage)); 386 if (state.page < totalPages) { 387 state.page += 1; 388 applyPagination(); 389 } 390 }); 391 } 392 393 applyFilter(); 394 } 395 396 function initNotificationToggles() { 397 document.querySelectorAll("[data-watchdog-toggle]").forEach((toggle) => { 398 const container = toggle.closest("[data-watchdog-notification]"); 399 if (!container) { 400 return; 401 } 402 403 const fieldWrapper = container.querySelector("[data-watchdog-fields]"); 404 if (!fieldWrapper) { 405 return; 406 } 407 408 // Hidden fields are still submitted, so a disabled channel keeps its saved values. 409 const syncState = () => { 410 fieldWrapper.hidden = !toggle.checked; 411 }; 412 413 toggle.addEventListener("change", syncState); 414 syncState(); 415 }); 416 } 417 418 function initFrequencyDescription() { 419 const select = document.getElementById( 420 "wp-watchdog-notification-frequency", 421 ); 422 const description = document.querySelector( 423 "[data-watchdog-frequency-description]", 424 ); 425 const optionContainer = document.querySelector( 426 "[data-watchdog-frequency-options]", 427 ); 428 429 if (!select || !description) { 430 return; 431 } 432 433 const defaultMessage = 434 description.getAttribute("data-default-message") || 435 description.textContent || 436 ""; 437 const testingMessage = 438 description.getAttribute("data-testing-message") || defaultMessage; 439 440 const updateMessage = () => { 441 if (select.value === "testing") { 442 description.textContent = testingMessage; 443 description.classList.add("wp-watchdog-frequency-description--testing"); 444 } else { 445 description.textContent = defaultMessage; 446 description.classList.remove( 447 "wp-watchdog-frequency-description--testing", 448 ); 449 } 450 }; 451 452 const updateOptionsVisibility = () => { 453 if (!optionContainer) { 454 return; 455 } 456 457 const active = select.value; 458 optionContainer 459 .querySelectorAll("[data-watchdog-frequency-target]") 460 .forEach((section) => { 461 const target = section.getAttribute("data-watchdog-frequency-target"); 462 const visible = target === active; 463 section.style.display = visible ? "" : "none"; 464 465 section.querySelectorAll("input, select").forEach((field) => { 466 field.disabled = !visible; 467 }); 468 }); 469 }; 470 471 select.addEventListener("change", () => { 472 updateMessage(); 473 updateOptionsVisibility(); 474 }); 475 476 updateMessage(); 477 updateOptionsVisibility(); 478 } 479 480 // WordPress exposes no variable for the admin accent color, and it differs 481 // between versions and color schemes, so take it from a primary button. 482 function initAccentColor() { 483 const root = document.querySelector(".wp-watchdog-admin"); 484 const primary = root ? root.querySelector(".button-primary") : null; 485 if (!primary) { 486 return; 487 } 488 489 const color = window.getComputedStyle(primary).backgroundColor; 490 if (color && color !== "transparent" && color !== "rgba(0, 0, 0, 0)") { 491 root.style.setProperty("--watchdog-accent", color); 492 } 493 } 494 495 function initTabs() { 496 const root = document.querySelector(".wp-watchdog-admin"); 497 if (!root) { 498 return; 499 } 500 501 const tabs = Array.from(root.querySelectorAll("[data-watchdog-tab]")); 502 const panels = Array.from(root.querySelectorAll("[data-watchdog-panel]")); 503 if (tabs.length === 0 || panels.length === 0) { 504 return; 505 } 506 507 const ids = panels.map((panel) => panel.id); 508 const storageKey = "siteAddOnWatchdogTab"; 509 510 const readRemembered = () => { 511 try { 512 return window.sessionStorage.getItem(storageKey); 513 } catch (error) { 514 return null; 515 } 516 }; 517 518 const activate = (id) => { 519 panels.forEach((panel) => { 520 panel.hidden = panel.id !== id; 521 }); 522 tabs.forEach((tab) => { 523 if (tab.dataset.watchdogTab === id) { 524 tab.setAttribute("aria-current", "true"); 525 } else { 526 tab.removeAttribute("aria-current"); 527 } 528 }); 529 530 try { 531 window.sessionStorage.setItem(storageKey, id); 532 } catch (error) { 533 // Storage can be unavailable in private windows; the tab still switches. 534 } 535 }; 536 537 // The address bar uses "#tab-risks" for the "watchdog-risks" panel. A hash 538 // that matched the panel id would make the browser scroll past the header. 539 const hashFor = (id) => `#${id.replace("watchdog-", "tab-")}`; 540 541 const panelFromHash = () => { 542 const hash = window.location.hash.slice(1); 543 if (!hash) { 544 return null; 545 } 546 547 const id = hash.replace(/^tab-/, "watchdog-"); 548 if (ids.includes(id)) { 549 return id; 550 } 551 552 const target = document.getElementById(hash); 553 const panel = target ? target.closest("[data-watchdog-panel]") : null; 554 return panel ? panel.id : null; 555 }; 556 557 const requested = root.dataset.watchdogInitialTab || ""; 558 const remembered = readRemembered(); 559 let initial = panelFromHash(); 560 if (!initial && ids.includes(requested)) { 561 initial = requested; 562 } 563 if (!initial && ids.includes(remembered)) { 564 initial = remembered; 565 } 566 567 root.classList.add("wp-watchdog-admin--js"); 568 activate(initial || ids[0]); 569 570 root.addEventListener("click", (event) => { 571 const link = event.target.closest('a[href^="#"]'); 572 if (!link) { 573 return; 574 } 575 576 const id = link.getAttribute("href").slice(1); 577 if (!ids.includes(id)) { 578 return; 579 } 580 581 event.preventDefault(); 582 activate(id); 583 if (window.history.replaceState) { 584 window.history.replaceState(null, "", hashFor(id)); 585 } 586 }); 587 588 window.addEventListener("hashchange", () => { 589 const id = panelFromHash(); 590 if (id) { 591 activate(id); 592 } 593 }); 594 } 595 596 function initCopyButtons() { 597 if (!navigator.clipboard || !navigator.clipboard.writeText) { 598 return; 599 } 600 601 document.querySelectorAll("[data-watchdog-copy]").forEach((button) => { 602 const source = document.getElementById(button.dataset.watchdogCopy); 603 if (!source) { 604 return; 605 } 606 607 const label = button.textContent; 608 button.hidden = false; 609 button.addEventListener("click", () => { 610 navigator.clipboard 611 .writeText(source.textContent.trim()) 612 .then(() => { 613 button.textContent = i18n.copied || "Copied"; 614 window.setTimeout(() => { 615 button.textContent = label; 616 }, 2000); 617 }) 618 .catch(() => { 619 // Clipboard permission was denied; the text can still be selected by hand. 620 }); 621 }); 622 }); 623 } 624 625 function initFormBusyState() { 626 // Downloads leave the page in place, so those forms must stay usable. 627 document 628 .querySelectorAll(".wp-watchdog-admin form:not([data-watchdog-download])") 629 .forEach((form) => { 630 form.addEventListener("submit", (event) => { 631 if (form.dataset.submitting === "true") { 632 event.preventDefault(); 12 633 return; 13 } 14 15 const perPage = parseInt(tableWrapper.dataset.perPage || '10', 10); 16 const baseRows = Array.from(tbody.querySelectorAll('tr')); 17 let rows = baseRows.slice(); 18 const pagination = tableWrapper.querySelector('[data-pagination]'); 19 const statusEl = tableWrapper.querySelector('[data-page-status]'); 20 const prevBtn = pagination ? pagination.querySelector('[data-action="prev"]') : null; 21 const nextBtn = pagination ? pagination.querySelector('[data-action="next"]') : null; 22 const headers = Array.from(tableWrapper.querySelectorAll('[data-sort-key]')); 23 const searchInput = tableWrapper.querySelector('[data-risk-search]'); 24 const sortSelect = tableWrapper.querySelector('[data-risk-sort]'); 25 const orderSelect = tableWrapper.querySelector('[data-risk-order]'); 26 27 const state = { 28 page: 1, 29 perPage: Number.isFinite(perPage) && perPage > 0 ? perPage : rows.length, 30 sortKey: null, 31 sortOrder: 'asc', 32 filterQuery: searchInput ? searchInput.value.trim() : '' 33 }; 34 35 const initialHeader = headers.find((header) => header.hasAttribute('data-sort-initial')); 36 if (initialHeader) { 37 state.sortKey = initialHeader.getAttribute('data-sort-key'); 38 state.sortOrder = initialHeader.getAttribute('data-sort-default') || 'asc'; 39 } 40 41 const VERSION_SORT_KEYS = new Set(['sortLocal', 'sortRemote']); 42 const NUMERIC_SORT_KEYS = new Set(['sortRiskCount', 'sortVersionGap']); 43 const NUMERIC_TOKEN_PATTERN = /^\d+$/; 44 45 if (sortSelect) { 46 const selectedOption = sortSelect.selectedOptions[0]; 47 if (selectedOption && selectedOption.dataset.sortKey) { 48 state.sortKey = selectedOption.dataset.sortKey; 49 state.sortOrder = orderSelect 50 ? orderSelect.value 51 : (selectedOption.dataset.sortDefault || 'asc'); 52 } 53 } 54 55 function getSortValue(row, key, options = {}) { 56 const { raw = false } = options; 57 58 if (NUMERIC_SORT_KEYS.has(key)) { 59 const datasetKey = key.replace(/-([a-z])/g, (_, char) => char.toUpperCase()); 60 const value = row.dataset[datasetKey]; 61 const parsed = value ? parseFloat(value) : 0; 62 return Number.isFinite(parsed) ? parsed : 0; 63 } 64 65 if (VERSION_SORT_KEYS.has(key)) { 66 const columnKey = key === 'sortLocal' ? 'local' : 'remote'; 67 const text = (row.querySelector(`[data-column="${columnKey}"]`)?.textContent || '').trim(); 68 if (!text) { 69 return raw ? '' : null; 70 } 71 if (text.toLowerCase() === 'n/a') { 72 return raw ? 'N/A' : null; 73 } 74 return raw ? text : text.toLowerCase(); 75 } 76 77 const datasetKey = key.replace(/-([a-z])/g, (_, char) => char.toUpperCase()); 78 const value = row.dataset[datasetKey]; 79 if (value !== undefined) { 80 return raw ? value : value.toLowerCase(); 81 } 82 const fallback = (row.querySelector(`[data-column="${key}"]`)?.textContent || '').trim(); 83 return raw ? fallback : fallback.toLowerCase(); 84 } 85 86 function parseVersionForSort(value) { 87 if (typeof value !== 'string') { 88 return { tokens: [], isMissing: true }; 89 } 90 91 const trimmed = value.trim(); 92 if (!trimmed || trimmed.toLowerCase() === 'n/a') { 93 return { tokens: [], isMissing: true }; 94 } 95 96 const tokens = trimmed.match(/[0-9]+|[a-zA-Z]+/g) || []; 97 return { tokens, isMissing: tokens.length === 0 }; 98 } 99 100 function compareVersionTokens(a, b) { 101 if (a.isMissing && b.isMissing) { 102 return 0; 103 } 104 105 if (a.isMissing) { 106 return 1; 107 } 108 109 if (b.isMissing) { 110 return -1; 111 } 112 113 const maxLength = Math.max(a.tokens.length, b.tokens.length); 114 for (let index = 0; index < maxLength; index += 1) { 115 const tokenA = a.tokens[index]; 116 const tokenB = b.tokens[index]; 117 118 if (tokenA === undefined && tokenB === undefined) { 119 return 0; 120 } 121 122 if (tokenA === undefined) { 123 if (tokenB === undefined) { 124 return 0; 125 } 126 const bIsNumeric = NUMERIC_TOKEN_PATTERN.test(tokenB); 127 return bIsNumeric ? -1 : 1; 128 } 129 130 if (tokenB === undefined) { 131 const aIsNumeric = NUMERIC_TOKEN_PATTERN.test(tokenA); 132 return aIsNumeric ? 1 : -1; 133 } 134 135 const aIsNumeric = NUMERIC_TOKEN_PATTERN.test(tokenA); 136 const bIsNumeric = NUMERIC_TOKEN_PATTERN.test(tokenB); 137 138 if (aIsNumeric && bIsNumeric) { 139 const diff = parseInt(tokenA, 10) - parseInt(tokenB, 10); 140 if (diff !== 0) { 141 return diff; 142 } 143 continue; 144 } 145 146 if (aIsNumeric && !bIsNumeric) { 147 return 1; 148 } 149 150 if (!aIsNumeric && bIsNumeric) { 151 return -1; 152 } 153 154 const tokenALower = tokenA.toLowerCase(); 155 const tokenBLower = tokenB.toLowerCase(); 156 if (tokenALower === tokenBLower) { 157 continue; 158 } 159 160 return tokenALower > tokenBLower ? 1 : -1; 161 } 162 163 return 0; 164 } 165 166 function updateSortIndicators() { 167 headers.forEach((header) => { 168 const sortKey = header.getAttribute('data-sort-key'); 169 if (!sortKey) { 170 return; 171 } 172 if (sortKey === state.sortKey) { 173 header.setAttribute('aria-sort', state.sortOrder === 'asc' ? 'ascending' : 'descending'); 174 } else { 175 header.setAttribute('aria-sort', 'none'); 176 } 177 }); 178 } 179 180 function applyPagination() { 181 const totalPages = Math.max(1, Math.ceil(rows.length / state.perPage)); 182 if (state.page > totalPages) { 183 state.page = totalPages; 184 } 185 186 const start = (state.page - 1) * state.perPage; 187 const end = start + state.perPage; 188 189 baseRows.forEach((row) => { 190 row.style.display = 'none'; 191 }); 192 193 if (statusEl) { 194 if (rows.length === 0) { 195 statusEl.textContent = i18n.noResults || 'No results.'; 196 } else { 197 const template = i18n.pageStatus || 'Page %1$d of %2$d'; 198 statusEl.textContent = template 199 .replace('%1$d', String(state.page)) 200 .replace('%2$d', String(totalPages)); 201 } 202 } 203 204 if (prevBtn) { 205 prevBtn.disabled = state.page <= 1 || rows.length === 0; 206 } 207 208 if (nextBtn) { 209 nextBtn.disabled = state.page >= totalPages || rows.length === 0; 210 } 211 212 rows.forEach((row, index) => { 213 row.style.display = index >= start && index < end ? '' : 'none'; 214 }); 215 } 216 217 function applySort() { 218 if (!state.sortKey) { 219 return; 220 } 221 222 const isVersionSort = VERSION_SORT_KEYS.has(state.sortKey); 223 224 const sortedRows = rows.slice().sort((a, b) => { 225 let comparison = 0; 226 227 if (NUMERIC_SORT_KEYS.has(state.sortKey)) { 228 const aValue = getSortValue(a, state.sortKey); 229 const bValue = getSortValue(b, state.sortKey); 230 comparison = aValue - bValue; 231 } else if (isVersionSort) { 232 const aVersion = parseVersionForSort(getSortValue(a, state.sortKey, { raw: true })); 233 const bVersion = parseVersionForSort(getSortValue(b, state.sortKey, { raw: true })); 234 comparison = compareVersionTokens(aVersion, bVersion); 235 } else { 236 const aValue = getSortValue(a, state.sortKey); 237 const bValue = getSortValue(b, state.sortKey); 238 239 if (aValue !== bValue) { 240 comparison = aValue > bValue ? 1 : -1; 241 } 242 } 243 244 if (state.sortOrder === 'asc') { 245 return comparison; 246 } 247 248 return -comparison; 249 }); 250 251 rows = sortedRows; 252 253 sortedRows.forEach((row) => tbody.appendChild(row)); 254 } 255 256 function syncSortControls() { 257 if (sortSelect) { 258 const option = sortSelect.querySelector(`[data-sort-key="${state.sortKey}"]`); 259 if (option) { 260 sortSelect.value = option.value; 261 } 262 } 263 264 if (orderSelect) { 265 orderSelect.value = state.sortOrder; 266 } 267 } 268 269 function getFilterText(row) { 270 return (row.dataset.filterText || row.textContent || '').toLowerCase(); 271 } 272 273 function applyFilter() { 274 const query = state.filterQuery.trim().toLowerCase(); 275 if (!query) { 276 rows = baseRows.slice(); 277 } else { 278 rows = baseRows.filter((row) => getFilterText(row).includes(query)); 279 } 280 281 applySort(); 282 updateSortIndicators(); 283 syncSortControls(); 284 state.page = 1; 285 applyPagination(); 286 } 287 288 headers.forEach((header) => { 289 header.addEventListener('click', () => { 290 const sortKey = header.getAttribute('data-sort-key'); 291 if (!sortKey) { 292 return; 293 } 294 295 if (state.sortKey === sortKey) { 296 state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc'; 297 } else { 298 state.sortKey = sortKey; 299 state.sortOrder = header.getAttribute('data-sort-default') || 'asc'; 300 } 301 302 applySort(); 303 updateSortIndicators(); 304 syncSortControls(); 305 state.page = 1; 306 applyPagination(); 307 }); 308 309 header.setAttribute('aria-sort', 'none'); 634 } 635 636 form.dataset.submitting = "true"; 637 form.classList.add("is-busy"); 638 639 const submitter = event.submitter; 640 if (submitter) { 641 submitter.setAttribute("aria-busy", "true"); 642 } 643 644 // Delay disabling buttons so the clicked channel-test value remains in the request. 645 window.setTimeout(() => { 646 form 647 .querySelectorAll('button[type="submit"], input[type="submit"]') 648 .forEach((button) => { 649 button.disabled = true; 650 }); 651 }, 0); 310 652 }); 311 312 if (searchInput) { 313 searchInput.addEventListener('input', () => { 314 state.filterQuery = searchInput.value; 315 applyFilter(); 316 }); 317 } 318 319 if (sortSelect) { 320 sortSelect.addEventListener('change', () => { 321 const option = sortSelect.selectedOptions[0]; 322 if (!option || !option.dataset.sortKey) { 323 return; 324 } 325 state.sortKey = option.dataset.sortKey; 326 state.sortOrder = orderSelect ? orderSelect.value : (option.dataset.sortDefault || 'asc'); 327 applySort(); 328 updateSortIndicators(); 329 syncSortControls(); 330 state.page = 1; 331 applyPagination(); 332 }); 333 } 334 335 if (orderSelect) { 336 orderSelect.addEventListener('change', () => { 337 state.sortOrder = orderSelect.value; 338 applySort(); 339 updateSortIndicators(); 340 syncSortControls(); 341 state.page = 1; 342 applyPagination(); 343 }); 344 } 345 346 if (prevBtn) { 347 prevBtn.addEventListener('click', () => { 348 if (state.page > 1) { 349 state.page -= 1; 350 applyPagination(); 351 } 352 }); 353 } 354 355 if (nextBtn) { 356 nextBtn.addEventListener('click', () => { 357 const totalPages = Math.max(1, Math.ceil(rows.length / state.perPage)); 358 if (state.page < totalPages) { 359 state.page += 1; 360 applyPagination(); 361 } 362 }); 363 } 364 365 applyFilter(); 366 } 367 368 function initNotificationToggles() { 369 document.querySelectorAll('[data-watchdog-toggle]').forEach((toggle) => { 370 const container = toggle.closest('[data-watchdog-notification]'); 371 if (!container) { 372 return; 373 } 374 375 const fieldWrapper = container.querySelector('[data-watchdog-fields]'); 376 if (!fieldWrapper) { 377 return; 378 } 379 380 const controlledFields = fieldWrapper.querySelectorAll('input, textarea, select'); 381 382 const syncState = () => { 383 const disabled = !toggle.checked; 384 fieldWrapper.classList.toggle('wp-watchdog-notification-fields--disabled', disabled); 385 386 controlledFields.forEach((field) => { 387 if ('readOnly' in field) { 388 field.readOnly = disabled; 389 } 390 391 if (disabled) { 392 field.setAttribute('aria-disabled', 'true'); 393 } else { 394 field.removeAttribute('aria-disabled'); 395 } 396 }); 397 }; 398 399 toggle.addEventListener('change', syncState); 400 syncState(); 401 }); 402 } 403 404 function initFrequencyDescription() { 405 const select = document.getElementById('wp-watchdog-notification-frequency'); 406 const description = document.querySelector('[data-watchdog-frequency-description]'); 407 const optionContainer = document.querySelector('[data-watchdog-frequency-options]'); 408 409 if (!select || !description) { 410 return; 411 } 412 413 const defaultMessage = description.getAttribute('data-default-message') || description.textContent || ''; 414 const testingMessage = description.getAttribute('data-testing-message') || defaultMessage; 415 416 const updateMessage = () => { 417 if (select.value === 'testing') { 418 description.textContent = testingMessage; 419 description.classList.add('wp-watchdog-frequency-description--testing'); 420 } else { 421 description.textContent = defaultMessage; 422 description.classList.remove('wp-watchdog-frequency-description--testing'); 423 } 424 }; 425 426 const updateOptionsVisibility = () => { 427 if (!optionContainer) { 428 return; 429 } 430 431 const active = select.value; 432 optionContainer.querySelectorAll('[data-watchdog-frequency-target]').forEach((section) => { 433 const target = section.getAttribute('data-watchdog-frequency-target'); 434 const visible = target === active; 435 section.style.display = visible ? '' : 'none'; 436 437 section.querySelectorAll('input, select').forEach((field) => { 438 field.disabled = !visible; 439 }); 440 }); 441 }; 442 443 select.addEventListener('change', () => { 444 updateMessage(); 445 updateOptionsVisibility(); 446 }); 447 448 updateMessage(); 449 updateOptionsVisibility(); 450 } 451 452 function initFormBusyState() { 453 document.querySelectorAll('.wp-watchdog-admin form').forEach((form) => { 454 form.addEventListener('submit', (event) => { 455 if (form.dataset.submitting === 'true') { 456 event.preventDefault(); 457 return; 458 } 459 460 form.dataset.submitting = 'true'; 461 form.classList.add('is-busy'); 462 463 const submitter = event.submitter; 464 if (submitter) { 465 submitter.setAttribute('aria-busy', 'true'); 466 } 467 468 // Delay disabling buttons so the clicked channel-test value remains in the request. 469 window.setTimeout(() => { 470 form.querySelectorAll('button[type="submit"], input[type="submit"]').forEach((button) => { 471 button.disabled = true; 472 }); 473 }, 0); 474 }); 475 }); 476 } 477 478 window.addEventListener('DOMContentLoaded', () => { 479 document.querySelectorAll('[data-wp-watchdog-table]').forEach((tableWrapper) => { 480 initTable(tableWrapper); 481 }); 482 483 initNotificationToggles(); 484 initFrequencyDescription(); 485 initFormBusyState(); 486 }); 653 }); 654 } 655 656 window.addEventListener("DOMContentLoaded", () => { 657 initAccentColor(); 658 initTabs(); 659 660 document 661 .querySelectorAll("[data-wp-watchdog-table]") 662 .forEach((tableWrapper) => { 663 initTable(tableWrapper); 664 }); 665 666 initNotificationToggles(); 667 initFrequencyDescription(); 668 initCopyButtons(); 669 initFormBusyState(); 670 }); 487 671 })(); -
site-add-on-watchdog/trunk/languages/site-add-on-watchdog.pot
r3770137 r3799692 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: Site Add-on Watchdog 1. 9.0\n"5 "Project-Id-Version: Site Add-on Watchdog 1.10.0\n" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/site-add-on-watchdog\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-1 0-28T07:35:21+00:00\n"12 "POT-Creation-Date: 2026-11-13T04:50:43+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 #: site-add-on-watchdog.php 19 #: src/AdminPage.php: 6019 #: src/AdminPage.php:82 20 20 #: src/Notifications/MessageFormatter.php:293 21 21 #: src/Notifications/MessageFormatter.php:376 22 #: templates/admin-page.php: 5722 #: templates/admin-page.php:171 23 23 msgid "Site Add-on Watchdog" 24 24 msgstr "" … … 39 39 msgstr "" 40 40 41 #: site-add-on-watchdog.php:2 141 #: site-add-on-watchdog.php:20 42 42 msgid "Site Add-on Watchdog requires PHP 8.1 or higher. The plugin has been disabled." 43 43 msgstr "" 44 44 45 #: site-add-on-watchdog.php:7 445 #: site-add-on-watchdog.php:73 46 46 msgid "Site Add-on Watchdog could not start. The rest of the site is still available; check the PHP error log and reinstall the plugin files." 47 47 msgstr "" 48 48 49 #: src/AdminPage.php: 6149 #: src/AdminPage.php:83 50 50 msgid "Watchdog" 51 51 msgstr "" 52 52 53 #: src/AdminPage.php: 7253 #: src/AdminPage.php:94 54 54 msgid "You do not have permission to access this page." 55 55 msgstr "" 56 56 57 #: src/AdminPage.php:2 1657 #: src/AdminPage.php:248 58 58 msgid "History retention cannot exceed 15 scans. The value has been limited to 15." 59 59 msgstr "" 60 60 61 #: src/AdminPage.php:3 2361 #: src/AdminPage.php:353 62 62 msgid "No failed notification payload is available." 63 63 msgstr "" 64 64 65 #: src/AdminPage.php:3 4265 #: src/AdminPage.php:372 66 66 msgid "Invalid history request." 67 67 msgstr "" 68 68 69 #: src/AdminPage.php:3 5369 #: src/AdminPage.php:383 70 70 msgid "History entry not found." 71 71 msgstr "" 72 72 73 #: src/AdminPage.php:3 6873 #: src/AdminPage.php:398 74 74 msgid "You do not have permission to perform this action." 75 75 msgstr "" 76 76 77 77 #. translators: 1: current page number, 2: total number of pages. 78 #: src/AdminPage.php:4 0878 #: src/AdminPage.php:438 79 79 #, php-format 80 80 msgid "Page %1$d of %2$d" 81 81 msgstr "" 82 82 83 #: src/AdminPage.php:4 0983 #: src/AdminPage.php:439 84 84 msgid "No risks match your search." 85 85 msgstr "" 86 86 87 #: src/AdminPage.php:4 9388 msgid " Unable to generate history export."87 #: src/AdminPage.php:440 88 msgid "Copied" 89 89 msgstr "" 90 90 91 91 #: src/Bootstrap.php:97 92 #: templates/admin-page.php: 8692 #: templates/admin-page.php:161 93 93 msgid "Settings" 94 94 msgstr "" … … 141 141 #: src/Notifications/MessageFormatter.php:402 142 142 #: src/Notifications/MessageFormatter.php:459 143 #: templates/admin-page.php: 568143 #: templates/admin-page.php:473 144 144 msgid "N/A" 145 145 msgstr "" … … 172 172 #: src/Notifications/MessageFormatter.php:226 173 173 #: src/Notifications/MessageFormatter.php:385 174 #: templates/admin-page.php:355 174 175 msgid "Review updates" 175 176 msgstr "" … … 223 224 224 225 #: src/Notifications/MessageFormatter.php:348 225 #: templates/admin-page.php: 409226 #: templates/admin-page.php:375 226 227 msgid "Directory Version" 227 228 msgstr "" … … 242 243 #: src/Notifications/MessageFormatter.php:401 243 244 #: src/Notifications/MessageFormatter.php:458 244 #: templates/admin-page.php: 505245 #: templates/admin-page.php:384 245 246 msgid "Directory version" 246 247 msgstr "" … … 249 250 #: src/Notifications/MessageFormatter.php:425 250 251 #: src/Notifications/MessageFormatter.php:483 251 #: templates/admin-page.php: 641252 #: templates/admin-page.php:499 252 253 #, php-format 253 254 msgid "Fixed in %s" … … 255 256 256 257 #: src/Notifications/MessageFormatter.php:465 257 #: templates/admin-page.php: 410258 #: templates/admin-page.php: 513258 #: templates/admin-page.php:376 259 #: templates/admin-page.php:385 259 260 msgid "Reasons" 260 261 msgstr "" … … 281 282 282 283 #: src/Notifier.php:149 283 #: templates/admin-page.php: 180284 #: templates/admin-page.php:55 284 285 msgid "Custom webhook" 285 286 msgstr "" … … 325 326 msgstr "" 326 327 327 #: src/Plugin.php:15 1328 #: src/Plugin.php:154 328 329 msgid "The scan could not be completed. No saved results were changed; please try again." 329 330 msgstr "" 330 331 331 #: src/Plugin.php:24 0332 #: src/Plugin.php:245 332 333 msgid "Once Weekly" 333 334 msgstr "" 334 335 335 #: src/Plugin.php:2 47336 #: src/Plugin.php:25 4336 #: src/Plugin.php:252 337 #: src/Plugin.php:259 337 338 msgid "Every 5 Minutes (Watchdog queue)" 338 339 msgstr "" 339 340 340 341 #. translators: %d: interval minutes 341 #: src/Plugin.php:26 3342 #: src/Plugin.php:268 342 343 #, php-format 343 344 msgid "Every %d Minutes (testing)" 344 345 msgstr "" 345 346 346 #: src/Plugin.php:35 2347 msgid " WP-Cron appears disabled. Set a system cron job to trigger wp-cron.php for Site Add-on Watchdog."348 msgstr "" 349 350 #: src/Plugin.php:3 63347 #: src/Plugin.php:350 348 msgid "The scheduled Site Add-on Watchdog scan is overdue and WP-Cron is disabled on this site. Make sure a system cron job calls wp-cron.php." 349 msgstr "" 350 351 #: src/Plugin.php:355 351 352 msgid "Site Add-on Watchdog scans are overdue. Ensure system cron calls wp-cron.php regularly." 352 353 msgstr "" 353 354 354 #: src/Plugin.php:6 56355 #: src/Plugin.php:668 355 356 msgid "Notifications processed." 356 357 msgstr "" 357 358 358 #: src/Plugin.php:6 67359 #: src/Plugin.php:679 359 360 msgid "Scan triggered successfully." 360 361 msgstr "" 361 362 362 #: src/Plugin.php: 710363 #: src/Plugin.php:693 363 364 msgid "The cron endpoint only accepts POST requests." 364 365 msgstr "" 365 366 367 #: src/Plugin.php:702 368 msgid "Cron secret is missing. Please resave your settings." 369 msgstr "" 370 371 #: src/Plugin.php:711 372 msgid "Invalid cron secret." 373 msgstr "" 374 366 375 #: src/Plugin.php:719 367 msgid "Cron secret is missing. Please resave your settings."368 msgstr ""369 370 #: src/Plugin.php:728371 msgid "Invalid cron secret."372 msgstr ""373 374 #: src/Plugin.php:736375 376 msgid "Force requests require header authentication." 376 377 msgstr "" 377 378 378 #: src/Scanner.php: 89379 #: src/Scanner.php:90 379 380 msgid "An update is available in the plugin directory." 380 381 msgstr "" 381 382 382 #: src/Scanner.php:10 0383 #: src/Scanner.php:101 383 384 msgid "Local version is more than two minor releases behind the directory version." 384 385 msgstr "" 385 386 386 #: src/Scanner.php:11 5387 #: src/Scanner.php:116 387 388 msgid "Changelog mentions security-related updates." 388 389 msgstr "" 389 390 390 #: src/Scanner.php:13 1391 #: src/Scanner.php:132 391 392 msgid "Active vulnerabilities reported by WPScan." 392 393 msgstr "" 393 394 394 #: src/Scanner.php:19 0395 #: src/Scanner.php:191 395 396 msgid "Severe" 396 397 msgstr "" 397 398 398 #: src/Scanner.php:19 7399 #: src/Scanner.php:198 399 400 msgid "High" 400 401 msgstr "" 401 402 402 #: src/Scanner.php:20 4403 #: src/Scanner.php:205 403 404 msgid "Medium" 404 405 msgstr "" 405 406 406 #: src/Scanner.php:21 0407 #: src/Scanner.php:211 407 408 msgid "Low" 408 409 msgstr "" … … 436 437 msgstr "" 437 438 438 #: templates/admin-page.php:38 439 #: templates/admin-page.php:751 439 #: templates/admin-page.php:51 440 msgid "Email" 441 msgstr "" 442 443 #: templates/admin-page.php:52 444 msgid "Discord" 445 msgstr "" 446 447 #: templates/admin-page.php:53 448 msgid "Slack" 449 msgstr "" 450 451 #: templates/admin-page.php:54 452 msgid "Microsoft Teams" 453 msgstr "" 454 455 #: templates/admin-page.php:65 456 #: templates/admin-page.php:594 440 457 msgid "Daily" 441 458 msgstr "" 442 459 443 #: templates/admin-page.php: 39444 #: templates/admin-page.php: 752460 #: templates/admin-page.php:66 461 #: templates/admin-page.php:595 445 462 msgid "Weekly" 446 463 msgstr "" 447 464 448 #: templates/admin-page.php: 40465 #: templates/admin-page.php:67 449 466 msgid "Testing" 450 467 msgstr "" 451 468 452 #: templates/admin-page.php:41 469 #: templates/admin-page.php:68 470 #: templates/admin-page.php:120 453 471 msgid "Manual" 454 472 msgstr "" 455 473 456 #: templates/admin-page.php:51 457 msgid "Plugin health and security" 458 msgstr "" 459 460 #: templates/admin-page.php:59 461 msgid "Monitor plugin health, review scan history, and verify every alert channel from one focused dashboard." 462 msgstr "" 463 464 #: templates/admin-page.php:67 474 #: templates/admin-page.php:77 475 msgid "No scan has run yet. Run a manual scan to create your first report." 476 msgstr "" 477 478 #. translators: %s: number of plugins. 479 #: templates/admin-page.php:81 480 #, php-format 481 msgid "%s plugin needs attention." 482 msgid_plural "%s plugins need attention." 483 msgstr[0] "" 484 msgstr[1] "" 485 486 #: templates/admin-page.php:85 487 msgid "No risks found in the latest scan." 488 msgstr "" 489 490 #: templates/admin-page.php:91 491 msgid "Not scanned yet" 492 msgstr "" 493 494 #: templates/admin-page.php:94 495 msgid "All clear" 496 msgstr "" 497 498 #. translators: %s: number of vulnerabilities. 499 #: templates/admin-page.php:99 500 #, php-format 501 msgid "%s known vulnerability" 502 msgid_plural "%s known vulnerabilities" 503 msgstr[0] "" 504 msgstr[1] "" 505 506 #: templates/admin-page.php:104 507 msgid "Updates available" 508 msgstr "" 509 510 #: templates/admin-page.php:108 511 msgid "Never" 512 msgstr "" 513 514 #: templates/admin-page.php:109 515 msgid "Run a manual scan" 516 msgstr "" 517 518 #. translators: %s: human-readable time difference, for example "2 hours". 519 #: templates/admin-page.php:113 520 #, php-format 521 msgid "%s ago" 522 msgstr "" 523 524 #: templates/admin-page.php:121 525 msgid "Automatic scans are off" 526 msgstr "" 527 528 #: templates/admin-page.php:123 529 msgid "Not scheduled" 530 msgstr "" 531 532 #: templates/admin-page.php:124 533 msgid "Save the settings to reschedule" 534 msgstr "" 535 536 #. translators: %s: human-readable time difference, for example "2 hours". 537 #: templates/admin-page.php:129 538 #, php-format 539 msgid "In %s" 540 msgstr "" 541 542 #: templates/admin-page.php:132 543 msgid "Due now" 544 msgstr "" 545 546 #. translators: 1: scan frequency, 2: date and time. 547 #: templates/admin-page.php:135 548 #, php-format 549 msgid "%1$s, %2$s" 550 msgstr "" 551 552 #: templates/admin-page.php:144 553 msgid "Last delivery failed" 554 msgstr "" 555 556 #. translators: %s: number of queued alerts. 557 #: templates/admin-page.php:148 558 #, php-format 559 msgid "%s alert queued" 560 msgid_plural "%s alerts queued" 561 msgstr[0] "" 562 msgstr[1] "" 563 564 #: templates/admin-page.php:153 565 msgid "No channel enabled" 566 msgstr "" 567 568 #: templates/admin-page.php:155 569 msgid "No failed deliveries" 570 msgstr "" 571 572 #: templates/admin-page.php:159 573 msgid "Risks" 574 msgstr "" 575 576 #: templates/admin-page.php:160 577 msgid "History" 578 msgstr "" 579 580 #: templates/admin-page.php:162 581 msgid "Delivery" 582 msgstr "" 583 584 #: templates/admin-page.php:183 585 msgid "Send notifications now" 586 msgstr "" 587 588 #: templates/admin-page.php:191 465 589 msgid "Run manual scan" 466 590 msgstr "" 467 591 468 #: templates/admin-page.php:76 469 msgid "Send notifications now" 470 msgstr "" 471 472 #: templates/admin-page.php:82 592 #: templates/admin-page.php:213 593 msgid "Cron secret has not been initialized or could not be saved. Please try saving the settings again." 594 msgstr "" 595 596 #. translators: 1: WPScan response code, 2: message 597 #: templates/admin-page.php:223 598 #, php-format 599 msgid "WPScan error (%1$s): %2$s" 600 msgstr "" 601 602 #: templates/admin-page.php:235 603 msgid "Settings saved." 604 msgstr "" 605 606 #: templates/admin-page.php:239 607 msgid "Manual scan completed." 608 msgstr "" 609 610 #: templates/admin-page.php:243 611 msgid "Notifications were dispatched." 612 msgstr "" 613 614 #: templates/admin-page.php:245 615 msgid "Notifications skipped to avoid rapid re-sends. Please wait a moment and try again." 616 msgstr "" 617 618 #: templates/admin-page.php:249 619 msgid "Queued the captured notification payload for resend." 620 msgstr "" 621 622 #: templates/admin-page.php:251 623 msgid "No failed notification payload was available to resend." 624 msgstr "" 625 626 #: templates/admin-page.php:257 627 msgid "Notification channel" 628 msgstr "" 629 630 #: templates/admin-page.php:263 631 msgid "WordPress accepted the test email for sending. This does not confirm inbox delivery. If it does not arrive, check spam and your site mail or SMTP configuration." 632 msgstr "" 633 634 #. translators: %s: notification channel name. 635 #: templates/admin-page.php:267 636 #, php-format 637 msgid "%s test notification sent successfully." 638 msgstr "" 639 640 #. translators: %s: notification channel name. 641 #: templates/admin-page.php:278 642 #, php-format 643 msgid "%s could not be tested until its settings are corrected." 644 msgstr "" 645 646 #. translators: %s: notification channel name. 647 #: templates/admin-page.php:288 648 #, php-format 649 msgid "%s is not configured yet." 650 msgstr "" 651 652 #. translators: %s: notification channel name. 653 #: templates/admin-page.php:298 654 #, php-format 655 msgid "%s test delivery failed. Open the Delivery tab for details." 656 msgstr "" 657 658 #: templates/admin-page.php:306 659 msgid "Watchdog overview" 660 msgstr "" 661 662 #: templates/admin-page.php:309 663 msgid "Open risks" 664 msgstr "" 665 666 #: templates/admin-page.php:315 667 msgid "Last scan" 668 msgstr "" 669 670 #: templates/admin-page.php:321 671 msgid "Next scan" 672 msgstr "" 673 674 #: templates/admin-page.php:327 675 msgid "Active channels" 676 msgstr "" 677 678 #: templates/admin-page.php:333 473 679 msgid "Watchdog sections" 474 680 msgstr "" 475 681 476 #: templates/admin-page.php:83 477 msgid "Risks" 478 msgstr "" 479 480 #: templates/admin-page.php:84 481 msgid "History" 482 msgstr "" 483 484 #: templates/admin-page.php:85 485 msgid "Delivery" 486 msgstr "" 487 488 #: templates/admin-page.php:89 489 msgid "Watchdog overview" 490 msgstr "" 491 492 #: templates/admin-page.php:93 493 msgid "Open risks" 494 msgstr "" 495 496 #: templates/admin-page.php:98 497 msgid "Active channels" 498 msgstr "" 499 500 #: templates/admin-page.php:103 501 msgid "Queued alerts" 502 msgstr "" 503 504 #: templates/admin-page.php:108 505 msgid "Scan schedule" 506 msgstr "" 507 508 #: templates/admin-page.php:126 509 msgid "Cron secret has not been initialized or could not be saved. Please try saving the settings again." 510 msgstr "" 511 512 #. translators: 1: WPScan response code, 2: message 513 #: templates/admin-page.php:136 514 #, php-format 515 msgid "WPScan error (%1$s): %2$s" 516 msgstr "" 517 518 #: templates/admin-page.php:148 519 msgid "Settings saved." 520 msgstr "" 521 522 #: templates/admin-page.php:152 523 msgid "Manual scan completed." 524 msgstr "" 525 526 #: templates/admin-page.php:157 527 msgid "Notifications were dispatched." 528 msgstr "" 529 530 #: templates/admin-page.php:159 531 msgid "Notifications skipped to avoid rapid re-sends. Please wait a moment and try again." 532 msgstr "" 533 534 #: templates/admin-page.php:161 535 msgid "No notification changes detected since the last send." 536 msgstr "" 537 538 #: templates/admin-page.php:167 539 msgid "Queued the captured notification payload for resend." 540 msgstr "" 541 542 #: templates/admin-page.php:169 543 msgid "No failed notification payload was available to resend." 544 msgstr "" 545 546 #: templates/admin-page.php:176 682 #: templates/admin-page.php:341 683 msgid "Needs attention" 684 msgstr "" 685 686 #: templates/admin-page.php:351 687 msgid "Potential Risks" 688 msgstr "" 689 690 #: templates/admin-page.php:352 691 msgid "Plugins with a newer release, a security note in the latest changelog, or a known vulnerability." 692 msgstr "" 693 694 #: templates/admin-page.php:363 695 msgid "No scan results yet" 696 msgstr "" 697 547 698 #: templates/admin-page.php:364 548 msgid "Email" 549 msgstr "" 550 551 #: templates/admin-page.php:177 699 msgid "Run a manual scan to check your plugins now." 700 msgstr "" 701 702 #: templates/admin-page.php:366 703 msgid "No risks detected." 704 msgstr "" 705 552 706 #: templates/admin-page.php:367 553 msgid "Discord" 554 msgstr "" 555 556 #: templates/admin-page.php:178 557 #: templates/admin-page.php:365 558 msgid "Slack" 559 msgstr "" 560 561 #: templates/admin-page.php:179 562 #: templates/admin-page.php:366 563 msgid "Microsoft Teams" 564 msgstr "" 565 566 #: templates/admin-page.php:183 567 msgid "Notification channel" 568 msgstr "" 569 570 #: templates/admin-page.php:189 571 msgid "WordPress accepted the test email for sending. This does not confirm inbox delivery. If it does not arrive, check spam and your site mail or SMTP configuration." 707 msgid "Nothing was flagged in the latest scan." 708 msgstr "" 709 710 #: templates/admin-page.php:373 711 msgid "Plugin" 712 msgstr "" 713 714 #: templates/admin-page.php:374 715 msgid "Local Version" 716 msgstr "" 717 718 #: templates/admin-page.php:377 719 msgid "Actions" 720 msgstr "" 721 722 #: templates/admin-page.php:380 723 msgid "Plugin name" 724 msgstr "" 725 726 #: templates/admin-page.php:381 727 msgid "Risk count" 728 msgstr "" 729 730 #: templates/admin-page.php:382 731 msgid "Version gap" 732 msgstr "" 733 734 #: templates/admin-page.php:383 735 msgid "Local version" 736 msgstr "" 737 738 #: templates/admin-page.php:400 739 msgid "Search risks" 740 msgstr "" 741 742 #: templates/admin-page.php:405 743 msgid "Search plugin or reason" 744 msgstr "" 745 746 #: templates/admin-page.php:409 747 msgid "Sort risks by" 748 msgstr "" 749 750 #: templates/admin-page.php:420 751 msgid "Sort order" 752 msgstr "" 753 754 #: templates/admin-page.php:422 755 msgid "Ascending" 756 msgstr "" 757 758 #: templates/admin-page.php:423 759 msgid "Descending" 760 msgstr "" 761 762 #: templates/admin-page.php:425 763 msgid "Apply" 764 msgstr "" 765 766 #: templates/admin-page.php:428 767 msgid "Previous page" 768 msgstr "" 769 770 #: templates/admin-page.php:430 771 msgid "Next page" 772 msgstr "" 773 774 #: templates/admin-page.php:482 775 msgid "WPScan vulnerabilities:" 776 msgstr "" 777 778 #: templates/admin-page.php:515 779 msgid "Ignore" 780 msgstr "" 781 782 #: templates/admin-page.php:528 783 msgid "Ignored Plugins" 784 msgstr "" 785 786 #: templates/admin-page.php:530 787 msgid "No plugins are being ignored." 788 msgstr "" 789 790 #: templates/admin-page.php:532 791 msgid "Ignored plugins are skipped by every scan. Select one to watch it again." 792 msgstr "" 793 794 #: templates/admin-page.php:543 795 msgid "Stop ignoring" 796 msgstr "" 797 798 #: templates/admin-page.php:555 799 msgid "Scan History" 800 msgstr "" 801 802 #: templates/admin-page.php:566 803 msgid "Scanning" 804 msgstr "" 805 806 #: templates/admin-page.php:568 807 msgid "Choose how often the automatic scan should run." 808 msgstr "" 809 810 #. translators: 1: interval minutes, 2: duration hours 811 #: templates/admin-page.php:571 812 #, php-format 813 msgid "Testing mode sends notifications every %1$d minutes to all configured channels and automatically switches back to daily scans after %2$d hours." 814 msgstr "" 815 816 #: templates/admin-page.php:581 817 msgid "Monday" 818 msgstr "" 819 820 #: templates/admin-page.php:582 821 msgid "Tuesday" 822 msgstr "" 823 824 #: templates/admin-page.php:583 825 msgid "Wednesday" 826 msgstr "" 827 828 #: templates/admin-page.php:584 829 msgid "Thursday" 830 msgstr "" 831 832 #: templates/admin-page.php:585 833 msgid "Friday" 834 msgstr "" 835 836 #: templates/admin-page.php:586 837 msgid "Saturday" 838 msgstr "" 839 840 #: templates/admin-page.php:587 841 msgid "Sunday" 842 msgstr "" 843 844 #: templates/admin-page.php:591 845 msgid "Scan frequency" 846 msgstr "" 847 848 #. translators: %d: interval minutes 849 #: templates/admin-page.php:600 850 #, php-format 851 msgid "Testing (every %d minutes)" 852 msgstr "" 853 854 #: templates/admin-page.php:605 855 msgid "Manual (no automatic scans)" 856 msgstr "" 857 858 #. translators: 1: datetime, 2: relative time 859 #: templates/admin-page.php:621 860 #, php-format 861 msgid "Testing mode will automatically switch back to daily scans on %1$s (%2$s remaining)." 862 msgstr "" 863 864 #: templates/admin-page.php:632 865 msgid "Daily send time" 866 msgstr "" 867 868 #: templates/admin-page.php:641 869 msgid "Time of day to start the daily scan in the WordPress site timezone." 870 msgstr "" 871 872 #: templates/admin-page.php:645 873 msgid "Weekly send day" 874 msgstr "" 875 876 #: templates/admin-page.php:653 877 msgid "Weekly send time" 878 msgstr "" 879 880 #: templates/admin-page.php:662 881 msgid "Day and time to start the weekly scan in the WordPress site timezone." 882 msgstr "" 883 884 #: templates/admin-page.php:667 885 msgid "History retention" 886 msgstr "" 887 888 #: templates/admin-page.php:679 889 msgid "Number of recent scans to keep available for review and download (maximum 15)." 890 msgstr "" 891 892 #: templates/admin-page.php:688 893 msgid "Save and test email" 894 msgstr "" 895 896 #: templates/admin-page.php:690 897 msgid "Recipients (comma separated)" 898 msgstr "" 899 900 #: templates/admin-page.php:693 901 msgid "Separate addresses with commas, semicolons, or spaces. WordPress administrator accounts are always included." 902 msgstr "" 903 904 #: templates/admin-page.php:694 905 msgid "Email uses WordPress mail delivery. Your host must support sending mail, or you must configure an SMTP/mail plugin. Discord and other webhooks do not use this mail configuration." 906 msgstr "" 907 908 #: templates/admin-page.php:699 909 msgid "Save and test Discord" 910 msgstr "" 911 912 #: templates/admin-page.php:701 913 msgid "Discord webhook URL" 914 msgstr "" 915 916 #: templates/admin-page.php:707 917 msgid "Save and test Slack" 918 msgstr "" 919 920 #: templates/admin-page.php:709 921 msgid "Slack webhook URL" 922 msgstr "" 923 924 #: templates/admin-page.php:715 925 msgid "Save and test Teams" 926 msgstr "" 927 928 #: templates/admin-page.php:717 929 msgid "Teams webhook URL" 930 msgstr "" 931 932 #: templates/admin-page.php:720 933 msgid "Supports Microsoft Teams Workflows and existing Incoming Webhook connectors." 934 msgstr "" 935 936 #: templates/admin-page.php:725 937 msgid "Save and test webhook" 938 msgstr "" 939 940 #: templates/admin-page.php:727 941 msgid "Webhook URL" 942 msgstr "" 943 944 #: templates/admin-page.php:728 945 msgid "Webhook secret (optional)" 946 msgstr "" 947 948 #: templates/admin-page.php:731 949 msgid "Signs the exact JSON body with HMAC-SHA256 in the X-Watchdog-Signature header. Test messages are marked in the payload." 950 msgstr "" 951 952 #: templates/admin-page.php:737 953 msgid "Notification channels" 954 msgstr "" 955 956 #: templates/admin-page.php:738 957 msgid "Save and test buttons save all settings on this form, then send a test to the selected channel." 958 msgstr "" 959 960 #: templates/admin-page.php:760 961 msgid "On" 962 msgstr "" 963 964 #: templates/admin-page.php:761 965 msgid "Off" 572 966 msgstr "" 573 967 574 968 #. translators: %s: notification channel name. 575 #: templates/admin-page.php:193 576 #, php-format 577 msgid "%s test notification sent successfully." 578 msgstr "" 579 580 #. translators: %s: notification channel name. 581 #: templates/admin-page.php:204 582 #, php-format 583 msgid "%s could not be tested until its settings are corrected." 584 msgstr "" 585 586 #. translators: %s: notification channel name. 587 #: templates/admin-page.php:214 588 #, php-format 589 msgid "%s is not configured yet." 590 msgstr "" 591 592 #. translators: %s: notification channel name. 593 #: templates/admin-page.php:224 594 #, php-format 595 msgid "%s test delivery failed. Review Delivery health for details." 596 msgstr "" 597 598 #: templates/admin-page.php:236 599 msgid "Delivery health" 600 msgstr "" 601 602 #: templates/admin-page.php:240 969 #: templates/admin-page.php:768 970 #, php-format 971 msgid "Enable %s notifications" 972 msgstr "" 973 974 #: templates/admin-page.php:803 975 msgid "Vulnerability data" 976 msgstr "" 977 978 #: templates/admin-page.php:805 979 msgid "WPScan API key" 980 msgstr "" 981 982 #: templates/admin-page.php:815 983 msgid "Optional. Provide your own WPScan API key to enrich vulnerability reports." 984 msgstr "" 985 986 #: templates/admin-page.php:821 987 msgid "Save settings" 988 msgstr "" 989 990 #: templates/admin-page.php:829 991 msgid "Scheduler" 992 msgstr "" 993 994 #: templates/admin-page.php:831 995 msgid "WP-Cron disabled" 996 msgstr "" 997 998 #: templates/admin-page.php:833 999 msgid "WP-Cron enabled" 1000 msgstr "" 1001 1002 #: templates/admin-page.php:837 603 1003 msgid "WP-Cron appears disabled. Use a real cron job or the server endpoint below to keep scans running." 604 1004 msgstr "" 605 1005 606 #: templates/admin-page.php:248 607 msgid "Retry notifications" 608 msgstr "" 609 610 #: templates/admin-page.php:252 1006 #: templates/admin-page.php:839 611 1007 msgid "WP-Cron is enabled. Scheduled tasks depend on site traffic or a server cron job." 612 1008 msgstr "" 613 1009 614 #: templates/admin-page.php:256 1010 #: templates/admin-page.php:843 1011 msgid "Trigger scans from a server cron job" 1012 msgstr "" 1013 1014 #: templates/admin-page.php:846 615 1015 msgid "Server cron endpoint:" 616 1016 msgstr "" 617 1017 618 #: templates/admin-page.php:261 1018 #: templates/admin-page.php:848 1019 #: templates/admin-page.php:854 1020 #: templates/admin-page.php:859 1021 msgid "Copy" 1022 msgstr "" 1023 1024 #: templates/admin-page.php:852 619 1025 msgid "Generated Cron secret:" 620 1026 msgstr "" 621 1027 622 #: templates/admin-page.php: 2651028 #: templates/admin-page.php:857 623 1029 msgid "Authenticated command:" 624 1030 msgstr "" 625 1031 626 #: templates/admin-page.php: 2681032 #: templates/admin-page.php:861 627 1033 msgid "Use POST and keep the generated secret private. The header-authenticated request can trigger scans or notification retries even when wp-cron is disabled." 628 1034 msgstr "" 629 1035 630 #: templates/admin-page.php: 2721036 #: templates/admin-page.php:870 631 1037 msgid "Queue status" 632 1038 msgstr "" 633 1039 634 #. translators: % d: number of queued jobs.635 #: templates/admin-page.php: 280636 #, php-format 637 msgid "Queued jobs: % d"1040 #. translators: %s: number of queued jobs. 1041 #: templates/admin-page.php:875 1042 #, php-format 1043 msgid "Queued jobs: %s" 638 1044 msgstr "" 639 1045 640 1046 #. translators: 1: scheduled time 641 #: templates/admin-page.php: 2901047 #: templates/admin-page.php:886 642 1048 #, php-format 643 1049 msgid "Next attempt: %1$s" 644 1050 msgstr "" 645 1051 646 #: templates/admin-page.php: 2961052 #: templates/admin-page.php:892 647 1053 msgid "No pending notifications in the queue." 648 1054 msgstr "" 649 1055 650 #: templates/admin-page.php: 3011056 #: templates/admin-page.php:898 651 1057 msgid "Captured notification payloads" 652 1058 msgstr "" 653 1059 654 #: templates/admin-page.php:308 1060 #: templates/admin-page.php:900 1061 msgid "Delivery failed" 1062 msgstr "" 1063 1064 #: templates/admin-page.php:909 655 1065 msgid "Unknown channel" 656 1066 msgstr "" 657 1067 658 1068 #. translators: 1: human readable time, 2: channel name 659 #: templates/admin-page.php: 3151069 #: templates/admin-page.php:916 660 1070 #, php-format 661 1071 msgid "Last failure recorded %1$s via %2$s." 662 1072 msgstr "" 663 1073 664 #: templates/admin-page.php: 3301074 #: templates/admin-page.php:931 665 1075 msgid "Re-queue payload" 666 1076 msgstr "" 667 1077 668 #: templates/admin-page.php: 3381078 #: templates/admin-page.php:939 669 1079 msgid "Download payload" 670 1080 msgstr "" 671 1081 672 #: templates/admin-page.php: 3431082 #: templates/admin-page.php:944 673 1083 msgid "No failed notification payload has been captured yet." 674 1084 msgstr "" 675 1085 676 #: templates/admin-page.php:353 677 msgid "Scan History" 678 msgstr "" 679 680 #: templates/admin-page.php:360 681 msgid "Notification channels" 682 msgstr "" 683 684 #: templates/admin-page.php:368 685 msgid "Custom Webhook" 686 msgstr "" 687 688 #: templates/admin-page.php:380 689 msgid "On" 690 msgstr "" 691 692 #: templates/admin-page.php:380 693 msgid "Off" 694 msgstr "" 695 696 #: templates/admin-page.php:386 697 msgid "Keep channels enabled to receive instant alerts and download-ready reports." 698 msgstr "" 699 700 #: templates/admin-page.php:395 701 msgid "Potential Risks" 702 msgstr "" 703 704 #: templates/admin-page.php:399 705 msgid "items flagged" 706 msgstr "" 707 708 #: templates/admin-page.php:403 709 msgid "No risks detected." 710 msgstr "" 711 712 #: templates/admin-page.php:407 713 msgid "Plugin" 714 msgstr "" 715 716 #: templates/admin-page.php:408 717 msgid "Local Version" 718 msgstr "" 719 720 #: templates/admin-page.php:411 721 msgid "Actions" 722 msgstr "" 723 724 #: templates/admin-page.php:455 725 msgid "Search risks" 726 msgstr "" 727 728 #: templates/admin-page.php:460 729 msgid "Search plugin or reason" 730 msgstr "" 731 732 #: templates/admin-page.php:465 733 msgid "Sort risks by" 734 msgstr "" 735 736 #: templates/admin-page.php:473 737 msgid "Plugin name" 738 msgstr "" 739 740 #: templates/admin-page.php:481 741 msgid "Risk count" 742 msgstr "" 743 744 #: templates/admin-page.php:489 745 msgid "Version gap" 746 msgstr "" 747 748 #: templates/admin-page.php:497 749 msgid "Local version" 750 msgstr "" 751 752 #: templates/admin-page.php:516 753 msgid "Sort order" 754 msgstr "" 755 756 #: templates/admin-page.php:519 757 msgid "Ascending" 758 msgstr "" 759 760 #: templates/admin-page.php:522 761 msgid "Descending" 762 msgstr "" 763 764 #: templates/admin-page.php:525 765 msgid "Apply" 766 msgstr "" 767 768 #: templates/admin-page.php:529 769 msgid "Previous page" 770 msgstr "" 771 772 #: templates/admin-page.php:531 773 msgid "Next page" 774 msgstr "" 775 776 #: templates/admin-page.php:623 777 msgid "WPScan vulnerabilities:" 778 msgstr "" 779 780 #: templates/admin-page.php:658 781 msgid "Ignore" 782 msgstr "" 783 784 #: templates/admin-page.php:674 785 msgid "Ignored Plugins" 786 msgstr "" 787 788 #: templates/admin-page.php:677 789 msgid "No plugins are being ignored." 790 msgstr "" 791 792 #: templates/admin-page.php:699 793 msgid "Settings and notifications" 794 msgstr "" 795 796 #: templates/admin-page.php:701 797 msgid "Save and test buttons save all settings on this form, then send a test to the selected channel." 798 msgstr "" 799 800 #: templates/admin-page.php:707 801 #: templates/admin-page.php:709 802 msgid "History retention" 803 msgstr "" 804 805 #: templates/admin-page.php:720 806 msgid "Number of recent scans to keep available for review and download (maximum 15)." 807 msgstr "" 808 809 #: templates/admin-page.php:725 810 #: templates/admin-page.php:749 811 msgid "Scan frequency" 812 msgstr "" 813 814 #: templates/admin-page.php:728 815 msgid "Choose how often the automatic scan should run." 816 msgstr "" 817 818 #. translators: 1: interval minutes, 2: duration hours 819 #: templates/admin-page.php:731 820 #, php-format 821 msgid "Testing mode sends notifications every %1$d minutes to all configured channels and automatically switches back to daily scans after %2$d hours." 822 msgstr "" 823 824 #. translators: %d: interval minutes 825 #: templates/admin-page.php:756 826 #, php-format 827 msgid "Testing (every %d minutes)" 828 msgstr "" 829 830 #: templates/admin-page.php:760 831 msgid "Manual (no automatic scans)" 832 msgstr "" 833 834 #: templates/admin-page.php:780 835 msgid "Daily send time" 836 msgstr "" 837 838 #: templates/admin-page.php:790 839 msgid "Time of day to start the daily scan in the WordPress site timezone." 840 msgstr "" 841 842 #: templates/admin-page.php:796 843 msgid "Weekly send day" 844 msgstr "" 845 846 #: templates/admin-page.php:799 847 msgid "Monday" 848 msgstr "" 849 850 #: templates/admin-page.php:800 851 msgid "Tuesday" 852 msgstr "" 853 854 #: templates/admin-page.php:801 855 msgid "Wednesday" 856 msgstr "" 857 858 #: templates/admin-page.php:802 859 msgid "Thursday" 860 msgstr "" 861 862 #: templates/admin-page.php:803 863 msgid "Friday" 864 msgstr "" 865 866 #: templates/admin-page.php:804 867 msgid "Saturday" 868 msgstr "" 869 870 #: templates/admin-page.php:805 871 msgid "Sunday" 872 msgstr "" 873 874 #: templates/admin-page.php:810 875 msgid "Weekly send time" 876 msgstr "" 877 878 #: templates/admin-page.php:821 879 msgid "Day and time to start the weekly scan in the WordPress site timezone." 880 msgstr "" 881 882 #. translators: 1: datetime, 2: relative time 883 #: templates/admin-page.php:861 884 #, php-format 885 msgid "Testing mode will automatically switch back to daily scans on %1$s (%2$s remaining)." 886 msgstr "" 887 888 #: templates/admin-page.php:877 889 msgid "Email notifications" 890 msgstr "" 891 892 #: templates/admin-page.php:881 893 #: templates/admin-page.php:899 894 #: templates/admin-page.php:915 895 #: templates/admin-page.php:931 896 #: templates/admin-page.php:948 897 msgid "Enabled" 898 msgstr "" 899 900 #: templates/admin-page.php:885 901 msgid "Recipients (comma separated)" 902 msgstr "" 903 904 #: templates/admin-page.php:888 905 msgid "Separate addresses with commas, semicolons, or spaces. WordPress administrator accounts are always included." 906 msgstr "" 907 908 #: templates/admin-page.php:889 909 msgid "Email uses WordPress mail delivery. Your host must support sending mail, or you must configure an SMTP/mail plugin. Discord and other webhooks do not use this mail configuration." 910 msgstr "" 911 912 #: templates/admin-page.php:890 913 msgid "Save and test email" 914 msgstr "" 915 916 #: templates/admin-page.php:895 917 msgid "Discord notifications" 918 msgstr "" 919 920 #: templates/admin-page.php:903 921 msgid "Discord webhook URL" 922 msgstr "" 923 924 #: templates/admin-page.php:906 925 msgid "Save and test Discord" 926 msgstr "" 927 928 #: templates/admin-page.php:911 929 msgid "Slack notifications" 930 msgstr "" 931 932 #: templates/admin-page.php:919 933 msgid "Slack webhook URL" 934 msgstr "" 935 936 #: templates/admin-page.php:922 937 msgid "Save and test Slack" 938 msgstr "" 939 940 #: templates/admin-page.php:927 941 msgid "Microsoft Teams notifications" 942 msgstr "" 943 944 #: templates/admin-page.php:935 945 msgid "Teams webhook URL" 946 msgstr "" 947 948 #: templates/admin-page.php:938 949 msgid "Supports Microsoft Teams Workflows and existing Incoming Webhook connectors." 950 msgstr "" 951 952 #: templates/admin-page.php:939 953 msgid "Save and test Teams" 954 msgstr "" 955 956 #: templates/admin-page.php:944 957 msgid "Generic webhook" 958 msgstr "" 959 960 #: templates/admin-page.php:952 961 msgid "Webhook URL" 962 msgstr "" 963 964 #: templates/admin-page.php:957 965 msgid "Webhook secret (optional)" 966 msgstr "" 967 968 #: templates/admin-page.php:960 969 msgid "Signs the exact JSON body with HMAC-SHA256 in the X-Watchdog-Signature header. Test messages are marked in the payload." 970 msgstr "" 971 972 #: templates/admin-page.php:962 973 msgid "Save and test webhook" 974 msgstr "" 975 976 #: templates/admin-page.php:967 977 msgid "WPScan API key" 978 msgstr "" 979 980 #: templates/admin-page.php:970 981 msgid "Optional. Provide your own WPScan API key to enrich vulnerability reports." 982 msgstr "" 983 984 #: templates/admin-page.php:974 985 msgid "Save settings" 986 msgstr "" 987 988 #: templates/history.php:21 1086 #: templates/history.php:14 989 1087 msgid "No scans have been recorded yet. Run a scan to populate your history." 990 1088 msgstr "" 991 1089 992 1090 #. translators: 1: number of scans shown, 2: total scans retained 993 #: templates/history.php: 301091 #: templates/history.php:20 994 1092 #, php-format 995 1093 msgid "Showing the last %1$s scans (retaining %2$s in total)." … … 997 1095 998 1096 #. translators: %s: number of risks. 999 #: templates/history.php: 551097 #: templates/history.php:41 1000 1098 #, php-format 1001 1099 msgid "%s risk" … … 1004 1102 msgstr[1] "" 1005 1103 1006 #: templates/history.php:69 1007 msgid "Download the report for this run:" 1008 msgstr "" 1009 1010 #: templates/history.php:73 1104 #: templates/history.php:48 1011 1105 msgid "Download JSON" 1012 1106 msgstr "" 1013 1107 1014 #: templates/history.php: 761108 #: templates/history.php:51 1015 1109 msgid "Download CSV" 1016 1110 msgstr "" -
site-add-on-watchdog/trunk/readme.txt
r3770137 r3799692 5 5 Tested up to: 7.1 6 6 Requires PHP: 8.1 7 Stable tag: 1. 9.07 Stable tag: 1.10.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 17 17 * A newer release is available for an installed plugin, with an additional warning when it is two or more minor releases behind. 18 18 * The latest release's official changelog mentions security or vulnerability fixes. 19 * (Optional) WPScan reports vulnerabilities affecting the installed version when you provide your own API key. 20 21 The plugin runs on a schedule you control—choose daily, weekly, a twenty-minute testing cadence, or rely on manual scans—and stores results locally. To compare public versions and changelogs, Watchdog looks up plugin slugs individually on WordPress.org and caches successful responses for six hours. WPScan lookups are optional. Email risk alerts to site administrators are enabled by default; webhook channels require setup and activation.19 * (Optional) WPScan reports vulnerabilities affecting the installed version when you provide your own API key. This also covers plugins that are not listed on WordPress.org. 20 21 The plugin runs on a schedule you control—choose daily, weekly, a twenty-minute testing cadence, or rely on manual scans—and stores results locally. To compare public versions and changelogs, Watchdog looks up plugin slugs individually on WordPress.org and caches each answer for six hours, including the answer that a plugin is not in the directory. A lookup that fails is retried on the next scan. WPScan lookups are optional. Email risk alerts to site administrators are enabled by default; webhook channels require setup and activation. 22 22 23 23 === Privacy first === … … 38 38 === Admin tools === 39 39 40 * Focused dashboard with risk summaries, searchable history, delivery health, and manual actions.40 * Tabbed dashboard with a searchable risk table, downloadable scan history, delivery status, and manual actions. 41 41 * Ignore list to suppress noisy plugins. 42 42 * Validated notification settings with a save-and-test action for every channel. … … 83 83 = How do I resend a failed notification payload? = 84 84 85 Open the Watchdog admin page and check the **Delivery health** section. If a notification fails, the payload is captured there with buttons to re-queue or download it. 85 Open the Watchdog admin page and select the **Delivery** tab. If a notification fails, the payload is captured there with buttons to re-queue or download it. 86 87 = What happens to my data when I delete the plugin? = 88 89 Deleting the plugin from the Plugins screen removes its settings, saved risks, ignore list, scan history, notification queue, and scheduled events. Deactivating it only stops the schedule and keeps the data. 86 90 87 91 = Where can I find the test suite? = … … 93 97 === Scheduled scans are not running === 94 98 95 Watchdog relies on WP-Cron to trigger scheduled scans and notifications. If you have set `DISABLE_WP_CRON` to `true` or your site receives very little traffic (so WP-Cron rarely runs), configure a system cron job to call either `wp-cron.php` or the plugin's REST endpoint. The admin **Delivery health** panelshows the endpoint and generated secret. Send the secret in an HTTP header so it does not appear in access logs; a typical example looks like this:99 Watchdog relies on WP-Cron to trigger scheduled scans and notifications. If you have set `DISABLE_WP_CRON` to `true` or your site receives very little traffic (so WP-Cron rarely runs), configure a system cron job to call either `wp-cron.php` or the plugin's REST endpoint. The **Delivery** tab on the Watchdog admin page shows the endpoint and generated secret. Send the secret in an HTTP header so it does not appear in access logs; a typical example looks like this: 96 100 97 101 `curl -X POST -H "X-Watchdog-Cron-Key: YOUR_GENERATED_SECRET" https://example.com/wp-json/site-add-on-watchdog/v1/cron` … … 103 107 Email uses WordPress's `wp_mail()` function and your site's mail configuration. Discord and other webhooks use HTTP, so a working webhook does not confirm that the server can send email. 104 108 105 Use "Save and test email", then check Delivery healthfor a mail or SMTP error. Configure your host's mail service or an SMTP/mail plugin if needed. If WordPress accepts the test but no email arrives, check spam and your mail provider's delivery logs, sender verification, and SPF/DKIM settings. Acceptance for sending does not confirm inbox delivery.109 Use "Save and test email", then check the Delivery tab for a mail or SMTP error. Configure your host's mail service or an SMTP/mail plugin if needed. If WordPress accepts the test but no email arrives, check spam and your mail provider's delivery logs, sender verification, and SPF/DKIM settings. Acceptance for sending does not confirm inbox delivery. 106 110 107 111 == CLI Usage == … … 125 129 126 130 == Changelog == 131 132 = 1.10.0 = 133 * Split the dashboard into Risks, History, Settings, and Delivery tabs, and show when the last scan ran and when the next one is due. 134 * Check plugins that are not in the WordPress.org directory against WPScan when an API key is configured. 135 * Remove the plugin's saved data when the plugin is deleted. 136 * Turn email alerts on by default, as documented. New installs had them off. Sites that never saved the Watchdog settings will start emailing alerts to administrators. 137 * Show the WP-Cron notice only when the scheduled scan is overdue. 138 * Stop writing to the database on every page load. 139 * Remove an ignored plugin from the risk table at once, and show every table column on narrow screens. 140 * Honor `force` on REST cron scan requests and stop repeating old notices. 127 141 128 142 = 1.9.0 = … … 173 187 == Upgrade Notice == 174 188 189 = 1.10.0 = 190 Email alerts to administrators are now on by default as documented. If you never saved the Watchdog settings and do not want them, switch Email off under Settings. Also adds a tabbed dashboard and removes a database write from every page load. 191 175 192 = 1.9.0 = 176 193 Adds custom webhook event metadata and clarifies test and scan status messages. -
site-add-on-watchdog/trunk/site-add-on-watchdog.php
r3770137 r3799692 4 4 * Plugin Name: Site Add-on Watchdog 5 5 * Description: Monitors installed plugins for potential security risks and outdated versions. 6 * Version: 1. 9.06 * Version: 1.10.0 7 7 * Author: Aaron 8 8 * Author URI: https://www.worksbyaaron.com/ … … 11 11 * Requires at least: 6.0 12 12 * Requires PHP: 8.1 13 * Tested up to: 7.114 13 */ 15 14 -
site-add-on-watchdog/trunk/src/Admin/RiskSorter.php
r3612684 r3799692 21 21 'remote' => $this->compareVersions($left->remoteVersion, $right->remoteVersion), 22 22 'reasons' => $this->compareText( 23 $this->reason SortValue($left),24 $this->reason SortValue($right)23 $this->reasonText($left), 24 $this->reasonText($right) 25 25 ), 26 'risk_count' => $this-> riskSignalCount($left) <=> $this->riskSignalCount($right),27 'version_gap' => $this->versionGap Score($left) <=> $this->versionGapScore($right),26 'risk_count' => $this->signalCount($left) <=> $this->signalCount($right), 27 'version_gap' => $this->versionGap($left) <=> $this->versionGap($right), 28 28 default => 0, 29 29 }; … … 43 43 } 44 44 45 p rivatefunction normalizeText(string $value): string45 public function normalizeText(string $value): string 46 46 { 47 47 $normalized = function_exists('remove_accents') ? remove_accents($value) : $value; … … 50 50 } 51 51 52 p rivate function reasonSortValue(Risk $risk): string52 public function reasonText(Risk $risk): string 53 53 { 54 54 $parts = $risk->reasons; … … 64 64 } 65 65 66 p rivate function riskSignalCount(Risk $risk): int66 public function signalCount(Risk $risk): int 67 67 { 68 68 return count($risk->reasons) + count($this->vulnerabilities($risk)); … … 72 72 * @return array<int, array<string, mixed>> 73 73 */ 74 p rivatefunction vulnerabilities(Risk $risk): array74 public function vulnerabilities(Risk $risk): array 75 75 { 76 76 $vulnerabilities = $risk->details['vulnerabilities'] ?? []; … … 154 154 } 155 155 156 p rivate function versionGapScore(Risk $risk): int156 public function versionGap(Risk $risk): int 157 157 { 158 158 return abs( -
site-add-on-watchdog/trunk/src/AdminPage.php
r3631212 r3799692 9 9 use Watchdog\Repository\SettingsRepository; 10 10 use Watchdog\Version; 11 use WP_Filesystem_Direct;12 11 13 12 class AdminPage … … 16 15 private const MENU_SLUG = 'site-add-on-watchdog'; 17 16 private const HISTORY_DOWNLOAD_ACTION = self::PREFIX . '_history_download'; 17 private const NOTICE_ARGS = [ 18 'updated', 19 'scan', 20 'notifications', 21 'failed_notification', 22 'channel_test', 23 'channel_test_status', 24 '_wpnonce', 25 ]; 18 26 19 27 private ?string $menuHook = null; 20 28 private bool $assetsEnqueued = false; 21 /**22 * @var WP_Filesystem_Direct|null23 */24 private $filesystem = null;25 29 private readonly RiskSorter $riskSorter; 26 30 … … 53 57 ); 54 58 add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']); 59 add_filter('removable_query_args', [$this, 'filterRemovableQueryArgs']); 60 } 61 62 /** 63 * Lets WordPress drop one-time notice arguments from the address bar so a 64 * reload does not show the same notice again. 65 * 66 * @param string[] $args 67 * @return string[] 68 */ 69 public function filterRemovableQueryArgs(array $args): array 70 { 71 $page = filter_input(INPUT_GET, 'page', FILTER_SANITIZE_FULL_SPECIAL_CHARS); 72 if ($page !== self::MENU_SLUG) { 73 return $args; 74 } 75 76 return array_merge($args, self::NOTICE_ARGS); 55 77 } 56 78 … … 105 127 $watchdogRiskSortSelection = $watchdogRiskSort !== '' ? $watchdogRiskSort : 'plugin'; 106 128 $watchdogRiskOrderSelection = $watchdogRiskOrder !== '' ? $watchdogRiskOrder : 'asc'; 129 $watchdogRiskRows = $this->buildRiskRows($watchdogRisks); 130 $watchdogVulnerabilityCount = 0; 131 foreach ($watchdogRiskRows as $watchdogRiskRow) { 132 $watchdogVulnerabilityCount += count($watchdogRiskRow['vulnerabilities']); 133 } 107 134 108 135 $watchdogSettingsError = get_transient(self::PREFIX . '_settings_error'); … … 140 167 ]; 141 168 } 169 170 $watchdogLastScanAt = isset($watchdogHistoryRecords[0]['run_at']) 171 ? (int) $watchdogHistoryRecords[0]['run_at'] 172 : null; 173 $watchdogNextScanAt = $this->plugin->getNextScanTimestamp(); 142 174 143 175 $watchdogLastFailedNotification = $this->notifier->getLastFailedNotification(); … … 259 291 } 260 292 261 wp_safe_redirect( wp_get_referer() ?: admin_url('admin.php?page=' . self::MENU_SLUG));293 wp_safe_redirect($this->returnUrl()); 262 294 exit; 263 295 } … … 273 305 } 274 306 275 wp_safe_redirect( wp_get_referer() ?: admin_url('admin.php?page=' . self::MENU_SLUG));307 wp_safe_redirect($this->returnUrl()); 276 308 exit; 277 309 } … … 293 325 check_admin_referer(self::PREFIX . '_send_notifications'); 294 326 295 $force = ! empty($_POST['force']); 296 $respectThrottle = empty($_POST['ignore_throttle']); 297 $result = $this->plugin->sendNotifications($force, $respectThrottle); 327 $result = $this->plugin->sendNotifications(true); 298 328 299 329 $this->redirectWithNotice(['notifications' => $result]); … … 408 438 'pageStatus' => __('Page %1$d of %2$d', 'site-add-on-watchdog'), 409 439 'noResults' => __('No risks match your search.', 'site-add-on-watchdog'), 440 'copied' => __('Copied', 'site-add-on-watchdog'), 410 441 ]); 411 442 … … 480 511 } 481 512 482 $target = 'php://output'; 483 484 if ($target === 'php://output') { 485 $csvContent = $this->buildCsvContent($rows); 486 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download response. 487 echo $csvContent; 488 } else { 489 $filesystem = $this->getFilesystem(); 490 $csvContent = $this->buildCsvContent($rows); 491 492 if (! $filesystem->put_contents($target, $csvContent)) { 493 wp_die(esc_html__('Unable to generate history export.', 'site-add-on-watchdog')); 494 } 495 } 513 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download response. 514 echo $this->buildCsvContent($rows); 496 515 exit; 497 516 } … … 534 553 $args['_wpnonce'] = wp_create_nonce(self::PREFIX . '_admin_notice'); 535 554 536 wp_safe_redirect( 537 add_query_arg( 538 $args, 539 wp_get_referer() ?: admin_url('admin.php?page=' . self::MENU_SLUG) 540 ) 541 ); 555 wp_safe_redirect(add_query_arg($args, $this->returnUrl())); 556 } 557 558 /** 559 * The page to return to, without the notice arguments of an earlier action. 560 * Leaving them in would replay an old notice next to the new one. 561 */ 562 private function returnUrl(): string 563 { 564 $referer = wp_get_referer(); 565 if (! $referer) { 566 return admin_url('admin.php?page=' . self::MENU_SLUG); 567 } 568 569 return remove_query_arg(self::NOTICE_ARGS, $referer); 542 570 } 543 571 … … 554 582 } 555 583 556 private function getFilesystem()557 {558 if ($this->filesystem !== null) {559 return $this->filesystem;560 }561 562 $this->ensureFilesystemDependenciesLoaded();563 564 global $wp_filesystem;565 566 if (! $wp_filesystem instanceof WP_Filesystem_Direct) {567 WP_Filesystem();568 }569 570 if ($wp_filesystem instanceof WP_Filesystem_Direct) {571 $ this->filesystem = $wp_filesystem;572 573 return $this->filesystem;574 }575 576 $this->filesystem = new WP_Filesystem_Direct(false);577 578 return $this->filesystem;579 }580 581 private function ensureFilesystemDependenciesLoaded(): void582 {583 if (! function_exists('request_filesystem_credentials')) {584 require_once ABSPATH . 'wp-admin/includes/file.php';585 }586 587 if (! class_exists('WP_Filesystem_Base')) {588 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';589 }590 591 if (! class_exists('WP_Filesystem_Direct')) {592 require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; 593 }584 /** 585 * Prepares the sort and search attributes the risk table script reads. 586 * 587 * @param Risk[] $risks 588 * @return array<int, array{ 589 * risk: Risk, 590 * vulnerabilities: array<int, array<string, mixed>>, 591 * attributes: array<string, string> 592 * }> 593 */ 594 private function buildRiskRows(array $risks): array 595 { 596 $rows = []; 597 598 foreach ($risks as $risk) { 599 $reasonText = $this->riskSorter->reasonText($risk); 600 601 $rows[] = [ 602 'risk' => $risk, 603 'vulnerabilities' => $this->riskSorter->vulnerabilities($risk), 604 'attributes' => [ 605 'data-sort-plugin' => $this->riskSorter->normalizeText($risk->pluginName), 606 'data-sort-local' => $this->riskSorter->normalizeText($risk->localVersion), 607 'data-sort-remote' => $this->riskSorter->normalizeText((string) $risk->remoteVersion), 608 'data-sort-reasons' => $this->riskSorter->normalizeText($reasonText), 609 'data-sort-risk-count' => (string) $this->riskSorter->signalCount($risk), 610 'data-sort-version-gap' => (string) $this->riskSorter->versionGap($risk), 611 'data-filter-text' => $this->riskSorter->normalizeText(implode(' ', [ 612 $risk->pluginName, 613 $risk->localVersion, 614 (string) $risk->remoteVersion, 615 $reasonText, 616 ])), 617 ], 618 ]; 619 } 620 621 return $rows; 594 622 } 595 623 } -
site-add-on-watchdog/trunk/src/Bootstrap.php
r3612684 r3799692 27 27 $settingsRepository = new SettingsRepository(); 28 28 $riskRepository = new RiskRepository(); 29 $settings = $settingsRepository->get(); 29 // Read the key on first use. Loading the settings here would run before 30 // `plugins_loaded`, which is too early for the user query behind the 31 // default recipient list. 30 32 $wpscanClient = new WPScanClient( 31 (string) ($settings['notifications']['wpscan_api_key'] ?? '')33 static fn (): string => (string) ($settingsRepository->get()['notifications']['wpscan_api_key'] ?? '') 32 34 ); 33 35 $scanner = new Scanner( -
site-add-on-watchdog/trunk/src/Plugin.php
r3631212 r3799692 4 4 5 5 use Watchdog\Models\Risk; 6 use Watchdog\Repository\MigratesLegacyOptions; 6 7 use Watchdog\Repository\RiskRepository; 7 8 use Watchdog\Repository\SettingsRepository; … … 12 13 class Plugin 13 14 { 15 use MigratesLegacyOptions; 16 14 17 private const PREFIX = Version::PREFIX; 15 18 private const CRON_HOOK = self::PREFIX . '_scheduled_scan'; … … 55 58 add_action('rest_api_init', [$this, 'registerRestRoutes']); 56 59 57 $this->cleanupUpdateCheckState();58 59 60 $this->hooksRegistered = true; 60 61 } … … 137 138 * Executes the scan and persists results. 138 139 * 139 * @param bool $notify Whether notifications should be dispatched. 140 * @param bool $notify Whether notifications should be dispatched. 141 * @param string $context 'automatic', 'manual', 'rest', or 'rest-force'. Manual scans are 142 * throttled; forced REST scans notify even when nothing changed. 140 143 * 141 144 * @return bool Whether notifications were dispatched. … … 176 179 $lastHash = $settings['last_notification'] ?? ''; 177 180 $isTestingMode = ($settings['notifications']['frequency'] ?? 'daily') === 'testing'; 178 179 $shouldNotify = $notify && ($isTestingMode || (! empty($risks) && $hash !== $lastHash)); 181 $isForced = $context === 'rest-force'; 182 183 $shouldNotify = $notify 184 && ($isTestingMode || $isForced || (! empty($risks) && $hash !== $lastHash)); 180 185 $manualThrottle = $context === 'manual' 181 186 && $this->isManualNotificationThrottled($settings, $runAt); … … 296 301 } 297 302 298 private function cleanupUpdateCheckState(): void299 {300 delete_option(self::UPDATE_CHECK_OPTION);301 delete_option(self::LEGACY_UPDATE_CHECK_OPTION);302 }303 304 303 private function scheduleNotificationQueueProcessor(): void 305 304 { 306 $this->clearScheduledHook(self::LEGACY_QUEUE_CRON_HOOK);307 308 305 $timestamp = wp_next_scheduled(self::QUEUE_CRON_HOOK); 309 306 $currentSchedule = $timestamp ? wp_get_schedule(self::QUEUE_CRON_HOOK) : false; … … 343 340 } 344 341 345 $status = $this->getOptionWithLegacy(self::CRON_STATUS_OPTION, self::LEGACY_CRON_STATUS_OPTION, []); 346 if (! is_array($status)) { 347 return; 348 } 349 350 if (! empty($status['cron_disabled'])) { 351 echo '<div class="notice notice-error"><p>' 352 . esc_html__( 353 'WP-Cron appears disabled. Set a system cron job to trigger wp-cron.php for Site Add-on Watchdog.', 354 'site-add-on-watchdog' 355 ) 356 . '</p></div>'; 357 358 return; 359 } 360 361 if (($status['overdue_streak'] ?? 0) >= 2) { 362 echo '<div class="notice notice-warning"><p>' 363 . esc_html__( 364 'Site Add-on Watchdog scans are overdue. Ensure system cron calls wp-cron.php regularly.', 365 'site-add-on-watchdog' 366 ) 367 . '</p></div>'; 368 } 369 } 370 342 $status = $this->getCronStatus(); 343 if ($status['overdue_streak'] < 2) { 344 return; 345 } 346 347 // A disabled WP-Cron is a normal production setup when a system cron 348 // calls wp-cron.php, so only warn once the scheduled scan is late. 349 $message = $status['cron_disabled'] 350 ? __( 351 // phpcs:ignore Generic.Files.LineLength.TooLong -- Translation source must remain one literal string. 352 'The scheduled Site Add-on Watchdog scan is overdue and WP-Cron is disabled on this site. Make sure a system cron job calls wp-cron.php.', 353 'site-add-on-watchdog' 354 ) 355 : __( 356 'Site Add-on Watchdog scans are overdue. Ensure system cron calls wp-cron.php regularly.', 357 'site-add-on-watchdog' 358 ); 359 360 echo '<div class="notice notice-warning"><p>' . esc_html($message) . '</p></div>'; 361 } 362 363 /** 364 * @return array{cron_disabled: bool, overdue_streak: int} 365 */ 371 366 public function getCronStatus(): array 372 367 { 373 $status = $this->getOptionWithLegacy(self::CRON_STATUS_OPTION, self::LEGACY_CRON_STATUS_OPTION, []);374 if (! is_array($status)) {375 $status = [];376 }377 378 return [379 'cron_disabled' => ! empty($status['cron_disabled']),380 'overdue_streak' => (int) ($status['overdue_streak'] ?? 0),381 'last_checked' => (int) ($status['last_checked'] ?? 0), 382 ];368 return $this->normalizeCronStatus( 369 $this->getOptionWithLegacy(self::CRON_STATUS_OPTION, self::LEGACY_CRON_STATUS_OPTION, []) 370 ); 371 } 372 373 public function getNextScanTimestamp(): ?int 374 { 375 $timestamp = wp_next_scheduled(self::CRON_HOOK); 376 377 return $timestamp ? (int) $timestamp : null; 383 378 } 384 379 … … 439 434 } 440 435 436 /** 437 * Runs on every request, so it only writes when the status changes. 438 */ 441 439 private function recordCronStatus(bool $cronDisabled, bool $overdue): void 442 440 { 443 $status = $this->getOptionWithLegacy(self::CRON_STATUS_OPTION, self::LEGACY_CRON_STATUS_OPTION, []); 444 if (! is_array($status)) { 445 $status = [ 446 'overdue_streak' => 0, 447 'cron_disabled' => false, 448 ]; 449 } 450 451 $status['cron_disabled'] = $cronDisabled; 452 $status['last_checked'] = time(); 453 454 if ($overdue) { 455 $status['overdue_streak'] = min(10, (int) ($status['overdue_streak'] ?? 0) + 1); 456 } else { 457 $status['overdue_streak'] = 0; 458 } 459 460 update_option(self::CRON_STATUS_OPTION, $status, false); 441 $stored = $this->getOptionWithLegacy(self::CRON_STATUS_OPTION, self::LEGACY_CRON_STATUS_OPTION, []); 442 $previous = $this->normalizeCronStatus($stored); 443 $status = [ 444 'cron_disabled' => $cronDisabled, 445 'overdue_streak' => $overdue ? min(10, $previous['overdue_streak'] + 1) : 0, 446 ]; 447 448 if ($stored === $status) { 449 return; 450 } 451 452 // Autoloaded because the status is read on every request. 453 update_option(self::CRON_STATUS_OPTION, $status, true); 454 455 if ($status === $previous) { 456 return; 457 } 461 458 462 459 if ($cronDisabled) { … … 467 464 . 'Ensure cron can reach wp-cron.php.'); 468 465 } 466 } 467 468 /** 469 * @return array{cron_disabled: bool, overdue_streak: int} 470 */ 471 private function normalizeCronStatus(mixed $status): array 472 { 473 if (! is_array($status)) { 474 $status = []; 475 } 476 477 return [ 478 'cron_disabled' => ! empty($status['cron_disabled']), 479 'overdue_streak' => (int) ($status['overdue_streak'] ?? 0), 480 ]; 469 481 } 470 482 … … 670 682 } 671 683 672 private function getOptionWithLegacy(string $option, string $legacy, mixed $default): mixed673 {674 $value = get_option($option, $default);675 if ($value !== $default) {676 return $value;677 }678 679 $legacyValue = get_option($legacy, $default);680 if ($legacyValue !== $default) {681 update_option($option, $legacyValue, false);682 683 return $legacyValue;684 }685 686 return $value;687 }688 689 public function validateCronRequest(WP_REST_Request $request): bool690 {691 [$provided] = $this->cronCredentialFromRequest($request);692 $stored = $this->getCronSecret();693 694 if ($stored === '') {695 return false;696 }697 698 return hash_equals($stored, $provided);699 }700 701 684 public function authorizeCronRequest(WP_REST_Request $request): bool|\WP_Error 702 685 { -
site-add-on-watchdog/trunk/src/Repository/RiskRepository.php
r3425314 r3799692 8 8 class RiskRepository 9 9 { 10 use MigratesLegacyOptions; 11 10 12 private const PREFIX = Version::PREFIX; 11 13 private const RISKS_OPTION = self::PREFIX . '_risks'; … … 29 31 } 30 32 33 // Ignoring a plugin takes effect at once instead of waiting for the next scan. 34 $ignored = $this->ignored(); 31 35 $normalized = []; 32 36 … … 37 41 38 42 if (! isset($entry['plugin_slug'], $entry['plugin_name'], $entry['local_version'])) { 43 continue; 44 } 45 46 if (in_array((string) $entry['plugin_slug'], $ignored, true)) { 39 47 continue; 40 48 } … … 241 249 } 242 250 243 private function getOptionWithLegacy(string $option, string $legacyOption, mixed $default): mixed244 {245 $value = get_option($option, $default);246 if ($value !== $default) {247 return $value;248 }249 250 $legacyValue = get_option($legacyOption, $default);251 if ($legacyValue !== $default) {252 update_option($option, $legacyValue, false);253 254 return $legacyValue;255 }256 257 return $value;258 }259 260 251 private function sanitizeRetention(?int $retention): int 261 252 { -
site-add-on-watchdog/trunk/src/Repository/SettingsRepository.php
r3612684 r3799692 69 69 } 70 70 71 $normalized = $this-> normalizeStoredSettings($stored);71 $normalized = $this->withoutNulls($this->normalizeStoredSettings($stored)); 72 72 $settings = array_replace_recursive($defaults, $normalized); 73 73 … … 92 92 93 93 $recipientList = $settings['notifications']['email']['recipients'] ?? ''; 94 if (! is_string($recipientList) || trim($recipientList) === '') { 95 $settings['notifications']['email']['recipients'] = $this->buildAdministratorEmailList(); 96 } else { 97 $settings['notifications']['email']['recipients'] = trim($recipientList); 98 } 94 $recipientList = is_string($recipientList) ? trim($recipientList) : ''; 95 if ($recipientList === '' && $shouldPersistDefaults) { 96 // Prefill once, when the defaults are first stored. A field the site 97 // owner emptied stays empty: administrators are added at send time, 98 // and looking them up here would cost a user query on every request. 99 $recipientList = $this->buildAdministratorEmailList(); 100 } 101 $settings['notifications']['email']['recipients'] = $recipientList; 99 102 100 103 $settings['history']['retention'] = $this->sanitizeRetention($settings['history']['retention'] ?? null); … … 396 399 } 397 400 401 /** 402 * A setting that was never saved is null after normalization. Merging that 403 * null over the defaults would switch default-on settings off, so drop it. 404 */ 405 private function withoutNulls(array $values): array 406 { 407 foreach ($values as $key => $value) { 408 if (is_array($value)) { 409 $values[$key] = $this->withoutNulls($value); 410 } elseif ($value === null) { 411 unset($values[$key]); 412 } 413 } 414 415 return $values; 416 } 417 398 418 private function sanitizeFrequency(mixed $frequency): string 399 419 { -
site-add-on-watchdog/trunk/src/Scanner.php
r3717433 r3799692 12 12 { 13 13 private const REMOTE_CACHE_HOURS = 6; 14 private const NOT_IN_DIRECTORY = 'not_in_directory'; 14 15 15 16 public function __construct( … … 229 230 return $cached; 230 231 } 232 if ($cached === self::NOT_IN_DIRECTORY) { 233 return false; 234 } 231 235 require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; 232 236 … … 240 244 241 245 if (is_wp_error($result)) { 246 if ($this->isMissingFromDirectory($result)) { 247 // Premium and custom plugins have no directory entry. That is a 248 // definitive answer, so cache it and keep the WPScan lookup running. 249 set_transient($cacheKey, self::NOT_IN_DIRECTORY, $this->remoteCacheTtl()); 250 251 return false; 252 } 253 242 254 // A temporary directory/API failure must not erase a saved risk or 243 255 // suppress retries for the full successful-response cache period. … … 248 260 249 261 return $result; 262 } 263 264 /** 265 * WordPress.org answers an unknown slug with this exact, untranslated API 266 * message. Any other error is treated as temporary. 267 */ 268 private function isMissingFromDirectory(object $error): bool 269 { 270 return method_exists($error, 'get_error_message') 271 && trim((string) $error->get_error_message()) === 'Plugin not found.'; 250 272 } 251 273 -
site-add-on-watchdog/trunk/src/Services/NotificationQueue.php
r3425098 r3799692 3 3 namespace Watchdog\Services; 4 4 5 use Watchdog\Repository\MigratesLegacyOptions; 5 6 use Watchdog\Version; 6 7 7 8 class NotificationQueue 8 9 { 10 use MigratesLegacyOptions; 11 9 12 private const PREFIX = Version::PREFIX; 10 13 private const QUEUE_OPTION = self::PREFIX . '_notification_queue'; … … 256 259 } 257 260 258 private function getOptionWithLegacy(string $option, string $legacy, mixed $default): mixed259 {260 $value = get_option($option, $default);261 if ($value !== $default) {262 return $value;263 }264 265 $legacyValue = get_option($legacy, $default);266 if ($legacyValue !== $default) {267 update_option($option, $legacyValue, false);268 269 return $legacyValue;270 }271 272 return $value;273 }274 275 261 private function calculateDelay(int $attempts): int 276 262 { -
site-add-on-watchdog/trunk/src/Services/WPScanClient.php
r3631212 r3799692 10 10 private const ERROR_TTL_HOURS = 6; 11 11 12 public function __construct(private readonly ?string $apiKey) 12 /** 13 * @param string|\Closure|null $apiKey The key, or a callback that returns it. The callback 14 * keeps the settings from being read while plugins load. 15 */ 16 public function __construct(private string|\Closure|null $apiKey) 13 17 { 14 18 } … … 16 20 public function isEnabled(): bool 17 21 { 18 return ! empty($this->apiKey); 22 return $this->apiKey() !== ''; 23 } 24 25 private function apiKey(): string 26 { 27 if ($this->apiKey instanceof \Closure) { 28 $this->apiKey = (string) ($this->apiKey)(); 29 } 30 31 return (string) $this->apiKey; 19 32 } 20 33 … … 45 58 [ 46 59 'headers' => [ 47 'Authorization' => sprintf('Token token=%s', $this->apiKey ),60 'Authorization' => sprintf('Token token=%s', $this->apiKey()), 48 61 'Accept' => 'application/json', 49 62 ], -
site-add-on-watchdog/trunk/src/Version.php
r3770137 r3799692 5 5 final class Version 6 6 { 7 public const NUMBER = '1. 9.0';7 public const NUMBER = '1.10.0'; 8 8 public const PREFIX = 'siteadwa'; 9 9 } -
site-add-on-watchdog/trunk/templates/admin-page.php
r3770137 r3799692 1 1 <?php 2 /** @var Risk[] $watchdogRisks */ 3 /** @var string[] $watchdogIgnored */ 4 /** @var array $watchdogSettings */ 5 /** @var int $watchdogHistoryRetention */ 6 /** @var int $watchdogHistoryDisplay */ 7 /** @var array<int, array{run_at:int, risks:array<int, array<string, mixed>>, risk_count:int}> $watchdogHistoryRecords */ 8 /** @var array<int, array<string, string>> $watchdogHistoryDownloads */ 9 /** @var array $watchdogCronStatus */ 10 /** @var string $watchdogCronEndpoint */ 11 /** @var string $watchdogCronSecret */ 12 /** @var string|null $watchdogSettingsError */ 13 /** @var array|null $watchdogWpScanError */ 14 /** @var string|false $watchdogScanError */ 15 /** @var array|null $watchdogLastFailedNotification */ 16 /** @var array{length:int,next_attempt_at:?int} $watchdogQueueStatus */ 17 /** @var string $watchdogActionPrefix */ 18 /** @var array<string, bool> $watchdogNoticeFlags */ 19 /** @var string $watchdogNotificationResult */ 20 /** @var string $watchdogFailedNotificationStatus */ 21 /** @var bool $watchdogCronSecretPersisted */ 22 /** @var string $watchdogChannelTest */ 23 /** @var string $watchdogChannelTestStatus */ 2 /** 3 * Watchdog dashboard. 4 * 5 * @var array<int, array{risk: Risk, vulnerabilities: array<int, array<string, mixed>>, attributes: array<string, string>}> $watchdogRiskRows 6 * @var int $watchdogVulnerabilityCount 7 * @var string[] $watchdogIgnored 8 * @var array $watchdogSettings 9 * @var int $watchdogHistoryRetention 10 * @var int $watchdogHistoryDisplay 11 * @var array<int, array{run_at:int, risks:array<int, array<string, mixed>>, risk_count:int}> $watchdogHistoryRecords 12 * @var array<int, array<string, string>> $watchdogHistoryDownloads 13 * @var int|null $watchdogLastScanAt 14 * @var int|null $watchdogNextScanAt 15 * @var array{cron_disabled: bool, overdue_streak: int} $watchdogCronStatus 16 * @var string $watchdogCronEndpoint 17 * @var string $watchdogCronSecret 18 * @var bool $watchdogCronSecretPersisted 19 * @var string|false $watchdogSettingsError 20 * @var array|false $watchdogWpScanError 21 * @var string|false $watchdogScanError 22 * @var array|null $watchdogLastFailedNotification 23 * @var array{length:int,next_attempt_at:?int} $watchdogQueueStatus 24 * @var string $watchdogActionPrefix 25 * @var array<string, bool> $watchdogNoticeFlags 26 * @var string $watchdogNotificationResult 27 * @var string $watchdogFailedNotificationStatus 28 * @var string $watchdogChannelTest 29 * @var string $watchdogChannelTestStatus 30 * @var string $watchdogRiskSearch 31 * @var string $watchdogRiskSortSelection 32 * @var string $watchdogRiskOrderSelection 33 */ 24 34 25 35 use Watchdog\Models\Risk; 26 36 use Watchdog\TestingMode; 37 27 38 defined('ABSPATH') || exit; 28 39 29 $watchdogActionPrefix = $watchdogActionPrefix ?? \Watchdog\Version::PREFIX; 40 $watchdogNotifications = $watchdogSettings['notifications']; 41 $watchdogPostUrl = admin_url('admin-post.php'); 42 $watchdogDateFormat = get_option('date_format') . ' ' . get_option('time_format'); 43 $watchdogNow = time(); 44 $watchdogRiskTotal = count($watchdogRiskRows); 45 $watchdogQueueLength = (int) ($watchdogQueueStatus['length'] ?? 0); 46 $watchdogQueueNextAttempt = $watchdogQueueStatus['next_attempt_at'] ?? null; 47 $watchdogHasFailedNotification = ! empty($watchdogLastFailedNotification); 48 $watchdogIsCronDisabled = ! empty($watchdogCronStatus['cron_disabled']); 49 50 $watchdogChannelLabels = [ 51 'email' => __('Email', 'site-add-on-watchdog'), 52 'discord' => __('Discord', 'site-add-on-watchdog'), 53 'slack' => __('Slack', 'site-add-on-watchdog'), 54 'teams' => __('Microsoft Teams', 'site-add-on-watchdog'), 55 'webhook' => __('Custom webhook', 'site-add-on-watchdog'), 56 ]; 30 57 $watchdogEnabledChannelCount = 0; 31 foreach ( ['email', 'discord', 'slack', 'teams', 'webhook']as $watchdogChannelKey) {32 if (! empty($watchdog Settings['notifications'][$watchdogChannelKey]['enabled'])) {58 foreach (array_keys($watchdogChannelLabels) as $watchdogChannelKey) { 59 if (! empty($watchdogNotifications[$watchdogChannelKey]['enabled'])) { 33 60 $watchdogEnabledChannelCount++; 34 61 } 35 62 } 36 $watchdogQueueLength = (int) ($watchdogQueueStatus['length'] ?? 0); 63 37 64 $watchdogFrequencyLabels = [ 38 'daily' => __('Daily', 'site-add-on-watchdog'),39 'weekly' => __('Weekly', 'site-add-on-watchdog'),65 'daily' => __('Daily', 'site-add-on-watchdog'), 66 'weekly' => __('Weekly', 'site-add-on-watchdog'), 40 67 'testing' => __('Testing', 'site-add-on-watchdog'), 41 'manual' => __('Manual', 'site-add-on-watchdog'),68 'manual' => __('Manual', 'site-add-on-watchdog'), 42 69 ]; 43 $watchdogCurrentFrequency = (string) ($watchdogSettings['notifications']['frequency'] ?? 'daily'); 44 $watchdogFrequencyLabel = $watchdogFrequencyLabels[$watchdogCurrentFrequency] 45 ?? $watchdogFrequencyLabels['daily']; 70 $watchdogCurrentFrequency = (string) ($watchdogNotifications['frequency'] ?? 'daily'); 71 if (! isset($watchdogFrequencyLabels[$watchdogCurrentFrequency])) { 72 $watchdogCurrentFrequency = 'daily'; 73 } 74 75 // Header summary. 76 if ($watchdogLastScanAt === null) { 77 $watchdogSummary = __('No scan has run yet. Run a manual scan to create your first report.', 'site-add-on-watchdog'); 78 } elseif ($watchdogRiskTotal > 0) { 79 $watchdogSummary = sprintf( 80 /* translators: %s: number of plugins. */ 81 _n('%s plugin needs attention.', '%s plugins need attention.', $watchdogRiskTotal, 'site-add-on-watchdog'), 82 number_format_i18n($watchdogRiskTotal) 83 ); 84 } else { 85 $watchdogSummary = __('No risks found in the latest scan.', 'site-add-on-watchdog'); 86 } 87 88 // Overview tiles. 89 if ($watchdogLastScanAt === null) { 90 $watchdogRiskTone = 'neutral'; 91 $watchdogRiskHint = __('Not scanned yet', 'site-add-on-watchdog'); 92 } elseif ($watchdogRiskTotal === 0) { 93 $watchdogRiskTone = 'ok'; 94 $watchdogRiskHint = __('All clear', 'site-add-on-watchdog'); 95 } elseif ($watchdogVulnerabilityCount > 0) { 96 $watchdogRiskTone = 'danger'; 97 $watchdogRiskHint = sprintf( 98 /* translators: %s: number of vulnerabilities. */ 99 _n('%s known vulnerability', '%s known vulnerabilities', $watchdogVulnerabilityCount, 'site-add-on-watchdog'), 100 number_format_i18n($watchdogVulnerabilityCount) 101 ); 102 } else { 103 $watchdogRiskTone = 'warn'; 104 $watchdogRiskHint = __('Updates available', 'site-add-on-watchdog'); 105 } 106 107 if ($watchdogLastScanAt === null) { 108 $watchdogLastScanValue = __('Never', 'site-add-on-watchdog'); 109 $watchdogLastScanHint = __('Run a manual scan', 'site-add-on-watchdog'); 110 } else { 111 $watchdogLastScanValue = sprintf( 112 /* translators: %s: human-readable time difference, for example "2 hours". */ 113 __('%s ago', 'site-add-on-watchdog'), 114 human_time_diff($watchdogLastScanAt, $watchdogNow) 115 ); 116 $watchdogLastScanHint = wp_date($watchdogDateFormat, $watchdogLastScanAt); 117 } 118 119 if ($watchdogCurrentFrequency === 'manual') { 120 $watchdogNextScanValue = __('Manual', 'site-add-on-watchdog'); 121 $watchdogNextScanHint = __('Automatic scans are off', 'site-add-on-watchdog'); 122 } elseif ($watchdogNextScanAt === null) { 123 $watchdogNextScanValue = __('Not scheduled', 'site-add-on-watchdog'); 124 $watchdogNextScanHint = __('Save the settings to reschedule', 'site-add-on-watchdog'); 125 } else { 126 $watchdogNextScanValue = $watchdogNextScanAt > $watchdogNow 127 ? sprintf( 128 /* translators: %s: human-readable time difference, for example "2 hours". */ 129 __('In %s', 'site-add-on-watchdog'), 130 human_time_diff($watchdogNow, $watchdogNextScanAt) 131 ) 132 : __('Due now', 'site-add-on-watchdog'); 133 $watchdogNextScanHint = sprintf( 134 /* translators: 1: scan frequency, 2: date and time. */ 135 __('%1$s, %2$s', 'site-add-on-watchdog'), 136 $watchdogFrequencyLabels[$watchdogCurrentFrequency], 137 wp_date($watchdogDateFormat, $watchdogNextScanAt) 138 ); 139 } 140 141 $watchdogChannelTone = 'neutral'; 142 if ($watchdogHasFailedNotification) { 143 $watchdogChannelTone = 'warn'; 144 $watchdogChannelHint = __('Last delivery failed', 'site-add-on-watchdog'); 145 } elseif ($watchdogQueueLength > 0) { 146 $watchdogChannelHint = sprintf( 147 /* translators: %s: number of queued alerts. */ 148 _n('%s alert queued', '%s alerts queued', $watchdogQueueLength, 'site-add-on-watchdog'), 149 number_format_i18n($watchdogQueueLength) 150 ); 151 } elseif ($watchdogEnabledChannelCount === 0) { 152 $watchdogChannelTone = 'warn'; 153 $watchdogChannelHint = __('No channel enabled', 'site-add-on-watchdog'); 154 } else { 155 $watchdogChannelHint = __('No failed deliveries', 'site-add-on-watchdog'); 156 } 157 158 $watchdogTabs = [ 159 'watchdog-risks' => __('Risks', 'site-add-on-watchdog'), 160 'watchdog-history' => __('History', 'site-add-on-watchdog'), 161 'watchdog-settings' => __('Settings', 'site-add-on-watchdog'), 162 'watchdog-delivery' => __('Delivery', 'site-add-on-watchdog'), 163 ]; 46 164 ?> 47 <div class="wrap wp-watchdog-admin"> 48 <header class="wp-watchdog-hero"> 49 <div class="wp-watchdog-hero__content"> 50 <div class="wp-watchdog-hero__eyebrow"> 51 <span><?php esc_html_e('Plugin health and security', 'site-add-on-watchdog'); ?></span> 52 <span aria-hidden="true">·</span> 53 <span><?php echo esc_html('v' . \Watchdog\Version::NUMBER); ?></span> 165 <div class="wrap wp-watchdog-admin"<?php echo ! empty($watchdogNoticeFlags['scan']) ? ' data-watchdog-initial-tab="watchdog-risks"' : ''; ?>> 166 <header class="wp-watchdog-header"> 167 <div class="wp-watchdog-header__brand"> 168 <span class="wp-watchdog-header__icon dashicons dashicons-shield" aria-hidden="true"></span> 169 <div> 170 <h1 class="wp-watchdog-header__title"> 171 <?php esc_html_e('Site Add-on Watchdog', 'site-add-on-watchdog'); ?> 172 <span class="wp-watchdog-header__version"><?php echo esc_html('v' . \Watchdog\Version::NUMBER); ?></span> 173 </h1> 174 <p class="wp-watchdog-header__summary"><?php echo esc_html($watchdogSummary); ?></p> 54 175 </div> 55 <h1 class="wp-watchdog-hero__title">56 <span class="dashicons dashicons-shield"></span>57 <?php esc_html_e('Site Add-on Watchdog', 'site-add-on-watchdog'); ?>58 </h1>59 <p class="wp-watchdog-hero__description"><?php esc_html_e('Monitor plugin health, review scan history, and verify every alert channel from one focused dashboard.', 'site-add-on-watchdog'); ?></p>60 176 </div> 61 177 <div class="wp-watchdog-actions"> 62 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> 178 <form method="post" action="<?php echo esc_url($watchdogPostUrl); ?>"> 179 <?php wp_nonce_field($watchdogActionPrefix . '_send_notifications'); ?> 180 <input type="hidden" name="action" value="<?php echo esc_attr($watchdogActionPrefix . '_send_notifications'); ?>"> 181 <button class="button" type="submit"> 182 <span class="dashicons dashicons-megaphone" aria-hidden="true"></span> 183 <?php esc_html_e('Send notifications now', 'site-add-on-watchdog'); ?> 184 </button> 185 </form> 186 <form method="post" action="<?php echo esc_url($watchdogPostUrl); ?>"> 63 187 <?php wp_nonce_field($watchdogActionPrefix . '_scan'); ?> 64 188 <input type="hidden" name="action" value="<?php echo esc_attr($watchdogActionPrefix . '_scan'); ?>"> 65 <button class="button button-primary button-hero" type="submit">189 <button class="button button-primary" type="submit"> 66 190 <span class="dashicons dashicons-update" aria-hidden="true"></span> 67 191 <?php esc_html_e('Run manual scan', 'site-add-on-watchdog'); ?> 68 192 </button> 69 193 </form> 70 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">71 <?php wp_nonce_field($watchdogActionPrefix . '_send_notifications'); ?>72 <input type="hidden" name="action" value="<?php echo esc_attr($watchdogActionPrefix . '_send_notifications'); ?>">73 <input type="hidden" name="force" value="1" />74 <button class="button button-secondary button-hero" type="submit">75 <span class="dashicons dashicons-megaphone" aria-hidden="true"></span>76 <?php esc_html_e('Send notifications now', 'site-add-on-watchdog'); ?>77 </button>78 </form>79 194 </div> 80 195 </header> 81 196 82 <nav class="wp-watchdog-nav" aria-label="<?php esc_attr_e('Watchdog sections', 'site-add-on-watchdog'); ?>"> 83 <a href="#watchdog-risks"><?php esc_html_e('Risks', 'site-add-on-watchdog'); ?></a> 84 <a href="#watchdog-history"><?php esc_html_e('History', 'site-add-on-watchdog'); ?></a> 85 <a href="#watchdog-delivery"><?php esc_html_e('Delivery', 'site-add-on-watchdog'); ?></a> 86 <a href="#watchdog-settings"><?php esc_html_e('Settings', 'site-add-on-watchdog'); ?></a> 87 </nav> 88 89 <section class="wp-watchdog-overview" aria-label="<?php esc_attr_e('Watchdog overview', 'site-add-on-watchdog'); ?>"> 90 <a class="wp-watchdog-stat" href="#watchdog-risks"> 91 <span class="wp-watchdog-stat__icon dashicons dashicons-shield-alt" aria-hidden="true"></span> 92 <span class="wp-watchdog-stat__value"><?php echo esc_html(number_format_i18n(count($watchdogRisks))); ?></span> 93 <span class="wp-watchdog-stat__label"><?php esc_html_e('Open risks', 'site-add-on-watchdog'); ?></span> 94 </a> 95 <a class="wp-watchdog-stat" href="#watchdog-settings"> 96 <span class="wp-watchdog-stat__icon dashicons dashicons-megaphone" aria-hidden="true"></span> 97 <span class="wp-watchdog-stat__value"><?php echo esc_html(number_format_i18n($watchdogEnabledChannelCount)); ?>/5</span> 98 <span class="wp-watchdog-stat__label"><?php esc_html_e('Active channels', 'site-add-on-watchdog'); ?></span> 99 </a> 100 <a class="wp-watchdog-stat" href="#watchdog-delivery"> 101 <span class="wp-watchdog-stat__icon dashicons dashicons-controls-repeat" aria-hidden="true"></span> 102 <span class="wp-watchdog-stat__value"><?php echo esc_html(number_format_i18n($watchdogQueueLength)); ?></span> 103 <span class="wp-watchdog-stat__label"><?php esc_html_e('Queued alerts', 'site-add-on-watchdog'); ?></span> 104 </a> 105 <a class="wp-watchdog-stat" href="#watchdog-settings"> 106 <span class="wp-watchdog-stat__icon dashicons dashicons-clock" aria-hidden="true"></span> 107 <span class="wp-watchdog-stat__value wp-watchdog-stat__value--text"><?php echo esc_html($watchdogFrequencyLabel); ?></span> 108 <span class="wp-watchdog-stat__label"><?php esc_html_e('Scan schedule', 'site-add-on-watchdog'); ?></span> 109 </a> 110 </section> 197 <hr class="wp-header-end"> 111 198 112 199 <?php $watchdogWebhookError = get_transient($watchdogActionPrefix . '_webhook_error'); ?> … … 153 240 <?php endif; ?> 154 241 155 <?php if ($watchdogNotificationResult !== '') : ?> 156 <?php if ($watchdogNotificationResult === 'sent') : ?> 157 <div class="notice notice-success is-dismissible"><p><?php esc_html_e('Notifications were dispatched.', 'site-add-on-watchdog'); ?></p></div> 158 <?php elseif ($watchdogNotificationResult === 'throttled') : ?> 159 <div class="notice notice-warning is-dismissible"><p><?php esc_html_e('Notifications skipped to avoid rapid re-sends. Please wait a moment and try again.', 'site-add-on-watchdog'); ?></p></div> 160 <?php elseif ($watchdogNotificationResult === 'unchanged') : ?> 161 <div class="notice notice-info is-dismissible"><p><?php esc_html_e('No notification changes detected since the last send.', 'site-add-on-watchdog'); ?></p></div> 162 <?php endif; ?> 242 <?php if ($watchdogNotificationResult === 'sent') : ?> 243 <div class="notice notice-success is-dismissible"><p><?php esc_html_e('Notifications were dispatched.', 'site-add-on-watchdog'); ?></p></div> 244 <?php elseif ($watchdogNotificationResult === 'throttled') : ?> 245 <div class="notice notice-warning is-dismissible"><p><?php esc_html_e('Notifications skipped to avoid rapid re-sends. Please wait a moment and try again.', 'site-add-on-watchdog'); ?></p></div> 163 246 <?php endif; ?> 164 247 165 <?php if ($watchdogFailedNotificationStatus !== '') : ?> 166 <?php if ($watchdogFailedNotificationStatus === 'resent') : ?> 167 <div class="notice notice-success is-dismissible"><p><?php esc_html_e('Queued the captured notification payload for resend.', 'site-add-on-watchdog'); ?></p></div> 168 <?php elseif ($watchdogFailedNotificationStatus === 'missing') : ?> 169 <div class="notice notice-error is-dismissible"><p><?php esc_html_e('No failed notification payload was available to resend.', 'site-add-on-watchdog'); ?></p></div> 170 <?php endif; ?> 248 <?php if ($watchdogFailedNotificationStatus === 'resent') : ?> 249 <div class="notice notice-success is-dismissible"><p><?php esc_html_e('Queued the captured notification payload for resend.', 'site-add-on-watchdog'); ?></p></div> 250 <?php elseif ($watchdogFailedNotificationStatus === 'missing') : ?> 251 <div class="notice notice-error is-dismissible"><p><?php esc_html_e('No failed notification payload was available to resend.', 'site-add-on-watchdog'); ?></p></div> 171 252 <?php endif; ?> 172 253 173 254 <?php if ($watchdogChannelTest !== '' && $watchdogChannelTestStatus !== '') : ?> 174 255 <?php 175 $watchdogChannelTestLabels = [ 176 'email' => __('Email', 'site-add-on-watchdog'), 177 'discord' => __('Discord', 'site-add-on-watchdog'), 178 'slack' => __('Slack', 'site-add-on-watchdog'), 179 'teams' => __('Microsoft Teams', 'site-add-on-watchdog'), 180 'webhook' => __('Custom webhook', 'site-add-on-watchdog'), 181 ]; 182 $watchdogChannelTestLabel = $watchdogChannelTestLabels[$watchdogChannelTest] 256 $watchdogChannelTestLabel = $watchdogChannelLabels[$watchdogChannelTest] 183 257 ?? __('Notification channel', 'site-add-on-watchdog'); 184 258 ?> … … 222 296 printf( 223 297 /* translators: %s: notification channel name. */ 224 esc_html__('%s test delivery failed. Review Delivery healthfor details.', 'site-add-on-watchdog'),298 esc_html__('%s test delivery failed. Open the Delivery tab for details.', 'site-add-on-watchdog'), 225 299 esc_html($watchdogChannelTestLabel) 226 300 ); … … 230 304 <?php endif; ?> 231 305 232 <div class="wp-watchdog-grid"> 233 <div class="wp-watchdog-surface" id="watchdog-delivery"> 234 <div class="wp-watchdog-section-title"> 235 <span class="dashicons dashicons-heart" aria-hidden="true"></span> 236 <?php esc_html_e('Delivery health', 'site-add-on-watchdog'); ?> 306 <section class="wp-watchdog-overview" aria-label="<?php esc_attr_e('Watchdog overview', 'site-add-on-watchdog'); ?>"> 307 <a class="wp-watchdog-stat wp-watchdog-stat--<?php echo esc_attr($watchdogRiskTone); ?>" href="#watchdog-risks"> 308 <span class="wp-watchdog-stat__icon dashicons dashicons-shield-alt" aria-hidden="true"></span> 309 <span class="wp-watchdog-stat__label"><?php esc_html_e('Open risks', 'site-add-on-watchdog'); ?></span> 310 <span class="wp-watchdog-stat__value"><?php echo esc_html(number_format_i18n($watchdogRiskTotal)); ?></span> 311 <span class="wp-watchdog-stat__hint"><?php echo esc_html($watchdogRiskHint); ?></span> 312 </a> 313 <a class="wp-watchdog-stat" href="#watchdog-history"> 314 <span class="wp-watchdog-stat__icon dashicons dashicons-backup" aria-hidden="true"></span> 315 <span class="wp-watchdog-stat__label"><?php esc_html_e('Last scan', 'site-add-on-watchdog'); ?></span> 316 <span class="wp-watchdog-stat__value wp-watchdog-stat__value--text"><?php echo esc_html($watchdogLastScanValue); ?></span> 317 <span class="wp-watchdog-stat__hint"><?php echo esc_html($watchdogLastScanHint); ?></span> 318 </a> 319 <a class="wp-watchdog-stat" href="#watchdog-settings"> 320 <span class="wp-watchdog-stat__icon dashicons dashicons-clock" aria-hidden="true"></span> 321 <span class="wp-watchdog-stat__label"><?php esc_html_e('Next scan', 'site-add-on-watchdog'); ?></span> 322 <span class="wp-watchdog-stat__value wp-watchdog-stat__value--text"><?php echo esc_html($watchdogNextScanValue); ?></span> 323 <span class="wp-watchdog-stat__hint"><?php echo esc_html($watchdogNextScanHint); ?></span> 324 </a> 325 <a class="wp-watchdog-stat wp-watchdog-stat--<?php echo esc_attr($watchdogChannelTone); ?>" href="#watchdog-delivery"> 326 <span class="wp-watchdog-stat__icon dashicons dashicons-megaphone" aria-hidden="true"></span> 327 <span class="wp-watchdog-stat__label"><?php esc_html_e('Active channels', 'site-add-on-watchdog'); ?></span> 328 <span class="wp-watchdog-stat__value"><?php echo esc_html(number_format_i18n($watchdogEnabledChannelCount) . '/' . number_format_i18n(count($watchdogChannelLabels))); ?></span> 329 <span class="wp-watchdog-stat__hint"><?php echo esc_html($watchdogChannelHint); ?></span> 330 </a> 331 </section> 332 333 <nav class="wp-watchdog-tabs" aria-label="<?php esc_attr_e('Watchdog sections', 'site-add-on-watchdog'); ?>"> 334 <?php foreach ($watchdogTabs as $watchdogTabId => $watchdogTabLabel) : ?> 335 <a class="wp-watchdog-tabs__tab" href="#<?php echo esc_attr($watchdogTabId); ?>" data-watchdog-tab="<?php echo esc_attr($watchdogTabId); ?>"> 336 <?php echo esc_html($watchdogTabLabel); ?> 337 <?php if ($watchdogTabId === 'watchdog-risks' && $watchdogRiskTotal > 0) : ?> 338 <span class="wp-watchdog-tabs__count"><?php echo esc_html(number_format_i18n($watchdogRiskTotal)); ?></span> 339 <?php elseif ($watchdogTabId === 'watchdog-delivery' && ($watchdogHasFailedNotification || $watchdogQueueLength > 0)) : ?> 340 <span class="wp-watchdog-tabs__dot" aria-hidden="true"></span> 341 <span class="screen-reader-text"><?php esc_html_e('Needs attention', 'site-add-on-watchdog'); ?></span> 342 <?php endif; ?> 343 </a> 344 <?php endforeach; ?> 345 </nav> 346 347 <section class="wp-watchdog-panel" id="watchdog-risks" data-watchdog-panel> 348 <div class="wp-watchdog-card"> 349 <div class="wp-watchdog-card__header"> 350 <div> 351 <h2 class="wp-watchdog-card__title"><?php esc_html_e('Potential Risks', 'site-add-on-watchdog'); ?></h2> 352 <p class="wp-watchdog-card__description"><?php esc_html_e('Plugins with a newer release, a security note in the latest changelog, or a known vulnerability.', 'site-add-on-watchdog'); ?></p> 353 </div> 354 <?php if ($watchdogRiskTotal > 0) : ?> 355 <a class="button" href="<?php echo esc_url(admin_url('update-core.php')); ?>"><?php esc_html_e('Review updates', 'site-add-on-watchdog'); ?></a> 356 <?php endif; ?> 237 357 </div> 238 <?php $watchdogIsCronDisabled = ! empty($watchdogCronStatus['cron_disabled']); ?> 358 359 <?php if ($watchdogRiskTotal === 0) : ?> 360 <div class="wp-watchdog-empty"> 361 <span class="wp-watchdog-empty__icon dashicons <?php echo $watchdogLastScanAt === null ? 'dashicons-search' : 'dashicons-yes-alt'; ?>" aria-hidden="true"></span> 362 <?php if ($watchdogLastScanAt === null) : ?> 363 <p class="wp-watchdog-empty__title"><?php esc_html_e('No scan results yet', 'site-add-on-watchdog'); ?></p> 364 <p class="wp-watchdog-empty__text"><?php esc_html_e('Run a manual scan to check your plugins now.', 'site-add-on-watchdog'); ?></p> 365 <?php else : ?> 366 <p class="wp-watchdog-empty__title"><?php esc_html_e('No risks detected.', 'site-add-on-watchdog'); ?></p> 367 <p class="wp-watchdog-empty__text"><?php esc_html_e('Nothing was flagged in the latest scan.', 'site-add-on-watchdog'); ?></p> 368 <?php endif; ?> 369 </div> 370 <?php else : ?> 371 <?php 372 $watchdogColumns = [ 373 'plugin' => __('Plugin', 'site-add-on-watchdog'), 374 'local' => __('Local Version', 'site-add-on-watchdog'), 375 'remote' => __('Directory Version', 'site-add-on-watchdog'), 376 'reasons' => __('Reasons', 'site-add-on-watchdog'), 377 'actions' => __('Actions', 'site-add-on-watchdog'), 378 ]; 379 $watchdogSortOptions = [ 380 'plugin' => ['sortPlugin', 'asc', __('Plugin name', 'site-add-on-watchdog')], 381 'risk_count' => ['sortRiskCount', 'desc', __('Risk count', 'site-add-on-watchdog')], 382 'version_gap' => ['sortVersionGap', 'desc', __('Version gap', 'site-add-on-watchdog')], 383 'local' => ['sortLocal', 'desc', __('Local version', 'site-add-on-watchdog')], 384 'remote' => ['sortRemote', 'desc', __('Directory version', 'site-add-on-watchdog')], 385 'reasons' => ['sortReasons', 'asc', __('Reasons', 'site-add-on-watchdog')], 386 ]; 387 $watchdogSortableColumns = [ 388 'plugin' => ['sortPlugin', 'asc'], 389 'local' => ['sortLocal', 'desc'], 390 'remote' => ['sortRemote', 'desc'], 391 'reasons' => ['sortReasons', 'asc'], 392 ]; 393 $watchdogPerPage = max(1, (int) apply_filters('site_add_on_watchdog_admin_risks_per_page', 10)); 394 ?> 395 <div class="wp-watchdog-risk-table" data-wp-watchdog-table data-per-page="<?php echo esc_attr((string) $watchdogPerPage); ?>"> 396 <div class="wp-watchdog-risk-table__controls"> 397 <form method="get" class="wp-watchdog-risk-table__filter-form"> 398 <input type="hidden" name="page" value="site-add-on-watchdog" /> 399 <label class="wp-watchdog-risk-table__search"> 400 <span class="screen-reader-text"><?php esc_html_e('Search risks', 'site-add-on-watchdog'); ?></span> 401 <input 402 type="search" 403 name="risk_search" 404 value="<?php echo esc_attr($watchdogRiskSearch); ?>" 405 placeholder="<?php esc_attr_e('Search plugin or reason', 'site-add-on-watchdog'); ?>" 406 data-risk-search 407 /> 408 </label> 409 <label class="screen-reader-text" for="wp-watchdog-risk-sort"><?php esc_html_e('Sort risks by', 'site-add-on-watchdog'); ?></label> 410 <select id="wp-watchdog-risk-sort" name="risk_sort" data-risk-sort> 411 <?php foreach ($watchdogSortOptions as $watchdogSortValue => [$watchdogSortKey, $watchdogSortDefault, $watchdogSortLabel]) : ?> 412 <option 413 value="<?php echo esc_attr($watchdogSortValue); ?>" 414 data-sort-key="<?php echo esc_attr($watchdogSortKey); ?>" 415 data-sort-default="<?php echo esc_attr($watchdogSortDefault); ?>" 416 <?php selected($watchdogRiskSortSelection, $watchdogSortValue); ?> 417 ><?php echo esc_html($watchdogSortLabel); ?></option> 418 <?php endforeach; ?> 419 </select> 420 <label class="screen-reader-text" for="wp-watchdog-risk-order"><?php esc_html_e('Sort order', 'site-add-on-watchdog'); ?></label> 421 <select id="wp-watchdog-risk-order" name="risk_order" data-risk-order> 422 <option value="asc" <?php selected($watchdogRiskOrderSelection, 'asc'); ?>><?php esc_html_e('Ascending', 'site-add-on-watchdog'); ?></option> 423 <option value="desc" <?php selected($watchdogRiskOrderSelection, 'desc'); ?>><?php esc_html_e('Descending', 'site-add-on-watchdog'); ?></option> 424 </select> 425 <button class="button wp-watchdog-no-js" type="submit"><?php esc_html_e('Apply', 'site-add-on-watchdog'); ?></button> 426 </form> 427 <div class="wp-watchdog-risk-table__pagination" data-pagination> 428 <button type="button" class="button" data-action="prev" aria-label="<?php esc_attr_e('Previous page', 'site-add-on-watchdog'); ?>" disabled>‹</button> 429 <span class="wp-watchdog-risk-table__page-status" data-page-status aria-live="polite"></span> 430 <button type="button" class="button" data-action="next" aria-label="<?php esc_attr_e('Next page', 'site-add-on-watchdog'); ?>" disabled>›</button> 431 </div> 432 </div> 433 <div class="wp-watchdog-risk-table__scroll"> 434 <table class="widefat striped"> 435 <thead> 436 <tr> 437 <?php foreach ($watchdogSortableColumns as $watchdogColumnKey => [$watchdogSortKey, $watchdogSortDefault]) : ?> 438 <th scope="col" class="wp-watchdog-risk-table__col-<?php echo esc_attr($watchdogColumnKey); ?>"> 439 <button 440 type="button" 441 class="wp-watchdog-risk-table__sort" 442 data-sort-key="<?php echo esc_attr($watchdogSortKey); ?>" 443 data-sort-default="<?php echo esc_attr($watchdogSortDefault); ?>" 444 <?php echo $watchdogColumnKey === 'plugin' ? 'data-sort-initial' : ''; ?> 445 aria-sort="none" 446 > 447 <?php echo esc_html($watchdogColumns[$watchdogColumnKey]); ?> 448 <span class="wp-watchdog-risk-table__sort-indicator" aria-hidden="true"></span> 449 </button> 450 </th> 451 <?php endforeach; ?> 452 <th scope="col" class="wp-watchdog-risk-table__col-actions"><?php echo esc_html($watchdogColumns['actions']); ?></th> 453 </tr> 454 </thead> 455 <tbody> 456 <?php foreach ($watchdogRiskRows as $watchdogRiskRow) : ?> 457 <?php 458 $watchdogRisk = $watchdogRiskRow['risk']; 459 $watchdogVulnerabilities = $watchdogRiskRow['vulnerabilities']; 460 ?> 461 <tr 462 <?php foreach ($watchdogRiskRow['attributes'] as $watchdogAttributeName => $watchdogAttributeValue) : ?> 463 <?php echo esc_attr($watchdogAttributeName); ?>="<?php echo esc_attr($watchdogAttributeValue); ?>" 464 <?php endforeach; ?> 465 > 466 <td data-column="plugin" data-column-label="<?php echo esc_attr($watchdogColumns['plugin']); ?>"> 467 <strong><?php echo esc_html($watchdogRisk->pluginName); ?></strong> 468 </td> 469 <td data-column="local" data-column-label="<?php echo esc_attr($watchdogColumns['local']); ?>"> 470 <?php echo esc_html($watchdogRisk->localVersion); ?> 471 </td> 472 <td data-column="remote" data-column-label="<?php echo esc_attr($watchdogColumns['remote']); ?>"> 473 <?php echo esc_html($watchdogRisk->remoteVersion ?? __('N/A', 'site-add-on-watchdog')); ?> 474 </td> 475 <td data-column="reasons" data-column-label="<?php echo esc_attr($watchdogColumns['reasons']); ?>"> 476 <ul class="wp-watchdog-reasons"> 477 <?php foreach ($watchdogRisk->reasons as $watchdogReason) : ?> 478 <li><?php echo esc_html($watchdogReason); ?></li> 479 <?php endforeach; ?> 480 </ul> 481 <?php if ($watchdogVulnerabilities !== []) : ?> 482 <ul class="wp-watchdog-vulnerabilities" aria-label="<?php esc_attr_e('WPScan vulnerabilities:', 'site-add-on-watchdog'); ?>"> 483 <?php foreach ($watchdogVulnerabilities as $watchdogVuln) : ?> 484 <li> 485 <?php if (! empty($watchdogVuln['severity']) && ! empty($watchdogVuln['severity_label'])) : ?> 486 <span class="wp-watchdog-severity wp-watchdog-severity--<?php echo esc_attr(sanitize_html_class((string) $watchdogVuln['severity'])); ?>"><?php echo esc_html($watchdogVuln['severity_label']); ?></span> 487 <?php endif; ?> 488 <?php if (! empty($watchdogVuln['title'])) : ?> 489 <span class="wp-watchdog-vulnerability__title"><?php echo esc_html($watchdogVuln['title']); ?></span> 490 <?php endif; ?> 491 <?php if (! empty($watchdogVuln['cve'])) : ?> 492 <span class="wp-watchdog-vulnerability__meta"><?php echo esc_html($watchdogVuln['cve']); ?></span> 493 <?php endif; ?> 494 <?php if (! empty($watchdogVuln['fixed_in'])) : ?> 495 <span class="wp-watchdog-vulnerability__meta"> 496 <?php 497 printf( 498 /* translators: %s: plugin version number. */ 499 esc_html__('Fixed in %s', 'site-add-on-watchdog'), 500 esc_html($watchdogVuln['fixed_in']) 501 ); 502 ?> 503 </span> 504 <?php endif; ?> 505 </li> 506 <?php endforeach; ?> 507 </ul> 508 <?php endif; ?> 509 </td> 510 <td data-column="actions" data-column-label="<?php echo esc_attr($watchdogColumns['actions']); ?>"> 511 <form method="post" action="<?php echo esc_url($watchdogPostUrl); ?>"> 512 <?php wp_nonce_field($watchdogActionPrefix . '_ignore'); ?> 513 <input type="hidden" name="action" value="<?php echo esc_attr($watchdogActionPrefix . '_ignore'); ?>"> 514 <input type="hidden" name="plugin_slug" value="<?php echo esc_attr($watchdogRisk->pluginSlug); ?>"> 515 <button class="button button-small" type="submit"><?php esc_html_e('Ignore', 'site-add-on-watchdog'); ?></button> 516 </form> 517 </td> 518 </tr> 519 <?php endforeach; ?> 520 </tbody> 521 </table> 522 </div> 523 </div> 524 <?php endif; ?> 525 </div> 526 527 <div class="wp-watchdog-card" id="watchdog-ignored"> 528 <h2 class="wp-watchdog-card__title"><?php esc_html_e('Ignored Plugins', 'site-add-on-watchdog'); ?></h2> 529 <?php if (empty($watchdogIgnored)) : ?> 530 <p class="wp-watchdog-card__description"><?php esc_html_e('No plugins are being ignored.', 'site-add-on-watchdog'); ?></p> 531 <?php else : ?> 532 <p class="wp-watchdog-card__description"><?php esc_html_e('Ignored plugins are skipped by every scan. Select one to watch it again.', 'site-add-on-watchdog'); ?></p> 533 <ul class="wp-watchdog-chips"> 534 <?php foreach ($watchdogIgnored as $watchdogIgnoredSlug) : ?> 535 <li> 536 <form method="post" action="<?php echo esc_url($watchdogPostUrl); ?>"> 537 <?php wp_nonce_field($watchdogActionPrefix . '_unignore'); ?> 538 <input type="hidden" name="action" value="<?php echo esc_attr($watchdogActionPrefix . '_unignore'); ?>"> 539 <input type="hidden" name="plugin_slug" value="<?php echo esc_attr($watchdogIgnoredSlug); ?>"> 540 <button class="wp-watchdog-chip" type="submit"> 541 <?php echo esc_html($watchdogIgnoredSlug); ?> 542 <span class="dashicons dashicons-no-alt" aria-hidden="true"></span> 543 <span class="screen-reader-text"><?php esc_html_e('Stop ignoring', 'site-add-on-watchdog'); ?></span> 544 </button> 545 </form> 546 </li> 547 <?php endforeach; ?> 548 </ul> 549 <?php endif; ?> 550 </div> 551 </section> 552 553 <section class="wp-watchdog-panel" id="watchdog-history" data-watchdog-panel> 554 <div class="wp-watchdog-card"> 555 <h2 class="wp-watchdog-card__title"><?php esc_html_e('Scan History', 'site-add-on-watchdog'); ?></h2> 556 <?php require __DIR__ . '/history.php'; ?> 557 </div> 558 </section> 559 560 <section class="wp-watchdog-panel" id="watchdog-settings" data-watchdog-panel> 561 <form class="wp-watchdog-settings-form" method="post" action="<?php echo esc_url($watchdogPostUrl); ?>"> 562 <?php wp_nonce_field($watchdogActionPrefix . '_settings'); ?> 563 <input type="hidden" name="action" value="<?php echo esc_attr($watchdogActionPrefix . '_save_settings'); ?>"> 564 565 <div class="wp-watchdog-card"> 566 <h2 class="wp-watchdog-card__title"><?php esc_html_e('Scanning', 'site-add-on-watchdog'); ?></h2> 567 <?php 568 $watchdogDefaultFrequencyMessage = __('Choose how often the automatic scan should run.', 'site-add-on-watchdog'); 569 $watchdogTestingFrequencyMessage = sprintf( 570 /* translators: 1: interval minutes, 2: duration hours */ 571 __('Testing mode sends notifications every %1$d minutes to all configured channels and automatically switches back to daily scans after %2$d hours.', 'site-add-on-watchdog'), 572 TestingMode::INTERVAL_MINUTES, 573 TestingMode::DURATION_HOURS 574 ); 575 $watchdogIsTestingFrequency = $watchdogCurrentFrequency === 'testing'; 576 $watchdogTestingExpiresAt = (int) ($watchdogNotifications['testing_expires_at'] ?? 0); 577 $watchdogDailyTime = (string) ($watchdogNotifications['daily_time'] ?? '08:00'); 578 $watchdogWeeklyDay = (int) ($watchdogNotifications['weekly_day'] ?? 1); 579 $watchdogWeeklyTime = (string) ($watchdogNotifications['weekly_time'] ?? '08:00'); 580 $watchdogWeekdays = [ 581 1 => __('Monday', 'site-add-on-watchdog'), 582 2 => __('Tuesday', 'site-add-on-watchdog'), 583 3 => __('Wednesday', 'site-add-on-watchdog'), 584 4 => __('Thursday', 'site-add-on-watchdog'), 585 5 => __('Friday', 'site-add-on-watchdog'), 586 6 => __('Saturday', 'site-add-on-watchdog'), 587 7 => __('Sunday', 'site-add-on-watchdog'), 588 ]; 589 ?> 590 <div class="wp-watchdog-field"> 591 <label class="wp-watchdog-field__label" for="wp-watchdog-notification-frequency"><?php esc_html_e('Scan frequency', 'site-add-on-watchdog'); ?></label> 592 <div class="wp-watchdog-field__control"> 593 <select id="wp-watchdog-notification-frequency" name="settings[notifications][frequency]"> 594 <option value="daily" <?php selected($watchdogCurrentFrequency, 'daily'); ?>><?php esc_html_e('Daily', 'site-add-on-watchdog'); ?></option> 595 <option value="weekly" <?php selected($watchdogCurrentFrequency, 'weekly'); ?>><?php esc_html_e('Weekly', 'site-add-on-watchdog'); ?></option> 596 <option value="testing" <?php selected($watchdogCurrentFrequency, 'testing'); ?>> 597 <?php 598 printf( 599 /* translators: %d: interval minutes */ 600 esc_html__('Testing (every %d minutes)', 'site-add-on-watchdog'), 601 (int) TestingMode::INTERVAL_MINUTES 602 ); 603 ?> 604 </option> 605 <option value="manual" <?php selected($watchdogCurrentFrequency, 'manual'); ?>><?php esc_html_e('Manual (no automatic scans)', 'site-add-on-watchdog'); ?></option> 606 </select> 607 <p 608 class="description wp-watchdog-frequency-description<?php echo $watchdogIsTestingFrequency ? ' wp-watchdog-frequency-description--testing' : ''; ?>" 609 data-watchdog-frequency-description 610 data-default-message="<?php echo esc_attr($watchdogDefaultFrequencyMessage); ?>" 611 data-testing-message="<?php echo esc_attr($watchdogTestingFrequencyMessage); ?>" 612 aria-live="polite" 613 > 614 <?php echo esc_html($watchdogIsTestingFrequency ? $watchdogTestingFrequencyMessage : $watchdogDefaultFrequencyMessage); ?> 615 </p> 616 <?php if ($watchdogIsTestingFrequency && $watchdogTestingExpiresAt > $watchdogNow) : ?> 617 <p class="description"> 618 <?php 619 printf( 620 /* translators: 1: datetime, 2: relative time */ 621 esc_html__('Testing mode will automatically switch back to daily scans on %1$s (%2$s remaining).', 'site-add-on-watchdog'), 622 esc_html(wp_date($watchdogDateFormat, $watchdogTestingExpiresAt)), 623 esc_html(human_time_diff($watchdogNow, $watchdogTestingExpiresAt)) 624 ); 625 ?> 626 </p> 627 <?php endif; ?> 628 </div> 629 </div> 630 <div data-watchdog-frequency-options> 631 <div class="wp-watchdog-field" data-watchdog-frequency-target="daily"> 632 <label class="wp-watchdog-field__label" for="wp-watchdog-daily-time"><?php esc_html_e('Daily send time', 'site-add-on-watchdog'); ?></label> 633 <div class="wp-watchdog-field__control"> 634 <input 635 id="wp-watchdog-daily-time" 636 name="settings[notifications][daily_time]" 637 type="time" 638 value="<?php echo esc_attr($watchdogDailyTime); ?>" 639 aria-describedby="wp-watchdog-daily-time-help" 640 /> 641 <p class="description" id="wp-watchdog-daily-time-help"><?php esc_html_e('Time of day to start the daily scan in the WordPress site timezone.', 'site-add-on-watchdog'); ?></p> 642 </div> 643 </div> 644 <div class="wp-watchdog-field" data-watchdog-frequency-target="weekly"> 645 <label class="wp-watchdog-field__label" for="wp-watchdog-weekly-day"><?php esc_html_e('Weekly send day', 'site-add-on-watchdog'); ?></label> 646 <div class="wp-watchdog-field__control"> 647 <div class="wp-watchdog-field__inline"> 648 <select id="wp-watchdog-weekly-day" name="settings[notifications][weekly_day]"> 649 <?php foreach ($watchdogWeekdays as $watchdogWeekdayNumber => $watchdogWeekdayLabel) : ?> 650 <option value="<?php echo esc_attr((string) $watchdogWeekdayNumber); ?>" <?php selected($watchdogWeeklyDay, $watchdogWeekdayNumber); ?>><?php echo esc_html($watchdogWeekdayLabel); ?></option> 651 <?php endforeach; ?> 652 </select> 653 <label class="screen-reader-text" for="wp-watchdog-weekly-time"><?php esc_html_e('Weekly send time', 'site-add-on-watchdog'); ?></label> 654 <input 655 id="wp-watchdog-weekly-time" 656 name="settings[notifications][weekly_time]" 657 type="time" 658 value="<?php echo esc_attr($watchdogWeeklyTime); ?>" 659 aria-describedby="wp-watchdog-weekly-time-help" 660 /> 661 </div> 662 <p class="description" id="wp-watchdog-weekly-time-help"><?php esc_html_e('Day and time to start the weekly scan in the WordPress site timezone.', 'site-add-on-watchdog'); ?></p> 663 </div> 664 </div> 665 </div> 666 <div class="wp-watchdog-field"> 667 <label class="wp-watchdog-field__label" for="wp-watchdog-history-retention"><?php esc_html_e('History retention', 'site-add-on-watchdog'); ?></label> 668 <div class="wp-watchdog-field__control"> 669 <input 670 type="number" 671 id="wp-watchdog-history-retention" 672 class="small-text" 673 name="settings[history][retention]" 674 value="<?php echo esc_attr((string) ($watchdogSettings['history']['retention'] ?? $watchdogHistoryRetention)); ?>" 675 min="1" 676 max="15" 677 step="1" 678 /> 679 <p class="description"><?php esc_html_e('Number of recent scans to keep available for review and download (maximum 15).', 'site-add-on-watchdog'); ?></p> 680 </div> 681 </div> 682 </div> 683 684 <?php 685 $watchdogChannels = [ 686 'email' => [ 687 'icon' => 'dashicons-email-alt', 688 'test' => __('Save and test email', 'site-add-on-watchdog'), 689 'fields' => [ 690 'recipients' => ['text', __('Recipients (comma separated)', 'site-add-on-watchdog')], 691 ], 692 'notes' => [ 693 __('Separate addresses with commas, semicolons, or spaces. WordPress administrator accounts are always included.', 'site-add-on-watchdog'), 694 __('Email uses WordPress mail delivery. Your host must support sending mail, or you must configure an SMTP/mail plugin. Discord and other webhooks do not use this mail configuration.', 'site-add-on-watchdog'), 695 ], 696 ], 697 'discord' => [ 698 'icon' => 'dashicons-format-chat', 699 'test' => __('Save and test Discord', 'site-add-on-watchdog'), 700 'fields' => [ 701 'webhook' => ['url', __('Discord webhook URL', 'site-add-on-watchdog')], 702 ], 703 'notes' => [], 704 ], 705 'slack' => [ 706 'icon' => 'dashicons-admin-comments', 707 'test' => __('Save and test Slack', 'site-add-on-watchdog'), 708 'fields' => [ 709 'webhook' => ['url', __('Slack webhook URL', 'site-add-on-watchdog')], 710 ], 711 'notes' => [], 712 ], 713 'teams' => [ 714 'icon' => 'dashicons-groups', 715 'test' => __('Save and test Teams', 'site-add-on-watchdog'), 716 'fields' => [ 717 'webhook' => ['url', __('Teams webhook URL', 'site-add-on-watchdog')], 718 ], 719 'notes' => [ 720 __('Supports Microsoft Teams Workflows and existing Incoming Webhook connectors.', 'site-add-on-watchdog'), 721 ], 722 ], 723 'webhook' => [ 724 'icon' => 'dashicons-rest-api', 725 'test' => __('Save and test webhook', 'site-add-on-watchdog'), 726 'fields' => [ 727 'url' => ['url', __('Webhook URL', 'site-add-on-watchdog')], 728 'secret' => ['password', __('Webhook secret (optional)', 'site-add-on-watchdog')], 729 ], 730 'notes' => [ 731 __('Signs the exact JSON body with HMAC-SHA256 in the X-Watchdog-Signature header. Test messages are marked in the payload.', 'site-add-on-watchdog'), 732 ], 733 ], 734 ]; 735 ?> 736 <div class="wp-watchdog-card"> 737 <h2 class="wp-watchdog-card__title"><?php esc_html_e('Notification channels', 'site-add-on-watchdog'); ?></h2> 738 <p class="wp-watchdog-card__description"><?php esc_html_e('Save and test buttons save all settings on this form, then send a test to the selected channel.', 'site-add-on-watchdog'); ?></p> 739 <div class="wp-watchdog-channels"> 740 <?php foreach ($watchdogChannels as $watchdogChannelKey => $watchdogChannel) : ?> 741 <?php 742 $watchdogChannelSettings = is_array($watchdogNotifications[$watchdogChannelKey] ?? null) 743 ? $watchdogNotifications[$watchdogChannelKey] 744 : []; 745 $watchdogChannelName = 'settings[notifications][' . $watchdogChannelKey . ']'; 746 ?> 747 <div class="wp-watchdog-channel" data-watchdog-notification> 748 <div class="wp-watchdog-channel__header"> 749 <span class="wp-watchdog-channel__icon dashicons <?php echo esc_attr($watchdogChannel['icon']); ?>" aria-hidden="true"></span> 750 <h3 class="wp-watchdog-channel__title"><?php echo esc_html($watchdogChannelLabels[$watchdogChannelKey]); ?></h3> 751 <label class="wp-watchdog-switch"> 752 <input 753 type="checkbox" 754 name="<?php echo esc_attr($watchdogChannelName . '[enabled]'); ?>" 755 <?php checked(! empty($watchdogChannelSettings['enabled'])); ?> 756 data-watchdog-toggle 757 /> 758 <span 759 class="wp-watchdog-switch__state" 760 data-on="<?php esc_attr_e('On', 'site-add-on-watchdog'); ?>" 761 data-off="<?php esc_attr_e('Off', 'site-add-on-watchdog'); ?>" 762 aria-hidden="true" 763 ></span> 764 <span class="screen-reader-text"> 765 <?php 766 printf( 767 /* translators: %s: notification channel name. */ 768 esc_html__('Enable %s notifications', 'site-add-on-watchdog'), 769 esc_html($watchdogChannelLabels[$watchdogChannelKey]) 770 ); 771 ?> 772 </span> 773 </label> 774 </div> 775 <div class="wp-watchdog-channel__body" data-watchdog-fields> 776 <?php foreach ($watchdogChannel['fields'] as $watchdogFieldKey => [$watchdogFieldType, $watchdogFieldLabel]) : ?> 777 <?php $watchdogFieldId = 'wp-watchdog-' . $watchdogChannelKey . '-' . $watchdogFieldKey; ?> 778 <p class="wp-watchdog-channel__field"> 779 <label for="<?php echo esc_attr($watchdogFieldId); ?>"><?php echo esc_html($watchdogFieldLabel); ?></label> 780 <input 781 type="<?php echo esc_attr($watchdogFieldType); ?>" 782 id="<?php echo esc_attr($watchdogFieldId); ?>" 783 class="regular-text" 784 name="<?php echo esc_attr($watchdogChannelName . '[' . $watchdogFieldKey . ']'); ?>" 785 value="<?php echo esc_attr((string) ($watchdogChannelSettings[$watchdogFieldKey] ?? '')); ?>" 786 <?php echo $watchdogFieldType === 'password' ? 'autocomplete="new-password"' : ''; ?> 787 /> 788 </p> 789 <?php endforeach; ?> 790 <?php foreach ($watchdogChannel['notes'] as $watchdogChannelNote) : ?> 791 <p class="description"><?php echo esc_html($watchdogChannelNote); ?></p> 792 <?php endforeach; ?> 793 <p class="wp-watchdog-channel__actions"> 794 <button class="button" type="submit" name="test_channel" value="<?php echo esc_attr($watchdogChannelKey); ?>"><?php echo esc_html($watchdogChannel['test']); ?></button> 795 </p> 796 </div> 797 </div> 798 <?php endforeach; ?> 799 </div> 800 </div> 801 802 <div class="wp-watchdog-card"> 803 <h2 class="wp-watchdog-card__title"><?php esc_html_e('Vulnerability data', 'site-add-on-watchdog'); ?></h2> 804 <div class="wp-watchdog-field"> 805 <label class="wp-watchdog-field__label" for="wp-watchdog-wpscan-api-key"><?php esc_html_e('WPScan API key', 'site-add-on-watchdog'); ?></label> 806 <div class="wp-watchdog-field__control"> 807 <input 808 type="password" 809 id="wp-watchdog-wpscan-api-key" 810 class="regular-text" 811 name="settings[notifications][wpscan_api_key]" 812 value="<?php echo esc_attr((string) ($watchdogNotifications['wpscan_api_key'] ?? '')); ?>" 813 autocomplete="new-password" 814 /> 815 <p class="description"><?php esc_html_e('Optional. Provide your own WPScan API key to enrich vulnerability reports.', 'site-add-on-watchdog'); ?></p> 816 </div> 817 </div> 818 </div> 819 820 <p class="wp-watchdog-settings-form__submit"> 821 <button class="button button-primary" type="submit"><?php esc_html_e('Save settings', 'site-add-on-watchdog'); ?></button> 822 </p> 823 </form> 824 </section> 825 826 <section class="wp-watchdog-panel" id="watchdog-delivery" data-watchdog-panel> 827 <div class="wp-watchdog-card"> 828 <div class="wp-watchdog-card__header"> 829 <h2 class="wp-watchdog-card__title"><?php esc_html_e('Scheduler', 'site-add-on-watchdog'); ?></h2> 830 <?php if ($watchdogIsCronDisabled) : ?> 831 <span class="wp-watchdog-badge wp-watchdog-badge--warn"><?php esc_html_e('WP-Cron disabled', 'site-add-on-watchdog'); ?></span> 832 <?php else : ?> 833 <span class="wp-watchdog-badge wp-watchdog-badge--ok"><?php esc_html_e('WP-Cron enabled', 'site-add-on-watchdog'); ?></span> 834 <?php endif; ?> 835 </div> 239 836 <?php if ($watchdogIsCronDisabled) : ?> 240 <p class="wp-watchdog-muted"><?php esc_html_e('WP-Cron appears disabled. Use a real cron job or the server endpoint below to keep scans running.', 'site-add-on-watchdog'); ?></p> 241 <form class="wp-watchdog-block-action" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> 242 <?php wp_nonce_field($watchdogActionPrefix . '_send_notifications'); ?> 243 <input type="hidden" name="action" value="<?php echo esc_attr($watchdogActionPrefix . '_send_notifications'); ?>"> 244 <input type="hidden" name="force" value="1" /> 245 <input type="hidden" name="ignore_throttle" value="1" /> 246 <button class="button button-primary" type="submit"> 247 <span class="dashicons dashicons-controls-repeat" aria-hidden="true"></span> 248 <?php esc_html_e('Retry notifications', 'site-add-on-watchdog'); ?> 249 </button> 250 </form> 837 <p class="wp-watchdog-card__description"><?php esc_html_e('WP-Cron appears disabled. Use a real cron job or the server endpoint below to keep scans running.', 'site-add-on-watchdog'); ?></p> 251 838 <?php else : ?> 252 <p class="wp-watchdog- muted"><?php esc_html_e('WP-Cron is enabled. Scheduled tasks depend on site traffic or a server cron job.', 'site-add-on-watchdog'); ?></p>839 <p class="wp-watchdog-card__description"><?php esc_html_e('WP-Cron is enabled. Scheduled tasks depend on site traffic or a server cron job.', 'site-add-on-watchdog'); ?></p> 253 840 <?php endif; ?> 254 <div class="wp-watchdog-divider"></div> 255 <p class="wp-watchdog-muted wp-watchdog-endpoint"> 256 <strong><?php esc_html_e('Server cron endpoint:', 'site-add-on-watchdog'); ?></strong><br /> 257 <code><?php echo esc_html($watchdogCronEndpoint); ?></code> 258 </p> 259 <?php if ($watchdogCronSecretPersisted) : ?> 260 <p class="wp-watchdog-muted wp-watchdog-endpoint"> 261 <strong><?php esc_html_e('Generated Cron secret:', 'site-add-on-watchdog'); ?></strong><br /> 262 <code><?php echo esc_html($watchdogCronSecret); ?></code> 263 </p> 264 <p class="wp-watchdog-muted wp-watchdog-endpoint"> 265 <strong><?php esc_html_e('Authenticated command:', 'site-add-on-watchdog'); ?></strong><br /> 266 <code><?php echo esc_html(sprintf('curl -X POST -H "X-Watchdog-Cron-Key: %s" "%s"', $watchdogCronSecret, $watchdogCronEndpoint)); ?></code> 267 </p> 268 <p class="wp-watchdog-muted"><?php esc_html_e('Use POST and keep the generated secret private. The header-authenticated request can trigger scans or notification retries even when wp-cron is disabled.', 'site-add-on-watchdog'); ?></p> 269 <?php endif; ?> 270 <div class="wp-watchdog-divider"></div> 271 <div> 272 <p class="wp-watchdog-muted wp-watchdog-subheading"><strong><?php esc_html_e('Queue status', 'site-add-on-watchdog'); ?></strong></p> 273 <?php 274 $watchdogQueueNextAttempt = $watchdogQueueStatus['next_attempt_at'] ?? null; 275 ?> 276 <p class="wp-watchdog-muted"> 277 <?php 278 printf( 279 /* translators: %d: number of queued jobs. */ 280 esc_html__('Queued jobs: %d', 'site-add-on-watchdog'), 281 esc_html(number_format_i18n($watchdogQueueLength)) 282 ); 283 ?> 284 </p> 841 842 <details class="wp-watchdog-details"> 843 <summary><?php esc_html_e('Trigger scans from a server cron job', 'site-add-on-watchdog'); ?></summary> 844 <div class="wp-watchdog-details__body"> 845 <div class="wp-watchdog-copy"> 846 <span class="wp-watchdog-copy__label"><?php esc_html_e('Server cron endpoint:', 'site-add-on-watchdog'); ?></span> 847 <code id="wp-watchdog-cron-endpoint"><?php echo esc_html($watchdogCronEndpoint); ?></code> 848 <button type="button" class="button button-small" data-watchdog-copy="wp-watchdog-cron-endpoint" hidden><?php esc_html_e('Copy', 'site-add-on-watchdog'); ?></button> 849 </div> 850 <?php if ($watchdogCronSecretPersisted) : ?> 851 <div class="wp-watchdog-copy"> 852 <span class="wp-watchdog-copy__label"><?php esc_html_e('Generated Cron secret:', 'site-add-on-watchdog'); ?></span> 853 <code id="wp-watchdog-cron-secret"><?php echo esc_html($watchdogCronSecret); ?></code> 854 <button type="button" class="button button-small" data-watchdog-copy="wp-watchdog-cron-secret" hidden><?php esc_html_e('Copy', 'site-add-on-watchdog'); ?></button> 855 </div> 856 <div class="wp-watchdog-copy"> 857 <span class="wp-watchdog-copy__label"><?php esc_html_e('Authenticated command:', 'site-add-on-watchdog'); ?></span> 858 <code id="wp-watchdog-cron-command"><?php echo esc_html(sprintf('curl -X POST -H "X-Watchdog-Cron-Key: %s" "%s"', $watchdogCronSecret, $watchdogCronEndpoint)); ?></code> 859 <button type="button" class="button button-small" data-watchdog-copy="wp-watchdog-cron-command" hidden><?php esc_html_e('Copy', 'site-add-on-watchdog'); ?></button> 860 </div> 861 <p class="description"><?php esc_html_e('Use POST and keep the generated secret private. The header-authenticated request can trigger scans or notification retries even when wp-cron is disabled.', 'site-add-on-watchdog'); ?></p> 862 <?php endif; ?> 863 </div> 864 </details> 865 </div> 866 867 <div class="wp-watchdog-card-grid"> 868 <div class="wp-watchdog-card"> 869 <div class="wp-watchdog-card__header"> 870 <h2 class="wp-watchdog-card__title"><?php esc_html_e('Queue status', 'site-add-on-watchdog'); ?></h2> 871 <span class="wp-watchdog-badge <?php echo $watchdogQueueLength > 0 ? 'wp-watchdog-badge--warn' : 'wp-watchdog-badge--ok'; ?>"> 872 <?php 873 printf( 874 /* translators: %s: number of queued jobs. */ 875 esc_html__('Queued jobs: %s', 'site-add-on-watchdog'), 876 esc_html(number_format_i18n($watchdogQueueLength)) 877 ); 878 ?> 879 </span> 880 </div> 285 881 <?php if ($watchdogQueueLength > 0 && $watchdogQueueNextAttempt) : ?> 286 <p class="wp-watchdog- muted">882 <p class="wp-watchdog-card__description"> 287 883 <?php 288 884 printf( 289 885 /* translators: 1: scheduled time */ 290 886 esc_html__('Next attempt: %1$s', 'site-add-on-watchdog'), 291 esc_html(wp_date( get_option('date_format') . ' ' . get_option('time_format'), (int) $watchdogQueueNextAttempt))887 esc_html(wp_date($watchdogDateFormat, (int) $watchdogQueueNextAttempt)) 292 888 ); 293 889 ?> 294 890 </p> 295 891 <?php else : ?> 296 <p class="wp-watchdog- muted"><?php esc_html_e('No pending notifications in the queue.', 'site-add-on-watchdog'); ?></p>892 <p class="wp-watchdog-card__description"><?php esc_html_e('No pending notifications in the queue.', 'site-add-on-watchdog'); ?></p> 297 893 <?php endif; ?> 298 894 </div> 299 <div class="wp-watchdog-divider"></div> 300 <div> 301 <p class="wp-watchdog-muted wp-watchdog-subheading"><strong><?php esc_html_e('Captured notification payloads', 'site-add-on-watchdog'); ?></strong></p> 302 <?php if (! empty($watchdogLastFailedNotification)) : ?> 895 896 <div class="wp-watchdog-card"> 897 <div class="wp-watchdog-card__header"> 898 <h2 class="wp-watchdog-card__title"><?php esc_html_e('Captured notification payloads', 'site-add-on-watchdog'); ?></h2> 899 <?php if ($watchdogHasFailedNotification) : ?> 900 <span class="wp-watchdog-badge wp-watchdog-badge--danger"><?php esc_html_e('Delivery failed', 'site-add-on-watchdog'); ?></span> 901 <?php endif; ?> 902 </div> 903 <?php if ($watchdogHasFailedNotification) : ?> 303 904 <?php 304 905 $watchdogFailedTime = isset($watchdogLastFailedNotification['failed_at']) 305 906 ? (int) $watchdogLastFailedNotification['failed_at'] 306 : time();907 : $watchdogNow; 307 908 $watchdogFailedChannel = $watchdogLastFailedNotification['description'] 308 909 ?: ($watchdogLastFailedNotification['channel'] ?? __('Unknown channel', 'site-add-on-watchdog')); 309 $watchdogFailedError = $watchdogLastFailedNotification['last_error'] ?? '';910 $watchdogFailedError = (string) ($watchdogLastFailedNotification['last_error'] ?? ''); 310 911 ?> 311 <p class="wp-watchdog- muted">912 <p class="wp-watchdog-card__description"> 312 913 <?php 313 914 printf( 314 915 /* translators: 1: human readable time, 2: channel name */ 315 916 esc_html__('Last failure recorded %1$s via %2$s.', 'site-add-on-watchdog'), 316 esc_html(wp_date( get_option('date_format') . ' ' . get_option('time_format'), $watchdogFailedTime)),917 esc_html(wp_date($watchdogDateFormat, $watchdogFailedTime)), 317 918 esc_html($watchdogFailedChannel) 318 919 ); 319 if ($watchdogFailedError !== '') {320 echo '<br />' . esc_html($watchdogFailedError);321 }322 920 ?> 323 921 </p> 922 <?php if ($watchdogFailedError !== '') : ?> 923 <p class="wp-watchdog-error-text"><?php echo esc_html($watchdogFailedError); ?></p> 924 <?php endif; ?> 324 925 <div class="wp-watchdog-actions"> 325 <form method="post" action="<?php echo esc_url( admin_url('admin-post.php')); ?>">926 <form method="post" action="<?php echo esc_url($watchdogPostUrl); ?>"> 326 927 <?php wp_nonce_field($watchdogActionPrefix . '_resend_failed_notification'); ?> 327 928 <input type="hidden" name="action" value="<?php echo esc_attr($watchdogActionPrefix . '_resend_failed_notification'); ?>" /> … … 331 932 </button> 332 933 </form> 333 <form method="post" action="<?php echo esc_url( admin_url('admin-post.php')); ?>">934 <form method="post" action="<?php echo esc_url($watchdogPostUrl); ?>" data-watchdog-download> 334 935 <?php wp_nonce_field($watchdogActionPrefix . '_download_failed_notification'); ?> 335 936 <input type="hidden" name="action" value="<?php echo esc_attr($watchdogActionPrefix . '_download_failed_notification'); ?>" /> … … 341 942 </div> 342 943 <?php else : ?> 343 <p class="wp-watchdog- muted"><?php esc_html_e('No failed notification payload has been captured yet.', 'site-add-on-watchdog'); ?></p>944 <p class="wp-watchdog-card__description"><?php esc_html_e('No failed notification payload has been captured yet.', 'site-add-on-watchdog'); ?></p> 344 945 <?php endif; ?> 345 946 </div> 346 947 </div> 347 </div> 348 349 <div class="wp-watchdog-grid"> 350 <div class="wp-watchdog-surface" id="watchdog-history"> 351 <div class="wp-watchdog-section-title"> 352 <span class="dashicons dashicons-list-view" aria-hidden="true"></span> 353 <?php esc_html_e('Scan History', 'site-add-on-watchdog'); ?> 354 </div> 355 <?php require __DIR__ . '/history.php'; ?> 356 </div> 357 <div class="wp-watchdog-surface" id="watchdog-channels"> 358 <div class="wp-watchdog-section-title"> 359 <span class="dashicons dashicons-format-status" aria-hidden="true"></span> 360 <?php esc_html_e('Notification channels', 'site-add-on-watchdog'); ?> 361 </div> 362 <?php 363 $watchdogChannels = [ 364 'email' => __('Email', 'site-add-on-watchdog'), 365 'slack' => __('Slack', 'site-add-on-watchdog'), 366 'teams' => __('Microsoft Teams', 'site-add-on-watchdog'), 367 'discord' => __('Discord', 'site-add-on-watchdog'), 368 'webhook' => __('Custom Webhook', 'site-add-on-watchdog'), 369 ]; 370 ?> 371 <ul class="wp-watchdog-inline-list"> 372 <?php foreach ($watchdogChannels as $watchdogChannelKey => $watchdogChannelLabel) : ?> 373 <?php $watchdogChannelEnabled = ! empty($watchdogSettings['notifications'][$watchdogChannelKey]['enabled']); ?> 374 <?php $watchdogBadgeClass = $watchdogChannelEnabled ? 'wp-watchdog-badge--success' : 'wp-watchdog-badge--muted'; ?> 375 <li> 376 <span class="wp-watchdog-badge <?php echo esc_attr($watchdogBadgeClass); ?>"> 377 <span class="dashicons <?php echo $watchdogChannelEnabled ? 'dashicons-yes-alt' : 'dashicons-dismiss'; ?>" aria-hidden="true"></span> 378 <?php echo esc_html($watchdogChannelLabel); ?> 379 <span aria-hidden="true">•</span> 380 <?php echo $watchdogChannelEnabled ? esc_html__('On', 'site-add-on-watchdog') : esc_html__('Off', 'site-add-on-watchdog'); ?> 381 </span> 382 </li> 383 <?php endforeach; ?> 384 </ul> 385 <p class="wp-watchdog-muted wp-watchdog-channel-summary"> 386 <?php esc_html_e('Keep channels enabled to receive instant alerts and download-ready reports.', 'site-add-on-watchdog'); ?> 387 </p> 388 </div> 389 </div> 390 391 <div class="wp-watchdog-surface" id="watchdog-risks"> 392 <div class="wp-watchdog-section-header"> 393 <div class="wp-watchdog-section-title"> 394 <span class="dashicons dashicons-shield-alt" aria-hidden="true"></span> 395 <?php esc_html_e('Potential Risks', 'site-add-on-watchdog'); ?> 396 </div> 397 <div class="wp-watchdog-summary"> 398 <span class="wp-watchdog-summary__count"><?php echo esc_html(number_format_i18n(count($watchdogRisks))); ?></span> 399 <span class="wp-watchdog-summary__label"><?php esc_html_e('items flagged', 'site-add-on-watchdog'); ?></span> 400 </div> 401 </div> 402 <?php if (empty($watchdogRisks)) : ?> 403 <p class="wp-watchdog-muted"><?php esc_html_e('No risks detected.', 'site-add-on-watchdog'); ?></p> 404 <?php else : ?> 405 <?php 406 $watchdogColumns = [ 407 'plugin' => __('Plugin', 'site-add-on-watchdog'), 408 'local' => __('Local Version', 'site-add-on-watchdog'), 409 'remote' => __('Directory Version', 'site-add-on-watchdog'), 410 'reasons' => __('Reasons', 'site-add-on-watchdog'), 411 'actions' => __('Actions', 'site-add-on-watchdog'), 412 ]; 413 $watchdogPerPage = (int) apply_filters('site_add_on_watchdog_admin_risks_per_page', 10); 414 $watchdogNormalizeForSort = static function (string $watchdogValue): string { 415 $watchdogNormalized = function_exists('remove_accents') ? remove_accents($watchdogValue) : $watchdogValue; 416 417 return strtolower($watchdogNormalized); 418 }; 419 $watchdogRiskCount = static function (Risk $watchdogRisk): int { 420 $watchdogCount = count($watchdogRisk->reasons); 421 $watchdogVulnerabilities = $watchdogRisk->details['vulnerabilities'] ?? []; 422 if (! is_array($watchdogVulnerabilities)) { 423 $watchdogVulnerabilities = []; 424 } 425 if ($watchdogVulnerabilities !== []) { 426 $watchdogCount += count($watchdogVulnerabilities); 427 } 428 429 return $watchdogCount; 430 }; 431 $watchdogVersionScore = static function (string $watchdogVersion): int { 432 preg_match_all('/\d+/', $watchdogVersion, $watchdogMatches); 433 $watchdogNumbers = $watchdogMatches[0] ?? []; 434 if (empty($watchdogNumbers)) { 435 return 0; 436 } 437 438 $watchdogWeights = [100000000, 100000, 100, 1]; 439 $watchdogScore = 0; 440 foreach ($watchdogWeights as $watchdogIndex => $watchdogWeight) { 441 if (! isset($watchdogNumbers[$watchdogIndex])) { 442 continue; 443 } 444 $watchdogScore += ((int) $watchdogNumbers[$watchdogIndex]) * $watchdogWeight; 445 } 446 447 return $watchdogScore; 448 }; 449 ?> 450 <div class="wp-watchdog-risk-table" data-wp-watchdog-table data-per-page="<?php echo esc_attr(max($watchdogPerPage, 1)); ?>"> 451 <div class="wp-watchdog-risk-table__controls"> 452 <form method="get" class="wp-watchdog-risk-table__filter-form"> 453 <input type="hidden" name="page" value="site-add-on-watchdog" /> 454 <label class="wp-watchdog-risk-table__search"> 455 <span class="screen-reader-text"><?php esc_html_e('Search risks', 'site-add-on-watchdog'); ?></span> 456 <input 457 type="search" 458 name="risk_search" 459 value="<?php echo esc_attr($watchdogRiskSearch); ?>" 460 placeholder="<?php esc_attr_e('Search plugin or reason', 'site-add-on-watchdog'); ?>" 461 data-risk-search 462 /> 463 </label> 464 <div class="wp-watchdog-risk-table__sort-select"> 465 <label class="screen-reader-text" for="wp-watchdog-risk-sort"><?php esc_html_e('Sort risks by', 'site-add-on-watchdog'); ?></label> 466 <select id="wp-watchdog-risk-sort" name="risk_sort" data-risk-sort> 467 <option 468 value="plugin" 469 data-sort-key="sortPlugin" 470 data-sort-default="asc" 471 <?php selected($watchdogRiskSortSelection, 'plugin'); ?> 472 > 473 <?php esc_html_e('Plugin name', 'site-add-on-watchdog'); ?> 474 </option> 475 <option 476 value="risk_count" 477 data-sort-key="sortRiskCount" 478 data-sort-default="desc" 479 <?php selected($watchdogRiskSortSelection, 'risk_count'); ?> 480 > 481 <?php esc_html_e('Risk count', 'site-add-on-watchdog'); ?> 482 </option> 483 <option 484 value="version_gap" 485 data-sort-key="sortVersionGap" 486 data-sort-default="desc" 487 <?php selected($watchdogRiskSortSelection, 'version_gap'); ?> 488 > 489 <?php esc_html_e('Version gap', 'site-add-on-watchdog'); ?> 490 </option> 491 <option 492 value="local" 493 data-sort-key="sortLocal" 494 data-sort-default="desc" 495 <?php selected($watchdogRiskSortSelection, 'local'); ?> 496 > 497 <?php esc_html_e('Local version', 'site-add-on-watchdog'); ?> 498 </option> 499 <option 500 value="remote" 501 data-sort-key="sortRemote" 502 data-sort-default="desc" 503 <?php selected($watchdogRiskSortSelection, 'remote'); ?> 504 > 505 <?php esc_html_e('Directory version', 'site-add-on-watchdog'); ?> 506 </option> 507 <option 508 value="reasons" 509 data-sort-key="sortReasons" 510 data-sort-default="asc" 511 <?php selected($watchdogRiskSortSelection, 'reasons'); ?> 512 > 513 <?php esc_html_e('Reasons', 'site-add-on-watchdog'); ?> 514 </option> 515 </select> 516 <label class="screen-reader-text" for="wp-watchdog-risk-order"><?php esc_html_e('Sort order', 'site-add-on-watchdog'); ?></label> 517 <select id="wp-watchdog-risk-order" name="risk_order" data-risk-order> 518 <option value="asc" <?php selected($watchdogRiskOrderSelection, 'asc'); ?>> 519 <?php esc_html_e('Ascending', 'site-add-on-watchdog'); ?> 520 </option> 521 <option value="desc" <?php selected($watchdogRiskOrderSelection, 'desc'); ?>> 522 <?php esc_html_e('Descending', 'site-add-on-watchdog'); ?> 523 </option> 524 </select> 525 <button class="button" type="submit"><?php esc_html_e('Apply', 'site-add-on-watchdog'); ?></button> 526 </div> 527 </form> 528 <div class="wp-watchdog-risk-table__pagination" data-pagination> 529 <button type="button" class="button" data-action="prev" aria-label="<?php esc_attr_e('Previous page', 'site-add-on-watchdog'); ?>" disabled>‹</button> 530 <span class="wp-watchdog-risk-table__page-status" data-page-status></span> 531 <button type="button" class="button" data-action="next" aria-label="<?php esc_attr_e('Next page', 'site-add-on-watchdog'); ?>" disabled>›</button> 532 </div> 533 </div> 534 <div class="wp-watchdog-risk-table__scroll"> 535 <table class="widefat fixed striped wp-list-table"> 536 <thead> 537 <tr> 538 <th scope="col"> 539 <button type="button" class="wp-watchdog-risk-table__sort" data-sort-key="sortPlugin" data-sort-default="asc" data-sort-initial aria-sort="ascending"> 540 <?php echo esc_html($watchdogColumns['plugin']); ?> 541 <span class="wp-watchdog-risk-table__sort-indicator" aria-hidden="true"></span> 542 </button> 543 </th> 544 <th scope="col"> 545 <button type="button" class="wp-watchdog-risk-table__sort" data-sort-key="sortLocal" data-sort-default="desc" aria-sort="none"> 546 <?php echo esc_html($watchdogColumns['local']); ?> 547 <span class="wp-watchdog-risk-table__sort-indicator" aria-hidden="true"></span> 548 </button> 549 </th> 550 <th scope="col"> 551 <button type="button" class="wp-watchdog-risk-table__sort" data-sort-key="sortRemote" data-sort-default="desc" aria-sort="none"> 552 <?php echo esc_html($watchdogColumns['remote']); ?> 553 <span class="wp-watchdog-risk-table__sort-indicator" aria-hidden="true"></span> 554 </button> 555 </th> 556 <th scope="col"> 557 <button type="button" class="wp-watchdog-risk-table__sort" data-sort-key="sortReasons" data-sort-default="asc" aria-sort="none"> 558 <?php echo esc_html($watchdogColumns['reasons']); ?> 559 <span class="wp-watchdog-risk-table__sort-indicator" aria-hidden="true"></span> 560 </button> 561 </th> 562 <th scope="col"><?php echo esc_html($watchdogColumns['actions']); ?></th> 563 </tr> 564 </thead> 565 <tbody> 566 <?php foreach ($watchdogRisks as $watchdogRisk) : ?> 567 <?php 568 $watchdogRemoteVersion = $watchdogRisk->remoteVersion ?? __('N/A', 'site-add-on-watchdog'); 569 $watchdogRemoteSort = is_string($watchdogRisk->remoteVersion) ? $watchdogNormalizeForSort($watchdogRisk->remoteVersion) : ''; 570 $watchdogReasonParts = $watchdogRisk->reasons; 571 $watchdogVulnerabilities = $watchdogRisk->details['vulnerabilities'] ?? []; 572 if (! is_array($watchdogVulnerabilities)) { 573 $watchdogVulnerabilities = []; 574 } 575 foreach ($watchdogVulnerabilities as $watchdogVulnerability) { 576 if (! empty($watchdogVulnerability['severity_label'])) { 577 $watchdogReasonParts[] = $watchdogVulnerability['severity_label']; 578 } 579 if (! empty($watchdogVulnerability['title'])) { 580 $watchdogReasonParts[] = $watchdogVulnerability['title']; 581 } 582 if (! empty($watchdogVulnerability['cve'])) { 583 $watchdogReasonParts[] = $watchdogVulnerability['cve']; 584 } 585 } 586 $watchdogReasonSort = $watchdogNormalizeForSort(implode(' ', $watchdogReasonParts)); 587 $watchdogRiskCountValue = $watchdogRiskCount($watchdogRisk); 588 $watchdogVersionGapValue = abs( 589 $watchdogVersionScore($watchdogRemoteVersion) - $watchdogVersionScore($watchdogRisk->localVersion) 590 ); 591 $watchdogFilterText = $watchdogNormalizeForSort(implode(' ', [ 592 $watchdogRisk->pluginName, 593 $watchdogRisk->localVersion, 594 $watchdogRemoteVersion, 595 implode(' ', $watchdogReasonParts), 596 ])); 597 ?> 598 <tr 599 data-sort-plugin="<?php echo esc_attr($watchdogNormalizeForSort($watchdogRisk->pluginName)); ?>" 600 data-sort-local="<?php echo esc_attr($watchdogNormalizeForSort($watchdogRisk->localVersion)); ?>" 601 data-sort-remote="<?php echo esc_attr($watchdogRemoteSort); ?>" 602 data-sort-reasons="<?php echo esc_attr($watchdogReasonSort); ?>" 603 data-sort-risk-count="<?php echo esc_attr((string) $watchdogRiskCountValue); ?>" 604 data-sort-version-gap="<?php echo esc_attr((string) $watchdogVersionGapValue); ?>" 605 data-filter-text="<?php echo esc_attr($watchdogFilterText); ?>" 606 > 607 <td class="column-primary" data-column="plugin" data-column-label="<?php echo esc_attr($watchdogColumns['plugin']); ?>"> 608 <?php echo esc_html($watchdogRisk->pluginName); ?> 609 </td> 610 <td data-column="local" data-column-label="<?php echo esc_attr($watchdogColumns['local']); ?>"> 611 <?php echo esc_html($watchdogRisk->localVersion); ?> 612 </td> 613 <td data-column="remote" data-column-label="<?php echo esc_attr($watchdogColumns['remote']); ?>"> 614 <?php echo esc_html($watchdogRemoteVersion); ?> 615 </td> 616 <td data-column="reasons" data-column-label="<?php echo esc_attr($watchdogColumns['reasons']); ?>"> 617 <ul> 618 <?php foreach ($watchdogRisk->reasons as $watchdogReason) : ?> 619 <li><?php echo esc_html($watchdogReason); ?></li> 620 <?php endforeach; ?> 621 <?php if (! empty($watchdogVulnerabilities)) : ?> 622 <li> 623 <?php esc_html_e('WPScan vulnerabilities:', 'site-add-on-watchdog'); ?> 624 <ul> 625 <?php foreach ($watchdogVulnerabilities as $watchdogVuln) : ?> 626 <li> 627 <?php if (! empty($watchdogVuln['severity']) && ! empty($watchdogVuln['severity_label'])) : ?> 628 <?php $watchdogSeverityClass = 'wp-watchdog-severity wp-watchdog-severity--' . sanitize_html_class((string) $watchdogVuln['severity']); ?> 629 <span class="<?php echo esc_attr($watchdogSeverityClass); ?>"><?php echo esc_html($watchdogVuln['severity_label']); ?></span> 630 <?php endif; ?> 631 <?php if (! empty($watchdogVuln['title'])) : ?> 632 <span class="wp-watchdog-vulnerability__title"><?php echo esc_html($watchdogVuln['title']); ?></span> 633 <?php endif; ?> 634 <?php if (! empty($watchdogVuln['cve'])) : ?> 635 <span class="wp-watchdog-vulnerability__cve">- <?php echo esc_html($watchdogVuln['cve']); ?></span> 636 <?php endif; ?> 637 <?php if (! empty($watchdogVuln['fixed_in'])) : ?> 638 <span class="wp-watchdog-vulnerability__fixed">(<?php 639 printf( 640 /* translators: %s: plugin version number. */ 641 esc_html__('Fixed in %s', 'site-add-on-watchdog'), 642 esc_html($watchdogVuln['fixed_in']) 643 ); 644 ?>)</span> 645 <?php endif; ?> 646 </li> 647 <?php endforeach; ?> 648 </ul> 649 </li> 650 <?php endif; ?> 651 </ul> 652 </td> 653 <td data-column="actions" data-column-label="<?php echo esc_attr($watchdogColumns['actions']); ?>"> 654 <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> 655 <?php wp_nonce_field($watchdogActionPrefix . '_ignore'); ?> 656 <input type="hidden" name="action" value="<?php echo esc_attr($watchdogActionPrefix . '_ignore'); ?>"> 657 <input type="hidden" name="plugin_slug" value="<?php echo esc_attr($watchdogRisk->pluginSlug); ?>"> 658 <button class="button" type="submit"><?php esc_html_e('Ignore', 'site-add-on-watchdog'); ?></button> 659 </form> 660 </td> 661 </tr> 662 <?php endforeach; ?> 663 </tbody> 664 </table> 665 </div> 666 </div> 667 <?php endif; ?> 668 </div> 669 670 <div class="wp-watchdog-grid"> 671 <div class="wp-watchdog-surface" id="watchdog-ignored"> 672 <div class="wp-watchdog-section-title"> 673 <span class="dashicons dashicons-hidden" aria-hidden="true"></span> 674 <?php esc_html_e('Ignored Plugins', 'site-add-on-watchdog'); ?> 675 </div> 676 <?php if (empty($watchdogIgnored)) : ?> 677 <p class="wp-watchdog-muted"><?php esc_html_e('No plugins are being ignored.', 'site-add-on-watchdog'); ?></p> 678 <?php else : ?> 679 <ul class="wp-watchdog-inline-list"> 680 <?php foreach ($watchdogIgnored as $watchdogIgnoredSlug) : ?> 681 <li> 682 <form class="wp-watchdog-inline-form" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> 683 <?php wp_nonce_field($watchdogActionPrefix . '_unignore'); ?> 684 <input type="hidden" name="action" value="<?php echo esc_attr($watchdogActionPrefix . '_unignore'); ?>"> 685 <input type="hidden" name="plugin_slug" value="<?php echo esc_attr($watchdogIgnoredSlug); ?>"> 686 <button class="button" type="submit"> 687 <span class="dashicons dashicons-no" aria-hidden="true"></span> 688 <?php echo esc_html($watchdogIgnoredSlug); ?> 689 </button> 690 </form> 691 </li> 692 <?php endforeach; ?> 693 </ul> 694 <?php endif; ?> 695 </div> 696 <div class="wp-watchdog-surface wp-watchdog-surface--wide" id="watchdog-settings"> 697 <div class="wp-watchdog-section-title"> 698 <span class="dashicons dashicons-email-alt" aria-hidden="true"></span> 699 <?php esc_html_e('Settings and notifications', 'site-add-on-watchdog'); ?> 700 </div> 701 <p class="description"><?php esc_html_e('Save and test buttons save all settings on this form, then send a test to the selected channel.', 'site-add-on-watchdog'); ?></p> 702 <form class="wp-watchdog-settings-form" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"> 703 <?php wp_nonce_field($watchdogActionPrefix . '_settings'); ?> 704 <input type="hidden" name="action" value="<?php echo esc_attr($watchdogActionPrefix . '_save_settings'); ?>"> 705 <table class="form-table" role="presentation"> 706 <tr> 707 <th scope="row"><?php esc_html_e('History retention', 'site-add-on-watchdog'); ?></th> 708 <td> 709 <label for="wp-watchdog-history-retention" class="screen-reader-text"><?php esc_html_e('History retention', 'site-add-on-watchdog'); ?></label> 710 <input 711 type="number" 712 id="wp-watchdog-history-retention" 713 name="settings[history][retention]" 714 value="<?php echo esc_attr($watchdogSettings['history']['retention'] ?? $watchdogHistoryRetention); ?>" 715 min="1" 716 max="15" 717 step="1" 718 /> 719 <p class="description"> 720 <?php esc_html_e('Number of recent scans to keep available for review and download (maximum 15).', 'site-add-on-watchdog'); ?> 721 </p> 722 </td> 723 </tr> 724 <tr> 725 <th scope="row"><?php esc_html_e('Scan frequency', 'site-add-on-watchdog'); ?></th> 726 <td> 727 <?php 728 $watchdogDefaultFrequencyMessage = __('Choose how often the automatic scan should run.', 'site-add-on-watchdog'); 729 $watchdogTestingFrequencyMessage = sprintf( 730 /* translators: 1: interval minutes, 2: duration hours */ 731 __('Testing mode sends notifications every %1$d minutes to all configured channels and automatically switches back to daily scans after %2$d hours.', 'site-add-on-watchdog'), 732 TestingMode::INTERVAL_MINUTES, 733 TestingMode::DURATION_HOURS 734 ); 735 $watchdogIsTestingFrequency = ($watchdogSettings['notifications']['frequency'] ?? '') === 'testing'; 736 $watchdogTestingExpiresAt = (int) ($watchdogSettings['notifications']['testing_expires_at'] ?? 0); 737 $watchdogNow = time(); 738 $watchdogShowTestingExpiry = $watchdogIsTestingFrequency && $watchdogTestingExpiresAt > $watchdogNow; 739 $watchdogDailyTime = isset($watchdogSettings['notifications']['daily_time']) 740 ? (string) $watchdogSettings['notifications']['daily_time'] 741 : '08:00'; 742 $watchdogWeeklyDay = isset($watchdogSettings['notifications']['weekly_day']) 743 ? (int) $watchdogSettings['notifications']['weekly_day'] 744 : 1; 745 $watchdogWeeklyTime = isset($watchdogSettings['notifications']['weekly_time']) 746 ? (string) $watchdogSettings['notifications']['weekly_time'] 747 : '08:00'; 748 ?> 749 <label for="wp-watchdog-notification-frequency" class="screen-reader-text"><?php esc_html_e('Scan frequency', 'site-add-on-watchdog'); ?></label> 750 <select id="wp-watchdog-notification-frequency" name="settings[notifications][frequency]"> 751 <option value="daily" <?php selected($watchdogSettings['notifications']['frequency'], 'daily'); ?>><?php esc_html_e('Daily', 'site-add-on-watchdog'); ?></option> 752 <option value="weekly" <?php selected($watchdogSettings['notifications']['frequency'], 'weekly'); ?>><?php esc_html_e('Weekly', 'site-add-on-watchdog'); ?></option> 753 <option value="testing" <?php selected($watchdogSettings['notifications']['frequency'], 'testing'); ?>><?php echo esc_html( 754 sprintf( 755 /* translators: %d: interval minutes */ 756 __('Testing (every %d minutes)', 'site-add-on-watchdog'), 757 TestingMode::INTERVAL_MINUTES 758 ) 759 ); ?></option> 760 <option value="manual" <?php selected($watchdogSettings['notifications']['frequency'], 'manual'); ?>><?php esc_html_e('Manual (no automatic scans)', 'site-add-on-watchdog'); ?></option> 761 </select> 762 <?php 763 $watchdogFrequencyDescriptionClass = 'description wp-watchdog-frequency-description'; 764 if ($watchdogIsTestingFrequency) { 765 $watchdogFrequencyDescriptionClass .= ' wp-watchdog-frequency-description--testing'; 766 } 767 ?> 768 <p 769 class="<?php echo esc_attr($watchdogFrequencyDescriptionClass); ?>" 770 data-watchdog-frequency-description 771 data-default-message="<?php echo esc_attr($watchdogDefaultFrequencyMessage); ?>" 772 data-testing-message="<?php echo esc_attr($watchdogTestingFrequencyMessage); ?>" 773 aria-live="polite" 774 > 775 <?php echo esc_html($watchdogIsTestingFrequency ? $watchdogTestingFrequencyMessage : $watchdogDefaultFrequencyMessage); ?> 776 </p> 777 <div class="wp-watchdog-frequency-options" data-watchdog-frequency-options> 778 <div class="wp-watchdog-frequency-options__row" data-watchdog-frequency-target="daily"> 779 <label for="wp-watchdog-daily-time" class="wp-watchdog-frequency-label"> 780 <?php esc_html_e('Daily send time', 'site-add-on-watchdog'); ?> 781 </label> 782 <input 783 id="wp-watchdog-daily-time" 784 name="settings[notifications][daily_time]" 785 type="time" 786 value="<?php echo esc_attr($watchdogDailyTime); ?>" 787 aria-describedby="wp-watchdog-daily-time-help" 788 /> 789 <p class="description" id="wp-watchdog-daily-time-help"> 790 <?php esc_html_e('Time of day to start the daily scan in the WordPress site timezone.', 'site-add-on-watchdog'); ?> 791 </p> 792 </div> 793 <div class="wp-watchdog-frequency-options__row" data-watchdog-frequency-target="weekly"> 794 <div class="wp-watchdog-frequency-weekly"> 795 <label for="wp-watchdog-weekly-day" class="wp-watchdog-frequency-label"> 796 <?php esc_html_e('Weekly send day', 'site-add-on-watchdog'); ?> 797 </label> 798 <select id="wp-watchdog-weekly-day" name="settings[notifications][weekly_day]"> 799 <option value="1" <?php selected($watchdogWeeklyDay, 1); ?>><?php esc_html_e('Monday', 'site-add-on-watchdog'); ?></option> 800 <option value="2" <?php selected($watchdogWeeklyDay, 2); ?>><?php esc_html_e('Tuesday', 'site-add-on-watchdog'); ?></option> 801 <option value="3" <?php selected($watchdogWeeklyDay, 3); ?>><?php esc_html_e('Wednesday', 'site-add-on-watchdog'); ?></option> 802 <option value="4" <?php selected($watchdogWeeklyDay, 4); ?>><?php esc_html_e('Thursday', 'site-add-on-watchdog'); ?></option> 803 <option value="5" <?php selected($watchdogWeeklyDay, 5); ?>><?php esc_html_e('Friday', 'site-add-on-watchdog'); ?></option> 804 <option value="6" <?php selected($watchdogWeeklyDay, 6); ?>><?php esc_html_e('Saturday', 'site-add-on-watchdog'); ?></option> 805 <option value="7" <?php selected($watchdogWeeklyDay, 7); ?>><?php esc_html_e('Sunday', 'site-add-on-watchdog'); ?></option> 806 </select> 807 </div> 808 <div class="wp-watchdog-frequency-weekly"> 809 <label for="wp-watchdog-weekly-time" class="wp-watchdog-frequency-label"> 810 <?php esc_html_e('Weekly send time', 'site-add-on-watchdog'); ?> 811 </label> 812 <input 813 id="wp-watchdog-weekly-time" 814 name="settings[notifications][weekly_time]" 815 type="time" 816 value="<?php echo esc_attr($watchdogWeeklyTime); ?>" 817 aria-describedby="wp-watchdog-weekly-time-help" 818 /> 819 </div> 820 <p class="description" id="wp-watchdog-weekly-time-help"> 821 <?php esc_html_e('Day and time to start the weekly scan in the WordPress site timezone.', 'site-add-on-watchdog'); ?> 822 </p> 823 </div> 824 </div> 825 <?php if ($watchdogShowTestingExpiry) : ?> 826 <?php 827 $watchdogTimezone = null; 828 if (function_exists('wp_timezone')) { 829 $watchdogTimezone = wp_timezone(); 830 } else { 831 $watchdogTimezoneString = (string) get_option('timezone_string'); 832 if ($watchdogTimezoneString !== '') { 833 try { 834 $watchdogTimezone = new DateTimeZone($watchdogTimezoneString); 835 } catch (Exception $exception) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch 836 } 837 } 838 839 if (! $watchdogTimezone) { 840 $watchdogGmtOffset = get_option('gmt_offset'); 841 if (is_numeric($watchdogGmtOffset)) { 842 $watchdogSecondsInHour = defined('HOUR_IN_SECONDS') ? HOUR_IN_SECONDS : 3600; 843 $watchdogSecondsOffset = (int) round((float) $watchdogGmtOffset * $watchdogSecondsInHour); 844 $watchdogTimezoneName = timezone_name_from_abbr('', $watchdogSecondsOffset, 0); 845 if ($watchdogTimezoneName !== false) { 846 try { 847 $watchdogTimezone = new DateTimeZone($watchdogTimezoneName); 848 } catch (Exception $exception) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch 849 } 850 } 851 } 852 } 853 } 854 855 if (! $watchdogTimezone && class_exists('DateTimeZone')) { 856 $watchdogTimezone = new DateTimeZone('UTC'); 857 } 858 859 $watchdogTestingExpiryMessage = sprintf( 860 /* translators: 1: datetime, 2: relative time */ 861 __('Testing mode will automatically switch back to daily scans on %1$s (%2$s remaining).', 'site-add-on-watchdog'), 862 wp_date( 863 get_option('date_format') . ' ' . get_option('time_format'), 864 $watchdogTestingExpiresAt, 865 $watchdogTimezone 866 ), 867 human_time_diff($watchdogNow, $watchdogTestingExpiresAt) 868 ); 869 ?> 870 <p class="description wp-watchdog-frequency-description wp-watchdog-frequency-description--expires"> 871 <?php echo esc_html($watchdogTestingExpiryMessage); ?> 872 </p> 873 <?php endif; ?> 874 </td> 875 </tr> 876 <tr> 877 <th scope="row"><?php esc_html_e('Email notifications', 'site-add-on-watchdog'); ?></th> 878 <td data-watchdog-notification> 879 <label class="wp-watchdog-notification-toggle"> 880 <input type="checkbox" name="settings[notifications][email][enabled]" <?php checked($watchdogSettings['notifications']['email']['enabled']); ?> data-watchdog-toggle /> 881 <?php esc_html_e('Enabled', 'site-add-on-watchdog'); ?> 882 </label> 883 <div class="wp-watchdog-notification-fields" data-watchdog-fields> 884 <label> 885 <?php esc_html_e('Recipients (comma separated)', 'site-add-on-watchdog'); ?><br /> 886 <input type="text" name="settings[notifications][email][recipients]" value="<?php echo esc_attr($watchdogSettings['notifications']['email']['recipients']); ?>" class="regular-text" /> 887 </label> 888 <p class="description"><?php esc_html_e('Separate addresses with commas, semicolons, or spaces. WordPress administrator accounts are always included.', 'site-add-on-watchdog'); ?></p> 889 <p class="description"><?php esc_html_e('Email uses WordPress mail delivery. Your host must support sending mail, or you must configure an SMTP/mail plugin. Discord and other webhooks do not use this mail configuration.', 'site-add-on-watchdog'); ?></p> 890 <p><button class="button" type="submit" name="test_channel" value="email"><?php esc_html_e('Save and test email', 'site-add-on-watchdog'); ?></button></p> 891 </div> 892 </td> 893 </tr> 894 <tr> 895 <th scope="row"><?php esc_html_e('Discord notifications', 'site-add-on-watchdog'); ?></th> 896 <td data-watchdog-notification> 897 <label class="wp-watchdog-notification-toggle"> 898 <input type="checkbox" name="settings[notifications][discord][enabled]" <?php checked($watchdogSettings['notifications']['discord']['enabled']); ?> data-watchdog-toggle /> 899 <?php esc_html_e('Enabled', 'site-add-on-watchdog'); ?> 900 </label> 901 <div class="wp-watchdog-notification-fields" data-watchdog-fields> 902 <label> 903 <?php esc_html_e('Discord webhook URL', 'site-add-on-watchdog'); ?><br /> 904 <input type="url" name="settings[notifications][discord][webhook]" value="<?php echo esc_attr($watchdogSettings['notifications']['discord']['webhook']); ?>" class="regular-text" /> 905 </label> 906 <p><button class="button" type="submit" name="test_channel" value="discord"><?php esc_html_e('Save and test Discord', 'site-add-on-watchdog'); ?></button></p> 907 </div> 908 </td> 909 </tr> 910 <tr> 911 <th scope="row"><?php esc_html_e('Slack notifications', 'site-add-on-watchdog'); ?></th> 912 <td data-watchdog-notification> 913 <label class="wp-watchdog-notification-toggle"> 914 <input type="checkbox" name="settings[notifications][slack][enabled]" <?php checked($watchdogSettings['notifications']['slack']['enabled']); ?> data-watchdog-toggle /> 915 <?php esc_html_e('Enabled', 'site-add-on-watchdog'); ?> 916 </label> 917 <div class="wp-watchdog-notification-fields" data-watchdog-fields> 918 <label> 919 <?php esc_html_e('Slack webhook URL', 'site-add-on-watchdog'); ?><br /> 920 <input type="url" name="settings[notifications][slack][webhook]" value="<?php echo esc_attr($watchdogSettings['notifications']['slack']['webhook']); ?>" class="regular-text" /> 921 </label> 922 <p><button class="button" type="submit" name="test_channel" value="slack"><?php esc_html_e('Save and test Slack', 'site-add-on-watchdog'); ?></button></p> 923 </div> 924 </td> 925 </tr> 926 <tr> 927 <th scope="row"><?php esc_html_e('Microsoft Teams notifications', 'site-add-on-watchdog'); ?></th> 928 <td data-watchdog-notification> 929 <label class="wp-watchdog-notification-toggle"> 930 <input type="checkbox" name="settings[notifications][teams][enabled]" <?php checked($watchdogSettings['notifications']['teams']['enabled']); ?> data-watchdog-toggle /> 931 <?php esc_html_e('Enabled', 'site-add-on-watchdog'); ?> 932 </label> 933 <div class="wp-watchdog-notification-fields" data-watchdog-fields> 934 <label> 935 <?php esc_html_e('Teams webhook URL', 'site-add-on-watchdog'); ?><br /> 936 <input type="url" name="settings[notifications][teams][webhook]" value="<?php echo esc_attr($watchdogSettings['notifications']['teams']['webhook']); ?>" class="regular-text" /> 937 </label> 938 <p class="description"><?php esc_html_e('Supports Microsoft Teams Workflows and existing Incoming Webhook connectors.', 'site-add-on-watchdog'); ?></p> 939 <p><button class="button" type="submit" name="test_channel" value="teams"><?php esc_html_e('Save and test Teams', 'site-add-on-watchdog'); ?></button></p> 940 </div> 941 </td> 942 </tr> 943 <tr> 944 <th scope="row"><?php esc_html_e('Generic webhook', 'site-add-on-watchdog'); ?></th> 945 <td data-watchdog-notification> 946 <label class="wp-watchdog-notification-toggle"> 947 <input type="checkbox" name="settings[notifications][webhook][enabled]" <?php checked($watchdogSettings['notifications']['webhook']['enabled']); ?> data-watchdog-toggle /> 948 <?php esc_html_e('Enabled', 'site-add-on-watchdog'); ?> 949 </label> 950 <div class="wp-watchdog-notification-fields" data-watchdog-fields> 951 <label> 952 <?php esc_html_e('Webhook URL', 'site-add-on-watchdog'); ?><br /> 953 <input type="url" name="settings[notifications][webhook][url]" value="<?php echo esc_attr($watchdogSettings['notifications']['webhook']['url']); ?>" class="regular-text" /> 954 </label> 955 <p> 956 <label> 957 <?php esc_html_e('Webhook secret (optional)', 'site-add-on-watchdog'); ?><br /> 958 <input type="password" name="settings[notifications][webhook][secret]" value="<?php echo esc_attr($watchdogSettings['notifications']['webhook']['secret'] ?? ''); ?>" class="regular-text" autocomplete="new-password" /> 959 </label> 960 <span class="description"><?php esc_html_e('Signs the exact JSON body with HMAC-SHA256 in the X-Watchdog-Signature header. Test messages are marked in the payload.', 'site-add-on-watchdog'); ?></span> 961 </p> 962 <p><button class="button" type="submit" name="test_channel" value="webhook"><?php esc_html_e('Save and test webhook', 'site-add-on-watchdog'); ?></button></p> 963 </div> 964 </td> 965 </tr> 966 <tr> 967 <th scope="row"><?php esc_html_e('WPScan API key', 'site-add-on-watchdog'); ?></th> 968 <td> 969 <input type="password" name="settings[notifications][wpscan_api_key]" value="<?php echo esc_attr($watchdogSettings['notifications']['wpscan_api_key']); ?>" class="regular-text" autocomplete="new-password" /> 970 <p class="description"><?php esc_html_e('Optional. Provide your own WPScan API key to enrich vulnerability reports.', 'site-add-on-watchdog'); ?></p> 971 </td> 972 </tr> 973 </table> 974 <p><button class="button button-primary" type="submit"><?php esc_html_e('Save settings', 'site-add-on-watchdog'); ?></button></p> 975 </form> 976 </div> 977 </div> 948 </section> 978 949 </div> -
site-add-on-watchdog/trunk/templates/history.php
r3690952 r3799692 5 5 * @var array $watchdogHistoryRecords 6 6 * @var array $watchdogHistoryDownloads 7 * @var int $historyRetention8 * @var int $historyDisplay9 * @var array $historyRecords10 * @var array $historyDownloads11 7 */ 12 8 defined('ABSPATH') || exit; 13 9 14 $watchdogHistoryRetention = $watchdogHistoryRetention ?? $historyRetention ?? 0; 15 $watchdogHistoryDisplay = $watchdogHistoryDisplay ?? $historyDisplay ?? 0; 16 $watchdogHistoryRecords = $watchdogHistoryRecords ?? $historyRecords ?? []; 17 $watchdogHistoryDownloads = $watchdogHistoryDownloads ?? $historyDownloads ?? []; 10 $watchdogHistoryFormat = sprintf('%s %s', get_option('date_format', 'Y-m-d'), get_option('time_format', 'H:i')); 18 11 ?> 19 12 20 13 <?php if (empty($watchdogHistoryRecords)) : ?> 21 <p ><?php echo esc_html__('No scans have been recorded yet. Run a scan to populate your history.', 'site-add-on-watchdog'); ?></p>14 <p class="wp-watchdog-card__description"><?php esc_html_e('No scans have been recorded yet. Run a scan to populate your history.', 'site-add-on-watchdog'); ?></p> 22 15 <?php else : ?> 23 <p class=" description">16 <p class="wp-watchdog-card__description"> 24 17 <?php 25 $watchdogDisplayCount = esc_html(number_format_i18n($watchdogHistoryDisplay)); 26 $watchdogRetentionCount = esc_html(number_format_i18n($watchdogHistoryRetention)); 27 echo esc_html( 28 sprintf( 29 /* translators: 1: number of scans shown, 2: total scans retained */ 30 esc_html__('Showing the last %1$s scans (retaining %2$s in total).', 'site-add-on-watchdog'), 31 $watchdogDisplayCount, 32 $watchdogRetentionCount 33 ) 18 printf( 19 /* translators: 1: number of scans shown, 2: total scans retained */ 20 esc_html__('Showing the last %1$s scans (retaining %2$s in total).', 'site-add-on-watchdog'), 21 esc_html(number_format_i18n($watchdogHistoryDisplay)), 22 esc_html(number_format_i18n($watchdogHistoryRetention)) 34 23 ); 35 24 ?> 36 25 </p> 37 < div class="wp-watchdog-history-grid" role="list">26 <ul class="wp-watchdog-history"> 38 27 <?php foreach ($watchdogHistoryRecords as $watchdogRecord) : ?> 39 <?php $watchdogDownloads = $watchdogHistoryDownloads[$watchdogRecord['run_at']] ?? []; ?> 40 <?php $watchdogHasRisks = (int) $watchdogRecord['risk_count'] > 0; ?> 41 <div class="wp-watchdog-history-card" role="listitem"> 42 <div class="wp-watchdog-history-card__header"> 43 <h3 class="wp-watchdog-history-card__title"> 44 <span class="dashicons dashicons-backup" aria-hidden="true"></span> 45 <?php 46 $watchdogFormat = sprintf('%s %s', get_option('date_format', 'Y-m-d'), get_option('time_format', 'H:i')); 47 echo esc_html(wp_date($watchdogFormat, $watchdogRecord['run_at'])); 48 ?> 49 </h3> 50 <span class="wp-watchdog-history-badge <?php echo $watchdogHasRisks ? 'wp-watchdog-history-badge--risk' : 'wp-watchdog-history-badge--safe'; ?>"> 51 <span class="dashicons <?php echo $watchdogHasRisks ? 'dashicons-warning' : 'dashicons-yes-alt'; ?>" aria-hidden="true"></span> 52 <?php 53 $watchdogRiskLabel = function_exists('_n') 54 ? /* translators: %s: number of risks. */ 55 _n( 56 '%s risk', 57 '%s risks', 58 $watchdogRecord['risk_count'], 59 'site-add-on-watchdog' 60 ) 61 : ($watchdogRecord['risk_count'] === 1 ? '%s risk' : '%s risks'); 62 63 $watchdogRiskCount = number_format_i18n($watchdogRecord['risk_count']); 64 printf(esc_html($watchdogRiskLabel), esc_html($watchdogRiskCount)); 65 ?> 66 </span> 67 </div> 68 <p class="wp-watchdog-history-card__meta"> 69 <?php echo esc_html__('Download the report for this run:', 'site-add-on-watchdog'); ?> 70 </p> 71 <div class="wp-watchdog-history-card__downloads"> 28 <?php 29 $watchdogDownloads = $watchdogHistoryDownloads[$watchdogRecord['run_at']] ?? []; 30 $watchdogRecordRiskCount = (int) $watchdogRecord['risk_count']; 31 ?> 32 <li class="wp-watchdog-history__item"> 33 <span class="wp-watchdog-history__date"> 34 <span class="dashicons dashicons-backup" aria-hidden="true"></span> 35 <?php echo esc_html(wp_date($watchdogHistoryFormat, $watchdogRecord['run_at'])); ?> 36 </span> 37 <span class="wp-watchdog-badge <?php echo $watchdogRecordRiskCount > 0 ? 'wp-watchdog-badge--warn' : 'wp-watchdog-badge--ok'; ?>"> 38 <?php 39 printf( 40 /* translators: %s: number of risks. */ 41 esc_html(_n('%s risk', '%s risks', $watchdogRecordRiskCount, 'site-add-on-watchdog')), 42 esc_html(number_format_i18n($watchdogRecordRiskCount)) 43 ); 44 ?> 45 </span> 46 <span class="wp-watchdog-history__downloads"> 72 47 <?php if (! empty($watchdogDownloads['json'])) : ?> 73 48 <a class="button button-small" href="<?php echo esc_url($watchdogDownloads['json']); ?>"><?php esc_html_e('Download JSON', 'site-add-on-watchdog'); ?></a> … … 76 51 <a class="button button-small" href="<?php echo esc_url($watchdogDownloads['csv']); ?>"><?php esc_html_e('Download CSV', 'site-add-on-watchdog'); ?></a> 77 52 <?php endif; ?> 78 </ div>79 </ div>53 </span> 54 </li> 80 55 <?php endforeach; ?> 81 </ div>56 </ul> 82 57 <?php endif; ?>
Note:
See TracChangeset
for help on using the changeset viewer.