From 9caab8ce681c642320ebc57546418865ff6a6425 Mon Sep 17 00:00:00 2001
From: Sophia Atkinson
Date: Fri, 1 Sep 2023 00:37:57 -0700
Subject: [PATCH] first commit
---
admin/blocks/js/blocks.asset.php | 1 +
admin/blocks/js/blocks.js | 2 +
admin/blocks/src/avatar/block.json | 29 +
admin/blocks/src/avatar/edit.js | 101 +++
admin/blocks/src/avatar/index.js | 46 ++
admin/blocks/src/blocks.js | 38 +
admin/blocks/src/frontend-form/block.json | 19 +
admin/blocks/src/frontend-form/edit.js | 82 ++
admin/blocks/src/frontend-form/index.js | 47 ++
admin/css/blocks.css | 4 +
admin/css/blocks.min.css | 1 +
admin/css/settings.css | 14 +
admin/css/settings.min.css | 1 +
admin/partials/network/section.php | 30 +
admin/partials/network/settings-page.php | 42 +
admin/partials/profile/allow-anonymous.php | 53 ++
admin/partials/profile/use-gravatar.php | 58 ++
admin/partials/profile/user-avatar-upload.php | 69 ++
.../sections/avatars-disabled-script.php | 39 +
admin/partials/sections/avatars-disabled.php | 39 +
admin/partials/sections/avatars-enabled.php | 42 +
avatar-privacy.php | 84 ++
includes/avatar-privacy-functions.php | 54 ++
.../avatar-handlers/class-avatar-handler.php | 84 ++
.../class-default-icons-handler.php | 200 +++++
.../class-gravatar-cache-handler.php | 249 ++++++
.../class-legacy-icon-handler.php | 185 +++++
.../class-user-avatar-handler.php | 221 ++++++
.../class-abstract-icon-provider.php | 127 ++++
.../class-custom-icon-provider.php | 139 ++++
.../class-generating-icon-provider.php | 116 +++
.../default-icons/class-generator.php | 48 ++
.../default-icons/class-icon-provider.php | 87 +++
.../class-static-icon-provider.php | 79 ++
.../default-icons/class-svg-icon-provider.php | 55 ++
.../class-bird-avatar-icon-provider.php | 73 ++
.../class-cat-avatar-icon-provider.php | 73 ++
.../class-identicon-icon-provider.php | 66 ++
.../class-monster-id-icon-provider.php | 66 ++
.../class-retro-icon-provider.php | 66 ++
.../class-rings-icon-provider.php | 77 ++
.../class-robohash-icon-provider.php | 73 ++
.../class-wavatar-icon-provider.php | 66 ++
.../generators/class-bird-avatar.php | 109 +++
.../generators/class-cat-avatar.php | 109 +++
.../generators/class-jdenticon.php | 74 ++
.../generators/class-monster-id.php | 460 +++++++++++
.../generators/class-parts-generator.php | 321 ++++++++
.../generators/class-png-parts-generator.php | 383 ++++++++++
.../default-icons/generators/class-retro.php | 198 +++++
.../default-icons/generators/class-rings.php | 55 ++
.../generators/class-robohash.php | 275 +++++++
.../generators/class-wavatar.php | 279 +++++++
.../class-bowling-pin-icon-provider.php | 57 ++
.../class-mystery-icon-provider.php | 46 ++
.../class-silhouette-icon-provider.php | 57 ++
.../class-speech-bubble-icon-provider.php | 57 ++
includes/avatar-privacy/class-component.php | 44 ++
includes/avatar-privacy/class-controller.php | 82 ++
includes/avatar-privacy/class-core.php | 490 ++++++++++++
includes/avatar-privacy/class-factory.php | 578 ++++++++++++++
.../avatar-privacy/class-requirements.php | 137 ++++
.../cli/class-abstract-command.php | 106 +++
includes/avatar-privacy/cli/class-command.php | 46 ++
.../avatar-privacy/cli/class-cron-command.php | 104 +++
.../cli/class-database-command.php | 324 ++++++++
.../cli/class-default-command.php | 209 +++++
.../cli/class-uninstall-command.php | 239 ++++++
.../avatar-privacy/cli/class-user-command.php | 206 +++++
.../components/class-avatar-handling.php | 682 +++++++++++++++++
.../components/class-block-editor.php | 249 ++++++
.../class-command-line-interface.php | 76 ++
.../components/class-comments.php | 334 ++++++++
.../components/class-image-proxy.php | 349 +++++++++
.../components/class-integrations.php | 80 ++
.../class-network-settings-page.php | 376 +++++++++
.../components/class-privacy-tools.php | 355 +++++++++
.../components/class-settings-page.php | 229 ++++++
.../avatar-privacy/components/class-setup.php | 485 ++++++++++++
.../components/class-shortcodes.php | 151 ++++
.../components/class-uninstallation.php | 288 +++++++
.../components/class-user-profile.php | 171 +++++
includes/avatar-privacy/core/class-api.php | 36 +
.../core/class-comment-author-fields.php | 413 ++++++++++
.../core/class-default-avatars.php | 270 +++++++
.../avatar-privacy/core/class-settings.php | 449 +++++++++++
.../avatar-privacy/core/class-user-fields.php | 525 +++++++++++++
.../data-storage/class-cache.php | 46 ++
.../data-storage/class-filesystem-cache.php | 278 +++++++
.../data-storage/class-network-options.php | 134 ++++
.../data-storage/class-options.php | 82 ++
.../data-storage/class-site-transients.php | 45 ++
.../data-storage/class-transients.php | 45 ++
.../database/class-comment-author-table.php | 454 +++++++++++
.../database/class-hashes-table.php | 162 ++++
.../data-storage/database/class-table.php | 694 +++++++++++++++++
.../class-avatar-comment-type-exception.php | 38 +
.../exceptions/class-database-exception.php | 37 +
.../class-file-deletion-exception.php | 39 +
.../exceptions/class-filesystem-exception.php | 37 +
.../class-form-field-not-found-exception.php | 39 +
.../class-invalid-nonce-exception.php | 38 +
.../class-object-factory-exception.php | 37 +
.../class-part-files-not-found-exception.php | 38 +
.../exceptions/class-png-image-exception.php | 37 +
.../class-upload-handling-exception.php | 37 +
includes/avatar-privacy/functions.php | 59 ++
.../class-bbpress-integration.php | 145 ++++
.../class-buddypress-integration.php | 275 +++++++
.../integrations/class-plugin-integration.php | 45 ++
.../class-simple-author-box-integration.php | 170 +++++
...class-simple-local-avatars-integration.php | 173 +++++
.../class-simple-user-avatar-integration.php | 164 ++++
...ss-theme-my-login-profiles-integration.php | 145 ++++
.../class-ultimate-member-integration.php | 177 +++++
.../class-wp-user-manager-integration.php | 161 ++++
.../class-wpdiscuz-integration.php | 191 +++++
.../avatar-privacy/tools/class-hasher.php | 133 ++++
.../avatar-privacy/tools/class-multisite.php | 80 ++
.../tools/class-number-generator.php | 72 ++
.../avatar-privacy/tools/class-template.php | 256 +++++++
includes/avatar-privacy/tools/functions.php | 51 ++
.../tools/html/class-dependencies.php | 242 ++++++
.../tools/html/class-user-form.php | 574 ++++++++++++++
.../tools/images/class-color.php | 135 ++++
.../tools/images/class-editor.php | 350 +++++++++
.../tools/images/class-image-file.php | 350 +++++++++
.../tools/images/class-image-stream.php | 579 ++++++++++++++
.../avatar-privacy/tools/images/class-png.php | 266 +++++++
.../avatar-privacy/tools/images/class-svg.php | 716 ++++++++++++++++++
.../tools/images/class-type.php | 62 ++
.../tools/network/class-gravatar-service.php | 287 +++++++
.../network/class-remote-image-service.php | 233 ++++++
...ass-custom-default-icon-upload-handler.php | 300 ++++++++
.../upload-handlers/class-upload-handler.php | 366 +++++++++
.../class-user-avatar-upload-handler.php | 257 +++++++
.../ui/class-file-upload-input.php | 139 ++++
public/images/LICENSE.md | 82 ++
public/images/birds/accessoire_1.png | Bin 0 -> 1154 bytes
public/images/birds/accessoire_10.png | Bin 0 -> 21719 bytes
public/images/birds/accessoire_11.png | Bin 0 -> 19617 bytes
public/images/birds/accessoire_12.png | Bin 0 -> 20814 bytes
public/images/birds/accessoire_13.png | Bin 0 -> 1154 bytes
public/images/birds/accessoire_14.png | Bin 0 -> 24307 bytes
public/images/birds/accessoire_15.png | Bin 0 -> 1154 bytes
public/images/birds/accessoire_16.png | Bin 0 -> 57418 bytes
public/images/birds/accessoire_17.png | Bin 0 -> 1154 bytes
public/images/birds/accessoire_18.png | Bin 0 -> 1154 bytes
public/images/birds/accessoire_19.png | Bin 0 -> 1154 bytes
public/images/birds/accessoire_2.png | Bin 0 -> 1154 bytes
public/images/birds/accessoire_20.png | Bin 0 -> 1154 bytes
public/images/birds/accessoire_3.png | Bin 0 -> 16538 bytes
public/images/birds/accessoire_4.png | Bin 0 -> 1154 bytes
public/images/birds/accessoire_5.png | Bin 0 -> 31256 bytes
public/images/birds/accessoire_6.png | Bin 0 -> 23989 bytes
public/images/birds/accessoire_7.png | Bin 0 -> 20560 bytes
public/images/birds/accessoire_8.png | Bin 0 -> 40871 bytes
public/images/birds/accessoire_9.png | Bin 0 -> 1154 bytes
public/images/birds/beak_1.png | Bin 0 -> 19875 bytes
public/images/birds/beak_2.png | Bin 0 -> 13109 bytes
public/images/birds/beak_3.png | Bin 0 -> 16880 bytes
public/images/birds/beak_4.png | Bin 0 -> 16564 bytes
public/images/birds/beak_5.png | Bin 0 -> 13407 bytes
public/images/birds/beak_6.png | Bin 0 -> 13495 bytes
public/images/birds/beak_7.png | Bin 0 -> 11784 bytes
public/images/birds/beak_8.png | Bin 0 -> 12530 bytes
public/images/birds/beak_9.png | Bin 0 -> 16121 bytes
public/images/birds/body_1.png | Bin 0 -> 51871 bytes
public/images/birds/body_2.png | Bin 0 -> 52945 bytes
public/images/birds/body_3.png | Bin 0 -> 64016 bytes
public/images/birds/body_4.png | Bin 0 -> 63904 bytes
public/images/birds/body_5.png | Bin 0 -> 95203 bytes
public/images/birds/body_6.png | Bin 0 -> 68081 bytes
public/images/birds/body_7.png | Bin 0 -> 57771 bytes
public/images/birds/body_8.png | Bin 0 -> 85868 bytes
public/images/birds/body_9.png | Bin 0 -> 65307 bytes
public/images/birds/eyes_1.png | Bin 0 -> 21166 bytes
public/images/birds/eyes_2.png | Bin 0 -> 22784 bytes
public/images/birds/eyes_3.png | Bin 0 -> 29633 bytes
public/images/birds/eyes_4.png | Bin 0 -> 24429 bytes
public/images/birds/eyes_5.png | Bin 0 -> 43086 bytes
public/images/birds/eyes_6.png | Bin 0 -> 49544 bytes
public/images/birds/eyes_7.png | Bin 0 -> 40419 bytes
public/images/birds/eyes_8.png | Bin 0 -> 22898 bytes
public/images/birds/eyes_9.png | Bin 0 -> 22350 bytes
public/images/birds/guide.png | Bin 0 -> 30012 bytes
public/images/birds/hoop_1.png | Bin 0 -> 17265 bytes
public/images/birds/hoop_10.png | Bin 0 -> 28832 bytes
public/images/birds/hoop_2.png | Bin 0 -> 18659 bytes
public/images/birds/hoop_3.png | Bin 0 -> 24208 bytes
public/images/birds/hoop_4.png | Bin 0 -> 21347 bytes
public/images/birds/hoop_5.png | Bin 0 -> 20449 bytes
public/images/birds/hoop_6.png | Bin 0 -> 16851 bytes
public/images/birds/hoop_7.png | Bin 0 -> 17756 bytes
public/images/birds/hoop_8.png | Bin 0 -> 1154 bytes
public/images/birds/hoop_9.png | Bin 0 -> 19382 bytes
public/images/birds/tail_1.png | Bin 0 -> 32146 bytes
public/images/birds/tail_2.png | Bin 0 -> 33521 bytes
public/images/birds/tail_3.png | Bin 0 -> 80234 bytes
public/images/birds/tail_4.png | Bin 0 -> 16890 bytes
public/images/birds/tail_5.png | Bin 0 -> 36826 bytes
public/images/birds/tail_6.png | Bin 0 -> 40426 bytes
public/images/birds/tail_7.png | Bin 0 -> 41481 bytes
public/images/birds/tail_8.png | Bin 0 -> 32175 bytes
public/images/birds/tail_9.png | Bin 0 -> 51238 bytes
public/images/birds/wing_1.png | Bin 0 -> 20111 bytes
public/images/birds/wing_2.png | Bin 0 -> 20285 bytes
public/images/birds/wing_3.png | Bin 0 -> 20916 bytes
public/images/birds/wing_4.png | Bin 0 -> 20159 bytes
public/images/birds/wing_5.png | Bin 0 -> 17367 bytes
public/images/birds/wing_6.png | Bin 0 -> 26855 bytes
public/images/birds/wing_7.png | Bin 0 -> 28041 bytes
public/images/birds/wing_8.png | Bin 0 -> 22129 bytes
public/images/birds/wing_9.png | Bin 0 -> 31448 bytes
public/images/cats/accessoire_1.png | Bin 0 -> 30367 bytes
public/images/cats/accessoire_10.png | Bin 0 -> 21357 bytes
public/images/cats/accessoire_11.png | Bin 0 -> 19617 bytes
public/images/cats/accessoire_12.png | Bin 0 -> 21921 bytes
public/images/cats/accessoire_13.png | Bin 0 -> 78046 bytes
public/images/cats/accessoire_14.png | Bin 0 -> 24307 bytes
public/images/cats/accessoire_15.png | Bin 0 -> 30137 bytes
public/images/cats/accessoire_16.png | Bin 0 -> 67553 bytes
public/images/cats/accessoire_17.png | Bin 0 -> 1154 bytes
public/images/cats/accessoire_18.png | Bin 0 -> 1154 bytes
public/images/cats/accessoire_19.png | Bin 0 -> 1154 bytes
public/images/cats/accessoire_2.png | Bin 0 -> 10600 bytes
public/images/cats/accessoire_20.png | Bin 0 -> 1154 bytes
public/images/cats/accessoire_3.png | Bin 0 -> 16538 bytes
public/images/cats/accessoire_4.png | Bin 0 -> 8521 bytes
public/images/cats/accessoire_5.png | Bin 0 -> 29685 bytes
public/images/cats/accessoire_6.png | Bin 0 -> 23989 bytes
public/images/cats/accessoire_7.png | Bin 0 -> 20560 bytes
public/images/cats/accessoire_8.png | Bin 0 -> 47364 bytes
public/images/cats/accessoire_9.png | Bin 0 -> 34943 bytes
public/images/cats/body_1.png | Bin 0 -> 72209 bytes
public/images/cats/body_10.png | Bin 0 -> 93365 bytes
public/images/cats/body_11.png | Bin 0 -> 76675 bytes
public/images/cats/body_12.png | Bin 0 -> 87220 bytes
public/images/cats/body_13.png | Bin 0 -> 69420 bytes
public/images/cats/body_14.png | Bin 0 -> 102399 bytes
public/images/cats/body_15.png | Bin 0 -> 82671 bytes
public/images/cats/body_2.png | Bin 0 -> 83734 bytes
public/images/cats/body_3.png | Bin 0 -> 95393 bytes
public/images/cats/body_4.png | Bin 0 -> 71104 bytes
public/images/cats/body_5.png | Bin 0 -> 84140 bytes
public/images/cats/body_6.png | Bin 0 -> 97372 bytes
public/images/cats/body_7.png | Bin 0 -> 83905 bytes
public/images/cats/body_8.png | Bin 0 -> 80819 bytes
public/images/cats/body_9.png | Bin 0 -> 82031 bytes
public/images/cats/eyes_1.png | Bin 0 -> 17343 bytes
public/images/cats/eyes_10.png | Bin 0 -> 17595 bytes
public/images/cats/eyes_11.png | Bin 0 -> 13166 bytes
public/images/cats/eyes_12.png | Bin 0 -> 11934 bytes
public/images/cats/eyes_13.png | Bin 0 -> 28162 bytes
public/images/cats/eyes_14.png | Bin 0 -> 13887 bytes
public/images/cats/eyes_15.png | Bin 0 -> 17427 bytes
public/images/cats/eyes_2.png | Bin 0 -> 12544 bytes
public/images/cats/eyes_3.png | Bin 0 -> 4261 bytes
public/images/cats/eyes_4.png | Bin 0 -> 13612 bytes
public/images/cats/eyes_5.png | Bin 0 -> 25733 bytes
public/images/cats/eyes_6.png | Bin 0 -> 13836 bytes
public/images/cats/eyes_7.png | Bin 0 -> 4716 bytes
public/images/cats/eyes_8.png | Bin 0 -> 18488 bytes
public/images/cats/eyes_9.png | Bin 0 -> 15271 bytes
public/images/cats/fur_1.png | Bin 0 -> 13713 bytes
public/images/cats/fur_10.png | Bin 0 -> 28965 bytes
public/images/cats/fur_2.png | Bin 0 -> 20685 bytes
public/images/cats/fur_3.png | Bin 0 -> 1154 bytes
public/images/cats/fur_4.png | Bin 0 -> 38615 bytes
public/images/cats/fur_5.png | Bin 0 -> 31096 bytes
public/images/cats/fur_6.png | Bin 0 -> 32904 bytes
public/images/cats/fur_7.png | Bin 0 -> 1154 bytes
public/images/cats/fur_8.png | Bin 0 -> 15228 bytes
public/images/cats/fur_9.png | Bin 0 -> 14064 bytes
public/images/cats/mouth_1.png | Bin 0 -> 5322 bytes
public/images/cats/mouth_10.png | Bin 0 -> 8270 bytes
public/images/cats/mouth_2.png | Bin 0 -> 4822 bytes
public/images/cats/mouth_3.png | Bin 0 -> 2883 bytes
public/images/cats/mouth_4.png | Bin 0 -> 10812 bytes
public/images/cats/mouth_5.png | Bin 0 -> 5594 bytes
public/images/cats/mouth_6.png | Bin 0 -> 5908 bytes
public/images/cats/mouth_7.png | Bin 0 -> 4535 bytes
public/images/cats/mouth_8.png | Bin 0 -> 4921 bytes
public/images/cats/mouth_9.png | Bin 0 -> 9648 bytes
public/images/cats/zz_1.png | Bin 0 -> 26493 bytes
public/images/cats/zz_2.png | Bin 0 -> 26487 bytes
public/images/comment-bubble.svg | 1 +
public/images/monster-id/arms_1.png | Bin 0 -> 1796 bytes
public/images/monster-id/arms_2.png | Bin 0 -> 2141 bytes
public/images/monster-id/arms_3.png | Bin 0 -> 8560 bytes
public/images/monster-id/arms_4.png | Bin 0 -> 6181 bytes
public/images/monster-id/arms_5.png | Bin 0 -> 12373 bytes
public/images/monster-id/arms_S1.png | Bin 0 -> 8795 bytes
public/images/monster-id/arms_S2.png | Bin 0 -> 14199 bytes
public/images/monster-id/arms_S3.png | Bin 0 -> 9928 bytes
public/images/monster-id/arms_S4.png | Bin 0 -> 2960 bytes
public/images/monster-id/arms_S5.png | Bin 0 -> 5236 bytes
public/images/monster-id/arms_S6.png | Bin 0 -> 14273 bytes
public/images/monster-id/arms_S7.png | Bin 0 -> 8545 bytes
public/images/monster-id/arms_S8.png | Bin 0 -> 14929 bytes
public/images/monster-id/arms_S9.png | Bin 0 -> 6424 bytes
public/images/monster-id/back.png | Bin 0 -> 136 bytes
public/images/monster-id/body_1.png | Bin 0 -> 7014 bytes
public/images/monster-id/body_10.png | Bin 0 -> 7685 bytes
public/images/monster-id/body_11.png | Bin 0 -> 12299 bytes
public/images/monster-id/body_12.png | Bin 0 -> 13138 bytes
public/images/monster-id/body_13.png | Bin 0 -> 7996 bytes
public/images/monster-id/body_14.png | Bin 0 -> 10474 bytes
public/images/monster-id/body_15.png | Bin 0 -> 10799 bytes
public/images/monster-id/body_2.png | Bin 0 -> 10266 bytes
public/images/monster-id/body_3.png | Bin 0 -> 8653 bytes
public/images/monster-id/body_4.png | Bin 0 -> 10977 bytes
public/images/monster-id/body_5.png | Bin 0 -> 13768 bytes
public/images/monster-id/body_6.png | Bin 0 -> 9507 bytes
public/images/monster-id/body_7.png | Bin 0 -> 11324 bytes
public/images/monster-id/body_8.png | Bin 0 -> 10629 bytes
public/images/monster-id/body_9.png | Bin 0 -> 10319 bytes
public/images/monster-id/body_S1.png | Bin 0 -> 10197 bytes
public/images/monster-id/body_S2.png | Bin 0 -> 7002 bytes
public/images/monster-id/body_S3.png | Bin 0 -> 9411 bytes
public/images/monster-id/body_S4.png | Bin 0 -> 14521 bytes
public/images/monster-id/body_S5.png | Bin 0 -> 11345 bytes
public/images/monster-id/eyes_1.png | Bin 0 -> 2307 bytes
public/images/monster-id/eyes_10.png | Bin 0 -> 2821 bytes
public/images/monster-id/eyes_11.png | Bin 0 -> 2730 bytes
public/images/monster-id/eyes_12.png | Bin 0 -> 2382 bytes
public/images/monster-id/eyes_13.png | Bin 0 -> 3992 bytes
public/images/monster-id/eyes_14.png | Bin 0 -> 4711 bytes
public/images/monster-id/eyes_15.png | Bin 0 -> 952 bytes
public/images/monster-id/eyes_2.png | Bin 0 -> 2074 bytes
public/images/monster-id/eyes_3.png | Bin 0 -> 2834 bytes
public/images/monster-id/eyes_4.png | Bin 0 -> 1199 bytes
public/images/monster-id/eyes_5.png | Bin 0 -> 2049 bytes
public/images/monster-id/eyes_6.png | Bin 0 -> 1682 bytes
public/images/monster-id/eyes_7.png | Bin 0 -> 1679 bytes
public/images/monster-id/eyes_8.png | Bin 0 -> 2502 bytes
public/images/monster-id/eyes_9.png | Bin 0 -> 889 bytes
public/images/monster-id/eyes_S1.png | Bin 0 -> 2973 bytes
public/images/monster-id/eyes_S2.png | Bin 0 -> 724 bytes
public/images/monster-id/eyes_S3.png | Bin 0 -> 2752 bytes
public/images/monster-id/eyes_S4.png | Bin 0 -> 2035 bytes
public/images/monster-id/eyes_S5.png | Bin 0 -> 2291 bytes
public/images/monster-id/hair_1.png | Bin 0 -> 2928 bytes
public/images/monster-id/hair_2.png | Bin 0 -> 1563 bytes
public/images/monster-id/hair_3.png | Bin 0 -> 5504 bytes
public/images/monster-id/hair_4.png | Bin 0 -> 4122 bytes
public/images/monster-id/hair_5.png | Bin 0 -> 2141 bytes
public/images/monster-id/hair_S1.png | Bin 0 -> 8218 bytes
public/images/monster-id/hair_S2.png | Bin 0 -> 5363 bytes
public/images/monster-id/hair_S3.png | Bin 0 -> 8868 bytes
public/images/monster-id/hair_S4.png | Bin 0 -> 9470 bytes
public/images/monster-id/hair_S5.png | Bin 0 -> 10038 bytes
public/images/monster-id/hair_S6.png | Bin 0 -> 8684 bytes
public/images/monster-id/hair_S7.png | Bin 0 -> 2719 bytes
public/images/monster-id/legs_1.png | Bin 0 -> 7274 bytes
public/images/monster-id/legs_2.png | Bin 0 -> 7575 bytes
public/images/monster-id/legs_3.png | Bin 0 -> 7617 bytes
public/images/monster-id/legs_4.png | Bin 0 -> 1995 bytes
public/images/monster-id/legs_5.png | Bin 0 -> 7272 bytes
public/images/monster-id/legs_S1.png | Bin 0 -> 8853 bytes
public/images/monster-id/legs_S10.png | Bin 0 -> 4388 bytes
public/images/monster-id/legs_S11.png | Bin 0 -> 3232 bytes
public/images/monster-id/legs_S12.png | Bin 0 -> 7214 bytes
public/images/monster-id/legs_S13.png | Bin 0 -> 7896 bytes
public/images/monster-id/legs_S2.png | Bin 0 -> 8696 bytes
public/images/monster-id/legs_S3.png | Bin 0 -> 11615 bytes
public/images/monster-id/legs_S4.png | Bin 0 -> 6961 bytes
public/images/monster-id/legs_S5.png | Bin 0 -> 9919 bytes
public/images/monster-id/legs_S6.png | Bin 0 -> 8156 bytes
public/images/monster-id/legs_S7.png | Bin 0 -> 4377 bytes
public/images/monster-id/legs_S8.png | Bin 0 -> 1401 bytes
public/images/monster-id/legs_S9.png | Bin 0 -> 4227 bytes
public/images/monster-id/mouth_1.png | Bin 0 -> 881 bytes
public/images/monster-id/mouth_10.png | Bin 0 -> 4709 bytes
public/images/monster-id/mouth_2.png | Bin 0 -> 1227 bytes
public/images/monster-id/mouth_3.png | Bin 0 -> 2984 bytes
public/images/monster-id/mouth_4.png | Bin 0 -> 3074 bytes
public/images/monster-id/mouth_5.png | Bin 0 -> 1944 bytes
public/images/monster-id/mouth_6.png | Bin 0 -> 2645 bytes
public/images/monster-id/mouth_7.png | Bin 0 -> 2071 bytes
public/images/monster-id/mouth_8.png | Bin 0 -> 2539 bytes
public/images/monster-id/mouth_9.png | Bin 0 -> 5094 bytes
public/images/monster-id/mouth_S1.png | Bin 0 -> 2373 bytes
public/images/monster-id/mouth_S2.png | Bin 0 -> 2212 bytes
public/images/monster-id/mouth_S3.png | Bin 0 -> 2500 bytes
public/images/monster-id/mouth_S4.png | Bin 0 -> 2400 bytes
public/images/monster-id/mouth_S5.png | Bin 0 -> 2323 bytes
public/images/monster-id/mouth_S6.png | Bin 0 -> 2411 bytes
public/images/monster-id/mouth_S7.png | Bin 0 -> 715 bytes
public/images/mystery.svg | 1 +
.../robohash/accessory/accessory-01.svg | 1 +
.../robohash/accessory/accessory-02.svg | 1 +
.../robohash/accessory/accessory-03.svg | 1 +
.../robohash/accessory/accessory-04.svg | 1 +
.../robohash/accessory/accessory-05.svg | 1 +
.../robohash/accessory/accessory-06.svg | 1 +
.../robohash/accessory/accessory-07.svg | 1 +
.../robohash/accessory/accessory-08.svg | 1 +
.../robohash/accessory/accessory-09.svg | 1 +
.../robohash/accessory/accessory-10.svg | 1 +
public/images/robohash/body/body-01.svg | 1 +
public/images/robohash/body/body-02.svg | 1 +
public/images/robohash/body/body-03.svg | 1 +
public/images/robohash/body/body-04.svg | 1 +
public/images/robohash/body/body-05.svg | 1 +
public/images/robohash/body/body-06.svg | 1 +
public/images/robohash/body/body-07.svg | 1 +
public/images/robohash/body/body-08.svg | 1 +
public/images/robohash/body/body-09.svg | 1 +
public/images/robohash/body/body-10.svg | 1 +
public/images/robohash/eyes/eyes-01.svg | 1 +
public/images/robohash/eyes/eyes-02.svg | 1 +
public/images/robohash/eyes/eyes-03.svg | 1 +
public/images/robohash/eyes/eyes-04.svg | 1 +
public/images/robohash/eyes/eyes-05.svg | 1 +
public/images/robohash/eyes/eyes-06.svg | 1 +
public/images/robohash/eyes/eyes-07.svg | 1 +
public/images/robohash/eyes/eyes-08.svg | 1 +
public/images/robohash/eyes/eyes-09.svg | 1 +
public/images/robohash/eyes/eyes-10.svg | 1 +
public/images/robohash/face/face-01.svg | 1 +
public/images/robohash/face/face-02.svg | 1 +
public/images/robohash/face/face-03.svg | 1 +
public/images/robohash/face/face-04.svg | 1 +
public/images/robohash/face/face-05.svg | 1 +
public/images/robohash/face/face-06.svg | 1 +
public/images/robohash/face/face-07.svg | 1 +
public/images/robohash/face/face-08.svg | 1 +
public/images/robohash/face/face-09.svg | 1 +
public/images/robohash/face/face-10.svg | 1 +
public/images/robohash/mouth/mouth-01.svg | 1 +
public/images/robohash/mouth/mouth-02.svg | 1 +
public/images/robohash/mouth/mouth-03.svg | 1 +
public/images/robohash/mouth/mouth-04.svg | 1 +
public/images/robohash/mouth/mouth-05.svg | 1 +
public/images/robohash/mouth/mouth-06.svg | 1 +
public/images/robohash/mouth/mouth-07.svg | 1 +
public/images/robohash/mouth/mouth-08.svg | 1 +
public/images/robohash/mouth/mouth-09.svg | 1 +
public/images/robohash/mouth/mouth-10.svg | 1 +
public/images/shaded-cone.svg | 1 +
public/images/silhouette.svg | 1 +
public/images/wavatars/brow_1.png | Bin 0 -> 189 bytes
public/images/wavatars/brow_2.png | Bin 0 -> 401 bytes
public/images/wavatars/brow_3.png | Bin 0 -> 363 bytes
public/images/wavatars/brow_4.png | Bin 0 -> 418 bytes
public/images/wavatars/brow_5.png | Bin 0 -> 346 bytes
public/images/wavatars/brow_6.png | Bin 0 -> 488 bytes
public/images/wavatars/brow_7.png | Bin 0 -> 383 bytes
public/images/wavatars/brow_8.png | Bin 0 -> 160 bytes
public/images/wavatars/eyes_1.png | Bin 0 -> 941 bytes
public/images/wavatars/eyes_10.png | Bin 0 -> 948 bytes
public/images/wavatars/eyes_11.png | Bin 0 -> 871 bytes
public/images/wavatars/eyes_12.png | Bin 0 -> 860 bytes
public/images/wavatars/eyes_13.png | Bin 0 -> 929 bytes
public/images/wavatars/eyes_2.png | Bin 0 -> 502 bytes
public/images/wavatars/eyes_3.png | Bin 0 -> 883 bytes
public/images/wavatars/eyes_4.png | Bin 0 -> 1235 bytes
public/images/wavatars/eyes_5.png | Bin 0 -> 903 bytes
public/images/wavatars/eyes_6.png | Bin 0 -> 934 bytes
public/images/wavatars/eyes_7.png | Bin 0 -> 160 bytes
public/images/wavatars/eyes_8.png | Bin 0 -> 715 bytes
public/images/wavatars/eyes_9.png | Bin 0 -> 709 bytes
public/images/wavatars/fade_1.png | Bin 0 -> 501 bytes
public/images/wavatars/fade_2.png | Bin 0 -> 3051 bytes
public/images/wavatars/fade_3.png | Bin 0 -> 1210 bytes
public/images/wavatars/fade_4.png | Bin 0 -> 160 bytes
public/images/wavatars/mask_1.png | Bin 0 -> 1865 bytes
public/images/wavatars/mask_10.png | Bin 0 -> 2422 bytes
public/images/wavatars/mask_11.png | Bin 0 -> 2189 bytes
public/images/wavatars/mask_2.png | Bin 0 -> 759 bytes
public/images/wavatars/mask_3.png | Bin 0 -> 1895 bytes
public/images/wavatars/mask_4.png | Bin 0 -> 1210 bytes
public/images/wavatars/mask_5.png | Bin 0 -> 1138 bytes
public/images/wavatars/mask_6.png | Bin 0 -> 969 bytes
public/images/wavatars/mask_7.png | Bin 0 -> 822 bytes
public/images/wavatars/mask_8.png | Bin 0 -> 1586 bytes
public/images/wavatars/mask_9.png | Bin 0 -> 1445 bytes
public/images/wavatars/mouth_1.png | Bin 0 -> 308 bytes
public/images/wavatars/mouth_10.png | Bin 0 -> 226 bytes
public/images/wavatars/mouth_11.png | Bin 0 -> 962 bytes
public/images/wavatars/mouth_12.png | Bin 0 -> 1028 bytes
public/images/wavatars/mouth_13.png | Bin 0 -> 350 bytes
public/images/wavatars/mouth_14.png | Bin 0 -> 391 bytes
public/images/wavatars/mouth_15.png | Bin 0 -> 1430 bytes
public/images/wavatars/mouth_16.png | Bin 0 -> 314 bytes
public/images/wavatars/mouth_17.png | Bin 0 -> 719 bytes
public/images/wavatars/mouth_18.png | Bin 0 -> 786 bytes
public/images/wavatars/mouth_19.png | Bin 0 -> 571 bytes
public/images/wavatars/mouth_2.png | Bin 0 -> 548 bytes
public/images/wavatars/mouth_3.png | Bin 0 -> 446 bytes
public/images/wavatars/mouth_4.png | Bin 0 -> 457 bytes
public/images/wavatars/mouth_5.png | Bin 0 -> 421 bytes
public/images/wavatars/mouth_6.png | Bin 0 -> 290 bytes
public/images/wavatars/mouth_7.png | Bin 0 -> 188 bytes
public/images/wavatars/mouth_8.png | Bin 0 -> 360 bytes
public/images/wavatars/mouth_9.png | Bin 0 -> 486 bytes
public/images/wavatars/pupils_1.png | Bin 0 -> 529 bytes
public/images/wavatars/pupils_10.png | Bin 0 -> 478 bytes
public/images/wavatars/pupils_11.png | Bin 0 -> 298 bytes
public/images/wavatars/pupils_2.png | Bin 0 -> 516 bytes
public/images/wavatars/pupils_3.png | Bin 0 -> 462 bytes
public/images/wavatars/pupils_4.png | Bin 0 -> 454 bytes
public/images/wavatars/pupils_5.png | Bin 0 -> 397 bytes
public/images/wavatars/pupils_6.png | Bin 0 -> 308 bytes
public/images/wavatars/pupils_7.png | Bin 0 -> 241 bytes
public/images/wavatars/pupils_8.png | Bin 0 -> 309 bytes
public/images/wavatars/pupils_9.png | Bin 0 -> 849 bytes
public/images/wavatars/shine_1.png | Bin 0 -> 5561 bytes
public/images/wavatars/shine_10.png | Bin 0 -> 4153 bytes
public/images/wavatars/shine_11.png | Bin 0 -> 4242 bytes
public/images/wavatars/shine_2.png | Bin 0 -> 3016 bytes
public/images/wavatars/shine_3.png | Bin 0 -> 3772 bytes
public/images/wavatars/shine_4.png | Bin 0 -> 3386 bytes
public/images/wavatars/shine_5.png | Bin 0 -> 3219 bytes
public/images/wavatars/shine_6.png | Bin 0 -> 3464 bytes
public/images/wavatars/shine_7.png | Bin 0 -> 3704 bytes
public/images/wavatars/shine_8.png | Bin 0 -> 3224 bytes
public/images/wavatars/shine_9.png | Bin 0 -> 3261 bytes
public/js/wpdiscuz/src/use-gravatar.js | 31 +
public/js/wpdiscuz/use-gravatar.js | 31 +
public/js/wpdiscuz/use-gravatar.min.js | 2 +
.../bbpress/profile/allow-anonymous.php | 55 ++
.../partials/bbpress/profile/use-gravatar.php | 59 ++
.../bbpress/profile/user-avatar-upload.php | 65 ++
.../partials/bbpress/user-profile-picture.php | 43 ++
public/partials/block/avatar.php | 51 ++
public/partials/block/frontend-form.php | 59 ++
public/partials/comments/use-gravatar.php | 29 +
public/partials/profile/allow-anonymous.php | 55 ++
public/partials/profile/use-gravatar.php | 59 ++
.../partials/profile/user-avatar-upload.php | 65 ++
public/partials/retro/svg.php | 69 ++
public/partials/robohash/svg.php | 50 ++
public/partials/shortcode/avatar-upload.php | 49 ++
.../partials/tml-profiles/allow-anonymous.php | 54 ++
public/partials/tml-profiles/use-gravatar.php | 58 ++
.../tml-profiles/user-avatar-upload.php | 63 ++
public/partials/wpdiscuz/use-gravatar.php | 53 ++
readme.txt | 365 +++++++++
uninstall.php | 63 ++
vendor-scoped/autoload.php | 25 +
vendor-scoped/composer/ClassLoader.php | 585 ++++++++++++++
vendor-scoped/composer/LICENSE | 21 +
vendor-scoped/composer/autoload_classmap.php | 181 +++++
vendor-scoped/composer/autoload_files.php | 12 +
.../composer/autoload_namespaces.php | 9 +
vendor-scoped/composer/autoload_real.php | 49 ++
vendor-scoped/composer/autoload_static.php | 234 ++++++
vendor-scoped/jdenticon/jdenticon/LICENSE | 21 +
vendor-scoped/jdenticon/jdenticon/README.md | 60 ++
.../jdenticon/jdenticon/src/Color.php | 673 ++++++++++++++++
.../jdenticon/jdenticon/src/Identicon.php | 421 ++++++++++
.../jdenticon/src/IdenticonStyle.php | 384 ++++++++++
.../src/Rendering/AbstractRenderer.php | 174 +++++
.../jdenticon/src/Rendering/ColorTheme.php | 83 ++
.../jdenticon/src/Rendering/IconGenerator.php | 257 +++++++
.../jdenticon/src/Rendering/Point.php | 51 ++
.../jdenticon/src/Rendering/Rectangle.php | 56 ++
.../src/Rendering/RendererInterface.php | 128 ++++
.../jdenticon/src/Rendering/SvgPath.php | 64 ++
.../jdenticon/src/Rendering/SvgRenderer.php | 114 +++
.../jdenticon/src/Rendering/Transform.php | 79 ++
.../src/Rendering/TriangleDirection.php | 35 +
.../jdenticon/jdenticon/src/Shapes/Shape.php | 54 ++
.../jdenticon/src/Shapes/ShapeCategory.php | 64 ++
.../jdenticon/src/Shapes/ShapeDefinitions.php | 154 ++++
.../jdenticon/src/Shapes/ShapePosition.php | 42 +
vendor-scoped/level-2/dice/Dice.php | 319 ++++++++
vendor-scoped/level-2/dice/README.md | 365 +++++++++
.../mistic100/randomcolor/src/RandomColor.php | 259 +++++++
.../check-wp-requirements/LICENSE | 339 +++++++++
.../check-wp-requirements/README.md | 59 ++
.../class-wp-requirements.php | 219 ++++++
.../partials/requirements-error-notice.php | 31 +
.../mundschenk-at/wp-data-storage/LICENSE | 339 +++++++++
.../mundschenk-at/wp-data-storage/README.md | 22 +
.../src/class-abstract-cache.php | 112 +++
.../wp-data-storage/src/class-cache.php | 105 +++
.../src/class-network-options.php | 97 +++
.../wp-data-storage/src/class-options.php | 108 +++
.../src/class-site-transients.php | 96 +++
.../wp-data-storage/src/class-transients.php | 183 +++++
.../mundschenk-at/wp-settings-ui/LICENSE | 339 +++++++++
.../mundschenk-at/wp-settings-ui/README.md | 17 +
.../wp-settings-ui/partials/control.php | 65 ++
.../src/ui/class-abstract-control.php | 413 ++++++++++
.../src/ui/class-control-factory.php | 76 ++
.../wp-settings-ui/src/ui/class-control.php | 116 +++
.../src/ui/controls/class-checkbox-input.php | 75 ++
.../src/ui/controls/class-display-text.php | 121 +++
.../src/ui/controls/class-hidden-input.php | 63 ++
.../src/ui/controls/class-input.php | 121 +++
.../src/ui/controls/class-number-input.php | 78 ++
.../src/ui/controls/class-select.php | 152 ++++
.../src/ui/controls/class-submit-input.php | 100 +++
.../src/ui/controls/class-text-input.php | 63 ++
.../src/ui/controls/class-textarea.php | 104 +++
vendor-scoped/splitbrain/php-ringicon/LICENSE | 19 +
.../splitbrain/php-ringicon/README.md | 24 +
.../php-ringicon/src/AbstractRingIcon.php | 69 ++
.../php-ringicon/src/RingIconSVG.php | 145 ++++
602 files changed, 33485 insertions(+)
create mode 100644 admin/blocks/js/blocks.asset.php
create mode 100644 admin/blocks/js/blocks.js
create mode 100644 admin/blocks/src/avatar/block.json
create mode 100644 admin/blocks/src/avatar/edit.js
create mode 100644 admin/blocks/src/avatar/index.js
create mode 100644 admin/blocks/src/blocks.js
create mode 100644 admin/blocks/src/frontend-form/block.json
create mode 100644 admin/blocks/src/frontend-form/edit.js
create mode 100644 admin/blocks/src/frontend-form/index.js
create mode 100644 admin/css/blocks.css
create mode 100644 admin/css/blocks.min.css
create mode 100644 admin/css/settings.css
create mode 100644 admin/css/settings.min.css
create mode 100644 admin/partials/network/section.php
create mode 100644 admin/partials/network/settings-page.php
create mode 100644 admin/partials/profile/allow-anonymous.php
create mode 100644 admin/partials/profile/use-gravatar.php
create mode 100644 admin/partials/profile/user-avatar-upload.php
create mode 100644 admin/partials/sections/avatars-disabled-script.php
create mode 100644 admin/partials/sections/avatars-disabled.php
create mode 100644 admin/partials/sections/avatars-enabled.php
create mode 100644 avatar-privacy.php
create mode 100644 includes/avatar-privacy-functions.php
create mode 100644 includes/avatar-privacy/avatar-handlers/class-avatar-handler.php
create mode 100644 includes/avatar-privacy/avatar-handlers/class-default-icons-handler.php
create mode 100644 includes/avatar-privacy/avatar-handlers/class-gravatar-cache-handler.php
create mode 100644 includes/avatar-privacy/avatar-handlers/class-legacy-icon-handler.php
create mode 100644 includes/avatar-privacy/avatar-handlers/class-user-avatar-handler.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/class-abstract-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/class-custom-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/class-generating-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/class-generator.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/class-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/class-static-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/class-svg-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-bird-avatar-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-cat-avatar-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-identicon-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-monster-id-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-retro-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-rings-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-robohash-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-wavatar-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generators/class-bird-avatar.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generators/class-cat-avatar.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generators/class-jdenticon.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generators/class-monster-id.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generators/class-parts-generator.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generators/class-png-parts-generator.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generators/class-retro.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generators/class-rings.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generators/class-robohash.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/generators/class-wavatar.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-bowling-pin-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-mystery-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-silhouette-icon-provider.php
create mode 100644 includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-speech-bubble-icon-provider.php
create mode 100644 includes/avatar-privacy/class-component.php
create mode 100644 includes/avatar-privacy/class-controller.php
create mode 100644 includes/avatar-privacy/class-core.php
create mode 100644 includes/avatar-privacy/class-factory.php
create mode 100644 includes/avatar-privacy/class-requirements.php
create mode 100644 includes/avatar-privacy/cli/class-abstract-command.php
create mode 100644 includes/avatar-privacy/cli/class-command.php
create mode 100644 includes/avatar-privacy/cli/class-cron-command.php
create mode 100644 includes/avatar-privacy/cli/class-database-command.php
create mode 100644 includes/avatar-privacy/cli/class-default-command.php
create mode 100644 includes/avatar-privacy/cli/class-uninstall-command.php
create mode 100644 includes/avatar-privacy/cli/class-user-command.php
create mode 100644 includes/avatar-privacy/components/class-avatar-handling.php
create mode 100644 includes/avatar-privacy/components/class-block-editor.php
create mode 100644 includes/avatar-privacy/components/class-command-line-interface.php
create mode 100644 includes/avatar-privacy/components/class-comments.php
create mode 100644 includes/avatar-privacy/components/class-image-proxy.php
create mode 100644 includes/avatar-privacy/components/class-integrations.php
create mode 100644 includes/avatar-privacy/components/class-network-settings-page.php
create mode 100644 includes/avatar-privacy/components/class-privacy-tools.php
create mode 100644 includes/avatar-privacy/components/class-settings-page.php
create mode 100644 includes/avatar-privacy/components/class-setup.php
create mode 100644 includes/avatar-privacy/components/class-shortcodes.php
create mode 100644 includes/avatar-privacy/components/class-uninstallation.php
create mode 100644 includes/avatar-privacy/components/class-user-profile.php
create mode 100644 includes/avatar-privacy/core/class-api.php
create mode 100644 includes/avatar-privacy/core/class-comment-author-fields.php
create mode 100644 includes/avatar-privacy/core/class-default-avatars.php
create mode 100644 includes/avatar-privacy/core/class-settings.php
create mode 100644 includes/avatar-privacy/core/class-user-fields.php
create mode 100644 includes/avatar-privacy/data-storage/class-cache.php
create mode 100644 includes/avatar-privacy/data-storage/class-filesystem-cache.php
create mode 100644 includes/avatar-privacy/data-storage/class-network-options.php
create mode 100644 includes/avatar-privacy/data-storage/class-options.php
create mode 100644 includes/avatar-privacy/data-storage/class-site-transients.php
create mode 100644 includes/avatar-privacy/data-storage/class-transients.php
create mode 100644 includes/avatar-privacy/data-storage/database/class-comment-author-table.php
create mode 100644 includes/avatar-privacy/data-storage/database/class-hashes-table.php
create mode 100644 includes/avatar-privacy/data-storage/database/class-table.php
create mode 100644 includes/avatar-privacy/exceptions/class-avatar-comment-type-exception.php
create mode 100644 includes/avatar-privacy/exceptions/class-database-exception.php
create mode 100644 includes/avatar-privacy/exceptions/class-file-deletion-exception.php
create mode 100644 includes/avatar-privacy/exceptions/class-filesystem-exception.php
create mode 100644 includes/avatar-privacy/exceptions/class-form-field-not-found-exception.php
create mode 100644 includes/avatar-privacy/exceptions/class-invalid-nonce-exception.php
create mode 100644 includes/avatar-privacy/exceptions/class-object-factory-exception.php
create mode 100644 includes/avatar-privacy/exceptions/class-part-files-not-found-exception.php
create mode 100644 includes/avatar-privacy/exceptions/class-png-image-exception.php
create mode 100644 includes/avatar-privacy/exceptions/class-upload-handling-exception.php
create mode 100644 includes/avatar-privacy/functions.php
create mode 100644 includes/avatar-privacy/integrations/class-bbpress-integration.php
create mode 100644 includes/avatar-privacy/integrations/class-buddypress-integration.php
create mode 100644 includes/avatar-privacy/integrations/class-plugin-integration.php
create mode 100644 includes/avatar-privacy/integrations/class-simple-author-box-integration.php
create mode 100644 includes/avatar-privacy/integrations/class-simple-local-avatars-integration.php
create mode 100644 includes/avatar-privacy/integrations/class-simple-user-avatar-integration.php
create mode 100644 includes/avatar-privacy/integrations/class-theme-my-login-profiles-integration.php
create mode 100644 includes/avatar-privacy/integrations/class-ultimate-member-integration.php
create mode 100644 includes/avatar-privacy/integrations/class-wp-user-manager-integration.php
create mode 100644 includes/avatar-privacy/integrations/class-wpdiscuz-integration.php
create mode 100644 includes/avatar-privacy/tools/class-hasher.php
create mode 100644 includes/avatar-privacy/tools/class-multisite.php
create mode 100644 includes/avatar-privacy/tools/class-number-generator.php
create mode 100644 includes/avatar-privacy/tools/class-template.php
create mode 100644 includes/avatar-privacy/tools/functions.php
create mode 100644 includes/avatar-privacy/tools/html/class-dependencies.php
create mode 100644 includes/avatar-privacy/tools/html/class-user-form.php
create mode 100644 includes/avatar-privacy/tools/images/class-color.php
create mode 100644 includes/avatar-privacy/tools/images/class-editor.php
create mode 100644 includes/avatar-privacy/tools/images/class-image-file.php
create mode 100644 includes/avatar-privacy/tools/images/class-image-stream.php
create mode 100644 includes/avatar-privacy/tools/images/class-png.php
create mode 100644 includes/avatar-privacy/tools/images/class-svg.php
create mode 100644 includes/avatar-privacy/tools/images/class-type.php
create mode 100644 includes/avatar-privacy/tools/network/class-gravatar-service.php
create mode 100644 includes/avatar-privacy/tools/network/class-remote-image-service.php
create mode 100644 includes/avatar-privacy/upload-handlers/class-custom-default-icon-upload-handler.php
create mode 100644 includes/avatar-privacy/upload-handlers/class-upload-handler.php
create mode 100644 includes/avatar-privacy/upload-handlers/class-user-avatar-upload-handler.php
create mode 100644 includes/avatar-privacy/upload-handlers/ui/class-file-upload-input.php
create mode 100644 public/images/LICENSE.md
create mode 100644 public/images/birds/accessoire_1.png
create mode 100644 public/images/birds/accessoire_10.png
create mode 100644 public/images/birds/accessoire_11.png
create mode 100644 public/images/birds/accessoire_12.png
create mode 100644 public/images/birds/accessoire_13.png
create mode 100644 public/images/birds/accessoire_14.png
create mode 100644 public/images/birds/accessoire_15.png
create mode 100644 public/images/birds/accessoire_16.png
create mode 100644 public/images/birds/accessoire_17.png
create mode 100644 public/images/birds/accessoire_18.png
create mode 100644 public/images/birds/accessoire_19.png
create mode 100644 public/images/birds/accessoire_2.png
create mode 100644 public/images/birds/accessoire_20.png
create mode 100644 public/images/birds/accessoire_3.png
create mode 100644 public/images/birds/accessoire_4.png
create mode 100644 public/images/birds/accessoire_5.png
create mode 100644 public/images/birds/accessoire_6.png
create mode 100644 public/images/birds/accessoire_7.png
create mode 100644 public/images/birds/accessoire_8.png
create mode 100644 public/images/birds/accessoire_9.png
create mode 100644 public/images/birds/beak_1.png
create mode 100644 public/images/birds/beak_2.png
create mode 100644 public/images/birds/beak_3.png
create mode 100644 public/images/birds/beak_4.png
create mode 100644 public/images/birds/beak_5.png
create mode 100644 public/images/birds/beak_6.png
create mode 100644 public/images/birds/beak_7.png
create mode 100644 public/images/birds/beak_8.png
create mode 100644 public/images/birds/beak_9.png
create mode 100644 public/images/birds/body_1.png
create mode 100644 public/images/birds/body_2.png
create mode 100644 public/images/birds/body_3.png
create mode 100644 public/images/birds/body_4.png
create mode 100644 public/images/birds/body_5.png
create mode 100644 public/images/birds/body_6.png
create mode 100644 public/images/birds/body_7.png
create mode 100644 public/images/birds/body_8.png
create mode 100644 public/images/birds/body_9.png
create mode 100644 public/images/birds/eyes_1.png
create mode 100644 public/images/birds/eyes_2.png
create mode 100644 public/images/birds/eyes_3.png
create mode 100644 public/images/birds/eyes_4.png
create mode 100644 public/images/birds/eyes_5.png
create mode 100644 public/images/birds/eyes_6.png
create mode 100644 public/images/birds/eyes_7.png
create mode 100644 public/images/birds/eyes_8.png
create mode 100644 public/images/birds/eyes_9.png
create mode 100644 public/images/birds/guide.png
create mode 100644 public/images/birds/hoop_1.png
create mode 100644 public/images/birds/hoop_10.png
create mode 100644 public/images/birds/hoop_2.png
create mode 100644 public/images/birds/hoop_3.png
create mode 100644 public/images/birds/hoop_4.png
create mode 100644 public/images/birds/hoop_5.png
create mode 100644 public/images/birds/hoop_6.png
create mode 100644 public/images/birds/hoop_7.png
create mode 100644 public/images/birds/hoop_8.png
create mode 100644 public/images/birds/hoop_9.png
create mode 100644 public/images/birds/tail_1.png
create mode 100644 public/images/birds/tail_2.png
create mode 100644 public/images/birds/tail_3.png
create mode 100644 public/images/birds/tail_4.png
create mode 100644 public/images/birds/tail_5.png
create mode 100644 public/images/birds/tail_6.png
create mode 100644 public/images/birds/tail_7.png
create mode 100644 public/images/birds/tail_8.png
create mode 100644 public/images/birds/tail_9.png
create mode 100644 public/images/birds/wing_1.png
create mode 100644 public/images/birds/wing_2.png
create mode 100644 public/images/birds/wing_3.png
create mode 100644 public/images/birds/wing_4.png
create mode 100644 public/images/birds/wing_5.png
create mode 100644 public/images/birds/wing_6.png
create mode 100644 public/images/birds/wing_7.png
create mode 100644 public/images/birds/wing_8.png
create mode 100644 public/images/birds/wing_9.png
create mode 100644 public/images/cats/accessoire_1.png
create mode 100644 public/images/cats/accessoire_10.png
create mode 100644 public/images/cats/accessoire_11.png
create mode 100644 public/images/cats/accessoire_12.png
create mode 100644 public/images/cats/accessoire_13.png
create mode 100644 public/images/cats/accessoire_14.png
create mode 100644 public/images/cats/accessoire_15.png
create mode 100644 public/images/cats/accessoire_16.png
create mode 100644 public/images/cats/accessoire_17.png
create mode 100644 public/images/cats/accessoire_18.png
create mode 100644 public/images/cats/accessoire_19.png
create mode 100644 public/images/cats/accessoire_2.png
create mode 100644 public/images/cats/accessoire_20.png
create mode 100644 public/images/cats/accessoire_3.png
create mode 100644 public/images/cats/accessoire_4.png
create mode 100644 public/images/cats/accessoire_5.png
create mode 100644 public/images/cats/accessoire_6.png
create mode 100644 public/images/cats/accessoire_7.png
create mode 100644 public/images/cats/accessoire_8.png
create mode 100644 public/images/cats/accessoire_9.png
create mode 100644 public/images/cats/body_1.png
create mode 100644 public/images/cats/body_10.png
create mode 100644 public/images/cats/body_11.png
create mode 100644 public/images/cats/body_12.png
create mode 100644 public/images/cats/body_13.png
create mode 100644 public/images/cats/body_14.png
create mode 100644 public/images/cats/body_15.png
create mode 100644 public/images/cats/body_2.png
create mode 100644 public/images/cats/body_3.png
create mode 100644 public/images/cats/body_4.png
create mode 100644 public/images/cats/body_5.png
create mode 100644 public/images/cats/body_6.png
create mode 100644 public/images/cats/body_7.png
create mode 100644 public/images/cats/body_8.png
create mode 100644 public/images/cats/body_9.png
create mode 100644 public/images/cats/eyes_1.png
create mode 100644 public/images/cats/eyes_10.png
create mode 100644 public/images/cats/eyes_11.png
create mode 100644 public/images/cats/eyes_12.png
create mode 100644 public/images/cats/eyes_13.png
create mode 100644 public/images/cats/eyes_14.png
create mode 100644 public/images/cats/eyes_15.png
create mode 100644 public/images/cats/eyes_2.png
create mode 100644 public/images/cats/eyes_3.png
create mode 100644 public/images/cats/eyes_4.png
create mode 100644 public/images/cats/eyes_5.png
create mode 100644 public/images/cats/eyes_6.png
create mode 100644 public/images/cats/eyes_7.png
create mode 100644 public/images/cats/eyes_8.png
create mode 100644 public/images/cats/eyes_9.png
create mode 100644 public/images/cats/fur_1.png
create mode 100644 public/images/cats/fur_10.png
create mode 100644 public/images/cats/fur_2.png
create mode 100644 public/images/cats/fur_3.png
create mode 100644 public/images/cats/fur_4.png
create mode 100644 public/images/cats/fur_5.png
create mode 100644 public/images/cats/fur_6.png
create mode 100644 public/images/cats/fur_7.png
create mode 100644 public/images/cats/fur_8.png
create mode 100644 public/images/cats/fur_9.png
create mode 100644 public/images/cats/mouth_1.png
create mode 100644 public/images/cats/mouth_10.png
create mode 100644 public/images/cats/mouth_2.png
create mode 100644 public/images/cats/mouth_3.png
create mode 100644 public/images/cats/mouth_4.png
create mode 100644 public/images/cats/mouth_5.png
create mode 100644 public/images/cats/mouth_6.png
create mode 100644 public/images/cats/mouth_7.png
create mode 100644 public/images/cats/mouth_8.png
create mode 100644 public/images/cats/mouth_9.png
create mode 100644 public/images/cats/zz_1.png
create mode 100644 public/images/cats/zz_2.png
create mode 100644 public/images/comment-bubble.svg
create mode 100644 public/images/monster-id/arms_1.png
create mode 100644 public/images/monster-id/arms_2.png
create mode 100644 public/images/monster-id/arms_3.png
create mode 100644 public/images/monster-id/arms_4.png
create mode 100644 public/images/monster-id/arms_5.png
create mode 100644 public/images/monster-id/arms_S1.png
create mode 100644 public/images/monster-id/arms_S2.png
create mode 100644 public/images/monster-id/arms_S3.png
create mode 100644 public/images/monster-id/arms_S4.png
create mode 100644 public/images/monster-id/arms_S5.png
create mode 100644 public/images/monster-id/arms_S6.png
create mode 100644 public/images/monster-id/arms_S7.png
create mode 100644 public/images/monster-id/arms_S8.png
create mode 100644 public/images/monster-id/arms_S9.png
create mode 100644 public/images/monster-id/back.png
create mode 100644 public/images/monster-id/body_1.png
create mode 100644 public/images/monster-id/body_10.png
create mode 100644 public/images/monster-id/body_11.png
create mode 100644 public/images/monster-id/body_12.png
create mode 100644 public/images/monster-id/body_13.png
create mode 100644 public/images/monster-id/body_14.png
create mode 100644 public/images/monster-id/body_15.png
create mode 100644 public/images/monster-id/body_2.png
create mode 100644 public/images/monster-id/body_3.png
create mode 100644 public/images/monster-id/body_4.png
create mode 100644 public/images/monster-id/body_5.png
create mode 100644 public/images/monster-id/body_6.png
create mode 100644 public/images/monster-id/body_7.png
create mode 100644 public/images/monster-id/body_8.png
create mode 100644 public/images/monster-id/body_9.png
create mode 100644 public/images/monster-id/body_S1.png
create mode 100644 public/images/monster-id/body_S2.png
create mode 100644 public/images/monster-id/body_S3.png
create mode 100644 public/images/monster-id/body_S4.png
create mode 100644 public/images/monster-id/body_S5.png
create mode 100644 public/images/monster-id/eyes_1.png
create mode 100644 public/images/monster-id/eyes_10.png
create mode 100644 public/images/monster-id/eyes_11.png
create mode 100644 public/images/monster-id/eyes_12.png
create mode 100644 public/images/monster-id/eyes_13.png
create mode 100644 public/images/monster-id/eyes_14.png
create mode 100644 public/images/monster-id/eyes_15.png
create mode 100644 public/images/monster-id/eyes_2.png
create mode 100644 public/images/monster-id/eyes_3.png
create mode 100644 public/images/monster-id/eyes_4.png
create mode 100644 public/images/monster-id/eyes_5.png
create mode 100644 public/images/monster-id/eyes_6.png
create mode 100644 public/images/monster-id/eyes_7.png
create mode 100644 public/images/monster-id/eyes_8.png
create mode 100644 public/images/monster-id/eyes_9.png
create mode 100644 public/images/monster-id/eyes_S1.png
create mode 100644 public/images/monster-id/eyes_S2.png
create mode 100644 public/images/monster-id/eyes_S3.png
create mode 100644 public/images/monster-id/eyes_S4.png
create mode 100644 public/images/monster-id/eyes_S5.png
create mode 100644 public/images/monster-id/hair_1.png
create mode 100644 public/images/monster-id/hair_2.png
create mode 100644 public/images/monster-id/hair_3.png
create mode 100644 public/images/monster-id/hair_4.png
create mode 100644 public/images/monster-id/hair_5.png
create mode 100644 public/images/monster-id/hair_S1.png
create mode 100644 public/images/monster-id/hair_S2.png
create mode 100644 public/images/monster-id/hair_S3.png
create mode 100644 public/images/monster-id/hair_S4.png
create mode 100644 public/images/monster-id/hair_S5.png
create mode 100644 public/images/monster-id/hair_S6.png
create mode 100644 public/images/monster-id/hair_S7.png
create mode 100644 public/images/monster-id/legs_1.png
create mode 100644 public/images/monster-id/legs_2.png
create mode 100644 public/images/monster-id/legs_3.png
create mode 100644 public/images/monster-id/legs_4.png
create mode 100644 public/images/monster-id/legs_5.png
create mode 100644 public/images/monster-id/legs_S1.png
create mode 100644 public/images/monster-id/legs_S10.png
create mode 100644 public/images/monster-id/legs_S11.png
create mode 100644 public/images/monster-id/legs_S12.png
create mode 100644 public/images/monster-id/legs_S13.png
create mode 100644 public/images/monster-id/legs_S2.png
create mode 100644 public/images/monster-id/legs_S3.png
create mode 100644 public/images/monster-id/legs_S4.png
create mode 100644 public/images/monster-id/legs_S5.png
create mode 100644 public/images/monster-id/legs_S6.png
create mode 100644 public/images/monster-id/legs_S7.png
create mode 100644 public/images/monster-id/legs_S8.png
create mode 100644 public/images/monster-id/legs_S9.png
create mode 100644 public/images/monster-id/mouth_1.png
create mode 100644 public/images/monster-id/mouth_10.png
create mode 100644 public/images/monster-id/mouth_2.png
create mode 100644 public/images/monster-id/mouth_3.png
create mode 100644 public/images/monster-id/mouth_4.png
create mode 100644 public/images/monster-id/mouth_5.png
create mode 100644 public/images/monster-id/mouth_6.png
create mode 100644 public/images/monster-id/mouth_7.png
create mode 100644 public/images/monster-id/mouth_8.png
create mode 100644 public/images/monster-id/mouth_9.png
create mode 100644 public/images/monster-id/mouth_S1.png
create mode 100644 public/images/monster-id/mouth_S2.png
create mode 100644 public/images/monster-id/mouth_S3.png
create mode 100644 public/images/monster-id/mouth_S4.png
create mode 100644 public/images/monster-id/mouth_S5.png
create mode 100644 public/images/monster-id/mouth_S6.png
create mode 100644 public/images/monster-id/mouth_S7.png
create mode 100644 public/images/mystery.svg
create mode 100644 public/images/robohash/accessory/accessory-01.svg
create mode 100644 public/images/robohash/accessory/accessory-02.svg
create mode 100644 public/images/robohash/accessory/accessory-03.svg
create mode 100644 public/images/robohash/accessory/accessory-04.svg
create mode 100644 public/images/robohash/accessory/accessory-05.svg
create mode 100644 public/images/robohash/accessory/accessory-06.svg
create mode 100644 public/images/robohash/accessory/accessory-07.svg
create mode 100644 public/images/robohash/accessory/accessory-08.svg
create mode 100644 public/images/robohash/accessory/accessory-09.svg
create mode 100644 public/images/robohash/accessory/accessory-10.svg
create mode 100644 public/images/robohash/body/body-01.svg
create mode 100644 public/images/robohash/body/body-02.svg
create mode 100644 public/images/robohash/body/body-03.svg
create mode 100644 public/images/robohash/body/body-04.svg
create mode 100644 public/images/robohash/body/body-05.svg
create mode 100644 public/images/robohash/body/body-06.svg
create mode 100644 public/images/robohash/body/body-07.svg
create mode 100644 public/images/robohash/body/body-08.svg
create mode 100644 public/images/robohash/body/body-09.svg
create mode 100644 public/images/robohash/body/body-10.svg
create mode 100644 public/images/robohash/eyes/eyes-01.svg
create mode 100644 public/images/robohash/eyes/eyes-02.svg
create mode 100644 public/images/robohash/eyes/eyes-03.svg
create mode 100644 public/images/robohash/eyes/eyes-04.svg
create mode 100644 public/images/robohash/eyes/eyes-05.svg
create mode 100644 public/images/robohash/eyes/eyes-06.svg
create mode 100644 public/images/robohash/eyes/eyes-07.svg
create mode 100644 public/images/robohash/eyes/eyes-08.svg
create mode 100644 public/images/robohash/eyes/eyes-09.svg
create mode 100644 public/images/robohash/eyes/eyes-10.svg
create mode 100644 public/images/robohash/face/face-01.svg
create mode 100644 public/images/robohash/face/face-02.svg
create mode 100644 public/images/robohash/face/face-03.svg
create mode 100644 public/images/robohash/face/face-04.svg
create mode 100644 public/images/robohash/face/face-05.svg
create mode 100644 public/images/robohash/face/face-06.svg
create mode 100644 public/images/robohash/face/face-07.svg
create mode 100644 public/images/robohash/face/face-08.svg
create mode 100644 public/images/robohash/face/face-09.svg
create mode 100644 public/images/robohash/face/face-10.svg
create mode 100644 public/images/robohash/mouth/mouth-01.svg
create mode 100644 public/images/robohash/mouth/mouth-02.svg
create mode 100644 public/images/robohash/mouth/mouth-03.svg
create mode 100644 public/images/robohash/mouth/mouth-04.svg
create mode 100644 public/images/robohash/mouth/mouth-05.svg
create mode 100644 public/images/robohash/mouth/mouth-06.svg
create mode 100644 public/images/robohash/mouth/mouth-07.svg
create mode 100644 public/images/robohash/mouth/mouth-08.svg
create mode 100644 public/images/robohash/mouth/mouth-09.svg
create mode 100644 public/images/robohash/mouth/mouth-10.svg
create mode 100644 public/images/shaded-cone.svg
create mode 100644 public/images/silhouette.svg
create mode 100644 public/images/wavatars/brow_1.png
create mode 100644 public/images/wavatars/brow_2.png
create mode 100644 public/images/wavatars/brow_3.png
create mode 100644 public/images/wavatars/brow_4.png
create mode 100644 public/images/wavatars/brow_5.png
create mode 100644 public/images/wavatars/brow_6.png
create mode 100644 public/images/wavatars/brow_7.png
create mode 100644 public/images/wavatars/brow_8.png
create mode 100644 public/images/wavatars/eyes_1.png
create mode 100644 public/images/wavatars/eyes_10.png
create mode 100644 public/images/wavatars/eyes_11.png
create mode 100644 public/images/wavatars/eyes_12.png
create mode 100644 public/images/wavatars/eyes_13.png
create mode 100644 public/images/wavatars/eyes_2.png
create mode 100644 public/images/wavatars/eyes_3.png
create mode 100644 public/images/wavatars/eyes_4.png
create mode 100644 public/images/wavatars/eyes_5.png
create mode 100644 public/images/wavatars/eyes_6.png
create mode 100644 public/images/wavatars/eyes_7.png
create mode 100644 public/images/wavatars/eyes_8.png
create mode 100644 public/images/wavatars/eyes_9.png
create mode 100644 public/images/wavatars/fade_1.png
create mode 100644 public/images/wavatars/fade_2.png
create mode 100644 public/images/wavatars/fade_3.png
create mode 100644 public/images/wavatars/fade_4.png
create mode 100644 public/images/wavatars/mask_1.png
create mode 100644 public/images/wavatars/mask_10.png
create mode 100644 public/images/wavatars/mask_11.png
create mode 100644 public/images/wavatars/mask_2.png
create mode 100644 public/images/wavatars/mask_3.png
create mode 100644 public/images/wavatars/mask_4.png
create mode 100644 public/images/wavatars/mask_5.png
create mode 100644 public/images/wavatars/mask_6.png
create mode 100644 public/images/wavatars/mask_7.png
create mode 100644 public/images/wavatars/mask_8.png
create mode 100644 public/images/wavatars/mask_9.png
create mode 100644 public/images/wavatars/mouth_1.png
create mode 100644 public/images/wavatars/mouth_10.png
create mode 100644 public/images/wavatars/mouth_11.png
create mode 100644 public/images/wavatars/mouth_12.png
create mode 100644 public/images/wavatars/mouth_13.png
create mode 100644 public/images/wavatars/mouth_14.png
create mode 100644 public/images/wavatars/mouth_15.png
create mode 100644 public/images/wavatars/mouth_16.png
create mode 100644 public/images/wavatars/mouth_17.png
create mode 100644 public/images/wavatars/mouth_18.png
create mode 100644 public/images/wavatars/mouth_19.png
create mode 100644 public/images/wavatars/mouth_2.png
create mode 100644 public/images/wavatars/mouth_3.png
create mode 100644 public/images/wavatars/mouth_4.png
create mode 100644 public/images/wavatars/mouth_5.png
create mode 100644 public/images/wavatars/mouth_6.png
create mode 100644 public/images/wavatars/mouth_7.png
create mode 100644 public/images/wavatars/mouth_8.png
create mode 100644 public/images/wavatars/mouth_9.png
create mode 100644 public/images/wavatars/pupils_1.png
create mode 100644 public/images/wavatars/pupils_10.png
create mode 100644 public/images/wavatars/pupils_11.png
create mode 100644 public/images/wavatars/pupils_2.png
create mode 100644 public/images/wavatars/pupils_3.png
create mode 100644 public/images/wavatars/pupils_4.png
create mode 100644 public/images/wavatars/pupils_5.png
create mode 100644 public/images/wavatars/pupils_6.png
create mode 100644 public/images/wavatars/pupils_7.png
create mode 100644 public/images/wavatars/pupils_8.png
create mode 100644 public/images/wavatars/pupils_9.png
create mode 100644 public/images/wavatars/shine_1.png
create mode 100644 public/images/wavatars/shine_10.png
create mode 100644 public/images/wavatars/shine_11.png
create mode 100644 public/images/wavatars/shine_2.png
create mode 100644 public/images/wavatars/shine_3.png
create mode 100644 public/images/wavatars/shine_4.png
create mode 100644 public/images/wavatars/shine_5.png
create mode 100644 public/images/wavatars/shine_6.png
create mode 100644 public/images/wavatars/shine_7.png
create mode 100644 public/images/wavatars/shine_8.png
create mode 100644 public/images/wavatars/shine_9.png
create mode 100644 public/js/wpdiscuz/src/use-gravatar.js
create mode 100644 public/js/wpdiscuz/use-gravatar.js
create mode 100644 public/js/wpdiscuz/use-gravatar.min.js
create mode 100644 public/partials/bbpress/profile/allow-anonymous.php
create mode 100644 public/partials/bbpress/profile/use-gravatar.php
create mode 100644 public/partials/bbpress/profile/user-avatar-upload.php
create mode 100644 public/partials/bbpress/user-profile-picture.php
create mode 100644 public/partials/block/avatar.php
create mode 100644 public/partials/block/frontend-form.php
create mode 100644 public/partials/comments/use-gravatar.php
create mode 100644 public/partials/profile/allow-anonymous.php
create mode 100644 public/partials/profile/use-gravatar.php
create mode 100644 public/partials/profile/user-avatar-upload.php
create mode 100644 public/partials/retro/svg.php
create mode 100644 public/partials/robohash/svg.php
create mode 100644 public/partials/shortcode/avatar-upload.php
create mode 100644 public/partials/tml-profiles/allow-anonymous.php
create mode 100644 public/partials/tml-profiles/use-gravatar.php
create mode 100644 public/partials/tml-profiles/user-avatar-upload.php
create mode 100644 public/partials/wpdiscuz/use-gravatar.php
create mode 100644 readme.txt
create mode 100644 uninstall.php
create mode 100644 vendor-scoped/autoload.php
create mode 100644 vendor-scoped/composer/ClassLoader.php
create mode 100644 vendor-scoped/composer/LICENSE
create mode 100644 vendor-scoped/composer/autoload_classmap.php
create mode 100644 vendor-scoped/composer/autoload_files.php
create mode 100644 vendor-scoped/composer/autoload_namespaces.php
create mode 100644 vendor-scoped/composer/autoload_real.php
create mode 100644 vendor-scoped/composer/autoload_static.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/LICENSE
create mode 100644 vendor-scoped/jdenticon/jdenticon/README.md
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Color.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Identicon.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/IdenticonStyle.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Rendering/AbstractRenderer.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Rendering/ColorTheme.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Rendering/IconGenerator.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Rendering/Point.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Rendering/Rectangle.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Rendering/RendererInterface.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Rendering/SvgPath.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Rendering/SvgRenderer.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Rendering/Transform.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Rendering/TriangleDirection.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Shapes/Shape.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Shapes/ShapeCategory.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Shapes/ShapeDefinitions.php
create mode 100644 vendor-scoped/jdenticon/jdenticon/src/Shapes/ShapePosition.php
create mode 100644 vendor-scoped/level-2/dice/Dice.php
create mode 100644 vendor-scoped/level-2/dice/README.md
create mode 100644 vendor-scoped/mistic100/randomcolor/src/RandomColor.php
create mode 100644 vendor-scoped/mundschenk-at/check-wp-requirements/LICENSE
create mode 100644 vendor-scoped/mundschenk-at/check-wp-requirements/README.md
create mode 100644 vendor-scoped/mundschenk-at/check-wp-requirements/class-wp-requirements.php
create mode 100644 vendor-scoped/mundschenk-at/check-wp-requirements/partials/requirements-error-notice.php
create mode 100644 vendor-scoped/mundschenk-at/wp-data-storage/LICENSE
create mode 100644 vendor-scoped/mundschenk-at/wp-data-storage/README.md
create mode 100644 vendor-scoped/mundschenk-at/wp-data-storage/src/class-abstract-cache.php
create mode 100644 vendor-scoped/mundschenk-at/wp-data-storage/src/class-cache.php
create mode 100644 vendor-scoped/mundschenk-at/wp-data-storage/src/class-network-options.php
create mode 100644 vendor-scoped/mundschenk-at/wp-data-storage/src/class-options.php
create mode 100644 vendor-scoped/mundschenk-at/wp-data-storage/src/class-site-transients.php
create mode 100644 vendor-scoped/mundschenk-at/wp-data-storage/src/class-transients.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/LICENSE
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/README.md
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/partials/control.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/class-abstract-control.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/class-control-factory.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/class-control.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-checkbox-input.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-display-text.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-hidden-input.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-input.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-number-input.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-select.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-submit-input.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-text-input.php
create mode 100644 vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-textarea.php
create mode 100644 vendor-scoped/splitbrain/php-ringicon/LICENSE
create mode 100644 vendor-scoped/splitbrain/php-ringicon/README.md
create mode 100644 vendor-scoped/splitbrain/php-ringicon/src/AbstractRingIcon.php
create mode 100644 vendor-scoped/splitbrain/php-ringicon/src/RingIconSVG.php
diff --git a/admin/blocks/js/blocks.asset.php b/admin/blocks/js/blocks.asset.php
new file mode 100644
index 0000000..3fb3332
--- /dev/null
+++ b/admin/blocks/js/blocks.asset.php
@@ -0,0 +1 @@
+ array('wp-blocks', 'wp-components', 'wp-data', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-server-side-render'), 'version' => '2226b124181394cb78e7');
diff --git a/admin/blocks/js/blocks.js b/admin/blocks/js/blocks.js
new file mode 100644
index 0000000..74b8b16
--- /dev/null
+++ b/admin/blocks/js/blocks.js
@@ -0,0 +1,2 @@
+!function(){"use strict";var e={n:function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,{a:r}),r},d:function(t,r){for(var a in r)e.o(r,a)&&!e.o(t,a)&&Object.defineProperty(t,a,{enumerable:!0,get:r[a]})},o:function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r:function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{metadata:function(){return v},name:function(){return f},settings:function(){return m}});var r={};function a(e){return a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a(e)}function n(e,t,r){return(t=function(e){var t=function(e,t){if("object"!==a(e)||null===e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var n=r.call(e,"string");if("object"!==a(n))return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}(e);return"symbol"===a(t)?t:String(t)}(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}e.r(r),e.d(r,{metadata:function(){return y},name:function(){return b},settings:function(){return _}});var i=window.wp.blocks,o=window.wp.i18n,u=window.wp.element,l=window.wp.components,c=window.wp.serverSideRender,s=e.n(c),p=window.wp.editor,v=JSON.parse('{"name":"avatar-privacy/form","title":"Avatar Privacy Form","category":"common","icon":"id-alt","description":"Inserts a form to upload a local avatar and manage related settings.","keywords":["avatar","upload","frontend"],"textdomain":"avatar-privacy","attributes":{"avatar_size":{"type":"integer","default":96},"show_descriptions":{"type":"boolean","default":true}}}'),f=v.name,m={title:(0,o.__)("Avatar Privacy Form","avatar-privacy"),supports:{html:!1,multiple:!1,reusable:!1},edit:function(e){var t=e.attributes,r=e.className,a=e.setAttributes;return(0,u.createElement)(u.Fragment,null,(0,u.createElement)(p.InspectorControls,null,(0,u.createElement)(l.PanelBody,{title:(0,o.__)("Form","avatar-privacy")},(0,u.createElement)(l.PanelRow,null,(0,u.createElement)(l.RangeControl,{label:(0,o.__)("Avatar Size","avatar-privacy"),value:t.avatar_size,initialPosition:t.avatar_size,onChange:function(e){return a({avatar_size:e})},min:48,max:240})),(0,u.createElement)(l.PanelRow,null,(0,u.createElement)(l.ToggleControl,{label:(0,o.__)("Show Descriptions","avatar-privacy"),checked:!!t.show_descriptions,onChange:function(){return a({show_descriptions:!t.show_descriptions})}})))),(0,u.createElement)(s(),{block:"avatar-privacy/form",attributes:{avatar_size:t.avatar_size,show_descriptions:t.show_descriptions,className:r,preview:!0}}))},save:function(){}},d=(0,window.wp.data.withSelect)((function(e){return{users:e("core").getAuthors()}}))((function(e){var t=e.attributes,r=e.setAttributes,a=e.users;if(!a||a.length<1)return(0,o.__)("Loading…","avatar-privacy");t.user_id=t.user_id||a[0].id;var n=function(e){return a.find((function(t){return parseInt(e)===t.id}))},i=n(t.user_id);return t.user=t.user||i,(0,u.createElement)(u.Fragment,null,(0,u.createElement)(p.InspectorControls,null,(0,u.createElement)(l.PanelBody,{title:(0,o.__)("Avatar","avatar-privacy")},(0,u.createElement)(l.PanelRow,null,(0,u.createElement)(l.SelectControl,{label:(0,o.__)("User","avatar-privacy"),value:t.user_id,options:a.map((function(e){return{label:e.name,value:e.id}})),onChange:function(e){return r({user_id:parseInt(e),user:n(e)})}})),(0,u.createElement)(l.PanelRow,null,(0,u.createElement)(l.RangeControl,{label:(0,o.__)("Avatar Size","avatar-privacy"),value:t.avatar_size,initialPosition:t.avatar_size,onChange:function(e){return r({avatar_size:e})},min:48,max:240})))),(0,u.createElement)("img",{width:t.avatar_size,src:t.user.avatar_urls[96],alt:(0,o.sprintf)(/* translators: user display name */
+(0,o.__)("Avatar of %s","avatar-privacy"),t.user.name)}))})),y=JSON.parse('{"name":"avatar-privacy/avatar","title":"Avatar","category":"common","icon":"admin-users","description":"Displays a user\'s avatar.","keywords":["avatar","user","icon"],"textdomain":"avatar-privacy","attributes":{"avatar_size":{"type":"integer","default":96},"user_id":{"type":"integer","default":0},"align":{"type":"string","default":""},"user":{"type":"object","source":"attribute","selector":"*","default":null}}}'),b=y.name,_={title:(0,o.__)("Avatar","avatar-privacy"),supports:{align:["left","center","right"],html:!1},edit:d,save:function(){}};function w(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,a)}return r}function g(e){for(var t=1;t
+ * @license GPL-2.0-or-later
+ * @since 2.3.0
+ */
+
+'use strict';
+
+/**
+ * WordPress dependencies
+ */
+import {
+ PanelBody,
+ PanelRow,
+ RangeControl,
+ SelectControl,
+} from '@wordpress/components';
+import { withSelect } from '@wordpress/data';
+import { InspectorControls } from '@wordpress/editor';
+import { Fragment } from '@wordpress/element';
+import { __, sprintf } from '@wordpress/i18n';
+
+/**
+ * Renders the markup for editing the block attributes of the Avatar block.
+ *
+ * @param {Object} props The block properties.
+ * @param {Object} props.attributes The block attributes.
+ * @param {Object} props.setAttributes The attribute setter function.
+ * @return {Object} ECMAScript JSX Markup for the editor
+ */
+export default withSelect(
+ // Retrieve WordPress authors.
+ ( select ) => ( { users: select( 'core' ).getAuthors() } )
+)( ( { attributes, setAttributes, users } ) => {
+ // The authors list has not finished loading yet.
+ if ( ! users || users.length < 1 ) {
+ return __( 'Loading…', 'avatar-privacy' );
+ }
+
+ // Set default for user_id.
+ attributes.user_id = attributes.user_id || users[ 0 ].id;
+
+ // Find the current user object.
+ const findUser = ( userID ) =>
+ users.find( ( user ) => parseInt( userID ) === user.id );
+ const currentUser = findUser( attributes.user_id );
+
+ // Set the user attribute if missing.
+ attributes.user = attributes.user || currentUser;
+
+ return (
+
+
+
+
+ ( {
+ label: user.name,
+ value: user.id,
+ } ) ) }
+ onChange={ ( newUser ) =>
+ setAttributes( {
+ user_id: parseInt( newUser ),
+ user: findUser( newUser ),
+ } )
+ }
+ />
+
+
+
+ setAttributes( { avatar_size: newSize } )
+ }
+ min={ 48 }
+ max={ 240 }
+ />
+
+
+
+
+
+ );
+} );
diff --git a/admin/blocks/src/avatar/index.js b/admin/blocks/src/avatar/index.js
new file mode 100644
index 0000000..df504ee
--- /dev/null
+++ b/admin/blocks/src/avatar/index.js
@@ -0,0 +1,46 @@
+/**
+ * Avatar block for the WordPress block editor.
+ *
+ * This file is part of Avatar Privacy.
+ *
+ * @file This file provides the Avatar block.
+ * @author Peter Putzer
+ * @license GPL-2.0-or-later
+ * @since 2.3.0
+ */
+
+'use strict';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import edit from './edit';
+import metadata from './block.json';
+
+const { name } = metadata;
+
+export { metadata, name };
+
+/**
+ * The Avatar block.
+ *
+ * The block is rendered server-side to be current (avatars can change frequently).
+ */
+export const settings = {
+ title: __( 'Avatar', 'avatar-privacy' ),
+
+ supports: {
+ align: [ 'left', 'center', 'right' ],
+ html: false,
+ },
+
+ edit,
+ save: () => {
+ // Intentionally empty because this is a dynamic block
+ },
+};
diff --git a/admin/blocks/src/blocks.js b/admin/blocks/src/blocks.js
new file mode 100644
index 0000000..36cdc37
--- /dev/null
+++ b/admin/blocks/src/blocks.js
@@ -0,0 +1,38 @@
+/**
+ * Blocks for the WordPress block editor.
+ *
+ * This file is part of Avatar Privacy.
+ *
+ * @file This file registers the blocks included with the Avatar Privacy plugin.
+ * @author Peter Putzer
+ * @license GPL-2.0-or-later
+ * @since 2.3.0
+ * @requires Gutenberg 4.3
+ */
+
+'use strict';
+
+/**
+ * WordPress dependencies
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import * as frontendForm from './frontend-form';
+import * as avatar from './avatar';
+
+/**
+ * Registers all our blocks.
+ */
+[ frontendForm, avatar ].forEach( ( block ) => {
+ if ( ! block ) {
+ return;
+ }
+ const { metadata, settings, name } = block;
+ registerBlockType( name, {
+ ...metadata,
+ ...settings,
+ } );
+} );
diff --git a/admin/blocks/src/frontend-form/block.json b/admin/blocks/src/frontend-form/block.json
new file mode 100644
index 0000000..7d3f8a6
--- /dev/null
+++ b/admin/blocks/src/frontend-form/block.json
@@ -0,0 +1,19 @@
+{
+ "name": "avatar-privacy/form",
+ "title": "Avatar Privacy Form",
+ "category": "common",
+ "icon": "id-alt",
+ "description": "Inserts a form to upload a local avatar and manage related settings.",
+ "keywords": [ "avatar", "upload", "frontend" ],
+ "textdomain": "avatar-privacy",
+ "attributes": {
+ "avatar_size": {
+ "type": "integer",
+ "default": 96
+ },
+ "show_descriptions": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+}
diff --git a/admin/blocks/src/frontend-form/edit.js b/admin/blocks/src/frontend-form/edit.js
new file mode 100644
index 0000000..10a69e9
--- /dev/null
+++ b/admin/blocks/src/frontend-form/edit.js
@@ -0,0 +1,82 @@
+/**
+ * Frontend Form block for the WordPress block editor.
+ *
+ * This file is part of Avatar Privacy.
+ *
+ * @file This file provides the edit method for the Avatar block.
+ * @author Peter Putzer
+ * @license GPL-2.0-or-later
+ * @since 2.3.0
+ */
+
+'use strict';
+
+/**
+ * WordPress dependencies
+ */
+import {
+ PanelBody,
+ PanelRow,
+ RangeControl,
+ ToggleControl,
+} from '@wordpress/components';
+import ServerSideRender from '@wordpress/server-side-render';
+import { InspectorControls } from '@wordpress/editor';
+import { Fragment } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Renders the markup for editing the block attributes of the Avatar block.
+ *
+ * @param {Object} props The block properties.
+ * @param {Object} props.attributes The block attributes.
+ * @param {string} props.className The CSS class to use.
+ * @param {Object} props.setAttributes The attribute setter function.
+ * @return {Object} ECMAScript JSX Markup for the editor
+ */
+export default ( { attributes, className, setAttributes } ) => {
+ return (
+
+
+
+
+
+ setAttributes( { avatar_size: newSize } )
+ }
+ min={ 48 }
+ max={ 240 }
+ />
+
+
+
+ setAttributes( {
+ show_descriptions:
+ ! attributes.show_descriptions,
+ } )
+ }
+ />
+
+
+
+
+
+ );
+};
diff --git a/admin/blocks/src/frontend-form/index.js b/admin/blocks/src/frontend-form/index.js
new file mode 100644
index 0000000..9e21951
--- /dev/null
+++ b/admin/blocks/src/frontend-form/index.js
@@ -0,0 +1,47 @@
+/**
+ * Frontend Form block for the WordPress block editor.
+ *
+ * This file is part of Avatar Privacy.
+ *
+ * @file This file provides the Frontend Form block.
+ * @author Peter Putzer
+ * @license GPL-2.0-or-later
+ * @since 2.3.0
+ */
+
+'use strict';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import edit from './edit';
+import metadata from './block.json';
+
+const { name } = metadata;
+
+export { metadata, name };
+
+/**
+ * The Frontend Form block.
+ *
+ * The block is rendered server-side to be current (avatars can change frequently).
+ */
+export const settings = {
+ title: __( 'Avatar Privacy Form', 'avatar-privacy' ),
+
+ supports: {
+ html: false,
+ multiple: false,
+ reusable: false,
+ },
+
+ edit,
+ save: () => {
+ // Intentionally empty because this is a dynamic block
+ },
+};
diff --git a/admin/css/blocks.css b/admin/css/blocks.css
new file mode 100644
index 0000000..29f04d8
--- /dev/null
+++ b/admin/css/blocks.css
@@ -0,0 +1,4 @@
+.components-panel__row .components-range-control .components-base-control__label {
+ max-width: 100%;
+}
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImJsb2Nrcy5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7RUFDRSxlQUFlO0FBQ2pCIiwiZmlsZSI6ImJsb2Nrcy5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyIuY29tcG9uZW50cy1wYW5lbF9fcm93IC5jb21wb25lbnRzLXJhbmdlLWNvbnRyb2wgLmNvbXBvbmVudHMtYmFzZS1jb250cm9sX19sYWJlbCB7XG4gIG1heC13aWR0aDogMTAwJTtcbn0iXX0= */
\ No newline at end of file
diff --git a/admin/css/blocks.min.css b/admin/css/blocks.min.css
new file mode 100644
index 0000000..d37ce77
--- /dev/null
+++ b/admin/css/blocks.min.css
@@ -0,0 +1 @@
+.components-panel__row .components-range-control .components-base-control__label{max-width:100%}
\ No newline at end of file
diff --git a/admin/css/settings.css b/admin/css/settings.css
new file mode 100644
index 0000000..d66c106
--- /dev/null
+++ b/admin/css/settings.css
@@ -0,0 +1,14 @@
+.submit .aux-buttons {
+ display: block;
+ float: right;
+}
+
+select,
+input {
+ margin-top: -0.1em;
+}
+
+input[type=number] {
+ width: 5em;
+}
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNldHRpbmdzLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtFQUNFLGNBQWM7RUFDZCxZQUFZO0FBQ2Q7O0FBRUE7O0VBRUUsa0JBQWtCO0FBQ3BCOztBQUVBO0VBQ0UsVUFBVTtBQUNaIiwiZmlsZSI6InNldHRpbmdzLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIi5zdWJtaXQgLmF1eC1idXR0b25zIHtcbiAgZGlzcGxheTogYmxvY2s7XG4gIGZsb2F0OiByaWdodDtcbn1cblxuc2VsZWN0LFxuaW5wdXQge1xuICBtYXJnaW4tdG9wOiAtMC4xZW07XG59XG5cbmlucHV0W3R5cGU9bnVtYmVyXSB7XG4gIHdpZHRoOiA1ZW07XG59Il19 */
\ No newline at end of file
diff --git a/admin/css/settings.min.css b/admin/css/settings.min.css
new file mode 100644
index 0000000..9130591
--- /dev/null
+++ b/admin/css/settings.min.css
@@ -0,0 +1 @@
+.submit .aux-buttons{display:block;float:right}select,input{margin-top:-0.1em}input[type=number]{width:5em}
\ No newline at end of file
diff --git a/admin/partials/network/section.php b/admin/partials/network/section.php
new file mode 100644
index 0000000..1a3adad
--- /dev/null
+++ b/admin/partials/network/section.php
@@ -0,0 +1,30 @@
+
+
+
+
+` element.
+ * @var string $value The checkbox value.
+ */
+?>
+
+
+
+
+
+ />
+
+
+
+
+
+
+` element.
+ * @var string $value The checkbox value.
+ */
+?>
+
+
+
+
+
+ />
+ get_use_gravatar_label( 'user' ), T::ALLOWED_HTML_LABEL ); ?>
+
+
+
+
+
+
+` element.
+ * @var string $erase_field The name of the erase checkbox ` ` element.
+ * @var bool $uploads_disabled Whether the uploads system has been disabled completely..
+ * @var bool $can_upload Whether the currently active user can upload files.
+ * @var bool $has_local_avatar Whether a local avatar has been uploaded.
+ * @var int $size The width/height of the avatar preview image (in pixels).
+ * @var bool $show_description Whether the field description should be shown.
+ */
+
+?>
+
+
+
+ true ] ); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ get_uploader_description( $can_upload, $has_local_avatar ), T::ALLOWED_HTML_LABEL ); ?>
+
+
+
+
+
+
+
+
+ [] ];
+
+?>
+
+check() ) {
+
+ /**
+ * Create and start the plugin.
+ *
+ * @var Controller
+ */
+ $plugin = Factory::get()->create( Controller::class );
+ $plugin->run();
+ }
+}
+run_avatar_privacy();
diff --git a/includes/avatar-privacy-functions.php b/includes/avatar-privacy-functions.php
new file mode 100644
index 0000000..b5e4280
--- /dev/null
+++ b/includes/avatar-privacy-functions.php
@@ -0,0 +1,54 @@
+
+ *
+ * @phpstan-type AvatarArguments array{ force?: bool, ... }
+ */
+interface Avatar_Handler {
+
+ /**
+ * Retrieves the URL for the given default icon type.
+ *
+ * @since 2.7.0 Removed argument index 'type' as it is not required for all implemntations.
+ *
+ * @param string $url The fallback image URL.
+ * @param string $hash The hashed mail address.
+ * @param int $size The size of the avatar image in pixels.
+ * @param array $args {
+ * An array of arguments.
+ *
+ * @type bool $force Optional. Whether to force the regeneration of the image file. Default false.
+ * }
+ *
+ * @return string
+ *
+ * @phpstan-param AvatarArguments $args
+ */
+ public function get_url( $url, $hash, $size, array $args );
+
+ /**
+ * Caches the image specified by the parameters.
+ *
+ * @param string $type The image (sub-)type.
+ * @param string $hash The hashed mail address.
+ * @param int $size The requested size in pixels.
+ * @param string $subdir The requested sub-directory (may contain implementation-specific data).
+ * @param string $extension The requested file extension.
+ *
+ * @return bool Returns `true` if successful, `false` otherwise.
+ */
+ public function cache_image( $type, $hash, $size, $subdir, $extension );
+
+ /**
+ * Retrieves the name of the cache directory for avatars provided by this handler
+ * (e.g. 'gravatar'). Implementations may return an empty string if the actual
+ * type can vary.
+ *
+ * @since 2.4.0
+ *
+ * @return string
+ */
+ public function get_type();
+}
diff --git a/includes/avatar-privacy/avatar-handlers/class-default-icons-handler.php b/includes/avatar-privacy/avatar-handlers/class-default-icons-handler.php
new file mode 100644
index 0000000..b246bb0
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/class-default-icons-handler.php
@@ -0,0 +1,200 @@
+
+ *
+ * @phpstan-type AvatarArguments array{
+ * type?: string,
+ * force?: bool,
+ * }
+ */
+class Default_Icons_Handler implements Avatar_Handler {
+
+ /**
+ * A list of icon providers.
+ *
+ * @var Icon_Provider[]
+ */
+ private array $icon_providers = [];
+
+ /**
+ * The mapping of icon types to providers.
+ *
+ * @var Icon_Provider[]
+ */
+ private array $icon_provider_mapping = [];
+
+ /**
+ * The remote images handler.
+ *
+ * @var Remote_Image_Service
+ */
+ private Remote_Image_Service $remote_images;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.1.0 Parameter $plugin_file removed.
+ * @since 2.3.4 Parameter $remote_images added.
+ * @since 2.4.0 Parameter $file_cache removed.
+ *
+ * @param Icon_Provider[] $icon_providers An array of icon providers.
+ * @param Remote_Image_Service $remote_images The remote images handler.
+ */
+ public function __construct( array $icon_providers, Remote_Image_Service $remote_images ) {
+ $this->icon_providers = $icon_providers;
+ $this->remote_images = $remote_images;
+ }
+
+ /**
+ * Returns a mapping from icon types to specific providers.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ *
+ * @return Icon_Provider[]
+ */
+ protected function get_provider_mapping() {
+ if ( empty( $this->icon_provider_mapping ) ) {
+ foreach ( $this->icon_providers as $provider ) {
+ foreach ( $provider->get_provided_types() as $type ) {
+ $this->icon_provider_mapping[ $type ] = $provider;
+ }
+ }
+ }
+
+ return $this->icon_provider_mapping;
+ }
+
+ /**
+ * Retrieves the URL for the given default icon type.
+ *
+ * @since 2.3.4 Documentation for optional arguments adapted to follow implementation.
+ * @since 2.7.0 Argument key 'default' replaced with 'type' for consistency.
+ *
+ * @param string $url The fallback image URL.
+ * @param string $hash The hashed mail address.
+ * @param int $size The size of the avatar image in pixels.
+ * @param array $args {
+ * An array of arguments.
+ *
+ * @type string $type The default icon type.
+ * @type bool $force Optional. Whether to force the regeneration of the image file. Default false.
+ * }
+ *
+ * @return string
+ *
+ * @phpstan-param AvatarArguments $args
+ */
+ public function get_url( $url, $hash, $size, array $args ) {
+ $defaults = [
+ 'type' => '',
+ 'force' => false,
+ ];
+
+ $args = \wp_parse_args( $args, $defaults );
+
+ // Check for named icon providers first.
+ $providers = $this->get_provider_mapping();
+ if ( ! empty( $providers[ $args['type'] ] ) ) {
+ return $providers[ $args['type'] ]->get_icon_url( $hash, $size, $args['force'] );
+ }
+
+ // Check if the given default icon type is a valid image URL (a common
+ // pattern due to how the default WordPress implementation uses Gravatar.com).
+ if ( $this->remote_images->validate_image_url( $args['type'], 'default_icon' ) ) {
+ // Prepare filter arguments.
+ $url = $args['type'];
+ $hash = $this->remote_images->get_hash( $url );
+
+ /** This filter is documented in avatar-privacy/components/class-avatar-handling.php */
+ return \apply_filters( 'avatar_privacy_legacy_icon_url', $url, $hash, $size, [ 'force' => $args['force'] ] );
+ }
+
+ // Return the fallback default icon URL.
+ return $url;
+ }
+
+ /**
+ * Caches the image specified by the parameters.
+ *
+ * @param string $type The image (sub-)type.
+ * @param string $hash The hashed mail address.
+ * @param int $size The requested size in pixels.
+ * @param string $subdir The requested sub-directory.
+ * @param string $extension The requested file extension.
+ *
+ * @return bool Returns `true` if successful, `false` otherwise.
+ */
+ public function cache_image( $type, $hash, $size, $subdir, $extension ) {
+ return ! empty( $this->get_url( '', $hash, $size, [ 'type' => $type ] ) );
+ }
+
+ /**
+ * Adds new images to the list of default avatar images.
+ *
+ * @param string[] $avatar_defaults The list of default avatar images.
+ *
+ * @return string[] The modified default avatar array.
+ */
+ public function avatar_defaults( $avatar_defaults ) {
+ // Remove Gravatar logo.
+ unset( $avatar_defaults['gravatar_default'] );
+
+ // Add non-default icons.
+ foreach ( $this->icon_providers as $provider ) {
+ $type = $provider->get_option_value();
+ if ( ! isset( $avatar_defaults[ $type ] ) ) {
+ $avatar_defaults[ $type ] = $provider->get_name();
+ }
+ }
+
+ return $avatar_defaults;
+ }
+
+ /**
+ * Retrieves the name of the cache subdirectory for avatars provided by this
+ * handler (e.g. 'gravatar'). Implementations may return an empty string if
+ * the actual type can vary.
+ *
+ * @since 2.4.0
+ *
+ * @return string
+ */
+ public function get_type() {
+ return '';
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/class-gravatar-cache-handler.php b/includes/avatar-privacy/avatar-handlers/class-gravatar-cache-handler.php
new file mode 100644
index 0000000..11098b9
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/class-gravatar-cache-handler.php
@@ -0,0 +1,249 @@
+
+ *
+ * @phpstan-type AvatarArguments array{
+ * email?: string,
+ * rating?: key-of,
+ * mimetype?: string,
+ * force?: bool
+ * }
+ */
+class Gravatar_Cache_Handler implements Avatar_Handler {
+
+ /**
+ * Valid Gravatar image ratings.
+ *
+ * @since 2.7.0.
+ */
+ private const GRAVATAR_RATING = [
+ 'g' => true,
+ 'pg' => true,
+ 'r' => true,
+ 'x' => true,
+ ];
+
+ /**
+ * The core API.
+ *
+ * @var Core
+ */
+ private Core $core;
+
+ /**
+ * The options handler.
+ *
+ * @var Options
+ */
+ private Options $options;
+
+ /**
+ * The filesystem cache handler.
+ *
+ * @var Filesystem_Cache
+ */
+ private Filesystem_Cache $file_cache;
+
+ /**
+ * The Gravatar network service.
+ *
+ * @var Gravatar_Service
+ */
+ private Gravatar_Service $gravatar;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.0.0 Parameter $gravatar added.
+ *
+ * @param Core $core The core API.
+ * @param Options $options The options handler.
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ * @param Gravatar_Service $gravatar The Gravatar network service.
+ */
+ public function __construct( Core $core, Options $options, Filesystem_Cache $file_cache, Gravatar_Service $gravatar ) {
+ $this->core = $core;
+ $this->options = $options;
+ $this->file_cache = $file_cache;
+ $this->gravatar = $gravatar;
+ }
+
+ /**
+ * Retrieves the Gravatar icon URL.
+ *
+ * Note: Not all of the arguments provided by the `avatar_privacy_gravatar_icon_url`
+ * filter hook are actually required.
+ *
+ * @since 2.7.0 Unused arguments key 'user_id' removed.
+ *
+ * @param string $url The fallback default icon URL.
+ * @param string $hash The hashed mail address.
+ * @param int $size The size of the avatar image in pixels.
+ * @param array $args {
+ * An array of arguments.
+ *
+ * @type string $email The mail address used to generate the identity hash.
+ * @type string $rating Optional. The audience rating (e.g. 'g', 'pg', 'r', 'x'). Default 'g'.
+ * @type string $mimetype Optional. The expected MIME type of the Gravatar image. Default 'image/png'.
+ * @type bool $force Optional. Whether to force re-caching of the image file. Default false.
+ * }
+ *
+ * @return string
+ *
+ * @phpstan-param AvatarArguments $args
+ */
+ public function get_url( $url, $hash, $size, array $args ) {
+ $defaults = [
+ 'email' => '',
+ 'rating' => 'g',
+ 'mimetype' => Image_File::PNG_IMAGE,
+ 'force' => false,
+ ];
+
+ $args = \wp_parse_args( $args, $defaults );
+ $filename = "gravatar/{$this->get_sub_dir( $hash )}/{$hash}-{$size}." . Image_File::FILE_EXTENSION[ $args['mimetype'] ];
+
+ // Only retrieve new Gravatar if necessary.
+ if ( $args['force'] || ! \file_exists( "{$this->file_cache->get_base_dir()}{$filename}" ) ) {
+ // Retrieve the gravatar icon.
+ $icon = $this->gravatar->get_image( $args['email'], $size, $args['rating'] );
+
+ // Store it (empty files will fail this check).
+ if ( ! $this->file_cache->set( $filename, $icon, $args['force'] ) ) {
+ // Something went wrong..
+ return $url;
+ }
+ }
+
+ return $this->file_cache->get_url( $filename );
+ }
+
+ /**
+ * Calculates the subdirectory from the given identity hash.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ * @since 2.4.0 Parameter $user removed.
+ *
+ * @param string $identity The identity (mail address) hash.
+ *
+ * @return string
+ */
+ protected function get_sub_dir( $identity ) {
+ return \implode( '/', \str_split( \substr( $identity, 0, 2 ) ) );
+ }
+
+ /**
+ * Caches the image specified by the parameters.
+ *
+ * @since 2.0.0
+ *
+ * @param string $type The image (sub-)type. Ignored.
+ * @param string $hash The hashed mail address.
+ * @param int $size The requested size in pixels.
+ * @param string $subdir The requested sub-directory.
+ * @param string $extension The requested file extension.
+ *
+ * @return bool Returns `true` if successful, `false` otherwise.
+ */
+ public function cache_image( $type, $hash, $size, $subdir, $extension ) {
+
+ // Lookup user and/or email address.
+ $user = $this->core->get_user_by_hash( $hash );
+
+ if ( ! empty( $user ) ) {
+ $email = ! empty( $user->user_email ) ? $user->user_email : '';
+ } else {
+ $email = $this->core->get_comment_author_email( $hash );
+ }
+
+ // Could not find user/comment author.
+ if ( empty( $email ) ) {
+ return false;
+ }
+
+ // Prepare arguments.
+ $args = [
+ 'email' => $email,
+ 'rating' => $this->get_avatar_rating(),
+ 'mimetype' => Image_File::CONTENT_TYPE[ $extension ],
+ ];
+
+ // Try to cache the icon.
+ return ! empty( $this->get_url( '', $hash, $size, $args ) );
+ }
+
+ /**
+ * Retrieves the name of the cache subdirectory for avatars provided by this
+ * handler (e.g. 'gravatar'). Implementations may return an empty string if
+ * the actual type can vary.
+ *
+ * @since 2.4.0
+ *
+ * @return string
+ */
+ public function get_type() {
+ return 'gravatar';
+ }
+
+ /**
+ * Retrieves the allowed avatar image ratings.
+ *
+ * @since 2.7.0
+ *
+ * @return string
+ *
+ * @phpstan-return key-of
+ */
+ protected function get_avatar_rating() {
+ $rating = $this->options->get( 'avatar_rating', 'g', true );
+
+ if ( ! \is_string( $rating ) || empty( self::GRAVATAR_RATING[ $rating ] ) ) {
+ $rating = 'g';
+ }
+
+ return $rating; // @phpstan-ignore-line -- https://github.com/phpstan/phpstan/issues/8559
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/class-legacy-icon-handler.php b/includes/avatar-privacy/avatar-handlers/class-legacy-icon-handler.php
new file mode 100644
index 0000000..7a49548
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/class-legacy-icon-handler.php
@@ -0,0 +1,185 @@
+
+ *
+ * @phpstan-import-type AvatarArguments from Avatar_Handler
+ */
+class Legacy_Icon_Handler implements Avatar_Handler {
+
+ /**
+ * The filesystem cache handler.
+ *
+ * @var Filesystem_Cache
+ */
+ private Filesystem_Cache $file_cache;
+
+ /**
+ * The remote image service.
+ *
+ * @var Remote_Image_Service
+ */
+ private Remote_Image_Service $remote_images;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ * @param Remote_Image_Service $remote_images The remote image service.
+ */
+ public function __construct( Filesystem_Cache $file_cache, Remote_Image_Service $remote_images ) {
+ $this->file_cache = $file_cache;
+ $this->remote_images = $remote_images;
+ }
+
+ /**
+ * Retrieves the legacy icon URL.
+ *
+ * @param string $url The legacy image URL.
+ * @param string $hash The hashed URL.
+ * @param int $size The size of the avatar image in pixels.
+ * @param array $args {
+ * An array of arguments.
+ *
+ * @type string $mimetype Optional. The expected MIME type of the avatar image. Default based on the URL file extension.
+ * @type bool $force Optional. Whether to force re-caching of the image file. Default false.
+ * }
+ *
+ * @return string
+ *
+ * @phpstan-param AvatarArguments $args
+ */
+ public function get_url( $url, $hash, $size, array $args ) {
+ $defaults = [
+ 'mimetype' => $this->get_target_mime_type( $url ),
+ 'force' => false,
+ ];
+
+ $args = \wp_parse_args( $args, $defaults );
+ $filename = "legacy/{$this->get_sub_dir( $hash )}/{$hash}-{$size}." . Image_File::FILE_EXTENSION[ $args['mimetype'] ];
+
+ // Only retrieve new Gravatar if necessary.
+ if ( $args['force'] || ! \file_exists( "{$this->file_cache->get_base_dir()}{$filename}" ) ) {
+ // Retrieve the legacy icon.
+ $icon = $this->remote_images->get_image( $url, $size, $args['mimetype'] );
+
+ // Store it (empty files will fail this check).
+ if ( ! $this->file_cache->set( $filename, $icon, $args['force'] ) ) {
+ // Something went wrong..
+ return $url;
+ }
+ }
+
+ return $this->file_cache->get_url( $filename );
+ }
+
+ /**
+ * Retrieves the target MIME type for our resized image.
+ *
+ * @param string $url The image URL.
+ *
+ * @return string The target MIME type ('image/png' if the URL extension
+ * is not one of our supported image formats).
+ */
+ protected function get_target_mime_type( $url ) {
+ $mimetype = Image_File::PNG_IMAGE;
+ $path = \wp_parse_url( $url, \PHP_URL_PATH );
+ $extension = \pathinfo( \is_string( $path ) ? $path : '', \PATHINFO_EXTENSION );
+
+ // Determine MIME type based on the file extension.
+ if ( isset( Image_File::CONTENT_TYPE[ $extension ] ) ) {
+ $mimetype = Image_File::CONTENT_TYPE[ $extension ];
+ }
+
+ return $mimetype;
+ }
+
+ /**
+ * Calculates the subdirectory from the given identity hash.
+ *
+ * @param string $identity The identity (mail address) hash.
+ *
+ * @return string
+ */
+ protected function get_sub_dir( $identity ) {
+ return \implode( '/', \str_split( \substr( $identity, 0, 2 ) ) );
+ }
+
+ /**
+ * Caches the image specified by the parameters.
+ *
+ * @param string $type The image (sub-)type. Ignored.
+ * @param string $hash The hashed mail address.
+ * @param int $size The requested size in pixels.
+ * @param string $subdir The requested sub-directory.
+ * @param string $extension The requested file extension.
+ *
+ * @return bool Returns `true` if successful, `false` otherwise.
+ */
+ public function cache_image( $type, $hash, $size, $subdir, $extension ) {
+
+ // Lookup image URL.
+ $url = $this->remote_images->get_image_url( $hash );
+
+ // Could not find legacy image URL.
+ if ( empty( $url ) ) {
+ return false;
+ }
+
+ // Prepare arguments.
+ $args = [
+ 'mimetype' => Image_File::CONTENT_TYPE[ $extension ],
+ ];
+
+ // Try to cache the icon.
+ return ! empty( $this->get_url( $url, $hash, $size, $args ) );
+ }
+
+ /**
+ * Retrieves the name of the cache subdirectory for avatars provided by this
+ * handler (e.g. 'gravatar'). Implementations may return an empty string if
+ * the actual type can vary.
+ *
+ * @return string
+ */
+ public function get_type() {
+ return 'legacy';
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/class-user-avatar-handler.php b/includes/avatar-privacy/avatar-handlers/class-user-avatar-handler.php
new file mode 100644
index 0000000..5e616ed
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/class-user-avatar-handler.php
@@ -0,0 +1,221 @@
+
+ *
+ * @phpstan-type AvatarArguments array{
+ * avatar?: string,
+ * mimetype?: string,
+ * timestamp?: bool,
+ * force?: bool
+ * }
+ */
+class User_Avatar_Handler implements Avatar_Handler {
+
+ /**
+ * The core API.
+ *
+ * @var User_Fields
+ */
+ private User_Fields $user_fields;
+
+ /**
+ * The filesystem cache handler.
+ *
+ * @var Filesystem_Cache
+ */
+ private Filesystem_Cache $file_cache;
+
+ /**
+ * The uploads base directory.
+ *
+ * @var string
+ */
+ private string $base_dir;
+
+ /**
+ * The image editor support class.
+ *
+ * @var Images\Editor
+ */
+ private Images\Editor $images;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.4.0 Parameter $core replaced by $user_fields.
+ *
+ * @param User_Fields $user_fields The user data API.
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ * @param Images\Editor $images The image editing handler.
+ */
+ public function __construct( User_Fields $user_fields, Filesystem_Cache $file_cache, Images\Editor $images ) {
+ $this->user_fields = $user_fields;
+ $this->file_cache = $file_cache;
+ $this->images = $images;
+ }
+
+ /**
+ * Retrieves the URL for the given default icon type.
+ *
+ * Note: Not all of the arguments provided by the `avatar_privacy_user_avatar_icon_url`
+ * filter hook are actually required.
+ *
+ * @since 2.7.0 Unused arguments key 'type' removed.
+ *
+ * @param string $url The fallback image URL.
+ * @param string $hash The hashed mail address.
+ * @param int $size The size of the avatar image in pixels.
+ * @param array $args {
+ * An array of arguments.
+ *
+ * @type string $avatar The full-size avatar image path.
+ * @type string $mimetype Optional. The expected MIME type of the avatar image. Default 'image/png'.
+ * @type bool $timestamp Optional. Whether to add a timestamp for cache busting. Default false.
+ * @type bool $force Optional. Whether to force the regeneration of the image file. Default false.
+ * }
+ *
+ * @return string
+ *
+ * @phpstan-param AvatarArguments $args
+ */
+ public function get_url( $url, $hash, $size, array $args ) {
+ // Cache base directory.
+ if ( empty( $this->base_dir ) ) {
+ $this->base_dir = $this->file_cache->get_base_dir();
+ }
+
+ // Default arguments.
+ $defaults = [
+ 'avatar' => '',
+ 'mimetype' => Image_File::PNG_IMAGE,
+ 'timestamp' => false,
+ 'force' => false,
+ ];
+
+ $args = \wp_parse_args( $args, $defaults );
+ $extension = Image_File::FILE_EXTENSION[ $args['mimetype'] ];
+ $filename = "user/{$this->get_sub_dir( $hash )}/{$hash}-{$size}.{$extension}";
+ $abspath = "{$this->base_dir}{$filename}";
+
+ if ( $args['force'] || ! \file_exists( $abspath ) ) {
+ $data = $this->images->get_resized_image_data(
+ $this->images->get_image_editor( $args['avatar'] ), $size, $size, $args['mimetype']
+ );
+
+ // Save the generated PNG file (empty files will fail this check).
+ if ( ! $this->file_cache->set( $filename, $data, $args['force'] ) ) {
+ // Something went wrong..
+ return $url;
+ }
+ }
+
+ // Optionally add file modification time as `ts` query argument to bust caches.
+ $query_args = [
+ 'ts' => $args['timestamp'] ? @\filemtime( $abspath ) : false,
+ ];
+
+ return \add_query_arg(
+ \rawurlencode_deep( \array_filter( $query_args ) ),
+ $this->file_cache->get_url( $filename )
+ );
+ }
+
+ /**
+ * Calculates the subdirectory from the given identity hash.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ *
+ * @param string $identity The identity (mail address) hash.
+ *
+ * @return string
+ */
+ protected function get_sub_dir( $identity ) {
+ return \implode( '/', \str_split( \substr( $identity, 0, 2 ) ) );
+ }
+
+ /**
+ * Caches the image specified by the parameters.
+ *
+ * @since 2.0.0
+ *
+ * @param string $type The image (sub-)type.
+ * @param string $hash The hashed mail address.
+ * @param int $size The requested size in pixels.
+ * @param string $subdir The requested sub-directory.
+ * @param string $extension The requested file extension.
+ *
+ * @return bool Returns `true` if successful, `false` otherwise.
+ */
+ public function cache_image( $type, $hash, $size, $subdir, $extension ) {
+ $user = $this->user_fields->get_user_by_hash( $hash );
+ if ( ! empty( $user ) ) {
+ $local_avatar = $this->user_fields->get_local_avatar( $user->ID );
+ }
+
+ // Could not find user or uploaded avatar.
+ if ( empty( $local_avatar ) ) {
+ return false;
+ }
+
+ // Prepare arguments.
+ $args = [
+ 'avatar' => $local_avatar['file'],
+ 'mimetype' => $local_avatar['type'],
+ ];
+
+ // Try to cache the icon.
+ return ! empty( $this->get_url( '', $hash, $size, $args ) );
+ }
+
+ /**
+ * Retrieves the name of the cache subdirectory for avatars provided by this
+ * handler (e.g. 'gravatar'). Implementations may return an empty string if
+ * the actual type can vary.
+ *
+ * @since 2.4.0
+ *
+ * @return string
+ */
+ public function get_type() {
+ return 'user';
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/class-abstract-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/class-abstract-icon-provider.php
new file mode 100644
index 0000000..36842f2
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/class-abstract-icon-provider.php
@@ -0,0 +1,127 @@
+
+ */
+abstract class Abstract_Icon_Provider implements Icon_Provider {
+
+ /**
+ * An array containing valid types as indexes.
+ *
+ * @var string[]
+ */
+ private array $valid_types;
+
+ /**
+ * The primary type (option value).
+ *
+ * @var string
+ */
+ private string $primary_type;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.0.0 Parameter $name added.
+ * @since 2.1.0 Parameter $name removed to allow proper translation loading.
+ *
+ * @param string[] $types An array of valid types.
+ */
+ protected function __construct( array $types ) {
+ $this->valid_types = \array_flip( $types );
+ $this->primary_type = $types[0];
+ }
+
+ /**
+ * Checks if this Default_Icon_Provider handles the given icon type.
+ *
+ * @param string $type The default icon type.
+ *
+ * @return bool
+ */
+ public function provides( $type ) {
+ return isset( $this->valid_types[ $type ] );
+ }
+
+ /**
+ * Retrieves all icon types handled by the class.
+ *
+ * @since 2.0.0
+ *
+ * @return string[]
+ */
+ public function get_provided_types() {
+ return \array_keys( $this->valid_types );
+ }
+
+ /**
+ * Retrieves the default icon.
+ *
+ * @since 2.7.0 Parameter `$force` added.
+ *
+ * @param string $identity The identity (mail address) hash.
+ * @param int $size The requested size in pixels.
+ * @param bool $force Whether the icon cache should be invalidated (if applicable).
+ *
+ * @return string
+ */
+ abstract public function get_icon_url( $identity, $size, bool $force = false );
+
+ /**
+ * Retrieves the option value (the primary provided type).
+ *
+ * @since 2.0.0
+ *
+ * @return string
+ */
+ public function get_option_value() {
+ return $this->primary_type;
+ }
+
+ /**
+ * Retrieves the user-visible, translated name. Replacements for existing
+ * core default icon styles may return the empty string instead (as does the
+ * default implementation).
+ *
+ * @since 2.0.0
+ * @since 2.1.0 Always returns '' to encourage subclasses to do just-in-time translation loading.
+ *
+ * @return string
+ */
+ public function get_name() {
+ return '';
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/class-custom-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/class-custom-icon-provider.php
new file mode 100644
index 0000000..0db3e1b
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/class-custom-icon-provider.php
@@ -0,0 +1,139 @@
+
+ */
+class Custom_Icon_Provider extends Abstract_Icon_Provider {
+
+ /**
+ * The filesystem cache handler.
+ *
+ * @var Filesystem_Cache
+ */
+ private $file_cache;
+
+ /**
+ * The settings API.
+ *
+ * @since 2.4.0
+ *
+ * @var Default_Avatars
+ */
+ private $default_avatars;
+
+ /**
+ * The image editor support class.
+ *
+ * @var Images\Editor
+ */
+ private $images;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.4.0 Parameter $default_avatars added, parameter $core removed.
+ *
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ * @param Default_Avatars $default_avatars The custom default avatars API.
+ * @param Images\Editor $images The image editing handler.
+ */
+ public function __construct( Filesystem_Cache $file_cache, Default_Avatars $default_avatars, Images\Editor $images ) {
+ parent::__construct( [ 'custom' ] );
+
+ $this->file_cache = $file_cache;
+ $this->default_avatars = $default_avatars;
+ $this->images = $images;
+ }
+
+ /**
+ * Retrieves the default icon.
+ *
+ * @since 2.7.0 Parameter `$force` added.
+ *
+ * @param string $identity The identity (mail address) hash.
+ * @param int $size The requested size in pixels.
+ * @param bool $force Whether the icon cache should be invalidated (if applicable).
+ *
+ * @return string
+ */
+ public function get_icon_url( $identity, $size, bool $force = false ) {
+ // Abort if no custom image has been set.
+ $default = \includes_url( 'images/blank.gif' );
+ $icon = $this->default_avatars->get_custom_default_avatar();
+ if ( empty( $icon['file'] ) ) {
+ return $default;
+ }
+
+ // We need the current site ID.
+ $site_id = \get_current_blog_id();
+ $extension = Image_File::FILE_EXTENSION[ $icon['type'] ];
+ $identity = $this->default_avatars->get_hash( $site_id );
+ $filename = "custom/{$site_id}/{$identity}-{$size}.{$extension}";
+
+ // Only generate a new icon if necessary.
+ if ( $force || ! \file_exists( "{$this->file_cache->get_base_dir()}{$filename}" ) ) {
+
+ $data = $this->images->get_resized_image_data( $this->images->get_image_editor( $icon['file'] ), $size, $size, $icon['type'] );
+ if ( empty( $data ) ) {
+ // Something went wrong..
+ return $default;
+ }
+
+ // Save the generated image file.
+ $this->file_cache->set( $filename, $data );
+ }
+
+ return $this->file_cache->get_url( $filename );
+ }
+
+ /**
+ * Retrieves the user-visible, translated name.
+ *
+ * @since 2.1.0
+ *
+ * @return string
+ */
+ public function get_name() {
+ return \__( 'Custom', 'avatar-privacy' );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/class-generating-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/class-generating-icon-provider.php
new file mode 100644
index 0000000..3f559f3
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/class-generating-icon-provider.php
@@ -0,0 +1,116 @@
+
+ */
+abstract class Generating_Icon_Provider extends Abstract_Icon_Provider {
+
+ /**
+ * The filesystem cache handler.
+ *
+ * @var Filesystem_Cache
+ */
+ private $file_cache;
+
+ /**
+ * The icon generator.
+ *
+ * @var Generator
+ */
+ private $generator;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.0.0 Parameter $name added.
+ * @since 2.1.0 Parameter $name removed to allow proper translation loading.
+ *
+ * @param Generator $generator An image generator.
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ * @param string[] $types An array of valid types. The first entry is used as the cache sub-directory.
+ */
+ protected function __construct( Generator $generator, Filesystem_Cache $file_cache, array $types ) {
+ parent::__construct( $types );
+
+ $this->generator = $generator;
+ $this->file_cache = $file_cache;
+ }
+
+ /**
+ * Retrieves the default icon.
+ *
+ * @since 2.7.0 Parameter `$force` added.
+ *
+ * @param string $identity The identity (mail address) hash.
+ * @param int $size The requested size in pixels.
+ * @param bool $force Whether the icon cache should be invalidated (if applicable).
+ *
+ * @return string
+ */
+ public function get_icon_url( $identity, $size, bool $force = false ) {
+ $filename = $this->get_filename( $identity, $size );
+
+ // Only generate a new icon if necessary.
+ if ( $force || ! \file_exists( "{$this->file_cache->get_base_dir()}{$filename}" ) ) {
+ $this->file_cache->set( $filename, (string) $this->generator->build( $identity, $size ) );
+ }
+
+ return $this->file_cache->get_url( $filename );
+ }
+
+ /**
+ * Calculates the subdirectory from the given identity hash.
+ *
+ * @param string $identity The identity (mail address) hash.
+ *
+ * @return string
+ */
+ protected function get_sub_dir( $identity ) {
+ return \implode( '/', \str_split( \substr( $identity, 0, 2 ) ) );
+ }
+
+ /**
+ * Retrieves the filename (including the sub-directory and file extension).
+ *
+ * @param string $identity The identity (mail address) hash.
+ * @param int $size The requested size in pixels.
+ *
+ * @return string
+ */
+ abstract protected function get_filename( $identity, $size );
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/class-generator.php b/includes/avatar-privacy/avatar-handlers/default-icons/class-generator.php
new file mode 100644
index 0000000..6ec85fb
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/class-generator.php
@@ -0,0 +1,48 @@
+
+ */
+interface Generator {
+
+ /**
+ * Builds an icon based on the given seed returns the image data.
+ *
+ * @param string $seed The seed data (hash).
+ * @param int $size The size in pixels.
+ *
+ * @return string|false
+ */
+ public function build( $seed, $size );
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/class-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/class-icon-provider.php
new file mode 100644
index 0000000..93eceda
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/class-icon-provider.php
@@ -0,0 +1,87 @@
+
+ */
+interface Icon_Provider {
+
+ /**
+ * Checks if this Icon_Provider handles the given icon type.
+ *
+ * @param string $type The default icon type.
+ *
+ * @return bool
+ */
+ public function provides( $type );
+
+ /**
+ * Retrieves all icon types handled by the class.
+ *
+ * @since 2.0.0
+ *
+ * @return string[]
+ */
+ public function get_provided_types();
+
+ /**
+ * Retrieves the default icon.
+ *
+ * @since 2.7.0 Parameter `$force` added.
+ *
+ * @param string $identity The identity (mail address) hash.
+ * @param int $size The requested size in pixels.
+ * @param bool $force Whether the icon cache should be invalidated (if applicable).
+ *
+ * @return string
+ */
+ public function get_icon_url( $identity, $size, bool $force = false );
+
+ /**
+ * Retrieves the option value (the primary provided type).
+ *
+ * @since 2.0.0
+ *
+ * @return string
+ */
+ public function get_option_value();
+
+ /**
+ * Retrieves the user-visible, translated name.
+ *
+ * @since 2.0.0
+ *
+ * @return string
+ */
+ public function get_name();
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/class-static-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/class-static-icon-provider.php
new file mode 100644
index 0000000..8d38747
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/class-static-icon-provider.php
@@ -0,0 +1,79 @@
+
+ */
+class Static_Icon_Provider extends Abstract_Icon_Provider {
+
+ /**
+ * The basename of the icon files residing in `public/images`.
+ *
+ * @var string
+ */
+ protected $icon_basename;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.0.0 Parameter $name added.
+ * @since 2.1.0 Parameters $name and $plugin_file removed.
+ *
+ * @param string[]|string $types Either a single identifier string or an array thereof.
+ * @param string $basename The icon basename (without extension or size suffix).
+ */
+ public function __construct( $types, $basename ) {
+ parent::__construct( (array) $types );
+
+ $this->icon_basename = $basename;
+ }
+
+ /**
+ * Retrieves the default icon.
+ *
+ * @since 2.7.0 Parameter `$force` added.
+ *
+ * @param string $identity The identity (mail address) hash.
+ * @param int $size The requested size in pixels.
+ * @param bool $force Whether the icon cache should be invalidated (if applicable).
+ *
+ * @return string
+ */
+ public function get_icon_url( $identity, $size, bool $force = false ) {
+ $use_size = ( $size > 64 ) ? '128' : '64';
+
+ return \plugins_url( "public/images/{$this->icon_basename}-{$use_size}.png", \AVATAR_PRIVACY_PLUGIN_FILE );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/class-svg-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/class-svg-icon-provider.php
new file mode 100644
index 0000000..0617f61
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/class-svg-icon-provider.php
@@ -0,0 +1,55 @@
+
+ */
+class SVG_Icon_Provider extends Static_Icon_Provider {
+
+ /**
+ * Retrieves the default icon.
+ *
+ * @since 2.7.0 Parameter `$force` added.
+ *
+ * @param string $identity The identity (mail address) hash.
+ * @param int $size The requested size in pixels.
+ * @param bool $force Whether the icon cache should be invalidated (if applicable).
+ *
+ * @return string
+ */
+ public function get_icon_url( $identity, $size, bool $force = false ) {
+ return \plugins_url( "public/images/{$this->icon_basename}.svg", \AVATAR_PRIVACY_PLUGIN_FILE );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-bird-avatar-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-bird-avatar-icon-provider.php
new file mode 100644
index 0000000..9f04a78
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-bird-avatar-icon-provider.php
@@ -0,0 +1,73 @@
+
+ */
+class Bird_Avatar_Icon_Provider extends Generating_Icon_Provider {
+
+ /**
+ * Creates a new instance.
+ *
+ * @param Generators\Bird_Avatar $generator A generator instance.
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ */
+ public function __construct( Generators\Bird_Avatar $generator, Filesystem_Cache $file_cache ) {
+ parent::__construct( $generator, $file_cache, [ 'bird' ] );
+ }
+
+ /**
+ * Retrieves the filename (including the sub-directory and file extension).
+ *
+ * @param string $identity The identity (mail address) hash. Ignored.
+ * @param int $size The requested size in pixels.
+ *
+ * @return string
+ */
+ protected function get_filename( $identity, $size ) {
+ return "bird/{$this->get_sub_dir( $identity )}/{$identity}-{$size}.png";
+ }
+
+ /**
+ * Retrieves the user-visible, translated name.
+ *
+ * @return string
+ */
+ public function get_name() {
+ return \__( 'Birds (Generated)', 'avatar-privacy' );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-cat-avatar-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-cat-avatar-icon-provider.php
new file mode 100644
index 0000000..02402c2
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-cat-avatar-icon-provider.php
@@ -0,0 +1,73 @@
+
+ */
+class Cat_Avatar_Icon_Provider extends Generating_Icon_Provider {
+
+ /**
+ * Creates a new instance.
+ *
+ * @param Generators\Cat_Avatar $generator A generator instance.
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ */
+ public function __construct( Generators\Cat_Avatar $generator, Filesystem_Cache $file_cache ) {
+ parent::__construct( $generator, $file_cache, [ 'cat' ] );
+ }
+
+ /**
+ * Retrieves the filename (including the sub-directory and file extension).
+ *
+ * @param string $identity The identity (mail address) hash. Ignored.
+ * @param int $size The requested size in pixels.
+ *
+ * @return string
+ */
+ protected function get_filename( $identity, $size ) {
+ return "cat/{$this->get_sub_dir( $identity )}/{$identity}-{$size}.png";
+ }
+
+ /**
+ * Retrieves the user-visible, translated name.
+ *
+ * @return string
+ */
+ public function get_name() {
+ return \__( 'Cats (Generated)', 'avatar-privacy' );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-identicon-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-identicon-icon-provider.php
new file mode 100644
index 0000000..3dd7121
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-identicon-icon-provider.php
@@ -0,0 +1,66 @@
+
+ */
+class Identicon_Icon_Provider extends Generating_Icon_Provider {
+
+ /**
+ * Creates a new instance.
+ *
+ * @param Generators\Jdenticon $generator A generator instance.
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ */
+ public function __construct( Generators\Jdenticon $generator, Filesystem_Cache $file_cache ) {
+ parent::__construct( $generator, $file_cache, [ 'identicon' ] );
+ }
+
+ /**
+ * Retrieves the filename (including the sub-directory and file extension).
+ *
+ * @param string $identity The identity (mail address) hash. Ignored.
+ * @param int $size The requested size in pixels.
+ *
+ * @return string
+ */
+ protected function get_filename( $identity, $size ) {
+ return "identicon/{$this->get_sub_dir( $identity )}/{$identity}.svg";
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-monster-id-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-monster-id-icon-provider.php
new file mode 100644
index 0000000..b06a9ea
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-monster-id-icon-provider.php
@@ -0,0 +1,66 @@
+
+ */
+class Monster_ID_Icon_Provider extends Generating_Icon_Provider {
+
+ /**
+ * Creates a new instance.
+ *
+ * @param Generators\Monster_ID $generator A generator instance.
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ */
+ public function __construct( Generators\Monster_ID $generator, Filesystem_Cache $file_cache ) {
+ parent::__construct( $generator, $file_cache, [ 'monsterid' ] );
+ }
+
+ /**
+ * Retrieves the filename (including the sub-directory and file extension).
+ *
+ * @param string $identity The identity (mail address) hash. Ignored.
+ * @param int $size The requested size in pixels.
+ *
+ * @return string
+ */
+ protected function get_filename( $identity, $size ) {
+ return "monsterid/{$this->get_sub_dir( $identity )}/{$identity}-{$size}.png";
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-retro-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-retro-icon-provider.php
new file mode 100644
index 0000000..d589b83
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-retro-icon-provider.php
@@ -0,0 +1,66 @@
+
+ */
+class Retro_Icon_Provider extends Generating_Icon_Provider {
+
+ /**
+ * Creates a new instance.
+ *
+ * @param Generators\Retro $generator A generator instance.
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ */
+ public function __construct( Generators\Retro $generator, Filesystem_Cache $file_cache ) {
+ parent::__construct( $generator, $file_cache, [ 'retro' ] );
+ }
+
+ /**
+ * Retrieves the filename (including the sub-directory and file extension).
+ *
+ * @param string $identity The identity (mail address) hash. Ignored.
+ * @param int $size The requested size in pixels.
+ *
+ * @return string
+ */
+ protected function get_filename( $identity, $size ) {
+ return "retro/{$this->get_sub_dir( $identity )}/{$identity}.svg";
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-rings-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-rings-icon-provider.php
new file mode 100644
index 0000000..7fd708d
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-rings-icon-provider.php
@@ -0,0 +1,77 @@
+
+ */
+class Rings_Icon_Provider extends Generating_Icon_Provider {
+
+ /**
+ * Creates a new instance.
+ *
+ * @param Generators\Rings $generator A generator instance.
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ */
+ public function __construct( Generators\Rings $generator, Filesystem_Cache $file_cache ) {
+ parent::__construct( $generator, $file_cache, [ 'rings' ] );
+ }
+
+ /**
+ * Retrieves the filename (including the sub-directory and file extension).
+ *
+ * @param string $identity The identity (mail address) hash. Ignored.
+ * @param int $size The requested size in pixels.
+ *
+ * @return string
+ */
+ protected function get_filename( $identity, $size ) {
+ return "rings/{$this->get_sub_dir( $identity )}/{$identity}.svg";
+ }
+
+ /**
+ * Retrieves the user-visible, translated name.
+ *
+ * @since 2.1.0
+ *
+ * @return string
+ */
+ public function get_name() {
+ return \__( 'Rings (Generated)', 'avatar-privacy' );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-robohash-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-robohash-icon-provider.php
new file mode 100644
index 0000000..b2670aa
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-robohash-icon-provider.php
@@ -0,0 +1,73 @@
+
+ */
+class Robohash_Icon_Provider extends Generating_Icon_Provider {
+
+ /**
+ * Creates a new instance.
+ *
+ * @param Generators\Robohash $generator A generator instance.
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ */
+ public function __construct( Generators\Robohash $generator, Filesystem_Cache $file_cache ) {
+ parent::__construct( $generator, $file_cache, [ 'robohash' ] );
+ }
+
+ /**
+ * Retrieves the filename (including the sub-directory and file extension).
+ *
+ * @param string $identity The identity (mail address) hash. Ignored.
+ * @param int $size The requested size in pixels.
+ *
+ * @return string
+ */
+ protected function get_filename( $identity, $size ) {
+ return "robohash/{$this->get_sub_dir( $identity )}/{$identity}.svg";
+ }
+
+ /**
+ * Retrieves the user-visible, translated name.
+ *
+ * @return string
+ */
+ public function get_name() {
+ return \__( 'Robohash (Generated)', 'avatar-privacy' );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-wavatar-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-wavatar-icon-provider.php
new file mode 100644
index 0000000..9f04837
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-wavatar-icon-provider.php
@@ -0,0 +1,66 @@
+
+ */
+class Wavatar_Icon_Provider extends Generating_Icon_Provider {
+
+ /**
+ * Creates a new instance.
+ *
+ * @param Generators\Wavatar $generator A generator instance.
+ * @param Filesystem_Cache $file_cache The file cache handler.
+ */
+ public function __construct( Generators\Wavatar $generator, Filesystem_Cache $file_cache ) {
+ parent::__construct( $generator, $file_cache, [ 'wavatar' ] );
+ }
+
+ /**
+ * Retrieves the filename (including the sub-directory and file extension).
+ *
+ * @param string $identity The identity (mail address) hash. Ignored.
+ * @param int $size The requested size in pixels.
+ *
+ * @return string
+ */
+ protected function get_filename( $identity, $size ) {
+ return "wavatar/{$this->get_sub_dir( $identity )}/{$identity}-{$size}.png";
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-bird-avatar.php b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-bird-avatar.php
new file mode 100644
index 0000000..6dad088
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-bird-avatar.php
@@ -0,0 +1,109 @@
+
+ *
+ * @phpstan-type PartType value-of
+ * @phpstan-type PartsTemplate array
+ * @phpstan-type AllPossibleParts array
+ * @phpstan-type RandomizedParts array
+ * @phpstan-type AdditionalArguments array{}
+ */
+class Bird_Avatar extends PNG_Parts_Generator {
+
+ /**
+ * All Bird parts in their natural order.
+ *
+ * @since 2.7.0
+ */
+ private const PARTS = [ 'tail', 'hoop', 'body', 'wing', 'eyes', 'beak', 'accessoire' ];
+
+ /**
+ * Creates a new instance.
+ *
+ * @param Images\Editor $editor The image editing handler.
+ * @param Images\PNG $png The PNG image helper.
+ * @param Number_Generator $number_generator A pseudo-random number generator.
+ * @param Site_Transients $site_transients The site transients handler.
+ */
+ public function __construct(
+ Images\Editor $editor,
+ Images\PNG $png,
+ Number_Generator $number_generator,
+ Site_Transients $site_transients
+ ) {
+ parent::__construct(
+ \AVATAR_PRIVACY_PLUGIN_PATH . '/public/images/birds',
+ self::PARTS,
+ 512,
+ $editor,
+ $png,
+ $number_generator,
+ $site_transients
+ );
+ }
+
+ /**
+ * Renders the avatar from its parts, using any of the given additional arguments.
+ *
+ * @since 2.5.0 Returns a resource or GdImage instance, depending on the PHP version.
+ *
+ * @param array $parts The (randomized) avatar parts.
+ * @param array $args Any additional arguments defined by the subclass.
+ *
+ * @return resource|GdImage
+ *
+ * @phpstan-param RandomizedParts $parts
+ * @phpstan-param AdditionalArguments $args
+ */
+ protected function render_avatar( array $parts, array $args ) {
+ // Create background.
+ $bird = $this->create_image( 'transparent' );
+
+ // Add parts.
+ foreach ( $parts as $file ) {
+ $this->combine_images( $bird, $file );
+ }
+
+ return $bird;
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-cat-avatar.php b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-cat-avatar.php
new file mode 100644
index 0000000..48ac71b
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-cat-avatar.php
@@ -0,0 +1,109 @@
+
+ *
+ * @phpstan-type PartType value-of
+ * @phpstan-type PartsTemplate array
+ * @phpstan-type AllPossibleParts array
+ * @phpstan-type RandomizedParts array
+ * @phpstan-type AdditionalArguments array{}
+ */
+class Cat_Avatar extends PNG_Parts_Generator {
+
+ /**
+ * All Cat parts in their natural order.
+ *
+ * @since 2.7.0
+ */
+ private const PARTS = [ 'body', 'fur', 'eyes', 'mouth', 'accessoire' ];
+
+ /**
+ * Creates a new instance.
+ *
+ * @param Images\Editor $editor The image editing handler.
+ * @param Images\PNG $png The PNG image helper.
+ * @param Number_Generator $number_generator A pseudo-random number generator.
+ * @param Site_Transients $site_transients The site transients handler.
+ */
+ public function __construct(
+ Images\Editor $editor,
+ Images\PNG $png,
+ Number_Generator $number_generator,
+ Site_Transients $site_transients
+ ) {
+ parent::__construct(
+ \AVATAR_PRIVACY_PLUGIN_PATH . '/public/images/cats',
+ self::PARTS,
+ 512,
+ $editor,
+ $png,
+ $number_generator,
+ $site_transients
+ );
+ }
+
+ /**
+ * Renders the avatar from its parts, using any of the given additional arguments.
+ *
+ * @since 2.5.0 Returns a resource or GdImage instance, depending on the PHP version.
+ *
+ * @param array $parts The (randomized) avatar parts.
+ * @param array $args Any additional arguments defined by the subclass.
+ *
+ * @return resource|GdImage
+ *
+ * @phpstan-param RandomizedParts $parts
+ * @phpstan-param AdditionalArguments $args
+ */
+ protected function render_avatar( array $parts, array $args ) {
+ // Create background.
+ $cat = $this->create_image( 'transparent' );
+
+ // Add parts.
+ foreach ( $parts as $file ) {
+ $this->combine_images( $cat, $file );
+ }
+
+ return $cat;
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-jdenticon.php b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-jdenticon.php
new file mode 100644
index 0000000..f9c0bc8
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-jdenticon.php
@@ -0,0 +1,74 @@
+
+ */
+class Jdenticon implements Generator {
+
+ /**
+ * The identicon instance.
+ *
+ * @var \Avatar_Privacy\Vendor\Jdenticon\Identicon
+ */
+ private $identicon;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.1.0 Parameter $identicon added.
+ *
+ * @param \Avatar_Privacy\Vendor\Jdenticon\Identicon $identicon The Jdenticon implementation.
+ */
+ public function __construct( \Avatar_Privacy\Vendor\Jdenticon\Identicon $identicon ) {
+ $this->identicon = $identicon;
+ }
+
+ /**
+ * Builds an icon based on the given seed returns the image data.
+ *
+ * @param string $seed The seed data (hash).
+ * @param int $size Optional. The size in pixels. Default 128 (but really ignored).
+ *
+ * @return string
+ */
+ public function build( $seed, $size = 128 ) {
+ $this->identicon->setHash( $seed );
+ $this->identicon->setSize( $size );
+
+ return $this->identicon->getImageData( 'svg' );
+ }
+
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-monster-id.php b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-monster-id.php
new file mode 100644
index 0000000..73f2db8
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-monster-id.php
@@ -0,0 +1,460 @@
+
+ * @author Scott Sherrill-Mix
+ *
+ * @phpstan-type PartType value-of
+ * @phpstan-type PartsTemplate array
+ * @phpstan-type AllPossibleParts array
+ * @phpstan-type RandomizedParts array
+ *
+ * @phpstan-type Hue int<-359, 359>
+ * @phpstan-type Saturation int<0, 100>
+ * @phpstan-type AdditionalArguments array{ hue: Hue, saturation: Saturation }
+ */
+class Monster_ID extends PNG_Parts_Generator {
+ // Monster ports.
+ private const PART_LEGS = 'legs';
+ private const PART_HAIR = 'hair';
+ private const PART_ARMS = 'arms';
+ private const PART_BODY = 'body';
+ private const PART_EYES = 'eyes';
+ private const PART_MOUTH = 'mouth';
+
+ /**
+ * All Monster parts in their natural order.
+ *
+ * @since 2.7.0
+ */
+ private const PARTS = [
+ self::PART_LEGS,
+ self::PART_HAIR,
+ self::PART_ARMS,
+ self::PART_BODY,
+ self::PART_EYES,
+ self::PART_MOUTH,
+ ];
+
+ const COLOR_HUE = 'hue';
+ const COLOR_SATURATION = 'saturation';
+
+ const SAME_COLOR_PARTS = [
+ 'arms_S8.png' => true,
+ 'legs_S5.png' => true,
+ 'legs_S13.png' => true,
+ 'mouth_S5.png' => true,
+ 'mouth_S4.png' => true,
+ ];
+ const SPECIFIC_COLOR_PARTS = [
+ // Blue (values are hue degrees).
+ 'hair_S4.png' => [ 216, 270 ],
+ // Red (values are hue degrees).
+ 'arms_S2.png' => [ -18, 18 ],
+ 'hair_S6.png' => [ -18, 18 ],
+ 'mouth_9.png' => [ -18, 18 ],
+ 'mouth_6.png' => [ -18, 18 ],
+ 'mouth_S2.png' => [ -18, 18 ],
+ ];
+ const RANDOM_COLOR_PARTS = [
+ 'arms_3.png' => true,
+ 'arms_4.png' => true,
+ 'arms_5.png' => true,
+ 'arms_S1.png' => true,
+ 'arms_S3.png' => true,
+ 'arms_S5.png' => true,
+ 'arms_S6.png' => true,
+ 'arms_S7.png' => true,
+ 'arms_S9.png' => true,
+ 'hair_S1.png' => true,
+ 'hair_S2.png' => true,
+ 'hair_S3.png' => true,
+ 'hair_S5.png' => true,
+ 'legs_1.png' => true,
+ 'legs_2.png' => true,
+ 'legs_3.png' => true,
+ 'legs_5.png' => true,
+ 'legs_S1.png' => true,
+ 'legs_S2.png' => true,
+ 'legs_S3.png' => true,
+ 'legs_S4.png' => true,
+ 'legs_S6.png' => true,
+ 'legs_S7.png' => true,
+ 'legs_S10.png' => true,
+ 'legs_S12.png' => true,
+ 'mouth_3.png' => true,
+ 'mouth_4.png' => true,
+ 'mouth_7.png' => true,
+ 'mouth_10.png' => true,
+ 'mouth_S6.png' => true,
+ ];
+ // Generated from get_parts_dimensions.
+ const PART_OPTIMIZATION = [
+ 'legs_1.png' => [ [ 17, 99 ], [ 58, 119 ] ],
+ 'legs_2.png' => [ [ 25, 94 ], [ 54, 119 ] ],
+ 'legs_3.png' => [ [ 34, 99 ], [ 48, 117 ] ],
+ 'legs_4.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'legs_5.png' => [ [ 28, 91 ], [ 64, 119 ] ],
+ 'legs_S1.png' => [ [ 17, 105 ], [ 53, 118 ] ],
+ 'legs_S10.png' => [ [ 42, 88 ], [ 54, 118 ] ],
+ 'legs_S11.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'legs_S12.png' => [ [ 15, 107 ], [ 60, 115 ] ],
+ 'legs_S13.png' => [ [ 8, 106 ], [ 69, 119 ] ],
+ 'legs_S2.png' => [ [ 23, 99 ], [ 56, 117 ] ],
+ 'legs_S3.png' => [ [ 30, 114 ], [ 53, 118 ] ],
+ 'legs_S4.png' => [ [ 12, 100 ], [ 50, 116 ] ],
+ 'legs_S5.png' => [ [ 17, 109 ], [ 63, 118 ] ],
+ 'legs_S6.png' => [ [ 10, 100 ], [ 56, 119 ] ],
+ 'legs_S7.png' => [ [ 33, 78 ], [ 73, 114 ] ],
+ 'legs_S8.png' => [ [ 33, 95 ], [ 102, 116 ] ],
+ 'legs_S9.png' => [ [ 42, 75 ], [ 72, 116 ] ],
+ 'hair_1.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'hair_2.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'hair_3.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'hair_4.png' => [ [ 34, 84 ], [ 0, 41 ] ],
+ 'hair_5.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'hair_S1.png' => [ [ 25, 96 ], [ 2, 58 ] ],
+ 'hair_S2.png' => [ [ 45, 86 ], [ 3, 51 ] ],
+ 'hair_S3.png' => [ [ 15, 105 ], [ 4, 48 ] ],
+ 'hair_S4.png' => [ [ 15, 102 ], [ 1, 51 ] ],
+ 'hair_S5.png' => [ [ 16, 95 ], [ 4, 65 ] ],
+ 'hair_S6.png' => [ [ 28, 88 ], [ 1, 48 ] ],
+ 'hair_S7.png' => [ [ 51, 67 ], [ 6, 49 ] ],
+ 'arms_1.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'arms_2.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'arms_3.png' => [ [ 2, 119 ], [ 20, 72 ] ],
+ 'arms_4.png' => [ [ 2, 115 ], [ 14, 98 ] ],
+ 'arms_5.png' => [ [ 5, 119 ], [ 17, 90 ] ],
+ 'arms_S1.png' => [ [ 0, 117 ], [ 23, 109 ] ],
+ 'arms_S2.png' => [ [ 2, 118 ], [ 8, 75 ] ],
+ 'arms_S3.png' => [ [ 2, 116 ], [ 17, 93 ] ],
+ 'arms_S4.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'arms_S5.png' => [ [ 1, 115 ], [ 6, 40 ] ],
+ 'arms_S6.png' => [ [ 3, 117 ], [ 7, 90 ] ],
+ 'arms_S7.png' => [ [ 1, 116 ], [ 21, 67 ] ],
+ 'arms_S8.png' => [ [ 2, 119 ], [ 18, 98 ] ],
+ 'arms_S9.png' => [ [ 8, 110 ], [ 18, 65 ] ],
+ 'body_1.png' => [ [ 22, 99 ], [ 17, 90 ] ],
+ 'body_10.png' => [ [ 37, 85 ], [ 22, 98 ] ],
+ 'body_11.png' => [ [ 23, 108 ], [ 10, 106 ] ],
+ 'body_12.png' => [ [ 9, 113 ], [ 6, 112 ] ],
+ 'body_13.png' => [ [ 29, 98 ], [ 26, 97 ] ],
+ 'body_14.png' => [ [ 31, 93 ], [ 25, 94 ] ],
+ 'body_15.png' => [ [ 23, 100 ], [ 20, 97 ] ],
+ 'body_2.png' => [ [ 14, 104 ], [ 16, 89 ] ],
+ 'body_3.png' => [ [ 22, 102 ], [ 22, 93 ] ],
+ 'body_4.png' => [ [ 18, 107 ], [ 22, 103 ] ],
+ 'body_5.png' => [ [ 22, 101 ], [ 12, 99 ] ],
+ 'body_6.png' => [ [ 24, 103 ], [ 10, 92 ] ],
+ 'body_7.png' => [ [ 22, 99 ], [ 7, 92 ] ],
+ 'body_8.png' => [ [ 21, 103 ], [ 12, 95 ] ],
+ 'body_9.png' => [ [ 20, 99 ], [ 9, 91 ] ],
+ 'body_S1.png' => [ [ 22, 102 ], [ 25, 96 ] ],
+ 'body_S2.png' => [ [ 35, 94 ], [ 17, 96 ] ],
+ 'body_S3.png' => [ [ 30, 100 ], [ 20, 102 ] ],
+ 'body_S4.png' => [ [ 26, 104 ], [ 14, 92 ] ],
+ 'body_S5.png' => [ [ 26, 100 ], [ 16, 97 ] ],
+ 'eyes_1.png' => [ [ 43, 76 ], [ 31, 48 ] ],
+ 'eyes_10.png' => [ [ 40, 80 ], [ 32, 50 ] ],
+ 'eyes_11.png' => [ [ 41, 82 ], [ 31, 54 ] ],
+ 'eyes_12.png' => [ [ 45, 78 ], [ 30, 50 ] ],
+ 'eyes_13.png' => [ [ 10, 111 ], [ 10, 34 ] ],
+ 'eyes_14.png' => [ [ 40, 79 ], [ 21, 56 ] ],
+ 'eyes_15.png' => [ [ 49, 72 ], [ 38, 43 ] ],
+ 'eyes_2.png' => [ [ 37, 72 ], [ 36, 53 ] ],
+ 'eyes_3.png' => [ [ 47, 75 ], [ 31, 53 ] ],
+ 'eyes_4.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'eyes_5.png' => [ [ 44, 77 ], [ 43, 52 ] ],
+ 'eyes_6.png' => [ [ 43, 57 ], [ 35, 49 ] ],
+ 'eyes_7.png' => [ [ 62, 76 ], [ 35, 49 ] ],
+ 'eyes_8.png' => [ [ 45, 72 ], [ 23, 51 ] ],
+ 'eyes_9.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'eyes_S1.png' => [ [ 41, 82 ], [ 29, 52 ] ],
+ 'eyes_S2.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'eyes_S3.png' => [ [ 34, 88 ], [ 39, 52 ] ],
+ 'eyes_S4.png' => [ [ 47, 74 ], [ 39, 51 ] ],
+ 'eyes_S5.png' => [ [ 41, 76 ], [ 36, 51 ] ],
+ 'mouth_1.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ 'mouth_10.png' => [ [ 40, 84 ], [ 56, 89 ] ],
+ 'mouth_2.png' => [ [ 57, 65 ], [ 56, 61 ] ],
+ 'mouth_3.png' => [ [ 38, 85 ], [ 54, 72 ] ],
+ 'mouth_4.png' => [ [ 44, 77 ], [ 56, 81 ] ],
+ 'mouth_5.png' => [ [ 53, 72 ], [ 59, 76 ] ],
+ 'mouth_6.png' => [ [ 48, 74 ], [ 56, 77 ] ],
+ 'mouth_7.png' => [ [ 51, 70 ], [ 57, 80 ] ],
+ 'mouth_8.png' => [ [ 44, 81 ], [ 64, 78 ] ],
+ 'mouth_9.png' => [ [ 49, 75 ], [ 52, 103 ] ],
+ 'mouth_S1.png' => [ [ 47, 82 ], [ 57, 73 ] ],
+ 'mouth_S2.png' => [ [ 45, 71 ], [ 65, 84 ] ],
+ 'mouth_S3.png' => [ [ 48, 77 ], [ 56, 86 ] ],
+ 'mouth_S4.png' => [ [ 46, 77 ], [ 56, 73 ] ],
+ 'mouth_S5.png' => [ [ 55, 69 ], [ 55, 98 ] ],
+ 'mouth_S6.png' => [ [ 40, 79 ], [ 56, 72 ] ],
+ 'mouth_S7.png' => [ [ 999999, 0 ], [ 999999, 0 ] ],
+ ];
+
+ /**
+ * The color conversion helper.
+ *
+ * @since 2.7.0
+ *
+ * @var Images\Color
+ */
+ protected Images\Color $color;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.1.0 Parameter $plugin_file removed.
+ *
+ * @param Images\Editor $editor The image editing handler.
+ * @param Images\PNG $png The PNG image helper.
+ * @param Images\Color $color The color conversion helper.
+ * @param Number_Generator $number_generator A pseudo-random number generator.
+ * @param Site_Transients $site_transients The site transients handler.
+ */
+ public function __construct(
+ Images\Editor $editor,
+ Images\PNG $png,
+ Images\Color $color,
+ Number_Generator $number_generator,
+ Site_Transients $site_transients
+ ) {
+ parent::__construct(
+ \AVATAR_PRIVACY_PLUGIN_PATH . '/public/images/monster-id',
+ self::PARTS,
+ 120,
+ $editor,
+ $png,
+ $number_generator,
+ $site_transients
+ );
+
+ $this->color = $color;
+ }
+
+ /**
+ * Prepares additional arguments needed for rendering the avatar image.
+ *
+ * @param string $seed The seed data (hash).
+ * @param int $size The size in pixels.
+ * @param array $parts The (randomized) avatar parts.
+ *
+ * @return array
+ *
+ * @phpstan-param RandomizedParts $parts
+ * @phpstan-return AdditionalArguments
+ */
+ protected function get_additional_arguments( $seed, $size, array $parts ) {
+ return [
+ self::COLOR_HUE => $this->get_hue(),
+ self::COLOR_SATURATION => $this->get_saturation( 25, 100 ),
+ ];
+ }
+
+ /**
+ * Renders the avatar from its parts, using any of the given additional arguments.
+ *
+ * @since 2.5.0 Returns a resource or GdImage instance, depending on the PHP version.
+ *
+ * @param array $parts The (randomized) avatar parts.
+ * @param array $args Any additional arguments defined by the subclass.
+ *
+ * @return resource|GdImage
+ *
+ * @phpstan-param RandomizedParts $parts
+ * @phpstan-param AdditionalArguments $args
+ */
+ protected function render_avatar( array $parts, array $args ) {
+ // Create background.
+ $monster = $this->png->create_from_file( "{$this->parts_dir}/back.png" );
+
+ // Add parts.
+ foreach ( $parts as $part => $file ) {
+ $im = $this->png->create_from_file( "{$this->parts_dir}/{$file}" );
+
+ // Randomly color body parts.
+ if ( self::PART_BODY === $part || isset( self::SAME_COLOR_PARTS[ $file ] ) ) {
+ // Use the main color.
+ $this->colorize_image( $im, $args[ self::COLOR_HUE ], $args[ self::COLOR_SATURATION ], $file );
+ } elseif ( isset( self::RANDOM_COLOR_PARTS[ $file ] ) ) {
+ $this->colorize_image(
+ $im,
+ $this->get_hue(),
+ $this->get_saturation( 25, 100 ),
+ $file
+ );
+ } elseif ( isset( self::SPECIFIC_COLOR_PARTS[ $file ] ) ) {
+ // Retrieve specific hue range.
+ list( $low, $high ) = self::SPECIFIC_COLOR_PARTS[ $file ];
+
+ $this->colorize_image(
+ $im,
+ $this->get_hue( $low, $high ),
+ $this->get_saturation( 25, 100 ),
+ $file
+ );
+ }
+
+ $this->combine_images( $monster, $im );
+ }
+
+ return $monster;
+ }
+
+ /**
+ * Adds color to the given image.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ * @since 2.3.0 Name changed to colorize_image() for consistency.
+ * @since 2.5.0 Parameter $image can now also be a GdImage. Returns a resource
+ * or GdImage instance, depending on the PHP version.
+ * @since 2.7.0 Default values removed and PHPStan annotation added.
+ * Parameter $part renamed to $file.
+ *
+ * @param resource|GdImage $image The image.
+ * @param int $hue The hue (0-360).
+ * @param int $saturation The saturation (0-100).
+ * @param string $file The image filename.
+ *
+ * @return resource|GdImage The image, for chaining.
+ *
+ * @phpstan-param Hue $hue
+ * @phpstan-param Saturation $saturation
+ */
+ protected function colorize_image( $image, $hue, $saturation, $file ) {
+ // Ensure non-negative hue.
+ $hue = $this->color->normalize_hue( $hue );
+
+ \imageAlphaBlending( $image, false );
+ if ( isset( self::PART_OPTIMIZATION[ $file ] ) ) {
+ $xmin = self::PART_OPTIMIZATION[ $file ][0][0];
+ $xmax = self::PART_OPTIMIZATION[ $file ][0][1];
+ $ymin = self::PART_OPTIMIZATION[ $file ][1][0];
+ $ymax = self::PART_OPTIMIZATION[ $file ][1][1];
+ } else {
+ $xmin = 0;
+ $xmax = \imageSX( $image ) - 1;
+ $ymin = 0;
+ $ymax = \imageSY( $image ) - 1;
+ }
+
+ for ( $i = $xmin; $i <= $xmax; $i++ ) {
+ for ( $j = $ymin; $j <= $ymax; $j++ ) {
+ $rgb = \imageColorAt( $image, $i, $j );
+ $r = ( $rgb >> 16 ) & 0xFF;
+ $g = ( $rgb >> 8 ) & 0xFF;
+ $b = $rgb & 0xFF;
+ $alpha = ( $rgb & 0x7F000000 ) >> 24;
+ $lightness = (int) ( ( $r + $g + $b ) / 3 / 255 * Images\Color::MAX_PERCENT );
+ if ( $lightness > 10 && $lightness < 99 && $alpha < 115 ) {
+ // Convert HSL color to RGB.
+ list( $r, $g, $b ) = $this->color->hsl_to_rgb( $hue, $saturation, $lightness );
+
+ // Change color of pixel.
+ $color = \imageColorAllocateAlpha( $image, $r, $g, $b, $alpha );
+ if ( false !== $color ) {
+ \imageSetPixel( $image, $i, $j, $color );
+ }
+ }
+ }
+ }
+ \imageAlphaBlending( $image, true );
+
+ return $image;
+ }
+
+ /**
+ * Generates a random hue.
+ *
+ * @param int $min Optional. The lower bound. Default 0.
+ * @param int $max Optional. The upper bound. Default 359.
+ *
+ * @return int
+ *
+ * @phpstan-param Hue $min
+ * @phpstan-param Hue $max
+ * @phpstan-return Hue
+ */
+ protected function get_hue( int $min = 0, int $max = Images\Color::MAX_DEGREE - 1 ) {
+ assert( $min > - Images\Color::MAX_DEGREE && $max < Images\Color::MAX_DEGREE && $min < $max );
+
+ /**
+ * Return a pseudo-random hue between the lower and the upper bound.
+ *
+ * @phpstan-var Hue
+ */
+ return $this->number_generator->get( $min, $max );
+ }
+
+ /**
+ * Generates a random saturation level.
+ *
+ * @param int $min Optional. The lower bound. Default 0 percent.
+ * @param int $max Optional. The upper bound. Default 100 percent.
+ *
+ * @return int
+ *
+ * @phpstan-param Saturation $min
+ * @phpstan-param Saturation $max
+ * @phpstan-return Saturation
+ */
+ protected function get_saturation( int $min = 0, int $max = Images\Color::MAX_PERCENT ) {
+ assert( $min >= 0 && $max <= Images\Color::MAX_PERCENT && $min < $max );
+
+ /**
+ * Return a pseudo-random saturation level between the lower and the upper bound.
+ *
+ * @phpstan-var Saturation
+ */
+ return $this->number_generator->get( $min, $max );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-parts-generator.php b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-parts-generator.php
new file mode 100644
index 0000000..17bdf5f
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-parts-generator.php
@@ -0,0 +1,321 @@
+
+ *
+ * @phpstan-type PartType string
+ * @phpstan-type PartsTemplate array
+ * @phpstan-type AllPossibleParts array
+ * @phpstan-type RandomizedParts array
+ * @phpstan-type AdditionalArguments array
+ */
+abstract class Parts_Generator implements Generator {
+
+ /**
+ * The path to the monster parts image files.
+ *
+ * @var string
+ */
+ protected string $parts_dir;
+
+ /**
+ * An array of part types.
+ *
+ * @var string[]
+ */
+ protected array $part_types;
+
+ /**
+ * Lists of files, indexed by part types.
+ *
+ * @since 2.4.0 Property renamed to $all_parts, visibility changed to private.
+ *
+ * @var array {
+ * @type string[] $type An array of files.
+ * }
+ *
+ * @phpstan-var array
+ */
+ private array $all_parts;
+
+ /**
+ * The random number generator.
+ *
+ * @var Number_Generator
+ */
+ protected Number_Generator $number_generator;
+
+ /**
+ * The site transients handler.
+ *
+ * @var Site_Transients
+ */
+ private Site_Transients $site_transients;
+
+ /**
+ * Creates a new generator.
+ *
+ * @param string $parts_dir The directory containing our image parts.
+ * @param string[] $part_types The valid part types for this generator.
+ * @param Number_Generator $number_generator A pseudo-random number generator.
+ * @param Site_Transients $site_transients The site transients handler.
+ */
+ public function __construct(
+ $parts_dir,
+ array $part_types,
+ Number_Generator $number_generator,
+ Site_Transients $site_transients
+ ) {
+ $this->parts_dir = $parts_dir;
+ $this->part_types = $part_types;
+ $this->number_generator = $number_generator;
+ $this->site_transients = $site_transients;
+ }
+
+ /**
+ * Builds an icon based on the given seed returns the image data.
+ *
+ * @param string $seed The seed data (hash).
+ * @param int $size The size in pixels.
+ *
+ * @return string|false
+ */
+ public function build( $seed, $size ) {
+ try {
+ // Set randomness from seed.
+ $this->number_generator->seed( $seed );
+
+ // Throw the dice for avatar parts.
+ $parts = $this->get_randomized_parts();
+
+ // Prepare any additional arguments needed.
+ $args = $this->get_additional_arguments( $seed, $size, $parts );
+
+ // Build the avatar image in its final size.
+ return $this->get_avatar( $size, $parts, $args );
+ } catch ( \Exception $e ) {
+ // Something went wrong but don't want to mess up blog layout.
+ return false;
+ } finally {
+ // Reset randomness to something unknonwn.
+ $this->number_generator->reset();
+ }
+ }
+
+ /**
+ * Retrieves the "randomized" parts for the avatar being built.
+ *
+ * @return array A simple array of files, indexed by part.
+ *
+ * @throws Part_Files_Not_Found_Exception The part files could not be found.
+ *
+ * @phpstan-return RandomizedParts
+ */
+ protected function get_randomized_parts() {
+ return $this->randomize_parts( $this->get_parts() );
+ }
+
+ /**
+ * Prepares any additional arguments needed for rendering the avatar image.
+ *
+ * The arguments will be passed to `render_avatar()`.
+ *
+ * @param string $seed The seed data (hash).
+ * @param int $size The size in pixels.
+ * @param array $parts The (randomized) avatar parts.
+ *
+ * @return array
+ *
+ * @phpstan-param RandomizedParts $parts
+ * @phpstan-return AdditionalArguments
+ */
+ protected function get_additional_arguments( $seed, $size, array $parts ) {
+ return [];
+ }
+
+ /**
+ * Renders the avatar from its parts in the given size, using any of the
+ * optional additional arguments.
+ *
+ * @param int $size The target image size in pixels.
+ * @param array $parts The (randomized) avatar parts.
+ * @param array $args Any additional arguments defined by the subclass.
+ *
+ * @return string The image data (bytes).
+ *
+ * @phpstan-param RandomizedParts $parts
+ * @phpstan-param AdditionalArguments $args
+ */
+ abstract protected function get_avatar( $size, array $parts, array $args );
+
+ /**
+ * Throws the dice for parts.
+ *
+ * @param array $parts An array of arrays containing all parts definitions.
+ *
+ * @return array A simple array of part definitions, indexed by type.
+ *
+ * @phpstan-param AllPossibleParts $parts
+ * @phpstan-return RandomizedParts
+ */
+ protected function randomize_parts( array $parts ) {
+ $randomized_parts = [];
+
+ // Throw the dice for every part type.
+ foreach ( $parts as $type => $list ) {
+ $randomized_parts[ $type ] = $list[ $this->get_random_part_index( $type, \count( $list ) ) ];
+ }
+
+ return $randomized_parts;
+ }
+
+ /**
+ * Generates a random but valid part index based on the type and number of parts.
+ *
+ * @param string $type The part type.
+ * @param int $count The number of different parts of the type.
+ *
+ * @return int
+ */
+ protected function get_random_part_index( $type, $count ) {
+ return $this->number_generator->get( 0, $count - 1 );
+ }
+
+ /**
+ * Retrieves the avatar parts image files.
+ *
+ * @return array {
+ * An array of file lists indexed by the part name.
+ *
+ * @type string[] $part An array of part definitions (the exact content
+ * is determined by the subclasses).
+ * }
+ *
+ * @throws Part_Files_Not_Found_Exception The part files could not be found.
+ *
+ * @phpstan-return AllPossibleParts
+ */
+ protected function get_parts() {
+ if ( empty( $this->all_parts ) ) {
+ // Calculate transient key.
+ $basename = \basename( $this->parts_dir );
+ $key = "avatar_privacy_{$basename}_parts";
+
+ // Check existence of transient first.
+ $cached_parts = $this->site_transients->get( $key );
+ if ( \is_array( $cached_parts ) && ! empty( $cached_parts ) ) {
+ /**
+ * The cached parts look good, let's use those.
+ *
+ * @phpstan-var AllPossibleParts $cached_parts
+ */
+ $this->all_parts = $cached_parts;
+ } else {
+ // Look at the actual filesystem.
+ $this->all_parts = $this->build_parts_array();
+
+ // Only store transient if we got a result.
+ if ( ! empty( $this->all_parts ) ) {
+ $this->site_transients->set( $key, $this->all_parts, \YEAR_IN_SECONDS );
+ }
+ }
+ }
+
+ return $this->all_parts;
+ }
+
+ /**
+ * Builds a sorted array of parts.
+ *
+ * @return array {
+ * An array of part type definitions.
+ *
+ * @type string $type The part definition list, indexed by type.
+ * }
+ *
+ * @throws Part_Files_Not_Found_Exception The part files could not be found.
+ *
+ * @phpstan-return AllPossibleParts
+ */
+ protected function build_parts_array() {
+ // Make sure the keys are in the correct order.
+ $empty_parts = \array_fill_keys( $this->part_types, [] );
+
+ // Read part definitions.
+ $parts = $this->read_parts_from_filesystem( $empty_parts );
+
+ // Raise an exception if there were no files found.
+ if ( $parts === $empty_parts ) {
+ throw new Part_Files_Not_Found_Exception( "Could not find parts images in {$this->parts_dir}" );
+ }
+
+ return $this->sort_parts( $parts );
+ }
+
+ /**
+ * Retrieves an array of SVG part type definitions.
+ *
+ * @param array $parts An array of empty arrays indexed by part type.
+ *
+ * @return array The same array, but now containing the part type definitions.
+ *
+ * @phpstan-param array $parts
+ * @phpstan-return AllPossibleParts
+ */
+ abstract protected function read_parts_from_filesystem( array $parts );
+
+ /**
+ * Sorts the parts array to be independent of filesystem sort order.
+ *
+ * @param array $parts {
+ * An array of part type definitions.
+ *
+ * @type string $type The part definition list, indexed by type.
+ * }
+ *
+ * @return array
+ *
+ * @phpstan-param AllPossibleParts $parts
+ * @phpstan-return AllPossibleParts
+ */
+ abstract protected function sort_parts( array $parts );
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-png-parts-generator.php b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-png-parts-generator.php
new file mode 100644
index 0000000..b0fc3fa
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-png-parts-generator.php
@@ -0,0 +1,383 @@
+
+ * @author Scott Sherrill-Mix
+ *
+ * @phpstan-import-type PartsTemplate from Parts_Generator
+ * @phpstan-import-type AllPossibleParts from Parts_Generator
+ * @phpstan-import-type RandomizedParts from Parts_Generator
+ * @phpstan-import-type AdditionalArguments from Parts_Generator
+ *
+ * @phpstan-type BoundsTuple array{ 0: int, 1: int }
+ */
+abstract class PNG_Parts_Generator extends Parts_Generator {
+
+ // Units used in HSL colors.
+ /**
+ * Use Image\Color::MAX_PERCENT instead.
+ *
+ * @deprecated 2.7.0
+ */
+ const PERCENT = Images\Color::MAX_PERCENT;
+ /**
+ * Use Image\Color::MAX_DEGREE instead.
+ *
+ * @deprecated 2.7.0
+ */
+ const DEGREE = Images\Color::MAX_DEGREE;
+
+ /**
+ * The base size of the generated avatar.
+ *
+ * @var int
+ */
+ protected int $size;
+
+ /**
+ * The image editor support class.
+ *
+ * @var Images\Editor
+ */
+ private Images\Editor $editor;
+
+ /**
+ * The PNG image helper.
+ *
+ * @var Images\PNG
+ */
+ protected Images\PNG $png;
+
+ /**
+ * Creates a new generator.
+ *
+ * @param string $parts_dir The directory containing our image parts.
+ * @param string[] $part_types The valid part types for this generator.
+ * @param int $size The width and height of the generated image (in pixels).
+ * @param Images\Editor $editor The image editing handler.
+ * @param Images\PNG $png The PNG image helper.
+ * @param Number_Generator $number_generator A pseudo-random number generator.
+ * @param Site_Transients $site_transients The site transients handler.
+ */
+ public function __construct(
+ $parts_dir,
+ array $part_types,
+ $size,
+ Images\Editor $editor,
+ Images\PNG $png,
+ Number_Generator $number_generator,
+ Site_Transients $site_transients
+ ) {
+ $this->size = $size;
+ $this->editor = $editor;
+ $this->png = $png;
+
+ parent::__construct( $parts_dir, $part_types, $number_generator, $site_transients );
+ }
+
+ /**
+ * Renders the avatar from its parts in the given size, using any of the
+ * optional additional arguments.
+ *
+ * @param int $size The target image size in pixels.
+ * @param array $parts The (randomized) avatar parts.
+ * @param array $args Any additional arguments defined by the subclass.
+ *
+ * @return string The image data (bytes).
+ *
+ * @phpstan-param RandomizedParts $parts
+ * @phpstan-param AdditionalArguments $args
+ */
+ protected function get_avatar( $size, array $parts, array $args ) {
+ // Build the avatar image in its native size.
+ $avatar = $this->render_avatar( $parts, $args );
+
+ // Resize if necessary.
+ return $this->get_resized_image_data( $avatar, $size );
+ }
+
+ /**
+ * Renders the avatar from its parts, using any of the given additional arguments.
+ *
+ * @since 2.7.0 Return type amended to include `GdImage` on PHP 8.x
+ *
+ * @param array $parts The (randomized) avatar parts.
+ * @param array $args Any additional arguments defined by the subclass.
+ *
+ * @return resource|GdImage
+ *
+ * @phpstan-param RandomizedParts $parts
+ * @phpstan-param AdditionalArguments $args
+ */
+ abstract protected function render_avatar( array $parts, array $args );
+
+ /**
+ * Resizes the image and returns the raw data.
+ *
+ * @since 2.5.0 Parameter $image can now also be a GdImage.
+ *
+ * @param resource|GdImage $image The image resource.
+ * @param int $size The size in pixels.
+ *
+ * @return string The image data (or the empty string on error).
+ */
+ protected function get_resized_image_data( $image, $size ) {
+ return $this->editor->get_resized_image_data(
+ $this->editor->create_from_image_resource( $image ), $size, $size, 'image/png'
+ );
+ }
+
+ /**
+ * Retrieves an array of SVG part type definitions.
+ *
+ * @param array $parts An array of empty arrays indexed by part type.
+ *
+ * @return array The same array, but now containing the part type definitions.
+ *
+ * @phpstan-param PartsTemplate $parts
+ * @phpstan-return AllPossibleParts
+ */
+ protected function read_parts_from_filesystem( array $parts ) {
+ // Iterate over the files in the parts directory.
+ $dir = new \FilesystemIterator(
+ $this->parts_dir,
+ \FilesystemIterator::KEY_AS_FILENAME |
+ \FilesystemIterator::CURRENT_AS_PATHNAME |
+ \FilesystemIterator::SKIP_DOTS
+ );
+
+ foreach ( $dir as $file => $path ) {
+ list( $partname, ) = \explode( '_', $file );
+ if ( isset( $parts[ $partname ] ) ) {
+ $parts[ $partname ][] = $file;
+ }
+ }
+
+ return $parts;
+ }
+
+ /**
+ * Sorts the parts array to be independent of filesystem sort order.
+ *
+ * @param array $parts {
+ * An array of part type definitions.
+ *
+ * @type string $type The part definition list, indexed by type.
+ * }
+ *
+ * @return array
+ *
+ * @phpstan-param AllPossibleParts $parts
+ * @phpstan-return AllPossibleParts
+ */
+ protected function sort_parts( array $parts ) {
+ foreach ( $parts as $key => $value ) {
+ \sort( $parts[ $key ], \SORT_NATURAL );
+ }
+
+ return $parts;
+ }
+
+ /**
+ * Creates a GD image of the chosen type with the set avatar size for width
+ * and height.
+ *
+ * @since 2.3.0
+ * @since 2.5.0 Returns a resource or GdImage instance, depending on the PHP version.
+ *
+ * @param string $type The type of background to create. Valid: 'white', 'black', 'transparent'.
+ *
+ * @return resource|GdImage
+ *
+ * @throws \RuntimeException The image could not be copied.
+ */
+ protected function create_image( $type ) {
+ return $this->png->create( $type, $this->size, $this->size );
+ }
+
+ /**
+ * Copies an image onto an existing base image. Image parts are loaded from
+ * the parts directory if a filename is given, assuming the avatar size for
+ * width and height.
+ *
+ * The GD image (resource) is freed after copying.
+ *
+ * @since 2.5.0 Parameters $base and $image can now also be GdImage instances.
+ *
+ * @param resource|GdImage $base The avatar image resource.
+ * @param string|resource|GdImage $image The image to be copied onto the base. Can
+ * be either the name of the image file
+ * relative to the parts directory, or an
+ * existing image resource.
+ *
+ * @return void
+ *
+ * @throws \RuntimeException The image could not be copied.
+ */
+ protected function combine_images( $base, $image ) {
+ // Load image if we are given a filename.
+ if ( \is_string( $image ) ) {
+ $image = $this->png->create_from_file( "{$this->parts_dir}/{$image}" );
+ }
+
+ $this->png->combine( $base, $image, $this->size, $this->size );
+ }
+
+ /**
+ * Determines exact dimensions for individual parts. Mainly useful for subclasses
+ * exchanging the provided images.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ * @since 2.3.0 Moved to PNG_Parts_Generator class and paramter $text removed.
+ * Use new method `get_parts_dimensions_as_text` to retrieve the
+ * human-readable array definition.
+ *
+ * @author Peter Putzer
+ * @author Scott Sherrill-Mix
+ *
+ * @return array {
+ * An array of boundary coordinates indexed by filename.
+ *
+ * @type array $file {
+ * The boundary coordinates for a single file.
+ *
+ * @type int[] $xbounds The low and high boundary on the X axis.
+ * @type int[] $ybounds The low and high boundary on the Y axis.
+ * }
+ * }
+ *
+ * @phpstan-return array
+ */
+ protected function get_parts_dimensions() {
+ /**
+ * An array of boundary coordinates indexed by filename.
+ *
+ * @phpstan-var array $bounds
+ */
+ $bounds = [];
+
+ foreach ( $this->get_parts() as $file_list ) {
+ foreach ( $file_list as $file ) {
+ $im = @\imageCreateFromPNG( "{$this->parts_dir}/{$file}" );
+
+ if ( false === $im ) {
+ // Not a valid image file.
+ continue;
+ }
+
+ $bounds[ $file ] = $this->get_image_bounds( $im );
+ }
+ }
+
+ return $bounds;
+ }
+
+ /**
+ * Determines exact dimensions for an image (i.e. not including very light or
+ * transparent pixels).
+ *
+ * @since 2.4.0 Extracted from ::get_parts_dimensions.
+ * @since 2.5.0 Parameter $im can now also be a GdImage.
+ *
+ * @author Peter Putzer
+ * @author Scott Sherrill-Mix
+ *
+ * @param resource|GdImage $im The image resource.
+ *
+ * @return array {
+ * The boundary coordinates for the image.
+ *
+ * @type int[] $xbounds The low and high boundary on the X axis.
+ * @type int[] $ybounds The low and high boundary on the Y axis.
+ * }
+ *
+ * @phpstan-return array{ 0: BoundsTuple, 1: BoundsTuple }
+ */
+ protected function get_image_bounds( $im ) {
+ $imgw = \imageSX( $im );
+ $imgh = \imageSY( $im );
+ $xbounds = [ 999999, 0 ];
+ $ybounds = [ 999999, 0 ];
+ for ( $i = 0;$i < $imgw;$i++ ) {
+ for ( $j = 0;$j < $imgh;$j++ ) {
+ $rgb = \imageColorAt( $im, $i, $j );
+ $r = ( $rgb >> 16 ) & 0xFF;
+ $g = ( $rgb >> 8 ) & 0xFF;
+ $b = $rgb & 0xFF;
+ $alpha = ( $rgb & 0x7F000000 ) >> 24;
+ $lightness = ( $r + $g + $b ) / 3 / Images\Color::MAX_RGB * Images\Color::MAX_PERCENT;
+ if ( $lightness > 10 && $lightness < 99 && $alpha < 115 ) {
+ $xbounds[0] = \min( $xbounds[0],$i );
+ $xbounds[1] = \max( $xbounds[1],$i );
+ $ybounds[0] = \min( $ybounds[0],$j );
+ $ybounds[1] = \max( $ybounds[1],$j );
+ }
+ }
+ }
+
+ return [ $xbounds, $ybounds ];
+ }
+
+ /**
+ * Prints the exact dimensions for individual parts as human-readable PHP
+ * array definitions.
+ *
+ * Mainly useful for subclasses exchanging the provided images.
+ *
+ * @since 2.3.0
+ *
+ * @return string
+ */
+ protected function get_parts_dimensions_as_text() {
+ $result = '';
+
+ foreach ( $this->get_parts_dimensions() as $part => $bounds ) {
+ list( $xbounds, $ybounds ) = $bounds;
+
+ $result .= "'$part' => [ [ {$xbounds[0]}, {$xbounds[1]} ], [ {$ybounds[0]}, {$ybounds[1]} ] ],\n";
+ }
+
+ return $result;
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-retro.php b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-retro.php
new file mode 100644
index 0000000..3983bbc
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-retro.php
@@ -0,0 +1,198 @@
+
+ * Copyright (c) 2015 Grummfy
+ * Copyright (c) 2016, 2017 Lucas Michot
+ * Copyright (c) 2019 Arjen van der Meijden
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is furnished
+ * to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * ***
+ *
+ * @package mundschenk-at/avatar-privacy
+ * @license http://www.gnu.org/licenses/gpl-2.0.html
+ */
+
+namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators;
+
+use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generator;
+
+use Avatar_Privacy\Tools\Number_Generator;
+use Avatar_Privacy\Tools\Template;
+
+use Avatar_Privacy\Vendor\Colors\RandomColor;
+
+/**
+ * Generates a "retro" SVG icon based on a hash.
+ *
+ * @since 1.0.0
+ * @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators
+ * @since 2.7.0 Now directly incorporates the SVG generation code from the
+ * deprecated package `yzalis/identicon`.
+ *
+ * @author Peter Putzer
+ * @author Grummfy
+ */
+class Retro implements Generator {
+
+ private const NUMBER_OF_PIXELS = 5;
+
+ private const DIVIDER = 3;
+
+ private const POSSIBLE_COLUMNS = [
+ 0 => [ 0, 4 ],
+ 1 => [ 1, 3 ],
+ 2 => [ 2 ],
+ ];
+
+ /**
+ * The random number generator.
+ *
+ * @since 2.3.0
+ *
+ * @var Number_Generator
+ */
+ protected Number_Generator $number_generator;
+
+
+ /**
+ * The templating handler.
+ *
+ * @since 2.7.0
+ *
+ * @var Template
+ */
+ protected Template $template;
+
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.1.0 Parameter `$identicon` added.
+ * @since 2.3.0 Parameter `$number_generator` added.
+ * @since 2.7.0 Parameter `$template` added.
+ *
+ * @param Number_Generator $number_generator A pseudo-random number generator.
+ * @param Template $template The templating handler.
+ */
+ public function __construct( Number_Generator $number_generator, Template $template ) {
+ $this->number_generator = $number_generator;
+ $this->template = $template;
+ }
+
+ /**
+ * Builds an icon based on the given seed returns the image data.
+ *
+ * @param string $seed The seed data (hash).
+ * @param int $size Optional. The size in pixels. Default 128 (but really ignored).
+ *
+ * @return string
+ */
+ public function build( $seed, $size = 128 ) {
+ // Initialize random number with seed.
+ $this->number_generator->seed( $seed );
+
+ // Generate icon.
+ $bitmap = $this->get_bitmap( \md5( $seed ) ); // The seed is already hashed, but we want to generate the same result as earlier versions did using `yzalis/identicon`.
+ $args = [
+ 'rows' => \count( $bitmap ),
+ 'columns' => \count( $bitmap[1] ),
+ 'path' => $this->draw_path( $bitmap ),
+ 'color' => RandomColor::one( [ 'luminosity' => 'bright' ] ),
+ 'bg_color' => RandomColor::one( [ 'luminosity' => 'light' ] ),
+ ];
+ $result = $this->template->get_partial( 'public/partials/retro/svg.php', $args );
+
+ // Restore randomness.
+ $this->number_generator->reset();
+
+ return $result;
+ }
+
+ /**
+ * Converts the hash into an two-dimensional array of boolean.
+ *
+ * @since 2.7.0
+ *
+ * @param string $hash The MD5 hash.
+ *
+ * @return array>
+ */
+ protected function get_bitmap( string $hash ): array {
+ $bitmap = [];
+
+ foreach ( \array_slice( \str_split( $hash, 2 ), 0, self::NUMBER_OF_PIXELS * self::DIVIDER ) as $i => $hex_tuple ) {
+ $row = (int) ( $i / self::DIVIDER );
+ $pixel = (bool) \round( \hexdec( $hex_tuple[0] ) / 10 );
+
+ foreach ( self::POSSIBLE_COLUMNS[ $i % self::DIVIDER ] as $column ) {
+ $bitmap[ $row ][ $column ] = $pixel;
+ }
+
+ \ksort( $bitmap[ $row ] );
+ }
+
+ return $bitmap;
+ }
+
+ /**
+ * Draws an SVG path from the given bitmap.
+ *
+ * @since 2.7.0
+ *
+ * @param array $bitmap A two-dimensional array of boolean pixel values.
+ *
+ * @return string
+ *
+ * @phpstan-param array> $bitmap
+ */
+ protected function draw_path( array $bitmap ): string {
+ $rects = [];
+ foreach ( $bitmap as $line_key => $line_value ) {
+ foreach ( $line_value as $col_key => $col_value ) {
+ if ( true === $col_value ) {
+ $rects[] = 'M' . $col_key . ',' . $line_key . 'h1v1h-1v-1';
+ }
+ }
+ }
+
+ return \implode( '', $rects );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-rings.php b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-rings.php
new file mode 100644
index 0000000..ac4b3c3
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-rings.php
@@ -0,0 +1,55 @@
+
+ */
+class Rings extends RingIconSVG implements Generator {
+
+ /**
+ * Builds an icon based on the given seed returns the image data.
+ *
+ * @param string $seed The seed data (hash).
+ * @param int $size The size in pixels (ignored for SVG images).
+ *
+ * @return string|false
+ */
+ public function build( $seed, $size ) {
+ return $this->generateSVGImage( $seed, true );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-robohash.php b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-robohash.php
new file mode 100644
index 0000000..da666d4
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-robohash.php
@@ -0,0 +1,275 @@
+
+ *
+ * @phpstan-type PartType value-of
+ * @phpstan-type PartsTemplate array
+ * @phpstan-type AllPossibleParts array
+ * @phpstan-type RandomizedParts array
+ * @phpstan-type AdditionalArguments array{ color: value-of, bg_color: value-of }
+ */
+class Robohash extends Parts_Generator {
+ // Robot parts.
+ private const PART_BODY = 'body';
+ private const PART_FACE = 'face';
+ private const PART_EYES = 'eyes';
+ private const PART_MOUTH = 'mouth';
+ private const PART_ACCESSORY = 'accessory';
+
+ /**
+ * All Robot parts in their natural order.
+ *
+ * @since 2.7.0
+ */
+ private const PARTS = [
+ self::PART_BODY,
+ self::PART_FACE,
+ self::PART_EYES,
+ self::PART_MOUTH,
+ self::PART_ACCESSORY,
+ ];
+
+ const COLORS = [
+ '#ff9800', // orange-500.
+ '#E53935', // red-600.
+ '#FDD835', // yellow-600.
+ '#3f51b5', // indigo-500.
+ '#03a9f4', // light-blue-500.
+ '#9c27b0', // purple-500.
+ '#009688', // teal-500.
+ '#EC407A', // pink-400.
+ '#8bc34a', // light-green-500.
+ '#795548', // brown-500.
+ ];
+
+ const BG_COLORS = [
+ /* Red */
+ '#FF8A80', // red-a100.
+ '#F48FB1', // pink-200.
+ '#ea80fc', // purple-a100.
+
+ /* Blue */
+ '#8c9eff', // indigo-a100.
+ '#80d8ff', // light-blue-a100.
+ '#CFD8DC', // blue-grey-100.
+
+ /* Green */
+ '#1DE9B6', // teal-a400.
+ '#00C853', // green-a-700.
+
+ /* Orange */
+ '#FF9E80', // deep-orange-a100.
+ '#FFE57F', // amber-a100.
+ ];
+
+ /**
+ * The templating handler.
+ *
+ * @var Template
+ */
+ private $template;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.4.0 Parameter $template added.
+ *
+ * @param Number_Generator $number_generator A pseudo-random number generator.
+ * @param Site_Transients $site_transients The transients handler.
+ * @param Template $template The templating handler.
+ */
+ public function __construct( Number_Generator $number_generator, Site_Transients $site_transients, Template $template ) {
+ parent::__construct(
+ \dirname( \AVATAR_PRIVACY_PLUGIN_FILE ) . '/public/images/robohash',
+ self::PARTS,
+ $number_generator,
+ $site_transients
+ );
+
+ $this->template = $template;
+ }
+
+ /**
+ * Prepares any additional arguments needed for rendering the avatar image.
+ *
+ * The arguments will be passed to `render_avatar()`.
+ *
+ * @param string $seed The seed data (hash).
+ * @param int $size The size in pixels.
+ * @param array $parts The (randomized) avatar parts.
+ *
+ * @return array
+ *
+ * @phpstan-param RandomizedParts $parts
+ * @phpstan-return AdditionalArguments
+ */
+ protected function get_additional_arguments( $seed, $size, array $parts ) {
+ // Randomize colors.
+ return [
+ 'color' => self::COLORS[ $this->number_generator->get( 0, \count( self::COLORS ) - 1 ) ],
+ 'bg_color' => self::BG_COLORS[ $this->number_generator->get( 0, \count( self::BG_COLORS ) - 1 ) ],
+ ];
+ }
+
+ /**
+ * Renders the avatar from its parts in the given size, using any of the
+ * optional additional arguments.
+ *
+ * @param int $size The target image size in pixels.
+ * @param array $parts The (randomized) avatar parts.
+ * @param array $args Any additional arguments defined by the subclass.
+ *
+ * @return string The image data (bytes).
+ *
+ * @phpstan-param RandomizedParts $parts
+ * @phpstan-param AdditionalArguments $args
+ */
+ protected function get_avatar( $size, array $parts, array $args ) {
+
+ // Add robot parts to arguments.
+ $args[ self::PART_BODY ] = $parts[ self::PART_BODY ];
+ $args[ self::PART_FACE ] = $parts[ self::PART_FACE ];
+ $args[ self::PART_EYES ] = $parts[ self::PART_EYES ];
+ $args[ self::PART_MOUTH ] = $parts[ self::PART_MOUTH ];
+ $args[ self::PART_ACCESSORY ] = $parts[ self::PART_ACCESSORY ];
+
+ return $this->template->get_partial( 'public/partials/robohash/svg.php', $args );
+ }
+
+ /**
+ * Retrieves an array of SVG part type definitions.
+ *
+ * @param array $parts An array of empty arrays indexed by part type.
+ *
+ * @return array The same array, but now containing the part type definitions.
+ *
+ * @phpstan-param PartsTemplate $parts
+ * @phpstan-return AllPossibleParts
+ */
+ protected function read_parts_from_filesystem( array $parts ) {
+ // Get a recursive depth-first iterator over the part type directories.
+ $dir = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator(
+ $this->parts_dir,
+ \FilesystemIterator::KEY_AS_FILENAME |
+ \FilesystemIterator::CURRENT_AS_FILEINFO |
+ \FilesystemIterator::SKIP_DOTS
+ ),
+ \RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ /**
+ * Iterate over the files in the parts directory.
+ *
+ * @var string $file
+ * @var \SplFileInfo $info
+ */
+ foreach ( $dir as $file => $info ) {
+ if ( ! $info->isFile() ) {
+ continue;
+ }
+
+ /**
+ * Extract the part from the filename.
+ *
+ * @phpstan-var PartType $partname
+ */
+ list( $partname, ) = \explode( '-', $file );
+ if ( isset( $parts[ $partname ] ) ) {
+ $parts[ $partname ][ $file ] = $this->prepare_svg_part(
+ (string) \file_get_contents( $info ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+ );
+ }
+ }
+
+ return $parts;
+ }
+
+ /**
+ * Sorts the parts array to be independent of filesystem sort order.
+ *
+ * @param array $parts {
+ * An array of part type definitions.
+ *
+ * @type string $type The part definition list, indexed by type.
+ * }
+ *
+ * @return array
+ *
+ * @phpstan-param AllPossibleParts $parts
+ * @phpstan-return AllPossibleParts
+ */
+ protected function sort_parts( array $parts ) {
+ foreach ( $parts as $key => $list ) {
+ \ksort( $list, \SORT_NATURAL );
+ $parts[ $key ] = \array_values( $list );
+ }
+
+ return $parts;
+ }
+
+ /**
+ * Prepares SVG elements for inclusion as robot parts.
+ *
+ * @param string $svg The part to include.
+ *
+ * @return string
+ */
+ protected function prepare_svg_part( $svg ) {
+ $svg = \preg_replace(
+ [
+ '#]+>(.*) #',
+ '/#26a9e0/',
+ ], [
+ '$1',
+ 'currentColor',
+ ],
+ $svg
+ );
+
+ return "{$svg} ";
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-wavatar.php b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-wavatar.php
new file mode 100644
index 0000000..edcc47d
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-wavatar.php
@@ -0,0 +1,279 @@
+
+ * @author Shamus Young
+ *
+ * @phpstan-import-type HueDegree from Images\Color
+ * @phpstan-import-type NormalizedHue from Images\Color
+ *
+ * @phpstan-type PartType value-of
+ * @phpstan-type PartsTemplate array
+ * @phpstan-type AllPossibleParts array
+ * @phpstan-type RandomizedParts array
+ * @phpstan-type AdditionalArguments array
+ */
+class Wavatar extends PNG_Parts_Generator {
+
+ // Wavatar parts.
+ private const PART_MASK = 'mask';
+ private const PART_SHINE = 'shine';
+ private const PART_FADE = 'fade';
+ private const PART_BROW = 'brow';
+ private const PART_EYES = 'eyes';
+ private const PART_PUPILS = 'pupils';
+ private const PART_MOUTH = 'mouth';
+
+ /**
+ * All Wavatar parts in their natural order.
+ *
+ * @since 2.7.0
+ */
+ private const PARTS = [
+ self::PART_FADE,
+ self::PART_MASK,
+ self::PART_SHINE,
+ self::PART_BROW,
+ self::PART_EYES,
+ self::PART_PUPILS,
+ self::PART_MOUTH,
+ ];
+
+ // Hues.
+ private const HUE_BACKGROUND = 'background_hue';
+ private const HUE_WAVATAR = 'wavatar_hue';
+
+ /**
+ * A mapping from part types to the seed positions to take their values from.
+ *
+ * @since 2.3.0
+ *
+ * @var array
+ */
+ const SEED_INDEX = [
+ // Mask and shine form the face, so they use the same random element.
+ self::PART_MASK => 1,
+ self::PART_SHINE => 1,
+ self::HUE_BACKGROUND => 3, // Not a part type, but part of the sequence.
+ self::PART_FADE => 5,
+ self::HUE_WAVATAR => 7, // Not a part type, but part of the sequence.
+ self::PART_BROW => 9,
+ self::PART_EYES => 11,
+ self::PART_PUPILS => 13,
+ self::PART_MOUTH => 15,
+ ];
+
+ /**
+ * The seed string used in the last call to `::build()`.
+ *
+ * @since 2.3.0
+ *
+ * @var string
+ */
+ private $current_seed;
+
+ /**
+ * The color conversion helper.
+ *
+ * @since 2.7.0
+ *
+ * @var Images\Color
+ */
+ protected Images\Color $color;
+
+ /**
+ * Creates a new Wavatars generator.
+ *
+ * @since 2.1.0 Parameter $plugin_file removed.
+ * @since 2.3.0 Parameter $images renamed to $editor. Parameters $png and
+ * $number_generator added.
+ * @since 2.7.0 Parameter $color added.
+ *
+ * @param Images\Editor $editor The image editing handler.
+ * @param Images\PNG $png The PNG image helper.
+ * @param Images\Color $color The color conversion helper.
+ * @param Number_Generator $number_generator A pseudo-random number generator.
+ * @param Site_Transients $site_transients The site transients handler.
+ */
+ public function __construct(
+ Images\Editor $editor,
+ Images\PNG $png,
+ Images\Color $color,
+ Number_Generator $number_generator,
+ Site_Transients $site_transients
+ ) {
+ parent::__construct(
+ \AVATAR_PRIVACY_PLUGIN_PATH . '/public/images/wavatars',
+ self::PARTS,
+ 80,
+ $editor,
+ $png,
+ $number_generator,
+ $site_transients
+ );
+
+ $this->color = $color;
+ }
+
+ /**
+ * Prepares additional arguments needed for rendering the avatar image.
+ *
+ * @param string $seed The seed data (hash).
+ * @param int $size The size in pixels.
+ * @param array $parts The (randomized) avatar parts.
+ *
+ * @return array
+ *
+ * @phpstan-param RandomizedParts $parts
+ * @phpstan-return AdditionalArguments
+ */
+ protected function get_additional_arguments( $seed, $size, array $parts ) {
+ // Also randomize the colors.
+ return [
+ self::HUE_BACKGROUND => $this->get_hue( $seed, self::HUE_BACKGROUND ),
+ self::HUE_WAVATAR => $this->get_hue( $seed, self::HUE_WAVATAR ),
+ ];
+ }
+
+ /**
+ * Renders the avatar from its parts, using any of the given additional arguments.
+ *
+ * @since 2.5.0 Returns a resource or GdImage instance, depending on the PHP version.
+ *
+ * @param array $parts The (randomized) avatar parts.
+ * @param array $args Any additional arguments defined by the subclass.
+ *
+ * @return resource|GdImage
+ *
+ * @phpstan-param RandomizedParts $parts
+ * @phpstan-param AdditionalArguments $args
+ */
+ protected function render_avatar( array $parts, array $args ) {
+ // Create background.
+ $avatar = $this->create_image( 'white' );
+
+ // Fill in the background color.
+ $this->png->fill_hsl( $avatar, $args[ self::HUE_BACKGROUND ], 94, 20, 1, 1 );
+
+ // Now add the various layers onto the image.
+ foreach ( $parts as $type => $file ) {
+ $this->combine_images( $avatar, $file );
+
+ if ( self::PART_MASK === $type ) {
+ $this->png->fill_hsl( $avatar, $args[ self::HUE_WAVATAR ], 94, 66, (int) ( $this->size / 2 ), (int) ( $this->size / 2 ) );
+ }
+ }
+
+ return $avatar;
+ }
+
+ /**
+ * Generates a random but valid part index based on the type and number of parts.
+ *
+ * @param string $type The part type.
+ * @param int $count The number of different parts of the type.
+ *
+ * @return int
+ */
+ protected function get_random_part_index( $type, $count ) {
+ return $this->seed( $this->current_seed, self::SEED_INDEX[ $type ], 2, $count );
+ }
+
+ /**
+ * Extract a "random" value from the seed string.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ *
+ * @param string $seed The seed.
+ * @param int $index The index.
+ * @param int $length The number of bytes.
+ * @param int $modulo The maximum value of the result.
+ *
+ * @return int
+ */
+ protected function seed( $seed, $index, $length, $modulo ) {
+ return \hexdec( \substr( $seed, $index, $length ) ) % $modulo;
+ }
+
+ /**
+ * Generate pseudo-random hue from the seed.
+ *
+ * @since 2.7.0
+ *
+ * @param string $seed The seed data (hash).
+ * @param string $seed_index The seed index to use for the generated hue.
+ *
+ * @return int
+ *
+ * @phpstan-param self::HUE_* $seed_index
+ * @phpstan-return NormalizedHue
+ */
+ protected function get_hue( string $seed, string $seed_index ) {
+ /**
+ * Generate hue from seed.
+ *
+ * @phpstan-var HueDegree
+ */
+ $seeded_hue = (int) ( $this->seed( $seed, self::SEED_INDEX[ $seed_index ], 2, 240 ) / 255 * Images\Color::MAX_DEGREE );
+
+ return $this->color->normalize_hue( $seeded_hue );
+ }
+
+ /**
+ * Builds an icon based on the given seed returns the image data.
+ *
+ * @param string $seed The seed data (hash).
+ * @param int $size The size in pixels.
+ *
+ * @return string|false
+ */
+ public function build( $seed, $size ) {
+ // Save seed for part randomization.
+ $this->current_seed = $seed;
+
+ return parent::build( $seed, $size );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-bowling-pin-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-bowling-pin-icon-provider.php
new file mode 100644
index 0000000..5e65756
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-bowling-pin-icon-provider.php
@@ -0,0 +1,57 @@
+
+ */
+class Bowling_Pin_Icon_Provider extends SVG_Icon_Provider {
+
+ /**
+ * Creates a new instance.
+ */
+ public function __construct() {
+ parent::__construct( [ 'bowling-pin', 'im-user-offline' ], 'shaded-cone' );
+ }
+
+ /**
+ * Retrieves the user-visible, translated name.
+ *
+ * @since 2.1.0
+ *
+ * @return string
+ */
+ public function get_name() {
+ return \__( 'Bowling Pin', 'avatar-privacy' );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-mystery-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-mystery-icon-provider.php
new file mode 100644
index 0000000..750bad1
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-mystery-icon-provider.php
@@ -0,0 +1,46 @@
+
+ */
+class Mystery_Icon_Provider extends SVG_Icon_Provider {
+
+ /**
+ * Creates a new instance.
+ */
+ public function __construct() {
+ parent::__construct( [ 'mystery', 'mystery-man', 'mm' ], 'mystery' );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-silhouette-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-silhouette-icon-provider.php
new file mode 100644
index 0000000..e8952be
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-silhouette-icon-provider.php
@@ -0,0 +1,57 @@
+
+ */
+class Silhouette_Icon_Provider extends SVG_Icon_Provider {
+
+ /**
+ * Creates a new instance.
+ */
+ public function __construct() {
+ parent::__construct( [ 'silhouette', 'view-media-artist' ], 'silhouette' );
+ }
+
+ /**
+ * Retrieves the user-visible, translated name.
+ *
+ * @since 2.1.0
+ *
+ * @return string
+ */
+ public function get_name() {
+ return \__( 'Silhouette', 'avatar-privacy' );
+ }
+}
diff --git a/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-speech-bubble-icon-provider.php b/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-speech-bubble-icon-provider.php
new file mode 100644
index 0000000..67ad4c3
--- /dev/null
+++ b/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-speech-bubble-icon-provider.php
@@ -0,0 +1,57 @@
+
+ */
+class Speech_Bubble_Icon_Provider extends SVG_Icon_Provider {
+
+ /**
+ * Creates a new instance.
+ */
+ public function __construct() {
+ parent::__construct( [ 'bubble', 'comment' ], 'comment-bubble' );
+ }
+
+ /**
+ * Retrieves the user-visible, translated name.
+ *
+ * @since 2.1.0
+ *
+ * @return string
+ */
+ public function get_name() {
+ return \__( 'Speech Bubble', 'avatar-privacy' );
+ }
+}
diff --git a/includes/avatar-privacy/class-component.php b/includes/avatar-privacy/class-component.php
new file mode 100644
index 0000000..450ce6a
--- /dev/null
+++ b/includes/avatar-privacy/class-component.php
@@ -0,0 +1,44 @@
+
+ */
+interface Component {
+
+ /**
+ * Sets up the various hooks for the plugin component.
+ *
+ * @return void
+ */
+ public function run();
+}
diff --git a/includes/avatar-privacy/class-controller.php b/includes/avatar-privacy/class-controller.php
new file mode 100644
index 0000000..e75c7f6
--- /dev/null
+++ b/includes/avatar-privacy/class-controller.php
@@ -0,0 +1,82 @@
+
+ */
+class Controller {
+
+ /**
+ * The settings page handler.
+ *
+ * @var Component[]
+ */
+ private $components = [];
+
+ /**
+ * The core plugin API.
+ *
+ * @var Core
+ */
+ private $core;
+
+ /**
+ * Creates an instance of the plugin controller.
+ *
+ * @since 2.3.0 Component parameters replaced with factory-cofigured array.
+ *
+ * @param Core $core The core API.
+ * @param Component[] $components An array of plugin components.
+ */
+ public function __construct( Core $core, array $components ) {
+ $this->core = $core;
+ $this->components = $components;
+ }
+
+ /**
+ * Starts the plugin for real.
+ *
+ * @return void
+ */
+ public function run() {
+ // Set plugin singleton.
+ Core::set_instance( $this->core );
+
+ foreach ( $this->components as $component ) {
+ $component->run();
+ }
+ }
+}
diff --git a/includes/avatar-privacy/class-core.php b/includes/avatar-privacy/class-core.php
new file mode 100644
index 0000000..75454bd
--- /dev/null
+++ b/includes/avatar-privacy/class-core.php
@@ -0,0 +1,490 @@
+
+ * @author Johannes Freudendahl
+ *
+ * @phpstan-import-type AvatarDefinition from Default_Avatars
+ * @phpstan-import-type SettingsFields from Settings
+ */
+class Core {
+
+ /**
+ * The default settings.
+ *
+ * @var Settings
+ */
+ private Settings $settings;
+
+ /**
+ * The hashing helper.
+ *
+ * @since 2.4.0
+ *
+ * @var Hasher
+ */
+ private Hasher $hasher;
+
+ /**
+ * The user data helper.
+ *
+ * @since 2.4.0
+ *
+ * @var User_Fields
+ */
+ private User_Fields $user_fields;
+
+ /**
+ * The comment author data helper.
+ *
+ * @since 2.4.0
+ *
+ * @var Comment_Author_Fields
+ */
+ private Comment_Author_Fields $comment_author_fields;
+
+ /**
+ * The default avatars API.
+ *
+ * @since 2.4.0
+ *
+ * @var Default_Avatars
+ */
+ private Default_Avatars $default_avatars;
+
+ /**
+ * The singleton instance.
+ *
+ * @var Core
+ */
+ private static $instance;
+
+ /**
+ * Creates a \Avatar_Privacy\Core instance and registers all necessary hooks
+ * and filters for the plugin.
+ *
+ * @since 2.1.0 Parameter $plugin_file removed.
+ * @since 2.4.0 Parameters $hasher, $user_fields, $comment_author_fields, and
+ * $default_avatars added, $transients, $version, $options,
+ * $site_transients, and $cache removed.
+ *
+ * @param Settings $settings Required.
+ * @param Hasher $hasher Required.
+ * @param User_Fields $user_fields Required.
+ * @param Comment_Author_Fields $comment_author_fields Required.
+ * @param Default_Avatars $default_avatars Required.
+ */
+ public function __construct(
+ Settings $settings,
+ Hasher $hasher,
+ User_Fields $user_fields,
+ Comment_Author_Fields $comment_author_fields,
+ Default_Avatars $default_avatars
+ ) {
+ $this->settings = $settings;
+ $this->hasher = $hasher;
+ $this->user_fields = $user_fields;
+ $this->comment_author_fields = $comment_author_fields;
+ $this->default_avatars = $default_avatars;
+ }
+
+ /**
+ * Retrieves (and if necessary creates) the API instance. Should not be called outside of plugin set-up.
+ *
+ * @internal
+ *
+ * @since 1.0.0
+ *
+ * @param Core $instance Only used for plugin initialization. Don't ever pass a value in user code.
+ *
+ * @return void
+ *
+ * @throws \BadMethodCallException Thrown when Avatar_Privacy_Core::set_instance after plugin initialization.
+ */
+ public static function set_instance( Core $instance ) {
+ if ( null === self::$instance ) {
+ self::$instance = $instance;
+ } else {
+ throw new \BadMethodCallException( __METHOD__ . ' called more than once.' );
+ }
+ }
+
+ /**
+ * Retrieves the plugin instance.
+ *
+ * @since 1.0.0
+ *
+ * @throws \BadMethodCallException Thrown when Avatar_Privacy_Core::get_instance is called before plugin initialization.
+ *
+ * @return Core
+ */
+ public static function get_instance() {
+ if ( null === self::$instance ) {
+ throw new \BadMethodCallException( __METHOD__ . ' called without prior plugin intialization.' );
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Retrieves the plugin version.
+ *
+ * @return string
+ */
+ public function get_version() {
+ return $this->settings->get_version();
+ }
+
+ /**
+ * Retrieves the full path to the main plugin file.
+ *
+ * @deprecated 2.3.0 Use AVATAR_PRIVACY_PLUGIN_FILE instead.
+ *
+ * @return string
+ */
+ public function get_plugin_file() {
+ \_deprecated_function( __METHOD__, '2.3.0' );
+
+ return \AVATAR_PRIVACY_PLUGIN_FILE;
+ }
+
+ /**
+ * Retrieves the plugin settings.
+ *
+ * @since 2.0.0 Parameter $force added.
+ *
+ * @param bool $force Optional. Forces retrieval of settings from database. Default false.
+ *
+ * @return array
+ *
+ * @phpstan-return SettingsFields
+ */
+ public function get_settings( $force = false ) {
+ return $this->settings->get_all_settings( $force );
+ }
+
+ /**
+ * Checks whether an anonymous comment author has opted-in to Gravatar usage.
+ *
+ * @param string $email_or_hash The comment author's e-mail address or the unique hash.
+ *
+ * @return bool
+ */
+ public function comment_author_allows_gravatar_use( $email_or_hash ) {
+ return $this->comment_author_fields->allows_gravatar_use( $email_or_hash );
+ }
+
+ /**
+ * Checks whether an anonymous comment author is in our Gravatar policy database.
+ *
+ * @param string $email_or_hash The comment author's e-mail address or the unique hash.
+ *
+ * @return bool
+ */
+ public function comment_author_has_gravatar_policy( $email_or_hash ) {
+ return $this->comment_author_fields->has_gravatar_policy( $email_or_hash );
+ }
+
+ /**
+ * Retrieves the database primary key for the given email address.
+ *
+ * @param string $email_or_hash The comment author's e-mail address or the unique hash.
+ *
+ * @return int The database key for the given email address (or 0).
+ */
+ public function get_comment_author_key( $email_or_hash ) {
+ return $this->comment_author_fields->get_key( $email_or_hash );
+ }
+
+ /**
+ * Retrieves the hash for the given user ID. If there currently is no hash,
+ * a new one is generated.
+ *
+ * @since 2.1.0 False is returned on error.
+ *
+ * @param int $user_id The user ID.
+ *
+ * @return string|false The hashed email, or `false` on failure.
+ */
+ public function get_user_hash( $user_id ) {
+ return $this->user_fields->get_hash( $user_id );
+ }
+
+ /**
+ * Retrieves the email for the given comment author database key.
+ *
+ * @param string $hash The hashed mail address.
+ *
+ * @return string
+ */
+ public function get_comment_author_email( $hash ) {
+ return $this->comment_author_fields->get_email( $hash );
+ }
+
+ /**
+ * Ensures that the comment author gravatar policy is updated.
+ *
+ * @param string $email The comment author's mail address.
+ * @param int $comment_id The comment ID.
+ * @param int $use_gravatar 1 if Gravatar.com is enabled, 0 otherwise.
+ *
+ * @return void
+ */
+ public function update_comment_author_gravatar_use( $email, $comment_id, $use_gravatar ) {
+ $this->comment_author_fields->update_gravatar_use( $email, $comment_id, $use_gravatar );
+ }
+
+ /**
+ * Updates the hash using the ID and email.
+ *
+ * @since 2.4.0 The parameter `$id` has been deprecated.
+ * @since 2.6.0 A warning is emitted if the deprecated argument `$id` is used.
+ *
+ * @param int|null $id Deprecated.
+ * @param string $email The email.
+ *
+ * @return void
+ */
+ public function update_comment_author_hash( $id, $email ) {
+ if ( ! empty( $id ) ) {
+ \_deprecated_argument( __FUNCTION__, '2.4.0', 'Please pass null to prevent this warning.' );
+ }
+
+ $this->comment_author_fields->update_hash( $email );
+ }
+
+ /**
+ * Retrieves the salt for current the site/network.
+ *
+ * @deprecated 2.4.0
+ *
+ * @return string
+ */
+ public function get_salt() {
+ \_deprecated_function( __METHOD__, '2.4.0' );
+
+ return $this->hasher->get_salt();
+ }
+
+ /**
+ * Generates a salted SHA-256 hash for the given e-mail address.
+ *
+ * @since 2.4.0 Implementation extracted to \Avatar_Privacy\Tools\Hasher
+ *
+ * @param string $email The mail address.
+ *
+ * @return string
+ */
+ public function get_hash( $email ) {
+ return $this->hasher->get_hash( $email );
+ }
+
+ /**
+ * Retrieves a user by email hash.
+ *
+ * @since 2.0.0
+ *
+ * @param string $hash The user's email hash.
+ *
+ * @return \WP_User|null
+ */
+ public function get_user_by_hash( $hash ) {
+ return $this->user_fields->get_user_by_hash( $hash );
+ }
+
+ /**
+ * Retrieves the full-size local avatar for a user (if one exists).
+ *
+ * @since 2.2.0
+ *
+ * @param int $user_id The user ID.
+ *
+ * @return array {
+ * An avatar definition, or the empty array.
+ *
+ * @type string $file The local filename.
+ * @type string $type The MIME type.
+ * }
+ *
+ * @phpstan-return AvatarDefinition|array{}
+ */
+ public function get_user_avatar( $user_id ) {
+ return $this->user_fields->get_local_avatar( $user_id );
+ }
+
+ /**
+ * Sets the local avatar for the given user.
+ *
+ * @since 2.4.0
+ *
+ * @param int $user_id The user ID.
+ * @param string $image Raw image data.
+ *
+ * @return void
+ *
+ * @throws \InvalidArgumentException An exception is thrown if the user ID does
+ * not exist or the upload result does not
+ * contain the 'file' key.
+ * @throws \RuntimeException A `RuntimeException` is thrown if the sideloading
+ * fails for some reason.
+ */
+ public function set_user_avatar( $user_id, $image ) {
+ $this->user_fields->set_local_avatar( $user_id, $image );
+ }
+
+ /**
+ * Checks whether a user has opted-in to Gravatar usage.
+ *
+ * @since 2.4.0
+ *
+ * @param int $user_id The user ID.
+ *
+ * @return bool
+ */
+ public function user_allows_gravatar_use( $user_id ) {
+ return $this->user_fields->allows_gravatar_use( $user_id );
+ }
+
+ /**
+ * Checks whether a user has set a Gravatar usage policy.
+ *
+ * @since 2.4.0
+ *
+ * @param int $user_id The user ID.
+ *
+ * @return bool
+ */
+ public function user_has_gravatar_policy( $user_id ) {
+ return $this->user_fields->has_gravatar_policy( $user_id );
+ }
+
+ /**
+ * Updates a user's gravatar policy.
+ *
+ * @since 2.4.0
+ *
+ * @param int $user_id The user ID.
+ * @param bool $use_gravatar Whether using Gravatar should be allowed or not.
+ *
+ * @return void
+ */
+ public function update_user_gravatar_use( $user_id, $use_gravatar ) {
+ $this->user_fields->update_gravatar_use( $user_id, $use_gravatar );
+ }
+
+ /**
+ * Checks whether a user has opted-in to anonymous commenting.
+ *
+ * @since 2.4.0
+ *
+ * @param int $user_id The user ID.
+ *
+ * @return bool
+ */
+ public function user_allows_anonymous_commenting( $user_id ) {
+ return $this->user_fields->allows_anonymous_commenting( $user_id );
+ }
+
+ /**
+ * Updates a user's gravatar policy.
+ *
+ * @since 2.4.0
+ *
+ * @param int $user_id The user ID.
+ * @param bool $anonymous Whether anonymous commenting should be allowed or not.
+ *
+ * @return void
+ */
+ public function update_user_anonymous_commenting( $user_id, $anonymous ) {
+ $this->user_fields->update_anonymous_commenting( $user_id, $anonymous );
+ }
+
+ /**
+ * Retrieves the full-size custom default avatar for the current site.
+ *
+ * Note: On multisite, the caller is responsible for switching to the site
+ * (using `switch_to_blog`) before calling this method, and for restoring
+ * the original site afterwards (using `restore_current_blog`).
+ *
+ * @since 2.4.0
+ *
+ * @return array {
+ * An avatar definition, or the empty array.
+ *
+ * @type string $file The local filename.
+ * @type string $type The MIME type.
+ * }
+ *
+ * @phpstan-return AvatarDefinition|array{}
+ */
+ public function get_custom_default_avatar() {
+ return $this->default_avatars->get_custom_default_avatar();
+ }
+
+ /**
+ * Sets the custom default avatar for the current site.
+ *
+ * Please note that the calling function is responsible for cleaning up the
+ * provided image if it is a temporary file (i.e the image is copied before
+ * being used as the new avatar).
+ *
+ * On multisite, the caller is responsible for switching to the site
+ * (using `switch_to_blog`) before calling this method, and for restoring
+ * the original site afterwards (using `restore_current_blog`).
+ *
+ * @since 2.4.0
+ *
+ * @param string $image_url The image URL or filename.
+ *
+ * @return void
+ *
+ * @throws \InvalidArgumentException An exception is thrown if the image URL
+ * is invalid.
+ * @throws Upload_Handling_Exception An exception is thrown if there was an
+ * while processing the image sideloading.
+ * @throws File_Deletion_Exception An exception is thrown if the previously
+ * set image could not be deleted.
+ */
+ public function set_custom_default_avatar( $image_url ) {
+ $this->default_avatars->set_custom_default_avatar( $image_url );
+ }
+}
diff --git a/includes/avatar-privacy/class-factory.php b/includes/avatar-privacy/class-factory.php
new file mode 100644
index 0000000..76dd606
--- /dev/null
+++ b/includes/avatar-privacy/class-factory.php
@@ -0,0 +1,578 @@
+
+ *
+ * @phpstan-import-type ConfigData from User_Form
+ */
+class Factory extends Dice {
+ const SHARED = [ 'shared' => true ];
+
+ // Named instances.
+ const USERFORM_PROFILE_INSTANCE = '$UserProfileForm';
+ const USERFORM_FRONTEND_INSTANCE = '$FrontendUserForm';
+ const USERFORM_THEME_MY_LOGIN_PROFILES_INSTANCE = '$ThemeMyLoginProfilesUserForm';
+ const USERFORM_BBPRESS_PROFILE_INSTANCE = '$bbPressProfileForm';
+ const JDENTICON_INSTANCE = '$JdenticonIdenticon';
+
+ /**
+ * The factory instance.
+ *
+ * @var Factory|null
+ */
+ private static $factory;
+
+ /**
+ * Creates a new instance.
+ */
+ final protected function __construct() {
+ }
+
+ /**
+ * Retrieves a factory set up for creating Avatar_Privacy instances.
+ *
+ * @since 2.1.0 Parameter $full_plugin_path replaced with AVATAR_PRIVACY_PLUGIN_FILE constant.
+ * @since 2.5.1 Now throws an Object_Factory_Exception in case of error.
+ *
+ * @return Factory
+ *
+ * @throws Object_Factory_Exception An exception is thrown if the factory cannot
+ * be created.
+ */
+ public static function get() {
+ if ( ! isset( self::$factory ) ) {
+
+ // Create factory.
+ $factory = new static();
+ $factory = $factory->addRules( $factory->get_rules() );
+
+ if ( $factory instanceof Factory ) {
+ self::$factory = $factory;
+ } else {
+ throw new Object_Factory_Exception( 'Could not create object factory.' ); // @codeCoverageIgnore
+ }
+ }
+
+ return self::$factory;
+ }
+
+ /**
+ * Retrieves the rules for setting up the plugin.
+ *
+ * @since 2.1.0
+ *
+ * @return array
+ *
+ * @phpstan-return array
+ */
+ protected function get_rules() {
+ return [
+ // Shared helpers.
+ Cache::class => self::SHARED,
+ Database\Table::class => self::SHARED,
+ Transients::class => self::SHARED,
+ Site_Transients::class => self::SHARED,
+ Options::class => self::SHARED,
+ Network_Options::class => self::SHARED,
+ Filesystem_Cache::class => self::SHARED,
+
+ // Core API.
+ API::class => self::SHARED,
+ Core::class => self::SHARED,
+ Settings::class => [
+ 'constructParams' => [ $this->get_plugin_version( \AVATAR_PRIVACY_PLUGIN_FILE ) ],
+ ],
+
+ // The plugin controller.
+ Controller::class => [
+ 'constructParams' => [ $this->get_components() ],
+ ],
+
+ // Components.
+ Component::class => self::SHARED,
+ Components\Block_Editor::class => [
+ 'substitutions' => [
+ User_Form::class => [ self::INSTANCE => self::USERFORM_FRONTEND_INSTANCE ],
+ ],
+ ],
+ Components\Command_Line_Interface::class => [
+ 'constructParams' => [ $this->get_cli_commands() ],
+ ],
+ Components\Image_Proxy::class => [
+ 'constructParams' => [ $this->get_avatar_handlers() ],
+ ],
+ Components\Integrations::class => [
+ 'constructParams' => [ $this->get_plugin_integrations() ],
+ ],
+ Components\Setup::class => [
+ 'constructParams' => [ $this->get_database_tables() ],
+ ],
+ Components\Shortcodes::class => [
+ 'substitutions' => [
+ User_Form::class => [ self::INSTANCE => self::USERFORM_FRONTEND_INSTANCE ],
+ ],
+ ],
+ Components\User_Profile::class => [
+ 'substitutions' => [
+ User_Form::class => [ self::INSTANCE => self::USERFORM_PROFILE_INSTANCE ],
+ ],
+ ],
+
+ // Default icon providers.
+ Static_Icons\Mystery_Icon_Provider::class => self::SHARED,
+ Static_Icons\Speech_Bubble_Icon_Provider::class => self::SHARED,
+ Static_Icons\Bowling_Pin_Icon_Provider::class => self::SHARED,
+ Static_Icons\Silhouette_Icon_Provider::class => self::SHARED,
+
+ // Avatar handlers.
+ Avatar_Handler::class => self::SHARED,
+ Default_Icons_Handler::class => [
+ 'constructParams' => [ $this->get_default_icons() ],
+ ],
+
+ // Default icon generators.
+ Default_Icons\Generator::class => self::SHARED,
+ Default_Icons\Generators\Jdenticon::class => [
+ 'substitutions' => [
+ \Avatar_Privacy\Vendor\Jdenticon\Identicon::class => [ self::INSTANCE => self::JDENTICON_INSTANCE ],
+ ],
+ ],
+ Default_Icons\Generators\Rings::class => [
+ 'constructParams' => [
+ 512, // The bounding box dimensions.
+ 3, // The number of rings.
+ ],
+ 'call' => [
+ [ 'setMono', [ true ] ], // The rings should be monochrome.
+ ],
+ ],
+
+ // Icon components.
+ self::JDENTICON_INSTANCE => [
+ 'instanceOf' => \Avatar_Privacy\Vendor\Jdenticon\Identicon::class,
+ 'constructParams' => [
+ // Some extra styling for the Jdenticon instance.
+ [ 'style' => [ 'padding' => 0 ] ],
+ ],
+ ],
+
+ // Upload handlers.
+ Upload_Handler::class => self::SHARED,
+
+ // Form helpers.
+ User_Form::class => self::SHARED,
+ self::USERFORM_PROFILE_INSTANCE => [
+ 'instanceOf' => User_Form::class,
+ 'constructParams' => $this->get_user_form_parameters( self::USERFORM_PROFILE_INSTANCE ),
+ ],
+ self::USERFORM_BBPRESS_PROFILE_INSTANCE => [
+ 'instanceOf' => User_Form::class,
+ 'constructParams' => $this->get_user_form_parameters( self::USERFORM_BBPRESS_PROFILE_INSTANCE ),
+ ],
+ self::USERFORM_FRONTEND_INSTANCE => [
+ 'instanceOf' => User_Form::class,
+ 'constructParams' => $this->get_user_form_parameters( self::USERFORM_FRONTEND_INSTANCE ),
+ ],
+ self::USERFORM_THEME_MY_LOGIN_PROFILES_INSTANCE => [
+ 'instanceOf' => User_Form::class,
+ 'constructParams' => $this->get_user_form_parameters( self::USERFORM_THEME_MY_LOGIN_PROFILES_INSTANCE ),
+ ],
+
+ // Plugin integrations.
+ Integrations\Plugin_Integration::class => self::SHARED,
+ Integrations\BBPress_Integration::class => [
+ 'substitutions' => [
+ User_Form::class => [ self::INSTANCE => self::USERFORM_BBPRESS_PROFILE_INSTANCE ],
+ ],
+ ],
+ Integrations\Theme_My_Login_Profiles_Integration::class => [
+ 'substitutions' => [
+ User_Form::class => [ self::INSTANCE => self::USERFORM_THEME_MY_LOGIN_PROFILES_INSTANCE ],
+ ],
+ ],
+
+ // Shared tools.
+ Tools\Hasher::class => self::SHARED,
+ Tools\Number_Generator::class => self::SHARED,
+ Tools\Multisite::class => self::SHARED,
+ Tools\Images\Color::class => self::SHARED,
+ Tools\Images\Editor::class => self::SHARED,
+ Tools\Images\PNG::class => self::SHARED,
+ Tools\Network\Gravatar_Service::class => self::SHARED,
+ ];
+ }
+
+ /**
+ * Retrieves the plugin version.
+ *
+ * @since 2.1.0
+ *
+ * @param string $plugin_file The full plugin path.
+ *
+ * @return string
+ */
+ protected function get_plugin_version( $plugin_file ) {
+ // Load version from plugin data.
+ if ( ! \function_exists( 'get_plugin_data' ) ) {
+ require_once \ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+
+ return \get_plugin_data( $plugin_file, false, false )['Version'];
+ }
+
+ /**
+ * Retrieves the list of plugin components run during normal operations
+ * (i.e. not including the Uninstallation component).
+ *
+ * @return array {
+ * An array of `Component` instances in `Dice` syntax.
+ *
+ * @type array {
+ * @type string $instance The classname.
+ * }
+ * }
+ *
+ * @phpstan-return array>>
+ */
+ protected function get_components() {
+ return [
+ [ self::INSTANCE => Components\Setup::class ],
+ [ self::INSTANCE => Components\Image_Proxy::class ],
+ [ self::INSTANCE => Components\Avatar_Handling::class ],
+ [ self::INSTANCE => Components\Comments::class ],
+ [ self::INSTANCE => Components\User_Profile::class ],
+ [ self::INSTANCE => Components\Settings_Page::class ],
+ [ self::INSTANCE => Components\Network_Settings_Page::class ],
+ [ self::INSTANCE => Components\Privacy_Tools::class ],
+ [ self::INSTANCE => Components\Integrations::class ],
+ [ self::INSTANCE => Components\Shortcodes::class ],
+ [ self::INSTANCE => Components\Block_Editor::class ],
+ [ self::INSTANCE => Components\Command_Line_Interface::class ],
+ ];
+ }
+
+ /**
+ * Retrieves a list of default icon providers suitable for inclusion in a `Dice` rule.
+ *
+ * @since 2.1.0
+ *
+ * @return array {
+ * An array of `Icon_Provider` instances in `Dice` syntax.
+ *
+ * @type array {
+ * @type string $instance The classname.
+ * }
+ * }
+ *
+ * @phpstan-return array>>
+ */
+ protected function get_default_icons() {
+ return [
+ // These are sorted as the should appear for selection in the discussion settings.
+ [ self::INSTANCE => Static_Icons\Mystery_Icon_Provider::class ],
+ [ self::INSTANCE => Generated_Icons\Identicon_Icon_Provider::class ],
+ [ self::INSTANCE => Generated_Icons\Wavatar_Icon_Provider::class ],
+ [ self::INSTANCE => Generated_Icons\Monster_ID_Icon_Provider::class ],
+ [ self::INSTANCE => Generated_Icons\Retro_Icon_Provider::class ],
+ [ self::INSTANCE => Generated_Icons\Rings_Icon_Provider::class ],
+ [ self::INSTANCE => Generated_Icons\Bird_Avatar_Icon_Provider::class ],
+ [ self::INSTANCE => Generated_Icons\Cat_Avatar_Icon_Provider::class ],
+ [ self::INSTANCE => Generated_Icons\Robohash_Icon_Provider::class ],
+ [ self::INSTANCE => Static_Icons\Speech_Bubble_Icon_Provider::class ],
+ [ self::INSTANCE => Static_Icons\Bowling_Pin_Icon_Provider::class ],
+ [ self::INSTANCE => Static_Icons\Silhouette_Icon_Provider::class ],
+ [ self::INSTANCE => Default_Icons\Custom_Icon_Provider::class ],
+ ];
+ }
+
+ /**
+ * Retrieves a list of plugin integrations.
+ *
+ * @since 2.1.0
+ *
+ * @return array {
+ * An array of `Plugin_Integration` instances in `Dice` syntax.
+ *
+ * @type array {
+ * @type string $instance The classname.
+ * }
+ * }
+ *
+ * @phpstan-return array>>
+ */
+ protected function get_plugin_integrations() {
+ return [
+ [ self::INSTANCE => Integrations\BBPress_Integration::class ],
+ [ self::INSTANCE => Integrations\BuddyPress_Integration::class ],
+ [ self::INSTANCE => Integrations\Simple_Author_Box_Integration::class ],
+ [ self::INSTANCE => Integrations\Simple_Local_Avatars_Integration::class ],
+ [ self::INSTANCE => Integrations\Simple_User_Avatar_Integration::class ],
+ [ self::INSTANCE => Integrations\Theme_My_Login_Profiles_Integration::class ],
+ [ self::INSTANCE => Integrations\Ultimate_Member_Integration::class ],
+ [ self::INSTANCE => Integrations\WPDiscuz_Integration::class ],
+ [ self::INSTANCE => Integrations\WP_User_Manager_Integration::class ],
+ ];
+ }
+
+ /**
+ * Retrieves a list of CLI commands.
+ *
+ * @since 2.3.0
+ *
+ * @return array {
+ * An array of `Command` instances in `Dice` syntax.
+ *
+ * @type array {
+ * @type string $instance The classname.
+ * }
+ * }
+ *
+ * @phpstan-return array>>
+ */
+ protected function get_cli_commands() {
+ return [
+ [ self::INSTANCE => CLI\Cron_Command::class ],
+ [ self::INSTANCE => CLI\Database_Command::class ],
+ [ self::INSTANCE => CLI\Default_Command::class ],
+ [ self::INSTANCE => CLI\Uninstall_Command::class ],
+ [ self::INSTANCE => CLI\User_Command::class ],
+ ];
+ }
+
+ /**
+ * Retrieves a list of database table handlers.
+ *
+ * @since 2.4.0
+ *
+ * @return array {
+ * An array of `Table` instances in `Dice` syntax.
+ *
+ * @type array {
+ * @type string $instance The classname.
+ * }
+ * }
+ *
+ * @phpstan-return array>>
+ */
+ protected function get_database_tables() {
+ $classes = [
+ Database\Comment_Author_Table::class,
+ Database\Hashes_Table::class,
+ ];
+ $tables = [];
+
+ foreach ( $classes as $table_class ) {
+ $tables[ $table_class::TABLE_BASENAME ] = [ self::INSTANCE => $table_class ];
+ }
+
+ return $tables;
+ }
+
+ /**
+ * Retrieves a list of avatar handlers.
+ *
+ * @since 2.4.0
+ *
+ * @return array {
+ * An array of `Avatar_Handler` instances (in `Dice` syntax), indexed by
+ * their filter hooks.
+ *
+ * @type array {
+ * @type array $hook The instance definition.
+ * }
+ * }
+ *
+ * @phpstan-return array>>
+ */
+ protected function get_avatar_handlers() {
+ return [
+ 'avatar_privacy_user_avatar_icon_url' => [ self::INSTANCE => User_Avatar_Handler::class ],
+ 'avatar_privacy_gravatar_icon_url' => [ self::INSTANCE => Gravatar_Cache_Handler::class ],
+ 'avatar_privacy_default_icon_url' => [ self::INSTANCE => Default_Icons_Handler::class ],
+ 'avatar_privacy_legacy_icon_url' => [ self::INSTANCE => Legacy_Icon_Handler::class ],
+ ];
+ }
+
+ /**
+ * Retrieves the constructor parameters for configuring named user form instances.
+ *
+ * @since 2.4.0
+ *
+ * @param string $instance The named instance.
+ *
+ * @return array The constructor parameter array for the named instance.
+ *
+ * @throws \InvalidArgumentException An exception is raised when $instance is
+ * not one of the expected constants.
+ *
+ * @phpstan-return array{ 0: ConfigData, 1: ConfigData, 2: ConfigData }
+ */
+ protected function get_user_form_parameters( $instance ) {
+ switch ( $instance ) {
+ case self::USERFORM_PROFILE_INSTANCE:
+ $use_gravatar = [
+ 'nonce' => 'avatar_privacy_use_gravatar_nonce_',
+ 'action' => 'avatar_privacy_edit_use_gravatar',
+ 'field' => 'avatar-privacy-use-gravatar',
+ 'partial' => 'admin/partials/profile/use-gravatar.php',
+ ];
+ $allow_anonymous = [
+ 'nonce' => 'avatar_privacy_allow_anonymous_nonce_',
+ 'action' => 'avatar_privacy_edit_allow_anonymous',
+ 'field' => 'avatar-privacy-allow-anonymous',
+ 'partial' => 'admin/partials/profile/allow-anonymous.php',
+ ];
+ $user_avatar = [
+ 'nonce' => 'avatar_privacy_upload_avatar_nonce_',
+ 'action' => 'avatar_privacy_upload_avatar',
+ 'field' => 'avatar-privacy-user-avatar-upload',
+ 'erase' => 'avatar-privacy-user-avatar-erase',
+ 'partial' => 'admin/partials/profile/user-avatar-upload.php',
+ ];
+ break;
+
+ case self::USERFORM_BBPRESS_PROFILE_INSTANCE:
+ $use_gravatar = [
+ 'nonce' => 'avatar_privacy_bbpress_use_gravatar_nonce_',
+ 'action' => 'avatar_privacy_bbpress_edit_use_gravatar',
+ 'field' => 'avatar-privacy-bbpress-use-gravatar',
+ 'partial' => 'public/partials/bbpress/profile/use-gravatar.php',
+ ];
+ $allow_anonymous = [
+ 'nonce' => 'avatar_privacy_bbpress_allow_anonymous_nonce_',
+ 'action' => 'avatar_privacy_bbpress_edit_allow_anonymous',
+ 'field' => 'avatar-privacy-bbpress-allow-anonymous',
+ 'partial' => 'public/partials/bbpress/profile/allow-anonymous.php',
+ ];
+ $user_avatar = [
+ 'nonce' => 'avatar_privacy_bbpress_upload_avatar_nonce_',
+ 'action' => 'avatar_privacy_bbpress_upload_avatar',
+ 'field' => 'avatar-privacy-bbpress-user-avatar-upload',
+ 'erase' => 'avatar-privacy-bbpress-user-avatar-erase',
+ 'partial' => 'public/partials/bbpress/profile/user-avatar-upload.php',
+ ];
+ break;
+
+ case self::USERFORM_FRONTEND_INSTANCE:
+ $use_gravatar = [
+ 'nonce' => 'avatar_privacy_frontend_use_gravatar_nonce_',
+ 'action' => 'avatar_privacy_frontend_edit_use_gravatar',
+ 'field' => 'avatar-privacy-frontend-use-gravatar',
+ 'partial' => 'public/partials/profile/use-gravatar.php',
+ ];
+ $allow_anonymous = [
+ 'nonce' => 'avatar_privacy_frontend_allow_anonymous_nonce_',
+ 'action' => 'avatar_privacy_frontend_edit_allow_anonymous',
+ 'field' => 'avatar_privacy-frontend-allow_anonymous',
+ 'partial' => 'public/partials/profile/allow-anonymous.php',
+ ];
+ $user_avatar = [
+ 'nonce' => 'avatar_privacy_frontend_upload_avatar_nonce_',
+ 'action' => 'avatar_privacy_frontend_upload_avatar',
+ 'field' => 'avatar-privacy-frontend-user-avatar-upload',
+ 'erase' => 'avatar-privacy-frontend-user-avatar-erase',
+ 'partial' => 'public/partials/profile/user-avatar-upload.php',
+ ];
+ break;
+
+ case self::USERFORM_THEME_MY_LOGIN_PROFILES_INSTANCE:
+ $use_gravatar = [
+ 'nonce' => 'avatar_privacy_tml_profiles_use_gravatar_nonce_',
+ 'action' => 'avatar_privacy_tml_profiles_edit_use_gravatar',
+ 'field' => 'avatar-privacy-tml-profiles-use-gravatar',
+ 'partial' => 'public/partials/tml-profiles/use-gravatar.php',
+ ];
+ $allow_anonymous = [
+ 'nonce' => 'avatar_privacy_tml_profiles_allow_anonymous_nonce_',
+ 'action' => 'avatar_privacy_tml_profiles_edit_allow_anonymous',
+ 'field' => 'avatar_privacy-tml-profiles-allow_anonymous',
+ 'partial' => 'public/partials/tml-profiles/allow-anonymous.php',
+ ];
+ $user_avatar = [
+ 'nonce' => 'avatar_privacy_tml_profiles_upload_avatar_nonce_',
+ 'action' => 'avatar_privacy_tml_profiles_upload_avatar',
+ 'field' => 'avatar-privacy-tml-profiles-user-avatar-upload',
+ 'erase' => 'avatar-privacy-tml-profiles-user-avatar-erase',
+ 'partial' => 'public/partials/tml-profiles/user-avatar-upload.php',
+ ];
+ break;
+
+ default:
+ throw new \InvalidArgumentException( "Invalid named instance {$instance}." );
+ }
+
+ return [ $use_gravatar, $allow_anonymous, $user_avatar ];
+ }
+}
diff --git a/includes/avatar-privacy/class-requirements.php b/includes/avatar-privacy/class-requirements.php
new file mode 100644
index 0000000..a36f5ac
--- /dev/null
+++ b/includes/avatar-privacy/class-requirements.php
@@ -0,0 +1,137 @@
+
+ */
+class Requirements extends \Avatar_Privacy\Vendor\Mundschenk\WP_Requirements {
+
+ const REQUIREMENTS = [
+ 'php' => '7.2.0',
+ 'multibyte' => false,
+ 'utf-8' => false,
+ 'gd' => true,
+ 'uploads_writable' => true,
+ ];
+
+ /**
+ * Creates a new requirements instance.
+ *
+ * @since 2.1.0 Parameter $plugin_file replaced with AVATAR_PRIVACY_PLUGIN_FILE constant.
+ */
+ public function __construct() {
+ parent::__construct( 'Avatar Privacy', \AVATAR_PRIVACY_PLUGIN_FILE, 'avatar-privacy', self::REQUIREMENTS );
+ }
+
+ /**
+ * Retrieves an array of requirement specifications.
+ *
+ * @return array {
+ * An array of requirements checks.
+ *
+ * @type string $enable_key An index in the $install_requirements array to switch the check on and off.
+ * @type callable $check A function returning true if the check was successful, false otherwise.
+ * @type callable $notice A function displaying an appropriate error notice.
+ * }
+ *
+ * @phpstan-return array
+ */
+ protected function get_requirements() {
+ $requirements = parent::get_requirements();
+ $requirements[] = [
+ 'enable_key' => 'gd',
+ 'check' => [ $this, 'check_gd_support' ],
+ 'notice' => [ $this, 'admin_notices_gd_incompatible' ],
+ ];
+ $requirements[] = [
+ 'enable_key' => 'uploads_writable',
+ 'check' => [ $this, 'check_uploads_writable' ],
+ 'notice' => [ $this, 'admin_notices_uploads_not_writable' ],
+ ];
+
+ return $requirements;
+ }
+
+ /**
+ * Checks for availability of the GD extension.
+ *
+ * @return bool
+ */
+ protected function check_gd_support() {
+ return \function_exists( 'imagecreatefrompng' )
+ && \function_exists( 'imagecopy' )
+ && \function_exists( 'imagedestroy' )
+ && \function_exists( 'imagepng' )
+ && \function_exists( 'imagecreatetruecolor' );
+ }
+
+ /**
+ * Prints 'GD extension missing' admin notice
+ *
+ * @return void
+ */
+ public function admin_notices_gd_incompatible() {
+ $this->display_error_notice(
+ /* translators: 1: plugin name 2: GD documentation URL */
+ \__( 'The activated plugin %1$s requires the GD PHP extension to be enabled on your server. Please deactivate this plugin, or enable the extension .', 'avatar-privacy' ),
+ 'Avatar Privacy ',
+ /* translators: URL with GD PHP extension installation instructions */
+ \__( 'http://php.net/manual/en/image.setup.php', 'avatar-privacy' )
+ );
+ }
+
+ /**
+ * Checks for availability of the GD extension.
+ *
+ * @return bool
+ */
+ protected function check_uploads_writable() {
+ $uploads = \wp_get_upload_dir();
+
+ return \wp_is_writable( $uploads['basedir'] );
+ }
+
+ /**
+ * Prints 'GD extension missing' admin notice
+ *
+ * @return void
+ */
+ public function admin_notices_uploads_not_writable() {
+ $this->display_error_notice(
+ /* translators: 1: plugin name */
+ \__( 'The activated plugin %1$s requires write access to the WordPress uploads folder on your server. Please check the folder\'s permissions, or deactivate this plugin.', 'avatar-privacy' ),
+ 'Avatar Privacy '
+ );
+ }
+}
diff --git a/includes/avatar-privacy/cli/class-abstract-command.php b/includes/avatar-privacy/cli/class-abstract-command.php
new file mode 100644
index 0000000..09865d5
--- /dev/null
+++ b/includes/avatar-privacy/cli/class-abstract-command.php
@@ -0,0 +1,106 @@
+
+ */
+abstract class Abstract_Command implements Command {
+
+ /**
+ * Clears all of the caches for memory management. Should be called after
+ * every 100 updates or so.
+ *
+ * @global \WP_Object_Cache $wp_object_cache The WordPress object cache.
+ * @global \wpdb $wpdb The WordPress database.
+ *
+ * @return void
+ */
+ protected function stop_the_insanity() {
+ global $wpdb, $wp_object_cache;
+
+ // Clean up saved queries.
+ $wpdb->queries = [];
+
+ // TODO: Check if any of these are at least somewhat universal.
+ if ( \is_object( $wp_object_cache ) ) {
+ if ( \property_exists( $wp_object_cache, 'group_ops' ) ) {
+ $wp_object_cache->group_ops = [];
+ }
+
+ if ( \property_exists( $wp_object_cache, 'stats' ) ) {
+ $wp_object_cache->stats = [];
+ }
+
+ if ( \property_exists( $wp_object_cache, 'memcache_debug' ) ) {
+ $wp_object_cache->memcache_debug = [];
+ }
+
+ if ( \property_exists( $wp_object_cache, 'cache' ) ) {
+ $wp_object_cache->cache = [];
+ }
+
+ // For some large memcached implementations.
+ if ( \method_exists( $wp_object_cache, '__remoteset' ) ) {
+ $wp_object_cache->__remoteset(); // @codeCoverageIgnore
+ }
+ }
+ }
+
+ /**
+ * Copies the iterator into an array.
+ *
+ * This method replaces to the builtin `\iterator_to_array()` to facilitate
+ * unit testing.
+ *
+ * @since 2.7.0 Documented as generic method.
+ *
+ * @template TKey of array-key
+ * @template TValue
+ *
+ * @param \Iterator $iterator Any iterator (but TKey must be a valid array key).
+ *
+ * @return array
+ *
+ * @phpstan-param \Iterator $iterator -- workaround for https://github.com/squizlabs/PHP_CodeSniffer/issues/3589
+ */
+ protected function iterator_to_array( \Iterator $iterator ) {
+ $result = [];
+
+ foreach ( $iterator as $key => $item ) {
+ $result[ $key ] = $item;
+ }
+
+ return $result;
+ }
+}
diff --git a/includes/avatar-privacy/cli/class-command.php b/includes/avatar-privacy/cli/class-command.php
new file mode 100644
index 0000000..71671c8
--- /dev/null
+++ b/includes/avatar-privacy/cli/class-command.php
@@ -0,0 +1,46 @@
+
+ */
+interface Command {
+
+ /**
+ * Registers the command (and any optional subcommands).
+ *
+ * The method assumes that `\WP_CLI` is available.
+ *
+ * @return void
+ */
+ public function register();
+}
diff --git a/includes/avatar-privacy/cli/class-cron-command.php b/includes/avatar-privacy/cli/class-cron-command.php
new file mode 100644
index 0000000..c800aed
--- /dev/null
+++ b/includes/avatar-privacy/cli/class-cron-command.php
@@ -0,0 +1,104 @@
+
+ */
+class Cron_Command extends Abstract_Command {
+
+ /**
+ * Registers the command (and any optional subcommands).
+ *
+ * The method assumes that `\WP_CLI` is available.
+ *
+ * @return void
+ */
+ public function register() {
+ WP_CLI::add_command( 'avatar-privacy cron list', [ $this, 'list_' ] );
+ WP_CLI::add_command( 'avatar-privacy cron delete', [ $this, 'delete' ] );
+ }
+
+ /**
+ * Displays information on the cron jobs installed by Avatar Privacy.
+ *
+ * ## EXAMPLES
+ *
+ * # Show when cron job will run next.
+ * $ wp avatar-privacy cron list
+ * Success: Cron job will run next at 2019-09-03 20:56:16 on this site.
+ *
+ * @subcommand list
+ *
+ * @return void
+ */
+ public function list_() {
+ $job = Image_Proxy::CRON_JOB_ACTION;
+ $next = \wp_next_scheduled( $job );
+
+ if ( false === $next ) {
+ WP_CLI::success( WP_CLI::colorize( "Cron job %B{$job}%n not scheduled on this site." ) );
+ } else {
+ $timestamp = \gmdate( 'Y-m-d H:i:s', $next );
+ WP_CLI::success( WP_CLI::colorize( "Cron job %B{$job}%n will run next at %B{$timestamp}%n on this site." ) );
+ }
+ }
+
+ /**
+ * Deletes the cron jobs installed by Avatar Privacy.
+ *
+ * They will be scheduled again on the next request.
+ *
+ * ## EXAMPLES
+ *
+ * # Delete all cron jobs hooked by Avatar Privacy.
+ * $ wp avatar-privacy cron delete
+ * Success: Cron job avatar_privacy_daily was unscheduled on this site (1 event).
+ *
+ * @return void
+ */
+ public function delete() {
+ $job = Image_Proxy::CRON_JOB_ACTION;
+ $events = \wp_unschedule_hook( $job );
+
+ if ( false === $events ) {
+ WP_CLI::error( WP_CLI::colorize( "Cron job %B{$job}%n could not be unscheduled on this site." ) );
+ } else {
+ $events = ( 1 === $events ) ? "{$events} event" : "{$events} events";
+ WP_CLI::success( WP_CLI::colorize( "Cron job %B{$job}%n was unscheduled on this site (%B{$events}%n)." ) );
+ }
+ }
+}
diff --git a/includes/avatar-privacy/cli/class-database-command.php b/includes/avatar-privacy/cli/class-database-command.php
new file mode 100644
index 0000000..3c04667
--- /dev/null
+++ b/includes/avatar-privacy/cli/class-database-command.php
@@ -0,0 +1,324 @@
+
+ */
+class Database_Command extends Abstract_Command {
+
+ /**
+ * The core API.
+ *
+ * @var Core
+ */
+ private $core;
+
+ /**
+ * The table handler.
+ *
+ * @var Comment_Author_Table
+ */
+ private $comment_author_table;
+
+
+ /**
+ * Creates a new command instance.
+ *
+ * @since 2.4.0 Parameter $db replaced with $comment_author_table.
+ *
+ * @param Core $core The core API.
+ * @param Comment_Author_Table $comment_author_table The table handler.
+ */
+ public function __construct( Core $core, Comment_Author_Table $comment_author_table ) {
+ $this->core = $core;
+ $this->comment_author_table = $comment_author_table;
+ }
+
+ /**
+ * Registers the command (and any optional subcommands).
+ *
+ * The method assumes that `\WP_CLI` is available.
+ *
+ * @return void
+ */
+ public function register() {
+ WP_CLI::add_command( 'avatar-privacy db create', [ $this, 'create' ] );
+ WP_CLI::add_command( 'avatar-privacy db show', [ $this, 'show' ] );
+ WP_CLI::add_command( 'avatar-privacy db list', [ $this, 'list_' ] );
+ WP_CLI::add_command( 'avatar-privacy db upgrade', [ $this, 'upgrade' ] );
+ }
+
+ /**
+ * Displays information about the database configuration of the Avatar Privacy installation.
+ *
+ * ## EXAMPLES
+ *
+ * # Output information on the custom table used by Avatar Privacy.
+ * $ wp avatar-privacy db show
+ * Avatar Privacy Database Information
+ * Version: 2.3.0
+ * Table name: wp_avatar_privacy
+ * The database currently contains 13 rows.
+ *
+ * @global \wpdb $wpdb The WordPress database.
+ *
+ * @return void
+ */
+ public function show() {
+ global $wpdb;
+
+ // Query data.
+ $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->avatar_privacy}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
+ $schema = $wpdb->get_results( "DESCRIBE {$wpdb->avatar_privacy}", \ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
+
+ // Display everything in a nice way.
+ WP_CLI::line( '' );
+ WP_CLI::line( WP_CLI::colorize( '%GAvatar Privacy Database Information%n' ) );
+ WP_CLI::line( '' );
+ WP_CLI::line( WP_CLI::colorize( "Version: %g{$this->core->get_version()}%n" ) );
+ WP_CLI::line( WP_CLI::colorize( "Table name: %g{$this->comment_author_table->get_table_name()}%n" ) );
+ WP_CLI::line( '' );
+ format_items( 'table', $schema, [ 'Field', 'Type', 'Null', 'Key', 'Default', 'Extra' ] );
+
+ if ( \is_multisite() ) {
+ if ( $this->comment_author_table->use_global_table() ) {
+ WP_CLI::line( 'The global table is used for all sites in this network.' );
+ } else {
+ WP_CLI::line( 'Each site in this network uses a separate table.' );
+ }
+ }
+
+ WP_CLI::line( '' );
+ WP_CLI::line( WP_CLI::colorize( "The table currently contains %g{$count} rows%n." ) );
+ WP_CLI::line( '' );
+ }
+
+ /**
+ * Lists the contents of Avatar Privacy's consent logging database for comment authors that were not logged in at the time.
+ *
+ * ## OPTIONS
+ *
+ * [--=]
+ * : Filter by one or more fields (see "Available Fields" section).
+ *
+ * [--field=]
+ * : Prints the value of a single field for each row.
+ *
+ * [--fields=]
+ * : Comma-separated list of fields to show.
+ *
+ * [--format=]
+ * : Render output in a particular format.
+ * ---
+ * default: table
+ * options:
+ * - table
+ * - csv
+ * - json
+ * - count
+ * - ids
+ * - yaml
+ * ---
+ *
+ * ## AVAILABLE FIELDS
+ *
+ * These fields will be displayed by default for each row:
+ *
+ * * id
+ * * email
+ * * use_gravatar
+ * * last_updated
+ *
+ * These fields are optionally available:
+ *
+ * * hash
+ * * log_message
+ *
+ * ## EXAMPLES
+ *
+ * # Output list of email address for which gravatars are enabled.
+ * $ wp avatar-privacy db list --field=email --use_gravatar=1
+ * firstname.lastname@example.org
+ * office@example.com
+ *
+ * @subcommand list
+ *
+ * @param string[] $args The positional arguments.
+ * @param string[] $assoc_args The associative arguments.
+ *
+ * @return void
+ */
+ public function list_( array $args, array $assoc_args ) {
+ $assoc_args = \wp_parse_args( $assoc_args, [
+ 'fields' => [ 'id', 'email', 'use_gravatar', 'last_updated' ],
+ 'format' => 'table',
+ ] );
+
+ // Create query data.
+ $where = [];
+ $db_cols = [ 'id', 'email', 'hash', 'use_gravatar', 'last_updated', 'log_message' ];
+ foreach ( $db_cols as $col ) {
+ if ( isset( $assoc_args[ $col ] ) ) {
+ $where[ $col ] = $assoc_args[ $col ];
+ }
+ }
+
+ /**
+ * Load table data.
+ *
+ * @phpstan-var \Iterator $iterator
+ */
+ $iterator = new Table_Iterator( [
+ 'table' => $this->comment_author_table->get_table_name(),
+ 'where' => $where,
+ ] );
+
+ // Optionally load only IDs.
+ $items = $iterator;
+ if ( 'ids' === $assoc_args['format'] ) {
+ $items = \wp_list_pluck( \iterator_to_array( $iterator ), 'id' );
+ }
+
+ // Display everything in a nice way.
+ $formatter = new Formatter( $assoc_args, null );
+ $formatter->display_items( $items ); // @phpstan-ignore-line -- https://github.com/php-stubs/wp-cli-stubs/issues/7
+ }
+
+ /**
+ * Creates the table for logging gravatar use consent for comment authors that are not logged-in WordPress users (e.g. anonymous comments).
+ *
+ * ## OPTIONS
+ *
+ * [ --global ]
+ * Creates the global table. Only valid in a multisite environment with global table use enabled.
+ *
+ * ## EXAMPLES
+ *
+ * # Creates the database table.
+ * $ wp avatar-privacy db create
+ * Success: Table wp_avatar_privacy created/updated successfully.
+ *
+ * @param string[] $args The positional arguments.
+ * @param string[] $assoc_args The associative arguments.
+ *
+ * @return void
+ */
+ public function create( array $args, array $assoc_args ) {
+ $global = get_flag_value( $assoc_args, 'global', false );
+ $multisite = \is_multisite();
+ if ( $global ) {
+ if ( ! $multisite ) {
+ WP_CLI::error( 'This is not a multisite installation.' );
+ } elseif ( ! $this->comment_author_table->use_global_table() ) {
+ WP_CLI::error( 'Cannot create global table because global table use is disabled.' );
+ }
+ } elseif ( $multisite && $this->comment_author_table->use_global_table() && ! \is_main_site() ) {
+ WP_CLI::error( 'Cannot create site-specific table because the global is used for all sites. Use `--global` switch to create the global table instead.' );
+ }
+
+ $table_name = $this->comment_author_table->get_table_name();
+
+ if ( $this->comment_author_table->table_exists( $table_name ) ) {
+ WP_CLI::error( WP_CLI::colorize( "Table %B{$table_name}%n already exists." ) );
+ }
+
+ if ( $this->comment_author_table->maybe_create_table( '' ) ) {
+ WP_CLI::success( WP_CLI::colorize( "Table %B{$table_name}%n created/updated successfully." ) );
+ } else {
+ WP_CLI::error( WP_CLI::colorize( "An error occured while creating the table %B{$table_name}%n." ) );
+ }
+ }
+
+ /**
+ * Upgrades the gravatar-use consent data.
+ *
+ * ## OPTIONS
+ *
+ * [ --global ]
+ * Upgrades the global table. Only valid in a multisite environment with global table use enabled.
+ *
+ * ## EXAMPLES
+ *
+ * # Creates the database table.
+ * $ wp avatar-privacy db upgrade
+ * Success: Table wp_avatar_privacy upgraded successfully.
+ *
+ * @param string[] $args The positional arguments.
+ * @param string[] $assoc_args The associative arguments.
+ *
+ * @return void
+ */
+ public function upgrade( array $args, array $assoc_args ) {
+ $global = get_flag_value( $assoc_args, 'global', false );
+ $multisite = \is_multisite();
+ if ( $global ) {
+ if ( ! $multisite ) {
+ WP_CLI::error( 'This is not a multisite installation.' );
+ } elseif ( ! $this->comment_author_table->use_global_table() ) {
+ WP_CLI::error( 'Cannot upgrade global table because global table use is disabled.' );
+ }
+ } elseif ( $multisite && $this->comment_author_table->use_global_table() && ! \is_main_site() ) {
+ WP_CLI::error( 'Cannot upgrade site-specific table because the global is used for all sites. Use `--global` switch to create the global table instead.' );
+ }
+
+ // Check for existence of table.
+ $table = $this->comment_author_table->get_table_name();
+ if ( ! $this->comment_author_table->table_exists( $table ) ) {
+ WP_CLI::error( WP_CLI::colorize( "Table %B{$table}%n does not exist. Use `wp avatar-privacy db create` to create it." ) );
+ }
+
+ // Upgrade table structure.
+ if ( ! $this->comment_author_table->maybe_create_table( '' ) ) {
+ WP_CLI::error( WP_CLI::colorize( "An error occured while creating or updating the table %B{$table}%n." ) );
+ }
+
+ // Upgrade data.
+ $rows = $this->comment_author_table->maybe_upgrade_data( '' );
+
+ if ( $rows > 0 ) {
+ WP_CLI::success( "Upgraded {$rows} rows in table {$table}." );
+ } else {
+ WP_CLI::success( "No rows to upgrade in table {$table}." );
+ }
+ }
+}
diff --git a/includes/avatar-privacy/cli/class-default-command.php b/includes/avatar-privacy/cli/class-default-command.php
new file mode 100644
index 0000000..5cc8e28
--- /dev/null
+++ b/includes/avatar-privacy/cli/class-default-command.php
@@ -0,0 +1,209 @@
+
+ */
+class Default_Command extends Abstract_Command {
+
+ /**
+ * The default avatars data helper.
+ *
+ * @var Default_Avatars
+ */
+ private Default_Avatars $default_avatars;
+
+ /**
+ * Creates a new command instance.
+ *
+ * @param Default_Avatars $default_avatars The default avatars API.
+ */
+ public function __construct( Default_Avatars $default_avatars ) {
+ $this->default_avatars = $default_avatars;
+ }
+
+ /**
+ * Registers the command (and any optional subcommands).
+ *
+ * The method assumes that `\WP_CLI` is available.
+ *
+ * @return void
+ */
+ public function register() {
+ WP_CLI::add_command( 'avatar-privacy default get-custom-default-avatar', [ $this, 'get_custom_default_avatar' ] );
+ WP_CLI::add_command( 'avatar-privacy default set-custom-default-avatar', [ $this, 'set_custom_default_avatar' ] );
+ WP_CLI::add_command( 'avatar-privacy default delete-custom-default-avatar', [ $this, 'delete_custom_default_avatar' ] );
+ }
+
+ /**
+ * Retrieves the custom default avatar for the site.
+ *
+ * ## EXAMPLES
+ *
+ * # Show the current custom default avatar.
+ * $ wp avatar-privacy default get-custom-default-avatar
+ * Success: Currently set custom default avatar: /path/image.jpg
+ *
+ * @return void
+ */
+ public function get_custom_default_avatar() {
+ $avatar = $this->default_avatars->get_custom_default_avatar();
+ if ( empty( $avatar['file'] ) ) {
+ WP_CLI::success( 'No custom default avatar set for this site.' );
+ } else {
+ WP_CLI::success( WP_CLI::colorize( "Currently set custom default avatar: %g{$avatar['file']}%n" ) );
+ }
+ }
+
+ /**
+ * Sets the custom default avatar for the site.
+ *
+ * ## OPTIONS
+ *
+ *
+ * : The URL of the avatar to set.
+ *
+ * [--live]
+ * : Actually change the default avatar (instead of only listing it).
+ *
+ * [--yes]
+ * : Do not ask for confirmation when updating data.
+ *
+ * ## EXAMPLES
+ *
+ * # Set a new custom default avatar.
+ * $ wp avatar-privacy user set-local-avatar http://example.org/image.jpg --live --yes
+ * Currently set custom default avatar: /path/old-image.jpg
+ * Success: Custom default avatar http://example.org/image.jpg has been set.
+ *
+ * @param string[] $args The positional arguments.
+ * @param string[] $assoc_args The associative arguments.
+ *
+ * @return void
+ */
+ public function set_custom_default_avatar( array $args, array $assoc_args ) {
+ $live = get_flag_value( $assoc_args, 'live', false );
+ $image_url = $args[0];
+
+ if ( \esc_url_raw( $image_url ) !== $image_url ) {
+ WP_CLI::error( "Invalid image URL {$image_url}" );
+ }
+
+ if ( ! $live ) {
+ WP_CLI::warning( 'Starting dry run.' );
+ }
+
+ $avatar = $this->default_avatars->get_custom_default_avatar();
+ $current_image = $avatar['file'] ?? 'none';
+
+ WP_CLI::line( WP_CLI::colorize( "Currently set custom default avatar: %g{$current_image}%n" ) );
+
+ // OK, let's do this.
+ if ( $live ) {
+ // Get confirmation.
+ WP_CLI::confirm( "Are you sure you want to set {$image_url} as the new custom default avatar for this site?", $assoc_args );
+
+ try {
+ // Actually set the new avatar image.
+ $this->default_avatars->set_custom_default_avatar( $image_url );
+ } catch ( \Exception $e ) {
+ WP_CLI::error( $e->getMessage() );
+ }
+
+ WP_CLI::success( WP_CLI::colorize( "Custom default avatar {$image_url} has been set." ) );
+ } else {
+ WP_CLI::success( 'Dry run finished.' );
+ }
+ }
+
+ /**
+ * Deletes the custom default avatar for the site.
+ *
+ * ## OPTIONS
+ *
+ * [--live]
+ * : Actually delete the custom default avatar (instead of only listing it).
+ *
+ * [--yes]
+ * : Do not ask for confirmation when removing data.
+ *
+ * ## EXAMPLES
+ *
+ * # Show when cron job will run next.
+ * $ wp avatar-privacy user delete-local-avatar --live --yes
+ * Currently set custom default avatar: /path/image.jpg
+ * Success: The custom default avatar for this site has been deleted.
+ *
+ * @param string[] $args The positional arguments.
+ * @param string[] $assoc_args The associative arguments.
+ *
+ * @return void
+ */
+ public function delete_custom_default_avatar( array $args, array $assoc_args ) {
+ $live = get_flag_value( $assoc_args, 'live', false );
+
+ $avatar = $this->default_avatars->get_custom_default_avatar();
+ if ( empty( $avatar['file'] ) ) {
+ WP_CLI::error( 'No custom default avatar set for this site.' );
+ }
+
+ if ( ! $live ) {
+ WP_CLI::warning( 'Starting dry run.' );
+ }
+
+ WP_CLI::line( WP_CLI::colorize( "Currently set custom default avatar: %g{$avatar['file']}%n" ) );
+
+ // OK, let's do this.
+ if ( $live ) {
+ // Get confirmation.
+ WP_CLI::confirm( 'Are you sure you want to delete the custom default avatar?', $assoc_args );
+
+ try {
+ // Actually set the new avatar image.
+ $this->default_avatars->delete_custom_default_avatar();
+ } catch ( \Exception $e ) {
+ WP_CLI::error( $e->getMessage() );
+ }
+
+ WP_CLI::success( WP_CLI::colorize( 'The custom default avatar for this site has been deleted.' ) );
+ } else {
+ WP_CLI::success( 'Dry run finished.' );
+ }
+ }
+}
diff --git a/includes/avatar-privacy/cli/class-uninstall-command.php b/includes/avatar-privacy/cli/class-uninstall-command.php
new file mode 100644
index 0000000..1faa2c8
--- /dev/null
+++ b/includes/avatar-privacy/cli/class-uninstall-command.php
@@ -0,0 +1,239 @@
+
+ */
+class Uninstall_Command extends Abstract_Command {
+
+ /**
+ * The setup component.
+ *
+ * @var Setup
+ */
+ private Setup $setup;
+
+ /**
+ * The uninstallation component.
+ *
+ * @var Uninstallation
+ */
+ private Uninstallation $uninstall;
+
+ /**
+ * The DB handler.
+ *
+ * @var Database
+ */
+ private Database $db;
+
+ /**
+ * Creates a new command instance.
+ *
+ * @param Setup $setup The setup component.
+ * @param Uninstallation $uninstall The uninstallation component.
+ * @param Database $db The database handler.
+ */
+ public function __construct( Setup $setup, Uninstallation $uninstall, Database $db ) {
+ $this->setup = $setup;
+ $this->uninstall = $uninstall;
+ $this->db = $db;
+ }
+
+ /**
+ * Registers the command (and any optional subcommands).
+ *
+ * The method assumes that `\WP_CLI` is available.
+ *
+ * @return void
+ */
+ public function register() {
+ WP_CLI::add_command( 'avatar-privacy uninstall', [ $this, 'uninstall' ] );
+ }
+
+ /**
+ * Removes all data from the current site. Optionally, also removes global data on multisite.
+ *
+ * Data that will be removed:
+ * * Cached avatar images
+ * * Uploaded user avatars
+ * * Uploaded custom default images
+ * * Avatar privacy user settings
+ * * Options created by Avatar Privacy
+ * * Transients created by Avatar Privacy
+ * * Network options (on multisite)
+ * * Network transients (on multisite)
+ * * The custom database table used for non-logged-in comment author consent logging.
+ *
+ * ## OPTIONS
+ *
+ * [--live]
+ * : Actually remove the data (instead of only listing it).
+ *
+ * [--yes]
+ * : Do not ask for confirmation when removing data.
+ *
+ * [--global]
+ * : Also uninstall global data (only applicable on multisite installations).
+ *
+ * ## EXAMPLES
+ *
+ * # Remove all data from a non-multisite installatin.
+ * $ wp avatar-privacy uninstall
+ *
+ * # Remove site-specific and global data from a multisite installation
+ * # (site-specific data needs to be deleted from each site seperately).
+ * $ wp avatar-privacy uninstall --global
+ *
+ * @global \wpdb $wpdb The WordPress database.
+ *
+ * @param string[] $args The positional arguments.
+ * @param string[] $assoc_args The associative arguments.
+ *
+ * @return void
+ */
+ public function uninstall( array $args, array $assoc_args ) {
+ $live = get_flag_value( $assoc_args, 'live', false );
+ $remove_global = get_flag_value( $assoc_args, 'global', false );
+ $multisite = \is_multisite();
+
+ // Abort early if we're not on a multsitie installation.
+ if ( $remove_global && ! $multisite ) {
+ WP_CLI::error( 'This is not a multisite installation.' );
+ }
+
+ // On non-multisite installations, global data is always removed.
+ $remove_global = $remove_global || ! $multisite;
+
+ if ( ! $live ) {
+ WP_CLI::warning( 'Starting dry run.' );
+ }
+
+ // Marker text for site-specific data.
+ $site_id = \get_current_blog_id();
+ $for_site = $multisite ? " for site {$site_id}" : '';
+
+ // List the data that will be deleted.
+ $this->print_data_to_delete( $for_site, $remove_global );
+
+ // OK, let's do this.
+ if ( $live ) {
+ // Get confirmation.
+ WP_CLI::confirm( 'Are you sure you want to delete this data?', $assoc_args );
+
+ // Actually delete data.
+ $this->delete_data( $site_id, $for_site, $remove_global );
+ } else {
+ WP_CLI::success( 'Dry run finished.' );
+ }
+ }
+
+ /**
+ * Deletes the data and prints a confirmation message.
+ *
+ * @param int $site_id The site ID.
+ * @param string $for_site Label part describing the site ("for site ").
+ * @param bool $remove_global A flag indicating that global data should be removed as well.
+ *
+ * @return void
+ */
+ protected function delete_data( $site_id, $for_site, $remove_global ) {
+
+ // Act as if deactivating plugin.
+ $this->setup->deactivate_plugin();
+
+ // Add tasks to uninstallation actions.
+ $this->uninstall->enqueue_cleanup_tasks();
+
+ if ( $remove_global ) {
+ // Remove global data.
+ /** This action is documented in class-uninstallation.php */
+ \do_action( 'avatar_privacy_uninstallation_global' );
+
+ if ( \is_multisite() ) {
+ // Only show extra message on multsite.
+ WP_CLI::success( 'Global data deleted.' );
+ }
+ }
+
+ // Remove site data.
+ /** This action is documented in class-uninstallation.php */
+ \do_action( 'avatar_privacy_uninstallation_site', $site_id );
+
+ WP_CLI::success( "Site data{$for_site} deleted." );
+ }
+
+ /**
+ * Prints the list of data to be deleted.
+ *
+ * @param string $for_site Label part describing the site ("for site ").
+ * @param bool $remove_global A flag indicating that global data should be removed as well.
+ *
+ * @return void
+ */
+ protected function print_data_to_delete( $for_site, $remove_global ) {
+ if ( $remove_global ) {
+ // List global data to delete.
+ WP_CLI::line( 'Deleting cached avatar images.' );
+ WP_CLI::line( 'Deleting uploaded user avatar and custom default images.' );
+ WP_CLI::line( 'Deleting avatar privacy user settings (user_meta).' );
+
+ if ( \is_multisite() ) {
+ // These do not make sense in single-site environment, even though
+ // they technically do exist.
+ WP_CLI::line( 'Deleting network options.' );
+ WP_CLI::line( 'Deleting network transients.' );
+ }
+ }
+
+ // List site data to delete.
+ WP_CLI::line( "Deleting options{$for_site}." );
+ WP_CLI::line( "Deleting transients{$for_site}." );
+
+ // Show dropped table.
+ $table_name = $this->db->get_table_name();
+ if ( ! $this->db->use_global_table() ) {
+ WP_CLI::line( "Dropping table {$table_name}{$for_site}." );
+ } else {
+ WP_CLI::line( "Dropping global table {$table_name}." );
+ }
+ }
+}
diff --git a/includes/avatar-privacy/cli/class-user-command.php b/includes/avatar-privacy/cli/class-user-command.php
new file mode 100644
index 0000000..9a7fdb5
--- /dev/null
+++ b/includes/avatar-privacy/cli/class-user-command.php
@@ -0,0 +1,206 @@
+
+ */
+class User_Command extends Abstract_Command {
+
+ /**
+ * The user data helper.
+ *
+ * @var User_Fields
+ */
+ private User_Fields $user_fields;
+
+ /**
+ * Creates a new command instance.
+ *
+ * @param User_Fields $user_fields The user data API.
+ */
+ public function __construct( User_Fields $user_fields ) {
+ $this->user_fields = $user_fields;
+ }
+
+ /**
+ * Registers the command (and any optional subcommands).
+ *
+ * The method assumes that `\WP_CLI` is available.
+ *
+ * @return void
+ */
+ public function register() {
+ WP_CLI::add_command( 'avatar-privacy user set-local-avatar', [ $this, 'set_local_avatar' ] );
+ WP_CLI::add_command( 'avatar-privacy user delete-local-avatar', [ $this, 'delete_local_avatar' ] );
+ }
+
+ /**
+ * Sets a new local avatar for the given user.
+ *
+ * ## OPTIONS
+ *
+ *
+ * : The ID of the user whose local avatar should be set.
+ *
+ *
+ * : The URL of the avatar to set.
+ *
+ * [--live]
+ * : Actually change the user avatar (instead of only listing it).
+ *
+ * [--yes]
+ * : Do not ask for confirmation when removing data.
+ *
+ * ## EXAMPLES
+ *
+ * # Show when cron job will run next.
+ * $ wp avatar-privacy user set-local-avatar 1 http://example.org/image.jpg --live --yes
+ * Success: Local avatar http://example.org/image.jpg set for user 'example_user' (ID: 1) has been set.
+ *
+ * @param string[] $args The positional arguments.
+ * @param string[] $assoc_args The associative arguments.
+ *
+ * @return void
+ */
+ public function set_local_avatar( array $args, array $assoc_args ) {
+ $live = get_flag_value( $assoc_args, 'live', false );
+ $user_id = (int) $args[0];
+ $image_url = $args[1];
+
+ $user = \get_user_by( 'id', $user_id );
+ $user_login = ! empty( $user ) ? $user->user_login : '';
+ if ( empty( $user_login ) ) {
+ WP_CLI::error( "Invalid user ID {$user_id}" );
+ }
+
+ if ( \esc_url_raw( $image_url ) !== $image_url ) {
+ WP_CLI::error( "Invalid image URL {$image_url}" );
+ }
+
+ if ( ! $live ) {
+ WP_CLI::warning( 'Starting dry run.' );
+ }
+
+ $avatar = $this->user_fields->get_local_avatar( $user_id );
+ $current_image = $avatar['file'] ?? 'none';
+
+ WP_CLI::line( WP_CLI::colorize( "Currently set local avatar for user '{$user_login}' (ID: {$user_id}): %g{$current_image}%n" ) );
+
+ // OK, let's do this.
+ if ( $live ) {
+ // Get confirmation.
+ WP_CLI::confirm( "Are you sure you want to set {$image_url} as the new local avatar for user '{$user_login}' (ID: {$user_id})?", $assoc_args );
+
+ try {
+ // Actually set the new avatar image.
+ $this->user_fields->set_local_avatar( $user_id, $image_url );
+ } catch ( \Exception $e ) {
+ WP_CLI::error( $e->getMessage() );
+ }
+
+ WP_CLI::success( WP_CLI::colorize( "Local avatar {$image_url} for user '{$user_login}' (ID: {$user_id}) has been set." ) );
+ } else {
+ WP_CLI::success( 'Dry run finished.' );
+ }
+ }
+
+ /**
+ * Deletes the local avatar for the given user.
+ *
+ * ## OPTIONS
+ *
+ *
+ * : The ID of the user whose local avatar should be deleted.
+ *
+ * [--live]
+ * : Actually change the user avatar (instead of only listing it).
+ *
+ * [--yes]
+ * : Do not ask for confirmation when removing data.
+ *
+ * ## EXAMPLES
+ *
+ * # Show when cron job will run next.
+ * $ wp avatar-privacy user delete-local-avatar 1 --live --yes
+ * Success: The local avatar for user 'example_user' (ID: 1) has been deleted.
+ *
+ * @param string[] $args The positional arguments.
+ * @param string[] $assoc_args The associative arguments.
+ *
+ * @return void
+ */
+ public function delete_local_avatar( array $args, array $assoc_args ) {
+ $live = get_flag_value( $assoc_args, 'live', false );
+ $user_id = (int) $args[0];
+
+ $user = \get_user_by( 'id', $user_id );
+ $user_login = ! empty( $user ) ? $user->user_login : '';
+ if ( empty( $user_login ) ) {
+ WP_CLI::error( "Invalid user ID {$user_id}" );
+ }
+
+ $avatar = $this->user_fields->get_local_avatar( $user_id );
+ if ( empty( $avatar['file'] ) ) {
+ WP_CLI::error( "No local avatar set for user '{$user_login}' (ID: {$user_id})." );
+ }
+
+ if ( ! $live ) {
+ WP_CLI::warning( 'Starting dry run.' );
+ }
+
+ WP_CLI::line( WP_CLI::colorize( "Currently set local avatar for user '{$user_login}' (ID: {$user_id}): %g{$avatar['file']}%n" ) );
+
+ // OK, let's do this.
+ if ( $live ) {
+ // Get confirmation.
+ WP_CLI::confirm( "Are you sure you want to delete the current local avatar of user '{$user_login}' (ID: {$user_id})?", $assoc_args );
+
+ try {
+ // Actually set the new avatar image.
+ $this->user_fields->delete_local_avatar( $user_id );
+ } catch ( \Exception $e ) {
+ WP_CLI::error( $e->getMessage() );
+ }
+
+ WP_CLI::success( WP_CLI::colorize( "The local avatar for user '{$user_login}' (ID: {$user_id}) has been deleted." ) );
+ } else {
+ WP_CLI::success( 'Dry run finished.' );
+ }
+ }
+}
diff --git a/includes/avatar-privacy/components/class-avatar-handling.php b/includes/avatar-privacy/components/class-avatar-handling.php
new file mode 100644
index 0000000..f7192ae
--- /dev/null
+++ b/includes/avatar-privacy/components/class-avatar-handling.php
@@ -0,0 +1,682 @@
+
+ *
+ * @phpstan-type AvatarData array{
+ * size: int, height: int, width: int,
+ * default: string, force_default: bool,
+ * rating: string,
+ * scheme: ?string,
+ * processed_args: ?mixed[],
+ * extra_attr: string,
+ * url?: string|false,
+ * found_avatar: bool
+ * }
+ * @phpstan-type IdentityTuple array{ 0: int|false, 1: string, 2: int }
+ */
+class Avatar_Handling implements Component {
+
+ /**
+ * The settings API.
+ *
+ * @var Settings
+ */
+ private Settings $settings;
+
+ /**
+ * The user data helper.
+ *
+ * @since 2.4.0
+ *
+ * @var User_Fields
+ */
+ private User_Fields $registered_user;
+
+ /**
+ * The comment author data helper.
+ *
+ * @since 2.4.0
+ *
+ * @var Comment_Author_Fields
+ */
+ private Comment_Author_Fields $comment_author;
+
+ /**
+ * The Gravatar network service.
+ *
+ * @var Gravatar_Service
+ */
+ private Gravatar_Service $gravatar;
+
+ /**
+ * The remote image network service.
+ *
+ * @var Remote_Image_Service
+ */
+ private Remote_Image_Service $remote_images;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.0.0 Parameter $gravatar added.
+ * @since 2.1.0 Parameter $plugin_file removed.
+ * @since 2.3.4 Parameter $remote_images added.
+ * @since 2.4.0 Parameters $settings, $user_fields and $comment_author_fields
+ * added, unused parameters $core and $options removed.
+ *
+ * @param Settings $settings The settings API.
+ * @param User_Fields $user_fields User data API.
+ * @param Comment_Author_Fields $comment_author_fields Comment author data API.
+ * @param Gravatar_Service $gravatar The Gravatar network service.
+ * @param Remote_Image_Service $remote_images The remote images network service.
+ */
+ public function __construct( Settings $settings, User_Fields $user_fields, Comment_Author_Fields $comment_author_fields, Gravatar_Service $gravatar, Remote_Image_Service $remote_images ) {
+ $this->settings = $settings;
+ $this->registered_user = $user_fields;
+ $this->comment_author = $comment_author_fields;
+ $this->gravatar = $gravatar;
+ $this->remote_images = $remote_images;
+ }
+
+ /**
+ * Sets up the various hooks for the plugin component.
+ *
+ * @return void
+ */
+ public function run() {
+ // Allow remote URLs by default for legacy avatar images. Use priority 9
+ // to allow filters with the default priority to override this consistently.
+ \add_filter( 'avatar_privacy_allow_remote_avatar_url', '__return_true', 9, 0 );
+
+ // Start handling avatars when all plugins have been loaded and initialized.
+ \add_action( 'init', [ $this, 'setup_avatar_filters' ] );
+
+ // Generate presets from saved settings.
+ \add_action( 'init', [ $this, 'enable_presets' ] );
+ }
+
+ /**
+ * Sets up avatar handling filters.
+ *
+ * @since 2.4.0 Renamed from init().
+ *
+ * @return void
+ */
+ public function setup_avatar_filters() {
+ /**
+ * Filters the priority used for filtering the `pre_get_avatar_data` hook.
+ *
+ * @since 2.3.4
+ *
+ * @param int $priority Default 9999.
+ */
+ $priority = \apply_filters( 'avatar_privacy_pre_get_avatar_data_filter_priority', 9999 );
+
+ // New default image display: filter the gravatar image upon display.
+ \add_filter( 'pre_get_avatar_data', [ $this, 'get_avatar_data' ], $priority, 2 );
+ }
+
+ /**
+ * Enables default filters from the user settings.
+ *
+ * @return void
+ */
+ public function enable_presets() {
+ if ( ! empty( $this->settings->get( Settings::GRAVATAR_USE_DEFAULT ) ) ) {
+ // Use priority 9 to allow filters with the default priority to override this consistently.
+ \add_filter( 'avatar_privacy_gravatar_use_default', '__return_true', 9, 0 );
+ }
+ }
+
+ /**
+ * Before displaying an avatar image, checks that displaying the gravatar
+ * for this e-mail address has been enabled (opted-in). Also, if the option
+ * "Don't publish encrypted e-mail addresses for non-members of Gravatar." is
+ * enabled, the function checks if a gravatar is actually available for the
+ * e-mail address. If not, it displays the default image directly.
+ *
+ * @param array $args Arguments passed to get_avatar_data(), after processing.
+ * @param int|string|object $id_or_email The Gravatar to retrieve. Accepts a user_id, user email, WP_User object, WP_Post object, or WP_Comment object.
+ *
+ * @return array
+ *
+ * @phpstan-param AvatarData $args
+ * @phpstan-return AvatarData
+ */
+ public function get_avatar_data( $args, $id_or_email ) {
+ // Process the user identifier.
+ try {
+ list( $user_id, $email, $age ) = $this->parse_id_or_email( $id_or_email );
+ } catch ( Avatar_Comment_Type_Exception $e ) {
+ // The $id_or_email is a comment of a type that should not display an avatar.
+ $args['url'] = false;
+ $args['found_avatar'] = false;
+ return $args;
+ }
+
+ // Generate the hash.
+ if ( ! empty( $user_id ) ) {
+ // Since we are having a non-empty $user_id, we'll always get a hash.
+ $hash = (string) $this->registered_user->get_hash( (int) $user_id );
+ } else {
+ // This might generate hashes for empty email addresses.
+ // That's OK in case some plugins want to display avatars for
+ // e.g. trackbacks and linkbacks.
+ $hash = $this->comment_author->get_hash( $email );
+ }
+
+ // We only need to check these if we are not forcing a default icon to be shown.
+ if ( empty( $args['force_default'] ) ) {
+ if ( ! empty( $user_id ) ) {
+ // Uploaded avatars take precedence.
+ $url = $this->get_local_avatar_url( $user_id, $hash, $args['size'], ! empty( $args['upload_timestamp'] ) );
+ }
+
+ if ( empty( $url ) ) {
+ // "Sniffed" Gravatar MIME type.
+ $mimetype = '';
+
+ // Maybe display a gravatar.
+ if ( $this->should_show_gravatar( $user_id, $email, $id_or_email, $age, $mimetype ) ) {
+ $url = $this->get_gravatar_url( $user_id, $email, $hash, $args['size'], $args['rating'], $mimetype );
+ } elseif ( ! empty( $args['url'] ) && $this->remote_images->validate_image_url( $args['url'], 'avatar' ) ) {
+ // Fall back to avatars set by other plugins.
+ $url = $this->get_legacy_icon_url( $args['url'], $args['size'] );
+ }
+ }
+ }
+
+ if ( empty( $url ) ) {
+ // Nothing so far, use the default icon.
+ $url = $this->get_default_icon_url( $hash, $args['default'], $args['size'] );
+ }
+
+ // Return found image.
+ $args['url'] = $url;
+ $args['found_avatar'] = true;
+
+ return $args;
+ }
+
+ /**
+ * Determines if we should go for a gravatar.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ *
+ * @param int|false $user_id A WordPress user ID (or false).
+ * @param string $email The email address.
+ * @param int|string|object $id_or_email The Gravatar to retrieve. Accepts a user_id, user email, WP_User object, WP_Post object, or WP_Comment object.
+ * @param int $age The seconds since the post or comment was first created, or 0 if $id_or_email was not one of these object types.
+ * @param string $mimetype The expected MIME type of the gravatar image (if any). Passed by reference.
+ *
+ * @return bool
+ */
+ protected function should_show_gravatar( $user_id, $email, $id_or_email, $age, &$mimetype ) {
+ // Find out if the user opted into displaying a gravatar.
+ $show_gravatar = $this->determine_gravatar_policy( $user_id, $email, $id_or_email );
+
+ // Check if a gravatar exists for the e-mail address.
+ if ( $show_gravatar ) {
+ /**
+ * Filters whether we check if opting-in users and commenters actually have a Gravatar.com account.
+ *
+ * @param bool $enable_check Defaults to true.
+ * @param string $email The email address.
+ * @param int|false $user_id A WordPress user ID (or false).
+ */
+ if ( \apply_filters( 'avatar_privacy_enable_gravatar_check', true, $email, $user_id ) ) {
+ $mimetype = $this->gravatar->validate( $email, $age );
+ $show_gravatar = ! empty( $mimetype );
+ }
+ }
+
+ return $show_gravatar;
+ }
+
+ /**
+ * Parses e-mail address and/or user ID from $id_or_email.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ * @since 2.3.4 Throws an Avatar_Comment_Type_Exception for invalid comment types.
+ *
+ * @param int|string|object $id_or_email The Gravatar to retrieve. Accepts a user_id, user email, WP_User object, WP_Post object, or WP_Comment object.
+ *
+ * @return array {
+ * The tuple `[ $user_id, $email, $age ]`.
+ *
+ * @type int|false $user_id The WordPress user ID, or `false`.
+ * @type string $email The email address (or the empty string).
+ * @type int $age The seconds since the post or comment was first created,
+ * or 0 if `$id_or_email was` not one of these object types.
+ * }
+ *
+ * @throws Avatar_Comment_Type_Exception The function throws an
+ * `Avatar_Comment_Type_Exception` if `$id_or_email` is an instance of
+ * `WP_Comment` but its comment type is not one of the allowed avatar
+ * comment types.
+ *
+ * @phpstan-return IdentityTuple
+ */
+ protected function parse_id_or_email( $id_or_email ) {
+ list( $user_id, $email, $age ) = $this->parse_id_or_email_unfiltered( $id_or_email );
+
+ if ( ! empty( $user_id ) && empty( $email ) ) {
+ $user = \get_user_by( 'ID', $user_id );
+
+ // Prevent warnings when a user ID is invalid (e.g. because a user was deleted directly from the database).
+ if ( ! empty( $user ) ) {
+ $email = $user->user_email;
+ } else {
+ $user_id = false; // The user ID was invalid.
+ }
+ } elseif ( empty( $user_id ) && ! empty( $email ) ) {
+ // Check if anonymous comments "as user" are allowed.
+ $user = $this->registered_user->get_user_by_email( $email );
+ if ( ! empty( $user ) && $this->registered_user->allows_anonymous_commenting( $user->ID ) ) {
+ $user_id = $user->ID;
+ }
+ }
+
+ /**
+ * Filters the parsed user ID, email address and "object age".
+ *
+ * @param array $parsed_data {
+ * The information parsed from $id_or_email.
+ *
+ * @type int|false $user_id The WordPress user ID, or `false`.
+ * @type string $email The email address (or the empty string).
+ * @type int $age The seconds since the post or comment was first created,
+ * or 0 if `$id_or_email was` not one of these object types.
+ * }
+ * @param int|string|object $id_or_email The Gravatar to retrieve. Accepts a user_id, user email, WP_User object, WP_Post object, or WP_Comment object.
+ *
+ * @phpstan-var IdentityTuple
+ */
+ return \apply_filters( 'avatar_privacy_parse_id_or_email', [ $user_id, $email, $age ], $id_or_email );
+ }
+
+ /**
+ * Parses e-mail address and/or user ID from $id_or_email without filtering
+ * the result in any way.
+ *
+ * @since 2.3.0
+ * @since 2.3.4 Throws an Avatar_Comment_Type_Exception for invalid comment types.
+ *
+ * @internal
+ *
+ * @param int|string|object $id_or_email The identity to retrieven an avatar for.
+ * Accepts a user_id, user email, WP_User object,
+ * WP_Post object, or WP_Comment object.
+ *
+ * @return array {
+ * The tuple `[ $user_id, $email, $age ]`.
+ *
+ * @type int|false $user_id The WordPress user ID, or `false`.
+ * @type string $email The email address (or the empty string).
+ * @type int $age The seconds since the post or comment was first created,
+ * or 0 if $id_or_email was not one of these object types.
+ * }
+ *
+ * @throws Avatar_Comment_Type_Exception The function throws an
+ * `Avatar_Comment_Type_Exception` if `$id_or_email` is an instance of
+ * `WP_Comment` but its comment type is not one of the allowed avatar
+ * comment types.
+ *
+ * @phpstan-return IdentityTuple
+ */
+ protected function parse_id_or_email_unfiltered( $id_or_email ) {
+ $user_id = false;
+ $email = '';
+ $age = 0;
+
+ if ( \is_numeric( $id_or_email ) ) {
+ $user_id = \absint( $id_or_email );
+ } elseif ( \is_string( $id_or_email ) ) {
+ // E-mail address.
+ $email = $id_or_email;
+ } elseif ( $id_or_email instanceof \WP_User ) {
+ // User object.
+ $user_id = $id_or_email->ID;
+ $email = $id_or_email->user_email;
+ } elseif ( $id_or_email instanceof \WP_Post ) {
+ // Post object.
+ $user_id = (int) $id_or_email->post_author;
+ $age = $this->get_age( $id_or_email->post_date_gmt );
+ } elseif ( $id_or_email instanceof \WP_Comment ) {
+ return $this->parse_comment( $id_or_email );
+ }
+
+ return [ $user_id, $email, $age ];
+ }
+
+ /**
+ * Parse a WP_Comment object.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ * @since 2.3.4 Throws an Avatar_Comment_Type_Exception for invalid comment types.
+ *
+ * @param \WP_Comment $comment A comment.
+ *
+ * @return array {
+ * The information parsed from $id_or_email.
+ *
+ * @type int|false $user_id The WordPress user ID, or false.
+ * @type string $email The email address.
+ * @type int $age The seconds since the post or comment was first created, or 0 if $id_or_email was not one of these object types.
+ * }
+ *
+ * @throws Avatar_Comment_Type_Exception The function throws an
+ * `Avatar_Comment_Type_Exception` if the comment type of `$comment` is
+ * not one of the allowed avatar comment types.
+ *
+ * @phpstan-return IdentityTuple
+ */
+ protected function parse_comment( \WP_Comment $comment ) {
+ /** This filter is documented in wp-includes/pluggable.php */
+ $allowed_comment_types = \apply_filters( 'get_avatar_comment_types', [ 'comment' ] ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- can be replaced with is_avatar_comment_type() once WordPress 5.1 is released.
+
+ if ( ! \in_array( \get_comment_type( $comment ), (array) $allowed_comment_types, true ) ) {
+ // Abort.
+ throw new Avatar_Comment_Type_Exception();
+ }
+
+ $user_id = false;
+ $email = '';
+ $age = $this->get_age( $comment->comment_date_gmt );
+ if ( ! empty( $comment->user_id ) ) {
+ $user_id = (int) $comment->user_id;
+ } elseif ( ! empty( $comment->comment_author_email ) ) {
+ $email = $comment->comment_author_email;
+ }
+
+ return [ $user_id, $email, $age ];
+ }
+
+ /**
+ * Calculates the age (seconds before now) from a GMT-based date/time string.
+ *
+ * @since 2.1.0
+ *
+ * @param string $date_gmt A date/time string in the GMT time zone.
+ *
+ * @return int The age in seconds.
+ */
+ protected function get_age( $date_gmt ) {
+ return \time() - (int) \mysql2date( 'U', $date_gmt );
+ }
+
+ /**
+ * Retrieves a URL pointing to the local avatar image of the appropriate size.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ * @since 2.4.0 Parameter $timestamp added.
+ *
+ * @param int $user_id The user ID.
+ * @param string $hash The hashed mail address.
+ * @param int $size The requested avatar size in pixels.
+ * @param bool $timestamp Optional. Whether a timestampe (`ts`) parameter
+ * should be added to the URL (for cache busting).
+ *
+ * @return string The URL, or '' if no local avatar has been set.
+ */
+ protected function get_local_avatar_url( $user_id, $hash, $size, $timestamp = false ) {
+ // Bail if we haven't got a valid user ID.
+ if ( empty( $user_id ) ) {
+ return '';
+ }
+
+ // Fetch local avatar from meta and make sure it's properly stzed.
+ $url = '';
+ $local_avatar = $this->registered_user->get_local_avatar( $user_id );
+ if ( ! empty( $local_avatar['file'] ) && ! empty( $local_avatar['type'] ) ) {
+ // Prepare filter arguments.
+ $args = [
+ 'user_id' => $user_id,
+ 'avatar' => $local_avatar['file'],
+ 'mimetype' => $local_avatar['type'],
+ 'timestamp' => $timestamp,
+ ];
+
+ /**
+ * Filters the uploaded avatar URL for the given user.
+ *
+ * @since 2.4.0 Optional 'timestamp' argument added.
+ *
+ * @param string $url The URL. Default empty.
+ * @param string $hash The hashed mail address.
+ * @param int $size The size of the avatar image in pixels.
+ * @param array $args {
+ * An array of arguments.
+ *
+ * @type int $user_id A WordPress user ID.
+ * @type string $avatar The full-size avatar image path.
+ * @type string $mimetype The expected MIME type of the avatar image.
+ * @type bool $timestamp Whether to add a timestamp for cache busting.
+ * }
+ */
+ $url = \apply_filters( 'avatar_privacy_user_avatar_icon_url', '', $hash, $size, $args );
+ }
+
+ return $url;
+ }
+
+ /**
+ * Retrieves the default icon URL for the given hash.
+ *
+ * @since 2.4.0
+ * @since 2.7.0 Parameter `$default` renamed to `$type` to prevent conflict with reserved keyword.
+ *
+ * @param string $hash The hashed mail address.
+ * @param string $type The default icon type.
+ * @param int $size The size of the avatar image in pixels.
+ *
+ * @return string
+ */
+ protected function get_default_icon_url( $hash, $type, $size ) {
+ // Prepare filter arguments.
+ $args = [
+ 'default' => $type,
+ 'type' => $type,
+ ];
+
+ /**
+ * Filters the default icon URL for the given e-mail.
+ *
+ * @since 2.7.0 The argument key 'default' has been deprecated. Use 'type' instead.
+ *
+ * @param string $url The fallback icon URL (a blank GIF).
+ * @param string $hash The hashed mail address.
+ * @param int $size The size of the avatar image in pixels.
+ * @param array $args {
+ * An array of arguments.
+ *
+ * @type string $default Deprecated.
+ * @type string $type The default icon type.
+ * }
+ */
+ return \apply_filters( 'avatar_privacy_default_icon_url', \includes_url( 'images/blank.gif' ), $hash, $size, $args );
+ }
+
+ /**
+ * Determines the gravatar use policy.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ *
+ * @param int|false $user_id A WordPress user ID (or false).
+ * @param string $email The email address.
+ * @param int|string|object $id_or_email The Gravatar to retrieve. Can be a user_id, user email, WP_User object, WP_Post object, or WP_Comment object.
+ *
+ * @return bool
+ */
+ protected function determine_gravatar_policy( $user_id, $email, $id_or_email ) {
+ $show_gravatar = false;
+ $use_default = false;
+
+ if ( ! empty( $user_id ) ) {
+ // For users get the value from the usermeta table.
+ $show_gravatar = $this->registered_user->allows_gravatar_use( $user_id );
+ $use_default = ! $this->registered_user->has_gravatar_policy( $user_id );
+ } else {
+ // For comments get the value from the plugin's table.
+ $show_gravatar = $this->comment_author->allows_gravatar_use( $email );
+
+ // Don't use the default policy for spam comments.
+ if ( ! $show_gravatar && ( ! $id_or_email instanceof \WP_Comment || ( 'spam' !== $id_or_email->comment_approved && 'trash' !== $id_or_email->comment_approved ) ) ) {
+ $use_default = ! $this->comment_author->has_gravatar_policy( $email );
+ }
+ }
+
+ if ( $use_default ) {
+ /**
+ * Filters the default policy for showing gravatars.
+ *
+ * The result only applies if a user or comment author has not
+ * explicitely set a value for `use_gravatar` (i.e. for comments
+ * created before the plugin was installed).
+ *
+ * @param bool $show Default false.
+ * @param int|string|object $id_or_email The Gravatar to retrieve. Can be a user_id, user email, WP_User object, WP_Post object, or WP_Comment object.
+ */
+ $show_gravatar = \apply_filters( 'avatar_privacy_gravatar_use_default', false, $id_or_email );
+
+ if ( $show_gravatar && empty( $user_id ) ) {
+ // Create only the hash so that the gravatar can be regenerated.
+ $this->comment_author->update_hash( $email );
+ }
+ }
+
+ return $show_gravatar;
+ }
+
+ /**
+ * Retrieves the Gravatar.com URL for the given e-mail.
+ *
+ * @since 2.4.0
+ *
+ * @param int|false $user_id A WordPress user ID (or false).
+ * @param string $email The mail address used to generate the identity hash.
+ * @param string $hash The hashed e-mail address.
+ * @param int $size The size of the avatar image in pixels.
+ * @param string $rating The audience rating (e.g. 'g', 'pg', 'r', 'x').
+ * @param string $mimetype The expected MIME type of the Gravatar image.
+ *
+ * @return string
+ */
+ protected function get_gravatar_url( $user_id, $email, $hash, $size, $rating, $mimetype = null ) {
+ // Prepare filter arguments.
+ $args = [
+ 'user_id' => $user_id,
+ 'email' => $email,
+ 'rating' => $rating,
+ 'mimetype' => empty( $mimetype ) ? Image_File::PNG_IMAGE : $mimetype,
+ ];
+
+ /**
+ * Filters the Gravatar.com URL for the given e-mail.
+ *
+ * @param string $url The fallback default icon URL (or '').
+ * @param string $hash The hashed e-mail address.
+ * @param int $size The size of the avatar image in pixels.
+ * @param array $args {
+ * An array of arguments.
+ *
+ * @type int|false $user_id A WordPress user ID (or false).
+ * @type string $email The mail address used to generate the identity hash.
+ * @type string $rating The audience rating (e.g. 'g', 'pg', 'r', 'x').
+ * @type string $mimetype The expected MIME type of the Gravatar image.
+ * }
+ */
+ return \apply_filters( 'avatar_privacy_gravatar_icon_url', '', $hash, $size, $args );
+ }
+
+ /**
+ * Checks if an image URL is valid to use as a fallback avatar icon.
+ *
+ * @since 2.4.0
+ *
+ * @deprecated 2.7.0
+ *
+ * @param string $url The image URL.
+ *
+ * @return bool
+ */
+ protected function is_valid_image_url( $url ) {
+ \_deprecated_function( __METHOD__, '2.7.0', 'Avatar_Privacy\Tools\Network\Remote_Image_Service::validate_image_url' );
+
+ return ( ! \strpos( $url, 'gravatar.com' ) && $this->remote_images->validate_image_url( $url, 'avatar' ) );
+ }
+
+ /**
+ * Retrieves a URL pointing to the legacy icon scaled to the appropriate size.
+ *
+ * @since 2.4.0
+ *
+ * @param string $url A valid image URL.
+ * @param int $size The size of the avatar image in pixels.
+ *
+ * @return string
+ */
+ protected function get_legacy_icon_url( $url, $size ) {
+ // Prepare filter arguments.
+ $hash = $this->remote_images->get_hash( $url );
+ $args = [];
+
+ /**
+ * Filters the legacy icon URL.
+ *
+ * @since 2.4.0
+ *
+ * @param string $url The legacy image URL.
+ * @param string $hash The hashed URL.
+ * @param int $size The size of the avatar image in pixels.
+ * @param array $args {
+ * An array of arguments. Currently unused.
+ * }
+ */
+ return \apply_filters( 'avatar_privacy_legacy_icon_url', $url, $hash, $size, $args );
+ }
+}
diff --git a/includes/avatar-privacy/components/class-block-editor.php b/includes/avatar-privacy/components/class-block-editor.php
new file mode 100644
index 0000000..31518af
--- /dev/null
+++ b/includes/avatar-privacy/components/class-block-editor.php
@@ -0,0 +1,249 @@
+
+ *
+ * @phpstan-type FormBlockAttributes array{ avatar_size: int, className: string }
+ * @phpstan-type AvatarBlockAttributes array{ user_id: int, avatar_size: int, className: string }
+ */
+class Block_Editor implements Component {
+
+ /**
+ * The script & style registration helper.
+ *
+ * @since 2.4.0
+ *
+ * @var Dependencies
+ */
+ private Dependencies $dependencies;
+
+ /**
+ * The template helper.
+ *
+ * @since 2.4.0
+ *
+ * @var Template
+ */
+ private Template $template;
+
+ /**
+ * The profile form helper.
+ *
+ * @var User_Form
+ */
+ private User_Form $form;
+
+ /**
+ * Initialize the class and set its properties.
+ *
+ * @param Dependencies $dependencies The script & style registration helper.
+ * @param Template $template The template helper.
+ * @param User_Form $form The profile form helper.
+ */
+ public function __construct( Dependencies $dependencies, Template $template, User_Form $form ) {
+ $this->dependencies = $dependencies;
+ $this->template = $template;
+ $this->form = $form;
+ }
+
+ /**
+ * Sets up the various hooks for the plugin component.
+ *
+ * @return void
+ */
+ public function run() {
+ if ( ! \function_exists( 'register_block_type' ) ) {
+ // Block editor not installed.
+ return;
+ }
+
+ // Initialize shortcodes after WordPress has loaded.
+ \add_action( 'init', [ $this, 'register_blocks' ] );
+
+ // Only process forms on the frontend.
+ if ( ! \is_admin() ) {
+ $this->form->register_form_submission();
+ }
+ }
+
+ /**
+ * Registers the Gutenberg blocks.
+ *
+ * @return void
+ */
+ public function register_blocks() {
+ // Register the script containing all our block types.
+ $this->dependencies->register_block_script( 'avatar-privacy-gutenberg', 'admin/blocks/js/blocks' );
+
+ // Register the stylesheet for the blocks.
+ $this->dependencies->register_style( 'avatar-privacy-gutenberg-style', 'admin/css/blocks.css' );
+
+ // Register each individual block type:
+ // The frontend form block.
+ \register_block_type(
+ 'avatar-privacy/form',
+ [
+ 'editor_script' => 'avatar-privacy-gutenberg',
+ 'editor_style' => 'avatar-privacy-gutenberg-style',
+ 'render_callback' => [ $this, 'render_frontend_form' ],
+ 'attributes' => [
+ 'avatar_size' => [
+ 'type' => 'integer',
+ 'default' => 96,
+ ],
+ 'show_descriptions' => [
+ 'type' => 'boolean',
+ 'default' => true,
+ ],
+ 'className' => [
+ 'type' => 'string',
+ 'default' => '',
+ ],
+ 'preview' => [
+ 'type' => 'boolean',
+ 'default' => false,
+ ],
+ ],
+ ]
+ );
+
+ // The avatar block.
+ \register_block_type(
+ 'avatar-privacy/avatar',
+ [
+ 'editor_script' => 'avatar-privacy-gutenberg',
+ 'editor_style' => 'avatar-privacy-gutenberg-style',
+ 'render_callback' => [ $this, 'render_avatar' ],
+ 'attributes' => [
+ 'avatar_size' => [
+ 'type' => 'integer',
+ 'default' => 96,
+ ],
+ 'user_id' => [
+ 'type' => 'integer',
+ 'default' => 0,
+ ],
+ 'align' => [
+ 'type' => 'string',
+ 'default' => '',
+ ],
+ 'className' => [
+ 'type' => 'string',
+ 'default' => '',
+ ],
+ ],
+ ]
+ );
+
+ // Enable i18n.
+ \wp_set_script_translations( 'avatar-privacy-gutenberg', 'avatar-privacy' );
+ }
+
+ /**
+ * Renders the frontend form.
+ *
+ * @param array $attributes {
+ * The `avatar-privacy/form` block attributes.
+ *
+ * @type int $avatar_size The width/height of the avatar preview image (in pixels).
+ * @type string $className The additional classname defined in the Block Editor.
+ * }
+ *
+ * @return string
+ *
+ * @phpstan-param FormBlockAttributes $attributes
+ */
+ public function render_frontend_form( array $attributes ) {
+ $user_id = \get_current_user_id();
+
+ // User not logged in.
+ if ( empty( $user_id ) ) {
+ return '';
+ }
+
+ // Include partial.
+ $markup = $this->form->get_form( 'public/partials/block/frontend-form.php', $user_id, [ 'attributes' => $attributes ] );
+
+ // As an additional precaution, remove some data if we are in preview mode.
+ if ( ! empty( $attributes['preview'] ) ) {
+ // Remove nonces and other hidden fields.
+ $markup = (string) \preg_replace( '/ ]+type=["\']hidden[^>]+>/Si', '', $markup );
+
+ // Also remove links.
+ $markup = (string) \preg_replace( '/(]+)href=("[^"]*"|\'[^\']*\')([^>]+>)/Si', '$1$3', $markup );
+ }
+
+ return $markup;
+ }
+
+ /**
+ * Renders the avatar block.
+ *
+ * @param array $attributes {
+ * The `avatar-privacy/avatar` block attributes.
+ *
+ * @type int $user_id The ID of the user whose avatar should be displayed.
+ * @type int $avatar_size The width/height of the avatar preview image (in pixels).
+ * @type string $className The additional classname defined in the Block Editor.
+ * }
+ *
+ * @return string
+ *
+ * @phpstan-param AvatarBlockAttributes $attributes
+ */
+ public function render_avatar( array $attributes ) {
+ // Attempt to retrieve user.
+ $user = \get_user_by( 'ID', $attributes['user_id'] );
+
+ // No valid user given.
+ if ( empty( $user ) ) {
+ return '';
+ }
+
+ // Set up variables used by the included partial.
+ $args = [
+ 'user' => $user,
+ 'size' => $attributes['avatar_size'],
+ 'class_name' => $attributes['className'],
+ 'align' => ! empty( $attributes['align'] ) ? "align{$attributes['align']}" : '',
+ ];
+
+ // Include partial.
+ return $this->template->get_partial( 'public/partials/block/avatar.php', $args );
+ }
+}
diff --git a/includes/avatar-privacy/components/class-command-line-interface.php b/includes/avatar-privacy/components/class-command-line-interface.php
new file mode 100644
index 0000000..569dd4b
--- /dev/null
+++ b/includes/avatar-privacy/components/class-command-line-interface.php
@@ -0,0 +1,76 @@
+
+ */
+class Command_Line_Interface implements Component {
+
+ /**
+ * An array of CLI commands.
+ *
+ * @var Command[]
+ */
+ private $commands;
+
+ /**
+ * Initialize the class and set its properties.
+ *
+ * @param Command[] $commands An array of CLI commands to register.
+ */
+ public function __construct( array $commands ) {
+ $this->commands = $commands;
+ }
+
+ /**
+ * Sets up the various hooks for the plugin component.
+ *
+ * @return void
+ */
+ public function run() {
+ \add_action( 'cli_init', [ $this, 'register_commands' ] );
+ }
+
+ /**
+ * Registeres all the different CLI commands.
+ *
+ * @return void
+ */
+ public function register_commands() {
+ foreach ( $this->commands as $cmd ) {
+ $cmd->register();
+ }
+ }
+}
diff --git a/includes/avatar-privacy/components/class-comments.php b/includes/avatar-privacy/components/class-comments.php
new file mode 100644
index 0000000..85aab3d
--- /dev/null
+++ b/includes/avatar-privacy/components/class-comments.php
@@ -0,0 +1,334 @@
+
+ */
+class Comments implements Component {
+
+ /**
+ * The name of the checkbox field in the comment form.
+ */
+ const CHECKBOX_FIELD_NAME = 'avatar-privacy-use-gravatar';
+
+ /**
+ * The prefix of the comment cookie (COOKIEHASH is added at the end).
+ *
+ * @var string
+ */
+ const COOKIE_PREFIX = 'comment_use_gravatar_';
+
+ /**
+ * The core API.
+ *
+ * @since 2.4.0
+ *
+ * @var Comment_Author_Fields
+ */
+ private $comment_author;
+
+ /**
+ * The templating handler.
+ *
+ * @since 2.4.0
+ *
+ * @var Template
+ */
+ private $template;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.1.0 Parameter $plugin_file removed.
+ * @since 2.4.0 Parameter $core removed, $comment_author added.
+ *
+ * @param Comment_Author_Fields $comment_author The comment author fields API.
+ * @param Template $template The templating handler.
+ */
+ public function __construct( Comment_Author_Fields $comment_author, Template $template ) {
+ $this->comment_author = $comment_author;
+ $this->template = $template;
+ }
+
+ /**
+ * Sets up the various hooks for the plugin component.
+ *
+ * @return void
+ */
+ public function run() {
+ \add_action( 'init', [ $this, 'init' ] );
+ }
+
+ /**
+ * Initialize additional plugin hooks.
+ *
+ * @return void
+ */
+ public function init() {
+ // Add the checkbox to the comment form.
+ \add_filter( 'comment_form_fields', [ $this, 'comment_form_fields' ] );
+
+ // Handle the checkbox data upon saving the comment.
+ \add_action( 'comment_post', [ $this, 'comment_post' ], 10, 2 );
+
+ // Store gravatar use choice in cookie, if those are enabled in Core.
+ if ( \has_action( 'set_comment_cookies', 'wp_set_comment_cookies' ) ) {
+ \add_action( 'set_comment_cookies', [ $this, 'set_comment_cookies' ], 10, 3 );
+ }
+ }
+
+ /**
+ * Adds the 'use gravatar' checkbox to the comment form. The checkbox value
+ * is read from a cookie if available.
+ *
+ * @param string[] $fields The array of comment fields.
+ *
+ * @return string[] The modified array of comment fields.
+ */
+ public function comment_form_fields( $fields ) {
+ // Don't change the form if a user is logged-in or the field already exists.
+ if ( \is_user_logged_in() || isset( $fields['use_gravatar'] ) ) {
+ return $fields;
+ }
+
+ // Define the new checkbox field.
+ $new_field = $this->get_gravatar_checkbox_markup();
+
+ /**
+ * Filters the insert position for the `use_gravatar` checkbox.
+ *
+ * @since 1.1.0
+ *
+ * @param string[] $position {
+ * Where to insert the checkbox.
+ *
+ * @type string $before_or_after Either 'before' or 'after'.
+ * @type string $insertion_point The index ('url', 'email', etc.) of the field where the checkbox should be inserted.
+ * }
+ */
+ list( $before_or_after, $insertion_point ) = \apply_filters( 'avatar_privacy_use_gravatar_position', $this->get_position( $fields ) );
+
+ if ( isset( $fields[ $insertion_point ] ) ) {
+ $result = [];
+ foreach ( $fields as $key => $value ) {
+ if ( $key === $insertion_point ) {
+ if ( 'before' === $before_or_after ) {
+ $result['use_gravatar'] = $new_field;
+ $result[ $key ] = $value;
+ } else {
+ $result[ $key ] = $value;
+ $result['use_gravatar'] = $new_field;
+ }
+ } else {
+ $result[ $key ] = $value;
+ }
+ }
+ $fields = $result;
+ } else {
+ $fields['use_gravatar'] = $new_field;
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Determines position for inserting the `use_gravatar` field.
+ *
+ * @since 2.1.0
+ *
+ * @param string[] $fields The array of comment fields.
+ *
+ * @return string[] $position {
+ * Where to insert the checkbox.
+ *
+ * @type string $before_or_after Either 'before' or 'after'.
+ * @type string $field The index ('url', 'email', etc.) of the field where the checkbox should be inserted.
+ * }
+ */
+ protected function get_position( array $fields ) {
+ if ( isset( $fields['cookies'] ) ) {
+ // If the `cookies` field exists, add the checkbox just before.
+ $before_or_after = 'before';
+ $field = 'cookies';
+ } elseif ( isset( $fields['url'] ) ) {
+ // Otherwise, if the `url` field exists, add our checkbox after it.
+ $before_or_after = 'after';
+ $field = 'url';
+ } elseif ( isset( $fields['email'] ) ) {
+ // Otherwise, look for the `email` field and add the checkbox after that.
+ $before_or_after = 'after';
+ $field = 'email';
+ } else {
+ // As a last ressort, add the checkbox after all the other fields.
+ \end( $fields );
+ $before_or_after = 'after';
+ $field = (string) \key( $fields );
+ }
+
+ return [ $before_or_after, $field ];
+ }
+
+ /**
+ * Retrieves the markup for the use_gravatar checkbox for the comment form.
+ *
+ * @since 2.1.0 Parameter $path removed.
+ *
+ * @return string
+ */
+ public static function get_gravatar_checkbox() {
+ /**
+ * The share Comments component.
+ *
+ * @var Comments
+ */
+ $comments = Factory::get()->create( self::class );
+
+ return $comments->get_gravatar_checkbox_markup();
+ }
+
+ /**
+ * Retrieves the markup for the use_gravatar checkbox for the comment form.
+ *
+ * @since 2.4.0
+ *
+ * @param string $partial Optional. The partial to use. Default 'public/partials/comments/use-gravatar.php'.
+ *
+ * @return string
+ */
+ public function get_gravatar_checkbox_markup( $partial = 'public/partials/comments/use-gravatar.php' ) {
+ // Determine if the checkbox should be checked.
+ $args = [
+ 'template' => $this->template,
+ 'is_checked' => $this->is_gravatar_prechecked(),
+ ];
+
+ return $this->template->get_partial( $partial, $args );
+ }
+
+ /**
+ * Verifies whether the Use Gravatar checkbox should be pre-checked.
+ *
+ * @since 2.4.0
+ *
+ * @global array $_POST Post request superglobal.
+ * @global array $_COOKIE Cookie superglobal.
+ *
+ * @return bool
+ */
+ protected function is_gravatar_prechecked() {
+ // Default is unchecked.
+ $is_checked = false;
+
+ if ( isset( $_POST[ self::CHECKBOX_FIELD_NAME ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- frontend form.
+ // Re-displaying the comment form with validation errors.
+ $is_checked = ! empty( $_POST[ self::CHECKBOX_FIELD_NAME ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- frontend form.
+ } elseif ( isset( $_COOKIE[ self::COOKIE_PREFIX . \COOKIEHASH ] ) ) {
+ // Read the value from the cookie, saved with previous comment.
+ $is_checked = ! empty( $_COOKIE[ self::COOKIE_PREFIX . \COOKIEHASH ] );
+ }
+
+ return $is_checked;
+ }
+
+ /**
+ * Saves the value of the 'use gravatar' checkbox from the comment form in
+ * the database, but only for non-spam comments.
+ *
+ * @param int $comment_id The ID of the comment that has just been saved.
+ * @param string $comment_approved Whether the comment has been approved (1)
+ * or not (0) or is marked as spam (spam).
+ *
+ * @return void
+ */
+ public function comment_post( $comment_id, $comment_approved ) {
+ // Don't save anything for spam comments, trackbacks/pingbacks, and registered user's comments.
+ if ( 'spam' === $comment_approved ) {
+ return;
+ }
+ $comment = \get_comment( $comment_id );
+ if (
+ ! $comment instanceof \WP_Comment ||
+ ( '' !== $comment->comment_type && 'comment' !== $comment->comment_type ) ||
+ ( '' === $comment->comment_author_email )
+ ) {
+ return;
+ }
+
+ // Make sure that the e-mail address does not belong to a registered user.
+ if ( \get_user_by( 'email', $comment->comment_author_email ) ) {
+ // This is either a comment with a fake identity or a user who didn't sign in
+ // and rather entered their details manually. Either way, don't save anything.
+ return;
+ }
+
+ // Save the 'use gravatar' value.
+ $use_gravatar = ( isset( $_POST[ self::CHECKBOX_FIELD_NAME ] ) && ( 'true' === $_POST[ self::CHECKBOX_FIELD_NAME ] ) ) ? 1 : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ $this->comment_author->update_gravatar_use( $comment->comment_author_email, $comment_id, $use_gravatar );
+ }
+
+ /**
+ * Sets the comment_use_gravatar_ cookie. Based on `wp_set_comment_cookies`.
+ *
+ * @param \WP_Comment $comment Comment object.
+ * @param \WP_User $user Comment author's user object. The user may not exist.
+ * @param bool $cookies_consent Optional. Comment author's consent to store cookies. Default true.
+ *
+ * @return void
+ */
+ public function set_comment_cookies( \WP_Comment $comment, \WP_User $user, $cookies_consent = true ) {
+ // If the user already exists, or the user opted out of cookies, don't set cookies.
+ if ( $user->exists() ) {
+ return;
+ }
+
+ if ( false === $cookies_consent ) {
+ // Remove any existing cookie.
+ \setcookie( self::COOKIE_PREFIX . \COOKIEHASH, '', time() - \YEAR_IN_SECONDS, \COOKIEPATH, \COOKIE_DOMAIN );
+ return;
+ }
+
+ // Does the author want to use gravatar?
+ $use_gravatar = $this->comment_author->allows_gravatar_use( $comment->comment_author_email );
+
+ // Set a cookie for the 'use gravatar' value.
+ /** This filter is documented in wp-includes/comment.php */
+ $comment_cookie_lifetime = \apply_filters( 'comment_cookie_lifetime', 30000000 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
+ $secure = ( 'https' === \wp_parse_url( \home_url(), \PHP_URL_SCHEME ) );
+ \setcookie( self::COOKIE_PREFIX . \COOKIEHASH, (string) $use_gravatar, \time() + $comment_cookie_lifetime, \COOKIEPATH, \COOKIE_DOMAIN, $secure );
+ }
+}
diff --git a/includes/avatar-privacy/components/class-image-proxy.php b/includes/avatar-privacy/components/class-image-proxy.php
new file mode 100644
index 0000000..8f7de5a
--- /dev/null
+++ b/includes/avatar-privacy/components/class-image-proxy.php
@@ -0,0 +1,349 @@
+
+ *
+ * @phpstan-type HandlerHooks array
+ */
+class Image_Proxy implements Component {
+
+ const CRON_JOB_LOCK_GRAVATARS = 'cron_job_lock_gravatars';
+ const CRON_JOB_LOCK_ALL_IMAGES = 'cron_job_lock_all_images';
+
+ const CRON_JOB_ACTION = 'avatar_privacy_daily';
+
+ /**
+ * The site transients handler.
+ *
+ * @var Site_Transients
+ */
+ private Site_Transients $site_transients;
+
+ /**
+ * The file system caching handler.
+ *
+ * @var Filesystem_Cache
+ */
+ private Filesystem_Cache $file_cache;
+
+ /**
+ * The available avatar handlers.
+ *
+ * @var Avatar_Handler[]
+ */
+ private array $handlers = [];
+
+ /**
+ * A mapping from filter hook to avatar handler.
+ *
+ * @var array {
+ * @type Avatar_Handler $hook The handler instance.
+ * }
+ *
+ * @phpstan-var HandlerHooks
+ */
+ private array $handler_hooks;
+
+ /**
+ * The default icons handler.
+ *
+ * @var Default_Icons_Handler
+ */
+ private Default_Icons_Handler $default_icons;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.4.0 Parameters $gravatar and $user_avatar replaced with the
+ * generic $handler. Parameter $options removed.
+ *
+ * @param Site_Transients $site_transients The site transients handler.
+ * @param Filesystem_Cache $file_cache The filesystem cache handler.
+ * @param Avatar_Handler[] $handlers The avatar handlers indexed
+ * by their filter hook (including
+ * the $default_icons handler).
+ * @param Default_Icons_Handler $default_icons The default icons handler.
+ *
+ * @phpstan-param HandlerHooks $handlers
+ */
+ public function __construct( Site_Transients $site_transients, Filesystem_Cache $file_cache, array $handlers, Default_Icons_Handler $default_icons ) {
+ $this->site_transients = $site_transients;
+ $this->file_cache = $file_cache;
+
+ // Avatar handlers.
+ $this->handler_hooks = $handlers;
+ $this->default_icons = $default_icons;
+
+ foreach ( $handlers as $avatar_handler ) {
+ $type = $avatar_handler->get_type();
+
+ // The default handler will be ignored.
+ if ( ! empty( $type ) ) {
+ $this->handlers[ $type ] = $avatar_handler;
+ }
+ }
+ }
+
+ /**
+ * Sets up the various hooks for the plugin component.
+ *
+ * @return void
+ */
+ public function run() {
+ // Add new default avatars.
+ \add_filter( 'avatar_defaults', [ $this->default_icons, 'avatar_defaults' ] );
+
+ // Generate the correct avatar images.
+ foreach ( $this->handler_hooks as $hook => $handler ) {
+ \add_filter( $hook, [ $handler, 'get_url' ], 10, 4 ); // @phpstan-ignore-line -- WordPress array syntax not supported (yet).
+ }
+
+ // Automatically regenerate missing image files.
+ \add_action( 'init', [ $this, 'add_cache_rewrite_rules' ] );
+ \add_action( 'parse_request', [ $this, 'load_cached_avatar' ] );
+
+ // Clean up cache once per day.
+ \add_action( 'init', [ $this, 'enable_image_cache_cleanup' ] );
+ }
+
+ /**
+ * Add rewrite rules for nice avatar caching.
+ *
+ * @return void
+ */
+ public function add_cache_rewrite_rules() {
+ /**
+ * The global WordPress instance.
+ *
+ * @var \WP
+ */
+ global $wp;
+ $wp->add_query_var( 'avatar-privacy-file' );
+
+ $basedir = \str_replace( \ABSPATH, '', $this->file_cache->get_base_dir() );
+ \add_rewrite_rule( "^{$basedir}(.*)", [ 'avatar-privacy-file' => '$matches[1]' ], 'top' );
+ }
+
+ /**
+ * Short-circuits WordPress initialization and load displays the cached avatar image.
+ *
+ * @param \WP $wp The WordPress global object.
+ *
+ * @return void
+ */
+ public function load_cached_avatar( \WP $wp ) {
+ if ( empty( $wp->query_vars['avatar-privacy-file'] ) || ! \preg_match( '#^([a-z]+)/((?:[0-9a-z]/)*)([a-f0-9]{64})(?:-([0-9]+))?\.(jpg|png|svg)$#i', $wp->query_vars['avatar-privacy-file'], $parts ) ) {
+ // Abort early.
+ return;
+ }
+
+ list(, $type, $subdir, $hash, $size, $extension ) = $parts;
+
+ $file = "{$this->file_cache->get_base_dir()}{$type}/" . ( $subdir ?: '' ) . $hash . ( empty( $size ) ? '' : "-{$size}" ) . ".{$extension}";
+
+ if ( ! \file_exists( $file ) ) {
+ // Default size (for SVGs mainly, which ignore it).
+ $size = (int) $size ?: 100;
+
+ if ( isset( $this->handlers[ $type ] ) ) {
+ $success = $this->handlers[ $type ]->cache_image( $type, $hash, $size, $subdir, $extension );
+ } else {
+ $success = $this->default_icons->cache_image( $type, $hash, $size, $subdir, $extension );
+ }
+
+ if ( ! $success ) {
+ /* translators: $file path */
+ \wp_die( \esc_html( \sprintf( \__( 'Error generating avatar file %s.', 'avatar-privacy' ), $file ) ) );
+ }
+ }
+
+ $this->send_image( $file, \DAY_IN_SECONDS, Image_File::CONTENT_TYPE[ $extension ] );
+
+ // We're done.
+ $this->exit_request();
+ }
+
+ /**
+ * Sends an image file to the browser.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ *
+ * @param string $file The full path to the image.
+ * @param int $cache_time The time the image should be cached by the brwoser (in seconds).
+ * @param string $content_type The content MIME type.
+ *
+ * @return void
+ */
+ protected function send_image( $file, $cache_time, $content_type ) {
+ $image = @\file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, Generic.PHP.NoSilencedErrors.Discouraged
+
+ if ( ! empty( $image ) ) {
+ $length = \strlen( $image );
+ $last_modified = (int) \filemtime( $file );
+
+ // Let's set some HTTP headers.
+ \header( "Content-Type: {$content_type}" );
+ \header( "Content-Length: {$length}" );
+ \header( 'Last-Modified: ' . \gmdate( 'D, d M Y H:i:s \G\M\T', $last_modified ) );
+ \header( 'Expires: ' . \gmdate( 'D, d M Y H:i:s \G\M\T', \time() + $cache_time ) );
+
+ // Here comes the content.
+ echo $image; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ } else {
+ /* translators: $file path */
+ \wp_die( \esc_html( \sprintf( \__( 'Error generating avatar file %s.', 'avatar-privacy' ), $file ) ) );
+ }
+ }
+
+ /**
+ * Schedules a cron job to clean up the image cache once per day. Otherwise the
+ * cache would grow unchecked and new avatar images uploaded to Gravatar.com
+ * would not be picked up.
+ *
+ * @return void
+ */
+ public function enable_image_cache_cleanup() {
+ // Schedule our cron action.
+ if ( ! \wp_next_scheduled( self::CRON_JOB_ACTION ) ) {
+ \wp_schedule_event( \time(), 'daily', self::CRON_JOB_ACTION );
+ }
+
+ // Add separate jobs for gravatars other images.
+ \add_action( self::CRON_JOB_ACTION, [ $this, 'trim_gravatar_cache' ] );
+ \add_action( self::CRON_JOB_ACTION, [ $this, 'trim_image_cache' ] );
+ }
+
+ /**
+ * Deletes cached gravatar images that are too old. Uses a site transient to ensure
+ * that the clean-up happens only once per day on multisite installations.
+ *
+ * @return void
+ */
+ public function trim_gravatar_cache() {
+ if ( ! $this->site_transients->get( self::CRON_JOB_LOCK_GRAVATARS ) ) {
+ /**
+ * Filters how long cached gravatar images are kept.
+ *
+ * @param int $max_age The maximum age of the cached files (in seconds). Default 2 days.
+ */
+ $max_age = \apply_filters( 'avatar_privacy_gravatars_max_age', 2 * \DAY_IN_SECONDS );
+
+ /**
+ * Filters how often the clean-up cron job for old gravatar images should run.
+ *
+ * @param int $interval Time until the cron job should run again (in seconds). Default 1 day.
+ */
+ $interval = \apply_filters( 'avatar_privacy_gravatars_cleanup_interval', \DAY_IN_SECONDS );
+
+ $this->invalidate_cached_images( self::CRON_JOB_LOCK_GRAVATARS, 'gravatar', $interval, $max_age );
+ }
+ }
+
+ /**
+ * Deletes cached image files that are too old. Uses a site transient to ensure
+ * that the clean-up happens only once per day on multisite installations.
+ *
+ * @return void
+ */
+ public function trim_image_cache() {
+ if ( ! $this->site_transients->get( self::CRON_JOB_LOCK_ALL_IMAGES ) ) {
+ /**
+ * Filters how long cached images are kept.
+ *
+ * Normally, generated default icons and local avatar images don't
+ * change, so they can be kept longer than gravatars. To keep cache
+ * size under control, the limit should be set approximately between
+ * a week and a month, depending on the number of commenters on your
+ * site.
+ *
+ * @param int $max_age The maximum age of the cached files (in seconds). Default 1 week.
+ */
+ $max_age = \apply_filters( 'avatar_privacy_all_images_max_age', 7 * \DAY_IN_SECONDS );
+
+ /**
+ * Filters how often the clean-up cron job for old images should run.
+ *
+ * @param int $interval Time until the cron job should run again (in seconds). Default 1 week.
+ */
+ $interval = \apply_filters( 'avatar_privacy_all_images_cleanup_interval', 7 * \DAY_IN_SECONDS );
+
+ $this->invalidate_cached_images( self::CRON_JOB_LOCK_ALL_IMAGES, '', $interval, $max_age );
+ }
+ }
+
+ /**
+ * Removes all files older than the maximum age from given subdirectory.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ *
+ * @param string $lock The site transient key for ensuring that the job is not run more often than necessary.
+ * @param string $subdir The subdirectory to clean.
+ * @param int $interval The cron job run interval in seconds.
+ * @param int $max_age The maximum age of the image files in seconds.
+ *
+ * @return void
+ */
+ protected function invalidate_cached_images( $lock, $subdir, $interval, $max_age ) {
+ // Invalidate all files in the subdirectory older than the maximum age.
+ $this->file_cache->invalidate_files_older_than( $max_age, $subdir );
+
+ // Don't run the job again until the interval is up.
+ $this->site_transients->set( $lock, true, $interval );
+ }
+
+ /**
+ * Stops executing the current request early.
+ *
+ * @since 2.1.0
+ * @codeCoverageIgnore
+ *
+ * @param int $status Optional. A status code in the range 0 to 254. Default 0.
+ *
+ * @return void
+ */
+ protected function exit_request( $status = 0 ) {
+ exit( $status ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ }
+}
diff --git a/includes/avatar-privacy/components/class-integrations.php b/includes/avatar-privacy/components/class-integrations.php
new file mode 100644
index 0000000..744617a
--- /dev/null
+++ b/includes/avatar-privacy/components/class-integrations.php
@@ -0,0 +1,80 @@
+
+ */
+class Integrations implements Component {
+
+ /**
+ * An array of plugin integration instances.
+ *
+ * @var Plugin_Integration[]
+ */
+ private $integrations = [];
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.2.0 Parameter $core removed.
+ *
+ * @param Plugin_Integration[] $integrations An array of plugin integration instances.
+ */
+ public function __construct( array $integrations ) {
+ $this->integrations = $integrations;
+ }
+
+ /**
+ * Activate all applicable plugin integrations.
+ *
+ * @return void
+ */
+ public function activate() {
+ foreach ( $this->integrations as $integration ) {
+ if ( $integration->check() ) {
+ $integration->run();
+ }
+ }
+ }
+
+ /**
+ * Start up enabled integrations.
+ *
+ * @return void
+ */
+ public function run() {
+ \add_action( 'plugins_loaded', [ $this, 'activate' ], 1 );
+ }
+}
diff --git a/includes/avatar-privacy/components/class-network-settings-page.php b/includes/avatar-privacy/components/class-network-settings-page.php
new file mode 100644
index 0000000..7608a7e
--- /dev/null
+++ b/includes/avatar-privacy/components/class-network-settings-page.php
@@ -0,0 +1,376 @@
+
+ *
+ * @phpstan-type SettingsSectionInfo array{ id: string, title: string, callback: callable }
+ */
+class Network_Settings_Page implements Component {
+
+ const OPTION_GROUP = 'avatar-privacy-network-settings';
+ const SECTION = 'general';
+ const ACTION = 'edit-avatar-privacy-network-settings';
+
+ /**
+ * The options handler.
+ *
+ * @var Network_Options
+ */
+ private Network_Options $network_options;
+
+ /**
+ * The (standard) transietns handler.
+ *
+ * @var Transients
+ */
+ private Transients $transients;
+
+ /**
+ * The default settings.
+ *
+ * @var Settings
+ */
+ private Settings $settings;
+
+ /**
+ * The multisite tools.
+ *
+ * @var Multisite
+ */
+ private Multisite $multisite;
+
+ /**
+ * The UI controls for the settings.
+ *
+ * @var Control[]
+ */
+ private array $controls;
+
+ /**
+ * An array to keep track of triggered admin notices.
+ *
+ * @var bool[]
+ */
+ private array $triggered_notice = [];
+
+ /**
+ * The script & style registration helper.
+ *
+ * @since 2.4.0
+ *
+ * @var Dependencies
+ */
+ private Dependencies $dependencies;
+
+ /**
+ * The templating handler.
+ *
+ * @since 2.4.0
+ *
+ * @var Template
+ */
+ private Template $template;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.1.0 Parameter $plugin_file removed.
+ * @since 2.4.0 Parameter $core removed, parameters $dependencies, and $template
+ * added.
+ *
+ * @param Network_Options $network_options The network options handler.
+ * @param Transients $transients The transients handler.
+ * @param Settings $settings The default settings.
+ * @param Multisite $multisite The the multisite handler.
+ * @param Dependencies $dependencies The script & style registration helper.
+ * @param Template $template The templating handler.
+ */
+ public function __construct( Network_Options $network_options, Transients $transients, Settings $settings, Multisite $multisite, Dependencies $dependencies, Template $template ) {
+ $this->network_options = $network_options;
+ $this->transients = $transients;
+ $this->settings = $settings;
+ $this->multisite = $multisite;
+ $this->dependencies = $dependencies;
+ $this->template = $template;
+ }
+
+ /**
+ * Sets up the various hooks for the plugin component.
+ *
+ * @return void
+ */
+ public function run() {
+ if ( \is_network_admin() ) {
+ // Load the field definitions.
+ $fields = $this->settings->get_network_fields();
+
+ // Initialize the controls.
+ $this->controls = Control_Factory::initialize( $fields, $this->network_options, '' );
+
+ // Add some actions.
+ \add_action( 'network_admin_menu', [ $this, 'register_network_settings' ] );
+ \add_action( 'network_admin_edit_' . self::ACTION, [ $this, 'save_network_settings' ] );
+ \add_action( 'network_admin_notices', 'settings_errors' );
+ }
+ }
+
+ /**
+ * Registers the settings with the settings API. This is only used to display
+ * an explanation of the wrong gravatar settings.
+ *
+ * @return void
+ */
+ public function register_network_settings() {
+ // Create our options page.
+ $page = \add_submenu_page( 'settings.php', \__( 'Avatar Privacy Network Settings', 'avatar-privacy' ), \__( 'Avatar Privacy', 'avatar-privacy' ), 'manage_network_options', self::OPTION_GROUP, [ $this, 'print_settings_page' ] );
+
+ // Add the section(s).
+ \add_settings_section( self::SECTION, '', [ $this, 'print_settings_section' ], self::OPTION_GROUP );
+
+ // Register control render callbacks.
+ foreach ( $this->controls as $option => $control ) {
+ $option_name = $this->network_options->get_name( $option );
+ $sanitize = [ $control, 'sanitize' ];
+
+ // Register the setting ...
+ \register_setting( self::OPTION_GROUP, $option_name, $sanitize );
+
+ // ... and the control.
+ $control->register( self::OPTION_GROUP );
+ }
+
+ // Trigger table migration if the settings are changed.
+ $use_global_table = $this->network_options->get_name( Network_Options::USE_GLOBAL_TABLE );
+ \add_action( "update_site_option_{$use_global_table}", [ $this, 'start_migration_from_global_table' ], 10, 3 );
+
+ // Use the registered $page handle to hook stylesheet and script loading.
+ \add_action( "admin_print_styles-{$page}", [ $this, 'print_styles' ] );
+ }
+
+ /**
+ * Displays the network options page.
+ *
+ * @return void
+ */
+ public function print_settings_page() {
+ // Load the settings page HTML.
+ $this->template->print_partial( 'admin/partials/network/settings-page.php' );
+ }
+
+ /**
+ * Saves the network settings.
+ *
+ * @global array $_POST Post request superglobal.
+ * @global array $new_whitelist_options The options whitelisted by the settings API.
+ *
+ * @return void
+ */
+ public function save_network_settings() {
+ // Check if the user has the correct permissions.
+ if ( ! \current_user_can( 'manage_network_options' ) ) {
+ \wp_die( \esc_html( \__( 'Sorry, you are not allowed to access this page.', 'avatar-privacy' ) ), 403 );
+ }
+
+ // Make sure we are posting from our options page.
+ \check_admin_referer( self::OPTION_GROUP . '-options' );
+
+ // This is the list of registered options.
+ global $new_whitelist_options;
+
+ // Go through the posted data and save only our options.
+ foreach ( $new_whitelist_options[ self::OPTION_GROUP ] as $option ) {
+ if ( isset( $_POST[ $option ] ) ) {
+ // The registered callback function to sanitize the option's value will be called here.
+ $this->network_options->set( $option, \wp_unslash( $_POST[ $option ] ), false, true ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ } else {
+ // Set false for checkboxes and unset everything else.
+ $id = $this->network_options->remove_prefix( $option );
+ if ( $this->controls[ $id ] instanceof Controls\Checkbox_Input ) {
+ $this->network_options->set( $option, false, false, true );
+ } else {
+ $this->network_options->delete( $option, true );
+ }
+ }
+ }
+
+ $settings_errors = \get_settings_errors();
+ if ( empty( $settings_errors ) ) { // @phpstan-ignore-line - $settings_errors array can be empty.
+ \add_settings_error( self::OPTION_GROUP, 'settings_updated', \__( 'Settings saved.', 'avatar-privacy' ), 'updated' );
+ }
+
+ // Save the settings errors until after the redirect.
+ $this->persist_settings_errors();
+
+ // At last we redirect back to our options page.
+ \wp_safe_redirect(
+ \add_query_arg( // @codeCoverageIgnoreStart
+ [
+ 'page' => self::OPTION_GROUP,
+ 'settings-updated' => 'true',
+ ],
+ \network_admin_url( 'settings.php' )
+ ) // @codeCoverageIgnoreEnd
+ );
+
+ // And we are done.
+ $this->exit_request();
+ }
+
+ /**
+ * Prints any additional markup for the given form section.
+ *
+ * @param array $section The section information.
+ *
+ * @return void
+ *
+ * @phpstan-param SettingsSectionInfo $section
+ */
+ public function print_settings_section( $section ) {
+ // Set up variables used by the included partial.
+ $args = [
+ 'section_id' => ! empty( $section['id'] ) ? $section['id'] : '',
+ 'description' => \__( 'General settings applying to all sites in the network.', 'avatar-privacy' ),
+ ];
+
+ // Load the settings page HTML.
+ $this->template->print_partial( 'admin/partials/network/section.php', $args );
+ }
+
+ /**
+ * Stops executing the current request early.
+ *
+ * @codeCoverageIgnore
+ *
+ * @param int $status Optional. A status code in the range 0 to 254. Default 0.
+ *
+ * @return void
+ */
+ protected function exit_request( $status = 0 ) {
+ exit( $status ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+ }
+
+ /**
+ * Enqueue stylesheet for options page.
+ *
+ * @return void
+ */
+ public function print_styles() {
+ $this->dependencies->register_style( 'avatar-privacy-settings', 'admin/css/settings.css' );
+ $this->dependencies->enqueue_style( 'avatar-privacy-settings' );
+ }
+
+ /**
+ * Use sanitization callback to trigger an admin notice.
+ *
+ * @param string $setting_name The setting used to trigger the notice (without the prefix).
+ * @param string $notice_id HTML ID attribute for the notice.
+ * @param string $message Translated message string.
+ * @param string $notice_level 'updated', 'notice-info', etc.
+ *
+ * @return void
+ */
+ protected function trigger_admin_notice( $setting_name, $notice_id, $message, $notice_level ) {
+ if ( empty( $this->triggered_notice[ $setting_name ] ) ) {
+ \add_settings_error( self::OPTION_GROUP, $notice_id, $message, $notice_level );
+
+ // Workaround for https://core.trac.wordpress.org/ticket/21989.
+ $this->triggered_notice[ $setting_name ] = true;
+ }
+ }
+
+ /**
+ * Persists the settings errors across the redirect.
+ *
+ * Uses a regular transient to stay compatible with core.
+ *
+ * @return void
+ */
+ protected function persist_settings_errors() {
+ // A regular transient is used here, since it is automatically cleared right after the redirect.
+ $this->transients->set( 'settings_errors', \get_settings_errors(), 30, true );
+ }
+
+ /**
+ * Triggers the migration from global to site specific tables when global table
+ * use is changed from enbled to disabled.
+ *
+ * @param string $option Name of the network option.
+ * @param mixed $value New value of the network option.
+ * @param mixed $old_value Old value of the network option.
+ *
+ * @return void
+ */
+ public function start_migration_from_global_table( $option, $value, $old_value ) {
+ if ( $option !== $this->network_options->get_name( Network_Options::USE_GLOBAL_TABLE ) ) {
+ // This should never happen.
+ return;
+ }
+
+ // Only trigger migration if USE_GLOBAL_TABLE was changed from "on" to "off".
+ if ( empty( $value ) && ! empty( $old_value ) ) {
+ // Add all sites in the current network to the queue.
+ $site_ids = $this->multisite->get_site_ids();
+ $queue = (array) \array_combine( $site_ids, $site_ids );
+
+ // Remove the main site ID from the queue.
+ unset( $queue[ \get_main_site_id() ] );
+
+ // Store new queue, overwriting any existing queue (since this per
+ // network and we already got all sites currently in the network).
+ // If the new queue is empty, the next page load will clean up the
+ // network options.
+ $this->network_options->set( Network_Options::START_GLOBAL_TABLE_MIGRATION, $queue );
+
+ // Notify admins.
+ $this->trigger_admin_notice( Network_Options::USE_GLOBAL_TABLE, 'settings_updated', \__( 'Settings saved. Consent data will be migrated to site-specific tables.', 'avatar-privacy' ), 'updated' );
+ } elseif ( ! empty( $value ) && empty( $old_value ) ) {
+ // Clean up any running migrations on the next page load.
+ $this->network_options->set( Network_Options::START_GLOBAL_TABLE_MIGRATION, [] );
+ }
+ }
+}
diff --git a/includes/avatar-privacy/components/class-privacy-tools.php b/includes/avatar-privacy/components/class-privacy-tools.php
new file mode 100644
index 0000000..a9c020f
--- /dev/null
+++ b/includes/avatar-privacy/components/class-privacy-tools.php
@@ -0,0 +1,355 @@
+
+ *
+ * @phpstan-type PrivacyExportResult array{ data: mixed[], done: bool }
+ * @phpstan-type PrivacyEraseResult array{ items_removed: int, items_retained: int, messages: string[], done: bool }
+ *
+ * @phpstan-type PrivacyExporter array{ exporter_friendly_name: string, callback: callable }
+ * @phpstan-type PrivacyEraser array{ eraser_friendly_name: string, callback: callable }
+ */
+class Privacy_Tools implements Component {
+
+ const PAGING = 500;
+
+ /**
+ * The user fields API.
+ *
+ * @since 2.4.0
+ *
+ * @var User_Fields
+ */
+ private User_Fields $registered_user;
+
+ /**
+ * The comment author API.
+ *
+ * @since 2.4.0
+ *
+ * @var Comment_Author_Fields
+ */
+ private Comment_Author_Fields $comment_author;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param User_Fields $registered_user The user fields API.
+ * @param Comment_Author_Fields $comment_author The comment author fields API.
+ */
+ public function __construct( User_Fields $registered_user, Comment_Author_Fields $comment_author ) {
+ $this->registered_user = $registered_user;
+ $this->comment_author = $comment_author;
+ }
+
+ /**
+ * Sets up the various hooks for the plugin component.
+ *
+ * @return void
+ */
+ public function run() {
+ \add_action( 'admin_init', [ $this, 'admin_init' ] );
+ }
+
+ /**
+ * Initializes additional plugin hooks.
+ *
+ * @return void
+ */
+ public function admin_init() {
+ // Add privacy notice suggestion.
+ $this->add_privacy_notice_content();
+
+ // Register data exporter.
+ \add_filter( 'wp_privacy_personal_data_exporters', [ $this, 'register_personal_data_exporter' ], 0 ); // Priority 0 to follow after the built-in exporters. Watch https://core.trac.wordpress.org/ticket/44151.
+
+ // Register data eraser.
+ \add_filter( 'wp_privacy_personal_data_erasers', [ $this, 'register_personal_data_eraser' ] );
+ }
+
+ /**
+ * Adds a privacy notice snippet.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ *
+ * @return void
+ */
+ protected function add_privacy_notice_content() {
+ // Don't crash on older versions of WordPress.
+ if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
+ return;
+ }
+
+ $suggested_text = '' . \__( 'Suggested text:' ) . ' '; // phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- Missing text domain is intentional to use Core translation.
+
+ $content = '' . \__( 'Comments', 'avatar-privacy' ) . ' ';
+ $content .= '' . \__( 'The information in this subsection supersedes the paragraph on Gravatar in the default "Comments" subsection provided by WordPress.', 'avatar-privacy' ) . '
';
+ $content .= "{$suggested_text}" . \__( 'At your option, an anonymized string created from your email address (also called a hash) may be provided to the Gravatar service to see if you are using it. The Gravatar service privacy policy is available here: https://automattic.com/privacy/. After approval of your comment, your profile picture is visible to the public in the context of your comment. Neither the hash nor your actual email address will be exposed to the public.', 'avatar-privacy' ) . '
';
+ $content .= '' . \__( 'Cookies', 'avatar-privacy' ) . ' ';
+ $content .= '' . \__( 'The information in this subsection should be included in addition to the information about any other cookies set by either WordPress or another plugin.', 'avatar-privacy' ) . '
';
+ $content .= "{$suggested_text}" . \__( 'If you leave a comment on our site and opt-in to display your Gravatar image, your choice will be stored in a cookie. This is for your convenience so that you do not have to fill the checkbox again when you leave another comment. This cookie will last for one year.', 'avatar-privacy' ) . '
';
+
+ \wp_add_privacy_policy_content( \__( 'Avatar Privacy', 'avatar-privacy' ), $content );
+ }
+
+ /**
+ * Registers an exporter function for the personal data collected by this plugin.
+ *
+ * @param array $exporters The registered exporter callbacks.
+ *
+ * @return array
+ *
+ * @phpstan-param PrivacyExporter[] $exporters
+ * @phpstan-return PrivacyExporter[]
+ */
+ public function register_personal_data_exporter( array $exporters ) {
+ $exporters['avatar-privacy-user'] = [
+ 'exporter_friendly_name' => \__( 'Avatar Privacy Plugin User Data', 'avatar-privacy' ),
+ 'callback' => [ $this, 'export_user_data' ],
+ ];
+ $exporters['avatar-privacy-comment-author'] = [
+ 'exporter_friendly_name' => \__( 'Avatar Privacy Plugin Comment Author Data', 'avatar-privacy' ),
+ 'callback' => [ $this, 'export_comment_author_data' ],
+ ];
+
+ return $exporters;
+ }
+
+ /**
+ * Registers an eraser function for the personal data collected by this plugin.
+ *
+ * @param array $erasers The registered eraser callbacks.
+ *
+ * @return array
+ *
+ * @phpstan-param PrivacyEraser[] $erasers
+ * @phpstan-return PrivacyEraser[]
+ */
+ public function register_personal_data_eraser( array $erasers ) {
+ $erasers['avatar-privacy'] = [
+ 'eraser_friendly_name' => \__( 'Avatar Privacy Plugin', 'avatar-privacy' ),
+ 'callback' => [ $this, 'erase_data' ],
+ ];
+
+ return $erasers;
+ }
+
+ /**
+ * Exports the data associated with a user account.
+ *
+ * @since 2.5.0 Unused parameter $page removed.
+ *
+ * @param string $email The email address.
+ *
+ * @return array {
+ * @type mixed $data The exported data.
+ * @type bool $done True if there is no more data to export, false otherwise.
+ * }
+ *
+ * @phpstan-return PrivacyExportResult
+ */
+ public function export_user_data( $email ) {
+ $user = \get_user_by( 'email', $email );
+ if ( empty( $user ) ) {
+ return [
+ 'data' => [],
+ 'done' => true,
+ ];
+ }
+
+ // Initialize export data.
+ $user_data = [];
+
+ // Export the hashed email.
+ $user_data[] = [
+ 'name' => \__( 'User Email Hash', 'avatar-privacy' ),
+ 'value' => $this->registered_user->get_hash( $user->ID ),
+ ];
+
+ // Export the `use_gravatar` setting.
+ if ( $this->registered_user->has_gravatar_policy( $user->ID ) ) {
+ $user_data[] = [
+ 'name' => \__( 'Use Gravatar.com', 'avatar-privacy' ),
+ 'value' => $this->registered_user->allows_gravatar_use( $user->ID ),
+ ];
+ }
+
+ // Export the `allow_anonymous` setting.
+ if ( $this->registered_user->has_anonymous_commenting_policy( $user->ID ) ) {
+ $user_data[] = [
+ 'name' => \__( 'Logged-out Commenting', 'avatar-privacy' ),
+ 'value' => $this->registered_user->allows_anonymous_commenting( $user->ID ),
+ ];
+ }
+
+ // Export the uploaded avatar.
+ // We don't want to use the filtered value here.
+ $local_avatar = $this->registered_user->get_local_avatar( $user->ID );
+ if ( ! empty( $local_avatar['file'] ) ) {
+ $user_data[] = [
+ 'name' => \__( 'User Profile Picture', 'avatar-privacy' ),
+ 'value' => \str_replace( \ABSPATH, \trailingslashit( \site_url() ), $local_avatar['file'] ),
+ ];
+ }
+
+ return [
+ 'data' => [
+ [
+ 'group_id' => 'user', // Existing Core group.
+ 'group_label' => \__( 'User' ), // phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- Missing text domain is intentional to use Core translation.
+ 'item_id' => "user-{$user->ID}", // Existing Core item ID.
+ 'data' => $user_data, // The personal data that should be exported.
+ ],
+ ],
+ 'done' => true,
+ ];
+ }
+
+ /**
+ * Exports the data associated with a comment author email address.
+ *
+ * @since 2.5.0 Unused parameter $page removed.
+ *
+ * @param string $email The email address.
+ *
+ * @return array {
+ * @type mixed $data The exported data.
+ * @type bool $done True if there is no more data to export, false otherwise.
+ * }
+ *
+ * @phpstan-return PrivacyExportResult
+ */
+ public function export_comment_author_data( $email ) {
+ // Load raw data.
+ $raw_data = $this->comment_author->load( $email );
+ if ( empty( $raw_data ) ) {
+ return [
+ 'data' => [],
+ 'done' => true,
+ ];
+ }
+
+ // Export the avatar privacy ID.
+ $data = [];
+ $id = $raw_data->id;
+ $data[] = [
+ 'name' => \__( 'Avatar Privacy Comment Author ID', 'avatar-privacy' ),
+ 'value' => $id,
+ ];
+
+ // Export the email.
+ $data[] = [
+ 'name' => \__( 'Comment Author Email', 'avatar-privacy' ),
+ 'value' => $raw_data->email,
+ ];
+
+ // Export the hashed email.
+ $data[] = [
+ 'name' => \__( 'Comment Author Email Hash', 'avatar-privacy' ),
+ 'value' => $raw_data->hash,
+ ];
+
+ // Export the `use_gravatar` setting.
+ $data[] = [
+ 'name' => \__( 'Use Gravatar.com', 'avatar-privacy' ),
+ 'value' => $raw_data->use_gravatar,
+ ];
+
+ // Export the last modified date.
+ $data[] = [
+ 'name' => \__( 'Last Updated', 'avatar-privacy' ),
+ 'value' => $raw_data->last_updated,
+ ];
+
+ // Export the log message.
+ $data[] = [
+ 'name' => \__( 'Log Message', 'avatar-privacy' ),
+ 'value' => $raw_data->log_message,
+ ];
+
+ return [
+ 'data' => [
+ [
+ 'group_id' => 'avatar-privacy', // An ID to identify this particular group of information.
+ 'group_label' => \__( 'Avatar Privacy', 'avatar-privacy' ), // A translatable string to label this group of information.
+ 'item_id' => "avatar-privacy-{$id}", // The item ID of what we're exporting.
+ 'data' => $data, // The personal data that should be exported.
+ ],
+ ],
+ 'done' => true,
+ ];
+ }
+
+ /**
+ * Erases the data collected by this plugin.
+ *
+ * @since 2.5.0 Unused parameter $page removed.
+ *
+ * @param string $email The email address.
+ *
+ * @return array {
+ * @type int $items_removed The number of removed items.
+ * @type int $items_retained The number of items that were retained and anonymized.
+ * @type array $messages Any additional information for the admin associated with the removal request.
+ * @type bool $done True if there is no more data to erase, false otherwise.
+ * }
+ *
+ * @phpstan-return PrivacyEraseResult
+ */
+ public function erase_data( $email ) {
+ $items_removed = 0;
+ $items_retained = 0; // We currently don't track this information.
+ $messages = [];
+
+ // Remove user data.
+ $user = \get_user_by( 'email', $email );
+ if ( ! empty( $user ) ) {
+ $items_removed += $this->registered_user->delete( $user->ID );
+ }
+
+ // Remove comment author data.
+ $items_removed += $this->comment_author->delete( $email );
+
+ return [
+ 'items_removed' => $items_removed,
+ 'items_retained' => $items_retained,
+ 'messages' => $messages,
+ 'done' => true,
+ ];
+ }
+}
diff --git a/includes/avatar-privacy/components/class-settings-page.php b/includes/avatar-privacy/components/class-settings-page.php
new file mode 100644
index 0000000..18144f9
--- /dev/null
+++ b/includes/avatar-privacy/components/class-settings-page.php
@@ -0,0 +1,229 @@
+
+ *
+ * @phpstan-import-type SettingsFields from Settings
+ */
+class Settings_Page implements Component {
+
+ /**
+ * The options handler.
+ *
+ * @var Options
+ */
+ private Options $options;
+
+ /**
+ * The file upload handler.
+ *
+ * @var Upload
+ */
+ private Upload $upload;
+
+ /**
+ * The settings API.
+ *
+ * @var Settings
+ */
+ private Settings $settings;
+
+ /**
+ * The templating handler.
+ *
+ * @since 2.4.0
+ *
+ * @var Template
+ */
+ private Template $template;
+
+ /**
+ * Indiciates whether the settings page is buffering its output.
+ *
+ * @var bool
+ */
+ private bool $buffering;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.1.0 Parameter $plugin_file removed.
+ * @since 2.4.0 Paraemter $core removed, parameter $template added.
+ *
+ * @param Options $options The options handler.
+ * @param Upload $upload The file upload handler.
+ * @param Settings $settings The settings API.
+ * @param Template $template The templating handler.
+ */
+ public function __construct( Options $options, Upload $upload, Settings $settings, Template $template ) {
+ $this->options = $options;
+ $this->upload = $upload;
+ $this->settings = $settings;
+ $this->template = $template;
+ $this->buffering = false;
+ }
+
+ /**
+ * Sets up the various hooks for the plugin component.
+ *
+ * @return void
+ */
+ public function run() {
+ if ( \is_admin() ) {
+ // Register scripts.
+ \add_action( 'admin_init', [ $this, 'register_settings' ] );
+
+ // Add form encoding.
+ \add_action( 'admin_head-options-discussion.php', [ $this, 'settings_head' ] );
+
+ // Print scripts.
+ \add_action( 'admin_footer-options-discussion.php', [ $this, 'settings_footer' ] );
+ }
+ }
+
+ /**
+ * Run tasks in the settings header.
+ *
+ * @return void
+ */
+ public function settings_head() {
+ if ( \ob_start( [ $this, 'add_form_encoding' ] ) ) {
+ $this->buffering = true;
+ }
+ }
+
+ /**
+ * Run tasks in the settings footer.
+ *
+ * @return void
+ */
+ public function settings_footer() {
+ // Add show/hide javascript.
+ if ( \wp_script_is( 'jquery', 'done' ) ) {
+ $this->template->print_partial( 'admin/partials/sections/avatars-disabled-script.php' );
+ }
+
+ // Clean up output buffering.
+ if ( $this->buffering && \ob_get_level() > 0 ) {
+ \ob_end_flush();
+ $this->buffering = false;
+ }
+ }
+
+ /**
+ * Registers the settings with the settings API. This is only used to display
+ * an explanation of the wrong gravatar settings.
+ *
+ * @return void
+ */
+ public function register_settings() {
+ \register_setting( 'discussion', $this->options->get_name( Settings::OPTION_NAME ), [ $this, 'sanitize_settings' ] );
+
+ // Register control render callbacks.
+ $controls = Control_Factory::initialize( $this->settings->get_fields( $this->get_settings_header() ), $this->options, Settings::OPTION_NAME );
+ foreach ( $controls as $control ) {
+ $control->register( 'discussion' );
+ }
+ }
+
+ /**
+ * Adds the enctype "multipart/form-data" to the form tag.
+ *
+ * @param string $content The captured HTML output.
+ *
+ * @return string
+ */
+ public function add_form_encoding( $content ) {
+ return (string) \preg_replace( '#(
instead of in the
. Default `false`.
+ *
+ * @return bool Whether the script has been registered. True
+ * on success, false on failure.
+ */
+ public function register_script( $handle, $src, $deps = [], $version = false, $in_footer = false ) {
+ // Use minified versions where appropriate.
+ $src = $this->maybe_add_minification_suffix( $src );
+
+ // Use file modification time as version.
+ $version = $this->maybe_add_file_modification_version( $version, $src );
+
+ // Register script.
+ return \wp_register_script( $handle, "{$this->url}/{$src}", $deps, $version, $in_footer );
+ }
+
+ /**
+ * Registers a CSS stylesheet.
+ *
+ * @param string $handle The name of the script. Should be unique.
+ * @param string $src The path of the script relative to the plugin's
+ * directory.
+ * @param string[] $deps Optional. An array of registered script
+ * handles this script depends on. Default [].
+ * @param string|bool $version Optional. A string specifying the script version
+ * number, if it has one, which is added to the
+ * URL as a query string for cache busting purposes.
+ * If $version is set to false, the style file's
+ * modification time is used automatically. If
+ * set to null, no version is added.
+ * @param string $media Optional. The media for which this stylesheet
+ * has been defined. Accepts media types like 'all',
+ * 'print' and 'screen', or media queries like
+ * '(orientation: portrait)' and '(max-width: 640px)'.
+ * Default 'all'.
+ *
+ * @return bool xxx
+ */
+ public function register_style( $handle, $src, $deps = [], $version = false, $media = 'all' ) {
+ // Use minified versions where appropriate.
+ $src = $this->maybe_add_minification_suffix( $src );
+
+ // Use file modification time as version.
+ $version = $this->maybe_add_file_modification_version( $version, $src );
+
+ // Register style.
+ return \wp_register_style( $handle, "{$this->url}/{$src}", $deps, $version, $media );
+ }
+
+ /**
+ * Enqueues the script specified by the handle.
+ *
+ * @param string $handle A registered script handle.
+ *
+ * @return void
+ */
+ public function enqueue_script( $handle ) {
+ $key = \preg_replace( [ '/^avatar-privacy-/', '/-/' ], [ '', '_' ], $handle );
+
+ /**
+ * Filters whether to enqueue the script.
+ *
+ * The $key part will be the script handle without the plugin prefix
+ * and with all '-' replaced with '_'.
+ *
+ * @since 2.4.0
+ *
+ * @param bool $allow Optional. Default true.
+ */
+ if ( \apply_filters( "avatar_privacy_enqueue_script_{$key}", true ) ) {
+ \wp_enqueue_script( $handle );
+ }
+ }
+
+ /**
+ * Enqueues the stylesheet specified by the handle.
+ *
+ * @param string $handle A registered stylesheet handle.
+ *
+ * @return void
+ */
+ public function enqueue_style( $handle ) {
+ $key = \preg_replace( [ '/^avatar-privacy-/', '/-style$/', '/-/' ], [ '', '', '_' ], $handle );
+
+ /**
+ * Filters whether to enqueue the stylesheet.
+ *
+ * The $key part will be the stylesheet handle without the plugin prefix
+ * or '-style' suffix and with all '-' replaced with '_'.
+ *
+ * @since 2.4.0
+ *
+ * @param bool $allow Optional. Default true.
+ */
+ if ( \apply_filters( "avatar_privacy_enqueue_style_{$key}", true ) ) {
+ \wp_enqueue_style( $handle );
+ }
+ }
+
+ /**
+ * Adds the minification suffix to a file path (if appropriate).
+ *
+ * @param string $src The path of a file relative to the plugin's directory.
+ *
+ * @return string
+ */
+ protected function maybe_add_minification_suffix( $src ) {
+ if ( ! empty( $this->suffix ) && ! empty( $src ) ) {
+ $i = \pathinfo( $src );
+ $ext = ! empty( $i['extension'] ) ? ".{$i['extension']}" : '';
+ $src = "{$i['dirname']}/{$i['filename']}{$this->suffix}{$ext}";
+ }
+
+ return $src;
+ }
+
+ /**
+ * Adds the file modification time as a version number if necessary.
+ *
+ * @param string|bool|null $version A string specifying a version number If
+ * version is set to false, the file's
+ * modification time is used automatically.
+ * If set to null, no version is added.
+ * @param string $src The path of a file relative to the plugin's
+ * directory.
+ *
+ * @return string|bool|null
+ */
+ protected function maybe_add_file_modification_version( $version, $src ) {
+ $full_src_path = "{$this->path}/{$src}";
+
+ if ( false === $version && ! empty( $src ) && \file_exists( $full_src_path ) ) {
+ $version = (string) @\filemtime( $full_src_path );
+ $version = empty( $version ) ? false : $version;
+ }
+
+ return $version;
+ }
+}
diff --git a/includes/avatar-privacy/tools/html/class-user-form.php b/includes/avatar-privacy/tools/html/class-user-form.php
new file mode 100644
index 0000000..6a80dbf
--- /dev/null
+++ b/includes/avatar-privacy/tools/html/class-user-form.php
@@ -0,0 +1,574 @@
+
+ *
+ * @phpstan-import-type PartialArguments from Template
+ *
+ * @phpstan-type ConfigData array{ nonce: string, action: string, field: string, erase?: string, partial: string }
+ * @phpstan-type AdditionArguments array{ avatar_size?: int, show_description?: bool }
+ */
+class User_Form {
+
+ /**
+ * The upload handler.
+ *
+ * @var Upload
+ */
+ protected Upload $upload;
+
+ /**
+ * The user fields API.
+ *
+ * @since 2.4.0
+ *
+ * @var User_Fields
+ */
+ protected User_Fields $registered_user;
+
+ /**
+ * The templating handler.
+ *
+ * @since 2.4.0
+ *
+ * @var Template
+ */
+ private Template $template;
+
+ /**
+ * The configuration data for the `use_gravatar` checkbox.
+ *
+ * @var array {
+ * @type string $nonce The nonce root (the ID of the user in question
+ * will be automatically added).
+ * @type string $action The nonce action.
+ * @type string $field The ID/name of the actual ` ` element.
+ * @type string $partial The path to the partial template file (relative
+ * to the plugin path).
+ * }
+ *
+ * @phpstan-var ConfigData
+ */
+ protected array $use_gravatar;
+
+ /**
+ * The configuration data for the `allow_anonymous` checkbox.
+ *
+ * @var array {
+ * @type string $nonce The nonce root (the ID of the user in question
+ * will be automatically added).
+ * @type string $action The nonce action.
+ * @type string $field The ID/name of the actual ` ` element.
+ * @type string $partial The path to the partial template file (relative
+ * to the plugin path).
+ * }
+ *
+ * @phpstan-var ConfigData
+ */
+ protected array $allow_anonymous;
+
+ /**
+ * The configuration data for the user avatar uploader.
+ *
+ * @var array {
+ * @type string $nonce The nonce root (the ID of the user in question
+ * will be automatically added).
+ * @type string $action The nonce action.
+ * @type string $field The ID/name of the upload ` ` element.
+ * @type string $erase The ID/name of the erase checkbox ` ` element.
+ * @type string $partial The path to the partial template file (relative
+ * to the plugin path).
+ * }
+ *
+ * @phpstan-var ConfigData&array{ erase: string }
+ */
+ protected array $user_avatar;
+
+ /**
+ * Creates a new form helper instance.
+ *
+ * @since 2.4.0 Parameters $registered_user, and $template added.
+ *
+ * @param Upload $upload The upload handler.
+ * @param User_Fields $registered_user The user fields API handler.
+ * @param Template $template The templating handler.
+ * @param array $use_gravatar {
+ * The configuration data for the `use_gravatar` checkbox.
+ *
+ * @type string $nonce The nonce root (the ID of the user in question
+ * will be automatically added).
+ * @type string $action The nonce action.
+ * @type string $field The ID/name of the actual ` ` element.
+ * @type string $partial The path to the partial template file (relative
+ * to the plugin path).
+ * }
+ * @param array $allow_anonymous {
+ * The configuration data for the `allow_anonymous` checkbox.
+ *
+ * @type string $nonce The nonce root (the ID of the user in question
+ * will be automatically added).
+ * @type string $action The nonce action.
+ * @type string $field The ID/name of the actual ` ` element.
+ * @type string $partial The path to the partial template file (relative
+ * to the plugin path).
+ * }
+ * @param array $user_avatar {
+ * The configuration data for the user avatar uploader.
+ *
+ * @type string $nonce The nonce root (the ID of the user in question
+ * will be automatically added).
+ * @type string $action The nonce action.
+ * @type string $field The ID/name of the upload ` ` element.
+ * @type string $erase The ID/name of the erase checkbox ` ` element.
+ * @type string $partial The path to the partial template file (relative
+ * to the plugin path).
+ * }
+ *
+ * @phpstan-param ConfigData $use_gravatar
+ * @phpstan-param ConfigData $allow_anonymous
+ * @phpstan-param ConfigData&array{ erase: string } $user_avatar
+ */
+ public function __construct( Upload $upload, User_Fields $registered_user, Template $template, array $use_gravatar, array $allow_anonymous, array $user_avatar ) {
+ $this->upload = $upload;
+ $this->registered_user = $registered_user;
+ $this->template = $template;
+ $this->use_gravatar = $use_gravatar;
+ $this->allow_anonymous = $allow_anonymous;
+ $this->user_avatar = $user_avatar;
+ }
+
+ /**
+ * Prints the markup for the `use_gravatar` checkbox.
+ *
+ * @param int $user_id The ID of the user to edit.
+ * @param array $args {
+ * Additional arguments for the template.
+ *
+ * @type bool $show_description True if the long description should be displayed. Default true.
+ * }
+ *
+ * @return void
+ *
+ * @phpstan-param AdditionArguments $args
+ */
+ public function use_gravatar_checkbox( $user_id, array $args = [] ) {
+ $this->checkbox( $this->registered_user->allows_gravatar_use( $user_id ), "{$this->use_gravatar['nonce']}{$user_id}", $this->use_gravatar['action'], $this->use_gravatar['field'], $this->use_gravatar['partial'], $args );
+ }
+
+ /**
+ * Retrieves the markup for the `use_gravatar` checkbox.
+ *
+ * @param int $user_id The ID of the user to edit.
+ * @param array $args {
+ * Additional arguments for the template.
+ *
+ * @type bool $show_descriptions True if the long description should be displayed. Default true.
+ * }
+ *
+ * @return string
+ *
+ * @phpstan-param AdditionArguments $args
+ */
+ public function get_use_gravatar_checkbox( $user_id, array $args = [] ) {
+ \ob_start();
+ $this->use_gravatar_checkbox( $user_id, $args );
+ return (string) \ob_get_clean();
+ }
+
+ /**
+ * Prints the markup for the `allow_anonymous` checkbox.
+ *
+ * @param int $user_id The ID of the user to edit.
+ * @param array $args {
+ * Additional arguments for the template.
+ *
+ * @type bool $show_descriptions True if the long description should be displayed. Default true.
+ * }
+ *
+ * @return void
+ *
+ * @phpstan-param AdditionArguments $args
+ */
+ public function allow_anonymous_checkbox( $user_id, array $args = [] ) {
+ $this->checkbox( $this->registered_user->allows_anonymous_commenting( $user_id ), "{$this->allow_anonymous['nonce']}{$user_id}", $this->allow_anonymous['action'], $this->allow_anonymous['field'], $this->allow_anonymous['partial'], $args );
+ }
+
+ /**
+ * Retrieves the markup for the `allow_anonymous` checkbox.
+ *
+ * @param int $user_id The ID of the user to edit.
+ * @param array $args {
+ * Additional arguments for the template.
+ *
+ * @type bool $show_descriptions True if the long description should be displayed. Default true.
+ * }
+ *
+ * @return string
+ *
+ * @phpstan-param AdditionArguments $args
+ */
+ public function get_allow_anonymous_checkbox( $user_id, array $args = [] ) {
+ \ob_start();
+ $this->allow_anonymous_checkbox( $user_id, $args );
+ return (string) \ob_get_clean();
+ }
+
+ /**
+ * Prints the markup for uploading user avatars.
+ *
+ * @param int $user_id The ID of the user to edit.
+ * @param array $args {
+ * Additional arguments for the template.
+ *
+ * @type int $avatar_size The width/height of the avatar preview image (in pixels). Default 96.
+ * @type bool $show_descriptions True if the long description should be displayed. Default true.
+ * }
+ *
+ * @return void
+ *
+ * @phpstan-param AdditionArguments $args
+ */
+ public function avatar_uploader( $user_id, array $args = [] ) {
+ // Merge default arguments.
+ $args = \wp_parse_args( $args, [
+ 'avatar_size' => 96,
+ 'show_descriptions' => true,
+ ] );
+
+ /**
+ * Filters whether native profile picture uploading is disabled for some
+ * reasone (e.g. because another plugin already provides for that).
+ *
+ * @since 2.3.0
+ *
+ * @param bool $disabled Default false.
+ */
+ $uploads_disabled = \apply_filters( 'avatar_privacy_profile_picture_upload_disabled', false );
+
+ // Set up variables used by the included partial.
+ $partial_args = [
+ 'user_id' => $user_id,
+ 'template' => $this->template,
+ 'nonce' => "{$this->user_avatar['nonce']}{$user_id}",
+ 'action' => $this->user_avatar['action'],
+ 'upload_field' => $this->user_avatar['field'],
+ 'erase_field' => $this->user_avatar['erase'],
+ 'uploads_disabled' => $uploads_disabled,
+ 'can_upload' => empty( $uploads_disabled ) && \current_user_can( 'upload_files' ),
+ 'has_local_avatar' => ! empty( $this->registered_user->get_local_avatar( $user_id ) ),
+ 'size' => $args['avatar_size'],
+ 'show_description' => $args['show_descriptions'],
+ ];
+
+ // Include partial.
+ $this->template->print_partial( $this->user_avatar['partial'], $partial_args );
+ }
+
+ /**
+ * Retrieves the markup for uploading user avatars.
+ *
+ * @param int $user_id The ID of the user to edit.
+ * @param array $args {
+ * Additional arguments for the template.
+ *
+ * @type int $avatar_size The width/height of the avatar preview image (in pixels). Default 96.
+ * @type bool $show_descriptions True if the long description should be displayed. Default true.
+ * }
+ *
+ * @return string
+ *
+ * @phpstan-param AdditionArguments $args
+ */
+ public function get_avatar_uploader( $user_id, array $args = [] ) {
+ \ob_start();
+ $this->avatar_uploader( $user_id, $args );
+ return (string) \ob_get_clean();
+ }
+
+ /**
+ * Prints checkbox markup, initialized from the given user meta key.
+ *
+ * @since 2.4.0 Parameters $user_id, and $meta_key removed, parameter $value
+ * added. The parameter $nonce now has to include the variable
+ * part (user ID).
+ *
+ * @param bool $value Whether the checkbox should be checked, or not.
+ * @param string $nonce The nonce root required for saving the field.
+ * @param string $action The action required for saving the field.
+ * @param string $field_name The HTML name of the checkbox field.
+ * @param string $partial The relative path to the partial to load.
+ * @param array $args {
+ * Additional arguments for the template.
+ *
+ * @type bool $show_descriptions True if the long description should be displayed. Default true.
+ * }
+ *
+ * @return void
+ *
+ * @phpstan-param AdditionArguments $args
+ */
+ protected function checkbox( $value, $nonce, $action, $field_name, $partial, array $args = [] ) {
+ // Merge default arguments.
+ $args = \wp_parse_args( $args, [
+ 'show_descriptions' => true,
+ ] );
+
+ // Set up variables used by the included partial.
+ $partial_args = [
+ 'template' => $this->template,
+ 'nonce' => $nonce,
+ 'action' => $action,
+ 'field_name' => $field_name,
+ 'value' => $value,
+ 'show_description' => $args['show_descriptions'],
+ ];
+
+ // Include partial.
+ $this->template->print_partial( $partial, $partial_args );
+ }
+
+ /**
+ * Saves the value of the 'use_gravatar' checkbox from the user profile in
+ * the database.
+ *
+ * @param int $user_id The ID of the user that has just been saved.
+ *
+ * @return void
+ *
+ * @throws Invalid_Nonce_Exception An exception is thrown when the nonce cannot
+ * be verified.
+ */
+ public function save_use_gravatar_checkbox( $user_id ) {
+ try {
+ $this->registered_user->update_gravatar_use( $user_id, $this->get_submitted_checkbox_value( "{$this->use_gravatar['nonce']}{$user_id}", $this->use_gravatar['action'], $this->use_gravatar['field'] ) );
+ } catch ( Form_Field_Not_Found_Exception $e ) {
+ // No attempt to set the field (probably no form submitted).
+ return;
+ }
+ }
+
+ /**
+ * Saves the value of the 'allow_anonymous' checkbox from the user profile in
+ * the database.
+ *
+ * @param int $user_id The ID of the user that has just been saved.
+ *
+ * @return void
+ *
+ * @throws Invalid_Nonce_Exception An exception is thrown when the nonce cannot
+ * be verified.
+ */
+ public function save_allow_anonymous_checkbox( $user_id ) {
+ try {
+ $this->registered_user->update_anonymous_commenting( $user_id, $this->get_submitted_checkbox_value( "{$this->allow_anonymous['nonce']}{$user_id}", $this->allow_anonymous['action'], $this->allow_anonymous['field'] ) );
+ } catch ( Form_Field_Not_Found_Exception $e ) {
+ // No attempt to set the field (probably no form submitted).
+ return;
+ }
+ }
+
+ /**
+ * Retrieves the checkbox value to save.
+ *
+ * @since 2.4.0
+ *
+ * @global array $_POST Post request superglobal.
+ *
+ * @param string $nonce The nonce root required for saving the field
+ * (the user ID will be automatically appended).
+ * @param string $action The action required for saving the field.
+ * @param string $field_name The HTML name of the field to be saved.
+ *
+ * @return bool
+ *
+ * @throws Form_Field_Not_Found_Exception An exception is thrown when the field
+ * does not exist (i.e. no form was submitted).
+ * @throws Invalid_Nonce_Exception An exception is thrown when the nonce
+ * cannot be verified.
+ */
+ protected function get_submitted_checkbox_value( $nonce, $action, $field_name ) {
+ if ( ! isset( $_POST[ $nonce ] ) && ! isset( $_POST[ $field_name ] ) ) {
+ throw new Form_Field_Not_Found_Exception( "Form field '{$field_name}' not found." );
+ }
+
+ // @phpstan-ignore-next-line -- super globals are all array.
+ if ( isset( $_POST[ $nonce ] ) && \wp_verify_nonce( \sanitize_key( $_POST[ $nonce ] ), $action ) ) {
+ return isset( $_POST[ $field_name ] ) && 'true' === $_POST[ $field_name ];
+ }
+
+ throw new Invalid_Nonce_Exception( 'Could not verify checkbox nonce.' );
+ }
+
+ /**
+ * Saves the uploaded avatar image to the proper directory.
+ *
+ * @param int $user_id The user ID.
+ *
+ * @return void
+ */
+ public function save_uploaded_user_avatar( $user_id ) {
+ $this->upload->save_uploaded_user_avatar( $user_id, $this->user_avatar['nonce'], $this->user_avatar['action'], $this->user_avatar['field'], $this->user_avatar['erase'] );
+ }
+
+ /**
+ * Saves all the custom fields of the user form into the database/the
+ * the filesystem. (The data is ultimately taken from the $_POST and $_FILES
+ * superglobals.)
+ *
+ * If the current user lacks the capability to edit the profile of the given
+ * user ID, the data is not saved.
+ *
+ * @since 2.4.0 The method now throws an exception when a nonce cannot be
+ * verified.
+ *
+ * @param int $user_id The ID of the edited user.
+ *
+ * @return void
+ *
+ * @throws Invalid_Nonce_Exception An exception is thrown when a nonce
+ * cannot be verified.
+ */
+ public function save( $user_id ) {
+ if ( ! \current_user_can( 'edit_user', $user_id ) ) {
+ return;
+ }
+
+ $this->save_use_gravatar_checkbox( $user_id );
+ $this->save_allow_anonymous_checkbox( $user_id );
+ $this->save_uploaded_user_avatar( $user_id );
+ }
+
+ /**
+ * Processes a form submission. Currently, it is limited to "self-editing".
+ *
+ * This method should only be hooked for frontend forms (via the `init`
+ * action hook).
+ *
+ * @since 2.4.0 The method now throws an exception when a nonce cannot be
+ * verified.
+ *
+ * @return void
+ *
+ * @throws Invalid_Nonce_Exception An exception is thrown when a nonce
+ * cannot be verified.
+ */
+ public function process_form_submission() {
+ // Check that user is logged in.
+ $user_id = \get_current_user_id();
+ if ( empty( $user_id ) ) {
+ return;
+ }
+
+ // Process upload.
+ $this->save( $user_id );
+ }
+
+ /**
+ * Registers the `process_form_submission` method with the `init` hook, but
+ * makes sure not to do it twice.
+ *
+ * @return void
+ */
+ public function register_form_submission() {
+ if ( ! \has_action( 'init', [ $this, 'process_form_submission' ] ) ) {
+ \add_action( 'init', [ $this, 'process_form_submission' ] );
+ }
+ }
+
+ /**
+ * Prints the form for the specified user, using the given PHP template.
+ *
+ * @since 2.4.0
+ *
+ * @param string $partial The file path of the partial to include (relative
+ * to the plugin directory.
+ * @param int $user_id The ID of the edited user.
+ * @param array $args Arguments passed to the partial. Only string keys
+ * allowed and the keys must be valid variable names.
+ *
+ * @return void
+ *
+ * @phpstan-param PartialArguments $args
+ */
+ public function print_form( $partial, $user_id, array $args = [] ) {
+ $this->template->print_partial( $partial, $this->get_partial_arguments( $user_id, $args ) );
+ }
+
+ /**
+ * Retrieves the form markup for the specified user, using the given PHP template.
+ *
+ * @since 2.4.0
+ *
+ * @param string $partial The file path of the partial to include (relative
+ * to the plugin directory.
+ * @param int $user_id The ID of the edited user.
+ * @param array $args Arguments passed to the partial. Only string keys
+ * allowed and the keys must be valid variable names.
+ *
+ * @return string
+ *
+ * @phpstan-param PartialArguments $args
+ */
+ public function get_form( $partial, $user_id, array $args = [] ) {
+ return $this->template->get_partial( $partial, $this->get_partial_arguments( $user_id, $args ) );
+ }
+
+ /**
+ * Prepares the arguments array for passing to Template::get_partial and
+ * Template::print_partial.
+ *
+ * @since 2.4.0
+ *
+ * @param int $user_id The ID of the edited user.
+ * @param array $args Arguments passed to the partial. Only string keys
+ * allowed and the keys must be valid variable names.
+ *
+ * @return array
+ *
+ * @phpstan-param PartialArguments $args
+ * @phpstan-return PartialArguments
+ */
+ protected function get_partial_arguments( $user_id, array $args ) {
+ $args['form'] = $this;
+ $args['user_id'] = $user_id;
+
+ return $args;
+ }
+}
diff --git a/includes/avatar-privacy/tools/images/class-color.php b/includes/avatar-privacy/tools/images/class-color.php
new file mode 100644
index 0000000..74df4fd
--- /dev/null
+++ b/includes/avatar-privacy/tools/images/class-color.php
@@ -0,0 +1,135 @@
+
+ *
+ * @phpstan-type RGBValue int<0,255>
+ * @phpstan-type PercentValue int<0,100>
+ *
+ * @phpstan-type HueDegree int<-360,360>
+ * @phpstan-type NormalizedHue int<0,359>
+ */
+class Color {
+ const MAX_DEGREE = 360;
+ const MAX_RGB = 255;
+ const MAX_PERCENT = 100;
+
+ /**
+ * Converts a color specified using HSL to its RGB representation.
+ *
+ * @since 2.5.0
+ * @since 2.7.0 Moved to Color class.
+ *
+ * @param int $hue The hue (in degrees, i.e. -360°–+360°).
+ * @param int $saturation The saturation (in percent, i.e. 0–100%).
+ * @param int $lightness The lightness (in percent, i.e. 0–100%).
+ *
+ * @return int[] {
+ * The RGB color as a tuple.
+ *
+ * @type int $red The red component (0–255).
+ * @type int $green The green component (0–255).
+ * @type int $blue The blue component (0–255).
+ * }
+ *
+ * @phpstan-param NormalizedHue $hue
+ * @phpstan-param PercentValue $saturation
+ * @phpstan-param PercentValue $lightness
+ * @phpstan-return array{ 0: RGBValue, 1: RGBValue, 2: RGBValue }
+ */
+ public function hsl_to_rgb( $hue, $saturation, $lightness ) {
+ /**
+ * Convert saturation to decimal notation.
+ *
+ * @var float
+ */
+ $saturation = $saturation / self::MAX_PERCENT;
+
+ /**
+ * Convert lightness to decimal notation.
+ *
+ * @var float
+ */
+ $lightness = $lightness / self::MAX_PERCENT;
+
+ /**
+ * Conversion function.
+ *
+ * @param int $n Conversion factor.
+ *
+ * @return float A floating point number between 0.0 and 1.0.
+ */
+ $f = function( $n ) use ( $hue, $saturation, $lightness ) {
+ $k = \fmod( $n + $hue / 30, 12 );
+ $a = $saturation * \min( $lightness, 1 - $lightness );
+ return $lightness - $a * \max( -1, \min( $k - 3, 9 - $k, 1 ) );
+ };
+
+ /**
+ * The red component.
+ *
+ * @phpstan-var RGBValue
+ */
+ $red = (int) \round( $f( 0 ) * self::MAX_RGB );
+ /**
+ * The green component.
+ *
+ * @phpstan-var RGBValue
+ */
+ $green = (int) \round( $f( 8 ) * self::MAX_RGB );
+ /**
+ * The blue component.
+ *
+ * @phpstan-var RGBValue
+ */
+ $blue = (int) \round( $f( 4 ) * self::MAX_RGB );
+
+ // Return result array.
+ return [ $red, $green, $blue ];
+ }
+
+ /**
+ * Normalizes the hue value.
+ *
+ * @param int $hue The hue as a positive or negative arc on the color wheel (-360°–+360°).
+ *
+ * @return int The normalized hue (0–359°).
+ *
+ * @phpstan-param HueDegree $hue
+ * @phpstan-return NormalizedHue
+ */
+ public function normalize_hue( int $hue ) {
+ // Ensure a unique, non-negative hue.
+ return ( $hue < 0 ? self::MAX_DEGREE + $hue : $hue ) % self::MAX_DEGREE;
+ }
+}
diff --git a/includes/avatar-privacy/tools/images/class-editor.php b/includes/avatar-privacy/tools/images/class-editor.php
new file mode 100644
index 0000000..dd8db40
--- /dev/null
+++ b/includes/avatar-privacy/tools/images/class-editor.php
@@ -0,0 +1,350 @@
+
+ */
+class Editor {
+
+ const DEFAULT_STREAM = 'avprimg://image_editor/dummy/path';
+
+ /**
+ * Allowed image formats for exporting.
+ *
+ * @internal
+ *
+ * @since 2.4.0
+ *
+ * @var array
+ */
+ const ALLOWED_IMAGE_FORMATS = [
+ Image_File::JPEG_IMAGE => true,
+ Image_File::PNG_IMAGE => true,
+ ];
+
+ /**
+ * A stream wrapper URL.
+ *
+ * @since 2.1.0
+ *
+ * @var string
+ */
+ private string $stream_url;
+
+ /**
+ * The stream implmentation to use.
+ *
+ * @since 2.1.0
+ *
+ * @var string
+ */
+ private string $stream_class;
+
+ /**
+ * The handle (hostname/path) parsed from the stream URL.
+ *
+ * @var string
+ */
+ private string $handle;
+
+ /**
+ * Creates a new image editor helper.
+ *
+ * @since 2.1.0
+ * @since 2.4.0 An exception is thrown when an invalid URL is passed to the method.
+ *
+ * @param string $url Optional. The stream URL to be used for in-memory-images. Default self::DEFAULT_STREAM.
+ * @param string $stream_class Optional. The stream wrapper class that should be used. Default Image_Stream.
+ *
+ * @throws \InvalidArgumentException Throws an exception if the default stream URL is not valid.
+ */
+ public function __construct( $url = self::DEFAULT_STREAM, $stream_class = Image_Stream::class ) {
+ $this->stream_url = $url;
+ $this->stream_class = $stream_class;
+
+ // Determine stream URL scheme.
+ $scheme = \parse_url( $url, \PHP_URL_SCHEME ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
+ if ( empty( $scheme ) ) {
+ throw new \InvalidArgumentException( "{$url} is not a valid stream URL" );
+ }
+
+ // Also save the memory handle.
+ $this->handle = $stream_class::get_handle_from_url( $url );
+
+ $stream_class::register( $scheme );
+ }
+
+ /**
+ * Creates a \WP_Image_Editor from a given stream wrapper.
+ *
+ * @param string $stream The image stream wrapper URL.
+ *
+ * @return \WP_Image_Editor|\WP_Error
+ */
+ public function create_from_stream( $stream ) {
+ // Create image editor instance from stream, but strongly prefer GD implementations.
+ $result = $this->get_image_editor( $stream );
+
+ // Clean up stream data.
+ $this->delete_stream( $stream );
+
+ // Return image editor.
+ return $result;
+ }
+
+ /**
+ * Creates a \WP_Image_Editor from in-memory image data.
+ *
+ * @param string $data Image data.
+ *
+ * @return \WP_Image_Editor|\WP_Error
+ */
+ public function create_from_string( $data ) {
+ // Copy data to stream implementation.
+ \file_put_contents( $this->stream_url, $data ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
+
+ return $this->create_from_stream( $this->stream_url );
+ }
+
+ /**
+ * Creates a \WP_Image_Editor from a GD image. The image is destroyed on success.
+ *
+ * @since 2.5.0 Parameter $image can now also be a GdImage.
+ *
+ * @param resource|GdImage $image Image data.
+ *
+ * @return \WP_Image_Editor|\WP_Error
+ */
+ public function create_from_image_resource( $image ) {
+ if ( \is_gd_image( $image ) && \imagePNG( $image, $this->stream_url ) ) {
+ // Clean up resource.
+ \imageDestroy( $image );
+
+ // Create editor.
+ return $this->create_from_stream( $this->stream_url );
+ }
+
+ return new \WP_Error( 'invalid_image', \__( 'Resource is not an image.', 'avatar-privacy' ) );
+ }
+
+ /**
+ * Retrieves the image data from the given editor object.
+ *
+ * @param \WP_Image_Editor|\WP_Error $image The image.
+ * @param string $format Optional. The image mimetype. Default 'image/png'.
+ *
+ * @return string
+ */
+ public function get_image_data( $image, $format = Image_File::PNG_IMAGE ) {
+ // Check for validity.
+ if (
+ $image instanceof \WP_Error ||
+ ! isset( self::ALLOWED_IMAGE_FORMATS[ $format ] ) ||
+ ! isset( Image_File::FILE_EXTENSION[ $format ] )
+ ) {
+ return '';
+ }
+
+ // Convert the image the given format and extract data.
+ $extension = Image_File::FILE_EXTENSION[ $format ];
+ if ( $image->save( "{$this->stream_url}.{$extension}", $format ) instanceof \WP_Error ) {
+ return '';
+ }
+
+ // Read the data from memory stream and clean up.
+ return $this->stream_class::get_data( "{$this->handle}.{$extension}", true );
+ }
+
+ /**
+ * Resizes the given image and returns the image data. If the aspect ratios
+ * differ, the image is center-cropped.
+ *
+ * @since 2.0.5 Parameter $crop has been deprecated.
+ * @since 2.1.0 Parameter $crop has been removed.
+ * @since 2.4.0 Image is cropped if the target aspect ratio differs from the
+ * original one.
+ *
+ * @param \WP_Image_Editor|\WP_Error $image The image.
+ * @param int $width The width in pixels.
+ * @param int $height The height in pixels.
+ * @param string $format Optional. The image mimetype. Default 'image/png'.
+ *
+ * @return string
+ */
+ public function get_resized_image_data( $image, $width, $height, $format = Image_File::PNG_IMAGE ) {
+
+ // Try to resize only if we haven't been handed an error object.
+ if ( $image instanceof \WP_Error ) {
+ return '';
+ }
+
+ // Caculate the crop dimensions.
+ $current = $image->get_size();
+ $crop = $this->get_crop_dimensions( $current['width'], $current['height'], $width, $height );
+
+ // We need to use the `crop` method because `resize` includes a block against enlarging images.
+ if ( $image->crop( $crop['x'], $crop['y'], $crop['width'], $crop['height'], $width, $height, false ) instanceof \WP_Error ) {
+ $result = '';
+ } else {
+ $result = $this->get_image_data( $image, $format );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Determines the necessary crop dimensions for moving from the original image
+ * to the destination image.
+ *
+ * @since 2.4.0
+ *
+ * @param int $orig_w Original image width.
+ * @param int $orig_h Original image height.
+ * @param int $dest_w Destination image width.
+ * @param int $dest_h Destination image height.
+ *
+ * @return array {
+ * The crop dimensions and coordinates.
+ *
+ * @type int $x The X coordinate for the crop.
+ * @type int $y The Y coordinate for the crop.
+ * @type int $width The width of the crop.
+ * @type int $height The height of the crop.
+ * }
+ *
+ * @phpstan-return array{ x: int, y: int, width: int, height: int }
+ */
+ protected function get_crop_dimensions( $orig_w, $orig_h, $dest_w, $dest_h ) {
+ // We crop to the largest rectangle fitting inside the original image
+ // with the same aspect ratio as the destination image.
+ $factor = \min( $orig_w / $dest_w, $orig_h / $dest_h );
+
+ // Caclulate the crop dimensions.
+ $crop_w = \round( $dest_w * $factor );
+ $crop_h = \round( $dest_h * $factor );
+
+ // Center the crop.
+ $x = \floor( ( $orig_w - $crop_w ) / 2 );
+ $y = \floor( ( $orig_h - $crop_h ) / 2 );
+
+ return [
+ 'x' => (int) $x,
+ 'y' => (int) $y,
+ 'width' => (int) $crop_w,
+ 'height' => (int) $crop_h,
+ ];
+ }
+
+ /**
+ * Returns a `\WP_Image_Editor` instance and loads file into it. Preference is
+ * given to stream-capable editor classes (i.e. GD-based ones).
+ *
+ * @param string $path Path to the file to load.
+ * @param mixed[] $args Optional. Additional arguments for retrieving the image editor. Default [].
+ *
+ * @return \WP_Image_Editor|\WP_Error
+ */
+ public function get_image_editor( $path, array $args = [] ) {
+ // Create image editor instance from path, but strongly prefer GD implementations.
+ \add_filter( 'wp_image_editors', [ $this, 'prefer_gd_image_editor' ], 9999 );
+ $result = \wp_get_image_editor( $path, $args );
+ \remove_filter( 'wp_image_editors', [ $this, 'prefer_gd_image_editor' ], 9999 );
+
+ return $result;
+ }
+
+ /**
+ * Moves Imagick-based image editors to the end of the queue.
+ *
+ * @param string[] $editors A list of image editor implementation class names.
+ *
+ * @return string[]
+ */
+ public function prefer_gd_image_editor( array $editors ) {
+ $preferred_editors = [];
+ $imagick_editors = [];
+ foreach ( $editors as $editor ) {
+ if ( \preg_match( '/imagick/i', $editor ) ) {
+ $imagick_editors[] = $editor;
+ } else {
+ $preferred_editors[] = $editor;
+ }
+ }
+
+ return \array_merge( $preferred_editors, $imagick_editors );
+ }
+
+ /**
+ * Retrieves the real MIME type of an image.
+ *
+ * @since 2.3.0
+ *
+ * @param string $data Image data.
+ *
+ * @return string|false The actual MIME type or false if the type cannot be determined.
+ */
+ public function get_mime_type( $data ) {
+ // Use custom handle.
+ $stream = $this->stream_url . '/mime/type/check';
+
+ // Copy data to stream implementation.
+ \file_put_contents( $stream, $data ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
+
+ // Retrieve MIME type.
+ $mime = \wp_get_image_mime( $stream );
+
+ // Clean up.
+ $this->delete_stream( $stream );
+
+ // Return the MIME type.
+ return $mime;
+ }
+
+ /**
+ * Deletes the handle/data for the given stream URL.
+ *
+ * @since 2.3.0
+ *
+ * @param string $stream The image stream wrapper URL.
+ *
+ * @return void
+ */
+ protected function delete_stream( $stream ) {
+ $this->stream_class::delete_handle( $this->stream_class::get_handle_from_url( $stream ) );
+ }
+}
diff --git a/includes/avatar-privacy/tools/images/class-image-file.php b/includes/avatar-privacy/tools/images/class-image-file.php
new file mode 100644
index 0000000..b7e026a
--- /dev/null
+++ b/includes/avatar-privacy/tools/images/class-image-file.php
@@ -0,0 +1,350 @@
+
+ *
+ * @phpstan-type HandleUploadOverrides array{ upload_dir: string, upload_error_handler?: callable, unique_filename_callback?: callable, upload_error_strings?: string[], test_form?: bool, test_size?: bool, test_type?: bool, mimes?: string[]}
+ * @phpstan-type HandleUploadSuccess array{ file: string, url: string, type: string }
+ * @phpstan-type HandleUploadError array{ error: string }
+ * @phpstan-type FileSlice array{ name: string, type: string, tmp_name: string, error: int|string, size: int }
+ */
+class Image_File {
+ const JPEG_IMAGE = 'image/jpeg';
+ const PNG_IMAGE = 'image/png';
+ const GIF_IMAGE = 'image/gif';
+ const SVG_IMAGE = 'image/svg+xml';
+
+ const JPEG_EXTENSION = 'jpg';
+ const JPEG_ALT_EXTENSION = 'jpeg';
+ const PNG_EXTENSION = 'png';
+ const GIF_EXTENSION = 'gif';
+ const SVG_EXTENSION = 'svg';
+
+ const CONTENT_TYPE = [
+ self::JPEG_EXTENSION => self::JPEG_IMAGE,
+ self::JPEG_ALT_EXTENSION => self::JPEG_IMAGE,
+ self::PNG_EXTENSION => self::PNG_IMAGE,
+ self::SVG_EXTENSION => self::SVG_IMAGE,
+ ];
+
+ const FILE_EXTENSION = [
+ self::JPEG_IMAGE => self::JPEG_EXTENSION,
+ self::PNG_IMAGE => self::PNG_EXTENSION,
+ self::GIF_IMAGE => self::GIF_EXTENSION,
+ self::SVG_IMAGE => self::SVG_EXTENSION,
+ ];
+
+ const ALLOWED_UPLOAD_MIME_TYPES = [
+ 'jpg|jpeg|jpe' => self::JPEG_IMAGE,
+ 'gif' => self::GIF_IMAGE,
+ 'png' => self::PNG_IMAGE,
+ ];
+
+ /**
+ * Handles the file upload by optionally switching to the primary site of the network.
+ *
+ * @since 4.4.0 Default value for parameter `$overrides` removed (as it would be invalid).
+ *
+ * @param array $file A slice of the $_FILES superglobal.
+ * @param array $overrides An associative array of names => values to override
+ * default variables. See `wp_handle_uploads` documentation
+ * for the full list of available overrides.
+ *
+ * @return string[] Information about the uploaded file.
+ *
+ * @phpstan-param FileSlice $file
+ * @phpstan-param HandleUploadOverrides $overrides
+ * @phpstan-return HandleUploadSuccess|HandleUploadError
+ */
+ public function handle_upload( array $file, array $overrides ) {
+ // Enable front end support.
+ if ( ! function_exists( 'wp_handle_upload' ) ) {
+ require_once \ABSPATH . 'wp-admin/includes/file.php'; // @codeCoverageIgnore
+ }
+
+ // Switch to primary site if this should be a global upload.
+ $use_global_upload_dir = $this->is_global_upload( $overrides );
+ if ( $use_global_upload_dir ) {
+ \switch_to_blog( \get_main_site_id() );
+ }
+
+ // Ensure custom upload directory.
+ $upload_dir = $overrides['upload_dir'];
+ $upload_dir_filter = function( array $uploads ) use ( $upload_dir ) {
+ // @codeCoverageIgnoreStart
+ $uploads['path'] = \str_replace( $uploads['subdir'], $upload_dir, $uploads['path'] );
+ $uploads['url'] = \str_replace( $uploads['subdir'], $upload_dir, $uploads['url'] );
+ $uploads['subdir'] = $upload_dir;
+
+ return $uploads;
+ // @codeCoverageIgnoreEnd
+ };
+
+ \add_filter( 'upload_dir', $upload_dir_filter );
+
+ // Move uploaded file.
+
+ /**
+ * Check if the image size falls within the allowed minimum and maximum dimensions.
+ *
+ * @phpstan-var FileSlice
+ */
+ $file = $this->validate_image_size( $file );
+
+ /**
+ * Let WordPress handle the upload natively.
+ *
+ * @phpstan-var HandleUploadSuccess|HandleUploadError
+ */
+ $result = \wp_handle_upload( $file, $this->prepare_overrides( $overrides ) );
+
+ // Restore standard upload directory.
+ \remove_filter( 'upload_dir', $upload_dir_filter );
+
+ // Ensure normalized path on Windows.
+ if ( ! empty( $result['file'] ) ) {
+ $result['file'] = \wp_normalize_path( $result['file'] );
+ }
+
+ // Switch back to current site.
+ if ( $use_global_upload_dir ) {
+ \restore_current_blog();
+ }
+
+ return $result;
+ }
+
+ /**
+ * Handles the file upload by optionally switching to the primary site of the network.
+ *
+ * @since 4.4.0 Default value for parameter `$overrides` removed (as it would be invalid).
+ *
+ * @param string $image_url The image file to sideload.
+ * @param array $overrides An associative array of names => values to override
+ * default variables. See `wp_handle_uploads` documentation
+ * for the full list of available overrides.
+ *
+ * @return string[] Information about the sideloaded file.
+ *
+ * @throws Upload_Handling_Exception The method throws a `RuntimeException`
+ * when an error is returned by `::handle_upload()`
+ * or the image file could not be copied.
+ *
+ * @phpstan-param HandleUploadOverrides $overrides
+ * @phpstan-return HandleUploadSuccess|HandleUploadError
+ */
+ public function handle_sideload( $image_url, array $overrides ) {
+ // Enable front end support.
+ if ( ! function_exists( 'wp_tempnam' ) ) {
+ require_once \ABSPATH . 'wp-admin/includes/file.php'; // @codeCoverageIgnore
+ }
+
+ // Save the file.
+ $temp_file = \wp_tempnam( $image_url );
+ if ( ! @\copy( $image_url, $temp_file ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors -- We throw our own exception.
+ throw new Upload_Handling_Exception( "Error copying $image_url to $temp_file." );
+ }
+
+ // Prepare file data.
+ $file_data = [
+ 'name' => $image_url,
+ 'type' => '', // No need to determine the MIME type here as it is untrusted anyway.
+ 'tmp_name' => $temp_file,
+ 'error' => \UPLOAD_ERR_OK,
+ 'size' => (int) @\filesize( $temp_file ),
+ ];
+
+ // Optionally override target filename.
+ if ( ! empty( $overrides['filename'] ) ) {
+ $file_data['name'] = $overrides['filename'];
+ }
+
+ // Use a custom action if none is set.
+ if ( empty( $overrides['action'] ) ) {
+ $overrides['action'] = 'avatar_privacy_sideload';
+ }
+
+ // Now, sideload it in.
+ $sideloaded = $this->handle_upload( $file_data, $overrides );
+
+ if ( ! empty( $sideloaded['error'] ) ) {
+ // Delete temporary file.
+ delete_file( $temp_file );
+
+ // Signal error.
+ throw new Upload_Handling_Exception( $sideloaded['error'] );
+ }
+
+ return $sideloaded;
+ }
+
+ /**
+ * Determines if the upload should use the global upload directory.
+ *
+ * @param array $overrides {
+ * An associative array of names => values to override default variables.
+ * See `wp_handle_uploads` documentation for the full list of available
+ * overrides.
+ *
+ * @type bool $global_upload Whether to use the global uploads directory on multisite.
+ * }
+ *
+ * @return bool
+ *
+ * @phpstan-param array{ global_upload?: bool } $overrides
+ */
+ protected function is_global_upload( $overrides ) {
+ return ( ! empty( $overrides['global_upload'] ) && \is_multisite() );
+ }
+
+ /**
+ * Prepares the overrides array for `wp_handle_upload()`.
+ *
+ * @param mixed[] $overrides An associative array of names => values to override
+ * default variables. See `wp_handle_uploads` documentation
+ * for the full list of available overrides.
+ *
+ * @return array{ mimes: array, action: string, test_form: bool }
+ */
+ protected function prepare_overrides( array $overrides ) {
+ $defaults = [
+ 'mimes' => self::ALLOWED_UPLOAD_MIME_TYPES,
+ 'action' => 'avatar_privacy_upload',
+ 'test_form' => false,
+ ];
+
+ /**
+ * Ensure that all necessary overrides have a default value.
+ *
+ * @phpstan-var array{ mimes: array, action: string, test_form: bool }
+ */
+ return \wp_parse_args( $overrides, $defaults );
+ }
+
+ /**
+ * Validates the image dimensions before uploading the file.
+ *
+ * @since 2.6.0
+ *
+ * @param array $file {
+ * Reference to a single element from $_FILES.
+ *
+ * @type string $name The original name of the file on the client machine.
+ * @type string $type The MIME type of the file, if the browser provided this information.
+ * @type string $tmp_name The temporary filename of the file in which the uploaded file was stored on the server.
+ * @type int $size The size, in bytes, of the uploaded file.
+ * @type int $error The error code associated with this file upload.
+ * }
+ *
+ * @return array The filtered $file array. The `error` key will be set to a
+ * string containing the error message if the image dimensions
+ * don't match the set limits.
+ *
+ * @phpstan-param FileSlice $file
+ * @phpstan-return FileSlice
+ */
+ public function validate_image_size( array $file ) {
+ $image_size = @\getimagesize( $file['tmp_name'] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors -- prevent additional errors if the file cannot be read.
+ if ( ! $image_size || empty( $image_size[0] ) || empty( $image_size[1] ) ) {
+ /* translators: uploaded image file name */
+ $file['error'] = \sprintf( \__( 'Error reading dimensions of image file %s.', 'avatar-privacy' ), $file['tmp_name'] );
+ } else {
+ $image_width = $image_size[0];
+ $image_height = $image_size[1];
+
+ /**
+ * Filters the minimum width for uploaded images.
+ *
+ * @since 2.6.0
+ *
+ * @param int $min_width The minimum width in pixels. Default 0.
+ */
+ $min_width = \apply_filters( 'avatar_privacy_upload_min_width', 0 );
+
+ /**
+ * Filters the minimum height for uploaded images.
+ *
+ * @since 2.6.0
+ *
+ * @param int $min_height The minimum width in pixels. Default 0.
+ */
+ $min_height = \apply_filters( 'avatar_privacy_upload_min_height', 0 );
+
+ /**
+ * Filters the maximum width for uploaded images.
+ *
+ * @since 2.6.0
+ *
+ * @param int $max_width The maximum width in pixels. Default 2000.
+ */
+ $max_width = \apply_filters( 'avatar_privacy_upload_max_width', 2000 );
+
+ /**
+ * Filters the maximum height for uploaded images.
+ *
+ * @since 2.6.0
+ *
+ * @param int $max_height The maximum height in pixels. Default 2000.
+ */
+ $max_height = \apply_filters( 'avatar_privacy_upload_max_height', 2000 );
+
+ if ( $image_width < $min_width || $image_height < $min_height ) {
+ $file['error'] = \sprintf(
+ /* translators: 1: minimum upload width, 2: minimum upload height, 3: actual image width, 4: actual image height */
+ \__( 'Image dimensions are too small. Minimum size is %1$d×%2$d pixels. Uploaded image is %3$d×%4$d pixels.', 'avatar-privacy' ),
+ $min_width,
+ $min_height,
+ $image_width,
+ $image_height
+ );
+ } elseif ( $image_width > $max_width || $image_height > $max_height ) {
+ $file['error'] = \sprintf(
+ /* translators: 1: maximum upload width, 2: maximum upload height, 3: actual image width, 4: actual image height */
+ \__( 'Image dimensions are too large. Maximum size is %1$d×%2$d pixels. Uploaded image is %3$d×%4$d pixels.', 'avatar-privacy' ),
+ $max_width,
+ $max_height,
+ $image_width,
+ $image_height
+ );
+ }
+ }
+
+ return $file;
+ }
+}
diff --git a/includes/avatar-privacy/tools/images/class-image-stream.php b/includes/avatar-privacy/tools/images/class-image-stream.php
new file mode 100644
index 0000000..e378366
--- /dev/null
+++ b/includes/avatar-privacy/tools/images/class-image-stream.php
@@ -0,0 +1,579 @@
+
+ *
+ * @phpstan-type StreamStat array{ dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int }
+ * @phpstan-type StreamHandle array{ data: string, atime: int, mtime: int }
+ */
+class Image_Stream {
+ const PROTOCOL = 'avprimg';
+
+ const READ_MODE = [
+ 'r' => true,
+ 'r+' => true,
+ 'w' => false,
+ 'w+' => true,
+ 'a' => false,
+ 'a+' => true,
+ 'c' => false,
+ 'c+' => true,
+ ];
+ const WRITE_MODE = [
+ 'r' => false,
+ 'r+' => true,
+ 'w' => true,
+ 'w+' => true,
+ 'a' => true,
+ 'a+' => true,
+ 'c' => true,
+ 'c+' => true,
+ ];
+
+ // The access keys for StreamHandle components.
+ private const DATA = 'data';
+ private const ACCESS_TIME = 'atime';
+ private const MODIFICATION_TIME = 'mtime';
+
+ /**
+ * The persistent handles for existing streams.
+ *
+ * @var array
+ *
+ * @phpstan-var array
+ */
+ private static array $handles = [];
+
+ /**
+ * The contents of the stream.
+ *
+ * @var string
+ */
+ private string $data;
+
+ /**
+ * The access time of the stream.
+ *
+ * @since 2.7.0
+ *
+ * @var int
+ */
+ private int $atime;
+
+ /**
+ * The modification time of the stream.
+ *
+ * @since 2.7.0
+ *
+ * @var int
+ */
+ private int $mtime;
+
+ /**
+ * Whether this stream can be read from.
+ *
+ * @var bool
+ */
+ private bool $read;
+
+ /**
+ * Whether this stream can be written to.
+ *
+ * @var bool
+ */
+ private bool $write;
+
+ /**
+ * The stream options.
+ *
+ * @var int
+ */
+ private int $options;
+
+ /**
+ * The current position within the stream.
+ *
+ * @var int
+ */
+ private int $position;
+
+ /**
+ * The stream context resource.
+ *
+ * @var resource
+ */
+ public $context;
+
+ /**
+ * Opens a stream. Only the host part is used as the handle.
+ *
+ * @param string $path The stream URL.
+ * @param string $mode The mode flags.
+ * @param int $options Holds additional flags set by the streams API.
+ * @param string $opened_path Return value of the actually opened path. Passed by reference.
+ *
+ * @return bool
+ */
+ public function stream_open( $path, $mode, $options, &$opened_path ) {
+ $real_path = static::get_handle_from_url( $path );
+ $this->options = $options;
+
+ [
+ self::DATA => &$this->data,
+ self::ACCESS_TIME => &$this->atime,
+ self::MODIFICATION_TIME => &$this->mtime,
+ ] = static::get_data_reference( $real_path );
+
+ // Strip binary/text flags from mode for comparison.
+ $mode = \str_replace( [ 'b', 't' ], '', $mode );
+
+ switch ( $mode ) {
+
+ case 'w':
+ case 'w+':
+ $this->stream_truncate( 0 );
+ // fall through.
+ case 'r':
+ case 'r+':
+ case 'c':
+ case 'c+':
+ $this->read = self::READ_MODE[ $mode ];
+ $this->write = self::WRITE_MODE[ $mode ];
+ $this->position = 0;
+ break;
+
+ case 'a':
+ case 'a+':
+ $this->read = self::READ_MODE[ $mode ];
+ $this->write = self::WRITE_MODE[ $mode ];
+ $this->position = \strlen( $this->data );
+ break;
+
+ default:
+ // Signal error.
+ $this->maybe_trigger_error( ! empty( $this->options & \STREAM_REPORT_ERRORS ), 'Invalid mode specified (mode specified makes no sense for this stream implementation)' );
+
+ return false;
+ }
+
+ // Set the opened path if requested.
+ if ( $this->options & \STREAM_USE_PATH ) {
+ $opened_path = $real_path;
+ }
+
+ return true;
+ }
+
+ /**
+ * Reads from the stream.
+ *
+ * @param int $bytes The number of bytes to read.
+ *
+ * @return string|false
+ */
+ public function stream_read( $bytes ) {
+ if ( $this->read ) {
+ $read_bytes = \substr( $this->data, $this->position, $bytes );
+ $this->position += \strlen( $read_bytes );
+
+ // Update access time.
+ $this->atime = \time();
+
+ return $read_bytes;
+ }
+
+ return false;
+ }
+
+ /**
+ * Writes to the stream.
+ *
+ * @param string $data The data to write.
+ *
+ * @return int
+ */
+ public function stream_write( $data ) {
+ if ( $this->write ) {
+ $data_length = \strlen( $data );
+ $left = \substr( $this->data, 0, $this->position );
+ $right = \substr( $this->data, $this->position + $data_length );
+ $this->data = "{$left}{$data}{$right}";
+ $this->position += $data_length;
+
+ // Update modification time.
+ $this->mtime = \time();
+
+ return $data_length;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Retrieves the current position.
+ *
+ * @return int
+ */
+ public function stream_tell() {
+ return $this->position;
+ }
+
+ /**
+ * Determines if the stream has reached its EOF.
+ *
+ * @return bool
+ */
+ public function stream_eof() {
+ return $this->position >= \strlen( $this->data );
+ }
+
+ /**
+ * Seeks to a new position.
+ *
+ * @param int $offset The amount by which to change the position.
+ * @param int $whence A flag indicating the direction of the seek operation.
+ *
+ * @return bool
+ */
+ public function stream_seek( $offset, $whence ) {
+ switch ( $whence ) {
+ case \SEEK_SET:
+ $this->position = $offset;
+ $truncate = true;
+ break;
+
+ case \SEEK_CUR:
+ $this->position += $offset;
+ $truncate = true;
+ break;
+
+ case \SEEK_END:
+ $this->position = \strlen( $this->data ) + $offset;
+ $truncate = true;
+ break;
+
+ default:
+ $truncate = false;
+ }
+
+ if ( $truncate ) {
+ $this->truncate_after_seek();
+ }
+
+ return $truncate;
+ }
+
+ /**
+ * Truncates the stream after a seek operation beyond its length.
+ *
+ * @return void
+ */
+ protected function truncate_after_seek() {
+ if ( $this->position > \strlen( $this->data ) ) {
+ $this->stream_truncate( $this->position );
+ }
+ }
+
+ /**
+ * Truncates the stream to a given size.
+ *
+ * @param int $length The new length in bytes.
+ *
+ * @return bool
+ */
+ public function stream_truncate( $length ) {
+ $current_length = \strlen( $this->data );
+
+ if ( $current_length > $length ) {
+ $this->data = \substr( $this->data, 0, $length );
+ $this->mtime = \time();
+ } elseif ( $current_length < $length ) {
+ $this->data = \str_pad( $this->data, $length, "\0", \STR_PAD_RIGHT );
+ $this->mtime = \time();
+ }
+
+ return true;
+ }
+
+ /**
+ * Retrieves information about the stream.
+ *
+ * @return array
+ *
+ * @phpstan-return StreamStat
+ */
+ public function stream_stat() {
+ return [
+ 'dev' => 0,
+ 'ino' => 0,
+ 'mode' => 0100777, // is_file & mode 0777.
+ 'nlink' => 0,
+ 'uid' => 0,
+ 'gid' => 0,
+ 'rdev' => 0,
+ 'size' => \strlen( $this->data ),
+ 'atime' => isset( $this->atime ) ? $this->atime : 0,
+ 'mtime' => isset( $this->mtime ) ? $this->mtime : 0,
+ 'ctime' => isset( $this->mtime ) ? $this->mtime : 0, // We don't have an inode.
+ 'blksize' => -1,
+ 'blocks' => -1,
+ ];
+ }
+
+ /**
+ * Retrieves information about the stream. Non-existing paths are silently
+ * ignored to simulate folders.
+ *
+ * @param string $path The URL.
+ * @param int $flags Additional flags set by the streams API.
+ *
+ * @return array|false
+ *
+ * @phpstan-return StreamStat|false
+ */
+ public function url_stat( $path, $flags ) {
+ $handle = static::get_handle_from_url( $path );
+
+ if ( static::handle_exists( $handle ) ) {
+ $h = @\fopen( $path, 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
+
+ // If fopen() failed, we are in trouble.
+ if ( ! \is_resource( $h ) ) {
+ $this->maybe_trigger_error( ! ( $flags & \STREAM_URL_STAT_QUIET ), 'Error opening stream handle for stat call' );
+
+ return false;
+ }
+
+ return \fstat( $h );
+ }
+
+ // Since we don't really have folders, treat every other call as if
+ // STREAM_URL_STAT_QUIET was set.
+ return [
+ 'dev' => 0,
+ 'ino' => 0,
+ 'mode' => 0,
+ 'nlink' => 0,
+ 'uid' => 0,
+ 'gid' => 0,
+ 'rdev' => 0,
+ 'size' => 0,
+ 'atime' => 0,
+ 'mtime' => 0,
+ 'ctime' => 0,
+ 'blksize' => -1,
+ 'blocks' => -1,
+ ];
+ }
+
+ /**
+ * Implements touch(), chmod(), chown() and chgrp() for the stream.
+ *
+ * @param string $path The URL to set the metadata for.
+ * @param int $option A flag indicating the originating function.
+ * @param array|string|int $value The arguments of the originating function.
+ *
+ * @return bool
+ *
+ * @phpstan-param array{ mtime: int, atime: int }|string|int $value
+ */
+ public function stream_metadata( $path, $option, $value ) {
+ if ( \STREAM_META_TOUCH === $option ) {
+ $stream = &static::get_data_reference( static::get_handle_from_url( $path ) );
+
+ $stream[ self::MODIFICATION_TIME ] = isset( $value[0] ) ? (int) $value[0] : \time();
+ $stream[ self::ACCESS_TIME ] = isset( $value[1] ) ? (int) $value[1] : $stream[ self::MODIFICATION_TIME ];
+ }
+
+ // Ignore metadata changing functions, but simulate success.
+ return true;
+ }
+
+ /**
+ * Unlinks the given URL.
+ *
+ * @param string $path The stream URL.
+ *
+ * @return bool
+ */
+ public function unlink( $path ) {
+ $handle = static::get_handle_from_url( $path );
+
+ if ( empty( $handle ) ) {
+ return false;
+ }
+
+ static::delete_handle( $handle );
+
+ // Clean up local references.
+ unset( $this->data );
+ unset( $this->atime );
+ unset( $this->mtime );
+
+ return true;
+ }
+
+ /**
+ * Triggers an error if the trigger condition is fulfilled.
+ *
+ * @since 2.4.0
+ *
+ * @param bool $condition Whether the error should be triggered.
+ * @param string $message The error message.
+ * @param int $error_level Optional. Only the E_USER_* constants are valid. Default E_USER_ERROR.
+ *
+ * @return void
+ */
+ protected function maybe_trigger_error( $condition, $message, $error_level = \E_USER_ERROR ) {
+ if ( $condition ) {
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
+ \trigger_error( \esc_html( $message ), $error_level );
+ }
+ }
+
+ /**
+ * Retrieves a reference to the handle and creates it if necessary.
+ *
+ * @since 2.1.0 Visibility changed to protected and renamed to get_data_reference.
+ *
+ * @param string $handle The stream handle.
+ *
+ * @return array A reference to the stream data.
+ *
+ * @phpstan-return StreamHandle
+ */
+ protected static function &get_data_reference( $handle ) { // phpcs:ignore ImportDetection.Imports.RequireImports.Symbol -- false positive.
+ if ( ! static::handle_exists( $handle ) ) {
+ $now = \time();
+
+ self::$handles[ $handle ] = [
+ self::DATA => '',
+ self::ACCESS_TIME => $now,
+ self::MODIFICATION_TIME => $now,
+ ];
+ }
+
+ return self::$handles[ $handle ];
+ }
+
+ /**
+ * Determines if the given handle already exists.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ *
+ * @param string $handle The stream handle.
+ *
+ * @return bool
+ */
+ protected static function handle_exists( $handle ) {
+ return isset( self::$handles[ $handle ] );
+ }
+
+ /**
+ * Retrieves the stream data and optionally deletes the handle.
+ *
+ * @param string $handle The stream handle.
+ * @param bool $delete Delete the handle after retrieving the stream data.
+ *
+ * @return string|null The stream data or null.
+ */
+ public static function get_data( $handle, $delete = false ) {
+ if ( ! static::handle_exists( $handle ) ) {
+ return null;
+ }
+
+ // Save data.
+ $result = static::get_data_reference( $handle )[ self::DATA ];
+
+ // Clean up, if requested.
+ if ( $delete ) {
+ static::delete_handle( $handle );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Deletes the given stream handle and its data.
+ *
+ * @param string $handle The stream handle.
+ *
+ * @return void
+ */
+ public static function delete_handle( $handle ) {
+ unset( self::$handles[ $handle ] );
+ }
+
+ /**
+ * Retrieves the stream handle from the wrapper URL.
+ *
+ * @since 2.4.0 An exception is thrown when an invalid URL is passed to the method.
+ *
+ * @param string $url The wrapper URL.
+ *
+ * @return string The handle.
+ *
+ * @throws \InvalidArgumentException Throws an exception if the URL is not valid.
+ */
+ public static function get_handle_from_url( $url ) {
+ $parts = \parse_url( $url ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
+
+ // Validate results.
+ if ( empty( $parts ) ) {
+ throw new \InvalidArgumentException( "{$url} is not a valid stream URL" );
+ }
+
+ $host = $parts['host'] ?? '';
+ $path = $parts['path'] ?? '/';
+
+ return $host . $path;
+ }
+
+ /**
+ * Registers the stream wrapper.
+ *
+ * @since 2.1.0
+ *
+ * @param string $protocol Optional. The wrapper-specific URL protocol. Default 'avprimg'.
+ *
+ * @return void
+ */
+ public static function register( $protocol = self::PROTOCOL ) {
+ if ( ! \in_array( $protocol, \stream_get_wrappers(), true ) ) {
+ \stream_wrapper_register( $protocol, static::class );
+ }
+ }
+}
diff --git a/includes/avatar-privacy/tools/images/class-png.php b/includes/avatar-privacy/tools/images/class-png.php
new file mode 100644
index 0000000..c96f868
--- /dev/null
+++ b/includes/avatar-privacy/tools/images/class-png.php
@@ -0,0 +1,266 @@
+
+ *
+ * @internal
+ *
+ * @phpstan-import-type RGBValue from Color
+ * @phpstan-import-type PercentValue from Color
+ * @phpstan-import-type NormalizedHue from Color
+ */
+class PNG {
+
+ /**
+ * The color helper.
+ *
+ * @since 2.7.0
+ *
+ * @var Color
+ */
+ private Color $color;
+
+ /**
+ * Creates a new PNG helper.
+ *
+ * @since 2.7.0
+ *
+ * @param Color $color The color helper.
+ */
+ public function __construct( Color $color ) {
+ $this->color = $color;
+ }
+
+ /**
+ * Creates an image resource of the chosen type.
+ *
+ * @since 2.5.0 Returns a resource or GdImage instance, depending on the PHP version.
+ *
+ * @param string $type The type of background to create. Valid: 'white', 'black', 'transparent'.
+ * @param int $width Image width in pixels.
+ * @param int $height Image height in pixels.
+ *
+ * @return resource|GdImage
+ *
+ * @throws \InvalidArgumentException Called with an incorrect type.
+ * @throws PNG_Image_Exception The image could not be created.
+ */
+ public function create( $type, $width, $height ) {
+ /**
+ * A bitmap image (class or resource).
+ *
+ * @var resource|GdImage $image The created GD image.
+ */
+ $image = \imageCreateTrueColor( $width, $height );
+
+ // Something went wrong, badly.
+ if ( ! \is_gd_image( $image ) ) {
+ throw new PNG_Image_Exception( "The image of type {$type} ($width x $height) could not be created." ); // @codeCoverageIgnore
+ }
+
+ // Don't do alpha blending for the initial fill operation.
+ \imageAlphaBlending( $image, false );
+ \imageSaveAlpha( $image, true );
+
+ try {
+ // Fill image with appropriate color.
+ switch ( $type ) {
+ case 'transparent':
+ $color = \imageColorAllocateAlpha( $image, 0, 0, 0, 127 );
+ break;
+
+ case 'white':
+ $color = \imageColorAllocateAlpha( $image, 255, 255, 255, 0 );
+ break;
+
+ case 'black':
+ // No need to do anything else.
+ return $image;
+
+ default:
+ throw new \InvalidArgumentException( "Invalid image type $type." );
+ }
+
+ if ( false === $color || ! \imageFilledRectangle( $image, 0, 0, $width, $height, $color ) ) {
+ throw new PNG_Image_Exception( "Error filling image of type $type." ); // @codeCoverageIgnore
+ }
+ } catch ( PNG_Image_Exception $e ) {
+ // Clean up and re-throw exception.
+ \imageDestroy( $image ); // @codeCoverageIgnoreStart
+ throw $e; // @codeCoverageIgnoreEnd
+ }
+
+ // Fix transparent background.
+ \imageAlphaBlending( $image, true );
+
+ return $image;
+ }
+
+ /**
+ * Creates an image resource from the given file.
+ *
+ * @since 2.5.0 Returns a resource or GdImage instance, depending on the PHP version.
+ *
+ * @param string $file The absolute path to a PNG image file.
+ *
+ * @return resource|GdImage
+ *
+ * @throws PNG_Image_Exception The image could not be read.
+ */
+ public function create_from_file( $file ) {
+ /**
+ * A bitmap image (class or resource).
+ *
+ * @var resource|GdImage $image The created GD image.
+ */
+ $image = @\imageCreateFromPNG( $file );
+
+ // Something went wrong, badly.
+ if ( ! \is_gd_image( $image ) ) {
+ throw new PNG_Image_Exception( "The PNG image {$file} could not be read." );
+ }
+
+ // Fix transparent background.
+ \imageAlphaBlending( $image, true );
+ \imageSaveAlpha( $image, true );
+
+ return $image;
+ }
+
+ /**
+ * Copies an image onto an existing base image. The image resource is freed
+ * after copying.
+ *
+ * @since 2.5.0 Parameters $base and $image can now also be instances of GdImage.
+ *
+ * @param resource|GdImage $base The avatar image resource.
+ * @param resource|GdImage $image The image to be copied onto the base.
+ * @param int $width Image width in pixels.
+ * @param int $height Image height in pixels.
+ *
+ * @return void
+ *
+ * @throws \InvalidArgumentException One of the first two parameters was not a valid image resource.
+ * @throws PNG_Image_Exception The image could not be copied.
+ */
+ public function combine( $base, $image, $width, $height ) {
+
+ // Abort if $image is not a valid resource.
+ if ( ! \is_gd_image( $base ) || ! \is_gd_image( $image ) ) {
+ throw new \InvalidArgumentException( 'Invalid image resource.' );
+ }
+
+ // Copy the image to the base.
+ $result = \imageCopy( $base, $image, 0, 0, 0, 0, $width, $height );
+
+ // Clean up.
+ \imageDestroy( $image );
+
+ // Return copy success status.
+ if ( ! $result ) {
+ throw new PNG_Image_Exception( 'Error while copying image.' ); // @codeCoverageIgnore
+ }
+ }
+
+ /**
+ * Fills the given image with a HSL color.
+ *
+ * @since 2.5.0 Parameter $image can now also be a GdImage.
+ *
+ * @param resource|GdImage $image The image.
+ * @param int $hue The hue (0-359).
+ * @param int $saturation The saturation (0-100).
+ * @param int $lightness The lightness/Luminosity (0-100).
+ * @param int $x The horizontal coordinate.
+ * @param int $y The vertical coordinate.
+ *
+ * @return void
+ *
+ * @throws \InvalidArgumentException Not a valid image resource.
+ * @throws PNG_Image_Exception The image could not be filled.
+ *
+ * @phpstan-param NormalizedHue $hue
+ * @phpstan-param PercentValue $saturation
+ * @phpstan-param PercentValue $lightness
+ */
+ public function fill_hsl( $image, $hue, $saturation, $lightness, $x, $y ) {
+ // Abort if $image is not a valid resource.
+ if ( ! \is_gd_image( $image ) ) {
+ throw new \InvalidArgumentException( 'Invalid image resource.' );
+ }
+
+ list( $red, $green, $blue ) = $this->color->hsl_to_rgb( $hue, $saturation, $lightness );
+ $color = \imageColorAllocate( $image, $red, $green, $blue );
+
+ if ( false === $color || ! \imageFill( $image, $x, $y, $color ) ) {
+ throw new PNG_Image_Exception( "Error filling image with HSL ({$hue}, {$saturation}, {$lightness})." ); // @codeCoverageIgnore
+ }
+ }
+
+ /**
+ * Converts a color specified using HSL to its RGB representation.
+ *
+ * @since 2.5.0
+ *
+ * @deprecated 2.7.0 Use Color::hsl_to_rgb instead.
+ *
+ * @param int $hue The hue (in degrees, i.e. 0-359).
+ * @param int $saturation The saturation (in percent, i.e. 0-100).
+ * @param int $lightness The lightness (in percent, i.e. 0-100).
+ *
+ * @return int[] {
+ * The RGB color as a tuple.
+ *
+ * @type int $red The red component (0-255).
+ * @type int $green The green component (0-255).
+ * @type int $blue The blue component (0-255).
+ * }
+ *
+ * @phpstan-param NormalizedHue $hue
+ * @phpstan-param PercentValue $saturation
+ * @phpstan-param PercentValue $lightness
+ * @phpstan-return array{ 0: RGBValue, 1: RGBValue, 2: RGBValue }
+ */
+ public function hsl_to_rgb( $hue, $saturation, $lightness ) {
+ \_deprecated_function( __METHOD__, 'Avatar Privacy 2.7.0', 'Avatar_Privacy\Tools\Images\Color::hsl_to_rgb' );
+
+ return $this->color->hsl_to_rgb( $hue, $saturation, $lightness );
+ }
+}
diff --git a/includes/avatar-privacy/tools/images/class-svg.php b/includes/avatar-privacy/tools/images/class-svg.php
new file mode 100644
index 0000000..900eb26
--- /dev/null
+++ b/includes/avatar-privacy/tools/images/class-svg.php
@@ -0,0 +1,716 @@
+
+ */
+abstract class SVG {
+
+ // SVG attributes.
+ const ATTR_CLASS = 'class';
+ const ATTR_CLIPPATHUNITS = 'clippathunits';
+ const ATTR_CLIP_PATH = 'clip-path';
+ const ATTR_CLIP_RULE = 'clip-rule';
+ const ATTR_COLOR_INTERPOLATION_FILTERS = 'color-interpolation-filters';
+ const ATTR_CX = 'cx';
+ const ATTR_CY = 'cy';
+ const ATTR_D = 'd';
+ const ATTR_DISPLAY = 'display';
+ const ATTR_DX = 'dx';
+ const ATTR_DY = 'dy';
+ const ATTR_FILL = 'fill';
+ const ATTR_FILL_OPACITY = 'fill-opacity';
+ const ATTR_FILL_RULE = 'fill-rule';
+ const ATTR_FILTER = 'filter';
+ const ATTR_FILTERRES = 'filterres';
+ const ATTR_FILTERUNITS = 'filterunits';
+ const ATTR_FONT_FAMILY = 'font-family';
+ const ATTR_FONT_SIZE = 'font-size';
+ const ATTR_FONT_STYLE = 'font-style';
+ const ATTR_FONT_WEIGHT = 'font-weight';
+ const ATTR_FX = 'fx';
+ const ATTR_FY = 'fy';
+ const ATTR_GRADIENTTRANSFORM = 'gradienttransform';
+ const ATTR_GRADIENTUNITS = 'gradientunits';
+ const ATTR_HEIGHT = 'height';
+ const ATTR_HREF = 'href';
+ const ATTR_ID = 'id';
+ const ATTR_MARKERHEIGHT = 'markerheight';
+ const ATTR_MARKERUNITS = 'markerunits';
+ const ATTR_MARKERWIDTH = 'markerwidth';
+ const ATTR_MARKER_END = 'marker-end';
+ const ATTR_MARKER_MID = 'marker-mid';
+ const ATTR_MARKER_START = 'marker-start';
+ const ATTR_MASK = 'mask';
+ const ATTR_MASKEDCONTENTUNITS = 'maskcontentunits';
+ const ATTR_MASKUNITS = 'maskunits';
+ const ATTR_METHOD = 'method';
+ const ATTR_OFFSET = 'offset';
+ const ATTR_OPACITY = 'opacity';
+ const ATTR_ORIENT = 'orient';
+ const ATTR_PATTERNCONTENTUNITS = 'patterncontentunits';
+ const ATTR_PATTERNTRANSFORM = 'patterntransform';
+ const ATTR_PATTERNUNITS = 'patternunits';
+ const ATTR_POINTS = 'points';
+ const ATTR_PRESERVEASPECTRATIO = 'preserveaspectratio';
+ const ATTR_PRIMITIVEUNITS = 'primitiveunits';
+ const ATTR_R = 'r';
+ const ATTR_REFX = 'refx';
+ const ATTR_REFY = 'refy';
+ const ATTR_REQUIREDFEATURES = 'requiredfeatures';
+ const ATTR_ROTATE = 'rotate';
+ const ATTR_RX = 'rx';
+ const ATTR_RY = 'ry';
+ const ATTR_SPACING = 'spacing';
+ const ATTR_SPREADMETHOD = 'spreadmethod';
+ const ATTR_STARTOFFSET = 'startoffset';
+ const ATTR_STDDEVIATION = 'stddeviation';
+ const ATTR_STOP_COLOR = 'stop-color';
+ const ATTR_STOP_OPACITY = 'stop-opacity';
+ const ATTR_STROKE = 'stroke';
+ const ATTR_STROKE_DASHARRAY = 'stroke-dasharray';
+ const ATTR_STROKE_DASHOFFSET = 'stroke-dashoffset';
+ const ATTR_STROKE_LINECAP = 'stroke-linecap';
+ const ATTR_STROKE_LINEJOIN = 'stroke-linejoin';
+ const ATTR_STROKE_MITERLIMIT = 'stroke-miterlimit';
+ const ATTR_STROKE_OPACITY = 'stroke-opacity';
+ const ATTR_STROKE_WIDTH = 'stroke-width';
+ const ATTR_STYLE = 'style';
+ const ATTR_SYSTEMLANGUAGE = 'systemlanguage';
+ const ATTR_TEXTLENGTH = 'textlength';
+ const ATTR_TEXT_ANCHOR = 'text-anchor';
+ const ATTR_TRANSFORM = 'transform';
+ const ATTR_TYPE = 'type';
+ const ATTR_WIDTH = 'width';
+ const ATTR_VIEWBOX = 'viewbox';
+ const ATTR_X = 'x';
+ const ATTR_X1 = 'x1';
+ const ATTR_X2 = 'x2';
+ const ATTR_XLINK_HREF = 'xlink:href';
+ const ATTR_XLINK_TITLE = 'xlink:title';
+ const ATTR_XMLNS = 'xmlns';
+ const ATTR_XMLNS_SE = 'xmlns:se';
+ const ATTR_XMLNS_XLINK = 'xmlns:xlink';
+ const ATTR_XML_SPACE = 'xml:space';
+ const ATTR_Y = 'y';
+ const ATTR_Y1 = 'y1';
+ const ATTR_Y2 = 'y2';
+
+ /**
+ * An array of allowed elements and attributes in wp_kses syntax.
+ *
+ * List initially compiled by Michael Pollett in Trac #24251.
+ *
+ * @link https://core.trac.wordpress.org/ticket/24251
+ *
+ * @var array>
+ */
+ const ALLOWED_ELEMENTS = [
+ 'a' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_ID => true,
+ self::ATTR_MASK => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TRANSFORM => true,
+ self::ATTR_HREF => true,
+ self::ATTR_XLINK_HREF => true,
+ self::ATTR_XLINK_TITLE => true,
+ ],
+ 'circle' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_CX => true,
+ self::ATTR_CY => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_ID => true,
+ self::ATTR_MASK => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_R => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TRANSFORM => true,
+ ],
+ 'clippath' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIPPATHUNITS => true,
+ self::ATTR_ID => true,
+ ],
+ 'defs' => [
+ // Global attributes.
+ self::ATTR_CLASS => true,
+ self::ATTR_ID => true,
+ self::ATTR_STYLE => true,
+ ],
+ 'style' => [
+ self::ATTR_TYPE => true,
+ ],
+ 'desc' => [
+ // Global attributes.
+ self::ATTR_CLASS => true,
+ self::ATTR_ID => true,
+ self::ATTR_STYLE => true,
+ ],
+ 'ellipse' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_CX => true,
+ self::ATTR_CY => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_ID => true,
+ self::ATTR_MASK => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_RX => true,
+ self::ATTR_RY => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TRANSFORM => true,
+ ],
+ 'fegaussianblur' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_COLOR_INTERPOLATION_FILTERS => true,
+ self::ATTR_ID => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STDDEVIATION => true,
+ ],
+ 'filter' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_COLOR_INTERPOLATION_FILTERS => true,
+ self::ATTR_FILTERRES => true,
+ self::ATTR_FILTERUNITS => true,
+ self::ATTR_HEIGHT => true,
+ self::ATTR_ID => true,
+ self::ATTR_PRIMITIVEUNITS => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_WIDTH => true,
+ self::ATTR_X => true,
+ self::ATTR_XLINK_HREF => true,
+ self::ATTR_Y => true,
+ ],
+ 'foreignobject' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_FONT_SIZE => true,
+ self::ATTR_HEIGHT => true,
+ self::ATTR_ID => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_TRANSFORM => true,
+ self::ATTR_WIDTH => true,
+ self::ATTR_X => true,
+ self::ATTR_Y => true,
+ ],
+ 'g' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_ID => true,
+ self::ATTR_DISPLAY => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_MASK => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TRANSFORM => true,
+ self::ATTR_FONT_FAMILY => true,
+ self::ATTR_FONT_SIZE => true,
+ self::ATTR_FONT_STYLE => true,
+ self::ATTR_FONT_WEIGHT => true,
+ self::ATTR_TEXT_ANCHOR => true,
+ ],
+ 'image' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_HEIGHT => true,
+ self::ATTR_ID => true,
+ self::ATTR_MASK => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TRANSFORM => true,
+ self::ATTR_WIDTH => true,
+ self::ATTR_X => true,
+ self::ATTR_XLINK_HREF => true,
+ self::ATTR_XLINK_TITLE => true,
+ self::ATTR_Y => true,
+ ],
+ 'line' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_ID => true,
+ self::ATTR_MARKER_END => true,
+ self::ATTR_MARKER_MID => true,
+ self::ATTR_MARKER_START => true,
+ self::ATTR_MASK => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TRANSFORM => true,
+ self::ATTR_X1 => true,
+ self::ATTR_X2 => true,
+ self::ATTR_Y1 => true,
+ self::ATTR_Y2 => true,
+ ],
+ 'lineargradient' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_ID => true,
+ self::ATTR_GRADIENTTRANSFORM => true,
+ self::ATTR_GRADIENTUNITS => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_SPREADMETHOD => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_X1 => true,
+ self::ATTR_X2 => true,
+ self::ATTR_XLINK_HREF => true,
+ self::ATTR_Y1 => true,
+ self::ATTR_Y2 => true,
+ ],
+ 'marker' => [
+ self::ATTR_ID => true,
+ self::ATTR_CLASS => true,
+ self::ATTR_MARKERHEIGHT => true,
+ self::ATTR_MARKERUNITS => true,
+ self::ATTR_MARKERWIDTH => true,
+ self::ATTR_ORIENT => true,
+ self::ATTR_PRESERVEASPECTRATIO => true,
+ self::ATTR_REFX => true,
+ self::ATTR_REFY => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_VIEWBOX => true,
+ ],
+ 'mask' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_HEIGHT => true,
+ self::ATTR_ID => true,
+ self::ATTR_MASKEDCONTENTUNITS => true,
+ self::ATTR_MASKUNITS => true,
+ self::ATTR_WIDTH => true,
+ self::ATTR_X => true,
+ self::ATTR_Y => true,
+ ],
+ 'metadata' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_ID => true,
+ ],
+ 'path' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_D => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_ID => true,
+ self::ATTR_MARKER_END => true,
+ self::ATTR_MARKER_MID => true,
+ self::ATTR_MARKER_START => true,
+ self::ATTR_MASK => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TRANSFORM => true,
+ ],
+ 'pattern' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_HEIGHT => true,
+ self::ATTR_ID => true,
+ self::ATTR_PATTERNCONTENTUNITS => true,
+ self::ATTR_PATTERNTRANSFORM => true,
+ self::ATTR_PATTERNUNITS => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_VIEWBOX => true,
+ self::ATTR_WIDTH => true,
+ self::ATTR_X => true,
+ self::ATTR_XLINK_HREF => true,
+ self::ATTR_Y => true,
+ ],
+ 'polygon' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_ID => true,
+ self::ATTR_MARKER_END => true,
+ self::ATTR_MARKER_MID => true,
+ self::ATTR_MARKER_START => true,
+ self::ATTR_MASK => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_POINTS => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TRANSFORM => true,
+ ],
+ 'polyline' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_ID => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_MARKER_END => true,
+ self::ATTR_MARKER_MID => true,
+ self::ATTR_MARKER_START => true,
+ self::ATTR_MASK => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_POINTS => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TRANSFORM => true,
+ ],
+ 'radialgradient' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CX => true,
+ self::ATTR_CY => true,
+ self::ATTR_FX => true,
+ self::ATTR_FY => true,
+ self::ATTR_GRADIENTTRANSFORM => true,
+ self::ATTR_GRADIENTUNITS => true,
+ self::ATTR_ID => true,
+ self::ATTR_R => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_SPREADMETHOD => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_XLINK_HREF => true,
+ ],
+ 'rect' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_HEIGHT => true,
+ self::ATTR_ID => true,
+ self::ATTR_MASK => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_RX => true,
+ self::ATTR_RY => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TRANSFORM => true,
+ self::ATTR_WIDTH => true,
+ self::ATTR_X => true,
+ self::ATTR_Y => true,
+ ],
+ 'stop' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_ID => true,
+ self::ATTR_OFFSET => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STOP_COLOR => true,
+ self::ATTR_STOP_OPACITY => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ ],
+ 'svg' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_ID => true,
+ self::ATTR_HEIGHT => true,
+ self::ATTR_MASK => true,
+ self::ATTR_PRESERVEASPECTRATIO => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_VIEWBOX => true,
+ self::ATTR_WIDTH => true,
+ self::ATTR_X => true,
+ self::ATTR_XMLNS => true,
+ self::ATTR_XMLNS_SE => true,
+ self::ATTR_XMLNS_XLINK => true,
+ self::ATTR_Y => true,
+ ],
+ 'switch' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_ID => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ ],
+ 'symbol' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_FONT_FAMILY => true,
+ self::ATTR_FONT_SIZE => true,
+ self::ATTR_FONT_STYLE => true,
+ self::ATTR_FONT_WEIGHT => true,
+ self::ATTR_ID => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_PRESERVEASPECTRATIO => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TRANSFORM => true,
+ self::ATTR_VIEWBOX => true,
+ ],
+ 'text' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_FONT_FAMILY => true,
+ self::ATTR_FONT_SIZE => true,
+ self::ATTR_FONT_STYLE => true,
+ self::ATTR_FONT_WEIGHT => true,
+ self::ATTR_ID => true,
+ self::ATTR_MASK => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TEXT_ANCHOR => true,
+ self::ATTR_TRANSFORM => true,
+ self::ATTR_X => true,
+ self::ATTR_XML_SPACE => true,
+ self::ATTR_Y => true,
+ ],
+ 'textpath' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_ID => true,
+ self::ATTR_METHOD => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_SPACING => true,
+ self::ATTR_STARTOFFSET => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TRANSFORM => true,
+ self::ATTR_XLINK_HREF => true,
+ ],
+ 'title' => [
+ // Global attributes.
+ self::ATTR_CLASS => true,
+ self::ATTR_ID => true,
+ self::ATTR_STYLE => true,
+ ],
+ 'tspan' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_DX => true,
+ self::ATTR_DY => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_FONT_FAMILY => true,
+ self::ATTR_FONT_SIZE => true,
+ self::ATTR_FONT_STYLE => true,
+ self::ATTR_FONT_WEIGHT => true,
+ self::ATTR_ID => true,
+ self::ATTR_MASK => true,
+ self::ATTR_OPACITY => true,
+ self::ATTR_REQUIREDFEATURES => true,
+ self::ATTR_ROTATE => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_SYSTEMLANGUAGE => true,
+ self::ATTR_TEXT_ANCHOR => true,
+ self::ATTR_TEXTLENGTH => true,
+ self::ATTR_TRANSFORM => true,
+ self::ATTR_X => true,
+ self::ATTR_XML_SPACE => true,
+ self::ATTR_Y => true,
+ ],
+ 'use' => [
+ self::ATTR_CLASS => true,
+ self::ATTR_CLIP_PATH => true,
+ self::ATTR_CLIP_RULE => true,
+ self::ATTR_FILL => true,
+ self::ATTR_FILL_OPACITY => true,
+ self::ATTR_FILL_RULE => true,
+ self::ATTR_FILTER => true,
+ self::ATTR_HEIGHT => true,
+ self::ATTR_ID => true,
+ self::ATTR_MASK => true,
+ self::ATTR_STROKE => true,
+ self::ATTR_STROKE_DASHARRAY => true,
+ self::ATTR_STROKE_DASHOFFSET => true,
+ self::ATTR_STROKE_LINECAP => true,
+ self::ATTR_STROKE_LINEJOIN => true,
+ self::ATTR_STROKE_MITERLIMIT => true,
+ self::ATTR_STROKE_OPACITY => true,
+ self::ATTR_STROKE_WIDTH => true,
+ self::ATTR_STYLE => true,
+ self::ATTR_TRANSFORM => true,
+ self::ATTR_WIDTH => true,
+ self::ATTR_X => true,
+ self::ATTR_XLINK_HREF => true,
+ self::ATTR_Y => true,
+ ],
+ ];
+}
diff --git a/includes/avatar-privacy/tools/images/class-type.php b/includes/avatar-privacy/tools/images/class-type.php
new file mode 100644
index 0000000..c253382
--- /dev/null
+++ b/includes/avatar-privacy/tools/images/class-type.php
@@ -0,0 +1,62 @@
+
+ */
+abstract class Type {
+ const JPEG_IMAGE = Image_File::JPEG_IMAGE;
+ const PNG_IMAGE = Image_File::PNG_IMAGE;
+ const SVG_IMAGE = Image_File::SVG_IMAGE;
+
+ const JPEG_EXTENSION = Image_File::JPEG_EXTENSION;
+ const JPEG_ALT_EXTENSION = Image_File::JPEG_ALT_EXTENSION;
+ const PNG_EXTENSION = Image_File::PNG_EXTENSION;
+ const SVG_EXTENSION = Image_File::SVG_EXTENSION;
+
+ const CONTENT_TYPE = [
+ self::JPEG_EXTENSION => self::JPEG_IMAGE,
+ self::JPEG_ALT_EXTENSION => self::JPEG_IMAGE,
+ self::PNG_EXTENSION => self::PNG_IMAGE,
+ self::SVG_EXTENSION => self::SVG_IMAGE,
+ ];
+
+ const FILE_EXTENSION = [
+ self::JPEG_IMAGE => self::JPEG_EXTENSION,
+ self::PNG_IMAGE => self::PNG_EXTENSION,
+ self::SVG_IMAGE => self::SVG_EXTENSION,
+ ];
+}
diff --git a/includes/avatar-privacy/tools/network/class-gravatar-service.php b/includes/avatar-privacy/tools/network/class-gravatar-service.php
new file mode 100644
index 0000000..71f2c44
--- /dev/null
+++ b/includes/avatar-privacy/tools/network/class-gravatar-service.php
@@ -0,0 +1,287 @@
+
+ */
+class Gravatar_Service {
+
+ /**
+ * A cache for the results of the validate method.
+ *
+ * @var string[]
+ */
+ private $validation_cache = [];
+
+ /**
+ * The transients handler.
+ *
+ * @var Transients
+ */
+ private $transients;
+
+ /**
+ * The site transients handler.
+ *
+ * @var Site_Transients
+ */
+ private $site_transients;
+
+ /**
+ * The image editor.
+ *
+ * @since 2.3.0
+ *
+ * @var Editor
+ */
+ private $editor;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.3.0 Parameter $editor added.
+ *
+ * @param Transients $transients The transients handler.
+ * @param Site_Transients $site_transients The site transients handler.
+ * @param Editor $editor The image editor.
+ */
+ public function __construct( Transients $transients, Site_Transients $site_transients, Editor $editor ) {
+ $this->transients = $transients;
+ $this->site_transients = $site_transients;
+ $this->editor = $editor;
+ }
+
+ /**
+ * Retrieves the gravatar image for a given e-mail address.
+ *
+ * @param string $email The e-mail address.
+ * @param int $size The size in pixels.
+ * @param string $rating The audience rating.
+ *
+ * @return string The image data.
+ */
+ public function get_image( $email, $size, $rating ) {
+ $image = \wp_remote_retrieve_body(
+ \wp_remote_get( $this->get_url( $email, $size, $rating ) )
+ );
+
+ if ( false === $this->editor->get_mime_type( $image ) ) {
+ // Propably a Varnish error, so we ignore the data.
+ $image = '';
+ }
+
+ return $image;
+ }
+
+ /**
+ * Constructs the Gravatar.com service URL from the given parameters.
+ *
+ * @param string $email The email address.
+ * @param int $size Optional. The size in pixels. Default 80 (the same as Gravatar.com).
+ * @param string $rating Optional. Either 'g', 'pg', 'r', or 'x'. Default 'x' (to allow any image).
+ *
+ * @return string
+ */
+ public function get_url( $email, $size = 80, $rating = 'x' ) {
+ $args = [
+ 'd' => '404', // We are never interested in gravatar default images.
+ 's' => empty( $size ) ? '' : $size,
+ 'r' => $rating,
+ ];
+
+ return \esc_url_raw( \add_query_arg( $args, "https://secure.gravatar.com/avatar/{$this->get_hash( $email )}" ) );
+ }
+
+ /**
+ * Creates a hash from the given mail address using the SHA-256 algorithm.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ *
+ * @param string $email An email address.
+ *
+ * @return string
+ */
+ protected function get_hash( $email ) {
+ return \md5( \strtolower( \trim( $email ) ) );
+ }
+
+ /**
+ * Checks if a gravatar exists for the given e-mail address and returns the
+ * MIME type of the image if successful.
+ *
+ * Function originally taken from: http://codex.wordpress.org/Using_Gravatars.
+ *
+ * @param string $email The e-mail address to check.
+ * @param int $age Optional. The age of the object associated with the e-mail address. Default 0.
+ *
+ * @return string Returns the MIME type if a gravatar exists for the given e-mail address,
+ * '' otherwise. This includes situations where Gravatar.com could not be
+ * reached, or answered with a different error code, or if no e-mail address
+ * was given.
+ */
+ public function validate( $email = '', $age = 0 ) {
+ // Make sure we have a real address to check.
+ if ( empty( $email ) ) {
+ return '';
+ }
+
+ // Calculate the hash of the e-mail address.
+ $hash = $this->get_hash( $email );
+
+ // Try to find something in the cache.
+ if ( isset( $this->validation_cache[ $hash ] ) ) {
+ return $this->validation_cache[ $hash ];
+ }
+
+ // Try to find it via transient cache. On multisite, we use site transients.
+ return $this->validate_and_cache( \is_multisite() ? $this->site_transients : $this->transients, $email, $hash, $age );
+ }
+
+ /**
+ * Checks if a gravatar exists for the given e-mail address and cachces the
+ * result.
+ *
+ * @since 2.4.0 Extracted from ::validate
+ *
+ * @param Transients_Helper $transients The transients API helper to use.
+ * @param string $email The e-mail address to check.
+ * @param string $hash The hashed e-mail address.
+ * @param int $age The age of the object associated with
+ * the e-mail address.
+ *
+ * @return string Returns the image MIME type if successful,
+ * or the empty string otherwise.
+ */
+ protected function validate_and_cache( Transients_Helper $transients, $email, $hash, $age ) {
+ $transient_key = "check_{$hash}";
+ $result = $transients->get( $transient_key );
+ if ( \is_string( $result ) ) {
+ // Warm 1st level cache.
+ $this->validation_cache[ $hash ] = $result;
+ return $result;
+ }
+
+ // Ask gravatar.com.
+ $result = $this->ping_gravatar( $email );
+ if ( false !== $result ) {
+ // Cache result.
+ $transients->set( $transient_key, $result, $this->calculate_caching_duration( $result, $age ) );
+ $this->validation_cache[ $hash ] = $result;
+ }
+
+ return $result ?: '';
+ }
+
+ /**
+ * Pings Gravatar.com to check if there is an image for the given hash.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ * @since 2.4.0 Returns '' instead of 0 when no Gravatar can be found.
+ *
+ * @param string $email The e-mail address to check.
+ *
+ * @return string|false Returns the image MIME type if successful, the empty
+ * string there is no Gravatar for the e-mail address,
+ * and `false` in case of an error.
+ */
+ protected function ping_gravatar( $email ) {
+ // Ask gravatar.com.
+ $response = \wp_remote_head( $this->get_url( $email ) );
+ if ( $response instanceof \WP_Error ) {
+ return false; // Don't cache the result.
+ }
+
+ switch ( \wp_remote_retrieve_response_code( $response ) ) {
+ case 200:
+ // Valid image found.
+ $result = \wp_remote_retrieve_header( $response, 'content-type' );
+ if ( \is_array( $result ) ) {
+ $result = $result[0];
+ }
+ break;
+
+ case 404:
+ // No image found.
+ $result = '';
+ break;
+
+ default:
+ $result = false; // Don't cache the result.
+ }
+
+ return $result;
+ }
+
+ /**
+ * Calculates the proper caching duration.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ * @since 2.4.0 The type hint of $result has been adjusted according the
+ * simplified return value of `::ping_gravatar`.
+ *
+ * @param string|false $result The result of the validation check.
+ * @param int $age The "age" (difference between now and the
+ * creation date) of a comment or post (in seconds).
+ *
+ * @return int
+ */
+ protected function calculate_caching_duration( $result, $age ) {
+ // Cache the result across all blogs (a YES for 1 week, a NO for 10 minutes
+ // or longer, depending on the age of the object (comment, post), since a
+ // YES basically shouldn't change, but a NO might change when the user
+ // signs up with gravatar.com).
+ if ( ! empty( $result ) || $age > \WEEK_IN_SECONDS ) {
+ $duration = \WEEK_IN_SECONDS;
+ } elseif ( $age > \DAY_IN_SECONDS ) {
+ $duration = \DAY_IN_SECONDS;
+ } elseif ( $age > \HOUR_IN_SECONDS ) {
+ $duration = \HOUR_IN_SECONDS;
+ } else {
+ $duration = 10 * \MINUTE_IN_SECONDS;
+ }
+
+ /**
+ * Filters the interval between gravatar validation checks.
+ *
+ * @param int $duration The validation interval. Default 1 week if the check was successful, less if not.
+ * @param bool $result The result of the validation check.
+ * @param int $age The "age" (difference between now and the creation date) of a comment or post (in sceonds).
+ */
+ return \apply_filters( 'avatar_privacy_validate_gravatar_interval', $duration, ! empty( $result ), $age );
+ }
+}
diff --git a/includes/avatar-privacy/tools/network/class-remote-image-service.php b/includes/avatar-privacy/tools/network/class-remote-image-service.php
new file mode 100644
index 0000000..7aa9b44
--- /dev/null
+++ b/includes/avatar-privacy/tools/network/class-remote-image-service.php
@@ -0,0 +1,233 @@
+
+ */
+class Remote_Image_Service {
+
+ /**
+ * The cache handler.
+ *
+ * @since 2.4.0
+ *
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * The hashing helper.
+ *
+ * @since 2.4.0
+ *
+ * @var Hasher
+ */
+ private $hasher;
+
+ /**
+ * The images editor.
+ *
+ * @since 2.4.0
+ *
+ * @var Editor
+ */
+ private $editor;
+
+ /**
+ * The hashes database table.
+ *
+ * @since 2.4.0
+ *
+ * @var Hashes_Table
+ */
+ private $table;
+
+ const IDENTIFIER_TYPE = 'image-url';
+ const URL_CACHE_DURATION = 24 * \HOUR_IN_SECONDS;
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.4.0
+ *
+ * @param Cache $cache The cache helper.
+ * @param Hasher $hasher The hashing helper.
+ * @param Editor $editor The image editor.
+ * @param Hashes_Table $table The database table for storing hash <=> URL mappings.
+ */
+ public function __construct( Cache $cache, Hasher $hasher, Editor $editor, Hashes_Table $table ) {
+ $this->cache = $cache;
+ $this->hasher = $hasher;
+ $this->editor = $editor;
+ $this->table = $table;
+ }
+
+ /**
+ * Retrieves the remote image.
+ *
+ * @since 2.4.0
+ *
+ * @param string $url The image URL.
+ * @param int $size The image size in pixels.
+ * @param string $mimetype The expected MIME type of the image data.
+ *
+ * @return string The image data (or an empty string on error).
+ */
+ public function get_image( $url, $size, $mimetype ) {
+ // Retrieve remote image.
+ $image = \wp_remote_retrieve_body(
+ \wp_remote_get( $url )
+ );
+
+ // Check if the image data is valid.
+ if ( false === $this->editor->get_mime_type( $image ) ) {
+ // Something went wrong, so we ignore the data.
+ return '';
+ }
+
+ // Resize and convert image.
+ return $this->editor->get_resized_image_data(
+ $this->editor->create_from_string( $image ), $size, $size, $mimetype
+ );
+ }
+
+ /**
+ * Checks that the given string is a valid image URL.
+ *
+ * @since 2.3.4
+ *
+ * @param string $maybe_url Possibly an image URL.
+ * @param string $context The URL context (e.g. `'default_icon'` or `'avatar'`).
+ *
+ * @return bool
+ */
+ public function validate_image_url( $maybe_url, $context ) {
+ /**
+ * Filters whether remote default icon URLs (i.e. having a different domain) are allowed.
+ *
+ * @since 2.3.4
+ *
+ * @param bool $allow Default false.
+ */
+ $allow_remote = \apply_filters( "avatar_privacy_allow_remote_{$context}_url", false );
+
+ // Get current site domain part (without schema).
+ $domain = \wp_parse_url( \get_site_url(), \PHP_URL_HOST );
+
+ // Make sure URL is valid and local (unless $allow_remote is set to true).
+ $result =
+ \filter_var( $maybe_url, \FILTER_VALIDATE_URL, \FILTER_FLAG_PATH_REQUIRED ) &&
+ ( $allow_remote || \wp_parse_url( $maybe_url, \PHP_URL_HOST ) === $domain );
+
+ /**
+ * Filters the result of checking whether the candidate URL is a valid image URL.
+ *
+ * @since 2.3.4
+ *
+ * @param bool $result The validation result.
+ * @param string $maybe_url The candidate URL.
+ * @param bool $allow_remote Whether URLs from other domains should be allowed.
+ */
+ return \apply_filters( "avatar_privacy_validate_{$context}_url", $result, $maybe_url, $allow_remote );
+ }
+
+ /**
+ * Retrieves the hash for the given image URL. This method ensures that a reverse
+ * lookup using is possible by storing the URL and the hash in a database table.
+ *
+ * @since 2.4.0
+ *
+ * @param string $url The remote image URL.
+ *
+ * @return string
+ */
+ public function get_hash( $url ) {
+ // Generate hash.
+ $hash = $this->hasher->get_hash( $url );
+
+ // Check cache.
+ $key = "image-url_{$hash}";
+ if ( $url !== $this->cache->get( $key ) ) {
+ // OK, we need to update the database.
+ $data = [
+ 'identifier' => $url,
+ 'hash' => $hash,
+ 'type' => self::IDENTIFIER_TYPE,
+ ];
+ $this->table->insert_or_update_row( $data );
+
+ // Also prime the URL cache, just in case.
+ $this->cache->set( $key, $url, self::URL_CACHE_DURATION );
+ }
+
+ return $hash;
+ }
+
+ /**
+ * Retrieves the image URL for the given hash value.
+ *
+ * @since 2.4.0
+ *
+ * @global \wpdb $wpdb The WordPress Database Access Abstraction.
+ *
+ * @param string $hash The hashed URL.
+ *
+ * @return string|false
+ */
+ public function get_image_url( $hash ) {
+ global $wpdb;
+
+ // Check cache.
+ $key = "image-url_{$hash}";
+ $url = $this->cache->get( $key );
+
+ if ( false === $url ) {
+ // Lookup image URL.
+ $url = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ "SELECT identifier FROM `{$this->table->get_table_name()}` WHERE hash = %s AND type = %s",
+ $hash,
+ self::IDENTIFIER_TYPE
+ ) );
+
+ // Store only positive results.
+ if ( ! empty( $url ) ) {
+ $this->cache->set( $key, $url, self::URL_CACHE_DURATION );
+ }
+ }
+
+ return $url;
+ }
+}
diff --git a/includes/avatar-privacy/upload-handlers/class-custom-default-icon-upload-handler.php b/includes/avatar-privacy/upload-handlers/class-custom-default-icon-upload-handler.php
new file mode 100644
index 0000000..7ca98cb
--- /dev/null
+++ b/includes/avatar-privacy/upload-handlers/class-custom-default-icon-upload-handler.php
@@ -0,0 +1,300 @@
+
+ *
+ * @phpstan-import-type UploadArgs from Upload_Handler
+ * @phpstan-import-type FileSlice from Image_File
+ * @phpstan-import-type HandleUploadSuccess from Image_File
+ * @phpstan-import-type HandleUploadError from Image_File
+ *
+ * @phpstan-type UploadArgsWithSiteID UploadArgs&array{site_id:int}
+ */
+class Custom_Default_Icon_Upload_Handler extends Upload_Handler {
+
+ /**
+ * The nonce action for updating custom default icons.
+ */
+ const ACTION_UPLOAD = 'avatar_privacy_upload_default_icon';
+
+ /**
+ * The nonce used for updating custom default icons.
+ */
+ const NONCE_UPLOAD = 'avatar_privacy_upload_default_icon_nonce_';
+
+ const CHECKBOX_ERASE = 'avatar-privacy-custom-default-icon-erase';
+ const FILE_UPLOAD = 'avatar-privacy-custom-default-icon-upload';
+
+ const UPLOAD_DIR = '/avatar-privacy/custom-default';
+
+ const ERROR_FILE = 'default_avatar_file_error';
+ const ERROR_INVALID_IMAGE = 'default_avatar_invalid_image_type';
+ const ERROR_OTHER = 'default_avatar_other_error';
+ const ERROR_UNKNOWN = 'default_avatar_unknown_error';
+
+ /**
+ * The default avatars API.
+ *
+ * @since 2.4.0
+ *
+ * @var Default_Avatars
+ */
+ private Default_Avatars $default_avatars;
+
+ /**
+ * The options handler.
+ *
+ * @var Options
+ */
+ private Options $options;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.1.0 Parameter $plugin_file removed.
+ * @since 2.4.0 Parameters $core and $file_cache removed, parameters $image_file,
+ * and $default_avatars added.
+ *
+ * @param Image_File $image_file The image file handler.
+ * @param Default_Avatars $default_avatars The default avatars API.
+ * @param Options $options The options handler.
+ */
+ public function __construct( Image_File $image_file, Default_Avatars $default_avatars, Options $options ) {
+ parent::__construct( self::UPLOAD_DIR, $image_file );
+
+ $this->default_avatars = $default_avatars;
+ $this->options = $options;
+ }
+
+ /**
+ * Stores the uploaded default icon in the proper directory.
+ *
+ * @global array $_POST Post request superglobal.
+ * @global array $_FILES Uploaded files superglobal.
+ *
+ * @param int $site_id A site ID.
+ * @param string|string[] $option_value The option value. Passed by reference.
+ *
+ * @return void
+ */
+ public function save_uploaded_default_icon( $site_id, &$option_value ) {
+ // Prepare arguments.
+ $args = [
+ 'nonce' => self::NONCE_UPLOAD . $site_id,
+ 'action' => self::ACTION_UPLOAD,
+ 'upload_field' => Settings::UPLOAD_CUSTOM_DEFAULT_AVATAR,
+ 'erase_field' => self::CHECKBOX_ERASE,
+ 'site_id' => $site_id,
+ 'option_value' => &$option_value,
+ ];
+
+ $this->maybe_save_data( $args );
+ }
+
+ /**
+ * Retrieves the relevant slice of the global $_FILES array.
+ *
+ * @since 2.4.0
+ *
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @return array A slice of the $_FILES array.
+ *
+ * @phpstan-param UploadArgs $args
+ * @phpstan-return FileSlice|array{}
+ */
+ protected function get_file_slice( array $args ) {
+ $upload_index = $this->options->get_name( Settings::OPTION_NAME );
+
+ // @phpstan-ignore-next-line -- super globals are all array.
+ if ( ! empty( $_FILES[ $upload_index ]['name'] ) ) {
+ // @phpstan-ignore-next-line -- super globals are all array.
+ $normalized_files = $this->normalize_files_array( $_FILES[ $upload_index ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- $_FILES does not need wp_unslash.
+
+ if ( ! empty( $normalized_files[ $args['upload_field'] ] ) ) {
+ return $normalized_files[ $args['upload_field'] ];
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Handles upload errors and prints appropriate notices.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ * @since 2.4.0 Renamed to handle_upload_errors, parameter $result renamed
+ * to $upload_result. Parameter $args added.
+ *
+ * @param array $upload_result The result of ::handle_upload().
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @phpstan-param HandleUploadSuccess|HandleUploadError $upload_result
+ * @phpstan-param UploadArgsWithSiteID $args
+ */
+ protected function handle_upload_errors( array $upload_result, array $args ) {
+ if ( empty( $upload_result['error'] ) ) {
+ $id = self::ERROR_UNKNOWN;
+ $message = \__( 'An unknown error occured while uploading the avatar', 'avatar-privacy' );
+ } elseif ( 'Sorry, this file type is not permitted for security reasons.' === $upload_result['error'] ) {
+ $id = self::ERROR_INVALID_IMAGE;
+ $message = \__( 'Please upload a valid PNG, GIF or JPEG image for the avatar.', 'avatar-privacy' );
+ } else {
+ $id = self::ERROR_OTHER;
+ $message = \sprintf( '%s %s', \__( 'There was an error uploading the avatar:', 'avatar-privacy' ), \esc_attr( $upload_result['error'] ) );
+ }
+
+ $this->raise_settings_error( $id, $message );
+ }
+
+ /**
+ * Stores metadata about the uploaded file.
+ *
+ * @since 2.4.0
+ *
+ * @param array $upload_result The result of ::handle_upload().
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @phpstan-param HandleUploadSuccess|HandleUploadError $upload_result
+ * @phpstan-param UploadArgsWithSiteID $args
+ */
+ protected function store_file_data( array $upload_result, array $args ) {
+ // Delete previous image and thumbnails.
+ if ( $this->delete_uploaded_icon( $args['site_id'] ) ) {
+ // Store new option value.
+ $args['option_value'] = $upload_result;
+ } else {
+ // There was an error deleting the previous image file.
+ $this->handle_file_delete_error();
+ }
+ }
+
+ /**
+ * Deletes a previously uploaded file and its metadata.
+ *
+ * @since 2.4.0
+ *
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @phpstan-param UploadArgsWithSiteID $args
+ */
+ protected function delete_file_data( array $args ) {
+ // Delete previous image and thumbnails.
+ if ( $this->delete_uploaded_icon( $args['site_id'] ) ) {
+ // Store new option value.
+ $args['option_value'] = [];
+ } else {
+ // There was an error deleting the previous image file.
+ $this->handle_file_delete_error();
+ }
+ }
+
+ /**
+ * Retrieves the filename to use.
+ *
+ * @since 2.4.0
+ *
+ * @param string $filename The proposed filename.
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @return string
+ *
+ * @phpstan-param UploadArgsWithSiteID $args
+ */
+ protected function get_filename( $filename, array $args ) {
+ return $this->default_avatars->get_custom_default_avatar_filename( $filename );
+ }
+
+ /**
+ * Delete the uploaded avatar (including all cached size variants) for the given site.
+ *
+ * @param int $site_id The site ID.
+ *
+ * @return bool
+ */
+ public function delete_uploaded_icon( $site_id ) {
+ if ( $this->default_avatars->delete_custom_default_avatar_image_file() ) {
+ $this->default_avatars->invalidate_custom_default_avatar_cache( $site_id );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Raises an error on the settings page.
+ *
+ * @since 2.4.0
+ *
+ * @internal
+ *
+ * @param string $id The error ID.
+ * @param string $message The error message.
+ * @param string $type Optional. The error type. Default 'error'.
+ *
+ * @return void
+ */
+ protected function raise_settings_error( $id, $message, $type = 'error' ) {
+ \add_settings_error(
+ $this->options->get_name( Settings::OPTION_NAME ) . '[' . Settings::UPLOAD_CUSTOM_DEFAULT_AVATAR . ']',
+ $id,
+ $message,
+ $type
+ );
+ }
+
+ /**
+ * Handles errors during file deletion.
+ *
+ * @since 2.4.0
+ *
+ * @internal
+ *
+ * @return void
+ */
+ protected function handle_file_delete_error() {
+ $icon = $this->default_avatars->get_custom_default_avatar();
+ $file = ! empty( $icon['file'] ) ? $icon['file'] : \__( 'No filename given.', 'avatar-privacy' );
+ $this->raise_settings_error( self::ERROR_FILE, \sprintf( '%s %s', \__( 'Could not delete avatar image file:', 'avatar-privacy' ), \esc_attr( $file ) ) );
+ }
+}
diff --git a/includes/avatar-privacy/upload-handlers/class-upload-handler.php b/includes/avatar-privacy/upload-handlers/class-upload-handler.php
new file mode 100644
index 0000000..338ec17
--- /dev/null
+++ b/includes/avatar-privacy/upload-handlers/class-upload-handler.php
@@ -0,0 +1,366 @@
+
+ *
+ * @phpstan-import-type UploadDir from \Avatar_Privacy\Data_Storage\Filesystem_Cache
+ * @phpstan-import-type FileSlice from Image_File
+ * @phpstan-import-type HandleUploadSuccess from Image_File
+ * @phpstan-import-type HandleUploadError from Image_File
+ *
+ * @phpstan-type UploadArgs array{ nonce: string, action: string, upload_field: string, erase_field: string, user_id?: int, site_id?: int }
+ * @phpstan-type FileSliceMulti array{ name: string[], type: string[], tmp_name: string[], error: int[], size: int[] }
+ */
+abstract class Upload_Handler {
+
+ /**
+ * A mapping of file extension patterns to MIME types.
+ *
+ * @deprecated 2.4.0
+ *
+ * @var string[]
+ */
+ const ALLOWED_MIME_TYPES = Image_File::ALLOWED_UPLOAD_MIME_TYPES;
+
+ /**
+ * The subfolder used for our uploaded files. Has to start with /.
+ *
+ * @var string
+ */
+ private string $upload_dir;
+
+ /**
+ * The image file handler.
+ *
+ * @since 2.4.0
+ *
+ * @var Image_File
+ */
+ private Image_File $image_file;
+
+ /**
+ * Whether to use the global upload directory.
+ *
+ * @since 2.4.0
+ *
+ * @var bool
+ */
+ private bool $global_upload;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.1.0 Parameter $plugin_file removed.
+ * @since 2.4.0 Parameters $core and $file_cache removed, parameters
+ * $image_File and $global_upload added.
+ *
+ * @param string $upload_dir The subfolder used for our uploaded files.
+ * Has to start with /.
+ * @param Image_File $image_file The image file handler.
+ * @param bool $global_upload Optional. A flag indicating whether there
+ * should be a global upload directory on
+ * multisite. Default false.
+ */
+ public function __construct( $upload_dir, Image_File $image_file, $global_upload = false ) {
+ $this->upload_dir = $upload_dir;
+ $this->image_file = $image_file;
+ $this->global_upload = $global_upload;
+ }
+
+ /**
+ * Returns a unique filename.
+ *
+ * @deprecated 2.4.0
+ *
+ * @param string $directory The uploads directory.
+ * @param string $filename The proposed filename.
+ * @param string $extension The file extension (including leading dot).
+ *
+ * @return string
+ */
+ public function get_unique_filename( $directory, $filename, $extension ) {
+ $number = 1;
+ $basename = \basename( $filename, $extension );
+ $filename = $basename;
+
+ while ( \file_exists( "$directory/{$filename}{$extension}" ) ) {
+ $filename = "{$basename}_{$number}";
+ ++$number;
+ }
+
+ return "{$filename}{$extension}";
+ }
+
+ /**
+ * Processes the submitted form and saves the upload results if possible.
+ *
+ * @since 2.4.0
+ *
+ * @global array $_POST Post request superglobal.
+ *
+ * @param array $args {
+ * An array of arguments passed to the form processing methods. All of
+ * the listed arguments are required.
+ *
+ * @type string $nonce The nonce.
+ * @type string $action The form action.
+ * @type string $upload_field The upload field name.
+ * @type string $erase_field The erase checkbox field name.
+ * }
+ *
+ * @return void
+ *
+ * @phpstan-param UploadArgs $args
+ */
+ protected function maybe_save_data( array $args ) {
+ // Check arguments.
+ if ( empty( $args['nonce'] ) || empty( $args['action'] ) || empty( $args['upload_field'] ) || empty( $args['erase_field'] ) ) {
+ \_doing_it_wrong( __METHOD__, 'Required arguments missing', 'Avatar Privacy 2.4.0' );
+ return;
+ }
+
+ // Verify nonce.
+ // @phpstan-ignore-next-line -- super globals are all array.
+ if ( ! isset( $_POST[ $args['nonce'] ] ) || ! \wp_verify_nonce( \sanitize_key( $_POST[ $args['nonce'] ] ), $args['action'] ) ) {
+ return;
+ }
+
+ // Verify a file was uploaded.
+ $file_slice = $this->get_file_slice( $args );
+ if ( ! empty( $file_slice['name'] ) ) {
+
+ // Upload to our custom directory.
+ $upload_result = $this->upload( $file_slice, $args );
+
+ // Handle upload failures.
+ if ( empty( $upload_result['file'] ) ) {
+ $this->handle_upload_errors( $upload_result, $args );
+ return; // Abort.
+ }
+
+ // Save the new avatar image.
+ $this->store_file_data( $upload_result, $args );
+ } elseif ( ! empty( $_POST[ $args['erase_field'] ] ) && 'true' === $_POST[ $args['erase_field'] ] ) {
+ // Just delete the current avatar.
+ $this->delete_file_data( $args );
+ }
+ }
+
+ /**
+ * Retrieves the relevant slice of the global $_FILES array.
+ *
+ * @since 2.4.0
+ *
+ * @global array $_FILES Uploaded files superglobal.
+ *
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @return array A slice of the $_FILES array.
+ *
+ * @phpstan-param UploadArgs $args
+ * @phpstan-return FileSlice|array{}
+ */
+ abstract protected function get_file_slice( array $args );
+
+ /**
+ * Handles upload errors and prints appropriate notices.
+ *
+ * @since 2.4.0
+ *
+ * @param array $upload_result The result of ::handle_upload().
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @return void
+ *
+ * @phpstan-param HandleUploadSuccess|HandleUploadError $upload_result
+ * @phpstan-param UploadArgs $args
+ */
+ abstract protected function handle_upload_errors( array $upload_result, array $args );
+
+ /**
+ * Stores metadata about the uploaded file.
+ *
+ * @since 2.4.0
+ *
+ * @param array $upload_result The result of ::handle_upload().
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @return void
+ *
+ * @phpstan-param HandleUploadSuccess|HandleUploadError $upload_result
+ * @phpstan-param UploadArgs $args
+ */
+ abstract protected function store_file_data( array $upload_result, array $args );
+
+ /**
+ * Deletes a previously uploaded file and its metadata.
+ *
+ * @since 2.4.0
+ *
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @return void
+ *
+ * @phpstan-param UploadArgs $args
+ */
+ abstract protected function delete_file_data( array $args );
+
+ /**
+ * Retrieves the filename to use.
+ *
+ * @since 2.4.0
+ *
+ * @param string $filename The proposed filename.
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @return string
+ *
+ * @phpstan-param UploadArgs $args
+ */
+ protected function get_filename( $filename, array $args ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Argument is used in subclasses.
+ return $filename;
+ }
+
+ /**
+ * Handles the file upload by optionally switching to the primary site of the network.
+ *
+ * @since 2.4.0 Parameter $global replaced with property.
+ *
+ * @param array $file A slice of the $_FILES superglobal.
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @return string[] Information about the uploaded file.
+ *
+ * @phpstan-param FileSlice $file
+ * @phpstan-param UploadArgs $args
+ * @phpstan-return HandleUploadSuccess|HandleUploadError
+ */
+ protected function upload( array $file, $args ) {
+ // Prepare arguments.
+ $overrides = [
+ 'global_upload' => $this->global_upload,
+ 'upload_dir' => $this->upload_dir,
+ ];
+
+ // Allow for subclass-specific filenames.
+ $file['name'] = $this->get_filename( $file['name'], $args );
+
+ return $this->image_file->handle_upload( $file, $overrides );
+ }
+
+ /**
+ * Returns a custom upload directory.
+ *
+ * @deprecated 2.4.0
+ *
+ * @param array $uploads The uploads data.
+ *
+ * @return array
+ *
+ * @phpstan-param UploadDir $uploads
+ * @phpstan-return UploadDir
+ */
+ public function custom_upload_dir( array $uploads ) {
+ \_deprecated_function( __METHOD__, 'Avatar Privacy 2.4.0' );
+
+ $uploads['path'] = \str_replace( $uploads['subdir'], $this->upload_dir, $uploads['path'] );
+ $uploads['url'] = \str_replace( $uploads['subdir'], $this->upload_dir, $uploads['url'] );
+ $uploads['subdir'] = $this->upload_dir;
+
+ return $uploads;
+ }
+
+ /**
+ * Normalizes the sliced $_FILES to be an array indexed by file handle/number.
+ *
+ * This functions assumes a single file or at most a two-dimensional array of files.
+ * Higher dimensional arrays are not supported.
+ *
+ * @since 4.4.0 Input assumptions/limitations documented.
+ *
+ * @param array $files_slice A slice of the $_FILES superglobal.
+ * @return array {
+ * An array containing one sub-array for each uploaded file.
+ *
+ * @type array $file {
+ * An array of properties for an uploaded file.
+ *
+ * @type string $name The name of the uploaded file.
+ * @type string $type The MIME type.
+ * @type string tmp_name The path of the temporary file created by the upload.
+ * @type int $error The error code for the upload.
+ * @type int $size The file size in bytes.
+ * }
+ * }
+ *
+ * @phpstan-param FileSlice|FileSliceMulti $files_slice
+ * @phpstan-return array
+ */
+ protected function normalize_files_array( array $files_slice ) {
+ if ( ! \is_array( $files_slice['name'] ) ) {
+ return [ $files_slice ];
+ }
+
+ /**
+ * The file properties.
+ *
+ * @var string[] $props
+ */
+ $props = \array_keys( $files_slice );
+
+ /**
+ * The file numbers.
+ *
+ * @var int[] $files
+ */
+ $files = \array_keys( $files_slice['name'] );
+
+ // Assemble the properties into a normalized slize per file.
+ $normalized_slice = [];
+ foreach ( $files as $file ) {
+ foreach ( $props as $property ) {
+ $normalized_slice[ $file ][ $property ] = $files_slice[ $property ][ $file ];
+ }
+ }
+
+ /**
+ * The normalized $_FILES array.
+ *
+ * @phpstan-var array
+ */
+ return $normalized_slice;
+ }
+}
diff --git a/includes/avatar-privacy/upload-handlers/class-user-avatar-upload-handler.php b/includes/avatar-privacy/upload-handlers/class-user-avatar-upload-handler.php
new file mode 100644
index 0000000..4857d98
--- /dev/null
+++ b/includes/avatar-privacy/upload-handlers/class-user-avatar-upload-handler.php
@@ -0,0 +1,257 @@
+
+ *
+ * @phpstan-import-type UploadArgs from Upload_Handler
+ * @phpstan-import-type FileSlice from Image_File
+ * @phpstan-import-type HandleUploadSuccess from Image_File
+ * @phpstan-import-type HandleUploadError from Image_File
+ *
+ * @phpstan-type UploadArgsWithUserID UploadArgs&array{user_id:int}
+ */
+class User_Avatar_Upload_Handler extends Upload_Handler {
+
+ const UPLOAD_DIR = '/avatar-privacy/user-avatar';
+
+ /**
+ * The user fields API.
+ *
+ * @since 2.4.0
+ *
+ * @var User_Fields
+ */
+ private User_Fields $registered_user;
+
+ /**
+ * Creates a new instance.
+ *
+ * @since 2.1.0 Parameter $plugin_file removed.
+ * @since 2.4.0 Parameters $core and $file_cache removed, parameters $image_file
+ * and $registered_user added.
+ *
+ * @param Image_File $image_file The image file handler.
+ * @param User_Fields $registered_user The user fields API.
+ */
+ public function __construct( Image_File $image_file, User_Fields $registered_user ) {
+ parent::__construct( self::UPLOAD_DIR, $image_file, true );
+
+ $this->registered_user = $registered_user;
+ }
+
+ /**
+ * Retrieves the markup for uploading user avatars.
+ *
+ * @deprecated 2.4.0
+ *
+ * @param \WP_User $user The profile user.
+ *
+ * @return string
+ */
+ public function get_avatar_upload_markup( \WP_User $user ) {
+ \_deprecated_function( __METHOD__, 'Avatar Privacy 2.4.0' );
+
+ \ob_start();
+ require \AVATAR_PRIVACY_PLUGIN_PATH . '/admin/partials/profile/user-avatar-upload.php';
+ return (string) \ob_get_clean();
+ }
+
+ /**
+ * Stores the uploaded avatar image in the proper directory.
+ *
+ * @global array $_POST Post request superglobal.
+ * @global array $_FILES Uploaded files superglobal.
+ *
+ * @param int $user_id The user ID.
+ * @param string $nonce The nonce root required for saving the field
+ * (the user ID will be automatically appended).
+ * @param string $action The action required for saving the field.
+ * @param string $upload_field The HTML name of the "upload" field.
+ * @param string $erase_field The HTML name of the "erase" checkbox.
+ *
+ * @return void
+ */
+ public function save_uploaded_user_avatar( $user_id, $nonce, $action, $upload_field, $erase_field ) {
+ // Prepare arguments.
+ $args = [
+ 'nonce' => "{$nonce}{$user_id}",
+ 'action' => $action,
+ 'upload_field' => $upload_field,
+ 'erase_field' => $erase_field,
+ 'user_id' => $user_id,
+ ];
+
+ $this->maybe_save_data( $args );
+ }
+
+ /**
+ * Retrieves the relevant slice of the global $_FILES array.
+ *
+ * @since 2.4.0
+ *
+ * @global array $_FILES Uploaded files superglobal.
+ *
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @return array A slice of the $_FILES array.
+ *
+ * @phpstan-param UploadArgs $args
+ * @phpstan-return FileSlice|array{}
+ */
+ protected function get_file_slice( array $args ) {
+ if ( ! empty( $_FILES[ $args['upload_field'] ] ) ) {
+ return (array) $_FILES[ $args['upload_field'] ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- $_FILES does not need wp_unslash.
+ }
+
+ return [];
+ }
+
+ /**
+ * Handles upload errors and prints appropriate notices.
+ *
+ * @since 2.1.0 Visibility changed to protected.
+ * @since 2.4.0 Renamed to handle_upload_errors, parameter $result renamed
+ * to $upload_result. Parameter $args added.
+ *
+ * @param array $upload_result The result of ::upload().
+ * @param mixed[] $args Arguments passed from ::maybe_save_data().
+ *
+ * @return void
+ *
+ * @phpstan-param array{ error?: string } $upload_result
+ */
+ protected function handle_upload_errors( array $upload_result, array $args ) {
+ if ( empty( $upload_result['error'] ) ) {
+ $error_message = \__( 'An unknown error occured while uploading the avatar', 'avatar-privacy' );
+ } elseif ( 'Sorry, this file type is not permitted for security reasons.' === $upload_result['error'] ) {
+ $error_message = \__( 'Please upload a valid PNG, GIF or JPEG image for the avatar.', 'avatar-privacy' );
+ } else {
+ $error_message = \sprintf( '%s %s', \__( 'There was an error uploading the avatar:', 'avatar-privacy' ), \esc_attr( $upload_result['error'] ) );
+ }
+
+ \add_action( 'user_profile_update_errors', function( \WP_Error $errors ) use ( $error_message ) {
+ $errors->add( 'avatar_error', $error_message ); // @codeCoverageIgnore
+ } );
+ }
+
+ /**
+ * Stores metadata about the uploaded file.
+ *
+ * @since 2.4.0
+ *
+ * @param mixed[] $upload_result The result of ::upload().
+ * @param mixed[] $args Arguments passed from ::maybe_save_data().
+ *
+ * @return void
+ *
+ * @phpstan-param HandleUploadSuccess|HandleUploadError $upload_result
+ * @phpstan-param UploadArgsWithUserID $args
+ */
+ protected function store_file_data( array $upload_result, array $args ) {
+ $this->registered_user->set_uploaded_local_avatar( $args['user_id'], $upload_result );
+ }
+
+ /**
+ * Deletes a previously uploaded file and its metadata.
+ *
+ * @since 2.4.0
+ *
+ * @param mixed[] $args Arguments passed from ::maybe_save_data().
+ *
+ * @return void
+ *
+ * @phpstan-param UploadArgsWithUserID $args
+ */
+ protected function delete_file_data( array $args ) {
+ $this->registered_user->delete_local_avatar( $args['user_id'] );
+ }
+
+ /**
+ * Retrieves the filename to use.
+ *
+ * @since 2.4.0
+ *
+ * @param string $filename The proposed filename.
+ * @param array $args Arguments passed from ::maybe_save_data().
+ *
+ * @return string
+ *
+ * @phpstan-param UploadArgsWithUserID $args
+ */
+ protected function get_filename( $filename, array $args ) {
+ return $this->registered_user->get_local_avatar_filename( $args['user_id'], $filename );
+ }
+
+ /**
+ * Delete the uploaded avatar (including all cached size variants) for the given user.
+ *
+ * @deprecated 2.4.0 Use \Avatar_Privacy\Core:delete_user_avatar instead.
+ *
+ * @param int $user_id The user ID.
+ *
+ * @return void
+ */
+ public function delete_uploaded_avatar( $user_id ) {
+ \_deprecated_function( __METHOD__, 'Avatar Privacy 2.4.0', 'Avatar_Privacy\Core:delete_user_avatar' );
+
+ $this->registered_user->delete_local_avatar( $user_id );
+ }
+
+ /**
+ * Invalidates cached avatar images.
+ *
+ * @since 2.2.0
+ *
+ * @deprecated 2.4.0 Use \Avatar_Privacy\Core::invalidate_user_avatar_cache
+ * instead.
+ *
+ * @param int $user_id The user ID.
+ *
+ * @return void
+ */
+ public function invalidate_user_avatar_cache( $user_id ) {
+ \_deprecated_function( __METHOD__, 'Avatar Privacy 2.4.0', 'Avatar_Privacy\Core::invalidate_user_avatar_cache' );
+
+ $this->registered_user->invalidate_local_avatar_cache( $user_id );
+ }
+}
diff --git a/includes/avatar-privacy/upload-handlers/ui/class-file-upload-input.php b/includes/avatar-privacy/upload-handlers/ui/class-file-upload-input.php
new file mode 100644
index 0000000..ad9d729
--- /dev/null
+++ b/includes/avatar-privacy/upload-handlers/ui/class-file-upload-input.php
@@ -0,0 +1,139 @@
+ element for file uploads.
+ *
+ * @since 2.0.0
+ *
+ * @author Peter Putzer
+ */
+class File_Upload_Input extends Controls\Input {
+
+ /**
+ * The HTML ID of the erase image checkbox.
+ *
+ * @var string
+ */
+ private string $erase_checkbox_id;
+
+ /**
+ * The nonce prefix for the upload.
+ *
+ * @var string
+ */
+ private string $nonce;
+
+ /**
+ * The action ID for the upload.
+ *
+ * @var string
+ */
+ private string $action;
+
+ /**
+ * Create a new input control object.
+ *
+ * @param Options $options Options API handler.
+ * @param string $options_key Database key for the options array.
+ * @param string $id Control ID (equivalent to option name). Required.
+ * @param array $args {
+ * Optional and required arguments.
+ *
+ * @type string $tab_id Tab ID. Required.
+ * @type string $section Section ID. Required.
+ * @type string|int $default The default value. Required, but may be an empty string.
+ * @type string|null $short Optional. Short label. Default null.
+ * @type string|null $label Optional. Label content with the position of the control marked as %1$s. Default null.
+ * @type string|null $help_text Optional. Help text. Default null.
+ * @type bool $inline_help Optional. Display help inline. Default false.
+ * @type array $attributes Optional. Default [],
+ * @type array $outer_attributes Optional. Default [],
+ * @type array $settings_args Optional. Default [],
+ * @type string $erase_checkbox Erase image checkbox ID.
+ * }
+ *
+ * @throws \InvalidArgumentException Missing argument.
+ *
+ * @phpstan-param array{ tab_id: string, section: string, default: string|int, short?: ?string, label?: ?string, help_text?: ?string, inline_help?: bool, attributes?: mixed[], outer_attributes?: mixed[], settings_args?: mixed[], erase_checkbox?: string } $args
+ */
+ public function __construct( Options $options, $options_key, $id, array $args ) {
+ $args = $this->prepare_args( $args, [ 'erase_checkbox', 'action', 'nonce', 'help_no_file', 'help_no_upload' ] );
+ $args['input_type'] = 'file';
+
+ parent::__construct( $options, $options_key, $id, $args );
+
+ if ( \current_user_can( 'upload_files' ) ) {
+ $value = $this->get_value();
+ if ( empty( $value ) ) {
+ $this->help_text = $args['help_no_file'];
+ }
+ } else {
+ $this->help_text = $args['help_no_upload'];
+ }
+
+ $this->erase_checkbox_id = \esc_attr( $args['erase_checkbox'] );
+ $this->action = \esc_attr( $args['action'] );
+ $this->nonce = \esc_attr( $args['nonce'] );
+ }
+
+ /**
+ * Render the value markup for this input.
+ *
+ * @param mixed $value The input value.
+ *
+ * @return string
+ */
+ protected function get_value_markup( $value ) {
+ // Don't display file names.
+ return 'value="" ';
+ }
+
+ /**
+ * Retrieves the control-specific HTML markup.
+ *
+ * @return string
+ */
+ protected function get_element_markup() {
+ $value = $this->get_value();
+ $checkbox_markup = '';
+ $nonce_markup = \wp_nonce_field( $this->action, $this->nonce . \get_current_blog_id(), true, false );
+
+ if ( ! empty( $value ) ) {
+ $checkbox_markup =
+ " erase_checkbox_id}\" name=\"{$this->erase_checkbox_id}\" value=\"true\" type=\"checkbox\">
+ erase_checkbox_id}\">" . \__( 'Delete custom default avatar.', 'avatar-privacy' ) . ' ';
+ }
+
+ return $nonce_markup . parent::get_element_markup() . $checkbox_markup;
+ }
+}
diff --git a/public/images/LICENSE.md b/public/images/LICENSE.md
new file mode 100644
index 0000000..ea63852
--- /dev/null
+++ b/public/images/LICENSE.md
@@ -0,0 +1,82 @@
+# Image License Terms
+
+The icons in this folder and its subfolders have been released under various free licenses. See below for details.
+
+## comment-bubble.svg
+
+Re-drawn in Illustrator, based on `comment.png` from the NDD Icon Set 1.0.
+
+Copyright: Fabian Volme (2007), Peter Putzer (2018)\
+URL: (archived)\
+License: LGPLv3\
+License URL:
+
+## mystery.svg
+
+Drawn from scratch in Illustrator, too simple for copyright protection.
+
+License: CC0\
+License URL:
+
+## shaded-cone.svg and silhouette.svg
+
+Taken from the Oxygen Icon theme 4.8.0 provided by KDE (`im-user-offline.svgz` and `view-media-artist.svgz`).
+
+Copyright: Nuno Pinheiro (2007), David Vignoni (2007), David Miller (2007), Johann Ollivier Lapeyre (2007), Kenneth Wimer (2007), Riccardo Iaconelli (2007)\
+URL: \
+License: LGPLv3\
+License URL:
+
+## birds/*.png
+
+The Bird Avatars implementation is based on the original package by David Revoy.
+
+Copyright: David Revoy (2019)\
+URL: \
+License: CC-BY 4.0 (attribution _David Revoy_), with the following exception:
+"Generated pictures used as Avatar (for blog,forum,social-network) don't need
+direct attribution and so, can be used as regular avatars without pasting David
+Revoy's name all over the place."\
+License URL:
+
+## cats/*.png
+
+The Cat Avatars implementation is based on the original package by David Revoy.
+
+Copyright: David Revoy (2016)\
+URL: \
+License: CC-BY 4.0 (attribution _David Revoy_), with the following exception:
+"Generated cats used as Avatar (for blog,forum,social-network) don't need direct
+attribution and so, can be used as regular avatars without pasting David Revoy's
+name all over the place."\
+License URL:
+
+## monster-id/*.png
+
+The MonsterID implementation is based on the hand-drawn icons provided in Scott Sherrill-Mix's WP_MonsterID plugin.
+
+Copyright: Katherine Garner (2008)\
+URL: \
+License: GPLv2 or later\
+License URL:
+
+## robohash/*.svg
+
+The RoboHash implementation is based on Nimiq's SVG version of the original images
+created by Zikri Kader for Colin Davis RoboHash.org project.
+
+Copyright: Zikri Kader, Colin Davis (2011), Nimiq (2017)\
+URL (PNG): \
+URL (SVG): \
+License: CC-BY 4.0 (attribution _Zikri Kader_)\
+License URL:
+
+
+## wavatars/*.png
+
+The Wavatars impelementation is based on the original WordPress plugin by Shamus Young.
+
+Copyright: Shames Young (2007)\
+URL:
+License: GPLv2 or later\
+License URL:
diff --git a/public/images/birds/accessoire_1.png b/public/images/birds/accessoire_1.png
new file mode 100644
index 0000000000000000000000000000000000000000..19501b6d2ca1d7a16e64ebb5040ee91c83023994
GIT binary patch
literal 1154
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
bopC~Ya&4CoCGE5?Na=2x0Zpq+JYxO>R39c!
literal 0
HcmV?d00001
diff --git a/public/images/birds/accessoire_11.png b/public/images/birds/accessoire_11.png
new file mode 100644
index 0000000000000000000000000000000000000000..11c8598b1f0d54b2922dd2b3e1a3399d2be6d009
GIT binary patch
literal 19617
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
z