commit 9caab8ce681c642320ebc57546418865ff6a6425 Author: Sophia Atkinson Date: Fri Sep 1 00:37:57 2023 -0700 first commit 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 @@ + +

+ +
+

+ +
+ + + +

+ 1 ] ); ?> +

+
+ +
+` element. + * @var string $value The checkbox value. + */ +?> + + + + + + /> +
+

+ +

+ + +` element. + * @var string $value The checkbox value. + */ +?> + + + + + + /> +
+

+ + +

+ + +` 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( '#(
)#Usi', '\1 enctype="multipart/form-data" \2', $content ); + } + + /** + * Adds a short explanation on the discussion settings page. + * + * @return string + */ + public function get_settings_header() { + // Set up variables used by the included partial. + $args = [ + 'show_avatars' => $this->options->get( 'show_avatars', false, true ), + ]; + + return $this->template->get_partial( 'admin/partials/sections/avatars-disabled.php', $args ) . + $this->template->get_partial( 'admin/partials/sections/avatars-enabled.php', $args ); + } + + /** + * Sanitize plugin settings array. + * + * @param array $input The plugin settings. + * + * @return array The sanitized plugin settings. + * + * @phpstan-param array, mixed> $input + * @phpstan-return SettingsFields + */ + public function sanitize_settings( $input ) { + foreach ( $this->settings->get_fields() as $key => $info ) { + if ( Controls\Checkbox_Input::class === $info['ui'] ) { + $input[ $key ] = ! empty( $input[ $key ] ); + } + } + + if ( ! isset( $input[ Settings::UPLOAD_CUSTOM_DEFAULT_AVATAR ] ) ) { + $input[ Settings::UPLOAD_CUSTOM_DEFAULT_AVATAR ] = $this->settings->get( Settings::UPLOAD_CUSTOM_DEFAULT_AVATAR ); + } + + $this->upload->save_uploaded_default_icon( \get_current_blog_id(), $input[ Settings::UPLOAD_CUSTOM_DEFAULT_AVATAR ] ); + + /** + * PHPStan type. + * + * @phpstan-var SettingsFields $input + */ + return $input; + } +} diff --git a/includes/avatar-privacy/components/class-setup.php b/includes/avatar-privacy/components/class-setup.php new file mode 100644 index 0000000..1f68dda --- /dev/null +++ b/includes/avatar-privacy/components/class-setup.php @@ -0,0 +1,485 @@ + + * + * @phpstan-import-type SettingsFields from \Avatar_Privacy\Core\Settings + * + * @phpstan-type ObsoleteSettingsFields array + */ +class Setup implements Component { + + /** + * Obsolete settings keys. + * + * @var string[] + */ + const OBSOLETE_SETTINGS = [ + 'mode_optin', + 'use_gravatar', + 'mode_checkforgravatar', + 'default_show', + 'checkbox_default', + ]; + + /** + * Obsolete avatar defaults and replacement values. + * + * @var string[] + */ + const OBSOLETE_AVATAR_DEFAULTS = [ + 'comment' => 'bubble', + 'im-user-offline' => 'bowling-pin', + 'view-media-artist' => 'silhouette', + ]; + + /** + * The options handler. + * + * @var Options + */ + private Options $options; + + /** + * The options handler. + * + * @var Network_Options + */ + private Network_Options $network_options; + + /** + * The transients handler. + * + * @var Transients + */ + private Transients $transients; + + /** + * The site transients handler. + * + * @var Site_Transients + */ + private Site_Transients $site_transients; + + /** + * The database table handlers. + * + * @var Table[] + */ + private array $tables; + + /** + * The multisite tools. + * + * @var Multisite + */ + private Multisite $multisite; + + /** + * The settings API. + * + * @since 2.4.0 + * + * @var Settings + */ + private Settings $settings; + + /** + * The user fields API. + * + * @since 2.4.0 + * + * @var User_Fields + */ + private User_Fields $registered_user; + + /** + * The comment author fields API. + * + * @since 2.4.0 + * + * @var Comment_Author_Fields + */ + private Comment_Author_Fields $comment_author; + + /** + * Creates a new Setup instance. + * + * @since 2.1.0 Parameter $plugin_file removed. + * @since 2.4.0 Parameters $settings, $registered_user, $comment_author, and + * $tables added, parameters $core and $database removed. + * + * @param Settings $settings The settings API. + * @param User_Fields $registered_user The user fields API. + * @param Comment_Author_Fields $comment_author The comment author fields API. + * @param Transients $transients The transients handler. + * @param Site_Transients $site_transients The site transients handler. + * @param Options $options The options handler. + * @param Network_Options $network_options The network options handler. + * @param Table[] $tables The database table handlers, indexed by their base name. + * @param Multisite $multisite The the multisite handler. + */ + public function __construct( Settings $settings, User_Fields $registered_user, Comment_Author_Fields $comment_author, Transients $transients, Site_Transients $site_transients, Options $options, Network_Options $network_options, array $tables, Multisite $multisite ) { + $this->settings = $settings; + $this->registered_user = $registered_user; + $this->comment_author = $comment_author; + $this->transients = $transients; + $this->site_transients = $site_transients; + $this->options = $options; + $this->network_options = $network_options; + $this->tables = $tables; + $this->multisite = $multisite; + } + + /** + * Sets up the various hooks for the plugin component. + * + * @return void + */ + public function run() { + // Register deactivation hook. Activation is handled by the update check instead. + \register_deactivation_hook( \AVATAR_PRIVACY_PLUGIN_FILE, [ $this, 'deactivate' ] ); + + // Update settings and database if necessary. + \add_action( 'plugins_loaded', [ $this, 'update_check' ] ); + + // Clean up uploaded files after deleting the refering metadata. + \add_action( 'deleted_user_meta', [ $this->registered_user, 'remove_orphaned_local_avatar' ], 10, 4 ); + } + + /** + * Checks if the default settings or database schema need to be upgraded. + * + * @return void + */ + public function update_check() { + // Force reading the settings from the DB, but do not cache the result. + $current_settings = $this->settings->get_all_settings( true ); + + // We can ignore errors here, just carry on as if for a new installation. + if ( ! empty( $current_settings[ Options::INSTALLED_VERSION ] ) ) { + $installed_version = $current_settings[ Options::INSTALLED_VERSION ]; + } elseif ( ! empty( $current_settings ) && ! isset( $current_settings[ Options::INSTALLED_VERSION ] ) ) { + // Plugin releases before 1.0 did not store the installed version. + $installed_version = '0.4-or-earlier'; + } else { + // The plugins was not installed previously. + $installed_version = ''; + } + + // Check if the plugin data needs to be updated. + $version = $this->settings->get_version(); + $update_needed = $version !== $installed_version; + $new_install = empty( $installed_version ); + + if ( $update_needed ) { + if ( ! $new_install ) { + // Update plugin settings if necessary. + $current_settings = $this->update_settings( $installed_version, $current_settings ); + + // Preserve previous multisite behavior. + // This needs to happen before the database tables are set up. + if ( \is_multisite() && \version_compare( $installed_version, '0.5', '<' ) ) { + $this->network_options->set( Network_Options::USE_GLOBAL_TABLE, true ); + } + } + + // Clear transients. + $this->transients->invalidate(); + $this->site_transients->invalidate(); + + // To be safe, let's always flush the rewrite rules if there has been an update. + $this->flush_rewrite_rules_soon(); + } + + // Check if our database tables need to created or updated. + // This also sets up the `$wpdb->avatar_privacy*` properties, so we have + // to check on every page load. + foreach ( $this->tables as $table ) { + $table->setup( $installed_version ); + } + + // Run additional upgrade routines now that the tables are set up, but + // only if this plugin has been previously installed. + if ( $update_needed && ! $new_install ) { + $this->update_plugin_data( $installed_version ); + } + + // Update installed version. + $current_settings[ Options::INSTALLED_VERSION ] = $version; + $this->options->set( Settings::OPTION_NAME, $current_settings ); + } + + /** + * Updates the plugin settings. + * + * @since 2.1.0 Visibility changed to protected. + * @since 2.4.0 Renamed to update_settings. Parameter $settings passed by value + * and returned as the result of of the function. + * + * @param string $previous_version The version we are upgrading from. + * @param array $settings The settings array. + * + * @return array The updated settings array. + * + * @phpstan-param ObsoleteSettingsFields $settings + * @phpstan-return mixed[] + */ + protected function update_settings( $previous_version, array $settings ) { + // Upgrade from version 0.4 or lower. + if ( \version_compare( $previous_version, '0.5', '<' ) ) { + // Drop old settings. + foreach ( self::OBSOLETE_SETTINGS as $key ) { + unset( $settings[ $key ] ); + } + } + + return $settings; + } + + /** + * Upgrades existing plugin data. + * + * @since 2.4.0 + * + * @param string $previous_version The version we are upgrading from. + * + * @return void + */ + protected function update_plugin_data( $previous_version ) { + // Upgrade from version 0.4 or lower. + if ( \version_compare( $previous_version, '0.5', '<' ) ) { + $this->maybe_update_user_hashes(); + } + + // Upgrade from anything below 1.0-rc.1. + if ( \version_compare( $previous_version, '1.0-rc.1', '<' ) ) { + $this->upgrade_old_avatar_defaults(); + } + + // Upgrade from anything below 2.1.0-alpha.3. + if ( \version_compare( $previous_version, '2.1.0-alpha.3', '<' ) ) { + $this->prefix_usermeta_keys(); + } + + // Upgrade from anything below 2.4.0. + if ( \version_compare( $previous_version, '2.4.0', '<' ) ) { + $this->maybe_add_email_hashes(); + } + } + + /** + * Handles plugin deactivation. + * + * @since 2.1.0 Parameter `$network_wide` added. + * + * @param bool $network_wide A flag indicating if the plugin was network-activated. + * + * @return void + */ + public function deactivate( $network_wide ) { + if ( ! $network_wide ) { + // We've only been activated on this site, all good. + $this->deactivate_plugin(); + } elseif ( ! \wp_is_large_network() ) { + // This is a "small" multisite network, so get WordPress to rebuild the rewrite rules. + $this->multisite->do_for_all_sites_in_network( [ $this, 'deactivate_plugin' ] ); + } else { + // OK, let's try not to break anything. + $this->multisite->do_for_all_sites_in_network( + // We still need to disable our cron jobs, though. + function() { + \wp_unschedule_hook( Image_Proxy::CRON_JOB_ACTION ); + } + ); + } + } + + /** + * Triggers a rebuild of the rewrite rules on the next page load. + * + * @since 2.1.0 + * + * @return void + */ + public function flush_rewrite_rules_soon() { + // Deleting the option forces a rebuild in the proper context on the next load. + $this->options->delete( 'rewrite_rules', true ); + } + + /** + * Adds a prefix to the GRAVATAR_USE_META_KEY. + * + * This migration method will not work if the standard `wp_usermeta` table is + * replaced with something else, but there does not seem to be a good way to + * use the `get_user_metadata` filter hook and fulfill the goal of not breaking + * future Core use of `use_gravatar` as a meta key. + * + * @since 2.1.0 + * + * @global wpdb $wpdb The WordPress Database Access Abstraction. + * + * @return void + */ + public function prefix_usermeta_keys() { + global $wpdb; + + // Get all users with the `use_gravatar` meta key. + $affected_users = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s", 'use_gravatar' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + + if ( \count( $affected_users ) > 0 ) { + // Update the database table. + $rows = $wpdb->update( $wpdb->usermeta, [ 'meta_key' => User_Fields::GRAVATAR_USE_META_KEY ], [ 'meta_key' => 'use_gravatar' ] ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + + // If there were any keys to update, we also have to clear the user_meta cache group. + if ( false !== $rows && $rows > 0 ) { + + // Clear user_meta cache for all affected users. + foreach ( $affected_users as $user_id ) { + \wp_cache_delete( $user_id, 'user_meta' ); + } + } + } + } + + /** + * The deactivation tasks for a single site. + * + * @since 2.1.0 + * + * @return void + */ + public function deactivate_plugin() { + // Disable cron jobs. + \wp_unschedule_hook( Image_Proxy::CRON_JOB_ACTION ); + + // Reset avatar defaults. + $this->options->reset_avatar_default(); + + // Flush rewrite rules on next page load. + $this->flush_rewrite_rules_soon(); + } + + /** + * Tries to upgrade the `avatar_defaults` option. + * + * @since 2.1.0 Visibility changed to protected, $options parameter removed. + * + * @return void + */ + protected function upgrade_old_avatar_defaults() { + $obsolete_avatar_defaults = self::OBSOLETE_AVATAR_DEFAULTS; + $old_default = $this->options->get( 'avatar_default', 'mystery', true ); + + if ( ! empty( $obsolete_avatar_defaults[ $old_default ] ) ) { + $this->options->set( 'avatar_default', $obsolete_avatar_defaults[ $old_default ], true, true ); + } + } + + /** + * Updates user hashes where they don't exist yet. + * + * @since 2.1.0 Visibility changed to protected. + * + * @return void + */ + protected function maybe_update_user_hashes() { + $args = [ + 'meta_key' => User_Fields::EMAIL_HASH_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_compare' => 'NOT EXISTS', + ]; + + foreach ( \get_users( $args ) as $user ) { + // Ensure that there is a user hash - retrieving the hash updates the meta field. + $this->registered_user->get_hash( $user->ID ); + } + } + + /** + * Adds hashes for stored e-mail addresses if necessary. + * + * @since 2.4.0 + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @return int The number of upgraded rows. + */ + protected function maybe_add_email_hashes() { + global $wpdb; + + // Add hashes when they are missing. + $emails = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder -- DB and column name. + 'SELECT c.email FROM `%1$s` c LEFT OUTER JOIN `%2$s` h ON c.email = h.identifier AND h.type = "comment" AND h.hash IS NULL', + $wpdb->avatar_privacy, + $wpdb->avatar_privacy_hashes + ) + ); + $email_count = \count( $emails ); + + if ( $email_count > 0 ) { + // Add hashes for all retrieved rows. + $rows = []; + foreach ( $emails as $email ) { + $rows[] = [ + 'identifier' => $email, + 'hash' => $this->comment_author->get_hash( $email ), + 'type' => 'comment', + ]; + } + + return (int) $this->tables[ Hashes_Table::TABLE_BASENAME ]->insert_or_update( [ 'identifier', 'hash', 'type' ], $rows ); + } + + return 0; + } +} diff --git a/includes/avatar-privacy/components/class-shortcodes.php b/includes/avatar-privacy/components/class-shortcodes.php new file mode 100644 index 0000000..7ae3f74 --- /dev/null +++ b/includes/avatar-privacy/components/class-shortcodes.php @@ -0,0 +1,151 @@ + + * + * @phpstan-type FrontendFormAttributes array{ avatar_size?: int } + */ +class Shortcodes implements Component { + + /** + * The shortcode attributes for `[avatar-privacy-form]`. + * + * @var array + */ + const FRONTEND_FORM_ATTRIBUTES = [ + 'avatar_size' => 96, + ]; + + /** + * The profile form helper. + * + * @var User_Form + */ + private User_Form $form; + + /** + * Initialize the class and set its properties. + * + * @param User_Form $form The profile form helper. + */ + public function __construct( User_Form $form ) { + $this->form = $form; + } + + /** + * Sets up the various hooks for the plugin component. + * + * @return void + */ + public function run() { + // Initialize shortcodes after WordPress has loaded. + \add_action( 'init', [ $this, 'add_shortcodes' ] ); + + // Only process forms on the frontend. + if ( ! \is_admin() ) { + $this->form->register_form_submission(); + } + } + + /** + * Adds our shortcode and overrdies the WordPress caption shortcodes to allow nesting. + * + * @return void + */ + public function add_shortcodes() { + // Add new media credit shortcode. + \add_shortcode( 'avatar-privacy-form', [ $this, 'render_frontend_form_shortcode' ] ); + } + + /** + * Renders the frontend form shortcode to allow avatar uploads et al. + * + * Usage: `[avatar-privacy-form]` + * + * @since 2.4.0 Unused parameter $content removed. + * + * @param array $atts { + * An array of shortcode attributes. + * + * @type int $avatar_size Optional. The width/height of the avatar preview image (in pixels). Default 96. + * } + * + * @return string The HTML markup for the upload form. + * + * @phpstan-param FrontendFormAttributes $atts + */ + public function render_frontend_form_shortcode( $atts ) { + $user_id = \get_current_user_id(); + + // User not logged in. + if ( empty( $user_id ) ) { + return ''; + } + + // Set up variables used by the included partial. + $args = [ + // Make sure that $atts really is an array, might be an empty string in some edge cases. + 'atts' => $this->sanitize_frontend_form_attributes( empty( $atts ) ? [] : $atts ), + ]; + + // Include partials. + return $this->form->get_form( 'public/partials/shortcode/avatar-upload.php', $user_id, $args ); + } + + /** + * Ensures all required attributes are present and sanitized. + * + * @param array $atts { + * The `[avatar-privacy-form]` shortcode attributes. + * + * @type int $avatar_size Optional. The width/height of the avatar preview image (in pixels). Default 96. + * } + * + * @return array + * + * @phpstan-param FrontendFormAttributes $atts + * @phpstan-return FrontendFormAttributes + */ + protected function sanitize_frontend_form_attributes( array $atts ) { + // Merge default shortcode attributes. + $atts = \shortcode_atts( self::FRONTEND_FORM_ATTRIBUTES, $atts, 'avatar-privacy-form' ); + + // Sanitize attribute values. + $atts['avatar_size'] = \absint( $atts['avatar_size'] ); + + return $atts; + } +} diff --git a/includes/avatar-privacy/components/class-uninstallation.php b/includes/avatar-privacy/components/class-uninstallation.php new file mode 100644 index 0000000..e41da5f --- /dev/null +++ b/includes/avatar-privacy/components/class-uninstallation.php @@ -0,0 +1,288 @@ + + */ +class Uninstallation implements Component { + + /** + * The options handler. + * + * @var Options + */ + private $options; + + /** + * The options handler. + * + * @var Network_Options + */ + private $network_options; + + /** + * The transients handler. + * + * @var Transients + */ + private $transients; + + /** + * The site transients handler. + * + * @var Site_Transients + */ + private $site_transients; + + /** + * The DB handler. + * + * @var Database + */ + private $database; + + /** + * The filesystem cache handler. + * + * @var Filesystem_Cache + */ + private $file_cache; + + /** + * Creates a new Setup instance. + * + * @since 2.1.0 Parameter $plugin_file removed. + * + * @param Options $options The options handler. + * @param Network_Options $network_options The network options handler. + * @param Transients $transients The transients handler. + * @param Site_Transients $site_transients The site transients handler. + * @param Database $database The database handler. + * @param Filesystem_Cache $file_cache The filesystem cache handler. + */ + public function __construct( Options $options, Network_Options $network_options, Transients $transients, Site_Transients $site_transients, Database $database, Filesystem_Cache $file_cache ) { + $this->options = $options; + $this->network_options = $network_options; + $this->transients = $transients; + $this->site_transients = $site_transients; + $this->database = $database; + $this->file_cache = $file_cache; + } + + /** + * Sets up the various hooks for the plugin component. + * + * @return void + */ + public function run() { + // Enqueue necessary tasks. + $this->enqueue_cleanup_tasks(); + + // Clean up the site-specific artifacts. + $this->do_site_cleanups(); + + /** + * Cleans up any remaining global artifacts. + */ + \do_action( 'avatar_privacy_uninstallation_global' ); + } + + /** + * Enqeueus the necessary uninstallation tasks to the `avatar_privacy_uninstallation_site` + * and `avatar_privacy_uninstallation_global` actions. + * + * @since 2.3.0 + * + * @return void + */ + public function enqueue_cleanup_tasks() { + // Delete cached files. + \add_action( 'avatar_privacy_uninstallation_global', [ $this->file_cache, 'invalidate' ], 10, 0 ); + + // Delete uploaded user avatars. + \add_action( 'avatar_privacy_uninstallation_global', [ $this, 'delete_uploaded_avatars' ], 11, 0 ); + + // Delete usermeta for all users. + \add_action( 'avatar_privacy_uninstallation_global', [ $this, 'delete_user_meta' ], 12, 0 ); + + // Delete/change options (from all sites in case of a multisite network). + \add_action( 'avatar_privacy_uninstallation_site', [ $this, 'delete_options' ], 10, 0 ); + \add_action( 'avatar_privacy_uninstallation_global', [ $this, 'delete_network_options' ], 13, 0 ); + + // Delete transients from sitemeta or options table. + \add_action( 'avatar_privacy_uninstallation_site', [ $this, 'delete_transients' ], 11, 0 ); + \add_action( 'avatar_privacy_uninstallation_global', [ $this, 'delete_network_transients' ], 14, 0 ); + + // Drop all our tables. + \add_action( 'avatar_privacy_uninstallation_site', [ $this->database, 'drop_table' ], 12, 1 ); + } + + /** + * Executes all the registered site clean-ups (for all sites if on multisite). + * + * @since 2.1.0 + * + * @return void + */ + protected function do_site_cleanups() { + if ( \is_multisite() ) { + // We want all the sites across all networks. + $query = [ + 'fields' => 'ids', + 'number' => '', + ]; + foreach ( \get_sites( $query ) as $site_id ) { // @phpstan-ignore-line -- get_sites() always returns a list with the 'ids' argument. + \switch_to_blog( $site_id ); + + /** + * Do the registered site clean-ups for the current site. + * + * @param int|null $site_id Optional. The site (blog) ID or null if not a multisite installation. + */ + \do_action( 'avatar_privacy_uninstallation_site', $site_id ); + + \restore_current_blog(); + } + } else { + /** This action is documented in class-uninstallation.php */ + \do_action( 'avatar_privacy_uninstallation_site', null ); + } + } + + /** + * Deletes uploaded avatar images. + * + * @since 2.1.0 Visibility changed to public, made non-static. + * + * @return void + */ + public function delete_uploaded_avatars() { + $user_avatar = User_Fields::USER_AVATAR_META_KEY; + $query = [ + 'meta_key' => $user_avatar, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_compare' => 'EXISTS', + ]; + + foreach ( \get_users( $query ) as $user ) { + $avatar = $user->$user_avatar; + + if ( ! empty( $avatar['file'] ) && \file_exists( $avatar['file'] ) ) { + delete_file( $avatar['file'] ); + } + } + } + + /** + * Deletes all user meta data added by the plugin. + * + * @since 2.1.0 Visibility changed to public, made non-static. + * + * @return void + */ + public function delete_user_meta() { + \delete_metadata( 'user', 0, User_Fields::GRAVATAR_USE_META_KEY, null, true ); + \delete_metadata( 'user', 0, User_Fields::ALLOW_ANONYMOUS_META_KEY, null, true ); + \delete_metadata( 'user', 0, User_Fields::USER_AVATAR_META_KEY, null, true ); + \delete_metadata( 'user', 0, User_Fields::EMAIL_HASH_META_KEY, null, true ); + } + + /** + * Deletes the site-specific plugin options. + * + * @since 2.1.0 Visibility changed to public, made non-static. + * + * @return void + */ + public function delete_options() { + // Delete our settings. + $this->options->delete( Settings::OPTION_NAME ); + + // Reset avatar_default to working value if necessary. + $this->options->reset_avatar_default(); + } + + /** + * Deletes the global plugin options (except for the salt). + * + * @since 2.1.0 + * + * @return void + */ + public function delete_network_options() { + $this->network_options->delete( Network_Options::USE_GLOBAL_TABLE ); + $this->network_options->delete( Network_Options::GLOBAL_TABLE_MIGRATION ); + $this->network_options->delete( Network_Options::START_GLOBAL_TABLE_MIGRATION ); + } + + /** + * Deletes all the plugin's site-specific transients. + * + * @since 2.1.0 Visibility changed to public, made non-static. + * + * @return void + */ + public function delete_transients() { + // Remove regular transients. + foreach ( $this->transients->get_keys_from_database() as $key ) { + $this->transients->delete( $key, true ); + } + } + + /** + * Deletes all the plugin's global transients ("site transients"). + * + * @since 2.1.0 + * + * @return void + */ + public function delete_network_transients() { + // Remove site transients. + foreach ( $this->site_transients->get_keys_from_database() as $key ) { + $this->site_transients->delete( $key, true ); + } + } +} diff --git a/includes/avatar-privacy/components/class-user-profile.php b/includes/avatar-privacy/components/class-user-profile.php new file mode 100644 index 0000000..f9490bf --- /dev/null +++ b/includes/avatar-privacy/components/class-user-profile.php @@ -0,0 +1,171 @@ + + */ +class User_Profile implements Component { + + /** + * The markup to inject. + * + * @var string + */ + private $markup; + + /** + * The profile form helper. + * + * @var User_Form + */ + private $form; + + /** + * Indiciates whether the settings page is buffering its output. + * + * @var bool + */ + private $buffering; + + /** + * Creates a new instance. + * + * @since 2.1.0 Parameter $plugin_file removed. + * @since 2.3.0 Parameter $upload removed, parameter $form added. + * + * @param User_Form $form The profile form helper. + */ + public function __construct( User_Form $form ) { + $this->form = $form; + $this->buffering = false; + } + + /** + * Sets up the various hooks for the plugin component. + * + * @return void + */ + public function run() { + if ( \is_admin() ) { + \add_action( 'admin_init', [ $this, 'admin_init' ] ); + } + } + + /** + * Initialize additional plugin hooks. + * + * @return void + */ + public function admin_init() { + // Add the checkbox to the user profile form if we're in the WP backend. + \add_action( 'user_edit_form_tag', [ $this, 'print_form_encoding' ] ); + \add_action( 'show_user_profile', [ $this, 'add_user_profile_fields' ] ); + \add_action( 'edit_user_profile', [ $this, 'add_user_profile_fields' ] ); + \add_action( 'personal_options_update', [ $this->form, 'save' ] ); + \add_action( 'edit_user_profile_update', [ $this->form, 'save' ] ); + + // Replace profile picture setting with our own settings. + \add_action( 'admin_head-profile.php', [ $this, 'admin_head' ] ); + \add_action( 'admin_head-user-edit.php', [ $this, 'admin_head' ] ); + \add_action( 'admin_footer-profile.php', [ $this, 'admin_footer' ] ); + \add_action( 'admin_footer-user-edit.php', [ $this, 'admin_footer' ] ); + } + + /** + * Enables output buffering. + * + * @return void + */ + public function admin_head() { + if ( \ob_start( [ $this, 'replace_profile_picture_section' ] ) ) { + $this->buffering = true; + } + } + + /** + * Cleans up any output buffering. + * + * @return void + */ + public function admin_footer() { + // Clean up output buffering. + if ( $this->buffering && \ob_get_level() > 0 ) { + \ob_end_flush(); + $this->buffering = false; + } + } + + /** + * Prints the enctype "multipart/form-data". + * + * @return void + */ + public function print_form_encoding() { + echo ' enctype="multipart/form-data"'; + } + + /** + * Remove the profile picture section from the user profile screen. + * + * @param string $content The captured HTML output. + * + * @return string + */ + public function replace_profile_picture_section( $content ) { + if ( ! empty( $this->markup ) ) { + return (string) \preg_replace( '#.*#Usi', $this->markup, $content ); + } + + return $content; + } + + /** + * Stores the profile fields markup for later use. + * + * @param \WP_User $user The current user whose profile to modify. + * + * @return void + */ + public function add_user_profile_fields( \WP_User $user ) { + $this->markup = + $this->form->get_avatar_uploader( $user->ID ) . + $this->form->get_use_gravatar_checkbox( $user->ID ) . + $this->form->get_allow_anonymous_checkbox( $user->ID ); + } +} diff --git a/includes/avatar-privacy/core/class-api.php b/includes/avatar-privacy/core/class-api.php new file mode 100644 index 0000000..b2dc025 --- /dev/null +++ b/includes/avatar-privacy/core/class-api.php @@ -0,0 +1,36 @@ + + */ +interface API {} diff --git a/includes/avatar-privacy/core/class-comment-author-fields.php b/includes/avatar-privacy/core/class-comment-author-fields.php new file mode 100644 index 0000000..1d73a61 --- /dev/null +++ b/includes/avatar-privacy/core/class-comment-author-fields.php @@ -0,0 +1,413 @@ + + * @author Johannes Freudendahl + * + * @phpstan-import-type ColumnValueTuples from \Avatar_Privacy\Data_Storage\Database\Table + */ +class Comment_Author_Fields implements API { + + /** + * Prefix for caching avatar privacy for non-logged-in users. + */ + const EMAIL_CACHE_PREFIX = 'email_'; + + /** + * The cache handler. + * + * @var Cache + */ + private Cache $cache; + + /** + * The hashing helper. + * + * @var Hasher + */ + private Hasher $hasher; + + /** + * The comment author table handler. + * + * @var Comment_Author_Table + */ + private Comment_Author_Table $comment_author_table; + + /** + * The comment author table handler. + * + * @var Hashes_Table + */ + private Hashes_Table $hashes_table; + + /** + * A cache to prevent multiple hash updates. + * + * @since 2.6.0 + * + * @var array + */ + private array $updated_hashes = []; + + /** + * Creates a new instance. + * + * @param Cache $cache Required. + * @param Hasher $hasher Required. + * @param Comment_Author_Table $comment_author_table The comment author database table. + * @param Hashes_Table $hashes_table The hashes database table. + */ + public function __construct( Cache $cache, Hasher $hasher, Comment_Author_Table $comment_author_table, Hashes_Table $hashes_table ) { + $this->cache = $cache; + $this->hasher = $hasher; + $this->comment_author_table = $comment_author_table; + $this->hashes_table = $hashes_table; + } + + /** + * Retrieves the hash for the given comment author e-mail address. + * + * @param string $email The comment author's e-mail address. + * + * @return string + */ + public function get_hash( $email ) { + return $this->hasher->get_hash( $email ); + } + + /** + * 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 allows_gravatar_use( $email_or_hash ) { + $data = $this->load( $email_or_hash ); + + return ! empty( $data ) && ! empty( $data->use_gravatar ); + } + + /** + * 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 has_gravatar_policy( $email_or_hash ) { + $data = $this->load( $email_or_hash ); + + return ! empty( $data ) && isset( $data->use_gravatar ); + } + + /** + * 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_key( $email_or_hash ) { + $data = $this->load( $email_or_hash ); + + if ( isset( $data->id ) ) { + return $data->id; + } + + return 0; + } + + /** + * Returns the dataset from the 'use gravatar' table for the given e-mail + * address. + * + * @internal + * + * @param string $email_or_hash The comment author's e-mail address or the unique hash. + * + * @return \stdClass|null The dataset as an object or null. + */ + public function load( $email_or_hash ) { + global $wpdb; + + // Won't change valid hashes. + $email_or_hash = \strtolower( \trim( $email_or_hash ) ); + if ( empty( $email_or_hash ) ) { + return null; + } + + // Check cache. + $type = ( false === \strpos( $email_or_hash, '@' ) ) ? 'hash' : 'email'; + $key = $this->get_cache_key( $email_or_hash, $type ); + $data = $this->cache->get( $key ); + + if ( false === $data ) { + // We need to query the database. + $data = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder -- DB and column name. + 'SELECT c.*, h.hash FROM `%1$s` c LEFT OUTER JOIN `%2$s` h ON c.email = h.identifier AND h.type = "comment" WHERE `%3$s` = "%4$s"', + $wpdb->avatar_privacy, + $wpdb->avatar_privacy_hashes, + $type, + $email_or_hash + ), + \OBJECT + ); + + $this->cache->set( $key, $data, 5 * MINUTE_IN_SECONDS ); + } + + return $data; + } + + /** + * Retrieves the email for the given comment author database key. + * + * @param string $hash The hashed mail address. + * + * @return string + */ + public function get_email( $hash ) { + $data = $this->load( $hash ); + + return ! empty( $data->email ) ? $data->email : ''; + } + + /** + * Updates the comment author information. Also clears the cache. + * + * @param int $id The row ID. + * @param string $email The mail address. + * @param array $columns An array of values index by column name. + * + * @return int|false The number of rows updated, or false on error. + * + * @throws \RuntimeException A \RuntimeException is raised when invalid + * column names are used. + * + * @phpstan-param ColumnValueTuples $columns + */ + protected function update( $id, $email, array $columns ) { + $result = $this->comment_author_table->update( $columns, [ 'id' => $id ] ); + + if ( false !== $result && $result > 0 ) { + // Clear any previously cached value. + $this->clear_cache( $email ); + } + + return $result; + } + + /** + * Inserts data into the comment author table (and the supplementary hashes + * table). Also clears the cache if successful. + * + * @param string $email The mail address. + * @param int|null $use_gravatar A flag indicating if gravatar use is allowed. `null` indicates the default policy (i.e. not set). + * @param string $log_message The log message. + * + * @return int|false The number of rows updated, or false on error. + */ + protected function insert( $email, $use_gravatar, $log_message ) { + $columns = [ + 'email' => $email, + 'use_gravatar' => $use_gravatar, + 'log_message' => $log_message, + ]; + + // Update database. + $result = $this->comment_author_table->insert( $columns ); + if ( ! empty( $result ) ) { + // Clear any previously cached value, just in case. + $this->update_hash( $email, true ); + } + + return $result; + } + + /** + * 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_gravatar_use( $email, $comment_id, $use_gravatar ) { + $data = $this->load( $email ); + if ( empty( $data ) ) { + // Nothing found in the database, insert the dataset. + $this->insert( $email, $use_gravatar, $this->get_log_message( $comment_id ) ); + } else { + if ( $data->use_gravatar !== $use_gravatar ) { + // Dataset found but with different value, update it. + $new_values = [ + 'use_gravatar' => $use_gravatar, + 'log_message' => $this->get_log_message( $comment_id ), + ]; + $this->update( $data->id, $data->email, $new_values ); + } + + // We might also need to update the hash. + if ( empty( $data->hash ) ) { + $this->update_hash( $data->email ); + } + } + } + + /** + * Updates the hash for the comment author email. Also clears the cache if + * necessary. + * + * @param string $email The email. + * @param bool $clear_cache Optional. Force the cache to be cleared. Default false. + * + * @return int|false The number of rows updated, or false on error. + */ + public function update_hash( $email, $clear_cache = false ) { + $result = 0; + $hash = $this->get_hash( $email ); + + // Let's check the update cache first. + if ( ! isset( $this->updated_hashes[ $email ] ) || $hash !== $this->updated_hashes[ $email ] ) { + $data = [ + 'identifier' => $email, + 'hash' => $hash, + 'type' => 'comment', + ]; + + // Actually update database. + $result = $this->hashes_table->insert_or_update_row( $data ); + + // Mark hash as updated. + $this->updated_hashes[ $email ] = $hash; + } + + // Check whether we need to clear the cache. + if ( $clear_cache || ! empty( $result ) ) { + $this->clear_cache( $hash, 'hash' ); + } + + return $result; + } + + /** + * Returns a formatted log message for comment author data. + * + * @param int $comment_id A valid comment ID. + * + * @return string + */ + protected function get_log_message( $comment_id ) { + $log_message = 'set with comment %d'; + $parameters = [ $comment_id ]; + + if ( \is_multisite() ) { + global $wpdb; + + $log_message .= ' (site: %d, blog: %d)'; + $parameters[] = $wpdb->siteid; + $parameters[] = $wpdb->blogid; + } + + return \vsprintf( $log_message, $parameters ); + } + + /** + * Clears the cache for the given comment author e-mail address or hash. + * + * @param string $email_or_hash The comment author's e-mail address or the unique hash. + * @param string $type Optional. The identifier type ('email' or 'hash'). Default 'email'. + * + * @return void + */ + public function clear_cache( $email_or_hash, $type = 'email' ) { + $this->cache->delete( $this->get_cache_key( $email_or_hash, $type ) ); + } + + /** + * Calculates the cache key for the given identifier. + * + * @param string $email_or_hash The comment author's e-mail address or the unique hash. + * @param string $type Optional. The identifier type ('email' or 'hash'). Default 'email'. + * + * @return string + */ + protected function get_cache_key( $email_or_hash, $type = 'email' ) { + if ( 'email' === $type ) { + // We only need the hash here. + $email_or_hash = $this->get_hash( $email_or_hash ); + } + + return self::EMAIL_CACHE_PREFIX . $email_or_hash; + } + + /** + * Deletes the data for the comment author identified by an email address. + * + * @param string $email The comment author's e-mail address or the unique hash. + * + * @return int|false The number of rows deleted, or false on error. + */ + public function delete( $email ) { + $comment_author_rows = $this->comment_author_table->delete( [ 'email' => $email ] ); + $hashes_rows = $this->hashes_table->delete( + [ + 'identifier' => $email, + 'type' => 'comment', + ] + ); + + if ( ! empty( $comment_author_rows ) || ! empty( $hashes_rows ) ) { + $this->clear_cache( $email ); + + return \max( (int) $comment_author_rows, (int) $hashes_rows ); + } + + return false; + } +} diff --git a/includes/avatar-privacy/core/class-default-avatars.php b/includes/avatar-privacy/core/class-default-avatars.php new file mode 100644 index 0000000..f5a8f2c --- /dev/null +++ b/includes/avatar-privacy/core/class-default-avatars.php @@ -0,0 +1,270 @@ + + * + * @phpstan-import-type AvatarDefinition from User_Fields + */ +class Default_Avatars implements API { + + /** + * The settings API. + * + * @var Settings + */ + private Settings $settings; + + /** + * The options handler. + * + * @var Options + */ + private Options $options; + + /** + * The hashing helper. + * + * @var Hasher + */ + private Hasher $hasher; + + /** + * The filesystem cache handler. + * + * @var Filesystem_Cache + */ + private Filesystem_Cache $file_cache; + + /** + * The image file handler. + * + * @var Image_File + */ + private Image_File $image_file; + + /** + * Creates a new instance. + * + * @param Settings $settings The settings API. + * @param Options $options The options handler. + * @param Hasher $hasher The hashing helper.. + * @param Filesystem_Cache $file_cache The file cache handler. + * @param Image_File $image_file The image file handler. + */ + public function __construct( Settings $settings, Options $options, Hasher $hasher, Filesystem_Cache $file_cache, Image_File $image_file ) { + $this->settings = $settings; + $this->options = $options; + $this->hasher = $hasher; + $this->file_cache = $file_cache; + $this->image_file = $image_file; + } + + /** + * Retrieves the hash for the custom default avatar for the given site. + * + * @param int $site_id The site ID. + * + * @return string + */ + public function get_hash( $site_id ) { + return $this->hasher->get_hash( "custom-default-{$site_id}" ); + } + + /** + * Retrieves the full-size custom default avatar for a site (if one exists). + * + * @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() { + $avatar = $this->settings->get( Settings::UPLOAD_CUSTOM_DEFAULT_AVATAR ); + if ( ! \is_array( $avatar ) || empty( $avatar['file'] ) ) { + $avatar = []; + } + + return $avatar; + } + + // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- until PHPCS bug is fixed. + /** + * 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). + * + * @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 ) { + $filename = \parse_url( $image_url, \PHP_URL_PATH ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- we only support PHP 7.0 and higher. + if ( empty( $filename ) ) { + throw new \InvalidArgumentException( "Malformed URL {$image_url}" ); + } + + // Prepare arguments. + $overrides = [ + 'global_upload' => false, + 'upload_dir' => Upload_Handler::UPLOAD_DIR, + 'filename' => $this->get_custom_default_avatar_filename( $filename ), + ]; + + // Sideload file and validate result. + $sideloaded_avatar = $this->image_file->handle_sideload( $image_url, $overrides ); + if ( empty( $sideloaded_avatar['file'] ) ) { + throw new Upload_Handling_Exception( 'Missing upload file path' ); + } elseif ( empty( $sideloaded_avatar['type'] ) ) { + throw new Upload_Handling_Exception( "Could not determine MIME type for {$image_url}" ); + } elseif ( ! isset( Image_File::FILE_EXTENSION[ $sideloaded_avatar['type'] ] ) ) { + throw new Upload_Handling_Exception( "Invalid MIME type {$sideloaded_avatar['type']}" ); + } + + $this->store_custom_default_avatar_data( $sideloaded_avatar ); + } + // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- until PHPCS bug is fixed. + + /** + * Deletes the custom default avatar set for the current site (including the setting). + * + * @return void + * + * @throws File_Deletion_Exception An exception is thrown if the previously + * set image could not be deleted. + */ + public function delete_custom_default_avatar() { + $this->store_custom_default_avatar_data( [] ); + } + + /** + * Stores the given avatar data and cleans up existing image files. + * + * @param string[] $avatar_data The avatar data. May be empty. + * + * @return void + * + * @throws File_Deletion_Exception An exception is thrown if the previously + * set image could not be deleted. + */ + protected function store_custom_default_avatar_data( array $avatar_data ) { + // Delete old images. + if ( ! $this->delete_custom_default_avatar_image_file() ) { + throw new File_Deletion_Exception( 'Could not delete previous avatar image.' ); + } + + // Invalidate cached thumbnails. + $this->invalidate_custom_default_avatar_cache( \get_current_blog_id() ); + + // Save the sideloaded default avatar. + $this->settings->set( Settings::UPLOAD_CUSTOM_DEFAULT_AVATAR, $avatar_data ); + } + + /** + * Deletes the custom default avatar image file for the current site (but not + * cached thumbnails). + * + * @internal + * + * @return bool + */ + public function delete_custom_default_avatar_image_file() { + // Delete original upload if it exists. + $icon = $this->get_custom_default_avatar(); + if ( empty( $icon['file'] ) || \file_exists( $icon['file'] ) && delete_file( $icon['file'] ) ) { + return true; + } + + return false; + } + + /** + * Invalidates cached avatar images. + * + * @internal + * + * @param int $site_id The site ID. + * + * @return void + */ + public function invalidate_custom_default_avatar_cache( $site_id ) { + $this->file_cache->invalidate( 'custom', "#/{$this->get_hash( $site_id )}-[1-9][0-9]*\.[a-z]{3}\$#" ); + } + + /** + * Retrieves the base filename (without the extension) for the custom avatar + * image for the current site. + * + * @internal + * + * @param string $filename The original filename. + * + * @return string + */ + public function get_custom_default_avatar_filename( $filename ) { + $extension = \pathinfo( $filename, \PATHINFO_EXTENSION ); + $filename = 'custom-default-icon'; + + $blogname = $this->options->get( 'blogname', '', true ); + if ( \is_string( $blogname ) && ! empty( $blogname ) ) { + $filename = \htmlspecialchars_decode( $blogname ); + } + + return \sanitize_file_name( "{$filename}.{$extension}" ); + } +} diff --git a/includes/avatar-privacy/core/class-settings.php b/includes/avatar-privacy/core/class-settings.php new file mode 100644 index 0000000..ef86d1f --- /dev/null +++ b/includes/avatar-privacy/core/class-settings.php @@ -0,0 +1,449 @@ + + * + * @phpstan-import-type AvatarDefinition from User_Fields + * + * @phpstan-type SettingsFieldMeta array{ + * ui: class-string<\Avatar_Privacy\Vendor\Mundschenk\UI\Control>, + * tab_id: string, + * section: string, + * help_no_file?: string, + * help_no_upload?: string, + * help_text?: string, + * short?: string, + * label?: string, + * erase_checkbox?: string, + * action?: string, + * nonce?: string, + * default?: mixed, + * attributes?: mixed[], + * settings_args?: mixed[], + * elements?: mixed[], + * grouped_with?: string, + * outer_attributes?: mixed[], + * } + * @phpstan-type SettingsFieldDefinitions array{ + * custom_default_avatar: SettingsFieldMeta, + * display: SettingsFieldMeta, + * gravatar_use_default: SettingsFieldMeta, + * } + * @phpstan-type SettingsFields array{ + * custom_default_avatar: AvatarDefinition|array{}, + * gravatar_use_default: bool, + * installed_version: string + * } + */ +class Settings implements API { + + /** + * The name of the combined settings in the database. + * + * @since 2.4.0 Moved from Avatar_Privacy\Core and renamed from SETTINGS_NAME. + * + * @var string + */ + const OPTION_NAME = 'settings'; + + /** + * The options array index of the custom default avatar image. + * + * @var string + */ + const UPLOAD_CUSTOM_DEFAULT_AVATAR = 'custom_default_avatar'; + + /** + * The defaults array index of the information headers. + * + * @var string + */ + const INFORMATION_HEADER = 'display'; + + /** + * The options array index of the default gravatar policy. + * + * @var string + */ + const GRAVATAR_USE_DEFAULT = 'gravatar_use_default'; + + /** + * The defaults array. + * + * @var array + * + * @phpstan-var SettingsFields + */ + private array $defaults; + + /** + * The fields definition array. + * + * @var array + * + * @phpstan-var SettingsFieldDefinitions + */ + private array $fields; + + /** + * The fields definition array for the network settings. + * + * @var array + * + * @phpstan-var array + */ + private array $network_fields; + + /** + * The cached information header markup. + * + * @var string + */ + private string $information_header; + + /** + * The plugin version. + * + * @since 2.4.0 + * + * @var string + */ + private string $version; + + /** + * The user's settings (indexed by site ID to be multisite-safe). + * + * @since 2.4.0 + * + * @var array { + * @type array $site_settings The plugin settings for the site. + * } + * + * @phpstan-var array + */ + private array $settings = []; + + /** + * The options handler. + * + * @since 2.4.0 + * + * @var Options + */ + private Options $options; + + /** + * Creates a new instance. + * + * @since 2.4.0 + * + * @param string $version The plugin version string (e.g. "3.0.0-beta.2"). + * @param Options $options The options handler. + */ + public function __construct( $version, Options $options ) { + $this->version = $version; + $this->options = $options; + } + + /** + * Retrieves the plugin version. + * + * @since 2.4.0 + * + * @return string + */ + public function get_version() { + return $this->version; + } + + /** + * Retrieves the complete plugin settings array. + * + * @since 2.0.0 Parameter $force added. + * @since 2.4.0 Moved to Avatar_Privacy\Core\Settings::get_all_settings. + * + * @param bool $force Optional. Forces retrieval of settings from database. Default false. + * + * @return array + * + * @phpstan-return SettingsFields + */ + public function get_all_settings( $force = false ) { + $site_id = \get_current_blog_id(); + + // Force a re-read if the cached settings do not appear to be from the current version. + if ( empty( $this->settings[ $site_id ] ) || + empty( $this->settings[ $site_id ][ Options::INSTALLED_VERSION ] ) || + $this->version !== $this->settings[ $site_id ][ Options::INSTALLED_VERSION ] || + $force + ) { + $this->settings[ $site_id ] = $this->load_settings(); + } + + return $this->settings[ $site_id ]; + } + + /** + * Load settings from the database and set defaults if necessary. + * + * @since 2.4.1 + * + * @return array + * + * @phpstan-return SettingsFields + */ + protected function load_settings() { + $_settings = $this->options->get( self::OPTION_NAME ); + $_defaults = $this->get_defaults(); + $modified = false; + + if ( \is_array( $_settings ) ) { + foreach ( $_defaults as $name => $default_value ) { + if ( ! isset( $_settings[ $name ] ) ) { + $_settings[ $name ] = $default_value; + $modified = true; + } + } + + /** + * PHPStan type. + * + * @phpstan-var SettingsFields $_settings + */ + } else { + $_settings = $_defaults; + $modified = true; + } + + if ( $modified ) { + $this->options->set( self::OPTION_NAME, $_settings ); + } + + return $_settings; + } + + /** + * Retrieves a single setting. + * + * @since 2.4.0 + * + * @param string $setting The setting name (index). + * @param bool $force Optional. Forces retrieval of settings from + * database. Default false. + * + * @return string|int|bool|array The requested setting value. + * + * @throws \UnexpectedValueException Thrown when the setting name is invalid. + * + * @phpstan-param key-of $setting + * @phpstan-return value-of + */ + public function get( $setting, $force = false ) { + $all_settings = $this->get_all_settings( $force ); + + if ( ! isset( $all_settings[ $setting ] ) ) { + throw new \UnexpectedValueException( "Invalid setting name '{$setting}'." ); + } + + return $all_settings[ $setting ]; + } + + /** + * Sets a single setting. + * + * @since 2.4.0 + * + * @internal + * + * @param string $setting The setting name (index). + * @param string|int|bool|array $value The setting value. + * + * @return bool + * + * @throws \UnexpectedValueException Thrown when the setting name is invalid. + * + * @phpstan-param key-of $setting + * @phpstan-param value-of $value + */ + public function set( $setting, $value ) { + $site_id = \get_current_blog_id(); + $all_settings = $this->get_all_settings(); + + if ( ! isset( $all_settings[ $setting ] ) ) { + throw new \UnexpectedValueException( "Invalid setting name '{$setting}'." ); + } + + // Update DB. + $all_settings[ $setting ] = $value; + $result = $this->options->set( self::OPTION_NAME, $all_settings ); + + // Update cached settings only if DB the DB write was successful. + if ( $result ) { + /** + * PHPStan type. + * + * @phpstan-var SettingsFields $all_settings + */ + $this->settings[ $site_id ] = $all_settings; + } + + return $result; + } + + /** + * Retrieves the settings field definitions. + * + * @param string $information_header Optional. The HTML markup for the informational header in the settings. Default ''. + * + * @return array + * + * @phpstan-return SettingsFieldDefinitions + */ + public function get_fields( $information_header = '' ) { + if ( ! isset( $this->fields ) ) { + $this->fields = [ // @codeCoverageIgnoreStart + self::UPLOAD_CUSTOM_DEFAULT_AVATAR => [ + 'ui' => \Avatar_Privacy\Upload_Handlers\UI\File_Upload_Input::class, + 'tab_id' => '', // Will be added to the 'discussions' page. + 'section' => 'avatars', + 'help_no_file' => \__( 'No custom default avatar is set. Use the upload field to add a custom default avatar image.', 'avatar-privacy' ), + 'help_no_upload' => \__( 'You do not have media management permissions. To change your custom default avatar, contact the site administrator.', 'avatar-privacy' ), + 'help_text' => \__( 'Replace the custom default avatar by uploading a new image, or erase it by checking the delete option.', 'avatar-privacy' ), + 'erase_checkbox' => Custom_Default_Icon_Upload_Handler::CHECKBOX_ERASE, + 'action' => Custom_Default_Icon_Upload_Handler::ACTION_UPLOAD, + 'nonce' => Custom_Default_Icon_Upload_Handler::NONCE_UPLOAD, + 'default' => 0, + 'attributes' => [ 'accept' => 'image/*' ], + 'settings_args' => [ 'class' => 'avatar-settings' ], + ], + self::INFORMATION_HEADER => [ + 'ui' => Controls\Display_Text::class, + 'tab_id' => '', // Will be added to the 'discussions' page. + 'section' => 'avatars', + 'elements' => [], // Will be updated below. + 'short' => \__( 'Avatar Privacy', 'avatar-privacy' ), + ], + self::GRAVATAR_USE_DEFAULT => [ + 'ui' => Controls\Checkbox_Input::class, + 'tab_id' => '', + 'section' => 'avatars', + /* translators: 1: checkbox HTML */ + 'label' => \__( '%1$s Display Gravatar images by default.', 'avatar-privacy' ), + 'help_text' => \__( 'Checking will ensure that gravatars are displayed when there is no explicit setting for the user or mail address (e.g. for comments made before installing Avatar Privacy). Please only enable this setting after careful consideration of the privacy implications.', 'avatar-privacy' ), + 'default' => 0, + 'grouped_with' => self::INFORMATION_HEADER, + 'outer_attributes' => [ 'class' => 'avatar-settings-enabled' ], + ], // @codeCoverageIgnoreEnd + ]; + } + + // Allow calls where the information header is not relevant by caching it separately. + if ( ! empty( $information_header ) && + ( ! isset( $this->information_header ) || $information_header !== $this->information_header ) ) { + $this->fields[ self::INFORMATION_HEADER ]['elements'] = [ $information_header ]; + $this->information_header = $information_header; + } + + return $this->fields; + } + + /** + * Retrieves the default settings. + * + * @return array + * + * @phpstan-return SettingsFields + */ + public function get_defaults() { + if ( ! isset( $this->defaults ) ) { + $_defaults = []; + foreach ( $this->get_fields() as $index => $field ) { + if ( isset( $field['default'] ) ) { + $_defaults[ $index ] = $field['default']; + } + } + + // Allow detection of new installations. + $_defaults[ Options::INSTALLED_VERSION ] = ''; + + /** + * PHPStan type. + * + * @phpstan-var SettingsFields $_defaults + */ + $this->defaults = $_defaults; + } + + return $this->defaults; + } + + /** + * Retrieves the network settings field definitions. + * + * @since 2.1.0 + * + * @return array + * + * @phpstan-return array + */ + public function get_network_fields() { + if ( ! isset( $this->network_fields ) ) { + $this->network_fields = [ // @codeCoverageIgnoreStart + Network_Options::USE_GLOBAL_TABLE => [ + 'ui' => Controls\Checkbox_Input::class, + 'tab_id' => '', + 'section' => Network_Settings_Page::SECTION, + /* translators: 1: checkbox HTML */ + 'label' => \__( '%1$s Use global table.', 'avatar-privacy' ), + 'short' => \__( 'Global Table', 'avatar-privacy' ), + 'help_text' => \__( 'Checking will make Avatar Privacy use a single table for each network (instead of for each site) for storing anonymous comment author consent. (Do not enable this setting unless you are sure about the privacy implications.)', 'avatar-privacy' ), + 'default' => 0, + ], // @codeCoverageIgnoreEnd + ]; + } + + return $this->network_fields; + } +} diff --git a/includes/avatar-privacy/core/class-user-fields.php b/includes/avatar-privacy/core/class-user-fields.php new file mode 100644 index 0000000..84202b1 --- /dev/null +++ b/includes/avatar-privacy/core/class-user-fields.php @@ -0,0 +1,525 @@ + + * + * @phpstan-type AvatarDefinition array{ file: string, type: string } + */ +class User_Fields implements API { + + /** + * The user meta key for the hashed email. + * + * @var string + */ + const EMAIL_HASH_META_KEY = 'avatar_privacy_hash'; + + /** + * The user meta key for the gravatar use flag. + * + * @var string + */ + const GRAVATAR_USE_META_KEY = 'avatar_privacy_use_gravatar'; + + /** + * The user meta key for the gravatar use flag. + * + * @var string + */ + const ALLOW_ANONYMOUS_META_KEY = 'avatar_privacy_allow_anonymous'; + + /** + * The user meta key for the local avatar. + * + * @var string + */ + const USER_AVATAR_META_KEY = 'avatar_privacy_user_avatar'; + + /** + * The hashing helper. + * + * @var Hasher + */ + private Hasher $hasher; + + /** + * The filesystem cache handler. + * + * @var Filesystem_Cache + */ + private Filesystem_Cache $file_cache; + + /** + * The image file handler. + * + * @var Image_File + */ + private Image_File $image_file; + + /** + * A request-level cache for user lookups. + * + * @var array + */ + private array $user_by_email = []; + + /** + * Creates a new instance. + * + * @param Hasher $hasher The hashing helper.. + * @param Filesystem_Cache $file_cache The file cache handler. + * @param Image_File $image_file The image file handler. + */ + public function __construct( Hasher $hasher, Filesystem_Cache $file_cache, Image_File $image_file ) { + $this->hasher = $hasher; + $this->file_cache = $file_cache; + $this->image_file = $image_file; + } + + /** + * Retrieves the hash for the given user ID. If there currently is no hash, + * a new one is generated. + * + * @param int $user_id The user ID. + * + * @return string|false The hashed email, or `false` on failure. + */ + public function get_hash( $user_id ) { + $hash = \get_user_meta( $user_id, self::EMAIL_HASH_META_KEY, true ); + + if ( ! \is_string( $hash ) || empty( $hash ) ) { + $user = \get_user_by( 'ID', $user_id ); + if ( empty( $user->user_email ) ) { + return false; + } + + $hash = $this->hasher->get_hash( $user->user_email ); + \update_user_meta( $user_id, self::EMAIL_HASH_META_KEY, $hash ); + } + + return $hash; + } + + /** + * Retrieves a user by email hash. + * + * @param string $hash The user's email hash. + * + * @return \WP_User|null + */ + public function get_user_by_hash( $hash ) { + // No extra caching necessary, WP Core already does that for us. + $args = [ + 'number' => 1, + 'meta_key' => self::EMAIL_HASH_META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_value' => $hash, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + 'meta_compare' => '=', + ]; + $users = \get_users( $args ); + + if ( empty( $users ) ) { + return null; + } + + return $users[0]; + } + + /** + * Retrieves a user by email. + * + * This method differs from `get_user_by` in that it caches the result even + * if no user is found for the duration of the request. + * + * @since 2.6.0 + * + * @param string $email The email to query. + * + * @return \WP_User|null + */ + public function get_user_by_email( $email ) { + if ( isset( $this->user_by_email[ $email ] ) || \array_key_exists( $email, $this->user_by_email ) ) { + return $this->user_by_email[ $email ]; + } + + $user = \get_user_by( 'email', $email ); + if ( empty( $user ) ) { + $user = null; + } + + // Cache lookup result. + $this->user_by_email[ $email ] = $user; + + return $user; + } + + /** + * Retrieves the full-size local avatar for a user (if one exists). + * + * @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_local_avatar( $user_id ) { + /** + * Filters whether to retrieve the user avatar early. If the filtered result + * contains both a filename and a MIME type, those will be returned immediately. + * + * @since 2.2.0 + * + * @param array|null $avatar { + * Optional. The user avatar information. Default null. + * + * @type string $file The local filename. + * @type string $type The MIME type. + * } + * @param int $user_id The user ID. + */ + $avatar = \apply_filters( 'avatar_privacy_pre_get_user_avatar', null, $user_id ); + if ( ! empty( $avatar ) && ! empty( $avatar['file'] ) && ! empty( $avatar['type'] ) ) { + return $avatar; + } + + $avatar = \get_user_meta( $user_id, self::USER_AVATAR_META_KEY, true ); + if ( ! \is_array( $avatar ) ) { + $avatar = []; + } + + return $avatar; + } + + // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- until PHPCS bug is fixed. + /** + * Sets the local avatar for the given user. + * + * 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). + * + * @param int $user_id The user ID. + * @param string $image_url The image URL or filename. + * + * @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 Upload_Handling_Exception An exceptions is thrown if the sideloading + * fails for some reason. + */ + public function set_local_avatar( $user_id, $image_url ) { + $filename = \parse_url( $image_url, \PHP_URL_PATH ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- we only support PHP 7.0 and higher. + if ( empty( $filename ) ) { + throw new \InvalidArgumentException( "Malformed URL {$image_url}" ); + } + + // Prepare arguments. + $overrides = [ + 'global_upload' => true, + 'upload_dir' => User_Avatar_Upload_Handler::UPLOAD_DIR, + 'filename' => $this->get_local_avatar_filename( $user_id, $filename ), + ]; + + $sideloaded_avatar = $this->image_file->handle_sideload( $image_url, $overrides ); + + $this->set_uploaded_local_avatar( $user_id, $sideloaded_avatar ); + } + // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- until PHPCS bug is fixed. + + /** + * Sets the local avatar to the uploaded image. + * + * @internal + * + * @param int $user_id The user ID. + * @param string[] $uploaded_avatar { + * The uploaded avatar information (the result of Image_File::handle_upload()). + * + * @type string $file The image file path. + * @type string $type The MIME type of the uploaded image. + * } + * + * @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. + */ + public function set_uploaded_local_avatar( $user_id, $uploaded_avatar ) { + if ( ! $this->user_exists( $user_id ) ) { + throw new \InvalidArgumentException( "Invalid user ID {$user_id}" ); + } elseif ( empty( $uploaded_avatar['file'] ) ) { + throw new \InvalidArgumentException( 'Missing upload file path' ); + } elseif ( empty( $uploaded_avatar['type'] ) ) { + throw new \InvalidArgumentException( 'Missing image MIME type' ); + } elseif ( ! isset( Image_File::FILE_EXTENSION[ $uploaded_avatar['type'] ] ) ) { + throw new \InvalidArgumentException( "Invalid MIME type {$uploaded_avatar['type']}" ); + } + + // Delete old images. + $this->delete_local_avatar( $user_id ); + + // Save user information (overwriting previous). + \update_user_meta( $user_id, self::USER_AVATAR_META_KEY, $uploaded_avatar ); + } + + /** + * Checks whether the given user ID is valid. + * + * @param int $user_id The user ID. + * + * @return bool + */ + protected function user_exists( $user_id ) { + $args = [ + 'include' => [ $user_id ], + 'fields' => 'ID', + ]; + + if ( \is_network_admin() ) { + $args['blog_id'] = 0; + } + + return (bool) \get_users( $args ); + } + + /** + * Deletes the local avatar of the given user. + * + * @param int $user_id The user ID. + * + * @return bool + */ + public function delete_local_avatar( $user_id ) { + // Invalidate cached avatar images. + $this->invalidate_local_avatar_cache( $user_id ); + + // Delete original upload. + $avatar = \get_user_meta( $user_id, self::USER_AVATAR_META_KEY, true ); + if ( \is_array( $avatar ) && ! empty( $avatar['file'] ) && \is_string( $avatar['file'] ) && \file_exists( $avatar['file'] ) && delete_file( $avatar['file'] ) ) { + return \delete_user_meta( $user_id, self::USER_AVATAR_META_KEY ); + } + + return false; + } + + /** + * Invalidates cached avatar images. + * + * @param int $user_id The user ID. + * + * @return void + */ + public function invalidate_local_avatar_cache( $user_id ) { + $hash = $this->get_hash( $user_id ); + if ( ! empty( $hash ) ) { + $this->file_cache->invalidate( 'user', "#/{$hash}-[1-9][0-9]*\.[a-z]{3}\$#" ); + } + } + + /** + * Retrieves the base filename (without the extension) for a local avatar image + * for the given user. + * + * @internal + * + * @param int $user_id The user ID. + * @param string $filename The original filename. + * + * @return string + */ + public function get_local_avatar_filename( $user_id, $filename ) { + $user = \get_user_by( 'id', $user_id ); + if ( ! $user instanceof \WP_User ) { + return $filename; + } + + $extension = \pathinfo( $filename, \PATHINFO_EXTENSION ); + + return \sanitize_file_name( "{$user->display_name}_avatar.{$extension}" ); + } + + /** + * Checks whether a user has opted-in to Gravatar usage. + * + * @param int $user_id The user ID. + * + * @return bool + */ + public function allows_gravatar_use( $user_id ) { + return 'true' === \get_user_meta( $user_id, self::GRAVATAR_USE_META_KEY, true ); + } + + /** + * Checks whether a user has set a Gravatar usage policy. + * + * @param int $user_id The user ID. + * + * @return bool + */ + public function has_gravatar_policy( $user_id ) { + return ! empty( \get_user_meta( $user_id, self::GRAVATAR_USE_META_KEY, true ) ); + } + + /** + * Updates a user's gravatar policy. + * + * @param int $user_id The user ID. + * @param bool $use_gravatar Whether using Gravatar should be allowed or not. + * + * @return void + */ + public function update_gravatar_use( $user_id, $use_gravatar ) { + // Use true/false instead of 1/0 since a '0' value is removed from + // the database and then we can't differentiate between "has opted-out" + // and "never saved a value". + \update_user_meta( $user_id, self::GRAVATAR_USE_META_KEY, $use_gravatar ? 'true' : 'false' ); + } + + /** + * Checks whether a user has opted-in to anonymous commenting. + * + * @param int $user_id The user ID. + * + * @return bool + */ + public function allows_anonymous_commenting( $user_id ) { + return 'true' === \get_user_meta( $user_id, self::ALLOW_ANONYMOUS_META_KEY, true ); + } + + /** + * Checks whether a user has set an anonymous commenting policy. + * + * @param int $user_id The user ID. + * + * @return bool + */ + public function has_anonymous_commenting_policy( $user_id ) { + return ! empty( \get_user_meta( $user_id, self::ALLOW_ANONYMOUS_META_KEY, true ) ); + } + + /** + * Updates a user's anonymous commenting policy. + * + * @param int $user_id The user ID. + * @param bool $anonymous Whether anonymous commenting should be allowed or not. + * + * @return void + */ + public function update_anonymous_commenting( $user_id, $anonymous ) { + // Use true/false instead of 1/0 since a '0' value is removed from + // the database and then we can't differentiate between "has opted-out" + // and "never saved a value". + \update_user_meta( $user_id, self::ALLOW_ANONYMOUS_META_KEY, $anonymous ? 'true' : 'false' ); + } + + /** + * Deletes the stored metadata for a user. + * + * Currently this includes: + * - the email hash, + * - the Gravatar usage policy, + * - the anonymous commenting policy, and + * - the local avatar. + * + * @internal + * + * @param int $user_id The user ID. + * + * @return int The number of removed metadata fields. + */ + public function delete( $user_id ) { + $count = 0; + + // Delete the "simple" meta fields. + $count += (int) \delete_user_meta( $user_id, self::EMAIL_HASH_META_KEY ); + $count += (int) \delete_user_meta( $user_id, self::GRAVATAR_USE_META_KEY ); + $count += (int) \delete_user_meta( $user_id, self::ALLOW_ANONYMOUS_META_KEY ); + + // Also delete a local avatar if one has been set (including all image files). + $count += (int) $this->delete_local_avatar( $user_id ); + + return $count; + } + + /** + * Removes local avatar files "orphaned" by the deletion of the referencing + * user meta data (e.g. when a user is deleted). + * + * @internal + * + * @since 2.5.2 + * + * @param string[] $meta_ids An array of metadata entry IDs to delete. + * @param int $object_id ID of the object metadata is for. + * @param string $meta_key Metadata key. + * @param mixed $meta_value Metadata value. + * + * @return void + */ + public function remove_orphaned_local_avatar( array $meta_ids, $object_id, $meta_key, $meta_value ) { + if ( self::USER_AVATAR_META_KEY !== $meta_key ) { + return; + } + + /** + * The filter provides inconsistent data depending on whether it is called + * by `delete_metadata` or `delete_metadata_by_mid` (@see https://core.trac.wordpress.org/ticket/53102). + * When run through `delete_metadata`, `$meta_value` is equal to the optional + * argument of the same name, not the actual metadata value. + * + * Fortunately, both `wp_delete_user` and `wpmu_delete_user` use `delete_metadata_by_mid`, + * so we can use `$meta_value`. Contrary to the documentation, non-scalar + * values are not serialized. + */ + if ( \is_array( $meta_value ) && ! empty( $meta_value['file'] ) && \is_string( $meta_value['file'] ) && \file_exists( $meta_value['file'] ) ) { + delete_file( $meta_value['file'] ); + } + } +} diff --git a/includes/avatar-privacy/data-storage/class-cache.php b/includes/avatar-privacy/data-storage/class-cache.php new file mode 100644 index 0000000..883cb95 --- /dev/null +++ b/includes/avatar-privacy/data-storage/class-cache.php @@ -0,0 +1,46 @@ + + */ +class Cache extends \Avatar_Privacy\Vendor\Mundschenk\Data_Storage\Cache { + const PREFIX = 'avatar_privacy_'; + const GROUP = 'avatar_privacy'; + + /** + * Creates a new instance. + */ + public function __construct() { + parent::__construct( self::PREFIX, self::GROUP ); + } +} diff --git a/includes/avatar-privacy/data-storage/class-filesystem-cache.php b/includes/avatar-privacy/data-storage/class-filesystem-cache.php new file mode 100644 index 0000000..4cdc1f1 --- /dev/null +++ b/includes/avatar-privacy/data-storage/class-filesystem-cache.php @@ -0,0 +1,278 @@ + + * + * @phpstan-type UploadDir array{ + * path: string, + * url: string, + * subdir: string, + * basedir: string, + * baseurl: string, + * error: string|false + * } + */ +class Filesystem_Cache { + + const CACHE_DIR = 'avatar-privacy/cache/'; + + /** + * The base directory for the filesystem cache. + * + * @var string + */ + private string $base_dir; + + /** + * The base URL for accessing cached files. + * + * @var string + */ + private string $base_url; + + /** + * Information about the uploads directory. + * + * @var array + * + * @phpstan-var UploadDir + */ + private array $upload_dir; + + /** + * Creates a new instance. + */ + public function __construct() { + $this->get_base_dir(); + } + + /** + * Retrieves the base directory for caching files. + * + * @since 2.4.0 A Filesystem_Exception is thrown instead of a generic \RuntimeException. + * + * @throws Filesystem_Exception An exception is thrown if the cache directory + * does not exist and can't be created. + * + * @return string + */ + public function get_base_dir() { + if ( empty( $this->base_dir ) ) { + $this->base_dir = "{$this->get_upload_dir()['basedir']}/" . self::CACHE_DIR; + + if ( ! \wp_mkdir_p( $this->base_dir ) ) { + throw new Filesystem_Exception( "The cache directory {$this->base_dir} could not be created." ); + } + } + + return $this->base_dir; + } + + /** + * Retrieves the base URL for accessing cached files. + * + * @return string + */ + public function get_base_url() { + if ( empty( $this->base_url ) ) { + $this->base_url = "{$this->get_upload_dir()['baseurl']}/" . self::CACHE_DIR; + } + + return $this->base_url; + } + + /** + * Retrieves information about the upload directory. + * + * @since 2.1.0 Visibility changed to protected. + * + * @return array + * + * @phpstan-return UploadDir + */ + protected function get_upload_dir() { + if ( ! isset( $this->upload_dir ) ) { + $multisite = \is_multisite(); + + if ( $multisite ) { + \switch_to_blog( \get_main_site_id() ); + } + + // We only need the basedir, so don't create the monthly sub-directory. + $this->upload_dir = \wp_upload_dir( null, false ); + + if ( $multisite ) { + \restore_current_blog(); + } + } + + return $this->upload_dir; + } + + /** + * Stores data in the filesystem cache. + * + * @since 2.6.0 The type of the `$data` parameter has been corrected to `string`. + * + * @param string $filename The filename (including any sub directory). + * @param string $data The (possibly binary) data. Will not be cached if empty. + * @param bool $force Optional. The cached file will only be overwritten if set to true. Default false. + * + * @return bool True if the file was successfully stored in the cache, false otherwise. + */ + public function set( $filename, $data, $force = false ) { + $file = $this->get_base_dir() . $filename; + + if ( \file_exists( $file ) && ! $force ) { + return true; + } + + return ! ( + // Don't create empty files. + 0 === \strlen( $data ) || + // Make sure that the file path is valid. + ! \wp_mkdir_p( \dirname( $file ) ) || + // Check if the file has been stored successfully. + false === \file_put_contents( $file, $data, \LOCK_EX ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + ); + } + + /** + * Retrieves the URL for the cached file. + * + * @param string $filename The filename (including any sub directory). + * + * @return string + */ + public function get_url( $filename ) { + return $this->get_base_url() . $filename; + } + + /** + * Removes a file from the cache. + * + * @param string $filename The filename (including any sub directory). + * + * @return bool True if the file was successfully removed from the cache, false otherwise. + */ + public function delete( $filename ) { + $file = $this->get_base_dir() . $filename; + + return \wp_is_writable( $file ) && delete_file( $file ); + } + + /** + * Invalidate all cached elements by recursively deleting all files and directories. + * + * @param string $subdir Optional. Limit invalidation to the given subdirectory. Default ''. + * @param string $regex Optional. Limit invalidation to files matching the given regular expression. Default ''. + * + * @return void + */ + public function invalidate( $subdir = '', $regex = '' ) { + try { + $iterator = $this->get_recursive_file_iterator( $subdir, $regex ); + } catch ( \UnexpectedValueException $e ) { + // Ignore non-existing subdirectories. + return; + } + + foreach ( $iterator as $path => $file ) { + if ( $file->isWritable() ) { + + if ( $file->isDir() ) { + \rmdir( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- Useing the WP_Filesystem API is not an option on the frontend. + } else { + delete_file( $path ); + } + } + } + } + + /** + * Invalidate all cached files older than the given age. + * + * @param int $age The maximum file age in seconds. + * @param string $subdir Optional. Limit invalidation to the given subdirectory. Default ''. + * @param string $regex Optional. Limit invalidation to files matching the given regular expression. Default ''. + * + * @return void + */ + public function invalidate_files_older_than( $age, $subdir = '', $regex = '' ) { + try { + $now = \time(); + $iterator = $this->get_recursive_file_iterator( $subdir, $regex ); + } catch ( \UnexpectedValueException $e ) { + // Ignore non-existing subdirectories. + return; + } + + foreach ( $iterator as $path => $file ) { + if ( $file->isWritable() && ! $file->isDir() && $now - $file->getMTime() > $age ) { + delete_file( $path ); + } + } + } + + /** + * Retrieves a recursive iterator for all files in the cache. + * + * @since 2.1.0 Visibility changed to protected. + * + * @param string $subdir Optional. Limit invalidation to the given subdirectory. Default ''. + * @param string $regex Optional. Limit invalidation to files matching the given regular expression. Default ''. + * + * @return \OuterIterator + */ + protected function get_recursive_file_iterator( $subdir = '', $regex = '' ) { + $files = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( "{$this->get_base_dir()}{$subdir}", \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + if ( ! empty( $regex ) ) { + /** + * Further filter the collected files using the given regular expression. + * + * @phpstan-var \RegexIterator>> $files + */ + $files = new \RegexIterator( $files, $regex, \RecursiveRegexIterator::MATCH ); + } + + return $files; + } +} diff --git a/includes/avatar-privacy/data-storage/class-network-options.php b/includes/avatar-privacy/data-storage/class-network-options.php new file mode 100644 index 0000000..56df211 --- /dev/null +++ b/includes/avatar-privacy/data-storage/class-network-options.php @@ -0,0 +1,134 @@ + + */ +class Network_Options extends \Avatar_Privacy\Vendor\Mundschenk\Data_Storage\Network_Options { + const PREFIX = 'avatar_privacy_'; + + /** + * The network option key (without the prefix) for using a global table in + * multisite installations. + * + * @var string + */ + const USE_GLOBAL_TABLE = 'use_global_table'; + + /** + * The network option key (without the prefix) for the queue of site IDs to migrate from + * global table usage in multisite installations. + * + * @since 2.1.0 + * + * @var string + */ + const GLOBAL_TABLE_MIGRATION = 'migrate_from_global_table'; + + /** + * The network option key (without the prefix) serving as temporary storagen when + * the the site ID queue is locked. + * + * @since 2.1.0 + * + * @var string + */ + const START_GLOBAL_TABLE_MIGRATION = 'start_global_table_migration'; + + /** + * The network option key (without the prefix) for storing the network-wide salt. + * + * @var string + */ + const SALT = 'salt'; + + /** + * Creates a new instance. + */ + public function __construct() { + parent::__construct( self::PREFIX ); + } + + /** + * Removes the prefix from an option name. + * + * @since 2.1.0 + * + * @param string $name The option name including the prefix. + * + * @return string The option name without the prefix, or '' if an invalid name was given. + */ + public function remove_prefix( $name ) { + $parts = \explode( self::PREFIX, $name, 2 ); + if ( '' === $parts[0] ) { + return $parts[1]; + } + + return ''; + } + + /** + * Tries to write-lock the given option (using a secondary option with the '_lock' + * suffix). + * + * @since 2.1.0 + * + * @param string $option The option name (without the plugin-specific prefix). + * + * @return bool True if the option can be safely set, false otherwise. + */ + public function lock( $option ) { + $now = \microtime( true ); + $secret = \wp_hash( "{$option}|{$now}", 'nonce' ); + $lock = "{$option}_lock"; + + return ! $this->get( $lock ) && $this->set( $lock, $secret ) && $secret === $this->get( $lock ); + } + + /** + * Tries to write-unlock the given option (using a secondary option with the '_lock' + * suffix). + * + * @since 2.1.0 + * + * @param string $option The option name (without the plugin-specific prefix). + * + * @return bool True if the option is now unlocked (either because + * it was not lockedor because unlock was successful), + * false otherwise. + */ + public function unlock( $option ) { + $lock = "{$option}_lock"; + + return ! $this->get( $lock ) || $this->delete( $lock ); + } +} diff --git a/includes/avatar-privacy/data-storage/class-options.php b/includes/avatar-privacy/data-storage/class-options.php new file mode 100644 index 0000000..962343c --- /dev/null +++ b/includes/avatar-privacy/data-storage/class-options.php @@ -0,0 +1,82 @@ + + */ +class Options extends \Avatar_Privacy\Vendor\Mundschenk\Data_Storage\Options { + /** + * The prefix for the plugin options. + * + * @var string + */ + const PREFIX = 'avatar_privacy_'; + + /** + * The name of the option containing the installed plugin version. + * + * @var string + */ + const INSTALLED_VERSION = 'installed_version'; + + /** + * Creates a new instance. + */ + public function __construct() { + parent::__construct( self::PREFIX ); + } + + /** + * Resets the `avatar_default` option to a safe value. + * + * @since 2.1.0 Moved from \Avatar_Privacy\Components\Setup and made non-static. + * + * @return void + */ + public function reset_avatar_default() { + switch ( $this->get( 'avatar_default', null, true ) ) { + case 'rings': + case 'comment': + case 'bubble': + case 'im-user-offline': + case 'bowling-pin': + case 'view-media-artist': + case 'silhouette': + case 'custom': + $this->set( 'avatar_default', 'mystery', true, true ); + break; + + default: + return; + } + } +} diff --git a/includes/avatar-privacy/data-storage/class-site-transients.php b/includes/avatar-privacy/data-storage/class-site-transients.php new file mode 100644 index 0000000..027808b --- /dev/null +++ b/includes/avatar-privacy/data-storage/class-site-transients.php @@ -0,0 +1,45 @@ + + */ +class Site_Transients extends \Avatar_Privacy\Vendor\Mundschenk\Data_Storage\Site_Transients { + const PREFIX = 'avatar_privacy_'; + + /** + * Creates a new instance. + */ + public function __construct() { + parent::__construct( self::PREFIX ); + } +} diff --git a/includes/avatar-privacy/data-storage/class-transients.php b/includes/avatar-privacy/data-storage/class-transients.php new file mode 100644 index 0000000..e9985c1 --- /dev/null +++ b/includes/avatar-privacy/data-storage/class-transients.php @@ -0,0 +1,45 @@ + + */ +class Transients extends \Avatar_Privacy\Vendor\Mundschenk\Data_Storage\Transients { + const PREFIX = 'avatar_privacy_'; + + /** + * Creates a new instance. + */ + public function __construct() { + parent::__construct( self::PREFIX ); + } +} diff --git a/includes/avatar-privacy/data-storage/database/class-comment-author-table.php b/includes/avatar-privacy/data-storage/database/class-comment-author-table.php new file mode 100644 index 0000000..d800aa8 --- /dev/null +++ b/includes/avatar-privacy/data-storage/database/class-comment-author-table.php @@ -0,0 +1,454 @@ + + */ +class Comment_Author_Table extends Table { + + /** + * The table basename without the prefix. + * + * @var string + */ + const TABLE_BASENAME = 'avatar_privacy'; + + /** + * The minimum version not needing a table update. + * + * @var string + */ + const LAST_UPDATED = '2.4.0'; + + /** + * A column/field to placeholder mapping. + * + * @since 2.3.0 + * + * @var string[] + */ + const COLUMN_FORMATS = [ + 'id' => '%d', + 'email' => '%s', + 'use_gravatar' => '%d', + 'last_updated' => '%s', + 'log_message' => '%s', + ]; + + /** + * A list auto-update columns (e.g. date-/timestamps). + * + * @since 2.6.0 + * + * @var string[] + */ + const AUTO_UPDATE_COLS = [ + 'last_updated', + ]; + + /** + * The options handler. + * + * @var Network_Options + */ + private $network_options; + + /** + * Creates a new instance. + * + * @since 2.3.0 Parameter $core added. + * @since 2.4.0 Parameter $core removed. + * + * @param Network_Options $network_options The network options handler. + */ + public function __construct( Network_Options $network_options ) { + parent::__construct( self::TABLE_BASENAME, self::LAST_UPDATED, self::COLUMN_FORMATS, self::AUTO_UPDATE_COLS ); + + $this->network_options = $network_options; + } + + /** + * Sets up the table, including necessary data upgrades. The method is called + * on every page load. + * + * @since 2.4.0 + * + * @param string $previous_version The previously installed plugin version. + * + * @return void + */ + public function setup( $previous_version ) { + parent::setup( $previous_version ); + + // The table is set up correctly, but maybe we need to migrate some data + // from the global table on network installations. + $this->maybe_prepare_migration_queue(); + $this->maybe_migrate_from_global_table(); + } + + /** + * Determines whether this (multisite) installation uses the global table. + * Result is ignored for single-site installations. + * + * @since 2.3.0 Visibility changed to public. + * + * @return bool + */ + public function use_global_table() { + $global_table = (bool) $this->network_options->get( Network_Options::USE_GLOBAL_TABLE, false ); + + /** + * Filters whether a global table should be enabled for multisite installations. + * + * @since 1.0.0 + * + * @param bool $enable Default false, unless this is a multisite installation + * upgraded from version 0.4 or earlier. + */ + return \apply_filters( 'avatar_privacy_enable_global_table', $global_table ); + } + + /** + * Retrieves the CREATE TABLE definition formatted for use by \db_delta(), + * without the charset collate clause. + * + * Example: + * `CREATE TABLE some_table ( + * id mediumint(9) NOT NULL AUTO_INCREMENT, + * some_column varchar(100) NOT NULL, + * PRIMARY KEY (id) + * )` + * + * @since 2.4.0 + * + * @param string $table_name The table name including any prefixes. + * + * @return string + */ + protected function get_table_definition( $table_name ) { + return "CREATE TABLE {$table_name} ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + email varchar(100) NOT NULL, + use_gravatar tinyint(1) DEFAULT NULL, + last_updated datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + log_message varchar(255), + PRIMARY KEY (id), + UNIQUE KEY email (email) + )"; + } + + /** + * Tries set up the migration queue if the trigger is set. + * + * @since 2.4.0 Moved to class Comment_Author_Table. + * + * @return void + */ + protected function maybe_prepare_migration_queue() { + $queue = $this->network_options->get( Network_Options::START_GLOBAL_TABLE_MIGRATION ); + + if ( \is_array( $queue ) && $this->network_options->lock( Network_Options::GLOBAL_TABLE_MIGRATION ) ) { + if ( ! empty( $queue ) ) { + // Store new queue, overwriting any existing queue (since this per network + // and we already got all sites currently in the network). + $this->network_options->set( Network_Options::GLOBAL_TABLE_MIGRATION, $queue ); + } else { + // The "start queue" is empty, which means we should cease the migration efforts. + $this->network_options->delete( Network_Options::GLOBAL_TABLE_MIGRATION ); + } + + // Unlock queue and delete trigger. + $this->network_options->unlock( Network_Options::GLOBAL_TABLE_MIGRATION ); + $this->network_options->delete( Network_Options::START_GLOBAL_TABLE_MIGRATION ); + } + } + + /** + * Tries to migrate global table data if the current site is queued. + * + * @since 2.4.0 Moved to class Comment_Author_Table. + * + * @return void + */ + protected function maybe_migrate_from_global_table() { + if ( + // The plugin is not network-activated (or not on a multisite installation). + ! \is_plugin_active_for_network( \plugin_basename( \AVATAR_PRIVACY_PLUGIN_FILE ) ) || + // The queue is empty. + ! $this->network_options->get( Network_Options::GLOBAL_TABLE_MIGRATION ) || + // The queue is locked. Try again next time. + ! $this->network_options->lock( Network_Options::GLOBAL_TABLE_MIGRATION ) + ) { + // Nothing to see here. + return; + } + + // Check if we are scheduled to migrate data from the global table. + $site_id = \get_current_blog_id(); + $queue = $this->network_options->get( Network_Options::GLOBAL_TABLE_MIGRATION, [] ); + + if ( \is_array( $queue ) && ! empty( $queue[ $site_id ] ) ) { + // Migrate the data. + $this->migrate_from_global_table( $site_id ); + + // Mark this site as done. + unset( $queue[ $site_id ] ); + + if ( ! empty( $queue ) ) { + // Save the new queue. + $this->network_options->set( Network_Options::GLOBAL_TABLE_MIGRATION, $queue ); + } else { + // Delete it. + $this->network_options->delete( Network_Options::GLOBAL_TABLE_MIGRATION ); + } + } + + // Unlock the queue again. + $this->network_options->unlock( Network_Options::GLOBAL_TABLE_MIGRATION ); + } + + /** + * Migrates data from the global database to the given site database. + * + * @since 2.4.0 Parameter $site_id made mandatory. + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param int|null $site_id The site ID. Null means the current $blog_id. + * + * @return int|false The number of migrated rows or false on error. + */ + public function migrate_from_global_table( $site_id ) { + global $wpdb; + + // Get table names. + $global_table_name = $this->get_table_name( \get_main_site_id() ); + $site_table_name = $this->get_table_name( $site_id ); + + // Either we are on the main site or the "use global table" option is enabled. + if ( $global_table_name === $site_table_name ) { + return false; + } + + // Select the rows to migrate. + + /** + * Rows to delete indexed by the ID column in the global table. + * + * @var \stdClass[] + */ + $rows_to_delete = []; + /** + * Rows to migrate indexed by the ID column in the global table. + * + * @var \stdClass[] + */ + $rows_to_migrate = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->prepare( + "SELECT * FROM `{$global_table_name}` WHERE log_message LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "set with comment % (site: %, blog: {$wpdb->esc_like( $site_id )})" + ), + \OBJECT_K + ); + + // Check for existing rows for the same email addresses. + $emails = \wp_list_pluck( $rows_to_migrate, 'email', 'id' ); + $emails_to_ids = \array_flip( $emails ); + $existing_rows = (array) $wpdb->get_results( $this->prepare_email_query( $emails, $site_table_name ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery + foreach ( $existing_rows as $row ) { + $global_row_id = $emails_to_ids[ $row->email ]; + + if ( ! empty( $rows_to_migrate[ $global_row_id ] ) ) { + $global_row = $rows_to_migrate[ $global_row_id ]; + + if ( (int) \strtotime( $row->last_updated ) >= (int) \strtotime( $global_row->last_updated ) ) { + unset( $rows_to_migrate[ $global_row_id ] ); + + // Just delete this row. + $rows_to_delete[ $global_row_id ] = $global_row; + } + } + } + + // Migrated rows need to be deleted, too. + $rows_to_delete = $rows_to_delete + $rows_to_migrate; + + // Do INSERTs and UPDATEs in one query. + $migrated = $this->insert_or_update( [ 'email', 'hash', 'use_gravatar', 'last_updated', 'log_message' ], $rows_to_migrate, $site_id ); + if ( false !== $migrated ) { + // Do DELETEs in one query. + $deleted = 0; + $delete_query = $this->prepare_delete_query( \array_keys( $rows_to_delete ), $global_table_name ); + if ( false !== $delete_query ) { + $deleted = $wpdb->query( $delete_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery + } + + if ( false !== $deleted ) { + // Count the deleted rows if they were not included in the migrated + // rows because they were too old. + return \max( $migrated, $deleted ); + } + } + + return false; + } + + /** + * Prepares the query for selecting existing rows by email. + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param string[] $emails An array of email adresses. + * @param string $table The table name. + * + * @return string|false The prepared query, or false. + */ + protected function prepare_email_query( array $emails, $table ) { + global $wpdb; + + if ( empty( $emails ) || empty( $table ) ) { + return false; + } + + $placeholders = \join( ',', \array_fill( 0, \count( $emails ), '%s' ) ); + + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + return $wpdb->prepare( "SELECT * FROM `{$table}` WHERE email IN ({$placeholders})", $emails ); + } + + /** + * Prepares the query for deleting obsolete rows from the database. + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param int[] $ids_to_delete The IDs to delete. + * @param string $table The table name. + * + * @return string|false The prepared query, or false. + */ + protected function prepare_delete_query( array $ids_to_delete, $table ) { + global $wpdb; + + if ( empty( $ids_to_delete ) || empty( $table ) ) { + return false; + } + + $placeholders = \join( ',', \array_fill( 0, \count( $ids_to_delete ), '%d' ) ); + + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + return $wpdb->prepare( "DELETE FROM `{$table}` WHERE id IN ({$placeholders})", $ids_to_delete ); + } + + /** + * Fixes the table schema when dbDelta cannot cope with the changes. + * + * The table itself is already guaranteed to exist. + * + * @since 2.4.0 + * + * @param string $previous_version The previously installed plugin version. + * + * @return bool True if the schema was modified, false otherwise. + */ + public function maybe_upgrade_schema( $previous_version ) { + $result = false; + + if ( \version_compare( $previous_version, '2.4.0', '<' ) ) { + $result = $this->maybe_drop_hash_column() || $result; // @phpstan-ignore-line -- to make copy & paste less error prone. + $result = $this->maybe_fix_last_updated_column_default() || $result; + } + + return $result; + } + + /** + * Drops the obsolete 'hash' column from the table (if it exists). + * + * @since 2.4.0 + * + * @return bool + */ + protected function maybe_drop_hash_column() { + global $wpdb; + + $table_name = $this->get_table_name(); + + // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQLPlaceholders + if ( 'hash' === $wpdb->get_var( $wpdb->prepare( 'SHOW COLUMNS FROM `%1$s` LIKE \'hash\'', $table_name ) ) ) { + return (bool) $wpdb->query( $wpdb->prepare( 'ALTER TABLE `%1$s` DROP COLUMN hash', $table_name ) ); + } + // phpcs:enable WordPress.DB + + return false; + } + + /** + * Drops the obsolete 'hash' column from the table. + * + * @since 2.4.0 + * + * @return bool + */ + protected function maybe_fix_last_updated_column_default() { + global $wpdb; + + $table_name = $this->get_table_name(); + + // phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQLPlaceholders + $column_definition = $wpdb->get_row( $wpdb->prepare( 'SHOW COLUMNS FROM `%1$s` LIKE \'last_updated\'', $table_name ), \ARRAY_A ); + if ( 'CURRENT_TIMESTAMP' !== $column_definition['Default'] ) { + return (bool) $wpdb->query( $wpdb->prepare( 'ALTER TABLE `%1$s` MODIFY COLUMN `last_updated` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL', $table_name ) ); + } + // phpcs:enable WordPress.DB + + return false; + } + + /** + * Upgrades the table data if necessary. + * + * @since 2.3.0 + * @since 2.4.0 Renamed to maybe_upgrade_data. Parameter $previous_version added. + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param string $previous_version The previously installed plugin version. + * + * @return int The number of upgraded rows. + */ + public function maybe_upgrade_data( $previous_version ) { + return 0; + } +} diff --git a/includes/avatar-privacy/data-storage/database/class-hashes-table.php b/includes/avatar-privacy/data-storage/database/class-hashes-table.php new file mode 100644 index 0000000..542bbdc --- /dev/null +++ b/includes/avatar-privacy/data-storage/database/class-hashes-table.php @@ -0,0 +1,162 @@ + + */ +class Hashes_Table extends Table { + + /** + * The table basename without the prefix. + * + * @var string + */ + const TABLE_BASENAME = 'avatar_privacy_hashes'; + + /** + * The minimum version not needing a table update. + * + * @var string + */ + const LAST_UPDATED = '2.7.0'; + + /** + * A column/field to placeholder mapping. + * + * @var string[] + */ + const COLUMN_FORMATS = [ + 'identifier' => '%s', + 'hash' => '%s', + 'type' => '%s', + 'last_updated' => '%s', + ]; + + /** + * A list auto-update columns (e.g. date-/timestamps). + * + * @since 2.6.0 + * + * @var string[] + */ + const AUTO_UPDATE_COLS = [ + 'last_updated', + ]; + + /** + * Creates a new instance. + */ + public function __construct() { + parent::__construct( self::TABLE_BASENAME, self::LAST_UPDATED, self::COLUMN_FORMATS, self::AUTO_UPDATE_COLS ); + } + + /** + * Determines whether this (multisite) installation uses the global table. + * Result is ignored for single-site installations. + * + * @return bool + */ + public function use_global_table() { + return false; + } + + /** + * Retrieves the CREATE TABLE definition formatted for use by \db_delta(), + * without the charset collate clause. + * + * Example: + * `CREATE TABLE some_table ( + * id mediumint(9) NOT NULL AUTO_INCREMENT, + * some_column varchar(100) NOT NULL, + * PRIMARY KEY (id) + * )` + * + * @param string $table_name The table name including any prefixes. + * + * @return string + */ + protected function get_table_definition( $table_name ) { + $identifier_length = $this->database_supports_large_index() ? 256 : 175; + + return "CREATE TABLE {$table_name} ( + identifier varchar({$identifier_length}) NOT NULL, + hash char(64) CHARACTER SET ascii NOT NULL, + type varchar(20) CHARACTER SET ascii NOT NULL, + last_updated datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY (hash, type), + UNIQUE KEY identifier (identifier, type) + )"; + } + + /** + * Checks if the database server supports large indices (determined by InnoDB version being at least 5.7.0). + * + * @since 2.7.0 + */ + protected function database_supports_large_index(): bool { + global $wpdb; + + $innodb_version = $wpdb->get_var( 'SHOW VARIABLES LIKE "innodb_version"', 1 ) ?? ''; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- No caching necessary, no actual wildcards used. + + return \version_compare( $innodb_version, '5.7', '>=' ); + } + + /** + * Fixes the table schema when dbDelta cannot cope with the changes. + * + * The table itself is already guaranteed to exist. + * + * @param string $previous_version The previously installed plugin version. + * + * @return bool True if the schema was modified, false otherwise. + */ + public function maybe_upgrade_schema( $previous_version ) { + return false; + } + + /** + * Sometimes, the table data needs to updated when upgrading. + * + * The table itself is already guarantueed to exist. + * + * @param string $previous_version The previously installed plugin version. + * + * @return int The number of upgraded rows. + */ + public function maybe_upgrade_data( $previous_version ) { + return 0; + } +} diff --git a/includes/avatar-privacy/data-storage/database/class-table.php b/includes/avatar-privacy/data-storage/database/class-table.php new file mode 100644 index 0000000..7553257 --- /dev/null +++ b/includes/avatar-privacy/data-storage/database/class-table.php @@ -0,0 +1,694 @@ + + * + * @phpstan-type SQLValue int|string|null + * @phpstan-type ColumnValueTuples array + * @phpstan-type ColumnFormats array + */ +abstract class Table { + + /** + * The basename (without site prefix) of the table. + * + * @since 2.4.0 + * + * @var string + */ + private string $table_basename; + + /** + * The minimum version number for which the table does not need to be updated. + * + * @since 2.4.0 + * + * @var string + */ + private string $update_threshold; + + /** + * A column/field to placeholder mapping. + * + * @since 2.3.0 + * + * @var string[] + * + * @phpstan-var ColumnFormats + */ + private array $column_formats; + + /** + * A list auto-update columns (e.g. date-/timestamps), stored in reverse format + * for fast read access (i.e as array). + * + * @since 2.6.0 + * + * @var array + */ + private array $auto_update_cols; + + /** + * Creates a new instance. + * + * @since 2.3.0 Parameter $core added. + * @since 2.4.0 Parameters replaced with $table_basename, $update_threshold, + * and $column_formats. + * @since 2.6.0 Parameter $auto_update_cols added. + * + * @param string $table_basename The basename (without site prefix) of the table. + * @param string $update_threshold The minimum version number for which the table does not need to be updated. + * @param array $column_formats A mapping from column to placeholder characters. + * @param string[] $auto_update_cols A list of auto-update columns. + * + * @phpstan-param ColumnFormats $column_formats + */ + public function __construct( $table_basename, $update_threshold, array $column_formats, array $auto_update_cols ) { + $this->table_basename = $table_basename; + $this->update_threshold = $update_threshold; + $this->column_formats = $column_formats; + $this->auto_update_cols = \array_flip( $auto_update_cols ); + } + + /** + * Sets up the table, including necessary data upgrades. The method is called + * on every page load. + * + * @since 2.4.0 + * + * @param string $previous_version The previously installed plugin version. + * + * @return void + */ + public function setup( $previous_version ) { + if ( $this->maybe_create_table( $previous_version ) ) { + // We may need to fix the schema manually. + $this->maybe_upgrade_schema( $previous_version ); + + // We may need to update the contents as well. + $this->maybe_upgrade_data( $previous_version ); + } + } + + /** + * Retrieves the table prefix to use (for a given site or the current site). + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param int|null $site_id Optional. The site ID. Null means the current $blog_id. Default null. + * + * @return string + */ + protected function get_table_prefix( $site_id = null ) { + global $wpdb; + + if ( ! $this->use_global_table() ) { + return $wpdb->get_blog_prefix( $site_id ); + } else { + return $wpdb->base_prefix; + } + } + + /** + * Retrieves the table name to use (for a given site or the current site). + * + * @since 2.3.0 Visibility changed to public. + * + * @param int|null $site_id Optional. The site ID. Null means the current $blog_id. Default null. + * + * @return string + */ + public function get_table_name( $site_id = null ) { + return $this->get_table_prefix( $site_id ) . $this->table_basename; + } + + /** + * Checks if the given table exists. + * + * @since 2.3.0 Visibility changed to public. + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param string $table_name A table name. + * + * @return bool + */ + public function table_exists( $table_name ) { + global $wpdb; + + return $table_name === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching + } + + /** + * Determines whether this (multisite) installation uses the global table. + * Result is ignored for single-site installations. + * + * @since 2.3.0 Visibility changed to public. + * @since 2.4.0 Made abstract. + * + * @return bool + */ + abstract public function use_global_table(); + + /** + * Creates the plugin's database table if it doesn't already exist. The + * table may be created as a global table for legacy multisite installations. + * Makes the name of the table available through $wpdb->avatar_privacy. + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param string $previous_version The previously installed plugin version. + * + * @return bool Returns true if the table was created/updated. + */ + public function maybe_create_table( $previous_version ) { + global $wpdb; + + // Force DB update? + $db_needs_update = \version_compare( $previous_version, $this->update_threshold, '<' ); + + // Check if the table has already been registered. + if ( ! $db_needs_update && \property_exists( $wpdb, $this->table_basename ) ) { + return false; + } + + // Set up table name and result status. + $table_name = $this->get_table_name(); + $updated = false; + + // Just fix $wpdb object if table already exists, unless we need an update. + if ( ! $db_needs_update && $this->table_exists( $table_name ) ) { + $this->register_table( $wpdb, $table_name ); + } else { + // Create/update the table. + $this->db_delta( $this->get_table_definition( $table_name ) . " {$wpdb->get_charset_collate()};" ); + + if ( ! $this->table_exists( $table_name ) ) { + // There was an error creating the table. + // TODO: Signal catastrophic error to the adminstrator. + return false; + } + + $this->register_table( $wpdb, $table_name ); + $updated = true; + } + + // We may need to update the charset/collation. + return $this->maybe_upgrade_charset_and_collation( $table_name ) || $updated; + } + + /** + * Retrieves the CREATE TABLE definition formatted for use by \db_delta(), + * without the charset collate clause. + * + * Example: + * `CREATE TABLE some_table ( + * id mediumint(9) NOT NULL AUTO_INCREMENT, + * some_column varchar(100) NOT NULL, + * PRIMARY KEY (id) + * )` + * + * @since 2.4.0 + * + * @param string $table_name The table name including any prefixes. + * + * @return string + */ + abstract protected function get_table_definition( $table_name ); + + /** + * Fixes the table schema when dbDelta cannot cope with the changes. + * + * The table itself is already guaranteed to exist. + * + * @since 2.4.0 + * + * @param string $previous_version The previously installed plugin version. + * + * @return bool True if the schema was modified, false otherwise. + */ + abstract public function maybe_upgrade_schema( $previous_version ); + + /** + * Sometimes, the table data needs to updated when upgrading. + * + * The table itself is already guaranteed to exist and have the correct schema. + * + * @param string $previous_version The previously installed plugin version. + * + * @return int The number of upgraded rows. + */ + abstract public function maybe_upgrade_data( $previous_version ); + + /** + * Registers the table with the given \wpdb instance. + * + * @param \wpdb $db The database instance. + * @param string $table_name The table name (with prefix). + * + * @return void + */ + protected function register_table( \wpdb $db, $table_name ) { + $basename = $this->table_basename; + + // Make sure that $wpdb knows about our table. + if ( \is_multisite() && $this->use_global_table() ) { + $db->ms_global_tables[] = $basename; + } else { + $db->tables[] = $basename; + } + + // Also register the "shortcut" property. + $db->$basename = $table_name; + } + + /** + * Fixes the table's charset and/or collation if the WordPress default has + * changed since the table was created (to prevent "Illegal mix of collations" + * errors in joins). Unfortunately, `dbDelta()` does not do that for us (viz. + * [Trac ticket #45697](https://core.trac.wordpress.org/ticket/45697)). + * + * The table itself is already guaranteed to exist. + * + * @since 2.4.4 + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param string $table_name The table name (with prefix). + * + * @return bool True if the collation was modified, false otherwise. + */ + protected function maybe_upgrade_charset_and_collation( $table_name ) { + global $wpdb; + + // Check if the charset and collation set for the table are the same as + // WordPress' default. + $collation = $wpdb->get_var( $wpdb->prepare( 'SELECT table_collation FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = DATABASE() AND table_name = %s', $table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery + if ( $wpdb->collate === $collation ) { + return false; + } + + $columns_clause = ''; + $query_args = [ $table_name ]; + + // Update existing columns first. + $columns = $wpdb->get_results( $wpdb->prepare( "SELECT column_name AS 'name', character_set_name AS 'charset', collation_name AS 'collate', column_type AS 'type', is_nullable AS 'nullable', column_default AS 'default' FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() AND table_name = %s AND collation_name = %s", $table_name, $collation ), \ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQLPlaceholders + if ( ! empty( $columns ) ) { + $alter_table = []; + foreach ( $columns as $c ) { + $default = 'YES' !== $c['nullable'] ? 'NOT NULL ' : ''; + if ( isset( $c['default'] ) ) { + $default .= 'DEFAULT %s'; + $query_args[] = $c['default']; + } + $alter_table[] = "MODIFY `{$c['name']}` {$c['type']} CHARACTER SET {$wpdb->charset} COLLATE {$wpdb->collate} {$default}"; + } + + $columns_clause = ', ' . \join( ', ', $alter_table ); + } + + // Then set the default charset and collation for the table. + return (bool) $wpdb->query( $wpdb->prepare( "ALTER TABLE `%1s` CHARSET {$wpdb->charset} COLLATE {$wpdb->collate}" . $columns_clause, $query_args ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL, WordPress.DB.PreparedSQLPlaceholders + } + + /** + * Applies the `dbDelta` function to the given queries. + * + * @param string|string[] $queries The query to run. Can be multiple queries in an array, or a string of queries separated by semicolons. + * @param bool $execute Optional. Whether or not to execute the query right away. Default `true`. + * + * @return string[] Strings containing the results of the various update queries. + */ + protected function db_delta( $queries, $execute = true ) { + if ( ! function_exists( 'dbDelta' ) ) { + // Load upgrade.php for the dbDelta function. + require_once \ABSPATH . 'wp-admin/includes/upgrade.php'; + } + + return \dbDelta( $queries, $execute ); + } + + /** + * Drops the table for the given site. + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param int|null $site_id Optional. The site ID. Null means the current $blog_id. Ddefault null. + * + * @return void + */ + public function drop_table( $site_id = null ) { + global $wpdb; + + $table_name = $this->get_table_name( $site_id ); + $wpdb->query( "DROP TABLE IF EXISTS {$table_name};" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + + /** + * Retrieves the correct format strings for the given columns. + * + * @since 2.4.0 Moved from Avatar_Privacy\Core\Comment_Author_Fields and renamed to get_format. + * + * @param array $columns An array of values index by column name. + * + * @return string[] + * + * @throws Database_Exception An exception is raised when invalid column names are used. + * + * @phpstan-param ColumnValueTuples $columns + */ + protected function get_format( array $columns ) { + $format_strings = []; + + foreach ( $columns as $key => $value ) { + if ( ! empty( $this->column_formats[ $key ] ) ) { + $format_strings[] = null === $value ? 'NULL' : $this->column_formats[ $key ]; + } else { + throw new Database_Exception( "Invalid column name '{$key}'." ); + } + } + + return $format_strings; + } + + /** + * Inserts a row into the table. + * + * @since 2.4.0 + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param array $data The data to insert (in column => value pairs). + * Both $data columns and $data values should be + * "raw" (neither should be SQL escaped). Sending + * a null value will cause the column to be set to + * NULL - the corresponding format is ignored in + * this case. + * @param int|null $site_id Optional. The site ID. Null means the current + * $blog_id. Default null. + * + * @return int|false The number of rows inserted, or false on error. + * + * @phpstan-param ColumnValueTuples $data + */ + public function insert( array $data, $site_id = null ) { + try { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + return $wpdb->insert( $this->get_table_name( $site_id ), $data, $this->get_format( $data ) ); + } catch ( \RuntimeException $e ) { + return false; + } + } + + /** + * Replaces a row into the table (i.e. it inserts the row if it does not exist + * or deletes and existing row and then inserts the new data). + * + * @since 2.4.0 + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param array $data The data to insert (in column => value pairs). + * Both $data columns and $data values should be + * "raw" (neither should be SQL escaped). Sending + * a null value will cause the column to be set to + * NULL - the corresponding format is ignored in + * this case. + * @param int|null $site_id Optional. The site ID. Null means the current + * $blog_id. Default null. + * + * @return int|false The number of rows updated, or false on error. + * + * @phpstan-param ColumnValueTuples $data + */ + public function replace( array $data, $site_id = null ) { + try { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + return $wpdb->replace( $this->get_table_name( $site_id ), $data, $this->get_format( $data ) ); + } catch ( \RuntimeException $e ) { + return false; + } + } + + /** + * Updates a row in the table. + * + * @since 2.4.0 + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param array $data The data to insert (in column => value pairs). + * Both $data columns and $data values should be + * "raw" (neither should be SQL escaped). Sending + * a null value will cause the column to be set to + * NULL - the corresponding format is ignored in + * this case. + * @param array $where A named array of WHERE clauses (in column => value + * pairs). Multiple clauses will be joined with ANDs. + * Both $where columns and $where values should be + * "raw". Sending a null value will create an IS NULL + * comparison - the corresponding format will be + * ignored in this case. + * @param int|null $site_id Optional. The site ID. Null means the current + * $blog_id. Default null. + * + * @return int|false The number of rows updated, or false on error. + * + * @phpstan-param ColumnValueTuples $data + * @phpstan-param ColumnValueTuples $where + */ + public function update( array $data, array $where, $site_id = null ) { + try { + global $wpdb; + + return $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $this->get_table_name( $site_id ), + $data, + $where, + $this->get_format( $data ), + $this->get_format( $where ) + ); + } catch ( \RuntimeException $e ) { + return false; + } + } + + /** + * Deletes a row from the table. + * + * @since 2.4.0 + * + * @global \wpdb $wpdb The WordPress Database Access Abstraction. + * + * @param array $where A named array of WHERE clauses (in column => value + * pairs). Multiple clauses will be joined with ANDs. + * Both $where columns and $where values should be + * "raw". Sending a null value will create an IS NULL + * comparison - the corresponding format will be + * ignored in this case. + * @param int|null $site_id Optional. The site ID. Null means the current + * $blog_id. Default null. + * + * @return int|false The number of rows deleted, or false on error. + * + * @phpstan-param ColumnValueTuples $where + */ + public function delete( array $where, $site_id = null ) { + try { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + return $wpdb->delete( $this->get_table_name( $site_id ), $where, $this->get_format( $where ) ); + } catch ( \RuntimeException $e ) { + return false; + } + } + + /** + * Inserts or updates multiple rows, as required. + * + * @param string[] $fields An array of database columns. + * @param array $rows An array of row objects or arrays (containing + * field => value tuples). + * @param int|null $site_id Optional. The site ID. Null means the current + * $blog_id. Default null. + * + * @return int|false The number of rows inserted or updated, or false + * on error. + * + * @phpstan-param \stdClass[]|ColumnValueTuples[] $rows + */ + public function insert_or_update( array $fields, array $rows, $site_id = null ) { + try { + global $wpdb; + + // Allow only valid fields. + $fields = \array_intersect( $fields, \array_keys( $this->column_formats ) ); + + if ( empty( $rows ) || empty( $fields ) ) { + return false; + } + + $rows = $this->prepare_rows( $rows, $fields ); + $columns = \join( ',', $fields ); + $values_clause = \join( ',', \array_map( function( $data ) { + return '(' . \join( ',', $this->get_format( $data ) ) . ')'; + }, $rows ) ); + + return $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->prepare( // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + "INSERT INTO `{$this->get_table_name( $site_id )}` ( {$columns} ) + VALUES {$values_clause} + ON DUPLICATE KEY UPDATE {$this->get_update_clause( $fields )}", + $this->prepare_values( $rows ) + ) + ); // phpcs:enable WordPress.DB + } catch ( \RuntimeException $e ) { + return false; + } + } + + /** + * Inserts or updates a single row, as required. + * + * @since 2.6.0 + * + * @param array $data The data to insert (in column => value pairs). + * Both $data columns and $data values should be + * "raw" (neither should be SQL escaped). Sending + * a null value will cause the column to be set to + * NULL - the corresponding format is ignored in + * this case. + * @param int|null $site_id Optional. The site ID. Null means the current + * $blog_id. Default null. + * + * @return int|false The number of rows inserted or updated, or false + * on error. + * + * @phpstan-param ColumnValueTuples $data + */ + public function insert_or_update_row( array $data, $site_id = null ) { + return $this->insert_or_update( \array_keys( $data ), [ $data ], $site_id ); + } + + /** + * Retrieves the update clause based on the updated fields. + * + * @since 2.3.0 + * + * @param string[] $fields An array of database columns. + * + * @return string + */ + protected function get_update_clause( array $fields ) { + + $updated_fields = \array_flip( $fields ); + $update_clause_parts = []; + + foreach ( \array_keys( $this->column_formats ) as $field ) { + if ( isset( $updated_fields[ $field ] ) ) { + $update_clause_parts[] = "{$field} = VALUES({$field})"; + } elseif ( ! isset( $this->auto_update_cols[ $field ] ) ) { + $update_clause_parts[] = "{$field} = {$field}"; + } + } + + return \join( ",\n", $update_clause_parts ); + } + + /** + * Prepares an array of rows for use in queries (i.e. add missing values and + * correctly sort the columns). + * + * @since 2.4.0 + * + * @param array $rows An array of row objects or arrays (containing + * field => value tuples). + * @param string[] $fields An array of database columns. + * + * @return array + * + * @phpstan-param \stdClass[]|ColumnValueTuples[] $rows + * @phpstan-return ColumnValueTuples[] + */ + protected function prepare_rows( array $rows, array $fields ) { + $result = []; + + foreach ( $rows as $data ) { + // Force array syntax (in case we were given an array of row objects). + $data = (array) $data; + $row = []; + foreach ( $fields as $column ) { + $row[ $column ] = isset( $data[ $column ] ) ? $data[ $column ] : null; + } + + $result[] = $row; + } + + return $result; + } + + /** + * Filters non-null values from a prepared database rows array. + * + * @since 2.4.0 + * + * @param array $prepared_rows An array of arrays containing $field => $value tuples. + * + * @return array A flat array containing all non-null values. + * + * @phpstan-param ColumnValueTuples[] $prepared_rows + * @phpstan-return array + */ + protected function prepare_values( array $prepared_rows ) { + $values = []; + + foreach ( $prepared_rows as $row ) { + foreach ( $row as $value ) { + if ( null !== $value ) { + $values[] = $value; + } + } + } + + return $values; + } +} diff --git a/includes/avatar-privacy/exceptions/class-avatar-comment-type-exception.php b/includes/avatar-privacy/exceptions/class-avatar-comment-type-exception.php new file mode 100644 index 0000000..7b696bd --- /dev/null +++ b/includes/avatar-privacy/exceptions/class-avatar-comment-type-exception.php @@ -0,0 +1,38 @@ + + */ +class Avatar_Comment_Type_Exception extends \RuntimeException { +} diff --git a/includes/avatar-privacy/exceptions/class-database-exception.php b/includes/avatar-privacy/exceptions/class-database-exception.php new file mode 100644 index 0000000..16114fa --- /dev/null +++ b/includes/avatar-privacy/exceptions/class-database-exception.php @@ -0,0 +1,37 @@ + + */ +class Database_Exception extends \RuntimeException { +} diff --git a/includes/avatar-privacy/exceptions/class-file-deletion-exception.php b/includes/avatar-privacy/exceptions/class-file-deletion-exception.php new file mode 100644 index 0000000..fd7d7bd --- /dev/null +++ b/includes/avatar-privacy/exceptions/class-file-deletion-exception.php @@ -0,0 +1,39 @@ + + */ +class File_Deletion_Exception extends Filesystem_Exception { +} diff --git a/includes/avatar-privacy/exceptions/class-filesystem-exception.php b/includes/avatar-privacy/exceptions/class-filesystem-exception.php new file mode 100644 index 0000000..acb96d3 --- /dev/null +++ b/includes/avatar-privacy/exceptions/class-filesystem-exception.php @@ -0,0 +1,37 @@ + + */ +class Filesystem_Exception extends \RuntimeException { +} diff --git a/includes/avatar-privacy/exceptions/class-form-field-not-found-exception.php b/includes/avatar-privacy/exceptions/class-form-field-not-found-exception.php new file mode 100644 index 0000000..25b82dd --- /dev/null +++ b/includes/avatar-privacy/exceptions/class-form-field-not-found-exception.php @@ -0,0 +1,39 @@ + + */ +class Form_Field_Not_Found_Exception extends \RuntimeException { +} diff --git a/includes/avatar-privacy/exceptions/class-invalid-nonce-exception.php b/includes/avatar-privacy/exceptions/class-invalid-nonce-exception.php new file mode 100644 index 0000000..905c2ed --- /dev/null +++ b/includes/avatar-privacy/exceptions/class-invalid-nonce-exception.php @@ -0,0 +1,38 @@ + + */ +class Invalid_Nonce_Exception extends \RuntimeException { +} diff --git a/includes/avatar-privacy/exceptions/class-object-factory-exception.php b/includes/avatar-privacy/exceptions/class-object-factory-exception.php new file mode 100644 index 0000000..d055569 --- /dev/null +++ b/includes/avatar-privacy/exceptions/class-object-factory-exception.php @@ -0,0 +1,37 @@ + + */ +class Object_Factory_Exception extends \RuntimeException { +} diff --git a/includes/avatar-privacy/exceptions/class-part-files-not-found-exception.php b/includes/avatar-privacy/exceptions/class-part-files-not-found-exception.php new file mode 100644 index 0000000..bbe938a --- /dev/null +++ b/includes/avatar-privacy/exceptions/class-part-files-not-found-exception.php @@ -0,0 +1,38 @@ + + */ +class Part_Files_Not_Found_Exception extends \RuntimeException { +} diff --git a/includes/avatar-privacy/exceptions/class-png-image-exception.php b/includes/avatar-privacy/exceptions/class-png-image-exception.php new file mode 100644 index 0000000..a77235d --- /dev/null +++ b/includes/avatar-privacy/exceptions/class-png-image-exception.php @@ -0,0 +1,37 @@ + + */ +class PNG_Image_Exception extends \RuntimeException { +} diff --git a/includes/avatar-privacy/exceptions/class-upload-handling-exception.php b/includes/avatar-privacy/exceptions/class-upload-handling-exception.php new file mode 100644 index 0000000..5112677 --- /dev/null +++ b/includes/avatar-privacy/exceptions/class-upload-handling-exception.php @@ -0,0 +1,37 @@ + + */ +class Upload_Handling_Exception extends \RuntimeException { +} diff --git a/includes/avatar-privacy/functions.php b/includes/avatar-privacy/functions.php new file mode 100644 index 0000000..9ebe274 --- /dev/null +++ b/includes/avatar-privacy/functions.php @@ -0,0 +1,59 @@ + + */ +class BBPress_Integration implements Plugin_Integration { + + /** + * The form helper. + * + * @var User_Form + */ + private User_Form $form; + + /** + * Creates a new instance. + * + * @since 2.1.0 Parameter $plugin_file removed. + * @since 2.3.0 Parameter $user_profile replaced by $form. + * + * @param User_Form $form The form handling helper. + */ + public function __construct( User_Form $form ) { + $this->form = $form; + } + + /** + * Check if the bbPress integration should be activated. + * + * @return bool + */ + public function check() { + return \function_exists( 'is_bbpress' ); + } + + /** + * Activate the integration. + * + * @@return void + */ + public function run() { + if ( ! \is_admin() ) { + \add_action( 'init', [ $this, 'init' ] ); + } + } + + /** + * Init action handler. + * + * @return void + */ + public function init() { + // Load user data from email for bbPress. + \add_filter( 'avatar_privacy_parse_id_or_email', [ $this, 'parse_id_or_email' ] ); + + // Add profile picture upload and `use_gravatar` checkbox to frontend profile editor. + \add_action( 'bbp_user_edit_after', [ $this, 'add_user_profile_fields' ] ); + \add_action( 'personal_options_update', [ $this->form, 'save' ] ); + \add_action( 'edit_user_profile_update', [ $this->form, 'save' ] ); + } + + /** + * Loads user ID from email if using bbPress. + * + * @param array $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. + * @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. + * } + * + * @return array { + * The filtered data. + * + * @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. + * } + * + * @phpstan-param array{0: int|false, 1: string, 2: int} $data + * @phpstan-return array{0: int|false, 1: string, 2: int} + */ + public function parse_id_or_email( $data ) { + list( $user_id, $email, $age ) = $data; + + if ( \is_bbpress() && false === $user_id ) { + $user = \get_user_by( 'email', $email ); + + if ( ! empty( $user ) ) { + $user_id = $user->ID; + } + } + + return [ $user_id, $email, $age ]; + } + + /** + * Add user profile fields for bbPress. + * + * @return void + */ + public function add_user_profile_fields() { + // Get user ID from bbPress. + $user_id = \bbp_get_user_id( 0, true, false ); + if ( empty( $user_id ) ) { + return; + } + + // Include partial. + $this->form->print_form( 'public/partials/bbpress/user-profile-picture.php', $user_id ); + } +} diff --git a/includes/avatar-privacy/integrations/class-buddypress-integration.php b/includes/avatar-privacy/integrations/class-buddypress-integration.php new file mode 100644 index 0000000..36671cd --- /dev/null +++ b/includes/avatar-privacy/integrations/class-buddypress-integration.php @@ -0,0 +1,275 @@ + + * + * @phpstan-import-type AvatarDefinition from User_Fields + * + * @phpstan-type BuddyPressAvatarParams array{ + * item_id: int|false, + * object: string, + * type: string, + * avatar_dir: string|false, + * width: int|false, + * height: int|false, + * class: string, + * css_id: string|false, + * title: string, + * alt: string, + * email: string|false, + * no_grav: bool, + * html: bool, + * extra_attr: string, + * scheme: string, + * rating: string, + * force_default: bool + * } + */ +class BuddyPress_Integration implements Plugin_Integration { + + /** + * The user data helper. + * + * @since 2.4.0 + * + * @var User_Fields + */ + private User_Fields $user_fields; + + /** + * Creates a new instance. + * + * @since 2.4.0 Parameter $uploads removed, $user_fields added. + * + * @param User_Fields $user_fields The user data API. + */ + public function __construct( User_Fields $user_fields ) { + $this->user_fields = $user_fields; + } + + /** + * Check if the BuddyPress integration should be activated. + * + * @return bool + */ + public function check() { + return \class_exists( \BuddyPress::class ); + } + + /** + * Activate the integration. + * + * @return void + */ + public function run() { + // Integrate with BuddyPress's avatar handling. + \add_action( 'init', [ $this, 'integrate_with_buddypress_avatars' ] ); + } + + /** + * Removes the BuddyPress avatar filter and disables Avatar Privacy + * profile image upload. + * + * @return void + */ + public function integrate_with_buddypress_avatars() { + if ( ! \function_exists( 'bp_get_version' ) ) { + return; + } + + // Set version dependent hooks. + $version = \bp_get_version(); + if ( \version_compare( $version, '6.0.0', '<' ) ) { + $avatar_uploaded_hook = 'xprofile_avatar_uploaded'; + } else { + $avatar_uploaded_hook = 'bp_members_avatar_uploaded'; + } + + // Remove BuddyPress avatar filter. + \remove_filter( 'get_avatar_url', 'bp_core_get_avatar_data_url_filter', 10 ); + + // Disable BuddyPress Gravatar usage. + \add_filter( 'bp_core_fetch_avatar_no_grav', '__return_true', 10, 0 ); + + // Disable profile image uploading. + \add_filter( 'avatar_privacy_profile_picture_upload_disabled', '__return_true', 10, 0 ); + + // Serve BuddyPress profile pictures via the filesystem cache. + \add_filter( 'avatar_privacy_pre_get_user_avatar', [ $this, 'enable_buddypress_user_avatars' ], 10, 2 ); + + // Add our own default avatars instead (for users). + \add_filter( 'bp_core_default_avatar_user', [ $this, 'add_default_avatars_to_buddypress' ], 10, 2 ); + \add_filter( 'bp_get_user_has_avatar', [ $this, 'has_buddypress_avatar' ], 10, 2 ); + + // Invalidate cache when a new image is uploaded or deleted. + \add_action( $avatar_uploaded_hook, [ $this, 'invalidate_cache_after_avatar_upload' ], 10, 3 ); + \add_action( 'bp_core_delete_existing_avatar', [ $this, 'invalidate_cache_after_avatar_deletion' ], 10, 1 ); + } + + /** + * Retrieves the user avatar from BuddyPress. + * + * @param array|null $avatar Optional. The user avatar information. Default null. + * @param int $user_id The user ID. + * + * @return array|null { + * Optional. The user avatar information. Default null. + * + * @type string $file The local filename. + * @type string $type The MIME type. + * } + * + * @phpstan-param AvatarDefinition|null $avatar + * @phpstan-return AvatarDefinition|null + */ + public function enable_buddypress_user_avatars( array $avatar = null, $user_id ) { + // Prevent loops. + if ( \doing_filter( 'bp_core_default_avatar_user' ) ) { + return $avatar; + } + + // Retrieve BuddyPress user data. + $avatar = $this->get_buddypress_avatar( $user_id ); + + if ( ! empty( $avatar ) ) { + $file = \ABSPATH . \wp_make_link_relative( $avatar ); + $type = \wp_check_filetype( $file )['type']; + + if ( ! empty( $type ) ) { + return [ + 'file' => $file, + 'type' => $type, + ]; + } + } + + return null; + } + + /** + * Invalidates the file cache after a new BuddyPress avatar has been uploaded. + * + * @param int $item_id The user ID (if `$args['object']` is `user` ). + * @param string $type Information about the capture method for the avatar. + * @param array $args Arguments for the avatar function. + * + * @return void + * + * @phpstan-param BuddyPressAvatarParams $args + */ + public function invalidate_cache_after_avatar_upload( $item_id, $type, array $args ) { + if ( ! empty( $args['object'] ) && 'user' === $args['object'] && ! empty( $item_id ) ) { + $this->user_fields->invalidate_local_avatar_cache( $item_id ); + } + } + + /** + * Invalidates the file cache after a new BuddyPress avatar has been deleted. + * + * @since 2.4.0 + * + * @param array $args Array of arguments used for avatar deletion. + * + * @return void + * + * @phpstan-param BuddyPressAvatarParams $args + */ + public function invalidate_cache_after_avatar_deletion( array $args ) { + if ( ! empty( $args['item_id'] ) ) { + $this->invalidate_cache_after_avatar_upload( $args['item_id'], 'delete', $args ); + } + } + + /** + * Adds Avatar Privacy's default avatars to BuddyPress. + * + * @since 2.4.0 + * @since 2.7.0 Parameter `$default` renamed to `$original_default` to prevent conflict with reserved keyword. + * + * @param string $original_default Default avatar for non-gravatar requests. + * @param array $params Array of parameters for the avatar request. + * + * @return string + * + * @phpstan-param BuddyPressAvatarParams $params + */ + public function add_default_avatars_to_buddypress( $original_default, array $params ) { + // Retrieve default avatar URL (Gravatar or local default avatar). + $args = [ + 'rating' => $params['rating'], + 'size' => (int) $params['width'], + ]; + $default_avatar = \get_avatar_url( $params['item_id'], $args ); + + return $default_avatar ?: $original_default; + } + + /** + * Determines whether a user has an avatar that has been uploaded in BuddyPress. + * + * @since 2.4.0 + * + * @param bool $retval The return value calculated by BuddyPress (ignored). + * @param int $user_id The user ID. + * + * @return bool + */ + public function has_buddypress_avatar( $retval, $user_id ) { + return ! empty( $this->get_buddypress_avatar( $user_id ) ); + } + + /** + * Retrieves the user avatar uploaded in BuddyPress (if any). + * + * @since 2.4.0 + * + * @param int $user_id The user ID. + * + * @return string + */ + protected function get_buddypress_avatar( $user_id ) { + \add_filter( 'bp_core_default_avatar_user', '__return_empty_string' ); + $avatar = \bp_core_fetch_avatar( [ + 'item_id' => $user_id, + 'object' => 'user', + 'type' => 'full', + 'html' => false, + 'no_grav' => true, + ] ); + \remove_filter( 'bp_core_default_avatar_user', '__return_empty_string' ); + + return $avatar; + } +} diff --git a/includes/avatar-privacy/integrations/class-plugin-integration.php b/includes/avatar-privacy/integrations/class-plugin-integration.php new file mode 100644 index 0000000..4ad0b65 --- /dev/null +++ b/includes/avatar-privacy/integrations/class-plugin-integration.php @@ -0,0 +1,45 @@ + + */ +interface Plugin_Integration extends \Avatar_Privacy\Component { + + /** + * Check if the ACF integration should be activated. + * + * @return bool + */ + public function check(); +} diff --git a/includes/avatar-privacy/integrations/class-simple-author-box-integration.php b/includes/avatar-privacy/integrations/class-simple-author-box-integration.php new file mode 100644 index 0000000..208b794 --- /dev/null +++ b/includes/avatar-privacy/integrations/class-simple-author-box-integration.php @@ -0,0 +1,170 @@ + + * + * @phpstan-import-type AvatarDefinition from User_Fields + */ +class Simple_Author_Box_Integration implements Plugin_Integration { + + const USER_META_KEY = 'sabox-profile-image'; + + /** + * The user data helper. + * + * @var User_Fields + */ + private User_Fields $user_fields; + + /** + * Creates a new instance. + * + * @param User_Fields $user_fields The user data API. + */ + public function __construct( User_Fields $user_fields ) { + $this->user_fields = $user_fields; + } + + /** + * Check if the bbPress integration should be activated. + * + * @return bool + */ + public function check() { + return \class_exists( Simple_Author_Box::class ); + } + + /** + * Activate the integration. + * + * @return void + */ + public function run() { + \add_action( 'init', [ $this, 'init' ] ); + } + + /** + * Init action handler. + * + * @return void + */ + public function init() { + $simple_author_box = Simple_Author_Box::get_instance(); + + // Disable profile image uploading. + \add_filter( 'avatar_privacy_profile_picture_upload_disabled', '__return_true', 10, 0 ); + + // Disable Simple Author Box' avatar integration. + \remove_filter( 'get_avatar', [ $simple_author_box, 'replace_gravatar_image' ], 10 ); + + // Serve Simple Author Box profile pictures via the filesystem cache. + \add_filter( 'avatar_privacy_pre_get_user_avatar', [ $this, 'enable_user_avatars' ], 10, 2 ); + + // Invalidate cache when a new image is uploaded or deleted. + \add_action( 'added_user_meta', [ $this, 'invalidate_cache_after_avatar_change' ], 10, 3 ); + \add_action( 'updated_user_meta', [ $this, 'invalidate_cache_after_avatar_change' ], 10, 3 ); + \add_action( 'deleted_user_meta', [ $this, 'invalidate_cache_after_avatar_change' ], 10, 3 ); + } + + /** + * Retrieves the user avatar from Simple Author Box. + * + * @param array|null $avatar Optional. The user avatar information. Default null. + * @param int $user_id The user ID. + * + * @return array|null { + * Optional. The user avatar information. Default null. + * + * @type string $file The local filename. + * @type string $type The MIME type. + * } + * + * @phpstan-param AvatarDefinition|null $avatar + * @phpstan-return AvatarDefinition|null + */ + public function enable_user_avatars( array $avatar = null, $user_id ) { + // Retrieve Simple Author Box image. + $local_avatar = $this->get_simple_author_box_avatar( $user_id ); + + if ( ! empty( $local_avatar ) ) { + $file = \ABSPATH . \wp_make_link_relative( $local_avatar ); + $type = \wp_check_filetype( $file )['type']; + if ( ! empty( $type ) ) { + return [ + 'file' => $file, + 'type' => $type, + ]; + } + } + + return $avatar; + } + + /** + * Retrieves the user avatar uploaded in Simple Author Box (if any). + * + * @param int $user_id The user ID. + * + * @return string + */ + protected function get_simple_author_box_avatar( $user_id ) { + $avatar = \get_user_meta( $user_id, self::USER_META_KEY, true ); + if ( \is_string( $avatar ) ) { + return $avatar; + } + + return ''; + } + + /** + * Invalidates the file cache after a new Simple Author Box avatar has been + * uploaded or deleted. + * + * @param int $meta_id ID of updated metadata entry. + * @param int $user_id The user ID. + * @param string $meta_key Metadata key. + * + * @return void + */ + public function invalidate_cache_after_avatar_change( $meta_id, $user_id, $meta_key ) { + if ( self::USER_META_KEY !== $meta_key ) { + return; + } + + $this->user_fields->invalidate_local_avatar_cache( $user_id ); + } +} diff --git a/includes/avatar-privacy/integrations/class-simple-local-avatars-integration.php b/includes/avatar-privacy/integrations/class-simple-local-avatars-integration.php new file mode 100644 index 0000000..c1f0225 --- /dev/null +++ b/includes/avatar-privacy/integrations/class-simple-local-avatars-integration.php @@ -0,0 +1,173 @@ + + * + * @phpstan-import-type AvatarDefinition from User_Fields + */ +class Simple_Local_Avatars_Integration implements Plugin_Integration { + + const USER_META_KEY = 'simple_local_avatar'; + + /** + * The user data helper. + * + * @var User_Fields + */ + private User_Fields $user_fields; + + /** + * Creates a new instance. + * + * @param User_Fields $user_fields The user data API. + */ + public function __construct( User_Fields $user_fields ) { + $this->user_fields = $user_fields; + } + + /** + * Check if the bbPress integration should be activated. + * + * @return bool + */ + public function check() { + return \class_exists( Simple_Local_Avatars::class ) && \method_exists( Simple_Local_Avatars::class, 'get_avatar_rest' ); + } + + /** + * Activate the integration. + * + * @return void + */ + public function run() { + \add_action( 'init', [ $this, 'init' ] ); + } + + /** + * Init action handler. + * + * @global Simple_Local_Avatars $simple_local_avatars The Simple Local Avatars singleton. + * + * @return void + */ + public function init() { + global $simple_local_avatars; + + // Disable profile image uploading. + \add_filter( 'avatar_privacy_profile_picture_upload_disabled', '__return_true', 10, 0 ); + + // Disable Simple Local Avatars' avatar integration. + \remove_filter( 'pre_get_avatar_data', [ $simple_local_avatars, 'get_avatar_data' ], 10 ); + + // Serve Simple Local Avatars profile pictures via the filesystem cache. + \add_filter( 'avatar_privacy_pre_get_user_avatar', [ $this, 'enable_user_avatars' ], 10, 2 ); + + // Invalidate cache when a new image is uploaded or deleted. + \add_action( 'added_user_meta', [ $this, 'invalidate_cache_after_avatar_change' ], 10, 3 ); + \add_action( 'updated_user_meta', [ $this, 'invalidate_cache_after_avatar_change' ], 10, 3 ); + \add_action( 'deleted_user_meta', [ $this, 'invalidate_cache_after_avatar_change' ], 10, 3 ); + } + + /** + * Retrieves the user avatar from Simple Local Avatars. + * + * @param array|null $avatar Optional. The user avatar information. Default null. + * @param int $user_id The user ID. + * + * @return array|null { + * Optional. The user avatar information. Default null. + * + * @type string $file The local filename. + * @type string $type The MIME type. + * } + * + * @phpstan-param AvatarDefinition|null $avatar + * @phpstan-return AvatarDefinition|null + */ + public function enable_user_avatars( array $avatar = null, $user_id ) { + // Retrieve Simple Local Avatars image. + $local_avatar = $this->get_simple_local_avatars_avatar( $user_id ); + + if ( ! empty( $local_avatar ) ) { + $type = \wp_check_filetype( $local_avatar )['type']; + if ( ! empty( $type ) ) { + return [ + 'file' => $local_avatar, + 'type' => $type, + ]; + } + } + + return $avatar; + } + + /** + * Retrieves the user avatar uploaded in Simple Local Avatars (if any). + * + * @param int $user_id The user ID. + * + * @return string + */ + protected function get_simple_local_avatars_avatar( $user_id ) { + global $simple_local_avatars; + + $local_avatar = $simple_local_avatars->get_avatar_rest( [ 'id' => $user_id ] ); + if ( ! \is_array( $local_avatar ) || empty( $local_avatar['full'] ) || ! \is_string( $local_avatar['full'] ) ) { + return ''; + } + + return $local_avatar['full']; + } + + /** + * Invalidates the file cache after a new Simple Local Avatars avatar has been + * uploaded or deleted. + * + * @param int|string[] $meta_id The ID(s) of the modified metadata entries. + * @param int $user_id The user ID. + * @param string $meta_key The metadata key. + * + * @return void + */ + public function invalidate_cache_after_avatar_change( $meta_id, $user_id, $meta_key ) { + if ( self::USER_META_KEY !== $meta_key || empty( $meta_id ) ) { + return; + } + + $this->user_fields->invalidate_local_avatar_cache( $user_id ); + } +} diff --git a/includes/avatar-privacy/integrations/class-simple-user-avatar-integration.php b/includes/avatar-privacy/integrations/class-simple-user-avatar-integration.php new file mode 100644 index 0000000..55a3891 --- /dev/null +++ b/includes/avatar-privacy/integrations/class-simple-user-avatar-integration.php @@ -0,0 +1,164 @@ + + * + * @phpstan-import-type AvatarDefinition from User_Fields + */ +class Simple_User_Avatar_Integration implements Plugin_Integration { + + /** + * The user data helper. + * + * @var User_Fields + */ + private User_Fields $user_fields; + + /** + * Creates a new instance. + * + * @param User_Fields $user_fields The user data API. + */ + public function __construct( User_Fields $user_fields ) { + $this->user_fields = $user_fields; + } + + /** + * Check if the bbPress integration should be activated. + * + * @return bool + */ + public function check() { + return \defined( 'SUA_USER_META_KEY' ) && \class_exists( SimpleUserAvatar_Public::class ); + } + + /** + * Activate the integration. + * + * @return void + */ + public function run() { + // Remove "hardcoded" Simple User Avatar frontend integration. + \remove_action( 'plugins_loaded', [ 'SimpleUserAvatar_Public', 'init' ] ); + + // Add our own stuff. + \add_action( 'init', [ $this, 'init' ] ); + } + + /** + * Init action handler. + * + * @return void + */ + public function init() { + // Disable profile image uploading. + \add_filter( 'avatar_privacy_profile_picture_upload_disabled', '__return_true', 10, 0 ); + + // Serve Simple User Avatar profile pictures via the filesystem cache. + \add_filter( 'avatar_privacy_pre_get_user_avatar', [ $this, 'enable_user_avatars' ], 10, 2 ); + + // Invalidate cache when a new image is uploaded or deleted. + \add_action( 'deleted_user_meta', [ $this, 'invalidate_cache_after_avatar_change' ], 10, 3 ); + } + + /** + * Retrieves the user avatar from Simple Author Box. + * + * @param array|null $avatar Optional. The user avatar information. Default null. + * @param int $user_id The user ID. + * + * @return array|null { + * Optional. The user avatar information. Default null. + * + * @type string $file The local filename. + * @type string $type The MIME type. + * } + * + * @phpstan-param AvatarDefinition|null $avatar + * @phpstan-return AvatarDefinition|null + */ + public function enable_user_avatars( array $avatar = null, $user_id ) { + // Retrieve Simple Author Box image. + $local_avatar = $this->get_simple_user_avatar_avatar( $user_id ); + + if ( ! empty( $local_avatar ) ) { + $type = \wp_check_filetype( $local_avatar )['type']; + if ( ! empty( $type ) ) { + return [ + 'file' => $local_avatar, + 'type' => $type, + ]; + } + } + + return $avatar; + } + + /** + * Retrieves the user avatar uploaded in Simple User Avatar (if any). + * + * @param int $user_id The user ID. + * + * @return string + */ + protected function get_simple_user_avatar_avatar( $user_id ) { + $attachment_id = \get_user_meta( $user_id, \SUA_USER_META_KEY, true ); + if ( \is_numeric( $attachment_id ) ) { + return (string) \get_attached_file( (int) $attachment_id ); + } + + return ''; + } + + /** + * Invalidates the file cache after a new Simple Author Box avatar has been + * uploaded or deleted. + * + * @param string[] $meta_ids IDs of updated metadata entry. + * @param int $user_id The user ID. + * @param string $meta_key Metadata key. + * + * @return void + */ + public function invalidate_cache_after_avatar_change( $meta_ids, $user_id, $meta_key ) { + if ( \SUA_USER_META_KEY !== $meta_key || empty( $meta_ids ) ) { + return; + } + + $this->user_fields->invalidate_local_avatar_cache( $user_id ); + } +} diff --git a/includes/avatar-privacy/integrations/class-theme-my-login-profiles-integration.php b/includes/avatar-privacy/integrations/class-theme-my-login-profiles-integration.php new file mode 100644 index 0000000..cba850f --- /dev/null +++ b/includes/avatar-privacy/integrations/class-theme-my-login-profiles-integration.php @@ -0,0 +1,145 @@ + + */ +class Theme_My_Login_Profiles_Integration implements Plugin_Integration { + /** + * The form helper. + * + * @var User_Form + */ + private $form; + + /** + * Creates a new instance. + * + * @param User_Form $form The form handling helper. + */ + public function __construct( User_Form $form ) { + $this->form = $form; + } + + /** + * Check if the bbPress integration should be activated. + * + * @return bool + */ + public function check() { + return \class_exists( Theme_My_Login_Profiles::class ) + && \function_exists( 'tml_get_form' ) + && \function_exists( 'tml_add_form_field' ); + } + + /** + * Activate the integration. + * + * @return void + */ + public function run() { + \add_action( 'init', [ $this, 'integrate_with_theme_my_login' ] ); + } + + /** + * Integrates with Theme My Login after all plugins have been loaded. + * + * @return void + */ + public function integrate_with_theme_my_login() { + $tml_form = \tml_get_form( 'profile' ); + if ( ! $tml_form instanceof Theme_My_Login_Form ) { + // Profiles extension not set up. + return; + } + + $avatar_field = $tml_form->get_field( 'avatar' ); + if ( ! $avatar_field instanceof Theme_My_Login_Form_Field ) { + // Avatars seem to be disabled. + return; + } + + // Add proper encoding for uploading. + $tml_form->add_attribute( 'enctype', 'multipart/form-data' ); + + // Change render callback for avatar field. + $avatar_field->set_content( [ $this, 'render_avatar_field' ] ); + + // Add additional fields. + \tml_add_form_field( $tml_form, 'avatar_privacy_use_gravatar', [ + 'type' => 'custom', + 'content' => [ $this, 'render_use_gravatar_checkbox' ], + 'priority' => 86, + ] ); + \tml_add_form_field( $tml_form, 'avatar_privacy_allow_anonymous', [ + 'type' => 'custom', + 'content' => [ $this, 'render_allow_anonymous_checkbox' ], + 'priority' => 86, + ] ); + + // Save the added fields. + \add_action( 'personal_options_update', [ $this->form, 'save' ] ); + } + + /** + * Renders the avatar uploader for use with Theme_My_Login_Profiles. + * + * @return string + */ + public function render_avatar_field() { + return $this->form->get_avatar_uploader( \get_current_user_id() ); + } + + /** + * Renders the "use gravatar" checkbox for use with Theme_My_Login_Profiles. + * + * @return string + */ + public function render_use_gravatar_checkbox() { + return $this->form->get_use_gravatar_checkbox( \get_current_user_id() ); + } + + /** + * Renders the "allow anonymous" checkbox for use with Theme_My_Login_Profiles. + * + * @return string + */ + public function render_allow_anonymous_checkbox() { + return $this->form->get_allow_anonymous_checkbox( \get_current_user_id() ); + } +} diff --git a/includes/avatar-privacy/integrations/class-ultimate-member-integration.php b/includes/avatar-privacy/integrations/class-ultimate-member-integration.php new file mode 100644 index 0000000..c11716b --- /dev/null +++ b/includes/avatar-privacy/integrations/class-ultimate-member-integration.php @@ -0,0 +1,177 @@ + + * + * @phpstan-import-type AvatarDefinition from \Avatar_Privacy\Core\User_Fields + * + * @phpstan-type UltimateMemberSettingsField array{ id: string, conditional: array{ 0: string, 1: string, 2: int } } + * @phpstan-type UltimateMemberSettingsStructure array{ '': array{ sections: array{ users: array{ fields: UltimateMemberSettingsField[] } } } } + */ +class Ultimate_Member_Integration implements Plugin_Integration { + + /** + * The avatar upload handler. + * + * @var User_Avatar_Upload_Handler + */ + private User_Avatar_Upload_Handler $upload; + + /** + * Creates a new instance. + * + * @param User_Avatar_Upload_Handler $upload The avatar upload handler. + */ + public function __construct( User_Avatar_Upload_Handler $upload ) { + $this->upload = $upload; + } + + /** + * Check if the Ultimate Member integration should be activated. + * + * @return bool + */ + public function check() { + return \class_exists( \UM::class ) && \function_exists( 'um_get_user_avatar_data' ); + } + + /** + * Activate the integration. + * + * @return void + */ + public function run() { + // Integrate with Ultimate Member's avatar handling. + \add_action( 'init', [ $this, 'integrate_with_ultimate_member_avatars' ] ); + + // Disable Ultimate Member's 'use_gravatars' setting a bit earlier to be sure.. + \add_filter( 'um_settings_structure', [ $this, 'remove_ultimate_member_gravatar_settings' ], 10, 1 ); + \add_filter( 'um_options_use_gravatars', '__return_false', 10, 1 ); + } + + /** + * Removes the Ultimate Member avatar filter and disables Avatar Privacy + * profile image upload. + * + * @return void + */ + public function integrate_with_ultimate_member_avatars() { + // Remove Ultimate Member avatar filter. + \remove_filter( 'get_avatar', 'um_get_avatar', 99999 ); + + // Disable profile image uploading. + \add_filter( 'avatar_privacy_profile_picture_upload_disabled', '__return_true', 10, 0 ); + + // Serve Ultime Member profile pictures via the filesystem cache. + \add_filter( 'avatar_privacy_pre_get_user_avatar', [ $this, 'enable_ultimate_member_user_avatars' ], 10, 2 ); + + // Invalidate cache when a new image is uploaded. + \add_action( 'um_after_upload_db_meta_profile_photo', [ $this->upload, 'invalidate_user_avatar_cache' ], 10, 1 ); + } + + /** + * Filters the Ultimate Member settings structure to remove conflicting checkboxes + * on Gravatar use. + * + * @param array $settings_structure The settings page definition. + * + * @return array + * + * @phpstan-param UltimateMemberSettingsStructure $settings_structure + * @phpstan-return UltimateMemberSettingsStructure + */ + public function remove_ultimate_member_gravatar_settings( array $settings_structure ) { + if ( + isset( $settings_structure['']['sections']['users']['fields'] ) && + \is_array( $settings_structure['']['sections']['users']['fields'] ) + ) { + /** + * Iterate over the fields. + * + * @phpstan-var UltimateMemberSettingsField $field + */ + foreach ( $settings_structure['']['sections']['users']['fields'] as &$field ) { + if ( 'use_gravatars' === $field['id'] ) { + // Make conditional on non-existing setting (hiding it). + $field['conditional'] = [ 'avatar_privacy_active', '=', 0 ]; + + // That's all. + break; + } + } + } + + return $settings_structure; + } + + /** + * Retrieves the user avatar from Ultimate Member. + * + * @param array|null $avatar Optional. The user avatar information. Default null. + * @param int $user_id The user ID. + * + * @return array|null { + * Optional. The user avatar information. Default null. + * + * @type string $file The local filename. + * @type string $type The MIME type. + * } + * + * @phpstan-param AvatarDefinition|null $avatar + * @phpstan-return AvatarDefinition|null + */ + public function enable_ultimate_member_user_avatars( array $avatar = null, $user_id ) { + // Retrieve Ultimate Member user data. + \add_filter( 'um_filter_avatar_cache_time', '__return_null' ); + $um_profile = \um_get_user_avatar_data( $user_id, 'original' ); + \remove_filter( 'um_filter_avatar_cache_time', '__return_null' ); + + if ( empty( $um_profile['type'] ) || 'upload' !== $um_profile['type'] || empty( $um_profile['url'] ) ) { + return null; + } + + $file = \ABSPATH . \wp_make_link_relative( $um_profile['url'] ); + $type = \wp_check_filetype( $file )['type']; + + if ( ! empty( $type ) ) { + return [ + 'file' => $file, + 'type' => $type, + ]; + } + + return null; + } +} diff --git a/includes/avatar-privacy/integrations/class-wp-user-manager-integration.php b/includes/avatar-privacy/integrations/class-wp-user-manager-integration.php new file mode 100644 index 0000000..4e4a13a --- /dev/null +++ b/includes/avatar-privacy/integrations/class-wp-user-manager-integration.php @@ -0,0 +1,161 @@ + + * + * @phpstan-import-type AvatarDefinition from User_Fields + */ +class WP_User_Manager_Integration implements Plugin_Integration { + + const WP_USER_MANAGER_META_KEY = 'current_user_avatar'; + + /** + * The user data helper. + * + * @since 2.4.0 + * + * @var User_Fields + */ + private User_Fields $user_fields; + + /** + * A flag indicating that the user avatar cache should be invalidated soon. + * + * @var bool + */ + private bool $flush_cache = false; + + /** + * Creates a new instance. + * + * @since 2.4.0 Parameter $uploads removed, $user_fields added. + * + * @param User_Fields $user_fields The user data API. + */ + public function __construct( User_Fields $user_fields ) { + $this->user_fields = $user_fields; + } + + /** + * Check if the WP_User_Manager integration should be activated. + * + * @return bool + */ + public function check() { + return \class_exists( \WP_User_Manager::class ) && \function_exists( 'wpum_get_option' ) && \wpum_get_option( 'custom_avatars' ); + } + + /** + * Activate the integration. + */ + public function run() { + // Disable profile image uploading. + \add_filter( 'avatar_privacy_profile_picture_upload_disabled', '__return_true' ); + + // Serve WP User Manager profile pictures via the filesystem cache. + \add_filter( 'avatar_privacy_pre_get_user_avatar', [ $this, 'enable_wpusermanager_user_avatars' ], 10, 2 ); + \add_filter( 'carbon_fields_should_save_field_value', [ $this, 'maybe_mark_user_avater_for_cache_flushing' ], 9999, 3 ); + \add_action( 'carbon_fields_user_meta_container_saved', [ $this, 'maybe_flush_cache_after_saving_user_avatar' ], 10, 1 ); + } + + /** + * Retrieves the user avatar from WP User Manager. + * + * @param array|null $avatar Optional. The user avatar information. Default null. + * @param int $user_id The user ID. + * + * @return array|null { + * Optional. The user avatar information. Default null. + * + * @type string $file The local filename. + * @type string $type The MIME type. + * } + * + * @phpstan-param AvatarDefinition|null $avatar + * @phpstan-return AvatarDefinition|null + */ + public function enable_wpusermanager_user_avatars( array $avatar = null, $user_id ) { + $file = \carbon_get_user_meta( $user_id, self::WP_USER_MANAGER_META_KEY ); + + if ( \is_string( $file ) && ! empty( $file ) ) { + $type = \wp_check_filetype( $file )['type']; + + if ( ! empty( $type ) ) { + return [ + 'file' => $file, + 'type' => $type, + ]; + } + } + + return null; + } + + /** + * Marks the user avatar cache for flushing if the corresponding WP_User_Manager + * field is about to been changed. + * + * @param bool $save Whether the field should be saved. Passed on as-is. + * @param mixed $value The field value. Ignored. + * @param Field $field A Carbon Fields object. + * + * @return bool + */ + public function maybe_mark_user_avater_for_cache_flushing( $save, $value, Field $field ) { + if ( $field->get_base_name() === self::WP_USER_MANAGER_META_KEY ) { + $this->flush_cache = true; + } + + return $save; + } + + /** + * Flushes the user avatar cache if necessary. + * + * @param int $user_id The user ID. + * + * @return void + */ + public function maybe_flush_cache_after_saving_user_avatar( $user_id ) { + if ( $this->flush_cache ) { + $this->user_fields->invalidate_local_avatar_cache( $user_id ); + } + } +} diff --git a/includes/avatar-privacy/integrations/class-wpdiscuz-integration.php b/includes/avatar-privacy/integrations/class-wpdiscuz-integration.php new file mode 100644 index 0000000..364cdf3 --- /dev/null +++ b/includes/avatar-privacy/integrations/class-wpdiscuz-integration.php @@ -0,0 +1,191 @@ + + */ +class WPDiscuz_Integration implements Plugin_Integration { + + /** + * The script & style registration helper. + * + * @since 2.4.0 + * + * @var Dependencies + */ + private Dependencies $dependencies; + + /** + * The comment handling component. + * + * @var Comments + */ + private Comments $comments; + + /** + * The field name for the cookies consent checkbox. + * + * @var string + */ + private string $cookie_consent_name; + + /** + * Creates a new instance. + * + * @param Dependencies $dependencies The script & style registration helper. + * @param Comments $comments The comment handler. + */ + public function __construct( Dependencies $dependencies, Comments $comments ) { + $this->dependencies = $dependencies; + $this->comments = $comments; + } + + /** + * Check if the bbPress integration should be activated. + * + * @return bool + */ + public function check() { + return \function_exists( 'wpDiscuz' ); + } + + /** + * Activate the integration. + * + * @return void + */ + public function run() { + \add_action( 'init', [ $this, 'init' ] ); + } + + /** + * Init action handler. + * + * @return void + */ + public function init() { + if ( ! \is_user_logged_in() ) { + if ( ! \is_admin() ) { + \add_action( 'wpdiscuz_submit_button_before', [ $this, 'print_gravatar_checkbox' ] ); + \add_action( 'wp_enqueue_scripts', [ $this, 'enqeue_styles_and_scripts' ] ); + } + + // Needed in AJAX calls. + \add_action( 'wpdiscuz_form_init', [ $this, 'store_cookie_consent_checkbox' ] ); + \add_action( 'wpdiscuz_before_save_commentmeta', [ $this, 'set_comment_cookies' ] ); + } + } + + /** + * Prints the wpDiscuz "Use Gravatar" checkbox. + * + * @return void + */ + public function print_gravatar_checkbox() { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- echoing a partial. + echo $this->comments->get_gravatar_checkbox_markup( 'public/partials/wpdiscuz/use-gravatar.php' ); + } + + /** + * Enqueue stylesheet comments form. + * + * @return void + */ + public function enqeue_styles_and_scripts() { + // Register script. + $this->dependencies->register_script( 'avatar-privacy-wpdiscuz-use-gravatar', 'public/js/wpdiscuz/use-gravatar.js', [ 'jquery' ], false, true ); + + // Set up the localized script data. + $data = [ + 'cookie' => Comments::COOKIE_PREFIX . \COOKIEHASH, + 'checkbox' => Comments::CHECKBOX_FIELD_NAME, + ]; + \wp_localize_script( 'avatar-privacy-wpdiscuz-use-gravatar', 'avatarPrivacy', $data ); + + // Enqueue script. + $this->dependencies->enqueue_script( 'avatar-privacy-wpdiscuz-use-gravatar' ); + } + + /** + * Sets the "Use Gravatar" cookie. + * + * @param \WP_Comment $comment Comment object. + * + * @return void + */ + public function set_comment_cookies( \WP_Comment $comment ) { + $user = \wp_get_current_user(); + $cookie_consent = $this->filter_bool( \INPUT_POST, $this->cookie_consent_name ); + + $this->comments->set_comment_cookies( $comment, $user, $cookie_consent ); + } + + /** + * Stores the wpDiscuz form fields for later use. + * + * @param Form $form The form object. + * + * @return void + */ + public function store_cookie_consent_checkbox( Form $form ) { + $form->initFormFields(); + + foreach ( $form->getFormCustomFields() as $field_name => $field ) { + if ( Field\CookiesConsent::class === $field['type'] ) { + $this->cookie_consent_name = $field_name; + } + } + } + + /** + * Filters boolean values in one of the input super globals to allow for + * unit testing. + * + * @since 2.6.0 Renamed to `filter_bool` and made more specific. + * + * @param int $type One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV. + * @param string $variable_name Name of a variable to get. + * + * @return bool + * + * @phpstan-param \INPUT_GET|\INPUT_POST|\INPUT_COOKIE|\INPUT_SERVER|\INPUT_ENV $type + */ + protected function filter_bool( $type, $variable_name ) { + return true === \filter_input( $type, $variable_name, \FILTER_VALIDATE_BOOLEAN ); // @codeCoverageIgnore + } +} diff --git a/includes/avatar-privacy/tools/class-hasher.php b/includes/avatar-privacy/tools/class-hasher.php new file mode 100644 index 0000000..cde734c --- /dev/null +++ b/includes/avatar-privacy/tools/class-hasher.php @@ -0,0 +1,133 @@ + + */ +class Hasher { + + /** + * The salt used for the get_hash() method. + * + * @var string + */ + private $salt; + + /** + * The network options handler. + * + * @var Network_Options + */ + private $network_options; + + /** + * Creates a new instance. + * + * @param Network_Options $network_options The network options handler. + */ + public function __construct( Network_Options $network_options ) { + $this->network_options = $network_options; + } + + /** + * Retrieves the salt for current the site/network. + * + * @return string + */ + public function get_salt() { + if ( empty( $this->salt ) ) { + /** + * Filters the salt used for generating this sites email hashes. + * + * If a non-empty string is returned, this value is used instead of + * the one stored in the network options. On first activation, a random + * value is generated and stored in the option. + * + * @param string $salt Default ''. + */ + $_salt = \apply_filters( 'avatar_privacy_salt', '' ); + + if ( empty( $_salt ) ) { + // Let's try the network option next. + $_salt = $this->network_options->get( Network_Options::SALT ); + + if ( ! \is_string( $_salt ) || empty( $_salt ) ) { + // Still nothing? Generate a random value. + $_salt = $this->generate_salt(); + + // Save the generated salt. + $this->network_options->set( Network_Options::SALT, $_salt ); + } + } + + $this->salt = $_salt; + } + + return $this->salt; + } + + /** + * Generates a new salt. + * + * @since 2.5.0 + * + * @return string + */ + protected function generate_salt() { + return \wp_generate_password( 64, true, true ); + } + + /** + * Generates a salted SHA-256 hash for the given identifier (an e-mail address + * in most cases). + * + * @param string $identifer The identifier. Whitespace on either side + * is ignored. + * @param bool $case_sensitive Optional. Whether the identifier is case-sensitive + * (e.g. an URL). Default false. + * + * @return string + */ + public function get_hash( $identifer, $case_sensitive = false ) { + $identifier = \trim( $identifer ); + + if ( ! $case_sensitive ) { + $identifer = \strtolower( $identifier ); + } + + return \hash( 'sha256', "{$this->get_salt()}{$identifer}" ); + } +} diff --git a/includes/avatar-privacy/tools/class-multisite.php b/includes/avatar-privacy/tools/class-multisite.php new file mode 100644 index 0000000..24213a2 --- /dev/null +++ b/includes/avatar-privacy/tools/class-multisite.php @@ -0,0 +1,80 @@ + + */ +class Multisite { + + /** + * Performs the given task on all sites in a network. + * + * Warning: This is potentially expensive. + * + * @param callable $task The task to execute. Should take the site ID as its parameter. + * @param int|null $network_id Optional. The network ID (`null` means the current netwrok). Default null. + * + * @return void + */ + public function do_for_all_sites_in_network( callable $task, $network_id = null ) { + foreach ( $this->get_site_ids( $network_id ) as $site_id ) { + \switch_to_blog( $site_id ); + + $task( $site_id ); + + \restore_current_blog(); + } + } + + /** + * Retrieves all site IDs for a network. + * + * @param int|null $network_id Optional. The network ID (`null` means the current network). Default null. + * + * @return int[] An array of site IDs. + */ + public function get_site_ids( $network_id = null ) { + $network_id = $network_id ?: \get_current_network_id(); + $query = [ + 'fields' => 'ids', + 'network_id' => $network_id, + 'number' => 0, + ]; + + $result = \get_sites( $query ); + if ( ! \is_array( $result ) ) { + $result = []; + } + + return $result; + } +} diff --git a/includes/avatar-privacy/tools/class-number-generator.php b/includes/avatar-privacy/tools/class-number-generator.php new file mode 100644 index 0000000..7c80625 --- /dev/null +++ b/includes/avatar-privacy/tools/class-number-generator.php @@ -0,0 +1,72 @@ + + */ +class Number_Generator { + + /** + * Initializes the pseudo-random number generator with the seed hash. + * + * @param string $hash A string of hexadecimal digits (i.e. the result of a + * hash function). + * + * @return void + */ + public function seed( $hash ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_seeding_mt_srand -- we need deterministic "randomness". + \mt_srand( (int) \hexdec( \substr( $hash, 0, 8 ) ) ); + } + + /** + * Resets the pseudo-random number generator to a less predictable value. + * + * @return void + */ + public function reset() { + \mt_srand(); // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_seeding_mt_srand + } + + /** + * Retrieves a pseudo-random number falling into the given interval. + * + * @param int $min The minimum. + * @param int $max The maximum. + * + * @return int + */ + public function get( $min, $max ) { + return \mt_rand( $min, $max ); // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand + } +} diff --git a/includes/avatar-privacy/tools/class-template.php b/includes/avatar-privacy/tools/class-template.php new file mode 100644 index 0000000..45bd6e2 --- /dev/null +++ b/includes/avatar-privacy/tools/class-template.php @@ -0,0 +1,256 @@ + + * + * @phpstan-type PartialArguments array + */ +class Template { + + /** + * The allowed HTML tags and attributes for checkbox labels. + * + * @var array> + */ + const ALLOWED_HTML_LABEL = [ + 'a' => [ + 'href' => true, + 'rel' => true, + 'target' => true, + ], + ]; + + /** + * Retrieves and filters the `rel` attribute for links to gravatar.com. + * + * @deprecated 2.4.0 + * + * @return string The result is safe for output. + */ + public static function get_gravatar_link_rel() { + \_deprecated_function( __METHOD__, '2.4.0', 'Please use ' . __CLASS__ . '::get_uploader_description() or ::get_use_gravatar_label() instead.' ); + + /** + * Filters the `rel` attribute for user-visible links to gravatar.com. + * + * @since 2.0.0 + * + * @param string $rel Default 'noopener nofollow'. + */ + return \esc_attr( \apply_filters( 'avatar_privacy_gravatar_link_rel', 'noopener nofollow' ) ); + } + + /** + * Retrieves and filters the `target` attribute for links to gravatar.com. + * + * @deprecated 2.4.0 + * + * @return string The result is safe for output. + */ + public static function get_gravatar_link_target() { + \_deprecated_function( __METHOD__, '2.4.0', 'Please use ' . __CLASS__ . '::get_uploader_description() or ::get_use_gravatar_label() instead.' ); + + /** + * Filters the `target` attribute for user-visible links to gravatar.com. + * + * @since 2.0.0 + * + * @param string $target Default '_self'. + */ + return \esc_attr( \apply_filters( 'avatar_privacy_gravatar_link_target', '_self' ) ); + } + + /** + * Retrieves the uploader description depending on whether the user is allowed + * upload images and whether a local avatar has already been set. + * + * @param bool $can_upload Optional. Whether the current user can upload images. Default false. + * @param bool $has_local_avatar Optional. Whether the user currently has a local avatar set. Default false. + * + * @return string + */ + public function get_uploader_description( $can_upload = false, $has_local_avatar = false ) { + $link_needed = false; + + if ( $can_upload ) { + if ( ! $has_local_avatar ) { + /* translators: 1: gravatar.com URL, 2: rel attribute, 3: target attribute */ + $description = \__( 'No local profile picture is set. Use the upload field to add a local profile picture or change your profile picture on Gravatar.', 'avatar-privacy' ); + $link_needed = true; + } else { + /* translators: 1: gravatar.com URL, 2: rel attribute, 3: target attribute */ + $description = \__( 'Replace the local profile picture by uploading a new avatar, or erase it (falling back on Gravatar) by checking the delete option.', 'avatar-privacy' ); + $link_needed = true; + } + } elseif ( ! $has_local_avatar ) { + /* translators: 1: gravatar.com URL, 2: rel attribute, 3: target attribute */ + $description = \__( 'No local profile picture is set. Change your profile picture on Gravatar.', 'avatar-privacy' ); + $link_needed = true; + } else { + $description = \__( 'You do not have media management permissions. To change your local profile picture, contact the site administrator.', 'avatar-privacy' ); + } + + if ( $link_needed ) { + $description = $this->fill_in_gravatar_url( $description ); + } + + return $description; + } + + /** + * Retrieves the label for the "Use gravatar" checkbox. + * + * @since 2.4.0 + * + * @param string $context Optional. The context for the label (valid: 'user', + * 'comment'). Default 'user'. + * + * @return string + */ + public function get_use_gravatar_label( $context = 'user' ) { + if ( 'user' === $context ) { + /* translators: 1: gravatar.com URL, 2: rel attribute, 3: target attribute */ + $label = \__( 'Display a Gravatar image for my e-mail address.', 'avatar-privacy' ); + } elseif ( 'comment' === $context ) { + /* translators: 1: gravatar.com URL, 2: rel attribute, 3: target attribute */ + $label = \__( 'Display a Gravatar image next to my comments.', 'avatar-privacy' ); + } else { + \_doing_it_wrong( __METHOD__, \esc_html( "Invalid context $context" ), '2.4.0' ); + return ''; + } + + return $this->fill_in_gravatar_url( $label ); + } + + /** + * Fills in the (translated) Gravatar.com URL and the link `rel` and `target` + * attributes. + * + * @since 2.4.0 + * + * @param string $message The message to fill in. It needs to contain placeholders + * in this order: + * 1. URL (default 'https://en.gravatar.com'), + * 2. `rel` attribute (default 'noopener nofollow'), and + * 3. `target` attribute (default '_self'). + * @return string + */ + protected function fill_in_gravatar_url( $message ) { + return \sprintf( + $message, + \__( 'https://en.gravatar.com/', 'avatar-privacy' ), + $this->get_gravatar_link_rel_attribute(), + $this->get_gravatar_link_target_attribute() + ); + } + /** + * Retrieves and filters the `rel` attribute for links to gravatar.com. + * + * @since 2.4.0 + * + * @return string The result is safe for output. + */ + protected function get_gravatar_link_rel_attribute() { + /** + * Filters the `rel` attribute for user-visible links to gravatar.com. + * + * @since 2.0.0 + * + * @param string $rel Default 'noopener nofollow'. + */ + return \esc_attr( \apply_filters( 'avatar_privacy_gravatar_link_rel', 'noopener nofollow' ) ); + } + + /** + * Retrieves and filters the `target` attribute for links to gravatar.com. + * + * @since 2.4.0 + * + * @return string The result is safe for output. + */ + protected function get_gravatar_link_target_attribute() { + /** + * Filters the `target` attribute for user-visible links to gravatar.com. + * + * @since 2.0.0 + * + * @param string $target Default '_self'. + */ + return \esc_attr( \apply_filters( 'avatar_privacy_gravatar_link_target', '_self' ) ); + } + + /** + * Parses and echoes a partial template. + * + * @since 2.4.0 + * + * @param string $partial The file path of the partial to include (relative + * to the plugin directory. + * @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_partial( $partial, array $args = [] ) { + if ( \extract( $args ) !== \count( $args ) ) { // phpcs:ignore WordPress.PHP.DontExtract.extract_extract -- needed for "natural" partials. + \_doing_it_wrong( __METHOD__, \esc_html( "Invalid arguments passed to partial {$partial}." ), 'Avatar Privacy 2.4.0' ); + } + + require \AVATAR_PRIVACY_PLUGIN_PATH . "/{$partial}"; + } + + /** + * Parses a partial template and returns the content as a string. + * + * @since 2.4.0 + * + * @param string $partial The file path of the partial to include (relative + * to the plugin directory. + * @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_partial( $partial, array $args = [] ) { + \ob_start(); + $this->print_partial( $partial, $args ); + return (string) \ob_get_clean(); + } +} diff --git a/includes/avatar-privacy/tools/functions.php b/includes/avatar-privacy/tools/functions.php new file mode 100644 index 0000000..cbecea6 --- /dev/null +++ b/includes/avatar-privacy/tools/functions.php @@ -0,0 +1,51 @@ + + */ +class Dependencies { + + /** + * The minification suffix. + * + * @var string + */ + private $suffix; + + /** + * The base URL for plugin scripts and styles. + * + * @var string + */ + private $url; + + /** + * The plugin base path for scripts and styles. + * + * @since 2.5.2 + * + * @var string + */ + private $path; + + /** + * Creates a new instance. + */ + public function __construct() { + $this->suffix = ( defined( 'SCRIPT_DEBUG' ) && \SCRIPT_DEBUG ) ? '' : '.min'; + $this->url = \plugins_url( '', \AVATAR_PRIVACY_PLUGIN_FILE ); + $this->path = \AVATAR_PRIVACY_PLUGIN_PATH; + } + + /** + * Registeres a script for the block editor (using the generated asset file). + * + * @param string $handle The name of the script. Should be unique. + * @param string $block The path of the script relative to the plugin's + * directory, but without the file extension. + * + * @return bool Whether the script has been registered. True on + * success, false on failure. + */ + public function register_block_script( $handle, $block ) { + // Load meta information. + $asset = include "{$this->path}/{$block}.asset.php"; + + // Register script. + return \wp_register_script( $handle, "{$this->url}/{$block}.js", $asset['dependencies'], $asset['version'], false ); + } + + /** + * Registers the script (does not overwrite). + * + * @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 + * script file's modification time is used + * automatically. If set to null, no version + * is added. + * @param bool $in_footer Optional. Whether to enqueue the script before + * 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\"> + '; + } + + 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 0000000..19501b6 Binary files /dev/null and b/public/images/birds/accessoire_1.png differ diff --git a/public/images/birds/accessoire_10.png b/public/images/birds/accessoire_10.png new file mode 100644 index 0000000..7f91c13 Binary files /dev/null and b/public/images/birds/accessoire_10.png differ diff --git a/public/images/birds/accessoire_11.png b/public/images/birds/accessoire_11.png new file mode 100644 index 0000000..11c8598 Binary files /dev/null and b/public/images/birds/accessoire_11.png differ diff --git a/public/images/birds/accessoire_12.png b/public/images/birds/accessoire_12.png new file mode 100644 index 0000000..afd9971 Binary files /dev/null and b/public/images/birds/accessoire_12.png differ diff --git a/public/images/birds/accessoire_13.png b/public/images/birds/accessoire_13.png new file mode 100644 index 0000000..526ac55 Binary files /dev/null and b/public/images/birds/accessoire_13.png differ diff --git a/public/images/birds/accessoire_14.png b/public/images/birds/accessoire_14.png new file mode 100644 index 0000000..364eaea Binary files /dev/null and b/public/images/birds/accessoire_14.png differ diff --git a/public/images/birds/accessoire_15.png b/public/images/birds/accessoire_15.png new file mode 100644 index 0000000..697ab77 Binary files /dev/null and b/public/images/birds/accessoire_15.png differ diff --git a/public/images/birds/accessoire_16.png b/public/images/birds/accessoire_16.png new file mode 100644 index 0000000..1523db5 Binary files /dev/null and b/public/images/birds/accessoire_16.png differ diff --git a/public/images/birds/accessoire_17.png b/public/images/birds/accessoire_17.png new file mode 100644 index 0000000..78457e8 Binary files /dev/null and b/public/images/birds/accessoire_17.png differ diff --git a/public/images/birds/accessoire_18.png b/public/images/birds/accessoire_18.png new file mode 100644 index 0000000..78457e8 Binary files /dev/null and b/public/images/birds/accessoire_18.png differ diff --git a/public/images/birds/accessoire_19.png b/public/images/birds/accessoire_19.png new file mode 100644 index 0000000..78457e8 Binary files /dev/null and b/public/images/birds/accessoire_19.png differ diff --git a/public/images/birds/accessoire_2.png b/public/images/birds/accessoire_2.png new file mode 100644 index 0000000..19501b6 Binary files /dev/null and b/public/images/birds/accessoire_2.png differ diff --git a/public/images/birds/accessoire_20.png b/public/images/birds/accessoire_20.png new file mode 100644 index 0000000..cfe353c Binary files /dev/null and b/public/images/birds/accessoire_20.png differ diff --git a/public/images/birds/accessoire_3.png b/public/images/birds/accessoire_3.png new file mode 100644 index 0000000..e4f4761 Binary files /dev/null and b/public/images/birds/accessoire_3.png differ diff --git a/public/images/birds/accessoire_4.png b/public/images/birds/accessoire_4.png new file mode 100644 index 0000000..19501b6 Binary files /dev/null and b/public/images/birds/accessoire_4.png differ diff --git a/public/images/birds/accessoire_5.png b/public/images/birds/accessoire_5.png new file mode 100644 index 0000000..be32283 Binary files /dev/null and b/public/images/birds/accessoire_5.png differ diff --git a/public/images/birds/accessoire_6.png b/public/images/birds/accessoire_6.png new file mode 100644 index 0000000..dff78a9 Binary files /dev/null and b/public/images/birds/accessoire_6.png differ diff --git a/public/images/birds/accessoire_7.png b/public/images/birds/accessoire_7.png new file mode 100644 index 0000000..bbe8551 Binary files /dev/null and b/public/images/birds/accessoire_7.png differ diff --git a/public/images/birds/accessoire_8.png b/public/images/birds/accessoire_8.png new file mode 100644 index 0000000..161d9a0 Binary files /dev/null and b/public/images/birds/accessoire_8.png differ diff --git a/public/images/birds/accessoire_9.png b/public/images/birds/accessoire_9.png new file mode 100644 index 0000000..10c7469 Binary files /dev/null and b/public/images/birds/accessoire_9.png differ diff --git a/public/images/birds/beak_1.png b/public/images/birds/beak_1.png new file mode 100644 index 0000000..0a96abe Binary files /dev/null and b/public/images/birds/beak_1.png differ diff --git a/public/images/birds/beak_2.png b/public/images/birds/beak_2.png new file mode 100644 index 0000000..bf9c0a3 Binary files /dev/null and b/public/images/birds/beak_2.png differ diff --git a/public/images/birds/beak_3.png b/public/images/birds/beak_3.png new file mode 100644 index 0000000..9a327f2 Binary files /dev/null and b/public/images/birds/beak_3.png differ diff --git a/public/images/birds/beak_4.png b/public/images/birds/beak_4.png new file mode 100644 index 0000000..fdfc40e Binary files /dev/null and b/public/images/birds/beak_4.png differ diff --git a/public/images/birds/beak_5.png b/public/images/birds/beak_5.png new file mode 100644 index 0000000..607c627 Binary files /dev/null and b/public/images/birds/beak_5.png differ diff --git a/public/images/birds/beak_6.png b/public/images/birds/beak_6.png new file mode 100644 index 0000000..2fb0a6d Binary files /dev/null and b/public/images/birds/beak_6.png differ diff --git a/public/images/birds/beak_7.png b/public/images/birds/beak_7.png new file mode 100644 index 0000000..8e147c8 Binary files /dev/null and b/public/images/birds/beak_7.png differ diff --git a/public/images/birds/beak_8.png b/public/images/birds/beak_8.png new file mode 100644 index 0000000..f594384 Binary files /dev/null and b/public/images/birds/beak_8.png differ diff --git a/public/images/birds/beak_9.png b/public/images/birds/beak_9.png new file mode 100644 index 0000000..5af8c90 Binary files /dev/null and b/public/images/birds/beak_9.png differ diff --git a/public/images/birds/body_1.png b/public/images/birds/body_1.png new file mode 100644 index 0000000..40511ab Binary files /dev/null and b/public/images/birds/body_1.png differ diff --git a/public/images/birds/body_2.png b/public/images/birds/body_2.png new file mode 100644 index 0000000..dda2363 Binary files /dev/null and b/public/images/birds/body_2.png differ diff --git a/public/images/birds/body_3.png b/public/images/birds/body_3.png new file mode 100644 index 0000000..dfd8f36 Binary files /dev/null and b/public/images/birds/body_3.png differ diff --git a/public/images/birds/body_4.png b/public/images/birds/body_4.png new file mode 100644 index 0000000..975d189 Binary files /dev/null and b/public/images/birds/body_4.png differ diff --git a/public/images/birds/body_5.png b/public/images/birds/body_5.png new file mode 100644 index 0000000..e866cbd Binary files /dev/null and b/public/images/birds/body_5.png differ diff --git a/public/images/birds/body_6.png b/public/images/birds/body_6.png new file mode 100644 index 0000000..869bf3a Binary files /dev/null and b/public/images/birds/body_6.png differ diff --git a/public/images/birds/body_7.png b/public/images/birds/body_7.png new file mode 100644 index 0000000..676c160 Binary files /dev/null and b/public/images/birds/body_7.png differ diff --git a/public/images/birds/body_8.png b/public/images/birds/body_8.png new file mode 100644 index 0000000..76f3077 Binary files /dev/null and b/public/images/birds/body_8.png differ diff --git a/public/images/birds/body_9.png b/public/images/birds/body_9.png new file mode 100644 index 0000000..62ace53 Binary files /dev/null and b/public/images/birds/body_9.png differ diff --git a/public/images/birds/eyes_1.png b/public/images/birds/eyes_1.png new file mode 100644 index 0000000..dcc39d5 Binary files /dev/null and b/public/images/birds/eyes_1.png differ diff --git a/public/images/birds/eyes_2.png b/public/images/birds/eyes_2.png new file mode 100644 index 0000000..0144760 Binary files /dev/null and b/public/images/birds/eyes_2.png differ diff --git a/public/images/birds/eyes_3.png b/public/images/birds/eyes_3.png new file mode 100644 index 0000000..8a3eb7d Binary files /dev/null and b/public/images/birds/eyes_3.png differ diff --git a/public/images/birds/eyes_4.png b/public/images/birds/eyes_4.png new file mode 100644 index 0000000..d1910d3 Binary files /dev/null and b/public/images/birds/eyes_4.png differ diff --git a/public/images/birds/eyes_5.png b/public/images/birds/eyes_5.png new file mode 100644 index 0000000..5e0de7e Binary files /dev/null and b/public/images/birds/eyes_5.png differ diff --git a/public/images/birds/eyes_6.png b/public/images/birds/eyes_6.png new file mode 100644 index 0000000..9ceea0f Binary files /dev/null and b/public/images/birds/eyes_6.png differ diff --git a/public/images/birds/eyes_7.png b/public/images/birds/eyes_7.png new file mode 100644 index 0000000..6a4a9d9 Binary files /dev/null and b/public/images/birds/eyes_7.png differ diff --git a/public/images/birds/eyes_8.png b/public/images/birds/eyes_8.png new file mode 100644 index 0000000..9411be6 Binary files /dev/null and b/public/images/birds/eyes_8.png differ diff --git a/public/images/birds/eyes_9.png b/public/images/birds/eyes_9.png new file mode 100644 index 0000000..81c5c95 Binary files /dev/null and b/public/images/birds/eyes_9.png differ diff --git a/public/images/birds/guide.png b/public/images/birds/guide.png new file mode 100644 index 0000000..9aecb1e Binary files /dev/null and b/public/images/birds/guide.png differ diff --git a/public/images/birds/hoop_1.png b/public/images/birds/hoop_1.png new file mode 100644 index 0000000..ab7aede Binary files /dev/null and b/public/images/birds/hoop_1.png differ diff --git a/public/images/birds/hoop_10.png b/public/images/birds/hoop_10.png new file mode 100644 index 0000000..337b5ae Binary files /dev/null and b/public/images/birds/hoop_10.png differ diff --git a/public/images/birds/hoop_2.png b/public/images/birds/hoop_2.png new file mode 100644 index 0000000..1c04aa4 Binary files /dev/null and b/public/images/birds/hoop_2.png differ diff --git a/public/images/birds/hoop_3.png b/public/images/birds/hoop_3.png new file mode 100644 index 0000000..3831ddc Binary files /dev/null and b/public/images/birds/hoop_3.png differ diff --git a/public/images/birds/hoop_4.png b/public/images/birds/hoop_4.png new file mode 100644 index 0000000..d1e4402 Binary files /dev/null and b/public/images/birds/hoop_4.png differ diff --git a/public/images/birds/hoop_5.png b/public/images/birds/hoop_5.png new file mode 100644 index 0000000..4153c53 Binary files /dev/null and b/public/images/birds/hoop_5.png differ diff --git a/public/images/birds/hoop_6.png b/public/images/birds/hoop_6.png new file mode 100644 index 0000000..81cedec Binary files /dev/null and b/public/images/birds/hoop_6.png differ diff --git a/public/images/birds/hoop_7.png b/public/images/birds/hoop_7.png new file mode 100644 index 0000000..335871f Binary files /dev/null and b/public/images/birds/hoop_7.png differ diff --git a/public/images/birds/hoop_8.png b/public/images/birds/hoop_8.png new file mode 100644 index 0000000..5574c95 Binary files /dev/null and b/public/images/birds/hoop_8.png differ diff --git a/public/images/birds/hoop_9.png b/public/images/birds/hoop_9.png new file mode 100644 index 0000000..e5464a4 Binary files /dev/null and b/public/images/birds/hoop_9.png differ diff --git a/public/images/birds/tail_1.png b/public/images/birds/tail_1.png new file mode 100644 index 0000000..674d77b Binary files /dev/null and b/public/images/birds/tail_1.png differ diff --git a/public/images/birds/tail_2.png b/public/images/birds/tail_2.png new file mode 100644 index 0000000..ca6cb62 Binary files /dev/null and b/public/images/birds/tail_2.png differ diff --git a/public/images/birds/tail_3.png b/public/images/birds/tail_3.png new file mode 100644 index 0000000..1aa86f5 Binary files /dev/null and b/public/images/birds/tail_3.png differ diff --git a/public/images/birds/tail_4.png b/public/images/birds/tail_4.png new file mode 100644 index 0000000..3ced171 Binary files /dev/null and b/public/images/birds/tail_4.png differ diff --git a/public/images/birds/tail_5.png b/public/images/birds/tail_5.png new file mode 100644 index 0000000..c653a7c Binary files /dev/null and b/public/images/birds/tail_5.png differ diff --git a/public/images/birds/tail_6.png b/public/images/birds/tail_6.png new file mode 100644 index 0000000..21cce9f Binary files /dev/null and b/public/images/birds/tail_6.png differ diff --git a/public/images/birds/tail_7.png b/public/images/birds/tail_7.png new file mode 100644 index 0000000..6426ea6 Binary files /dev/null and b/public/images/birds/tail_7.png differ diff --git a/public/images/birds/tail_8.png b/public/images/birds/tail_8.png new file mode 100644 index 0000000..b166504 Binary files /dev/null and b/public/images/birds/tail_8.png differ diff --git a/public/images/birds/tail_9.png b/public/images/birds/tail_9.png new file mode 100644 index 0000000..a087b2c Binary files /dev/null and b/public/images/birds/tail_9.png differ diff --git a/public/images/birds/wing_1.png b/public/images/birds/wing_1.png new file mode 100644 index 0000000..d2beb74 Binary files /dev/null and b/public/images/birds/wing_1.png differ diff --git a/public/images/birds/wing_2.png b/public/images/birds/wing_2.png new file mode 100644 index 0000000..ce1adcc Binary files /dev/null and b/public/images/birds/wing_2.png differ diff --git a/public/images/birds/wing_3.png b/public/images/birds/wing_3.png new file mode 100644 index 0000000..6e427d3 Binary files /dev/null and b/public/images/birds/wing_3.png differ diff --git a/public/images/birds/wing_4.png b/public/images/birds/wing_4.png new file mode 100644 index 0000000..88b80f8 Binary files /dev/null and b/public/images/birds/wing_4.png differ diff --git a/public/images/birds/wing_5.png b/public/images/birds/wing_5.png new file mode 100644 index 0000000..d9fa55c Binary files /dev/null and b/public/images/birds/wing_5.png differ diff --git a/public/images/birds/wing_6.png b/public/images/birds/wing_6.png new file mode 100644 index 0000000..bef614f Binary files /dev/null and b/public/images/birds/wing_6.png differ diff --git a/public/images/birds/wing_7.png b/public/images/birds/wing_7.png new file mode 100644 index 0000000..b6e47b9 Binary files /dev/null and b/public/images/birds/wing_7.png differ diff --git a/public/images/birds/wing_8.png b/public/images/birds/wing_8.png new file mode 100644 index 0000000..8dd2c80 Binary files /dev/null and b/public/images/birds/wing_8.png differ diff --git a/public/images/birds/wing_9.png b/public/images/birds/wing_9.png new file mode 100644 index 0000000..cf03b4e Binary files /dev/null and b/public/images/birds/wing_9.png differ diff --git a/public/images/cats/accessoire_1.png b/public/images/cats/accessoire_1.png new file mode 100644 index 0000000..ba81b2e Binary files /dev/null and b/public/images/cats/accessoire_1.png differ diff --git a/public/images/cats/accessoire_10.png b/public/images/cats/accessoire_10.png new file mode 100644 index 0000000..25670a0 Binary files /dev/null and b/public/images/cats/accessoire_10.png differ diff --git a/public/images/cats/accessoire_11.png b/public/images/cats/accessoire_11.png new file mode 100644 index 0000000..b5702b6 Binary files /dev/null and b/public/images/cats/accessoire_11.png differ diff --git a/public/images/cats/accessoire_12.png b/public/images/cats/accessoire_12.png new file mode 100644 index 0000000..fc5693e Binary files /dev/null and b/public/images/cats/accessoire_12.png differ diff --git a/public/images/cats/accessoire_13.png b/public/images/cats/accessoire_13.png new file mode 100644 index 0000000..88b2a92 Binary files /dev/null and b/public/images/cats/accessoire_13.png differ diff --git a/public/images/cats/accessoire_14.png b/public/images/cats/accessoire_14.png new file mode 100644 index 0000000..c3bb3bd Binary files /dev/null and b/public/images/cats/accessoire_14.png differ diff --git a/public/images/cats/accessoire_15.png b/public/images/cats/accessoire_15.png new file mode 100644 index 0000000..da05d71 Binary files /dev/null and b/public/images/cats/accessoire_15.png differ diff --git a/public/images/cats/accessoire_16.png b/public/images/cats/accessoire_16.png new file mode 100644 index 0000000..420e693 Binary files /dev/null and b/public/images/cats/accessoire_16.png differ diff --git a/public/images/cats/accessoire_17.png b/public/images/cats/accessoire_17.png new file mode 100644 index 0000000..f98efaa Binary files /dev/null and b/public/images/cats/accessoire_17.png differ diff --git a/public/images/cats/accessoire_18.png b/public/images/cats/accessoire_18.png new file mode 100644 index 0000000..f98efaa Binary files /dev/null and b/public/images/cats/accessoire_18.png differ diff --git a/public/images/cats/accessoire_19.png b/public/images/cats/accessoire_19.png new file mode 100644 index 0000000..f98efaa Binary files /dev/null and b/public/images/cats/accessoire_19.png differ diff --git a/public/images/cats/accessoire_2.png b/public/images/cats/accessoire_2.png new file mode 100644 index 0000000..97ff9e5 Binary files /dev/null and b/public/images/cats/accessoire_2.png differ diff --git a/public/images/cats/accessoire_20.png b/public/images/cats/accessoire_20.png new file mode 100644 index 0000000..3dfce53 Binary files /dev/null and b/public/images/cats/accessoire_20.png differ diff --git a/public/images/cats/accessoire_3.png b/public/images/cats/accessoire_3.png new file mode 100644 index 0000000..73af90f Binary files /dev/null and b/public/images/cats/accessoire_3.png differ diff --git a/public/images/cats/accessoire_4.png b/public/images/cats/accessoire_4.png new file mode 100644 index 0000000..c5bd25b Binary files /dev/null and b/public/images/cats/accessoire_4.png differ diff --git a/public/images/cats/accessoire_5.png b/public/images/cats/accessoire_5.png new file mode 100644 index 0000000..94e622b Binary files /dev/null and b/public/images/cats/accessoire_5.png differ diff --git a/public/images/cats/accessoire_6.png b/public/images/cats/accessoire_6.png new file mode 100644 index 0000000..f3a4691 Binary files /dev/null and b/public/images/cats/accessoire_6.png differ diff --git a/public/images/cats/accessoire_7.png b/public/images/cats/accessoire_7.png new file mode 100644 index 0000000..7f5b6a5 Binary files /dev/null and b/public/images/cats/accessoire_7.png differ diff --git a/public/images/cats/accessoire_8.png b/public/images/cats/accessoire_8.png new file mode 100644 index 0000000..5e27887 Binary files /dev/null and b/public/images/cats/accessoire_8.png differ diff --git a/public/images/cats/accessoire_9.png b/public/images/cats/accessoire_9.png new file mode 100644 index 0000000..49bb9c8 Binary files /dev/null and b/public/images/cats/accessoire_9.png differ diff --git a/public/images/cats/body_1.png b/public/images/cats/body_1.png new file mode 100644 index 0000000..aa245e6 Binary files /dev/null and b/public/images/cats/body_1.png differ diff --git a/public/images/cats/body_10.png b/public/images/cats/body_10.png new file mode 100644 index 0000000..a9d8cd7 Binary files /dev/null and b/public/images/cats/body_10.png differ diff --git a/public/images/cats/body_11.png b/public/images/cats/body_11.png new file mode 100644 index 0000000..adc1044 Binary files /dev/null and b/public/images/cats/body_11.png differ diff --git a/public/images/cats/body_12.png b/public/images/cats/body_12.png new file mode 100644 index 0000000..a2a6a57 Binary files /dev/null and b/public/images/cats/body_12.png differ diff --git a/public/images/cats/body_13.png b/public/images/cats/body_13.png new file mode 100644 index 0000000..f108a73 Binary files /dev/null and b/public/images/cats/body_13.png differ diff --git a/public/images/cats/body_14.png b/public/images/cats/body_14.png new file mode 100644 index 0000000..b5aa1a6 Binary files /dev/null and b/public/images/cats/body_14.png differ diff --git a/public/images/cats/body_15.png b/public/images/cats/body_15.png new file mode 100644 index 0000000..7a5c9c7 Binary files /dev/null and b/public/images/cats/body_15.png differ diff --git a/public/images/cats/body_2.png b/public/images/cats/body_2.png new file mode 100644 index 0000000..883df3d Binary files /dev/null and b/public/images/cats/body_2.png differ diff --git a/public/images/cats/body_3.png b/public/images/cats/body_3.png new file mode 100644 index 0000000..a0c451c Binary files /dev/null and b/public/images/cats/body_3.png differ diff --git a/public/images/cats/body_4.png b/public/images/cats/body_4.png new file mode 100644 index 0000000..57aace7 Binary files /dev/null and b/public/images/cats/body_4.png differ diff --git a/public/images/cats/body_5.png b/public/images/cats/body_5.png new file mode 100644 index 0000000..5bc3e18 Binary files /dev/null and b/public/images/cats/body_5.png differ diff --git a/public/images/cats/body_6.png b/public/images/cats/body_6.png new file mode 100644 index 0000000..78628b3 Binary files /dev/null and b/public/images/cats/body_6.png differ diff --git a/public/images/cats/body_7.png b/public/images/cats/body_7.png new file mode 100644 index 0000000..d50d1a8 Binary files /dev/null and b/public/images/cats/body_7.png differ diff --git a/public/images/cats/body_8.png b/public/images/cats/body_8.png new file mode 100644 index 0000000..41425e5 Binary files /dev/null and b/public/images/cats/body_8.png differ diff --git a/public/images/cats/body_9.png b/public/images/cats/body_9.png new file mode 100644 index 0000000..351e5a6 Binary files /dev/null and b/public/images/cats/body_9.png differ diff --git a/public/images/cats/eyes_1.png b/public/images/cats/eyes_1.png new file mode 100644 index 0000000..0ed3c3c Binary files /dev/null and b/public/images/cats/eyes_1.png differ diff --git a/public/images/cats/eyes_10.png b/public/images/cats/eyes_10.png new file mode 100644 index 0000000..f22e2ce Binary files /dev/null and b/public/images/cats/eyes_10.png differ diff --git a/public/images/cats/eyes_11.png b/public/images/cats/eyes_11.png new file mode 100644 index 0000000..fa5d57c Binary files /dev/null and b/public/images/cats/eyes_11.png differ diff --git a/public/images/cats/eyes_12.png b/public/images/cats/eyes_12.png new file mode 100644 index 0000000..6a17e5c Binary files /dev/null and b/public/images/cats/eyes_12.png differ diff --git a/public/images/cats/eyes_13.png b/public/images/cats/eyes_13.png new file mode 100644 index 0000000..e1702a4 Binary files /dev/null and b/public/images/cats/eyes_13.png differ diff --git a/public/images/cats/eyes_14.png b/public/images/cats/eyes_14.png new file mode 100644 index 0000000..c2092e0 Binary files /dev/null and b/public/images/cats/eyes_14.png differ diff --git a/public/images/cats/eyes_15.png b/public/images/cats/eyes_15.png new file mode 100644 index 0000000..6e2979f Binary files /dev/null and b/public/images/cats/eyes_15.png differ diff --git a/public/images/cats/eyes_2.png b/public/images/cats/eyes_2.png new file mode 100644 index 0000000..27951dd Binary files /dev/null and b/public/images/cats/eyes_2.png differ diff --git a/public/images/cats/eyes_3.png b/public/images/cats/eyes_3.png new file mode 100644 index 0000000..b366836 Binary files /dev/null and b/public/images/cats/eyes_3.png differ diff --git a/public/images/cats/eyes_4.png b/public/images/cats/eyes_4.png new file mode 100644 index 0000000..6e414b8 Binary files /dev/null and b/public/images/cats/eyes_4.png differ diff --git a/public/images/cats/eyes_5.png b/public/images/cats/eyes_5.png new file mode 100644 index 0000000..4585d55 Binary files /dev/null and b/public/images/cats/eyes_5.png differ diff --git a/public/images/cats/eyes_6.png b/public/images/cats/eyes_6.png new file mode 100644 index 0000000..26c4746 Binary files /dev/null and b/public/images/cats/eyes_6.png differ diff --git a/public/images/cats/eyes_7.png b/public/images/cats/eyes_7.png new file mode 100644 index 0000000..9cbc252 Binary files /dev/null and b/public/images/cats/eyes_7.png differ diff --git a/public/images/cats/eyes_8.png b/public/images/cats/eyes_8.png new file mode 100644 index 0000000..709ba77 Binary files /dev/null and b/public/images/cats/eyes_8.png differ diff --git a/public/images/cats/eyes_9.png b/public/images/cats/eyes_9.png new file mode 100644 index 0000000..b8c4a80 Binary files /dev/null and b/public/images/cats/eyes_9.png differ diff --git a/public/images/cats/fur_1.png b/public/images/cats/fur_1.png new file mode 100644 index 0000000..c1bff3e Binary files /dev/null and b/public/images/cats/fur_1.png differ diff --git a/public/images/cats/fur_10.png b/public/images/cats/fur_10.png new file mode 100644 index 0000000..791ede8 Binary files /dev/null and b/public/images/cats/fur_10.png differ diff --git a/public/images/cats/fur_2.png b/public/images/cats/fur_2.png new file mode 100644 index 0000000..596a909 Binary files /dev/null and b/public/images/cats/fur_2.png differ diff --git a/public/images/cats/fur_3.png b/public/images/cats/fur_3.png new file mode 100644 index 0000000..0d9e74a Binary files /dev/null and b/public/images/cats/fur_3.png differ diff --git a/public/images/cats/fur_4.png b/public/images/cats/fur_4.png new file mode 100644 index 0000000..cefd9e7 Binary files /dev/null and b/public/images/cats/fur_4.png differ diff --git a/public/images/cats/fur_5.png b/public/images/cats/fur_5.png new file mode 100644 index 0000000..4d183cd Binary files /dev/null and b/public/images/cats/fur_5.png differ diff --git a/public/images/cats/fur_6.png b/public/images/cats/fur_6.png new file mode 100644 index 0000000..89d726c Binary files /dev/null and b/public/images/cats/fur_6.png differ diff --git a/public/images/cats/fur_7.png b/public/images/cats/fur_7.png new file mode 100644 index 0000000..2874570 Binary files /dev/null and b/public/images/cats/fur_7.png differ diff --git a/public/images/cats/fur_8.png b/public/images/cats/fur_8.png new file mode 100644 index 0000000..1b45ccd Binary files /dev/null and b/public/images/cats/fur_8.png differ diff --git a/public/images/cats/fur_9.png b/public/images/cats/fur_9.png new file mode 100644 index 0000000..1dc4573 Binary files /dev/null and b/public/images/cats/fur_9.png differ diff --git a/public/images/cats/mouth_1.png b/public/images/cats/mouth_1.png new file mode 100644 index 0000000..d8fc9e4 Binary files /dev/null and b/public/images/cats/mouth_1.png differ diff --git a/public/images/cats/mouth_10.png b/public/images/cats/mouth_10.png new file mode 100644 index 0000000..0a6b5be Binary files /dev/null and b/public/images/cats/mouth_10.png differ diff --git a/public/images/cats/mouth_2.png b/public/images/cats/mouth_2.png new file mode 100644 index 0000000..54f2d81 Binary files /dev/null and b/public/images/cats/mouth_2.png differ diff --git a/public/images/cats/mouth_3.png b/public/images/cats/mouth_3.png new file mode 100644 index 0000000..9c633ae Binary files /dev/null and b/public/images/cats/mouth_3.png differ diff --git a/public/images/cats/mouth_4.png b/public/images/cats/mouth_4.png new file mode 100644 index 0000000..3b873e0 Binary files /dev/null and b/public/images/cats/mouth_4.png differ diff --git a/public/images/cats/mouth_5.png b/public/images/cats/mouth_5.png new file mode 100644 index 0000000..8a5a4bf Binary files /dev/null and b/public/images/cats/mouth_5.png differ diff --git a/public/images/cats/mouth_6.png b/public/images/cats/mouth_6.png new file mode 100644 index 0000000..519dae7 Binary files /dev/null and b/public/images/cats/mouth_6.png differ diff --git a/public/images/cats/mouth_7.png b/public/images/cats/mouth_7.png new file mode 100644 index 0000000..e402784 Binary files /dev/null and b/public/images/cats/mouth_7.png differ diff --git a/public/images/cats/mouth_8.png b/public/images/cats/mouth_8.png new file mode 100644 index 0000000..7dbcc1e Binary files /dev/null and b/public/images/cats/mouth_8.png differ diff --git a/public/images/cats/mouth_9.png b/public/images/cats/mouth_9.png new file mode 100644 index 0000000..56652ea Binary files /dev/null and b/public/images/cats/mouth_9.png differ diff --git a/public/images/cats/zz_1.png b/public/images/cats/zz_1.png new file mode 100644 index 0000000..b71b6df Binary files /dev/null and b/public/images/cats/zz_1.png differ diff --git a/public/images/cats/zz_2.png b/public/images/cats/zz_2.png new file mode 100644 index 0000000..fb819c9 Binary files /dev/null and b/public/images/cats/zz_2.png differ diff --git a/public/images/comment-bubble.svg b/public/images/comment-bubble.svg new file mode 100644 index 0000000..dd197ca --- /dev/null +++ b/public/images/comment-bubble.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/monster-id/arms_1.png b/public/images/monster-id/arms_1.png new file mode 100644 index 0000000..cca1950 Binary files /dev/null and b/public/images/monster-id/arms_1.png differ diff --git a/public/images/monster-id/arms_2.png b/public/images/monster-id/arms_2.png new file mode 100644 index 0000000..19f2d4b Binary files /dev/null and b/public/images/monster-id/arms_2.png differ diff --git a/public/images/monster-id/arms_3.png b/public/images/monster-id/arms_3.png new file mode 100644 index 0000000..49f9519 Binary files /dev/null and b/public/images/monster-id/arms_3.png differ diff --git a/public/images/monster-id/arms_4.png b/public/images/monster-id/arms_4.png new file mode 100644 index 0000000..1c51012 Binary files /dev/null and b/public/images/monster-id/arms_4.png differ diff --git a/public/images/monster-id/arms_5.png b/public/images/monster-id/arms_5.png new file mode 100644 index 0000000..83e8b04 Binary files /dev/null and b/public/images/monster-id/arms_5.png differ diff --git a/public/images/monster-id/arms_S1.png b/public/images/monster-id/arms_S1.png new file mode 100644 index 0000000..ed9f142 Binary files /dev/null and b/public/images/monster-id/arms_S1.png differ diff --git a/public/images/monster-id/arms_S2.png b/public/images/monster-id/arms_S2.png new file mode 100644 index 0000000..8b52237 Binary files /dev/null and b/public/images/monster-id/arms_S2.png differ diff --git a/public/images/monster-id/arms_S3.png b/public/images/monster-id/arms_S3.png new file mode 100644 index 0000000..154afa1 Binary files /dev/null and b/public/images/monster-id/arms_S3.png differ diff --git a/public/images/monster-id/arms_S4.png b/public/images/monster-id/arms_S4.png new file mode 100644 index 0000000..8cd9165 Binary files /dev/null and b/public/images/monster-id/arms_S4.png differ diff --git a/public/images/monster-id/arms_S5.png b/public/images/monster-id/arms_S5.png new file mode 100644 index 0000000..fd12e22 Binary files /dev/null and b/public/images/monster-id/arms_S5.png differ diff --git a/public/images/monster-id/arms_S6.png b/public/images/monster-id/arms_S6.png new file mode 100644 index 0000000..df65a3d Binary files /dev/null and b/public/images/monster-id/arms_S6.png differ diff --git a/public/images/monster-id/arms_S7.png b/public/images/monster-id/arms_S7.png new file mode 100644 index 0000000..c2e0d49 Binary files /dev/null and b/public/images/monster-id/arms_S7.png differ diff --git a/public/images/monster-id/arms_S8.png b/public/images/monster-id/arms_S8.png new file mode 100644 index 0000000..15d0fe2 Binary files /dev/null and b/public/images/monster-id/arms_S8.png differ diff --git a/public/images/monster-id/arms_S9.png b/public/images/monster-id/arms_S9.png new file mode 100644 index 0000000..f650fc2 Binary files /dev/null and b/public/images/monster-id/arms_S9.png differ diff --git a/public/images/monster-id/back.png b/public/images/monster-id/back.png new file mode 100644 index 0000000..a79840b Binary files /dev/null and b/public/images/monster-id/back.png differ diff --git a/public/images/monster-id/body_1.png b/public/images/monster-id/body_1.png new file mode 100644 index 0000000..eca2663 Binary files /dev/null and b/public/images/monster-id/body_1.png differ diff --git a/public/images/monster-id/body_10.png b/public/images/monster-id/body_10.png new file mode 100644 index 0000000..9f0e817 Binary files /dev/null and b/public/images/monster-id/body_10.png differ diff --git a/public/images/monster-id/body_11.png b/public/images/monster-id/body_11.png new file mode 100644 index 0000000..2452065 Binary files /dev/null and b/public/images/monster-id/body_11.png differ diff --git a/public/images/monster-id/body_12.png b/public/images/monster-id/body_12.png new file mode 100644 index 0000000..53e4d55 Binary files /dev/null and b/public/images/monster-id/body_12.png differ diff --git a/public/images/monster-id/body_13.png b/public/images/monster-id/body_13.png new file mode 100644 index 0000000..c8f8103 Binary files /dev/null and b/public/images/monster-id/body_13.png differ diff --git a/public/images/monster-id/body_14.png b/public/images/monster-id/body_14.png new file mode 100644 index 0000000..9edede1 Binary files /dev/null and b/public/images/monster-id/body_14.png differ diff --git a/public/images/monster-id/body_15.png b/public/images/monster-id/body_15.png new file mode 100644 index 0000000..fddad25 Binary files /dev/null and b/public/images/monster-id/body_15.png differ diff --git a/public/images/monster-id/body_2.png b/public/images/monster-id/body_2.png new file mode 100644 index 0000000..40f0809 Binary files /dev/null and b/public/images/monster-id/body_2.png differ diff --git a/public/images/monster-id/body_3.png b/public/images/monster-id/body_3.png new file mode 100644 index 0000000..afd8d07 Binary files /dev/null and b/public/images/monster-id/body_3.png differ diff --git a/public/images/monster-id/body_4.png b/public/images/monster-id/body_4.png new file mode 100644 index 0000000..30195a6 Binary files /dev/null and b/public/images/monster-id/body_4.png differ diff --git a/public/images/monster-id/body_5.png b/public/images/monster-id/body_5.png new file mode 100644 index 0000000..6b7cffb Binary files /dev/null and b/public/images/monster-id/body_5.png differ diff --git a/public/images/monster-id/body_6.png b/public/images/monster-id/body_6.png new file mode 100644 index 0000000..c035b5b Binary files /dev/null and b/public/images/monster-id/body_6.png differ diff --git a/public/images/monster-id/body_7.png b/public/images/monster-id/body_7.png new file mode 100644 index 0000000..294e545 Binary files /dev/null and b/public/images/monster-id/body_7.png differ diff --git a/public/images/monster-id/body_8.png b/public/images/monster-id/body_8.png new file mode 100644 index 0000000..b2efb69 Binary files /dev/null and b/public/images/monster-id/body_8.png differ diff --git a/public/images/monster-id/body_9.png b/public/images/monster-id/body_9.png new file mode 100644 index 0000000..becb63e Binary files /dev/null and b/public/images/monster-id/body_9.png differ diff --git a/public/images/monster-id/body_S1.png b/public/images/monster-id/body_S1.png new file mode 100644 index 0000000..ecd2006 Binary files /dev/null and b/public/images/monster-id/body_S1.png differ diff --git a/public/images/monster-id/body_S2.png b/public/images/monster-id/body_S2.png new file mode 100644 index 0000000..bf0ac3e Binary files /dev/null and b/public/images/monster-id/body_S2.png differ diff --git a/public/images/monster-id/body_S3.png b/public/images/monster-id/body_S3.png new file mode 100644 index 0000000..c61b3b7 Binary files /dev/null and b/public/images/monster-id/body_S3.png differ diff --git a/public/images/monster-id/body_S4.png b/public/images/monster-id/body_S4.png new file mode 100644 index 0000000..91df5d2 Binary files /dev/null and b/public/images/monster-id/body_S4.png differ diff --git a/public/images/monster-id/body_S5.png b/public/images/monster-id/body_S5.png new file mode 100644 index 0000000..22f2895 Binary files /dev/null and b/public/images/monster-id/body_S5.png differ diff --git a/public/images/monster-id/eyes_1.png b/public/images/monster-id/eyes_1.png new file mode 100644 index 0000000..116a5b6 Binary files /dev/null and b/public/images/monster-id/eyes_1.png differ diff --git a/public/images/monster-id/eyes_10.png b/public/images/monster-id/eyes_10.png new file mode 100644 index 0000000..2ff1305 Binary files /dev/null and b/public/images/monster-id/eyes_10.png differ diff --git a/public/images/monster-id/eyes_11.png b/public/images/monster-id/eyes_11.png new file mode 100644 index 0000000..5403e27 Binary files /dev/null and b/public/images/monster-id/eyes_11.png differ diff --git a/public/images/monster-id/eyes_12.png b/public/images/monster-id/eyes_12.png new file mode 100644 index 0000000..b74c0d6 Binary files /dev/null and b/public/images/monster-id/eyes_12.png differ diff --git a/public/images/monster-id/eyes_13.png b/public/images/monster-id/eyes_13.png new file mode 100644 index 0000000..d4c8074 Binary files /dev/null and b/public/images/monster-id/eyes_13.png differ diff --git a/public/images/monster-id/eyes_14.png b/public/images/monster-id/eyes_14.png new file mode 100644 index 0000000..676badd Binary files /dev/null and b/public/images/monster-id/eyes_14.png differ diff --git a/public/images/monster-id/eyes_15.png b/public/images/monster-id/eyes_15.png new file mode 100644 index 0000000..d531126 Binary files /dev/null and b/public/images/monster-id/eyes_15.png differ diff --git a/public/images/monster-id/eyes_2.png b/public/images/monster-id/eyes_2.png new file mode 100644 index 0000000..b8c5b38 Binary files /dev/null and b/public/images/monster-id/eyes_2.png differ diff --git a/public/images/monster-id/eyes_3.png b/public/images/monster-id/eyes_3.png new file mode 100644 index 0000000..27be6e0 Binary files /dev/null and b/public/images/monster-id/eyes_3.png differ diff --git a/public/images/monster-id/eyes_4.png b/public/images/monster-id/eyes_4.png new file mode 100644 index 0000000..a10d616 Binary files /dev/null and b/public/images/monster-id/eyes_4.png differ diff --git a/public/images/monster-id/eyes_5.png b/public/images/monster-id/eyes_5.png new file mode 100644 index 0000000..07edf0a Binary files /dev/null and b/public/images/monster-id/eyes_5.png differ diff --git a/public/images/monster-id/eyes_6.png b/public/images/monster-id/eyes_6.png new file mode 100644 index 0000000..ff90073 Binary files /dev/null and b/public/images/monster-id/eyes_6.png differ diff --git a/public/images/monster-id/eyes_7.png b/public/images/monster-id/eyes_7.png new file mode 100644 index 0000000..c35e398 Binary files /dev/null and b/public/images/monster-id/eyes_7.png differ diff --git a/public/images/monster-id/eyes_8.png b/public/images/monster-id/eyes_8.png new file mode 100644 index 0000000..e1f8cb5 Binary files /dev/null and b/public/images/monster-id/eyes_8.png differ diff --git a/public/images/monster-id/eyes_9.png b/public/images/monster-id/eyes_9.png new file mode 100644 index 0000000..a4cced6 Binary files /dev/null and b/public/images/monster-id/eyes_9.png differ diff --git a/public/images/monster-id/eyes_S1.png b/public/images/monster-id/eyes_S1.png new file mode 100644 index 0000000..26b4abe Binary files /dev/null and b/public/images/monster-id/eyes_S1.png differ diff --git a/public/images/monster-id/eyes_S2.png b/public/images/monster-id/eyes_S2.png new file mode 100644 index 0000000..32cd93b Binary files /dev/null and b/public/images/monster-id/eyes_S2.png differ diff --git a/public/images/monster-id/eyes_S3.png b/public/images/monster-id/eyes_S3.png new file mode 100644 index 0000000..cd4c15d Binary files /dev/null and b/public/images/monster-id/eyes_S3.png differ diff --git a/public/images/monster-id/eyes_S4.png b/public/images/monster-id/eyes_S4.png new file mode 100644 index 0000000..3871844 Binary files /dev/null and b/public/images/monster-id/eyes_S4.png differ diff --git a/public/images/monster-id/eyes_S5.png b/public/images/monster-id/eyes_S5.png new file mode 100644 index 0000000..493635c Binary files /dev/null and b/public/images/monster-id/eyes_S5.png differ diff --git a/public/images/monster-id/hair_1.png b/public/images/monster-id/hair_1.png new file mode 100644 index 0000000..2cefe0b Binary files /dev/null and b/public/images/monster-id/hair_1.png differ diff --git a/public/images/monster-id/hair_2.png b/public/images/monster-id/hair_2.png new file mode 100644 index 0000000..140954c Binary files /dev/null and b/public/images/monster-id/hair_2.png differ diff --git a/public/images/monster-id/hair_3.png b/public/images/monster-id/hair_3.png new file mode 100644 index 0000000..8971c78 Binary files /dev/null and b/public/images/monster-id/hair_3.png differ diff --git a/public/images/monster-id/hair_4.png b/public/images/monster-id/hair_4.png new file mode 100644 index 0000000..2175d93 Binary files /dev/null and b/public/images/monster-id/hair_4.png differ diff --git a/public/images/monster-id/hair_5.png b/public/images/monster-id/hair_5.png new file mode 100644 index 0000000..6abe23b Binary files /dev/null and b/public/images/monster-id/hair_5.png differ diff --git a/public/images/monster-id/hair_S1.png b/public/images/monster-id/hair_S1.png new file mode 100644 index 0000000..f956910 Binary files /dev/null and b/public/images/monster-id/hair_S1.png differ diff --git a/public/images/monster-id/hair_S2.png b/public/images/monster-id/hair_S2.png new file mode 100644 index 0000000..b2ed50c Binary files /dev/null and b/public/images/monster-id/hair_S2.png differ diff --git a/public/images/monster-id/hair_S3.png b/public/images/monster-id/hair_S3.png new file mode 100644 index 0000000..4801b3e Binary files /dev/null and b/public/images/monster-id/hair_S3.png differ diff --git a/public/images/monster-id/hair_S4.png b/public/images/monster-id/hair_S4.png new file mode 100644 index 0000000..c5cfe28 Binary files /dev/null and b/public/images/monster-id/hair_S4.png differ diff --git a/public/images/monster-id/hair_S5.png b/public/images/monster-id/hair_S5.png new file mode 100644 index 0000000..b0f61bf Binary files /dev/null and b/public/images/monster-id/hair_S5.png differ diff --git a/public/images/monster-id/hair_S6.png b/public/images/monster-id/hair_S6.png new file mode 100644 index 0000000..8ebbb68 Binary files /dev/null and b/public/images/monster-id/hair_S6.png differ diff --git a/public/images/monster-id/hair_S7.png b/public/images/monster-id/hair_S7.png new file mode 100644 index 0000000..ba71d08 Binary files /dev/null and b/public/images/monster-id/hair_S7.png differ diff --git a/public/images/monster-id/legs_1.png b/public/images/monster-id/legs_1.png new file mode 100644 index 0000000..677330a Binary files /dev/null and b/public/images/monster-id/legs_1.png differ diff --git a/public/images/monster-id/legs_2.png b/public/images/monster-id/legs_2.png new file mode 100644 index 0000000..b9da212 Binary files /dev/null and b/public/images/monster-id/legs_2.png differ diff --git a/public/images/monster-id/legs_3.png b/public/images/monster-id/legs_3.png new file mode 100644 index 0000000..2f4ac4d Binary files /dev/null and b/public/images/monster-id/legs_3.png differ diff --git a/public/images/monster-id/legs_4.png b/public/images/monster-id/legs_4.png new file mode 100644 index 0000000..78628ea Binary files /dev/null and b/public/images/monster-id/legs_4.png differ diff --git a/public/images/monster-id/legs_5.png b/public/images/monster-id/legs_5.png new file mode 100644 index 0000000..aee8f20 Binary files /dev/null and b/public/images/monster-id/legs_5.png differ diff --git a/public/images/monster-id/legs_S1.png b/public/images/monster-id/legs_S1.png new file mode 100644 index 0000000..770f40a Binary files /dev/null and b/public/images/monster-id/legs_S1.png differ diff --git a/public/images/monster-id/legs_S10.png b/public/images/monster-id/legs_S10.png new file mode 100644 index 0000000..047cb35 Binary files /dev/null and b/public/images/monster-id/legs_S10.png differ diff --git a/public/images/monster-id/legs_S11.png b/public/images/monster-id/legs_S11.png new file mode 100644 index 0000000..b1d794f Binary files /dev/null and b/public/images/monster-id/legs_S11.png differ diff --git a/public/images/monster-id/legs_S12.png b/public/images/monster-id/legs_S12.png new file mode 100644 index 0000000..1b1477e Binary files /dev/null and b/public/images/monster-id/legs_S12.png differ diff --git a/public/images/monster-id/legs_S13.png b/public/images/monster-id/legs_S13.png new file mode 100644 index 0000000..49a5c46 Binary files /dev/null and b/public/images/monster-id/legs_S13.png differ diff --git a/public/images/monster-id/legs_S2.png b/public/images/monster-id/legs_S2.png new file mode 100644 index 0000000..a0d2bd4 Binary files /dev/null and b/public/images/monster-id/legs_S2.png differ diff --git a/public/images/monster-id/legs_S3.png b/public/images/monster-id/legs_S3.png new file mode 100644 index 0000000..dafedc9 Binary files /dev/null and b/public/images/monster-id/legs_S3.png differ diff --git a/public/images/monster-id/legs_S4.png b/public/images/monster-id/legs_S4.png new file mode 100644 index 0000000..2a262de Binary files /dev/null and b/public/images/monster-id/legs_S4.png differ diff --git a/public/images/monster-id/legs_S5.png b/public/images/monster-id/legs_S5.png new file mode 100644 index 0000000..54410ad Binary files /dev/null and b/public/images/monster-id/legs_S5.png differ diff --git a/public/images/monster-id/legs_S6.png b/public/images/monster-id/legs_S6.png new file mode 100644 index 0000000..05dd00f Binary files /dev/null and b/public/images/monster-id/legs_S6.png differ diff --git a/public/images/monster-id/legs_S7.png b/public/images/monster-id/legs_S7.png new file mode 100644 index 0000000..292bf68 Binary files /dev/null and b/public/images/monster-id/legs_S7.png differ diff --git a/public/images/monster-id/legs_S8.png b/public/images/monster-id/legs_S8.png new file mode 100644 index 0000000..f5ab03f Binary files /dev/null and b/public/images/monster-id/legs_S8.png differ diff --git a/public/images/monster-id/legs_S9.png b/public/images/monster-id/legs_S9.png new file mode 100644 index 0000000..012f1a3 Binary files /dev/null and b/public/images/monster-id/legs_S9.png differ diff --git a/public/images/monster-id/mouth_1.png b/public/images/monster-id/mouth_1.png new file mode 100644 index 0000000..4300353 Binary files /dev/null and b/public/images/monster-id/mouth_1.png differ diff --git a/public/images/monster-id/mouth_10.png b/public/images/monster-id/mouth_10.png new file mode 100644 index 0000000..728333e Binary files /dev/null and b/public/images/monster-id/mouth_10.png differ diff --git a/public/images/monster-id/mouth_2.png b/public/images/monster-id/mouth_2.png new file mode 100644 index 0000000..03976dc Binary files /dev/null and b/public/images/monster-id/mouth_2.png differ diff --git a/public/images/monster-id/mouth_3.png b/public/images/monster-id/mouth_3.png new file mode 100644 index 0000000..9e5afc6 Binary files /dev/null and b/public/images/monster-id/mouth_3.png differ diff --git a/public/images/monster-id/mouth_4.png b/public/images/monster-id/mouth_4.png new file mode 100644 index 0000000..758d8a1 Binary files /dev/null and b/public/images/monster-id/mouth_4.png differ diff --git a/public/images/monster-id/mouth_5.png b/public/images/monster-id/mouth_5.png new file mode 100644 index 0000000..fe5d462 Binary files /dev/null and b/public/images/monster-id/mouth_5.png differ diff --git a/public/images/monster-id/mouth_6.png b/public/images/monster-id/mouth_6.png new file mode 100644 index 0000000..d619ef5 Binary files /dev/null and b/public/images/monster-id/mouth_6.png differ diff --git a/public/images/monster-id/mouth_7.png b/public/images/monster-id/mouth_7.png new file mode 100644 index 0000000..f8ae10c Binary files /dev/null and b/public/images/monster-id/mouth_7.png differ diff --git a/public/images/monster-id/mouth_8.png b/public/images/monster-id/mouth_8.png new file mode 100644 index 0000000..027a5dd Binary files /dev/null and b/public/images/monster-id/mouth_8.png differ diff --git a/public/images/monster-id/mouth_9.png b/public/images/monster-id/mouth_9.png new file mode 100644 index 0000000..95048d3 Binary files /dev/null and b/public/images/monster-id/mouth_9.png differ diff --git a/public/images/monster-id/mouth_S1.png b/public/images/monster-id/mouth_S1.png new file mode 100644 index 0000000..d44b965 Binary files /dev/null and b/public/images/monster-id/mouth_S1.png differ diff --git a/public/images/monster-id/mouth_S2.png b/public/images/monster-id/mouth_S2.png new file mode 100644 index 0000000..49ce6c5 Binary files /dev/null and b/public/images/monster-id/mouth_S2.png differ diff --git a/public/images/monster-id/mouth_S3.png b/public/images/monster-id/mouth_S3.png new file mode 100644 index 0000000..77a8afa Binary files /dev/null and b/public/images/monster-id/mouth_S3.png differ diff --git a/public/images/monster-id/mouth_S4.png b/public/images/monster-id/mouth_S4.png new file mode 100644 index 0000000..491f840 Binary files /dev/null and b/public/images/monster-id/mouth_S4.png differ diff --git a/public/images/monster-id/mouth_S5.png b/public/images/monster-id/mouth_S5.png new file mode 100644 index 0000000..d9cf9e9 Binary files /dev/null and b/public/images/monster-id/mouth_S5.png differ diff --git a/public/images/monster-id/mouth_S6.png b/public/images/monster-id/mouth_S6.png new file mode 100644 index 0000000..bba2a58 Binary files /dev/null and b/public/images/monster-id/mouth_S6.png differ diff --git a/public/images/monster-id/mouth_S7.png b/public/images/monster-id/mouth_S7.png new file mode 100644 index 0000000..60c5ec1 Binary files /dev/null and b/public/images/monster-id/mouth_S7.png differ diff --git a/public/images/mystery.svg b/public/images/mystery.svg new file mode 100644 index 0000000..06c024e --- /dev/null +++ b/public/images/mystery.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/accessory/accessory-01.svg b/public/images/robohash/accessory/accessory-01.svg new file mode 100644 index 0000000..cab4cbe --- /dev/null +++ b/public/images/robohash/accessory/accessory-01.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/accessory/accessory-02.svg b/public/images/robohash/accessory/accessory-02.svg new file mode 100644 index 0000000..6507391 --- /dev/null +++ b/public/images/robohash/accessory/accessory-02.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/accessory/accessory-03.svg b/public/images/robohash/accessory/accessory-03.svg new file mode 100644 index 0000000..89cdad0 --- /dev/null +++ b/public/images/robohash/accessory/accessory-03.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/accessory/accessory-04.svg b/public/images/robohash/accessory/accessory-04.svg new file mode 100644 index 0000000..f3571d5 --- /dev/null +++ b/public/images/robohash/accessory/accessory-04.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/accessory/accessory-05.svg b/public/images/robohash/accessory/accessory-05.svg new file mode 100644 index 0000000..31aa02d --- /dev/null +++ b/public/images/robohash/accessory/accessory-05.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/accessory/accessory-06.svg b/public/images/robohash/accessory/accessory-06.svg new file mode 100644 index 0000000..bcf280c --- /dev/null +++ b/public/images/robohash/accessory/accessory-06.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/accessory/accessory-07.svg b/public/images/robohash/accessory/accessory-07.svg new file mode 100644 index 0000000..5ff6a66 --- /dev/null +++ b/public/images/robohash/accessory/accessory-07.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/accessory/accessory-08.svg b/public/images/robohash/accessory/accessory-08.svg new file mode 100644 index 0000000..515003e --- /dev/null +++ b/public/images/robohash/accessory/accessory-08.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/accessory/accessory-09.svg b/public/images/robohash/accessory/accessory-09.svg new file mode 100644 index 0000000..84cb83c --- /dev/null +++ b/public/images/robohash/accessory/accessory-09.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/accessory/accessory-10.svg b/public/images/robohash/accessory/accessory-10.svg new file mode 100644 index 0000000..2e249dc --- /dev/null +++ b/public/images/robohash/accessory/accessory-10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/body/body-01.svg b/public/images/robohash/body/body-01.svg new file mode 100644 index 0000000..f1eb056 --- /dev/null +++ b/public/images/robohash/body/body-01.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/body/body-02.svg b/public/images/robohash/body/body-02.svg new file mode 100644 index 0000000..55a7bdc --- /dev/null +++ b/public/images/robohash/body/body-02.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/body/body-03.svg b/public/images/robohash/body/body-03.svg new file mode 100644 index 0000000..2b15b21 --- /dev/null +++ b/public/images/robohash/body/body-03.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/body/body-04.svg b/public/images/robohash/body/body-04.svg new file mode 100644 index 0000000..c0b2f1a --- /dev/null +++ b/public/images/robohash/body/body-04.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/body/body-05.svg b/public/images/robohash/body/body-05.svg new file mode 100644 index 0000000..a0c765f --- /dev/null +++ b/public/images/robohash/body/body-05.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/body/body-06.svg b/public/images/robohash/body/body-06.svg new file mode 100644 index 0000000..4c9f5ed --- /dev/null +++ b/public/images/robohash/body/body-06.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/body/body-07.svg b/public/images/robohash/body/body-07.svg new file mode 100644 index 0000000..630dd55 --- /dev/null +++ b/public/images/robohash/body/body-07.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/body/body-08.svg b/public/images/robohash/body/body-08.svg new file mode 100644 index 0000000..d05ebab --- /dev/null +++ b/public/images/robohash/body/body-08.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/body/body-09.svg b/public/images/robohash/body/body-09.svg new file mode 100644 index 0000000..540a1a1 --- /dev/null +++ b/public/images/robohash/body/body-09.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/body/body-10.svg b/public/images/robohash/body/body-10.svg new file mode 100644 index 0000000..f919302 --- /dev/null +++ b/public/images/robohash/body/body-10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/eyes/eyes-01.svg b/public/images/robohash/eyes/eyes-01.svg new file mode 100644 index 0000000..4b060ed --- /dev/null +++ b/public/images/robohash/eyes/eyes-01.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/eyes/eyes-02.svg b/public/images/robohash/eyes/eyes-02.svg new file mode 100644 index 0000000..0c5487f --- /dev/null +++ b/public/images/robohash/eyes/eyes-02.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/eyes/eyes-03.svg b/public/images/robohash/eyes/eyes-03.svg new file mode 100644 index 0000000..42669d7 --- /dev/null +++ b/public/images/robohash/eyes/eyes-03.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/eyes/eyes-04.svg b/public/images/robohash/eyes/eyes-04.svg new file mode 100644 index 0000000..3a3d0bd --- /dev/null +++ b/public/images/robohash/eyes/eyes-04.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/eyes/eyes-05.svg b/public/images/robohash/eyes/eyes-05.svg new file mode 100644 index 0000000..3cf8b24 --- /dev/null +++ b/public/images/robohash/eyes/eyes-05.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/eyes/eyes-06.svg b/public/images/robohash/eyes/eyes-06.svg new file mode 100644 index 0000000..668bbb3 --- /dev/null +++ b/public/images/robohash/eyes/eyes-06.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/eyes/eyes-07.svg b/public/images/robohash/eyes/eyes-07.svg new file mode 100644 index 0000000..84d14dc --- /dev/null +++ b/public/images/robohash/eyes/eyes-07.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/eyes/eyes-08.svg b/public/images/robohash/eyes/eyes-08.svg new file mode 100644 index 0000000..c5147de --- /dev/null +++ b/public/images/robohash/eyes/eyes-08.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/eyes/eyes-09.svg b/public/images/robohash/eyes/eyes-09.svg new file mode 100644 index 0000000..0316a9c --- /dev/null +++ b/public/images/robohash/eyes/eyes-09.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/eyes/eyes-10.svg b/public/images/robohash/eyes/eyes-10.svg new file mode 100644 index 0000000..5d5927a --- /dev/null +++ b/public/images/robohash/eyes/eyes-10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/face/face-01.svg b/public/images/robohash/face/face-01.svg new file mode 100644 index 0000000..c0e53eb --- /dev/null +++ b/public/images/robohash/face/face-01.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/face/face-02.svg b/public/images/robohash/face/face-02.svg new file mode 100644 index 0000000..e809167 --- /dev/null +++ b/public/images/robohash/face/face-02.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/face/face-03.svg b/public/images/robohash/face/face-03.svg new file mode 100644 index 0000000..dd9545c --- /dev/null +++ b/public/images/robohash/face/face-03.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/face/face-04.svg b/public/images/robohash/face/face-04.svg new file mode 100644 index 0000000..a39af6b --- /dev/null +++ b/public/images/robohash/face/face-04.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/face/face-05.svg b/public/images/robohash/face/face-05.svg new file mode 100644 index 0000000..9eec28e --- /dev/null +++ b/public/images/robohash/face/face-05.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/face/face-06.svg b/public/images/robohash/face/face-06.svg new file mode 100644 index 0000000..92cb543 --- /dev/null +++ b/public/images/robohash/face/face-06.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/face/face-07.svg b/public/images/robohash/face/face-07.svg new file mode 100644 index 0000000..6466f14 --- /dev/null +++ b/public/images/robohash/face/face-07.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/face/face-08.svg b/public/images/robohash/face/face-08.svg new file mode 100644 index 0000000..51743fd --- /dev/null +++ b/public/images/robohash/face/face-08.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/face/face-09.svg b/public/images/robohash/face/face-09.svg new file mode 100644 index 0000000..9719921 --- /dev/null +++ b/public/images/robohash/face/face-09.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/face/face-10.svg b/public/images/robohash/face/face-10.svg new file mode 100644 index 0000000..1942193 --- /dev/null +++ b/public/images/robohash/face/face-10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/mouth/mouth-01.svg b/public/images/robohash/mouth/mouth-01.svg new file mode 100644 index 0000000..a59a2af --- /dev/null +++ b/public/images/robohash/mouth/mouth-01.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/mouth/mouth-02.svg b/public/images/robohash/mouth/mouth-02.svg new file mode 100644 index 0000000..614fad2 --- /dev/null +++ b/public/images/robohash/mouth/mouth-02.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/mouth/mouth-03.svg b/public/images/robohash/mouth/mouth-03.svg new file mode 100644 index 0000000..b137de2 --- /dev/null +++ b/public/images/robohash/mouth/mouth-03.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/mouth/mouth-04.svg b/public/images/robohash/mouth/mouth-04.svg new file mode 100644 index 0000000..38d2e51 --- /dev/null +++ b/public/images/robohash/mouth/mouth-04.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/mouth/mouth-05.svg b/public/images/robohash/mouth/mouth-05.svg new file mode 100644 index 0000000..fd33d6e --- /dev/null +++ b/public/images/robohash/mouth/mouth-05.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/mouth/mouth-06.svg b/public/images/robohash/mouth/mouth-06.svg new file mode 100644 index 0000000..f9aed3f --- /dev/null +++ b/public/images/robohash/mouth/mouth-06.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/mouth/mouth-07.svg b/public/images/robohash/mouth/mouth-07.svg new file mode 100644 index 0000000..944d873 --- /dev/null +++ b/public/images/robohash/mouth/mouth-07.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/mouth/mouth-08.svg b/public/images/robohash/mouth/mouth-08.svg new file mode 100644 index 0000000..7a58ebe --- /dev/null +++ b/public/images/robohash/mouth/mouth-08.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/mouth/mouth-09.svg b/public/images/robohash/mouth/mouth-09.svg new file mode 100644 index 0000000..d2e5217 --- /dev/null +++ b/public/images/robohash/mouth/mouth-09.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/robohash/mouth/mouth-10.svg b/public/images/robohash/mouth/mouth-10.svg new file mode 100644 index 0000000..092d96b --- /dev/null +++ b/public/images/robohash/mouth/mouth-10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/shaded-cone.svg b/public/images/shaded-cone.svg new file mode 100644 index 0000000..fe59e63 --- /dev/null +++ b/public/images/shaded-cone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/silhouette.svg b/public/images/silhouette.svg new file mode 100644 index 0000000..ab2b1c3 --- /dev/null +++ b/public/images/silhouette.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/wavatars/brow_1.png b/public/images/wavatars/brow_1.png new file mode 100644 index 0000000..15695e9 Binary files /dev/null and b/public/images/wavatars/brow_1.png differ diff --git a/public/images/wavatars/brow_2.png b/public/images/wavatars/brow_2.png new file mode 100644 index 0000000..dfc32bd Binary files /dev/null and b/public/images/wavatars/brow_2.png differ diff --git a/public/images/wavatars/brow_3.png b/public/images/wavatars/brow_3.png new file mode 100644 index 0000000..7c9a3d7 Binary files /dev/null and b/public/images/wavatars/brow_3.png differ diff --git a/public/images/wavatars/brow_4.png b/public/images/wavatars/brow_4.png new file mode 100644 index 0000000..6d29c40 Binary files /dev/null and b/public/images/wavatars/brow_4.png differ diff --git a/public/images/wavatars/brow_5.png b/public/images/wavatars/brow_5.png new file mode 100644 index 0000000..4e4f12b Binary files /dev/null and b/public/images/wavatars/brow_5.png differ diff --git a/public/images/wavatars/brow_6.png b/public/images/wavatars/brow_6.png new file mode 100644 index 0000000..556c9ef Binary files /dev/null and b/public/images/wavatars/brow_6.png differ diff --git a/public/images/wavatars/brow_7.png b/public/images/wavatars/brow_7.png new file mode 100644 index 0000000..314c1a2 Binary files /dev/null and b/public/images/wavatars/brow_7.png differ diff --git a/public/images/wavatars/brow_8.png b/public/images/wavatars/brow_8.png new file mode 100644 index 0000000..2716f7e Binary files /dev/null and b/public/images/wavatars/brow_8.png differ diff --git a/public/images/wavatars/eyes_1.png b/public/images/wavatars/eyes_1.png new file mode 100644 index 0000000..831a572 Binary files /dev/null and b/public/images/wavatars/eyes_1.png differ diff --git a/public/images/wavatars/eyes_10.png b/public/images/wavatars/eyes_10.png new file mode 100644 index 0000000..82c4397 Binary files /dev/null and b/public/images/wavatars/eyes_10.png differ diff --git a/public/images/wavatars/eyes_11.png b/public/images/wavatars/eyes_11.png new file mode 100644 index 0000000..315337c Binary files /dev/null and b/public/images/wavatars/eyes_11.png differ diff --git a/public/images/wavatars/eyes_12.png b/public/images/wavatars/eyes_12.png new file mode 100644 index 0000000..520abe1 Binary files /dev/null and b/public/images/wavatars/eyes_12.png differ diff --git a/public/images/wavatars/eyes_13.png b/public/images/wavatars/eyes_13.png new file mode 100644 index 0000000..471026a Binary files /dev/null and b/public/images/wavatars/eyes_13.png differ diff --git a/public/images/wavatars/eyes_2.png b/public/images/wavatars/eyes_2.png new file mode 100644 index 0000000..f12b9ce Binary files /dev/null and b/public/images/wavatars/eyes_2.png differ diff --git a/public/images/wavatars/eyes_3.png b/public/images/wavatars/eyes_3.png new file mode 100644 index 0000000..e648a4b Binary files /dev/null and b/public/images/wavatars/eyes_3.png differ diff --git a/public/images/wavatars/eyes_4.png b/public/images/wavatars/eyes_4.png new file mode 100644 index 0000000..0091a6b Binary files /dev/null and b/public/images/wavatars/eyes_4.png differ diff --git a/public/images/wavatars/eyes_5.png b/public/images/wavatars/eyes_5.png new file mode 100644 index 0000000..8d3a4e5 Binary files /dev/null and b/public/images/wavatars/eyes_5.png differ diff --git a/public/images/wavatars/eyes_6.png b/public/images/wavatars/eyes_6.png new file mode 100644 index 0000000..a552665 Binary files /dev/null and b/public/images/wavatars/eyes_6.png differ diff --git a/public/images/wavatars/eyes_7.png b/public/images/wavatars/eyes_7.png new file mode 100644 index 0000000..7df55ae Binary files /dev/null and b/public/images/wavatars/eyes_7.png differ diff --git a/public/images/wavatars/eyes_8.png b/public/images/wavatars/eyes_8.png new file mode 100644 index 0000000..4f461cb Binary files /dev/null and b/public/images/wavatars/eyes_8.png differ diff --git a/public/images/wavatars/eyes_9.png b/public/images/wavatars/eyes_9.png new file mode 100644 index 0000000..70c8191 Binary files /dev/null and b/public/images/wavatars/eyes_9.png differ diff --git a/public/images/wavatars/fade_1.png b/public/images/wavatars/fade_1.png new file mode 100644 index 0000000..66eadd1 Binary files /dev/null and b/public/images/wavatars/fade_1.png differ diff --git a/public/images/wavatars/fade_2.png b/public/images/wavatars/fade_2.png new file mode 100644 index 0000000..9186b9a Binary files /dev/null and b/public/images/wavatars/fade_2.png differ diff --git a/public/images/wavatars/fade_3.png b/public/images/wavatars/fade_3.png new file mode 100644 index 0000000..98393c2 Binary files /dev/null and b/public/images/wavatars/fade_3.png differ diff --git a/public/images/wavatars/fade_4.png b/public/images/wavatars/fade_4.png new file mode 100644 index 0000000..0ea6a15 Binary files /dev/null and b/public/images/wavatars/fade_4.png differ diff --git a/public/images/wavatars/mask_1.png b/public/images/wavatars/mask_1.png new file mode 100644 index 0000000..cd47106 Binary files /dev/null and b/public/images/wavatars/mask_1.png differ diff --git a/public/images/wavatars/mask_10.png b/public/images/wavatars/mask_10.png new file mode 100644 index 0000000..458cf4c Binary files /dev/null and b/public/images/wavatars/mask_10.png differ diff --git a/public/images/wavatars/mask_11.png b/public/images/wavatars/mask_11.png new file mode 100644 index 0000000..a91ec4d Binary files /dev/null and b/public/images/wavatars/mask_11.png differ diff --git a/public/images/wavatars/mask_2.png b/public/images/wavatars/mask_2.png new file mode 100644 index 0000000..586c956 Binary files /dev/null and b/public/images/wavatars/mask_2.png differ diff --git a/public/images/wavatars/mask_3.png b/public/images/wavatars/mask_3.png new file mode 100644 index 0000000..68d0c0c Binary files /dev/null and b/public/images/wavatars/mask_3.png differ diff --git a/public/images/wavatars/mask_4.png b/public/images/wavatars/mask_4.png new file mode 100644 index 0000000..842a505 Binary files /dev/null and b/public/images/wavatars/mask_4.png differ diff --git a/public/images/wavatars/mask_5.png b/public/images/wavatars/mask_5.png new file mode 100644 index 0000000..2d01d4b Binary files /dev/null and b/public/images/wavatars/mask_5.png differ diff --git a/public/images/wavatars/mask_6.png b/public/images/wavatars/mask_6.png new file mode 100644 index 0000000..76c6080 Binary files /dev/null and b/public/images/wavatars/mask_6.png differ diff --git a/public/images/wavatars/mask_7.png b/public/images/wavatars/mask_7.png new file mode 100644 index 0000000..7853634 Binary files /dev/null and b/public/images/wavatars/mask_7.png differ diff --git a/public/images/wavatars/mask_8.png b/public/images/wavatars/mask_8.png new file mode 100644 index 0000000..f44437e Binary files /dev/null and b/public/images/wavatars/mask_8.png differ diff --git a/public/images/wavatars/mask_9.png b/public/images/wavatars/mask_9.png new file mode 100644 index 0000000..a0900f3 Binary files /dev/null and b/public/images/wavatars/mask_9.png differ diff --git a/public/images/wavatars/mouth_1.png b/public/images/wavatars/mouth_1.png new file mode 100644 index 0000000..1e50574 Binary files /dev/null and b/public/images/wavatars/mouth_1.png differ diff --git a/public/images/wavatars/mouth_10.png b/public/images/wavatars/mouth_10.png new file mode 100644 index 0000000..fc30bdb Binary files /dev/null and b/public/images/wavatars/mouth_10.png differ diff --git a/public/images/wavatars/mouth_11.png b/public/images/wavatars/mouth_11.png new file mode 100644 index 0000000..4edb738 Binary files /dev/null and b/public/images/wavatars/mouth_11.png differ diff --git a/public/images/wavatars/mouth_12.png b/public/images/wavatars/mouth_12.png new file mode 100644 index 0000000..8a866a1 Binary files /dev/null and b/public/images/wavatars/mouth_12.png differ diff --git a/public/images/wavatars/mouth_13.png b/public/images/wavatars/mouth_13.png new file mode 100644 index 0000000..2fe3782 Binary files /dev/null and b/public/images/wavatars/mouth_13.png differ diff --git a/public/images/wavatars/mouth_14.png b/public/images/wavatars/mouth_14.png new file mode 100644 index 0000000..76319e2 Binary files /dev/null and b/public/images/wavatars/mouth_14.png differ diff --git a/public/images/wavatars/mouth_15.png b/public/images/wavatars/mouth_15.png new file mode 100644 index 0000000..c0de713 Binary files /dev/null and b/public/images/wavatars/mouth_15.png differ diff --git a/public/images/wavatars/mouth_16.png b/public/images/wavatars/mouth_16.png new file mode 100644 index 0000000..0b584d9 Binary files /dev/null and b/public/images/wavatars/mouth_16.png differ diff --git a/public/images/wavatars/mouth_17.png b/public/images/wavatars/mouth_17.png new file mode 100644 index 0000000..7d8ac6d Binary files /dev/null and b/public/images/wavatars/mouth_17.png differ diff --git a/public/images/wavatars/mouth_18.png b/public/images/wavatars/mouth_18.png new file mode 100644 index 0000000..af8ad85 Binary files /dev/null and b/public/images/wavatars/mouth_18.png differ diff --git a/public/images/wavatars/mouth_19.png b/public/images/wavatars/mouth_19.png new file mode 100644 index 0000000..5526f1a Binary files /dev/null and b/public/images/wavatars/mouth_19.png differ diff --git a/public/images/wavatars/mouth_2.png b/public/images/wavatars/mouth_2.png new file mode 100644 index 0000000..d4f374e Binary files /dev/null and b/public/images/wavatars/mouth_2.png differ diff --git a/public/images/wavatars/mouth_3.png b/public/images/wavatars/mouth_3.png new file mode 100644 index 0000000..7f1fec6 Binary files /dev/null and b/public/images/wavatars/mouth_3.png differ diff --git a/public/images/wavatars/mouth_4.png b/public/images/wavatars/mouth_4.png new file mode 100644 index 0000000..ffd293d Binary files /dev/null and b/public/images/wavatars/mouth_4.png differ diff --git a/public/images/wavatars/mouth_5.png b/public/images/wavatars/mouth_5.png new file mode 100644 index 0000000..adeac82 Binary files /dev/null and b/public/images/wavatars/mouth_5.png differ diff --git a/public/images/wavatars/mouth_6.png b/public/images/wavatars/mouth_6.png new file mode 100644 index 0000000..5efe620 Binary files /dev/null and b/public/images/wavatars/mouth_6.png differ diff --git a/public/images/wavatars/mouth_7.png b/public/images/wavatars/mouth_7.png new file mode 100644 index 0000000..5874c66 Binary files /dev/null and b/public/images/wavatars/mouth_7.png differ diff --git a/public/images/wavatars/mouth_8.png b/public/images/wavatars/mouth_8.png new file mode 100644 index 0000000..7d701a7 Binary files /dev/null and b/public/images/wavatars/mouth_8.png differ diff --git a/public/images/wavatars/mouth_9.png b/public/images/wavatars/mouth_9.png new file mode 100644 index 0000000..91da0eb Binary files /dev/null and b/public/images/wavatars/mouth_9.png differ diff --git a/public/images/wavatars/pupils_1.png b/public/images/wavatars/pupils_1.png new file mode 100644 index 0000000..5d8c488 Binary files /dev/null and b/public/images/wavatars/pupils_1.png differ diff --git a/public/images/wavatars/pupils_10.png b/public/images/wavatars/pupils_10.png new file mode 100644 index 0000000..f0c113d Binary files /dev/null and b/public/images/wavatars/pupils_10.png differ diff --git a/public/images/wavatars/pupils_11.png b/public/images/wavatars/pupils_11.png new file mode 100644 index 0000000..c8e62c7 Binary files /dev/null and b/public/images/wavatars/pupils_11.png differ diff --git a/public/images/wavatars/pupils_2.png b/public/images/wavatars/pupils_2.png new file mode 100644 index 0000000..b3e4ea4 Binary files /dev/null and b/public/images/wavatars/pupils_2.png differ diff --git a/public/images/wavatars/pupils_3.png b/public/images/wavatars/pupils_3.png new file mode 100644 index 0000000..12ef6b7 Binary files /dev/null and b/public/images/wavatars/pupils_3.png differ diff --git a/public/images/wavatars/pupils_4.png b/public/images/wavatars/pupils_4.png new file mode 100644 index 0000000..bf0be41 Binary files /dev/null and b/public/images/wavatars/pupils_4.png differ diff --git a/public/images/wavatars/pupils_5.png b/public/images/wavatars/pupils_5.png new file mode 100644 index 0000000..7c5474d Binary files /dev/null and b/public/images/wavatars/pupils_5.png differ diff --git a/public/images/wavatars/pupils_6.png b/public/images/wavatars/pupils_6.png new file mode 100644 index 0000000..a281624 Binary files /dev/null and b/public/images/wavatars/pupils_6.png differ diff --git a/public/images/wavatars/pupils_7.png b/public/images/wavatars/pupils_7.png new file mode 100644 index 0000000..08d3e80 Binary files /dev/null and b/public/images/wavatars/pupils_7.png differ diff --git a/public/images/wavatars/pupils_8.png b/public/images/wavatars/pupils_8.png new file mode 100644 index 0000000..1508414 Binary files /dev/null and b/public/images/wavatars/pupils_8.png differ diff --git a/public/images/wavatars/pupils_9.png b/public/images/wavatars/pupils_9.png new file mode 100644 index 0000000..752da68 Binary files /dev/null and b/public/images/wavatars/pupils_9.png differ diff --git a/public/images/wavatars/shine_1.png b/public/images/wavatars/shine_1.png new file mode 100644 index 0000000..8294995 Binary files /dev/null and b/public/images/wavatars/shine_1.png differ diff --git a/public/images/wavatars/shine_10.png b/public/images/wavatars/shine_10.png new file mode 100644 index 0000000..1bdef49 Binary files /dev/null and b/public/images/wavatars/shine_10.png differ diff --git a/public/images/wavatars/shine_11.png b/public/images/wavatars/shine_11.png new file mode 100644 index 0000000..86286e0 Binary files /dev/null and b/public/images/wavatars/shine_11.png differ diff --git a/public/images/wavatars/shine_2.png b/public/images/wavatars/shine_2.png new file mode 100644 index 0000000..34ab631 Binary files /dev/null and b/public/images/wavatars/shine_2.png differ diff --git a/public/images/wavatars/shine_3.png b/public/images/wavatars/shine_3.png new file mode 100644 index 0000000..082c756 Binary files /dev/null and b/public/images/wavatars/shine_3.png differ diff --git a/public/images/wavatars/shine_4.png b/public/images/wavatars/shine_4.png new file mode 100644 index 0000000..84cf9e7 Binary files /dev/null and b/public/images/wavatars/shine_4.png differ diff --git a/public/images/wavatars/shine_5.png b/public/images/wavatars/shine_5.png new file mode 100644 index 0000000..2ffe334 Binary files /dev/null and b/public/images/wavatars/shine_5.png differ diff --git a/public/images/wavatars/shine_6.png b/public/images/wavatars/shine_6.png new file mode 100644 index 0000000..07d3f09 Binary files /dev/null and b/public/images/wavatars/shine_6.png differ diff --git a/public/images/wavatars/shine_7.png b/public/images/wavatars/shine_7.png new file mode 100644 index 0000000..96dbc7b Binary files /dev/null and b/public/images/wavatars/shine_7.png differ diff --git a/public/images/wavatars/shine_8.png b/public/images/wavatars/shine_8.png new file mode 100644 index 0000000..df834cc Binary files /dev/null and b/public/images/wavatars/shine_8.png differ diff --git a/public/images/wavatars/shine_9.png b/public/images/wavatars/shine_9.png new file mode 100644 index 0000000..363d112 Binary files /dev/null and b/public/images/wavatars/shine_9.png differ diff --git a/public/js/wpdiscuz/src/use-gravatar.js b/public/js/wpdiscuz/src/use-gravatar.js new file mode 100644 index 0000000..29667bd --- /dev/null +++ b/public/js/wpdiscuz/src/use-gravatar.js @@ -0,0 +1,31 @@ +/** + * Compatibility functions for the wpDiscuz plugin. + * + * This file is part of Avatar Privacy. + * + * @file This file handles the use_gravatar checkbox for wpDiscuz. + * @author Peter Putzer + * @license GPL-2.0-or-later + * @since 2.2.0 + */ + +/** + * Resets the use_gravatar checkbox after posting a new comment in wpDiscuz. + */ +jQuery( function ( $ ) { + 'use strict'; + + const $useGravatarCheckbox = $( '#' + avatarPrivacy.checkbox ); + + function resetUseGravatar() { + const useGravatar = + Cookies.get( avatarPrivacy.cookie ) !== undefined && + '' !== Cookies.get( avatarPrivacy.cookie ) + ? 'checked' + : ''; + + $useGravatarCheckbox.prop( 'checked', useGravatar ); + } + + $( document ).on( 'ajaxComplete', resetUseGravatar ); +} ); diff --git a/public/js/wpdiscuz/use-gravatar.js b/public/js/wpdiscuz/use-gravatar.js new file mode 100644 index 0000000..116a3a1 --- /dev/null +++ b/public/js/wpdiscuz/use-gravatar.js @@ -0,0 +1,31 @@ +/** + * Compatibility functions for the wpDiscuz plugin. + * + * This file is part of Avatar Privacy. + * + * @file This file handles the use_gravatar checkbox for wpDiscuz. + * @author Peter Putzer + * @license GPL-2.0-or-later + * @since 2.2.0 + */ + +/** + * Resets the use_gravatar checkbox after posting a new comment in wpDiscuz. + */ +jQuery(function ($) { + 'use strict'; + + var $useGravatarCheckbox = $('#' + avatarPrivacy.checkbox); + + function resetUseGravatar() { + var useGravatar = + Cookies.get(avatarPrivacy.cookie) !== undefined && + '' !== Cookies.get(avatarPrivacy.cookie) ? + 'checked' : + ''; + + $useGravatarCheckbox.prop('checked', useGravatar); + } + + $(document).on('ajaxComplete', resetUseGravatar); +}); diff --git a/public/js/wpdiscuz/use-gravatar.min.js b/public/js/wpdiscuz/use-gravatar.min.js new file mode 100644 index 0000000..79faade --- /dev/null +++ b/public/js/wpdiscuz/use-gravatar.min.js @@ -0,0 +1,2 @@ +/*! avatar-privacy use-gravatar.js 2023-05-01 10:26:31 PM */ +jQuery(function(a){"use strict";var e=a("#"+avatarPrivacy.checkbox);a(document).on("ajaxComplete",function(){var a=void 0!==Cookies.get(avatarPrivacy.cookie)&&""!==Cookies.get(avatarPrivacy.cookie)?"checked":"";e.prop("checked",a)})}); \ No newline at end of file diff --git a/public/partials/bbpress/profile/allow-anonymous.php b/public/partials/bbpress/profile/allow-anonymous.php new file mode 100644 index 0000000..4853a9e --- /dev/null +++ b/public/partials/bbpress/profile/allow-anonymous.php @@ -0,0 +1,55 @@ +` element. + * @var string $value The checkbox value. + */ +?> +
+ + + + + +
+` element. + * @var string $value The checkbox value. + */ +?> +
+ + + + + +
+` 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. + */ + +?> +
+ + + + + + + + + + + + get_uploader_description( $can_upload, $has_local_avatar ), T::ALLOWED_HTML_LABEL ); ?> + + +
+ +

+
+ + avatar_uploader( $user_id ); ?> + use_gravatar_checkbox( $user_id ); ?> + allow_anonymous_checkbox( $user_id ); ?> +
+display_name +); +?> +
+ +
+ +" method="post" enctype="multipart/form-data"> + +
+ + + avatar_uploader( $user_id, $attributes ); ?> + use_gravatar_checkbox( $user_id, $attributes ); ?> + allow_anonymous_checkbox( $user_id, $attributes ); ?> + + +
+ + +` element. + * @var string $value The checkbox value. + * @var string $show_description True if the long description should be displayed. + */ +?> +
+ + + /> +
+ +

+ +

+ +
+` element. + * @var string $value The checkbox value. + * @var string $show_description True if the long description should be displayed. +*/ +?> +
+ + + /> +
+ +

+ + +

+ +
+` 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. + */ + +?> +
+ + + +

+ + + + +
+ +

+ + +

+ get_uploader_description( $can_upload, $has_local_avatar ), T::ALLOWED_HTML_LABEL ); ?> +

+ +
+ + * 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 + */ + +/** + * Retro SVG image. + * + * Required template variables: + * + * @var int $rows The number of rows. + * @var int $columns The number of columns. + * @var string $path The path to draw. + * @var string $color The robot body color as CSS color string (e.g. `#ff9800`). + * @var string $bg_color The background color as a CSS color string (e.g. `#80d8ff`). + */ +?> +" width="320" height="320" xmlns="http://www.w3.org/2000/svg"> + " height="" fill="" stroke-width="0"/> + + + + + + + + + + +
+ avatar_uploader( $user_id, $atts ); ?> + use_gravatar_checkbox( $user_id, $atts ); ?> + allow_anonymous_checkbox( $user_id, $atts ); ?> + +
+` element. + * @var string $value The checkbox value. + * @var string $show_description True if the long description should be displayed. + */ +?> + + +/> +
+ +

+ +

+ +` element. + * @var string $value The checkbox value. + * @var string $show_description True if the long description should be displayed. +*/ +?> + + +/> +
+ +

+ + +

+ +` 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. + */ + +?> + + + +

+ + + + +
+ +

+ + +

+ get_uploader_description( $can_upload, $has_local_avatar ), T::ALLOWED_HTML_LABEL ); ?> +

+ + +
+
+
+ /> + +
+
+
+ +
+
+` + += What happens if I disable the plugin? Are any of the data altered? = + +The plugin saves additional data about whether commenters and users want to display a gravatar or not (if you select that mode in the settings). These data are deleted when you properly uninstall the plugin. + +Apart from that, the plugin only filters data, but does not permanently change them. Especially, if you deactivate the plugin und have gravatars turned on, they will again show up for everybody, even those commenters and users who opted out of displaying gravatars. You do have to change the default gravatar back manually, though. + += Can this plugin be used together with cache plugins? = + +Yes, it certainly can. You have to be careful though which plugin options you select and how you cache your content. The first plugin option checks if a gravatar exists for a given e-mail address and, if not, displays the default image directly. If you cache the output of this check, the gravatar will not be displayed if the user later decides to sign up with Gravatar.com. If you're using this option, you should invalidate cached pages with gravatars on them (mostly the single view of entries) regularly. + += Can this plugin be used on a multisite installation? = + +Yes, the plugin can be used on a multisite installation. You can either activate it on individual blogs or do a network activation. As users are global to a multisite installation, their choice regarding Gravatar.com use will affect all sites in the network. So if a user comments on blog A and chooses to display gravatars, this decision will be followed on blog B and C too. On new installations, comment author (i.e. non-user) opt-in is recorded per site, not per network. If you first installed Avatar Privacy 0.4 or earlier, the global table `wp_avatar_privacy` continues to be used for all sites in the multisite network. This behavior can be overriden by the network admin via the filter hook `avatar_privacy_enable_global_table`. + += Won't spam comments flood the database table with useless entries for the checkbox in the comment form? = + +The plugin doesn't save the value of the "use gravatar" checkbox for comments by registered users (see below), trackbacks/pingbacks (there is no e-mail address) and comments that are marked as spam when they are saved. If you mark a comment as spam later, the table entry is not removed, since the same e-mail address might also be used by non-spam comments. If a comment is marked as spam by Akismet or similar plugins and you later manually mark it as non-spam, what the user selected when submitting the comment will already be lost. This only happens with spam comments, not comments who just need to be moderated, e.g. because of the 'needs at least one published comment' rule. + += Will the avatar caching make my disk space run out? = + +While storing the cached avatar images on your own server will take some extra disk space, the plugin makes sure that it does not grow out of bounds by deleting cached gravatars every other day and all other images once a week. When the cached file is accessed again, it is automatically regenerated. + +If you don't have to worry about the amount of disk space consumed, you can extend the maximum age of cached files via the filter hooks `avatar_privacy_gravatars_max_age` and `avatar_privacy_all_images_max_age`. The cron job intervals can also be adjusted via hooks (`avatar_privacy_gravatars_cleanup_interval` and `avatar_privacy_all_images_cleanup_interval`, respectively). + += Can commenters override a registered user's choice whether to display a gravatar by creating fake comments? = + +No, for registered users the user profile is checked, not the table for the commenter's choices. Commenters can not override this value, not even the user themselves if they post a comment when not signed-in. + += Which plugins are known to work with Avatar Privacy? = + +These plugins have been tested successfully in conjunction with Avatar Privacy: + +* [AntiSpam Bee](https://wordpress.org/plugins/antispam-bee/) +* [bbPress](https://wordpress.org/plugins/bbpress/) +* [BuddyPress](https://wordpress.org/plugins/buddypress/) +* [Comments – wpDiscuz](https://wordpress.org/plugins/wpdiscuz/) +* [EWWW Image Optimizer](https://wordpress.org/plugins/ewww-image-optimizer/) +* [Simple Author Box](https://wordpress.org/plugins/simple-author-box/) +* [Simple Local Avatars](https://wordpress.org/plugins/simple-local-avatars/) +* [Simple User Avatar](https://wordpress.org/plugins/simple-user-avatar/) +* [Theme My Login](https://wordpress.org/plugins/theme-my-login/) +* [Ultimate Member](https://wordpress.org/plugins/ultimate-member/) +* [Webmention](https://wordpress.org/plugins/webmention/) +* [WP User Manager – User Profile Builder & Membership](https://wordpress.org/plugins/wp-user-manager/) + +Please note that several [Jetpack by WordPress.com modules](https://wordpress.org/plugins/jetpack/) do not work well with Avatar Privacy because they generate their HTML markup on the WordPress.com servers. + +If you find any problems with particular plugins, please tell me! + += What happens if I remove the plugin? = + +There is a difference between deactivating the plugin and uninstalling it. The plugin gets deactivated if you do so on the plugins page or if you simply delete the plugin files via FTP. No uninstallation tasks are performed then, so you can activate and deactivate the plugin as you want without losing the plugin's settings. + +If you deactivate the plugin und have gravatars turned on, they will again show up for everybody, even those commenters and users who opted out of displaying gravatars. If you changed the default avatar to one of the new local avatar images, the gravatars will not be displayed until you change the default avatar image back. + += OK, but I really want to get rid of everything. How do I that? = + +If you want to completely uninstall the plugin and get rid of any data in the database, you should properly uninstall it: Deactivate the plugin first via the WordPress plugins page and then click 'delete' (same page, next to the plugin). For multisite installations, this has to be done by the network administrator on the network plugins page. + +The plugin saves additional data about whether commenters and users want to display a gravatar or not (if you select that mode in the settings). The following data are stored by the plugin and deleted upon uninstallation: + +* custom table(s) `[prefix]_avatar_privacy` (global or per blog on new multisite installations) +* `usermeta` values per user: `use_gravatar`, `avatar_privacy_hash`, `avatar_privacy_user_avatar` +* `option` per blog: `avatar_privacy_settings` +* option per network (`sitemeta`) on multisite installations: `avatar_privacy_salt` +* `transient` per commenter: `avapr_check_[mail hash]` + +The default avatar image is set to the mystery man if you selected one of the new local default avatar images. + + +== Changelog == + += 2.7.0 (2023-05-01) = +* _Feature_: Avatar Privacy is now compatible with PHP 8.2. +* _Feature_: The plugin now honors the `wp_delete_file` filter when deleting files. +* _Change_: Requires at least PHP 7.4. +* _Change_: Upgrades `identifier` column of `avatar_privacy_hashes` table to 256 characters on supported MySQL/MariaDB versions (as it was in 2.4.0). +* _Change_: The library `yzalis/identicon` has been removed as a dependency. +* _Change_: Avatar Privacy now honors the `wp_delete_file` filter hook. +* _Bugfix_: Icons from Webmentions using Gravatar will get cached now. +* _Bugfix_: Uploading avatars for users with no role on the primary site of a Multsite network now works as expected. + += 2.6.0 (2022-04-18) = +* _Feature_: The size of uploaded images is now checked to make sure processing does not overload the server. By default, all uploaded images have to be smaller than 2000×2000 pixels. The constraints can be adjusted with these new filter hooks: + - `avatar_privacy_upload_min_width` + - `avatar_privacy_upload_min_height` + - `avatar_privacy_upload_max_width` + - `avatar_privacy_upload_max_height` +* _Feature_: Improved caching to reduce the number of database queries. +* _Change_: Requires at least WordPress 5.6 and PHP 7.2. +* _Change_: Support for Internet Explorer (all extant versions, i.e. 9, 10, and 11) has been dropped. +* _Change_: A fabulous new plugin icon designed by [Johanna Amann](https://www.instagram.com/_jo_am/). + += 2.5.2 (2021-04-30) = +* _Bugfix_: When a user is deleted, their local avatar image is removed as well. +* _Bugfix_: The dependency version for JS and CSS files is properly calculated. (This also fixes the apparently empty PHP warning when `WP_DEBUG` is enabled.) + += 2.5.1 (2021-03-13) = +* _Bugfix_: Fixes PHP 8.0 deprecation warning in the `level-2/dice` package. + += 2.5.0 (2021-03-11) = +* _Feature_: Avatar Privacy is now compatible with PHP 8.0. +* _Feature_: Integration for the following plugins had been added: + - [Simple Local Avatars](https://wordpress.org/plugins/simple-local-avatars/) + - [Simple User Avatar](https://wordpress.org/plugins/simple-user-avatar/) +* _Change_: The library `scripturadesign/color` has been removed as a dependency. +* _Change_: Additional hardening. + += 2.4.6 (2021-02-21) = +* _Bugfix_: Unchecking the Gravatar opt-in and anonymous commenting checkboxes in a user's profile screen works again. + += 2.4.5 (2021-02-07) = +* _Bugfix_: Gravatar opt-ins by anonymous commenters are now properly saved on WordPress 5.5 and later. + += 2.4.4 (2021-02-03) = +* _Bugfix_: Gracefully handle changes to WordPress' default DB collation (no more `Illegal mix of collations` errors). + += 2.4.3 (2021-01-15) = +* _Bugfix_: Don't break stuff (another build process fix, for real this time). + += 2.4.2 (2021-01-15) = +* _Bugfix_: An unfortunate oversight in the build process led to crashes instead of the intended graceful failure when the installation requirements were not met. + += 2.4.1 (2021-01-10) = +* _Bugfix_: Don't break the site when the options value in the DB has become corrupted. +* _Bugfix_: Workaround for maximum database key length when using MySQL < 5.7.7 or MariaDB < 10.2.2. + += 2.4.0 (2021-01-10) = +* _Feature_: Legacy (default) avatars are now properly cached and resized. +* _Feature_: There are now API methods to get and set a user's (local) avatar (and their Gravatar and anonymous commenting policies). +* _Feature_: New WP-CLI commands relating to local avatars added. +* _Feature_: Integration for the [Simple Author Box](https://wordpress.org/plugins/simple-author-box/) plugin has been added. +* _Change_: Requires at least WordPress 5.2 and PHP 7.0. +* _Change_: The `yzalis/identicon` package has been updated to version 2.0. +* _Change_: Some unused files have been removed from the `vendor-scoped` directory. +* _Change_: A new per-site database table for fast hash lookup has been introduced (base name `avatar_privacy_hashes`). +* _Change_: General code clean-up and removal of PHP 5.6 workarounds. +* _Bugfix_: Gravatars will be properly regenerated for comment authors that have not set a policy (when the site-admin has switched the default to "opt-out"). +* _Bugfix_: When a user requests deletion of their personal data, this now includes the uploaded avatar image files. +* _Bugfix_: A timestamp is added to uploaded avatar images for better browser caching in the Profile screen. + += 2.3.4 (2020-03-22) = +* _Bugfix_: Allow plain URLs as default avatars. Use the filter hook `avatar_privacy_allow_remote_default_icon_url` to allow third-party domains and `avatar_privacy_validate_default_icon_url` if you want to implement your own image URL validation. +* _Bugfix_: Properly handle trackback/linkback avatars. This includes a workaround for avatars provided by the [Webmention](https://wordpress.org/plugins/webmention/) plugin. You can use `avatar_privacy_allow_remote_avatar_url` to prohibit third-party domains (the default is to allow them for webmentions) and `avatar_privacy_validate_avatar_url` if you want to implement your own image URL validation. +* _Change_: Due to the trackback/linkback bug fix, the priority for `pre_get_avatar_data` filter can now be adjusted using the `avatar_privacy_pre_get_avatar_data_filter_priority` hook instead of being hardcoded. + += 2.3.3 (2019-12-27) = +* _Bugfix_: Timestamps in WP-CLI commands now always use GMT. + += 2.3.2 (2019-11-09) = +* _Bugfix_: Some error messages were not getting translated because of a [WP.org infrastructure change](https://make.wordpress.org/core/2018/11/09/new-javascript-i18n-support-in-wordpress/). + += 2.3.1 (2019-09-29) = +* _Bugfix_: Will not crash on WordPress 4.9 anymore when the Gutenberg plugin is not activated. +* _Bugfix_: The label for the block settings panel of the Avatar block was incorrect. + += 2.3.0 (2019-09-28) = +* _Feature_: New shortcode `[avatar-privacy-form]` (optional parameter: `avatar-size`) to allow changing Avatar Privacy's user settings on the frontend of the site. +* _Feature_: Two blocks have been added to the Block Editor to allow displaying a user's avatar and to change the settings related Avatar Privacy on the frontend. +* _Feature_: Three new generated default avatars: + - Bird Avatars, + - Cat Avatars (both designed by David Revoy), and + - RoboHash (designed by Zikri Kader). +* _Feature_: Integration for the following plugins had been added: + - [BuddyPress](https://wordpress.org/plugins/buddypress/) + - [Theme My Login](https://wordpress.org/plugins/theme-my-login/) + - [Ultimate Member](https://wordpress.org/plugins/ultimate-member/) +* _Feature_: New template function `\Avatar_Privacy\gravatar_checkbox()` for legacy themes added. +* _Feature_: There is a CLI interface to some parts of Avatar Privacy: + - `wp avatar-privacy db show`: Show information about the custom database table(s). + - `wp avatar-privacy db list`: List entries in the custom database table(s). + - `wp avatar-privacy db create`: Create the custom database table. + - `wp avatar-privacy db upgrade`: Upgrade the structure of the custom database table. + - `wp avatar-privacy uninstall`: Remove data added by Avatar Privacy. + - `wp avatar-privacy cron list`: List active cron jobs created by the plugin. + - `wp avatar-privacy cron delete`: Delete cron jobs created by the plugin. +* _Change_: `avapr_get_avatar_checkbox()` has been deprecated in favor of `\Avatar_Privacy\get_gravatar_checkbox()`. +* _Change_: The ID and name of the `use_gravatar` comment form checkbox has been changed to `avatar-privacy-use-gravatar`. Please update custom CSS rules accordingly. +* _Change_: Additional inline styling is added to the `avatar-privacy-use-gravatar` comment form checkbox to work around common theme limitations. Styling can be disabled using the `avatar_privacy_comment_checkbox_disable_inline_style` filter hook. +* _Change_: All external PHP dependencies have been moved to the namespace `Avatar_Privacy\Vendor` to reduce the chance of conflicts with other plugins. +* _Bugfix_: Gravatars are only cached if they are actually images to prevent issues with temporary Varnish errors on Gravatar.com. + += 2.2.2 (2019-06-23) = +* _Bugfix_: Re-allow installation on 4.9.x for increased compatibility with possible future WordPress Core changes. The 2.2.x branch of Avatar Privacy is the last that will support WordPress < 5.2. + += 2.2.1 (2019-06-08) = +* _Bugfix_: Compatibility with Windows servers. + += 2.2.0 (2019-05-12) = +* _Feature_: Integration for the following plugins had been added: + - [wpDiscuz](https://wordpress.org/plugins/wpdiscuz/) + - [WP User Manager](https://wordpress.org/plugins/wp-user-manager/) + += 2.1.0 (2019-04-14) = +* _Feature_: Improved compatibility with multisite installations. Plugin data will be properly deleted on uninstallation or when a site is removed. ("Large Networks" will still have to take manual action to prevent timeouts.) +* _Feature_: Network settings for enabling/disabling global table use on multisite. Existing global data will be migrated to the site-specific tables when global table use is disabled (but not in the other direction). +* _Change_: `usermeta` keys are now prefixed (`avatar_privacy_use_gravatar` instead of `use_gravatar`). +* _Change_: Generally improved code quality through unit tests. +* _Bugfix_: New multisite installations were incorrectly detected as "legacy", making them use the global table (instead of per-site tables). Affected installations can be switched via the new network settings page. + += 2.0.5 (2019-02-23) = +* _Bugfix_: Fixed a previously undiscovered compatibility issue with recent versions of EWWW Image Optimizer. + += 2.0.4 (2019-02-22) = +* _Bugfix_: Updated included libraries for improved compatibility with other plugins. + += 2.0.3 (2018-11-30) = +* _Bugfix_: Prevent warnings when trying to retrieve the avatar for an invalid user ID. + += 2.0.2 (2018-09-09) = +* _Bugfix_: Updated included libraries for improved compatibility with other plugins. + += 2.0.1 (2018-08-16) = +* _Bugfix_: The plugin no longer fails with a fatal error on PHP 5.6 (accidentally introduced in 2.0.0). + += 2.0.0 (2018-08-11) = +* _Feature_: Administrators can now upload site-specific default avatar images. +* _Feature_: The default policy previously only accessible via the `avatar_privacy_gravatar_use_default` hook can now be set from the `Discussion` settings page. +* _Feature_: New filter hooks `avatar_privacy_gravatar_link_rel` and `avatar_privacy_gravatar_link_target` to filter the `rel` and `target` attributes of all links to Gravatar.com. +* _Bugfix_: The REST API returned incorrect avatar URLs for registered users (workaround for [trac ticket #40030](https://core.trac.wordpress.org/ticket/40030)). +* _Bugfix_: The gravatar use cookie is only set when the comment author has given consent. +* _Change_: Internal restructuring to make maintenance easier. + += 1.1.1 (2018-06-11) = +* _Bugfix_: Changing the default gravatar policy via `avatar_privacy_gravatar_use_default` works again for registered users. + += 1.1.0 (2018-06-10) = +* _Feature_: Supports the new privacy tools on WordPress >= 4.9.6 (export and deletion of personal data, suggested privacy notice text). +* _Feature_: Registered users can opt into allowing logged-out comments with the same mail address to user their profile pictures. +* _Feature_: The plugin is now compatible with bbPress. +* _Feature_: The position of the `use_gravatar` checkbox can be adjusted via the new filter hook `avatar_privacy_use_gravatar_position`. +* _Change_: Trashed comments and comments marked as spam do not trigger a validation request to Gravatar.com if the admin has set the default gravatar use policy to "enabled" via the filter hook `avatar_privacy_gravatar_use_default`. + += 1.0.7 (2018-06-06) = +* _Bugfix_: The `use_gravatar` is actually checked when the cookie has been set. +* _Bugfix_: A (harmless) PHP warning has been fixed. + += 1.0.6 (2018-05-29) = +* _Bugfix_: Only valid response codes from Gravatar.com are cached (200 and 404). +* _Bugfix_: Plugin transients are cleared on plugin upgrades. +* _Bugfix_: The workaround for [trac ticket #42663](https://core.trac.wordpress.org/ticket/42663) introduced in 1.0.5 is expanded to all uses of `wp_get_image_editor()`. + += 1.0.5 (2018-05-22) = +* _Bugfix_: Prefer GD-based implementations of `WP_Image_Editor` to work around [trac ticket #42663](https://core.trac.wordpress.org/ticket/42663). +* _Bugfix_: The `rel` and `target` attributes are allowed in `use_gravatar` checkbox labels and by the default, the `noopener` and `nofollow` values for the `rel` attribute are added to the Gravatar.com link. +* _Bugfix_: Invalid 0-byte image files are not saved anymore. + += 1.0.4 (2018-05-20) = +* _Bugfix_: When the plugin is uninstalled, the default avatar image is really reset to `mystery` if necessary. +* _Bugfix_: The `use_gravatar` checkbox is compatible with more themes now. + += 1.0.3 (2018-05-17) = +* _Bugfix_: The plugin no longer fails with a fatal error on PHP 5.6. + += 1.0.2 (2018-05-16) = +* _Bugfix_: PNG avatars were not created correctly when EWWW Image Optimizer was enabled. + += 1.0.1 (2018-05-14) = +* _Bugfix_: Non-multisite installations triggered an SQL error in some situations. + += 1.0 (2018-05-13) = +* _Feature_: All default avatars are generated on your server. +* _Feature_: Gravatar.com avatars are cached locally. (The cache is cleaned regularly via a cron job to prevent unlimited growth.) +* _Feature_: Registered users can upload their own avatar images to your server. +* _Change_: Refactored according to modern development principles. +* _Change_: Removed settings in favor of sensible default values and filter hooks: + - Gravatar.com usage is opt-in and gravatars are only displayed if the exist. + - The default behavior for legacy comments can be customized via the `avatar_privacy_gravatar_use_default` filter hook. +* _Change_: All static default icons are now SVG images. diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 0000000..4ddb380 --- /dev/null +++ b/uninstall.php @@ -0,0 +1,63 @@ +create( Uninstallation::class ); + $uninstaller->run(); +} +uninstall_avatar_privacy(); diff --git a/vendor-scoped/autoload.php b/vendor-scoped/autoload.php new file mode 100644 index 0000000..d0fdb60 --- /dev/null +++ b/vendor-scoped/autoload.php @@ -0,0 +1,25 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\AutoloadAvatarPrivacy; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\AutoloadAvatarPrivacy\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var ?string */ + private $vendorDir; + + // PSR-4 + /** + * @var array[] + * @psalm-var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array[] + * @psalm-var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var array[] + * @psalm-var array + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * @var array[] + * @psalm-var array> + */ + private $prefixesPsr0 = array(); + /** + * @var array[] + * @psalm-var array + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var string[] + * @psalm-var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var bool[] + * @psalm-var array + */ + private $missingClasses = array(); + + /** @var ?string */ + private $apcuPrefix; + + /** + * @var self[] + */ + private static $registeredLoaders = array(); + + /** + * @param ?string $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return string[] + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array[] + * @psalm-return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return array[] + * @psalm-return array + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return array[] + * @psalm-return array + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return string[] Array of classname => path + * @psalm-return array + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param string[] $classMap Class to filename map + * @psalm-param array $classMap + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders indexed by their corresponding vendor directories. + * + * @return self[] + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/vendor-scoped/composer/LICENSE b/vendor-scoped/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/vendor-scoped/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +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. + diff --git a/vendor-scoped/composer/autoload_classmap.php b/vendor-scoped/composer/autoload_classmap.php new file mode 100644 index 0000000..fe56c30 --- /dev/null +++ b/vendor-scoped/composer/autoload_classmap.php @@ -0,0 +1,181 @@ + $baseDir . '/includes/avatar-privacy/avatar-handlers/class-avatar-handler.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Abstract_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/class-abstract-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Custom_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/class-custom-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Bird_Avatar_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-bird-avatar-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Cat_Avatar_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-cat-avatar-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Identicon_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-identicon-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Monster_ID_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-monster-id-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Retro_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-retro-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Rings_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-rings-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Robohash_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-robohash-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Wavatar_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-wavatar-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generating_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/class-generating-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generator' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/class-generator.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Bird_Avatar' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-bird-avatar.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Cat_Avatar' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-cat-avatar.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Jdenticon' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-jdenticon.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Monster_ID' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-monster-id.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\PNG_Parts_Generator' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-png-parts-generator.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Parts_Generator' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-parts-generator.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Retro' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-retro.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Rings' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-rings.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Robohash' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-robohash.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Wavatar' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-wavatar.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/class-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\SVG_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/class-svg-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Static_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/class-static-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Static_Icons\\Bowling_Pin_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-bowling-pin-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Static_Icons\\Mystery_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-mystery-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Static_Icons\\Silhouette_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-silhouette-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Static_Icons\\Speech_Bubble_Icon_Provider' => $baseDir . '/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-speech-bubble-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons_Handler' => $baseDir . '/includes/avatar-privacy/avatar-handlers/class-default-icons-handler.php', + 'Avatar_Privacy\\Avatar_Handlers\\Gravatar_Cache_Handler' => $baseDir . '/includes/avatar-privacy/avatar-handlers/class-gravatar-cache-handler.php', + 'Avatar_Privacy\\Avatar_Handlers\\Legacy_Icon_Handler' => $baseDir . '/includes/avatar-privacy/avatar-handlers/class-legacy-icon-handler.php', + 'Avatar_Privacy\\Avatar_Handlers\\User_Avatar_Handler' => $baseDir . '/includes/avatar-privacy/avatar-handlers/class-user-avatar-handler.php', + 'Avatar_Privacy\\CLI\\Abstract_Command' => $baseDir . '/includes/avatar-privacy/cli/class-abstract-command.php', + 'Avatar_Privacy\\CLI\\Command' => $baseDir . '/includes/avatar-privacy/cli/class-command.php', + 'Avatar_Privacy\\CLI\\Cron_Command' => $baseDir . '/includes/avatar-privacy/cli/class-cron-command.php', + 'Avatar_Privacy\\CLI\\Database_Command' => $baseDir . '/includes/avatar-privacy/cli/class-database-command.php', + 'Avatar_Privacy\\CLI\\Default_Command' => $baseDir . '/includes/avatar-privacy/cli/class-default-command.php', + 'Avatar_Privacy\\CLI\\Uninstall_Command' => $baseDir . '/includes/avatar-privacy/cli/class-uninstall-command.php', + 'Avatar_Privacy\\CLI\\User_Command' => $baseDir . '/includes/avatar-privacy/cli/class-user-command.php', + 'Avatar_Privacy\\Component' => $baseDir . '/includes/avatar-privacy/class-component.php', + 'Avatar_Privacy\\Components\\Avatar_Handling' => $baseDir . '/includes/avatar-privacy/components/class-avatar-handling.php', + 'Avatar_Privacy\\Components\\Block_Editor' => $baseDir . '/includes/avatar-privacy/components/class-block-editor.php', + 'Avatar_Privacy\\Components\\Command_Line_Interface' => $baseDir . '/includes/avatar-privacy/components/class-command-line-interface.php', + 'Avatar_Privacy\\Components\\Comments' => $baseDir . '/includes/avatar-privacy/components/class-comments.php', + 'Avatar_Privacy\\Components\\Image_Proxy' => $baseDir . '/includes/avatar-privacy/components/class-image-proxy.php', + 'Avatar_Privacy\\Components\\Integrations' => $baseDir . '/includes/avatar-privacy/components/class-integrations.php', + 'Avatar_Privacy\\Components\\Network_Settings_Page' => $baseDir . '/includes/avatar-privacy/components/class-network-settings-page.php', + 'Avatar_Privacy\\Components\\Privacy_Tools' => $baseDir . '/includes/avatar-privacy/components/class-privacy-tools.php', + 'Avatar_Privacy\\Components\\Settings_Page' => $baseDir . '/includes/avatar-privacy/components/class-settings-page.php', + 'Avatar_Privacy\\Components\\Setup' => $baseDir . '/includes/avatar-privacy/components/class-setup.php', + 'Avatar_Privacy\\Components\\Shortcodes' => $baseDir . '/includes/avatar-privacy/components/class-shortcodes.php', + 'Avatar_Privacy\\Components\\Uninstallation' => $baseDir . '/includes/avatar-privacy/components/class-uninstallation.php', + 'Avatar_Privacy\\Components\\User_Profile' => $baseDir . '/includes/avatar-privacy/components/class-user-profile.php', + 'Avatar_Privacy\\Controller' => $baseDir . '/includes/avatar-privacy/class-controller.php', + 'Avatar_Privacy\\Core' => $baseDir . '/includes/avatar-privacy/class-core.php', + 'Avatar_Privacy\\Core\\API' => $baseDir . '/includes/avatar-privacy/core/class-api.php', + 'Avatar_Privacy\\Core\\Comment_Author_Fields' => $baseDir . '/includes/avatar-privacy/core/class-comment-author-fields.php', + 'Avatar_Privacy\\Core\\Default_Avatars' => $baseDir . '/includes/avatar-privacy/core/class-default-avatars.php', + 'Avatar_Privacy\\Core\\Settings' => $baseDir . '/includes/avatar-privacy/core/class-settings.php', + 'Avatar_Privacy\\Core\\User_Fields' => $baseDir . '/includes/avatar-privacy/core/class-user-fields.php', + 'Avatar_Privacy\\Data_Storage\\Cache' => $baseDir . '/includes/avatar-privacy/data-storage/class-cache.php', + 'Avatar_Privacy\\Data_Storage\\Database\\Comment_Author_Table' => $baseDir . '/includes/avatar-privacy/data-storage/database/class-comment-author-table.php', + 'Avatar_Privacy\\Data_Storage\\Database\\Hashes_Table' => $baseDir . '/includes/avatar-privacy/data-storage/database/class-hashes-table.php', + 'Avatar_Privacy\\Data_Storage\\Database\\Table' => $baseDir . '/includes/avatar-privacy/data-storage/database/class-table.php', + 'Avatar_Privacy\\Data_Storage\\Filesystem_Cache' => $baseDir . '/includes/avatar-privacy/data-storage/class-filesystem-cache.php', + 'Avatar_Privacy\\Data_Storage\\Network_Options' => $baseDir . '/includes/avatar-privacy/data-storage/class-network-options.php', + 'Avatar_Privacy\\Data_Storage\\Options' => $baseDir . '/includes/avatar-privacy/data-storage/class-options.php', + 'Avatar_Privacy\\Data_Storage\\Site_Transients' => $baseDir . '/includes/avatar-privacy/data-storage/class-site-transients.php', + 'Avatar_Privacy\\Data_Storage\\Transients' => $baseDir . '/includes/avatar-privacy/data-storage/class-transients.php', + 'Avatar_Privacy\\Exceptions\\Avatar_Comment_Type_Exception' => $baseDir . '/includes/avatar-privacy/exceptions/class-avatar-comment-type-exception.php', + 'Avatar_Privacy\\Exceptions\\Database_Exception' => $baseDir . '/includes/avatar-privacy/exceptions/class-database-exception.php', + 'Avatar_Privacy\\Exceptions\\File_Deletion_Exception' => $baseDir . '/includes/avatar-privacy/exceptions/class-file-deletion-exception.php', + 'Avatar_Privacy\\Exceptions\\Filesystem_Exception' => $baseDir . '/includes/avatar-privacy/exceptions/class-filesystem-exception.php', + 'Avatar_Privacy\\Exceptions\\Form_Field_Not_Found_Exception' => $baseDir . '/includes/avatar-privacy/exceptions/class-form-field-not-found-exception.php', + 'Avatar_Privacy\\Exceptions\\Invalid_Nonce_Exception' => $baseDir . '/includes/avatar-privacy/exceptions/class-invalid-nonce-exception.php', + 'Avatar_Privacy\\Exceptions\\Object_Factory_Exception' => $baseDir . '/includes/avatar-privacy/exceptions/class-object-factory-exception.php', + 'Avatar_Privacy\\Exceptions\\PNG_Image_Exception' => $baseDir . '/includes/avatar-privacy/exceptions/class-png-image-exception.php', + 'Avatar_Privacy\\Exceptions\\Part_Files_Not_Found_Exception' => $baseDir . '/includes/avatar-privacy/exceptions/class-part-files-not-found-exception.php', + 'Avatar_Privacy\\Exceptions\\Upload_Handling_Exception' => $baseDir . '/includes/avatar-privacy/exceptions/class-upload-handling-exception.php', + 'Avatar_Privacy\\Factory' => $baseDir . '/includes/avatar-privacy/class-factory.php', + 'Avatar_Privacy\\Integrations\\BBPress_Integration' => $baseDir . '/includes/avatar-privacy/integrations/class-bbpress-integration.php', + 'Avatar_Privacy\\Integrations\\BuddyPress_Integration' => $baseDir . '/includes/avatar-privacy/integrations/class-buddypress-integration.php', + 'Avatar_Privacy\\Integrations\\Plugin_Integration' => $baseDir . '/includes/avatar-privacy/integrations/class-plugin-integration.php', + 'Avatar_Privacy\\Integrations\\Simple_Author_Box_Integration' => $baseDir . '/includes/avatar-privacy/integrations/class-simple-author-box-integration.php', + 'Avatar_Privacy\\Integrations\\Simple_Local_Avatars_Integration' => $baseDir . '/includes/avatar-privacy/integrations/class-simple-local-avatars-integration.php', + 'Avatar_Privacy\\Integrations\\Simple_User_Avatar_Integration' => $baseDir . '/includes/avatar-privacy/integrations/class-simple-user-avatar-integration.php', + 'Avatar_Privacy\\Integrations\\Theme_My_Login_Profiles_Integration' => $baseDir . '/includes/avatar-privacy/integrations/class-theme-my-login-profiles-integration.php', + 'Avatar_Privacy\\Integrations\\Ultimate_Member_Integration' => $baseDir . '/includes/avatar-privacy/integrations/class-ultimate-member-integration.php', + 'Avatar_Privacy\\Integrations\\WPDiscuz_Integration' => $baseDir . '/includes/avatar-privacy/integrations/class-wpdiscuz-integration.php', + 'Avatar_Privacy\\Integrations\\WP_User_Manager_Integration' => $baseDir . '/includes/avatar-privacy/integrations/class-wp-user-manager-integration.php', + 'Avatar_Privacy\\Requirements' => $baseDir . '/includes/avatar-privacy/class-requirements.php', + 'Avatar_Privacy\\Tools\\HTML\\Dependencies' => $baseDir . '/includes/avatar-privacy/tools/html/class-dependencies.php', + 'Avatar_Privacy\\Tools\\HTML\\User_Form' => $baseDir . '/includes/avatar-privacy/tools/html/class-user-form.php', + 'Avatar_Privacy\\Tools\\Hasher' => $baseDir . '/includes/avatar-privacy/tools/class-hasher.php', + 'Avatar_Privacy\\Tools\\Images\\Color' => $baseDir . '/includes/avatar-privacy/tools/images/class-color.php', + 'Avatar_Privacy\\Tools\\Images\\Editor' => $baseDir . '/includes/avatar-privacy/tools/images/class-editor.php', + 'Avatar_Privacy\\Tools\\Images\\Image_File' => $baseDir . '/includes/avatar-privacy/tools/images/class-image-file.php', + 'Avatar_Privacy\\Tools\\Images\\Image_Stream' => $baseDir . '/includes/avatar-privacy/tools/images/class-image-stream.php', + 'Avatar_Privacy\\Tools\\Images\\PNG' => $baseDir . '/includes/avatar-privacy/tools/images/class-png.php', + 'Avatar_Privacy\\Tools\\Images\\SVG' => $baseDir . '/includes/avatar-privacy/tools/images/class-svg.php', + 'Avatar_Privacy\\Tools\\Images\\Type' => $baseDir . '/includes/avatar-privacy/tools/images/class-type.php', + 'Avatar_Privacy\\Tools\\Multisite' => $baseDir . '/includes/avatar-privacy/tools/class-multisite.php', + 'Avatar_Privacy\\Tools\\Network\\Gravatar_Service' => $baseDir . '/includes/avatar-privacy/tools/network/class-gravatar-service.php', + 'Avatar_Privacy\\Tools\\Network\\Remote_Image_Service' => $baseDir . '/includes/avatar-privacy/tools/network/class-remote-image-service.php', + 'Avatar_Privacy\\Tools\\Number_Generator' => $baseDir . '/includes/avatar-privacy/tools/class-number-generator.php', + 'Avatar_Privacy\\Tools\\Template' => $baseDir . '/includes/avatar-privacy/tools/class-template.php', + 'Avatar_Privacy\\Upload_Handlers\\Custom_Default_Icon_Upload_Handler' => $baseDir . '/includes/avatar-privacy/upload-handlers/class-custom-default-icon-upload-handler.php', + 'Avatar_Privacy\\Upload_Handlers\\UI\\File_Upload_Input' => $baseDir . '/includes/avatar-privacy/upload-handlers/ui/class-file-upload-input.php', + 'Avatar_Privacy\\Upload_Handlers\\Upload_Handler' => $baseDir . '/includes/avatar-privacy/upload-handlers/class-upload-handler.php', + 'Avatar_Privacy\\Upload_Handlers\\User_Avatar_Upload_Handler' => $baseDir . '/includes/avatar-privacy/upload-handlers/class-user-avatar-upload-handler.php', + 'Avatar_Privacy\\Vendor\\Colors\\RandomColor' => $vendorDir . '/mistic100/randomcolor/src/RandomColor.php', + 'Avatar_Privacy\\Vendor\\Dice\\Dice' => $vendorDir . '/level-2/dice/Dice.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Canvas' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Canvas.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\CanvasContext' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/CanvasContext.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\ColorUtils' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/ColorUtils.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Matrix' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Matrix.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Png\\PngBuffer' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Png/PngBuffer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Png\\PngEncoder' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Png/PngEncoder.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Png\\PngPalette' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Png/PngPalette.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Point' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Point.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\Edge' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Rasterization/Edge.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\EdgeIntersection' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Rasterization/EdgeIntersection.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\EdgeSuperSampleIntersection' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Rasterization/EdgeSuperSampleIntersection.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\EdgeTable' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Rasterization/EdgeTable.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\Layer' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Rasterization/Layer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\LayerManager' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Rasterization/LayerManager.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\Rasterizer' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Rasterization/Rasterizer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\SuperSampleBuffer' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Rasterization/SuperSampleBuffer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\SuperSampleRange' => $vendorDir . '/jdenticon/jdenticon/src/Canvas/Rasterization/SuperSampleRange.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Color' => $vendorDir . '/jdenticon/jdenticon/src/Color.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Identicon' => $vendorDir . '/jdenticon/jdenticon/src/Identicon.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\IdenticonStyle' => $vendorDir . '/jdenticon/jdenticon/src/IdenticonStyle.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\AbstractRenderer' => $vendorDir . '/jdenticon/jdenticon/src/Rendering/AbstractRenderer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\ColorTheme' => $vendorDir . '/jdenticon/jdenticon/src/Rendering/ColorTheme.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\IconGenerator' => $vendorDir . '/jdenticon/jdenticon/src/Rendering/IconGenerator.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\ImagickRenderer' => $vendorDir . '/jdenticon/jdenticon/src/Rendering/ImagickRenderer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\InternalPngRenderer' => $vendorDir . '/jdenticon/jdenticon/src/Rendering/InternalPngRenderer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\Point' => $vendorDir . '/jdenticon/jdenticon/src/Rendering/Point.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\Rectangle' => $vendorDir . '/jdenticon/jdenticon/src/Rendering/Rectangle.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\RendererInterface' => $vendorDir . '/jdenticon/jdenticon/src/Rendering/RendererInterface.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\SvgPath' => $vendorDir . '/jdenticon/jdenticon/src/Rendering/SvgPath.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\SvgRenderer' => $vendorDir . '/jdenticon/jdenticon/src/Rendering/SvgRenderer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\Transform' => $vendorDir . '/jdenticon/jdenticon/src/Rendering/Transform.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\TriangleDirection' => $vendorDir . '/jdenticon/jdenticon/src/Rendering/TriangleDirection.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Shapes\\Shape' => $vendorDir . '/jdenticon/jdenticon/src/Shapes/Shape.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Shapes\\ShapeCategory' => $vendorDir . '/jdenticon/jdenticon/src/Shapes/ShapeCategory.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Shapes\\ShapeDefinitions' => $vendorDir . '/jdenticon/jdenticon/src/Shapes/ShapeDefinitions.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Shapes\\ShapePosition' => $vendorDir . '/jdenticon/jdenticon/src/Shapes/ShapePosition.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\Data_Storage\\Abstract_Cache' => $vendorDir . '/mundschenk-at/wp-data-storage/src/class-abstract-cache.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\Data_Storage\\Cache' => $vendorDir . '/mundschenk-at/wp-data-storage/src/class-cache.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\Data_Storage\\Network_Options' => $vendorDir . '/mundschenk-at/wp-data-storage/src/class-network-options.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\Data_Storage\\Options' => $vendorDir . '/mundschenk-at/wp-data-storage/src/class-options.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\Data_Storage\\Site_Transients' => $vendorDir . '/mundschenk-at/wp-data-storage/src/class-site-transients.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\Data_Storage\\Transients' => $vendorDir . '/mundschenk-at/wp-data-storage/src/class-transients.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Abstract_Control' => $vendorDir . '/mundschenk-at/wp-settings-ui/src/ui/class-abstract-control.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Control' => $vendorDir . '/mundschenk-at/wp-settings-ui/src/ui/class-control.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Control_Factory' => $vendorDir . '/mundschenk-at/wp-settings-ui/src/ui/class-control-factory.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Checkbox_Input' => $vendorDir . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-checkbox-input.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Display_Text' => $vendorDir . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-display-text.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Hidden_Input' => $vendorDir . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-hidden-input.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Input' => $vendorDir . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-input.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Number_Input' => $vendorDir . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-number-input.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Select' => $vendorDir . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-select.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Submit_Input' => $vendorDir . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-submit-input.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Text_Input' => $vendorDir . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-text-input.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Textarea' => $vendorDir . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-textarea.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\WP_Requirements' => $vendorDir . '/mundschenk-at/check-wp-requirements/class-wp-requirements.php', + 'Avatar_Privacy\\Vendor\\splitbrain\\RingIcon\\AbstractRingIcon' => $vendorDir . '/splitbrain/php-ringicon/src/AbstractRingIcon.php', + 'Avatar_Privacy\\Vendor\\splitbrain\\RingIcon\\RingIcon' => $vendorDir . '/splitbrain/php-ringicon/src/RingIcon.php', + 'Avatar_Privacy\\Vendor\\splitbrain\\RingIcon\\RingIconSVG' => $vendorDir . '/splitbrain/php-ringicon/src/RingIconSVG.php', +); diff --git a/vendor-scoped/composer/autoload_files.php b/vendor-scoped/composer/autoload_files.php new file mode 100644 index 0000000..5515576 --- /dev/null +++ b/vendor-scoped/composer/autoload_files.php @@ -0,0 +1,12 @@ + $baseDir . '/includes/avatar-privacy-functions.php', + '96cc9366f0f8a682f3b272c65c21b84f' => $baseDir . '/includes/avatar-privacy/functions.php', + '664e375fee598657b31fb4bcd80b6963' => $baseDir . '/includes/avatar-privacy/tools/functions.php', +); diff --git a/vendor-scoped/composer/autoload_namespaces.php b/vendor-scoped/composer/autoload_namespaces.php new file mode 100644 index 0000000..15a2ff3 --- /dev/null +++ b/vendor-scoped/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ +setClassMapAuthoritative(true); + $loader->register(true); + + $filesToLoad = \Composer\Autoload\ComposerStaticInitAvatarPrivacy::$files; + $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } + }, null, null); + foreach ($filesToLoad as $fileIdentifier => $file) { + $requireFile($fileIdentifier, $file); + } + + return $loader; + } +} diff --git a/vendor-scoped/composer/autoload_static.php b/vendor-scoped/composer/autoload_static.php new file mode 100644 index 0000000..6f03f6c --- /dev/null +++ b/vendor-scoped/composer/autoload_static.php @@ -0,0 +1,234 @@ + __DIR__ . '/../..' . '/includes/avatar-privacy-functions.php', + '96cc9366f0f8a682f3b272c65c21b84f' => __DIR__ . '/../..' . '/includes/avatar-privacy/functions.php', + '664e375fee598657b31fb4bcd80b6963' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/functions.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'D' => + array ( + ), + 'A' => + array ( + 'Avatar_Privacy\\Vendor\\splitbrain\\RingIcon\\' => 42, + 'Avatar_Privacy\\Vendor\\Jdenticon\\' => 32, + 'Avatar_Privacy\\Vendor\\Dice\\' => 27, + 'Avatar_Privacy\\Vendor\\Colors\\' => 29, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Avatar_Privacy\\Vendor\\splitbrain\\RingIcon\\' => + array ( + 0 => __DIR__ . '/..' . '/splitbrain/php-ringicon/src', + ), + 'Avatar_Privacy\\Vendor\\Jdenticon\\' => + array ( + 0 => __DIR__ . '/..' . '/jdenticon/jdenticon/src', + ), + 'Avatar_Privacy\\Vendor\\Dice\\' => + array ( + 0 => __DIR__ . '/..' . '/level-2/dice', + ), + 'Avatar_Privacy\\Vendor\\Colors\\' => + array ( + 0 => __DIR__ . '/..' . '/mistic100/randomcolor/src', + ), + ); + + public static $classMap = array ( + 'Avatar_Privacy\\Avatar_Handlers\\Avatar_Handler' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/class-avatar-handler.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Abstract_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/class-abstract-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Custom_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/class-custom-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Bird_Avatar_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-bird-avatar-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Cat_Avatar_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-cat-avatar-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Identicon_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-identicon-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Monster_ID_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-monster-id-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Retro_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-retro-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Rings_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-rings-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Robohash_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-robohash-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generated_Icons\\Wavatar_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generated-icons/class-wavatar-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generating_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/class-generating-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generator' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/class-generator.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Bird_Avatar' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-bird-avatar.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Cat_Avatar' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-cat-avatar.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Jdenticon' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-jdenticon.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Monster_ID' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-monster-id.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\PNG_Parts_Generator' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-png-parts-generator.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Parts_Generator' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-parts-generator.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Retro' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-retro.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Rings' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-rings.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Robohash' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-robohash.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Generators\\Wavatar' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/generators/class-wavatar.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/class-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\SVG_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/class-svg-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Static_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/class-static-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Static_Icons\\Bowling_Pin_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-bowling-pin-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Static_Icons\\Mystery_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-mystery-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Static_Icons\\Silhouette_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-silhouette-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons\\Static_Icons\\Speech_Bubble_Icon_Provider' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/default-icons/static-icons/class-speech-bubble-icon-provider.php', + 'Avatar_Privacy\\Avatar_Handlers\\Default_Icons_Handler' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/class-default-icons-handler.php', + 'Avatar_Privacy\\Avatar_Handlers\\Gravatar_Cache_Handler' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/class-gravatar-cache-handler.php', + 'Avatar_Privacy\\Avatar_Handlers\\Legacy_Icon_Handler' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/class-legacy-icon-handler.php', + 'Avatar_Privacy\\Avatar_Handlers\\User_Avatar_Handler' => __DIR__ . '/../..' . '/includes/avatar-privacy/avatar-handlers/class-user-avatar-handler.php', + 'Avatar_Privacy\\CLI\\Abstract_Command' => __DIR__ . '/../..' . '/includes/avatar-privacy/cli/class-abstract-command.php', + 'Avatar_Privacy\\CLI\\Command' => __DIR__ . '/../..' . '/includes/avatar-privacy/cli/class-command.php', + 'Avatar_Privacy\\CLI\\Cron_Command' => __DIR__ . '/../..' . '/includes/avatar-privacy/cli/class-cron-command.php', + 'Avatar_Privacy\\CLI\\Database_Command' => __DIR__ . '/../..' . '/includes/avatar-privacy/cli/class-database-command.php', + 'Avatar_Privacy\\CLI\\Default_Command' => __DIR__ . '/../..' . '/includes/avatar-privacy/cli/class-default-command.php', + 'Avatar_Privacy\\CLI\\Uninstall_Command' => __DIR__ . '/../..' . '/includes/avatar-privacy/cli/class-uninstall-command.php', + 'Avatar_Privacy\\CLI\\User_Command' => __DIR__ . '/../..' . '/includes/avatar-privacy/cli/class-user-command.php', + 'Avatar_Privacy\\Component' => __DIR__ . '/../..' . '/includes/avatar-privacy/class-component.php', + 'Avatar_Privacy\\Components\\Avatar_Handling' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-avatar-handling.php', + 'Avatar_Privacy\\Components\\Block_Editor' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-block-editor.php', + 'Avatar_Privacy\\Components\\Command_Line_Interface' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-command-line-interface.php', + 'Avatar_Privacy\\Components\\Comments' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-comments.php', + 'Avatar_Privacy\\Components\\Image_Proxy' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-image-proxy.php', + 'Avatar_Privacy\\Components\\Integrations' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-integrations.php', + 'Avatar_Privacy\\Components\\Network_Settings_Page' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-network-settings-page.php', + 'Avatar_Privacy\\Components\\Privacy_Tools' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-privacy-tools.php', + 'Avatar_Privacy\\Components\\Settings_Page' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-settings-page.php', + 'Avatar_Privacy\\Components\\Setup' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-setup.php', + 'Avatar_Privacy\\Components\\Shortcodes' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-shortcodes.php', + 'Avatar_Privacy\\Components\\Uninstallation' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-uninstallation.php', + 'Avatar_Privacy\\Components\\User_Profile' => __DIR__ . '/../..' . '/includes/avatar-privacy/components/class-user-profile.php', + 'Avatar_Privacy\\Controller' => __DIR__ . '/../..' . '/includes/avatar-privacy/class-controller.php', + 'Avatar_Privacy\\Core' => __DIR__ . '/../..' . '/includes/avatar-privacy/class-core.php', + 'Avatar_Privacy\\Core\\API' => __DIR__ . '/../..' . '/includes/avatar-privacy/core/class-api.php', + 'Avatar_Privacy\\Core\\Comment_Author_Fields' => __DIR__ . '/../..' . '/includes/avatar-privacy/core/class-comment-author-fields.php', + 'Avatar_Privacy\\Core\\Default_Avatars' => __DIR__ . '/../..' . '/includes/avatar-privacy/core/class-default-avatars.php', + 'Avatar_Privacy\\Core\\Settings' => __DIR__ . '/../..' . '/includes/avatar-privacy/core/class-settings.php', + 'Avatar_Privacy\\Core\\User_Fields' => __DIR__ . '/../..' . '/includes/avatar-privacy/core/class-user-fields.php', + 'Avatar_Privacy\\Data_Storage\\Cache' => __DIR__ . '/../..' . '/includes/avatar-privacy/data-storage/class-cache.php', + 'Avatar_Privacy\\Data_Storage\\Database\\Comment_Author_Table' => __DIR__ . '/../..' . '/includes/avatar-privacy/data-storage/database/class-comment-author-table.php', + 'Avatar_Privacy\\Data_Storage\\Database\\Hashes_Table' => __DIR__ . '/../..' . '/includes/avatar-privacy/data-storage/database/class-hashes-table.php', + 'Avatar_Privacy\\Data_Storage\\Database\\Table' => __DIR__ . '/../..' . '/includes/avatar-privacy/data-storage/database/class-table.php', + 'Avatar_Privacy\\Data_Storage\\Filesystem_Cache' => __DIR__ . '/../..' . '/includes/avatar-privacy/data-storage/class-filesystem-cache.php', + 'Avatar_Privacy\\Data_Storage\\Network_Options' => __DIR__ . '/../..' . '/includes/avatar-privacy/data-storage/class-network-options.php', + 'Avatar_Privacy\\Data_Storage\\Options' => __DIR__ . '/../..' . '/includes/avatar-privacy/data-storage/class-options.php', + 'Avatar_Privacy\\Data_Storage\\Site_Transients' => __DIR__ . '/../..' . '/includes/avatar-privacy/data-storage/class-site-transients.php', + 'Avatar_Privacy\\Data_Storage\\Transients' => __DIR__ . '/../..' . '/includes/avatar-privacy/data-storage/class-transients.php', + 'Avatar_Privacy\\Exceptions\\Avatar_Comment_Type_Exception' => __DIR__ . '/../..' . '/includes/avatar-privacy/exceptions/class-avatar-comment-type-exception.php', + 'Avatar_Privacy\\Exceptions\\Database_Exception' => __DIR__ . '/../..' . '/includes/avatar-privacy/exceptions/class-database-exception.php', + 'Avatar_Privacy\\Exceptions\\File_Deletion_Exception' => __DIR__ . '/../..' . '/includes/avatar-privacy/exceptions/class-file-deletion-exception.php', + 'Avatar_Privacy\\Exceptions\\Filesystem_Exception' => __DIR__ . '/../..' . '/includes/avatar-privacy/exceptions/class-filesystem-exception.php', + 'Avatar_Privacy\\Exceptions\\Form_Field_Not_Found_Exception' => __DIR__ . '/../..' . '/includes/avatar-privacy/exceptions/class-form-field-not-found-exception.php', + 'Avatar_Privacy\\Exceptions\\Invalid_Nonce_Exception' => __DIR__ . '/../..' . '/includes/avatar-privacy/exceptions/class-invalid-nonce-exception.php', + 'Avatar_Privacy\\Exceptions\\Object_Factory_Exception' => __DIR__ . '/../..' . '/includes/avatar-privacy/exceptions/class-object-factory-exception.php', + 'Avatar_Privacy\\Exceptions\\PNG_Image_Exception' => __DIR__ . '/../..' . '/includes/avatar-privacy/exceptions/class-png-image-exception.php', + 'Avatar_Privacy\\Exceptions\\Part_Files_Not_Found_Exception' => __DIR__ . '/../..' . '/includes/avatar-privacy/exceptions/class-part-files-not-found-exception.php', + 'Avatar_Privacy\\Exceptions\\Upload_Handling_Exception' => __DIR__ . '/../..' . '/includes/avatar-privacy/exceptions/class-upload-handling-exception.php', + 'Avatar_Privacy\\Factory' => __DIR__ . '/../..' . '/includes/avatar-privacy/class-factory.php', + 'Avatar_Privacy\\Integrations\\BBPress_Integration' => __DIR__ . '/../..' . '/includes/avatar-privacy/integrations/class-bbpress-integration.php', + 'Avatar_Privacy\\Integrations\\BuddyPress_Integration' => __DIR__ . '/../..' . '/includes/avatar-privacy/integrations/class-buddypress-integration.php', + 'Avatar_Privacy\\Integrations\\Plugin_Integration' => __DIR__ . '/../..' . '/includes/avatar-privacy/integrations/class-plugin-integration.php', + 'Avatar_Privacy\\Integrations\\Simple_Author_Box_Integration' => __DIR__ . '/../..' . '/includes/avatar-privacy/integrations/class-simple-author-box-integration.php', + 'Avatar_Privacy\\Integrations\\Simple_Local_Avatars_Integration' => __DIR__ . '/../..' . '/includes/avatar-privacy/integrations/class-simple-local-avatars-integration.php', + 'Avatar_Privacy\\Integrations\\Simple_User_Avatar_Integration' => __DIR__ . '/../..' . '/includes/avatar-privacy/integrations/class-simple-user-avatar-integration.php', + 'Avatar_Privacy\\Integrations\\Theme_My_Login_Profiles_Integration' => __DIR__ . '/../..' . '/includes/avatar-privacy/integrations/class-theme-my-login-profiles-integration.php', + 'Avatar_Privacy\\Integrations\\Ultimate_Member_Integration' => __DIR__ . '/../..' . '/includes/avatar-privacy/integrations/class-ultimate-member-integration.php', + 'Avatar_Privacy\\Integrations\\WPDiscuz_Integration' => __DIR__ . '/../..' . '/includes/avatar-privacy/integrations/class-wpdiscuz-integration.php', + 'Avatar_Privacy\\Integrations\\WP_User_Manager_Integration' => __DIR__ . '/../..' . '/includes/avatar-privacy/integrations/class-wp-user-manager-integration.php', + 'Avatar_Privacy\\Requirements' => __DIR__ . '/../..' . '/includes/avatar-privacy/class-requirements.php', + 'Avatar_Privacy\\Tools\\HTML\\Dependencies' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/html/class-dependencies.php', + 'Avatar_Privacy\\Tools\\HTML\\User_Form' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/html/class-user-form.php', + 'Avatar_Privacy\\Tools\\Hasher' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/class-hasher.php', + 'Avatar_Privacy\\Tools\\Images\\Color' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/images/class-color.php', + 'Avatar_Privacy\\Tools\\Images\\Editor' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/images/class-editor.php', + 'Avatar_Privacy\\Tools\\Images\\Image_File' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/images/class-image-file.php', + 'Avatar_Privacy\\Tools\\Images\\Image_Stream' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/images/class-image-stream.php', + 'Avatar_Privacy\\Tools\\Images\\PNG' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/images/class-png.php', + 'Avatar_Privacy\\Tools\\Images\\SVG' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/images/class-svg.php', + 'Avatar_Privacy\\Tools\\Images\\Type' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/images/class-type.php', + 'Avatar_Privacy\\Tools\\Multisite' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/class-multisite.php', + 'Avatar_Privacy\\Tools\\Network\\Gravatar_Service' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/network/class-gravatar-service.php', + 'Avatar_Privacy\\Tools\\Network\\Remote_Image_Service' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/network/class-remote-image-service.php', + 'Avatar_Privacy\\Tools\\Number_Generator' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/class-number-generator.php', + 'Avatar_Privacy\\Tools\\Template' => __DIR__ . '/../..' . '/includes/avatar-privacy/tools/class-template.php', + 'Avatar_Privacy\\Upload_Handlers\\Custom_Default_Icon_Upload_Handler' => __DIR__ . '/../..' . '/includes/avatar-privacy/upload-handlers/class-custom-default-icon-upload-handler.php', + 'Avatar_Privacy\\Upload_Handlers\\UI\\File_Upload_Input' => __DIR__ . '/../..' . '/includes/avatar-privacy/upload-handlers/ui/class-file-upload-input.php', + 'Avatar_Privacy\\Upload_Handlers\\Upload_Handler' => __DIR__ . '/../..' . '/includes/avatar-privacy/upload-handlers/class-upload-handler.php', + 'Avatar_Privacy\\Upload_Handlers\\User_Avatar_Upload_Handler' => __DIR__ . '/../..' . '/includes/avatar-privacy/upload-handlers/class-user-avatar-upload-handler.php', + 'Avatar_Privacy\\Vendor\\Colors\\RandomColor' => __DIR__ . '/..' . '/mistic100/randomcolor/src/RandomColor.php', + 'Avatar_Privacy\\Vendor\\Dice\\Dice' => __DIR__ . '/..' . '/level-2/dice/Dice.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Canvas' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Canvas.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\CanvasContext' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/CanvasContext.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\ColorUtils' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/ColorUtils.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Matrix' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Matrix.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Png\\PngBuffer' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Png/PngBuffer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Png\\PngEncoder' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Png/PngEncoder.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Png\\PngPalette' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Png/PngPalette.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Point' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Point.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\Edge' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Rasterization/Edge.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\EdgeIntersection' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Rasterization/EdgeIntersection.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\EdgeSuperSampleIntersection' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Rasterization/EdgeSuperSampleIntersection.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\EdgeTable' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Rasterization/EdgeTable.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\Layer' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Rasterization/Layer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\LayerManager' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Rasterization/LayerManager.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\Rasterizer' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Rasterization/Rasterizer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\SuperSampleBuffer' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Rasterization/SuperSampleBuffer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Canvas\\Rasterization\\SuperSampleRange' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Canvas/Rasterization/SuperSampleRange.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Color' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Color.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Identicon' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Identicon.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\IdenticonStyle' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/IdenticonStyle.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\AbstractRenderer' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Rendering/AbstractRenderer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\ColorTheme' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Rendering/ColorTheme.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\IconGenerator' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Rendering/IconGenerator.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\ImagickRenderer' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Rendering/ImagickRenderer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\InternalPngRenderer' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Rendering/InternalPngRenderer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\Point' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Rendering/Point.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\Rectangle' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Rendering/Rectangle.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\RendererInterface' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Rendering/RendererInterface.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\SvgPath' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Rendering/SvgPath.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\SvgRenderer' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Rendering/SvgRenderer.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\Transform' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Rendering/Transform.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Rendering\\TriangleDirection' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Rendering/TriangleDirection.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Shapes\\Shape' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Shapes/Shape.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Shapes\\ShapeCategory' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Shapes/ShapeCategory.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Shapes\\ShapeDefinitions' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Shapes/ShapeDefinitions.php', + 'Avatar_Privacy\\Vendor\\Jdenticon\\Shapes\\ShapePosition' => __DIR__ . '/..' . '/jdenticon/jdenticon/src/Shapes/ShapePosition.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\Data_Storage\\Abstract_Cache' => __DIR__ . '/..' . '/mundschenk-at/wp-data-storage/src/class-abstract-cache.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\Data_Storage\\Cache' => __DIR__ . '/..' . '/mundschenk-at/wp-data-storage/src/class-cache.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\Data_Storage\\Network_Options' => __DIR__ . '/..' . '/mundschenk-at/wp-data-storage/src/class-network-options.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\Data_Storage\\Options' => __DIR__ . '/..' . '/mundschenk-at/wp-data-storage/src/class-options.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\Data_Storage\\Site_Transients' => __DIR__ . '/..' . '/mundschenk-at/wp-data-storage/src/class-site-transients.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\Data_Storage\\Transients' => __DIR__ . '/..' . '/mundschenk-at/wp-data-storage/src/class-transients.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Abstract_Control' => __DIR__ . '/..' . '/mundschenk-at/wp-settings-ui/src/ui/class-abstract-control.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Control' => __DIR__ . '/..' . '/mundschenk-at/wp-settings-ui/src/ui/class-control.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Control_Factory' => __DIR__ . '/..' . '/mundschenk-at/wp-settings-ui/src/ui/class-control-factory.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Checkbox_Input' => __DIR__ . '/..' . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-checkbox-input.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Display_Text' => __DIR__ . '/..' . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-display-text.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Hidden_Input' => __DIR__ . '/..' . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-hidden-input.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Input' => __DIR__ . '/..' . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-input.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Number_Input' => __DIR__ . '/..' . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-number-input.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Select' => __DIR__ . '/..' . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-select.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Submit_Input' => __DIR__ . '/..' . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-submit-input.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Text_Input' => __DIR__ . '/..' . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-text-input.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\UI\\Controls\\Textarea' => __DIR__ . '/..' . '/mundschenk-at/wp-settings-ui/src/ui/controls/class-textarea.php', + 'Avatar_Privacy\\Vendor\\Mundschenk\\WP_Requirements' => __DIR__ . '/..' . '/mundschenk-at/check-wp-requirements/class-wp-requirements.php', + 'Avatar_Privacy\\Vendor\\splitbrain\\RingIcon\\AbstractRingIcon' => __DIR__ . '/..' . '/splitbrain/php-ringicon/src/AbstractRingIcon.php', + 'Avatar_Privacy\\Vendor\\splitbrain\\RingIcon\\RingIcon' => __DIR__ . '/..' . '/splitbrain/php-ringicon/src/RingIcon.php', + 'Avatar_Privacy\\Vendor\\splitbrain\\RingIcon\\RingIconSVG' => __DIR__ . '/..' . '/splitbrain/php-ringicon/src/RingIconSVG.php', + ); + + public static function getInitializer(ClassLoaderAvatarPrivacy $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitAvatarPrivacy::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitAvatarPrivacy::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInitAvatarPrivacy::$classMap; + + }, null, ClassLoaderAvatarPrivacy::class); + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/LICENSE b/vendor-scoped/jdenticon/jdenticon/LICENSE new file mode 100644 index 0000000..cc1d6da --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Daniel Mester Pirttijärvi + +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. diff --git a/vendor-scoped/jdenticon/jdenticon/README.md b/vendor-scoped/jdenticon/jdenticon/README.md new file mode 100644 index 0000000..4954e03 --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/README.md @@ -0,0 +1,60 @@ +# [Jdenticon-php](https://jdenticon.com) +PHP library for generating highly recognizable identicons. + +![Sample identicons](https://jdenticon.com/hosted/github-samples.png) + +[![Test Status](https://github.com/dmester/jdenticon-php/workflows/Tests/badge.svg)](https://github.com/dmester/jdenticon-php/actions) +[![Total Downloads](https://poser.pugx.org/jdenticon/jdenticon/downloads)](https://packagist.org/packages/jdenticon/jdenticon) + +## Features +Jdenticon-php is a PHP port of the JavaScript library [Jdenticon](https://github.com/dmester/jdenticon). + +* Renders identicons as PNG or SVG with no extension requirements. +* Runs on PHP 5.3 and later. + +## Live demo +https://jdenticon.com + +## Getting started +Using Jdenticon is simple. Follow the steps below to integrate Jdenticon into your website. + +### 1. Install the Jdenticon Composer package +The easiest way to get started using Jdenticon for PHP is to install the Jdenticon Composer package. + +``` +composer require jdenticon/jdenticon +``` + +### 2. Create a php file that will serve an icon +Now create a file that you call icon.php and place it in the root of your application. Add the following content to the file. + +```PHP +setValue($value); +$icon->setSize($size); +$icon->displayImage('png'); +``` + +### 3. Use icon.php +Open up your favourite browser and navigate to http://localhost:PORT/icon.php?size=100&value=anything. +An identicon should be displayed. Try to change the url parameters to see the difference in the generated icon. + +## Other resources +### API documentation +For more usage examples and API documentation, please see: + +https://jdenticon.com/php-api.html + +## License +Jdenticon-php is released under the [MIT license](https://github.com/dmester/jdenticon-php/blob/master/LICENSE). diff --git a/vendor-scoped/jdenticon/jdenticon/src/Color.php b/vendor-scoped/jdenticon/jdenticon/src/Color.php new file mode 100644 index 0000000..b8369a3 --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Color.php @@ -0,0 +1,673 @@ +r = $red; + $color->g = $green; + $color->b = $blue; + $color->a = $alpha; + return $color; + } + /** + * Creates a Color instance from HSL color parameters. + * + * @param float $hue Hue in the range [0, 1] + * @param float $saturation Saturation in the range [0, 1] + * @param float $lightness Lightness in the range [0, 1] + * @param float $alpha Alpha channel value in the range [0, 1]. + */ + public static function fromHsl($hue, $saturation, $lightness, $alpha = 1.0) + { + if ($hue < 0) { + $hue = 0; + } + if ($hue > 1) { + $hue = 1; + } + if ($saturation < 0) { + $saturation = 0; + } + if ($saturation > 1) { + $saturation = 1; + } + if ($lightness < 0) { + $lightness = 0; + } + if ($lightness > 1) { + $lightness = 1; + } + if ($alpha < 0) { + $alpha = 0; + } + if ($alpha > 1) { + $alpha = 1; + } + // Based on http://www.w3.org/TR/2011/REC-css3-color-20110607/#hsl-color + if ($saturation == 0) { + $value = (int) ($lightness * 255); + return self::fromRgb($value, $value, $value, (int) ($alpha * 255)); + } else { + if ($lightness <= 0.5) { + $m2 = $lightness * ($saturation + 1); + } else { + $m2 = $lightness + $saturation - $lightness * $saturation; + } + $m1 = $lightness * 2 - $m2; + return self::fromRgb(self::hueToRgb($m1, $m2, $hue * 6 + 2), self::hueToRgb($m1, $m2, $hue * 6), self::hueToRgb($m1, $m2, $hue * 6 - 2), (int) ($alpha * 255)); + } + } + /** + * Creates a Color> instance from HSL color parameters and will compensate + * the lightness for hues that appear to be darker than others. + * + * @param float $hue Hue in the range [0, 1]. + * @param float $saturation Saturation in the range [0, 1]. + * @param float $lightness Lightness in the range [0, 1]. + * @param float $alpha Alpha channel value in the range [0, 1]. + */ + public static function fromHslCompensated($hue, $saturation, $lightness, $alpha = 1.0) + { + if ($hue < 0) { + $hue = 0; + } + if ($hue > 1) { + $hue = 1; + } + $lightnessCompensation = self::$lightnessCompensations[(int) ($hue * 6 + 0.5)]; + // Adjust the input lightness relative to the compensation + $lightness = $lightness < 0.5 ? $lightness * $lightnessCompensation * 2 : $lightnessCompensation + ($lightness - 0.5) * (1 - $lightnessCompensation) * 2; + return self::fromHsl($hue, $saturation, $lightness, $alpha); + } + // Helper method for FromHsl + private static function hueToRgb($m1, $m2, $h) + { + if ($h < 0) { + $h = $h + 6; + } elseif ($h > 6) { + $h = $h - 6; + } + if ($h < 1) { + $r = $m1 + ($m2 - $m1) * $h; + } elseif ($h < 3) { + $r = $m2; + } elseif ($h < 4) { + $r = $m1 + ($m2 - $m1) * (4 - $h); + } else { + $r = $m1; + } + return (int) (255 * $r); + } + /** + * Gets the argb value of this color. + * + * @return int + */ + public function toRgba() + { + return $this->r << 24 | $this->g << 16 | $this->b << 8 | $this->a; + } + /** + * Gets a hexadecimal representation of this color on the format #rrggbbaa. + * + * @return string + */ + public function __toString() + { + return '#' . \bin2hex(\pack('N', $this->toRgba())); + } + /** + * Gets a hexadecimal representation of this color on the format #rrggbbaa. + * + * @return string + */ + public function toHexString($length = 8) + { + if ($length === 8) { + return $this->__toString(); + } + return '#' . \substr(\bin2hex(\pack('N', $this->toRgba())), 0, 6); + } + /** + * Tries to parse a value as a Color. + * + * @param mixed $value Value to parse. + * @throws InvalidArgumentException + * @return \Jdenticon\Color + */ + public static function parse($value) + { + if ($value instanceof Color) { + return $value; + } + $value = \strtolower("{$value}"); + if (\preg_match('/^#?[0-9a-f]{3,8}$/', $value) && self::parseHexColor($value, $result)) { + return $result; + } + if (\preg_match('/^rgba?\\(([^,]+),([^,]+),([^,]+)(?:,([^,]+))?\\)$/', $value, $matches) && self::parseRgbComponent($matches[1], $r) && self::parseRgbComponent($matches[2], $g) && self::parseRgbComponent($matches[3], $b) && self::parseAlpha(isset($matches[4]) ? $matches[4] : null, $a)) { + return self::fromRgb($r, $g, $b, (int) (255 * $a)); + } + if (\preg_match('/^hsla?\\(([^,]+),([^,]+),([^,]+)(?:,([^,]+))?\\)$/', $value, $matches) && self::parseHue($matches[1], $h) && self::parsePercent($matches[2], $s) && self::parsePercent($matches[3], $l) && self::parseAlpha(isset($matches[4]) ? $matches[4] : null, $a)) { + return self::fromHsl($h, $s, $l, $a); + } + $result = self::parseNamedColor($value); + if ($result !== null) { + return $result; + } + throw new \InvalidArgumentException("Cannot parse '{$value}' as a color."); + } + /** + * Parses a percent value. + * + * @param string $input Input string. + * @param float $result Resulting value in range [0, 1]. + * + * @return boolean + */ + private static function parsePercent($input, &$result) + { + // Detect and remove percent sign + if (\preg_match('/^\\s*(\\d*(?:\\.\\d*)?)%\\s*$/', $input, $matches)) { + $result = \floatval($matches[1]) / 100; + if ($result < 0) { + $result = 0; + } + if ($result > 1) { + $result = 1; + } + return \true; + } + return \false; + } + /** + * Parses an alpha value. + * + * @param string $input Input string. + * @param float $result Resulting alpha in range [0, 1]. + * + * @return boolean + */ + private static function parseAlpha($input, &$result) + { + if ($input === null || $input === '') { + $result = 1; + return \true; + } + if (\preg_match('/^\\s*(\\d*(?:\\.\\d*)?)(%?)\\s*$/', $input, $matches)) { + $result = \floatval($matches[1]); + // Percentage + if ($matches[2] !== '') { + $result = $result / 100; + } + if ($result < 0) { + $result = 0; + } + if ($result > 1) { + $result = 1; + } + return \true; + } + return \false; + } + /** + * Parses an RGB component. + * + * @param string $input Input string. + * @param float $result Hue in range [0, 255]. + * + * @return boolean + */ + private static function parseRgbComponent($input, &$result) + { + if (\preg_match('/^\\s*(\\d*(?:\\.\\d*)?)(%?)\\s*$/', $input, $matches)) { + $result = \floatval($matches[1]); + if ($matches[2] === '%') { + $result = 255 * $result / 100; + } + $result = (int) $result; + if ($result < 0) { + $result = 0; + } + if ($result > 255) { + $result = 255; + } + return \true; + } + return \false; + } + /** + * Parses a hue component. + * + * @param string $input Input string. + * @param float $result Hue in range [0, 1]. + * + * @return boolean + */ + private static function parseHue($input, &$result) + { + if (\preg_match('/^\\s*(\\d*(?:\\.\\d*)?)(deg|grad|rad|turn|)\\s*$/', $input, $matches)) { + $result = \floatval($matches[1]); + // Percentage + switch ($matches[2]) { + case "grad": + // Gradians: range 0 - 400 + $result = $result / 400; + break; + case "rad": + // Radians: range 0 - 2pi + $result = $result / \M_PI / 2; + break; + case "turn": + // Turns: range 0 - 1 + $result = $result; + break; + default: + // Degree: range 0 - 360 + $result = $result / 360; + break; + } + $result = \fmod($result, 1); + if ($result < 0) { + $result += 1; + } + return \true; + } + return \false; + } + /** + * Parses a hex color string. + * + * @param string $input Input string. + * @param float $result Hue in range [0, 1]. + * + * @return boolean + */ + private static function parseHexColor($input, &$result) + { + if ($input[0] === '#') { + $input = \substr($input, 1); + } + // intval does not support unsigned 32-bit integers + // so we need to parse large numbers stepwise + $numeric24bit = \intval(\substr($input, 0, 6), 16); + $alpha8bit = \intval(\substr($input, 6, 2), 16); + switch (\strlen($input)) { + case 3: + $result = self::fromRgb(($numeric24bit & 0xf00) >> 8 | ($numeric24bit & 0xf00) >> 4, ($numeric24bit & 0xf0) >> 4 | $numeric24bit & 0xf0, ($numeric24bit & 0xf) << 4 | $numeric24bit & 0xf); + return \true; + case 4: + $result = self::fromRgb(($numeric24bit & 0xf000) >> 12 | ($numeric24bit & 0xf000) >> 8, ($numeric24bit & 0xf00) >> 8 | ($numeric24bit & 0xf00) >> 4, ($numeric24bit & 0xf0) >> 4 | $numeric24bit & 0xf0, ($numeric24bit & 0xf) << 4 | $numeric24bit & 0xf); + return \true; + case 6: + $result = self::fromRgb(0xff & $numeric24bit >> 16, 0xff & $numeric24bit >> 8, 0xff & $numeric24bit); + return \true; + case 8: + $result = self::fromRgb(0xff & $numeric24bit >> 16, 0xff & $numeric24bit >> 8, 0xff & $numeric24bit, 0xff & $alpha8bit); + return \true; + } + return \false; + } + /** + * Looks up a named color to a Color instance. + * + * @param string $input Input string. + * + * @return \Jdenticon\Color + */ + private static function parseNamedColor($input) + { + // Source: https://www.w3.org/TR/css-color-4/#named-colors + switch ($input) { + case 'aliceblue': + return self::fromRgb(240, 248, 255); + case 'antiquewhite': + return self::fromRgb(250, 235, 215); + case 'aqua': + return self::fromRgb(0, 255, 255); + case 'aquamarine': + return self::fromRgb(127, 255, 212); + case 'azure': + return self::fromRgb(240, 255, 255); + case 'beige': + return self::fromRgb(245, 245, 220); + case 'bisque': + return self::fromRgb(255, 228, 196); + case 'black': + return self::fromRgb(0, 0, 0); + case 'blanchedalmond': + return self::fromRgb(255, 235, 205); + case 'blue': + return self::fromRgb(0, 0, 255); + case 'blueviolet': + return self::fromRgb(138, 43, 226); + case 'brown': + return self::fromRgb(165, 42, 42); + case 'burlywood': + return self::fromRgb(222, 184, 135); + case 'cadetblue': + return self::fromRgb(95, 158, 160); + case 'chartreuse': + return self::fromRgb(127, 255, 0); + case 'chocolate': + return self::fromRgb(210, 105, 30); + case 'coral': + return self::fromRgb(255, 127, 80); + case 'cornflowerblue': + return self::fromRgb(100, 149, 237); + case 'cornsilk': + return self::fromRgb(255, 248, 220); + case 'crimson': + return self::fromRgb(220, 20, 60); + case 'cyan': + return self::fromRgb(0, 255, 255); + case 'darkblue': + return self::fromRgb(0, 0, 139); + case 'darkcyan': + return self::fromRgb(0, 139, 139); + case 'darkgoldenrod': + return self::fromRgb(184, 134, 11); + case 'darkgray': + return self::fromRgb(169, 169, 169); + case 'darkgreen': + return self::fromRgb(0, 100, 0); + case 'darkgrey': + return self::fromRgb(169, 169, 169); + case 'darkkhaki': + return self::fromRgb(189, 183, 107); + case 'darkmagenta': + return self::fromRgb(139, 0, 139); + case 'darkolivegreen': + return self::fromRgb(85, 107, 47); + case 'darkorange': + return self::fromRgb(255, 140, 0); + case 'darkorchid': + return self::fromRgb(153, 50, 204); + case 'darkred': + return self::fromRgb(139, 0, 0); + case 'darksalmon': + return self::fromRgb(233, 150, 122); + case 'darkseagreen': + return self::fromRgb(143, 188, 143); + case 'darkslateblue': + return self::fromRgb(72, 61, 139); + case 'darkslategray': + return self::fromRgb(47, 79, 79); + case 'darkslategrey': + return self::fromRgb(47, 79, 79); + case 'darkturquoise': + return self::fromRgb(0, 206, 209); + case 'darkviolet': + return self::fromRgb(148, 0, 211); + case 'deeppink': + return self::fromRgb(255, 20, 147); + case 'deepskyblue': + return self::fromRgb(0, 191, 255); + case 'dimgray': + return self::fromRgb(105, 105, 105); + case 'dimgrey': + return self::fromRgb(105, 105, 105); + case 'dodgerblue': + return self::fromRgb(30, 144, 255); + case 'firebrick': + return self::fromRgb(178, 34, 34); + case 'floralwhite': + return self::fromRgb(255, 250, 240); + case 'forestgreen': + return self::fromRgb(34, 139, 34); + case 'fuchsia': + return self::fromRgb(255, 0, 255); + case 'gainsboro': + return self::fromRgb(220, 220, 220); + case 'ghostwhite': + return self::fromRgb(248, 248, 255); + case 'gold': + return self::fromRgb(255, 215, 0); + case 'goldenrod': + return self::fromRgb(218, 165, 32); + case 'gray': + return self::fromRgb(128, 128, 128); + case 'green': + return self::fromRgb(0, 128, 0); + case 'greenyellow': + return self::fromRgb(173, 255, 47); + case 'grey': + return self::fromRgb(128, 128, 128); + case 'honeydew': + return self::fromRgb(240, 255, 240); + case 'hotpink': + return self::fromRgb(255, 105, 180); + case 'indianred': + return self::fromRgb(205, 92, 92); + case 'indigo': + return self::fromRgb(75, 0, 130); + case 'ivory': + return self::fromRgb(255, 255, 240); + case 'khaki': + return self::fromRgb(240, 230, 140); + case 'lavender': + return self::fromRgb(230, 230, 250); + case 'lavenderblush': + return self::fromRgb(255, 240, 245); + case 'lawngreen': + return self::fromRgb(124, 252, 0); + case 'lemonchiffon': + return self::fromRgb(255, 250, 205); + case 'lightblue': + return self::fromRgb(173, 216, 230); + case 'lightcoral': + return self::fromRgb(240, 128, 128); + case 'lightcyan': + return self::fromRgb(224, 255, 255); + case 'lightgoldenrodyellow': + return self::fromRgb(250, 250, 210); + case 'lightgray': + return self::fromRgb(211, 211, 211); + case 'lightgreen': + return self::fromRgb(144, 238, 144); + case 'lightgrey': + return self::fromRgb(211, 211, 211); + case 'lightpink': + return self::fromRgb(255, 182, 193); + case 'lightsalmon': + return self::fromRgb(255, 160, 122); + case 'lightseagreen': + return self::fromRgb(32, 178, 170); + case 'lightskyblue': + return self::fromRgb(135, 206, 250); + case 'lightslategray': + return self::fromRgb(119, 136, 153); + case 'lightslategrey': + return self::fromRgb(119, 136, 153); + case 'lightsteelblue': + return self::fromRgb(176, 196, 222); + case 'lightyellow': + return self::fromRgb(255, 255, 224); + case 'lime': + return self::fromRgb(0, 255, 0); + case 'limegreen': + return self::fromRgb(50, 205, 50); + case 'linen': + return self::fromRgb(250, 240, 230); + case 'magenta': + return self::fromRgb(255, 0, 255); + case 'maroon': + return self::fromRgb(128, 0, 0); + case 'mediumaquamarine': + return self::fromRgb(102, 205, 170); + case 'mediumblue': + return self::fromRgb(0, 0, 205); + case 'mediumorchid': + return self::fromRgb(186, 85, 211); + case 'mediumpurple': + return self::fromRgb(147, 112, 219); + case 'mediumseagreen': + return self::fromRgb(60, 179, 113); + case 'mediumslateblue': + return self::fromRgb(123, 104, 238); + case 'mediumspringgreen': + return self::fromRgb(0, 250, 154); + case 'mediumturquoise': + return self::fromRgb(72, 209, 204); + case 'mediumvioletred': + return self::fromRgb(199, 21, 133); + case 'midnightblue': + return self::fromRgb(25, 25, 112); + case 'mintcream': + return self::fromRgb(245, 255, 250); + case 'mistyrose': + return self::fromRgb(255, 228, 225); + case 'moccasin': + return self::fromRgb(255, 228, 181); + case 'navajowhite': + return self::fromRgb(255, 222, 173); + case 'navy': + return self::fromRgb(0, 0, 128); + case 'oldlace': + return self::fromRgb(253, 245, 230); + case 'olive': + return self::fromRgb(128, 128, 0); + case 'olivedrab': + return self::fromRgb(107, 142, 35); + case 'orange': + return self::fromRgb(255, 165, 0); + case 'orangered': + return self::fromRgb(255, 69, 0); + case 'orchid': + return self::fromRgb(218, 112, 214); + case 'palegoldenrod': + return self::fromRgb(238, 232, 170); + case 'palegreen': + return self::fromRgb(152, 251, 152); + case 'paleturquoise': + return self::fromRgb(175, 238, 238); + case 'palevioletred': + return self::fromRgb(219, 112, 147); + case 'papayawhip': + return self::fromRgb(255, 239, 213); + case 'peachpuff': + return self::fromRgb(255, 218, 185); + case 'peru': + return self::fromRgb(205, 133, 63); + case 'pink': + return self::fromRgb(255, 192, 203); + case 'plum': + return self::fromRgb(221, 160, 221); + case 'powderblue': + return self::fromRgb(176, 224, 230); + case 'purple': + return self::fromRgb(128, 0, 128); + case 'rebeccapurple': + return self::fromRgb(102, 51, 153); + case 'red': + return self::fromRgb(255, 0, 0); + case 'rosybrown': + return self::fromRgb(188, 143, 143); + case 'royalblue': + return self::fromRgb(65, 105, 225); + case 'saddlebrown': + return self::fromRgb(139, 69, 19); + case 'salmon': + return self::fromRgb(250, 128, 114); + case 'sandybrown': + return self::fromRgb(244, 164, 96); + case 'seagreen': + return self::fromRgb(46, 139, 87); + case 'seashell': + return self::fromRgb(255, 245, 238); + case 'sienna': + return self::fromRgb(160, 82, 45); + case 'silver': + return self::fromRgb(192, 192, 192); + case 'skyblue': + return self::fromRgb(135, 206, 235); + case 'slateblue': + return self::fromRgb(106, 90, 205); + case 'slategray': + return self::fromRgb(112, 128, 144); + case 'slategrey': + return self::fromRgb(112, 128, 144); + case 'snow': + return self::fromRgb(255, 250, 250); + case 'springgreen': + return self::fromRgb(0, 255, 127); + case 'steelblue': + return self::fromRgb(70, 130, 180); + case 'tan': + return self::fromRgb(210, 180, 140); + case 'teal': + return self::fromRgb(0, 128, 128); + case 'thistle': + return self::fromRgb(216, 191, 216); + case 'tomato': + return self::fromRgb(255, 99, 71); + case 'transparent': + return self::fromRgb(0, 0, 0, 0); + case 'turquoise': + return self::fromRgb(64, 224, 208); + case 'violet': + return self::fromRgb(238, 130, 238); + case 'wheat': + return self::fromRgb(245, 222, 179); + case 'white': + return self::fromRgb(255, 255, 255); + case 'whitesmoke': + return self::fromRgb(245, 245, 245); + case 'yellow': + return self::fromRgb(255, 255, 0); + case 'yellowgreen': + return self::fromRgb(154, 205, 50); + default: + return null; + } + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Identicon.php b/vendor-scoped/jdenticon/jdenticon/src/Identicon.php new file mode 100644 index 0000000..8116cf3 --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Identicon.php @@ -0,0 +1,421 @@ +iconGenerator = IconGenerator::getDefaultGenerator(); + if ($options !== null) { + $this->setOptions($options); + } + if ($this->style === null) { + $this->style = new IdenticonStyle(); + } + } + /** + * Creates an Identicon instance from a specified hash. + * + * @param string $hash A binary string containing the hash that will be used + * as base for this icon. The hash must contain at least 6 bytes. + * @param int $size The size of the icon in pixels (the icon is quadratic). + * @return \Jdenticon\Identicon + */ + public static function fromHash($hash, $size) + { + return new Identicon(array('hash' => $hash, 'size' => $size)); + } + /** + * Creates an Identicon instance from a specified value. + * + * @param mixed $value The value that will be used as base for this icon. + * The value will be converted to a UTF8 encoded string and then hashed + * using SHA1. + * @param int $size The size of the icon in pixels (the icon is quadratic). + * @return \Jdenticon\Identicon + */ + public static function fromValue($value, $size) + { + return new Identicon(array('value' => $value, 'size' => $size)); + } + /** + * Gets an associative array of all options of this identicon. + * + * @return array + */ + public function getOptions() + { + $options = array(); + if ($this->valueSet) { + $options['value'] = $this->getValue(); + } elseif ($this->hash !== null) { + $options['hash'] = $this->getHash(); + } + $options['size'] = $this->getSize(); + $options['style'] = $this->getStyle()->getOptions(); + if ($this->enableImageMagick !== null) { + $options['enableImageMagick'] = $this->getEnableImageMagick(); + } + if ($this->iconGenerator !== IconGenerator::getDefaultGenerator()) { + $options['iconGenerator'] = $this->getIconGenerator(); + } + return $options; + } + /** + * Sets options in this identicon by specifying an associative array of + * option values. + * + * @param array $options Options to set. + * @return self + */ + public function setOptions(array $options) + { + foreach ($options as $key => $value) { + $this->__set($key, $value); + } + return $this; + } + public function __get($name) + { + switch (\strtolower($name)) { + case 'size': + return $this->getSize(); + case 'hash': + return $this->getHash(); + case 'value': + return $this->getValue(); + case 'style': + return $this->getStyle(); + case 'icongenerator': + return $this->getIconGenerator(); + case 'enableimagemagick': + return $this->getEnableImageMagick(); + default: + throw new \InvalidArgumentException("Unknown Identicon option '{$name}'."); + } + } + public function __set($name, $value) + { + switch (\strtolower($name)) { + case 'size': + $this->setSize($value); + break; + case 'hash': + $this->setHash($value); + break; + case 'value': + $this->setValue($value); + break; + case 'style': + $this->setStyle($value); + break; + case 'icongenerator': + $this->setIconGenerator($value); + break; + case 'enableimagemagick': + $this->setEnableImageMagick($value); + break; + default: + throw new \InvalidArgumentException("Unknown Identicon option '{$name}'."); + } + } + /** + * Gets the size of the icon in pixels. + */ + public function getSize() + { + return $this->size; + } + /** + * Sets the size of this icon in pixels. + * + * @param int|float|double $size The width and height of the icon. + */ + public function setSize($size) + { + if (!\is_numeric($size) || $size < 1) { + throw new \InvalidArgumentException("An invalid identicon size was specified. " . "A numeric value >= 1 was expected. Specified value: {$size}."); + } + $this->size = (int) $size; + } + /** + * Gets the size of the icon in pixels. + */ + public function getEnableImageMagick() + { + // Enable ImageMagick on PHP < 7. On PHP 7 the performance increase + // is not as obvious as on PHP 5. Since the ImageMagick renderer has a + // lot of quirks, we don't want to use it unless really needed. + if ($this->enableImageMagick === null) { + return \PHP_MAJOR_VERSION < 7 && \extension_loaded('imagick'); + } + return $this->enableImageMagick; + } + /** + * Sets whether ImageMagick should be used to generate PNG icons. + * + * @param bool $enable true to enable ImageMagick. + */ + public function setEnableImageMagick($enable) + { + if (!\is_bool($enable)) { + throw new \InvalidArgumentException("enableImageMagick can only assume boolean values. Specified value: {$enable}."); + } + // Verify that the Imagick extension is installed + if ($enable && !\extension_loaded('imagick')) { + throw new \Exception('Failed to enable ImageMagick. ' . 'The Imagick PHP extension was not found on this system.'); + } + $this->enableImageMagick = $enable; + } + /** + * Gets the {@see IconGenerator} used to generate icons. + * + * @return \Jdenticon\Rendering\IconGenerator + */ + public function getIconGenerator() + { + return $this->iconGenerator; + } + /** + * Sets the {@see IconGenerator} used to generate icons. + * + * @param \Jdenticon\Rendering\IconGenerator $iconGenerator Icon generator + * that will render the shapes of the identicon. + * @return \Jdenticon\Identicon + */ + public function setIconGenerator(IconGenerator $iconGenerator) + { + if ($iconGenerator === null) { + $iconGenerator = IconGenerator::getDefaultGenerator(); + } + $this->iconGenerator = $iconGenerator; + return $this; + } + /** + * Gets or sets the style of the icon. + * + * @return \Jdenticon\IdenticonStyle + */ + public function getStyle() + { + return $this->style; + } + /** + * Gets or sets the style of the icon. + * + * @param array|\Jdenticon\IdenticonStyle $style The new style of the icon. + * NULL will revert the identicon to use the default style. + * @return self + */ + public function setStyle($style) + { + if ($style == null) { + $this->style = new IdenticonStyle(); + } elseif ($style instanceof IdenticonStyle) { + $this->style = $style; + } elseif (\is_array($style)) { + $this->style = new IdenticonStyle($style); + } else { + throw new \InvalidArgumentException("Invalid indenticon style was specified. " . "Allowed values are IdenticonStyle instances and associative " . "arrays containing IdenticonStyle options."); + } + return $this; + } + /** + * Gets a binary string containing the hash that is used as base for this + * icon. + */ + public function getHash() + { + return $this->hash; + } + /** + * Sets a binary string containing the hash that is used as base for this + * icon. The string should contain at least 6 bytes. + * + * @param string $hash Binary string containing the hash. + */ + public function setHash($hash) + { + if (!\is_string($hash)) { + throw new \InvalidArgumentException('An invalid $hash was passed to Identicon. ' . 'A binary string was expected.'); + } + if (\strlen($hash) < 6) { + throw new \InvalidArgumentException('An invalid $hash was passed to Identicon. ' . 'The hash was expected to contain at least 6 bytes.'); + } + $this->hash = $hash; + $this->value = null; + $this->valueSet = \false; + return $this; + } + /** + * Gets a binary string containing the hash that is used as base for this + * icon. + */ + public function getValue() + { + return $this->value; + } + /** + * Sets a value that will be used as base for this icon. The value will + * be converted to a string and then hashed using SHA1. + * + * @param mixed $value Value that will be hashed. + */ + public function setValue($value) + { + $this->hash = \sha1("{$value}"); + $this->value = $value; + $this->valueSet = \true; + return $this; + } + /** + * Gets the bounds of the icon excluding its padding. + * + * @return \Jdenticon\Rendering\Rectangle + */ + public function getIconBounds() + { + // Round padding to nearest integer + $padding = (int) ($this->style->getPadding() * $this->size + 0.5); + return new Rectangle($padding, $padding, $this->size - $padding * 2, $this->size - $padding * 2); + } + private function getRenderer($imageFormat) + { + switch (\strtolower($imageFormat)) { + case 'svg': + return new SvgRenderer($this->size, $this->size); + default: + return $this->getEnableImageMagick() ? new ImagickRenderer($this->size, $this->size) : new InternalPngRenderer($this->size, $this->size); + } + } + /** + * Draws this icon using a specified renderer. + * + * This method is only intended for usage with custom renderers. A custom + * renderer could as an example render an Identicon in a file format not + * natively supported by Jdenticon. To implement a new file format, + * implement {@see \Jdenticon\Rendering\RendererInterface}. + * + * @param \Jdenticon\Rendering\RendererInterface $renderer The renderer used + * to render this icon. + * @param \Jdenticon\Rendering\Rectangle $rect The bounds of the rendered + * icon. No padding will be applied to the rectangle. If the parameter + * is omitted, the rectangle is calculated from the current icon + * size and padding. + */ + public function draw(\Avatar_Privacy\Vendor\Jdenticon\Rendering\RendererInterface $renderer, \Avatar_Privacy\Vendor\Jdenticon\Rendering\Rectangle $rect = null) + { + if ($rect === null) { + $rect = $this->getIconBounds(); + } + $this->iconGenerator->generate($renderer, $rect, $this->style, $this->hash); + } + /** + * Renders the icon directly to the page output. + * + * The method will set the 'Content-Type' HTTP header. You are recommended + * to set an appropriate 'Cache-Control' header before calling this method + * to ensure the icon is cached client side. + * + * @param string $imageFormat The image format of the output. + * Supported values are 'png' and 'svg'. + */ + public function displayImage($imageFormat = 'png') + { + $renderer = $this->getRenderer($imageFormat); + $this->draw($renderer, $this->getIconBounds()); + $mimeType = $renderer->getMimeType(); + $data = $renderer->getData(); + \header("Content-Type: {$mimeType}"); + echo $data; + } + /** + * Renders the icon to a binary string. + * + * @param string $imageFormat The image format of the output string. + * Supported values are 'png' and 'svg'. + * @return string + */ + public function getImageData($imageFormat = 'png') + { + $renderer = $this->getRenderer($imageFormat); + $this->draw($renderer, $this->getIconBounds()); + return $renderer->getData(); + } + /** + * Renders the icon as a data URI. It is recommended to avoid using this + * method unless really necessary, since it will effectively disable client + * caching of generated icons, and will also cause the same icon to be + * rendered multiple times, when used multiple times on a single page. + * + * @param string $imageFormat The image format of the data URI. + * Supported values are 'png' and 'svg'. + * @return string + */ + public function getImageDataUri($imageFormat = 'png') + { + $renderer = $this->getRenderer($imageFormat); + $this->draw($renderer, $this->getIconBounds()); + $mimeType = $renderer->getMimeType(); + $base64 = \base64_encode($renderer->getData()); + return "data:{$mimeType};base64,{$base64}"; + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/IdenticonStyle.php b/vendor-scoped/jdenticon/jdenticon/src/IdenticonStyle.php new file mode 100644 index 0000000..781cba5 --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/IdenticonStyle.php @@ -0,0 +1,384 @@ +backgroundColor = self::getDefaultBackgroundColor(); + $this->padding = self::getDefaultPadding(); + $this->colorSaturation = self::getDefaultColorSaturation(); + $this->grayscaleSaturation = self::getDefaultGrayscaleSaturation(); + $this->colorLightness = self::getDefaultColorLightness(); + $this->grayscaleLightness = self::getDefaultGrayscaleLightness(); + if ($options !== null) { + $this->setOptions($options); + } + } + /** + * Gets an associative array of all options of this style. + * + * @return array + */ + public function getOptions() + { + $options = array(); + $options['backgroundColor'] = $this->getBackgroundColor()->__toString(); + $options['padding'] = $this->getPadding(); + $options['colorSaturation'] = $this->getColorSaturation(); + $options['grayscaleSaturation'] = $this->getGrayscaleSaturation(); + $options['colorLightness'] = $this->getColorLightness(); + $options['grayscaleLightness'] = $this->getGrayscaleLightness(); + if ($this->hues !== null) { + $options['hues'] = $this->getHues(); + } + return $options; + } + /** + * Sets options in this style by specifying an associative array of option + * values. + * + * @param array $options Options to set. + * @return self + */ + public function setOptions(array $options) + { + foreach ($options as $key => $value) { + $this->__set($key, $value); + } + return $this; + } + public function __get($name) + { + switch (\strtolower($name)) { + case 'backgroundcolor': + return $this->getBackgroundColor(); + case 'padding': + return $this->getPadding(); + case 'colorsaturation': + return $this->getColorSaturation(); + case 'grayscalesaturation': + return $this->getGrayscaleSaturation(); + case 'colorlightness': + return $this->getColorLightness(); + case 'grayscalelightness': + return $this->getGrayscaleLightness(); + case 'hues': + return $this->getHues(); + default: + throw new \InvalidArgumentException("Unknown IdenticonStyle option '{$name}'."); + } + } + public function __set($name, $value) + { + switch (\strtolower($name)) { + case 'backgroundcolor': + $this->setBackgroundColor($value); + break; + case 'padding': + $this->setPadding($value); + break; + case 'colorsaturation': + $this->setColorSaturation($value); + break; + case 'grayscalesaturation': + $this->setGrayscaleSaturation($value); + break; + case 'colorlightness': + $this->setColorLightness($value); + break; + case 'grayscalelightness': + $this->setGrayscaleLightness($value); + break; + case 'hues': + $this->setHues($value); + break; + default: + throw new \InvalidArgumentException("Unknown IdenticonStyle option '{$name}'."); + } + } + /** + * Normalizes a hue to the first turn [0, 360). + * + * @param mixed $hue + * @return integer + */ + private static function normalizeHue($hue) + { + if (!\is_numeric($hue)) { + throw new \InvalidArgumentException("'{$hue}' is not a valid hue."); + } + $hue = $hue % 360; + if ($hue < 0) { + $hue += 360; + } + return $hue; + } + /** + * Gets an array of allowed hues, or null if there are no restrictions. + * + * @return array(int)|null + */ + public function getHues() + { + return $this->hues; + } + /** + * Sets the allowed hues of generated icons. + * + * @param array(integer)|integer|null $value A hue specified in degrees, + * or an array of hues specified in degrees. If set to null, the hue + * list is cleared. + * @return self + */ + public function setHues($value) + { + $hues = array(); + if ($value !== null) { + if (\is_array($value)) { + foreach ($value as $hue) { + $hues[] = self::normalizeHue($hue); + } + } else { + $hues[] = self::normalizeHue($value); + } + } + $this->hues = empty($hues) ? null : $hues; + return $this; + } + /** + * Gets the padding of an icon in percents in the range [0.0, 0.4]. + * + * @return float + */ + public function getPadding() + { + return $this->padding; + } + /** + * Sets the padding of an icon in percents. + * + * @param float $value New padding in the range [0.0, 0.4]. + * @return self + */ + public function setPadding($value) + { + if (!\is_numeric($value) || $value < 0 || $value > 0.4) { + throw new \InvalidArgumentException("Padding '{$value}' out of range. " . "Values in the range [0.0, 0.4] are allowed."); + } + $this->padding = (float) $value; + return $this; + } + /** + * Gets the color of the identicon background. + * + * @return \Jdenticon\Color + */ + public function getBackgroundColor() + { + return $this->backgroundColor; + } + /** + * Sets the color of the identicon background. + * + * @param \Jdenticon\Color|string $value New background color. + * @return \Jdenticon\IdenticonStyle + */ + public function setBackgroundColor($value) + { + if ($value instanceof Color) { + $this->backgroundColor = $value; + } else { + $this->backgroundColor = Color::parse($value); + } + return $this; + } + /** + * Gets the saturation of the originally grayscale identicon shapes. + * + * @return float Saturation in the range [0.0, 1.0]. + */ + public function getGrayscaleSaturation() + { + return $this->grayscaleSaturation; + } + /** + * Sets the saturation of the originally grayscale identicon shapes. + * + * @param $value float Saturation in the range [0.0, 1.0]. + * @return self + */ + public function setGrayscaleSaturation($value) + { + if (!\is_numeric($value) || $value < 0 || $value > 1) { + throw new \InvalidArgumentException("The grayscale saturation was invalid. " . "Only values in the range [0.0, 1.0] are allowed."); + } + $this->grayscaleSaturation = (float) $value; + return $this; + } + /** + * Gets the saturation of the colored identicon shapes. + * + * @return float Saturation in the range [0.0, 1.0]. + */ + public function getColorSaturation() + { + return $this->colorSaturation; + } + /** + * Sets the saturation of the colored identicon shapes. + * + * @param $value float Saturation in the range [0.0, 1.0]. + * @return self + */ + public function setColorSaturation($value) + { + if (!\is_numeric($value) || $value < 0 || $value > 1) { + throw new \InvalidArgumentException("The color saturation was invalid. " . "Only values in the range [0.0, 1.0] are allowed."); + } + $this->colorSaturation = (float) $value; + return $this; + } + /** + * Gets the value of the ColorLightness property. + * + * @return array(float, float) + */ + public function getColorLightness() + { + return $this->colorLightness; + } + /** + * Sets the value of the ColorLightness property. + * + * @param $value array(float, float) Lightness range. + * @return self + */ + public function setColorLightness($value) + { + if (!\is_array($value) || !\array_key_exists(0, $value) || !\array_key_exists(1, $value) || !\is_numeric($value[0]) || !\is_numeric($value[1]) || $value[0] < 0 || $value[0] > 1 || $value[1] < 0 || $value[1] > 1) { + throw new \InvalidArgumentException("The value passed to setColorLightness was invalid. " . "Please check the documentation."); + } + $this->colorLightness = array((float) $value[0], (float) $value[1]); + return $this; + } + /** + * Gets the value of the GrayscaleLightness property. + * + * @return array(float, float) + */ + public function getGrayscaleLightness() + { + return $this->grayscaleLightness; + } + /** + * Sets the value of the GrayscaleLightness property. + * + * @param $value array(float, float) Lightness range. + * @return self + */ + public function setGrayscaleLightness($value) + { + if (!\is_array($value) || !\array_key_exists(0, $value) || !\array_key_exists(1, $value) || !\is_numeric($value[0]) || !\is_numeric($value[1]) || $value[0] < 0 || $value[0] > 1 || $value[1] < 0 || $value[1] > 1) { + throw new \InvalidArgumentException("The value passed to setGrayscaleLightness was invalid. " . "Please check the documentation."); + } + $this->grayscaleLightness = array((float) $value[0], (float) $value[1]); + return $this; + } + /** + * Gets the default value of the BackgroundColor property. Resolves to transparent. + * + * @return \Jdenticon\Color + */ + public static function getDefaultBackgroundColor() + { + return Color::fromRgb(255, 255, 255, 255); + } + /** + * Gets the default value of the Padding property. Resolves to 0.08. + * + * @return float + */ + public static function getDefaultPadding() + { + return 0.08; + } + /** + * Gets the default value of the ColorSaturation property. Resolves to 0.5. + * + * @return float + */ + public static function getDefaultColorSaturation() + { + return 0.5; + } + /** + * Gets the default value of the GrayscaleSaturation property. Resolves to 0. + * + * @return float + */ + public static function getDefaultGrayscaleSaturation() + { + return 0; + } + /** + * Gets the default value of the ColorLightness property. Resolves to [0.4, 0.8]. + * + * @return array + */ + public static function getDefaultColorLightness() + { + return array(0.4, 0.8); + } + /** + * Gets the default value of the GrayscaleLightness property. Resolves to [0.3, 0.9]. + * + * @return array + */ + public static function getDefaultGrayscaleLightness() + { + return array(0.3, 0.9); + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Rendering/AbstractRenderer.php b/vendor-scoped/jdenticon/jdenticon/src/Rendering/AbstractRenderer.php new file mode 100644 index 0000000..42fb82e --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Rendering/AbstractRenderer.php @@ -0,0 +1,174 @@ +transform = Transform::getEmpty(); + } + /** + * Sets the current transform that will be applied on all coordinates before + * being rendered to the target image. + * + * @param \Jdenticon\Rendering\Transform $transform The transform to set. + * If NULL is specified any existing transform is removed. + */ + public function setTransform(\Avatar_Privacy\Vendor\Jdenticon\Rendering\Transform $transform) + { + $this->transform = $transform === null ? Transform::getEmpty() : $transform; + } + /** + * Gets the current transform that will be applied on all coordinates before + * being rendered to the target image. + * + * @return \Jdenticon\Rendering\Transform + */ + public function getTransform() + { + return $this->transform; + } + /** + * Adds a polygon without translating its coordinates. + * + * @param array $points An array of the points that the polygon consists of. + */ + protected abstract function addPolygonNoTransform($points); + /** + * Adds a circle without translating its coordinates. + * + * @param float $x The x-coordinate of the bounding rectangle + * upper-left corner. + * @param float $y The y-coordinate of the bounding rectangle + * upper-left corner. + * @param float $size The size of the bounding rectangle. + * @param bool $counterClockwise If true the circle will be drawn + * counter clockwise. + */ + protected abstract function addCircleNoTransform($x, $y, $size, $counterClockwise); + /** + * Sets the background color of the image. + * + * @param \Jdenticon\Color $color The image background color. + */ + public function setBackgroundColor(\Avatar_Privacy\Vendor\Jdenticon\Color $color) + { + $this->backgroundColor = $color; + } + /** + * Gets the background color of the image. + * + * @return \Jdenticon\Color + */ + public function getBackgroundColor() + { + return $this->backgroundColor; + } + private function addPolygonCore(array $points, $invert) + { + $transformedPoints = array(); + foreach ($points as $point) { + $transformedPoints[] = $this->transform->transformPoint($point->x, $point->y); + } + if ($invert) { + $transformedPoints = \array_reverse($transformedPoints); + } + //var_dump($transformedPoints); + $this->addPolygonNoTransform($transformedPoints); + } + /** + * Adds a rectangle to the image. + * + * @param float $x The x-coordinate of the rectangle upper-left corner. + * @param float $y The y-coordinate of the rectangle upper-left corner. + * @param float $width The width of the rectangle. + * @param float $height The height of the rectangle. + * @param bool $invert If true the area of the rectangle will be removed + * from the filled area. + */ + public function addRectangle($x, $y, $width, $height, $invert = \false) + { + $this->addPolygonCore(array(new Point($x, $y), new Point($x + $width, $y), new Point($x + $width, $y + $height), new Point($x, $y + $height)), $invert); + } + /** + * Adds a circle to the image. + * + * @param float $x The x-coordinate of the bounding rectangle + * upper-left corner. + * @param float $y The y-coordinate of the bounding rectangle + * upper-left corner. + * @param float $size The size of the bounding rectangle. + * @param bool $invert If true the area of the circle will be removed + * from the filled area. + */ + public function addCircle($x, $y, $size, $invert = \false) + { + $northWest = $this->transform->transformPoint($x, $y, $size, $size); + $this->addCircleNoTransform($northWest->x, $northWest->y, $size, $invert); + } + /** + * Adds a polygon to the image. + * + * @param array $points Array of points that the polygon consists of. + * @param bool $invert If true the area of the polygon will be removed + * from the filled area. + */ + public function addPolygon($points, $invert = \false) + { + $this->addPolygonCore($points, $invert); + } + /** + * Adds a triangle to the image. + * + * @param float $x The x-coordinate of the bounding rectangle + * upper-left corner. + * @param float $y The y-coordinate of the bounding rectangle + * upper-left corner. + * @param float $width The width of the bounding rectangle. + * @param float $height The height of the bounding rectangle. + * @param float $direction The direction of the 90 degree corner of the + * triangle. + * @param bool $invert If true the area of the triangle will be removed + * from the filled area. + */ + public function addTriangle($x, $y, $width, $height, $direction, $invert = \false) + { + $points = array(new Point($x + $width, $y), new Point($x + $width, $y + $height), new Point($x, $y + $height), new Point($x, $y)); + \array_splice($points, $direction, 1); + $this->addPolygonCore($points, $invert); + } + /** + * Adds a rhombus to the image. + * + * @param float $x The x-coordinate of the bounding rectangle + * upper-left corner. + * @param float $y The y-coordinate of the bounding rectangle + * upper-left corner. + * @param float $width The width of the bounding rectangle. + * @param float $height The height of the bounding rectangle. + * @param bool $invert If true the area of the rhombus will be removed + * from the filled area. + */ + public function addRhombus($x, $y, $width, $height, $invert = \false) + { + $this->addPolygonCore(array(new Point($x + $width / 2, $y), new Point($x + $width, $y + $height / 2), new Point($x + $width / 2, $y + $height), new Point($x, $y + $height / 2)), $invert); + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Rendering/ColorTheme.php b/vendor-scoped/jdenticon/jdenticon/src/Rendering/ColorTheme.php new file mode 100644 index 0000000..3c7d878 --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Rendering/ColorTheme.php @@ -0,0 +1,83 @@ +getGrayscaleLightness(); + $colorLightness = $style->getColorLightness(); + $hues = $style->getHues(); + if ($hues !== null) { + // $hue is in the range [0, 1] + // Multiply with 0.999 to change the range to [0, 1) + $hueIndex = (int) ($hue * 0.999 * \count($hues)); + $hue = (float) $hues[$hueIndex] / 360; + } + $this->darkGray = Color::fromHslCompensated($hue, $style->getGrayscaleSaturation(), $grayscaleLightness[0]); + $this->midColor = Color::fromHslCompensated($hue, $style->getColorSaturation(), ($colorLightness[0] + $colorLightness[1]) / 2); + $this->lightGray = Color::fromHslCompensated($hue, $style->getGrayscaleSaturation(), $grayscaleLightness[1]); + $this->lightColor = Color::fromHslCompensated($hue, $style->getColorSaturation(), $colorLightness[1]); + $this->darkColor = Color::fromHslCompensated($hue, $style->getColorSaturation(), $colorLightness[0]); + } + /** + * Gets a color from this color theme by index. + * + * @param int $index Color index in the range [0, getCount()). + * @return Jdenticon\Color + */ + public function getByIndex($index) + { + if ($index === 0) { + return $this->darkGray; + } + if ($index === 1) { + return $this->midColor; + } + if ($index === 2) { + return $this->lightGray; + } + if ($index === 3) { + return $this->lightColor; + } + if ($index === 4) { + return $this->darkColor; + } + return null; + } + /** + * Gets the number of available colors in this theme. + * + * @return int + */ + public function getCount() + { + return 5; + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Rendering/IconGenerator.php b/vendor-scoped/jdenticon/jdenticon/src/Rendering/IconGenerator.php new file mode 100644 index 0000000..caa1a8f --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Rendering/IconGenerator.php @@ -0,0 +1,257 @@ +defaultShapes = array( + // Sides + new ShapeCategory( + /*$colorIndex=*/ + 8, + /*$shapes=*/ + ShapeDefinitions::getOuterShapes(), + /*$shapeIndex=*/ + 2, + /*$rotationIndex=*/ + 3, + /*$positions=*/ + array(1, 0, 2, 0, 2, 3, 1, 3, 0, 1, 3, 1, 3, 2, 0, 2) + ), + // Corners + new ShapeCategory( + /*$colorIndex=*/ + 9, + /*$shapes=*/ + ShapeDefinitions::getOuterShapes(), + /*$shapeIndex=*/ + 4, + /*$rotationIndex=*/ + 5, + /*$positions=*/ + array(0, 0, 3, 0, 3, 3, 0, 3) + ), + // Center + new ShapeCategory( + /*$colorIndex=*/ + 10, + /*$shapes=*/ + ShapeDefinitions::getCenterShapes(), + /*$shapeIndex=*/ + 1, + /*$rotationIndex=*/ + null, + /*$positions=*/ + array(1, 1, 2, 1, 2, 2, 1, 2) + ), + ); + } + public static function getDefaultGenerator() + { + if (self::$instance === null) { + self::$instance = new IconGenerator(); + } + return self::$instance; + } + /** + * Gets the number of cells in each direction of the icons generated by + * this IconGenerator. + * + * @return int + */ + public function getCellCount() + { + return 4; + } + /** + * Determines the hue to be used in an icon for the specified hash. + * + * @return float Hue in the range [0, 1]. + */ + protected static function getHue($hash) + { + $value = \hexdec(\substr($hash, -7)); + return $value / 0xfffffff; + } + /** + * Determines whether $newValue is duplicated in $source if all values + * in $duplicateValues are determined to be equal. + * + * @return bool + */ + private static function isDuplicate(array $source, $newValue, array $duplicateValues) + { + if (\in_array($newValue, $duplicateValues, \true)) { + foreach ($duplicateValues as $value) { + if (\in_array($value, $source, \true)) { + return \true; + } + } + } + return \false; + } + /** + * Gets the specified octet from a byte array. + * + * @param string $hash The hexstring from which the octet will be retrieved. + * @param int $index The zero-based index of the octet to be returned. + * @return int + */ + protected static function getOctet($hash, $index) + { + return \hexdec($hash[$index]); + } + /** + * Gets an array of the shape categories to be rendered in icons generated + * by this IconGenerator. + * + * @return array + */ + protected function getCategories() + { + return $this->defaultShapes; + } + /** + * Gets an enumeration of individual shapes to be rendered in an icon for a + * specific hash. + * + * @param \Jdenticon\Rendering\ColorTheme $colorTheme A color theme + * specifying the colors to be used in the icon. + * @param string $hash The hash for which the shapes will be returned. + * @return array(Jdenticon\Shapes\Shape) + */ + protected function getShapes($colorTheme, $hash) + { + $usedColorThemeIndexes = array(); + $categories = self::getCategories(); + $shapes = array(); + $colorCount = $colorTheme->getCount(); + foreach ($categories as $category) { + $colorThemeIndex = self::getOctet($hash, $category->colorIndex) % $colorCount; + if (self::isDuplicate( + // Disallow dark gray and dark color combo + $usedColorThemeIndexes, + $colorThemeIndex, + array(0, 4) + ) || self::isDuplicate( + // Disallow light gray and light color combo + $usedColorThemeIndexes, + $colorThemeIndex, + array(2, 3) + )) { + $colorThemeIndex = 1; + } + $usedColorThemeIndexes[] = $colorThemeIndex; + $startRotationIndex = $category->rotationIndex === null ? 0 : self::getOctet($hash, $category->rotationIndex); + $shapeIndex = self::getOctet($hash, $category->shapeIndex) % \count($category->shapes); + $shape = $category->shapes[$shapeIndex]; + $shapes[] = new Shape( + /*$definition=*/ + $shape, + /*$color=*/ + $colorTheme->getByIndex($colorThemeIndex), + /*$positions=*/ + $category->positions, + /*$startRotationIndex=*/ + $startRotationIndex + ); + } + return $shapes; + } + /** + * Creates a quadratic copy of the specified + * {@link \Jdenticon\Rendering\Rectangle} with a multiple of the cell count + * as size. + * + * @param \Jdenticon\Rendering\Rectangle $rect The rectangle to be + * normalized. + */ + protected function normalizeRectangle(\Avatar_Privacy\Vendor\Jdenticon\Rendering\Rectangle $rect) + { + $size = (int) \min($rect->width, $rect->height); + // Make size a multiple of the cell count + $size -= $size % $this->getCellCount(); + return new Rectangle((int) ($rect->x + ($rect->width - $size) / 2), (int) ($rect->y + ($rect->height - $size) / 2), $size, $size); + } + /** + * Renders the background of an icon. + * + * @param \Jdenticon\Rendering\RendererInterface $renderer The renderer to + * be used for rendering the icon on the target surface. + * @param \Jdenticon\Rendering\Rectangle $rect The outer bounds of the icon. + * @param \Jdenticon\IdenticonStyle $style The style of the icon. + * @param \Jdenticon\Rendering\ColorTheme $colorTheme A color theme + * specifying the colors to be used in the icon. + * @param string $hash The hash to be used as basis for the generated icon. + */ + protected function renderBackground(\Avatar_Privacy\Vendor\Jdenticon\Rendering\RendererInterface $renderer, \Avatar_Privacy\Vendor\Jdenticon\Rendering\Rectangle $rect, \Avatar_Privacy\Vendor\Jdenticon\IdenticonStyle $style, \Avatar_Privacy\Vendor\Jdenticon\Rendering\ColorTheme $colorTheme, $hash) + { + $renderer->setBackgroundColor($style->getBackgroundColor()); + } + /** + * Renders the foreground of an icon. + * + * @param \Jdenticon\Rendering\RendererInterface $renderer The renderer to + * be used for rendering the icon on the target surface. + * @param \Jdenticon\Rendering\Rectangle $rect The outer bounds of the icon. + * @param \Jdenticon\IdenticonStyle $style The style of the icon. + * @param \Jdenticon\Rendering\ColorTheme $colorTheme A color theme + * specifying the colors to be used in the icon. + * @param string $hash The hash to be used as basis for the generated icon. + */ + protected function renderForeground(\Avatar_Privacy\Vendor\Jdenticon\Rendering\RendererInterface $renderer, \Avatar_Privacy\Vendor\Jdenticon\Rendering\Rectangle $rect, \Avatar_Privacy\Vendor\Jdenticon\IdenticonStyle $style, \Avatar_Privacy\Vendor\Jdenticon\Rendering\ColorTheme $colorTheme, $hash) + { + // Ensure rect is quadratic and a multiple of the cell count + $normalizedRect = $this->normalizeRectangle($rect); + $cellSize = $normalizedRect->width / $this->getCellCount(); + foreach ($this->getShapes($colorTheme, $hash) as $shape) { + $rotation = $shape->startRotationIndex; + $renderer->beginShape($shape->color); + $positionCount = \count($shape->positions); + for ($i = 0; $i + 1 < $positionCount; $i += 2) { + $renderer->setTransform(new Transform($normalizedRect->x + $shape->positions[$i + 0] * $cellSize, $normalizedRect->y + $shape->positions[$i + 1] * $cellSize, $cellSize, $rotation++ % 4)); + $shape->definition->__invoke($renderer, $cellSize, $i / 2); + } + $renderer->endShape(); + } + } + /** + * Generates an identicon for the specified hash. + * + * @param \Jdenticon\Rendering\RendererInterface $renderer The renderer to + * be used for rendering the icon on the target surface. + * @param \Jdenticon\Rendering\Rectangle $rect The outer bounds of the icon. + * @param \Jdenticon\IdenticonStyle $style The style of the icon. + * @param string $hash The hash to be used as basis for the generated icon. + */ + public function generate(\Avatar_Privacy\Vendor\Jdenticon\Rendering\RendererInterface $renderer, \Avatar_Privacy\Vendor\Jdenticon\Rendering\Rectangle $rect, \Avatar_Privacy\Vendor\Jdenticon\IdenticonStyle $style, $hash) + { + $hue = self::getHue($hash); + $colorTheme = new ColorTheme($hue, $style); + $this->renderBackground($renderer, $rect, $style, $colorTheme, $hash); + $this->renderForeground($renderer, $rect, $style, $colorTheme, $hash); + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Rendering/Point.php b/vendor-scoped/jdenticon/jdenticon/src/Rendering/Point.php new file mode 100644 index 0000000..33e58de --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Rendering/Point.php @@ -0,0 +1,51 @@ +x = $x; + $this->y = $y; + } + /** + * The X coordinate of this point. + * + * @var float + */ + public $x; + /** + * The Y coordinate of this point. + * + * @var float + */ + public $y; + /** + * Gets a string representation of the point. + * + * @return string + */ + public function __toString() + { + return $this->x + ", " + $this->y; + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Rendering/Rectangle.php b/vendor-scoped/jdenticon/jdenticon/src/Rendering/Rectangle.php new file mode 100644 index 0000000..68a9d6e --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Rendering/Rectangle.php @@ -0,0 +1,56 @@ +x = $x; + $this->y = $y; + $this->width = $width; + $this->height = $height; + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Rendering/RendererInterface.php b/vendor-scoped/jdenticon/jdenticon/src/Rendering/RendererInterface.php new file mode 100644 index 0000000..de48253 --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Rendering/RendererInterface.php @@ -0,0 +1,128 @@ +dataString = ''; + } + /** + * Adds a circle to the SVG. + * + * @param float $x X coordinate of the left side of the containing rectangle. + * @param float $y Y coordinate of the top side of the containing rectangle. + * @param float $size The diameter of the circle. + * @param bool $counterClockwise If true the circle will be drawn counter + * clockwise. This affects the rendering since the evenodd filling rule + * is used by Jdenticon. + */ + public function addCircle($x, $y, $size, $counterClockwise) + { + $sweepFlag = $counterClockwise ? '0' : '1'; + $radiusAsString = \number_format($size / 2, 2, '.', ''); + $this->dataString .= 'M' . \number_format($x, 2, '.', '') . ' ' . \number_format($y + $size / 2, 2, '.', '') . 'a' . $radiusAsString . ',' . $radiusAsString . ' 0 1,' . $sweepFlag . ' ' . \number_format($size, 2, '.', '') . ',0' . 'a' . $radiusAsString . ',' . $radiusAsString . ' 0 1,' . $sweepFlag . ' ' . \number_format(-$size, 2, '.', '') . ',0'; + } + /** + * Adds a polygon to the SVG. + * + * @param array(\Jdenticon\Rendering\Point) $points The corners of the + * polygon. + */ + public function addPolygon($points) + { + $pointCount = \count($points); + $this->dataString .= 'M' . \number_format($points[0]->x, 2, '.', '') . ' ' . \number_format($points[0]->y, 2, '.', ''); + for ($i = 1; $i < $pointCount; $i++) { + $this->dataString .= 'L' . \number_format($points[$i]->x, 2, '.', '') . ' ' . \number_format($points[$i]->y, 2, '.', ''); + } + $this->dataString .= 'Z'; + } + /** + * Gets the path as a SVG path string. + * + * @return string + */ + public function __toString() + { + return $this->dataString; + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Rendering/SvgRenderer.php b/vendor-scoped/jdenticon/jdenticon/src/Rendering/SvgRenderer.php new file mode 100644 index 0000000..764d907 --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Rendering/SvgRenderer.php @@ -0,0 +1,114 @@ +width = $width; + $this->height = $height; + } + /** + * Gets the MIME type of the renderer output. + * + * @return string + */ + public function getMimeType() + { + return 'image/svg+xml'; + } + /** + * Adds a circle without translating its coordinates. + * + * @param float $x The x-coordinate of the bounding rectangle + * upper-left corner. + * @param float $y The y-coordinate of the bounding rectangle + * upper-left corner. + * @param float $size The size of the bounding rectangle. + * @param bool $counterClockwise If true the circle will be drawn + * counter clockwise. + */ + protected function addCircleNoTransform($x, $y, $size, $counterClockwise) + { + $this->path->addCircle($x, $y, $size, $counterClockwise); + } + /** + * Adds a polygon without translating its coordinates. + * + * @param array $points An array of the points that the polygon consists of. + */ + protected function addPolygonNoTransform($points) + { + $this->path->addPolygon($points); + } + /** + * Begins a new shape. The shape should be ended with a call to endShape. + * + * @param \Jdenticon\Color $color The color of the shape. + */ + public function beginShape(\Avatar_Privacy\Vendor\Jdenticon\Color $color) + { + $colorString = $color->toHexString(6); + if (isset($this->pathsByColor[$colorString])) { + $this->path = $this->pathsByColor[$colorString]; + } else { + $this->path = new SvgPath(); + $this->pathsByColor[$colorString] = $this->path; + } + } + /** + * Ends the currently drawn shape. + */ + public function endShape() + { + } + /** + * Generates an SVG string of the renderer output. + * + * @param bool $fragment If true an SVG string without the root svg element + * will be rendered. + */ + public function getData($fragment = \false) + { + $svg = ''; + $widthAsString = \number_format($this->width, 0, '.', ''); + $heightAsString = \number_format($this->height, 0, '.', ''); + if (!$fragment) { + $svg .= ''; + } + if ($this->backgroundColor->a > 0) { + $opacity = (float) $this->backgroundColor->a / 255; + $svg .= ''; + } + foreach ($this->pathsByColor as $color => $path) { + $svg .= ""; + } + if (!$fragment) { + $svg .= ''; + } + return $svg; + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Rendering/Transform.php b/vendor-scoped/jdenticon/jdenticon/src/Rendering/Transform.php new file mode 100644 index 0000000..3d970ef --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Rendering/Transform.php @@ -0,0 +1,79 @@ +x = $x; + $this->y = $y; + $this->size = $size; + $this->rotation = $rotation; + } + /** + * Gets a noop transform. + * + * @return \Jdenticon\Rendering\Transform + */ + public static function getEmpty() + { + return new Transform(0, 0, 0, 0); + } + /** + * Transforms the specified point based on the translation and rotation + * specification for this Transform. + * + * @param float $x x-coordinate + * @param float $y y-coordinate + * @param float $width The width of the transformed rectangle. If greater + * than 0, this will ensure the returned point is of the upper left + * corner of the transformed rectangle. + * @param float $height The height of the transformed rectangle. If greater + * than 0, this will ensure the returned point is of the upper left + * corner of the transformed rectangle. + * @return \Jdenticon\Rendering\Point + */ + public function transformPoint($x, $y, $width = 0, $height = 0) + { + $right = $this->x + $this->size; + $bottom = $this->y + $this->size; + switch ($this->rotation) { + case 1: + return new Point($right - $y - $height, $this->y + $x); + case 2: + return new Point($right - $x - $width, $bottom - $y - $height); + case 3: + return new Point($this->x + $y, $bottom - $x - $width); + default: + return new Point($this->x + $x, $this->y + $y); + } + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Rendering/TriangleDirection.php b/vendor-scoped/jdenticon/jdenticon/src/Rendering/TriangleDirection.php new file mode 100644 index 0000000..4d3948d --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Rendering/TriangleDirection.php @@ -0,0 +1,35 @@ +definition = $definition; + $this->color = $color; + $this->positions = $positions; + $this->startRotationIndex = $startRotationIndex; + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Shapes/ShapeCategory.php b/vendor-scoped/jdenticon/jdenticon/src/Shapes/ShapeCategory.php new file mode 100644 index 0000000..bf3d4b5 --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Shapes/ShapeCategory.php @@ -0,0 +1,64 @@ +colorIndex = $colorIndex; + $this->shapes = $shapes; + $this->shapeIndex = $shapeIndex; + $this->rotationIndex = $rotationIndex; + $this->positions = $positions; + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Shapes/ShapeDefinitions.php b/vendor-scoped/jdenticon/jdenticon/src/Shapes/ShapeDefinitions.php new file mode 100644 index 0000000..1e11294 --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Shapes/ShapeDefinitions.php @@ -0,0 +1,154 @@ +addTriangle(0, 0, $cell, $cell, 0); + }, function ($renderer, $cell, $index) { + $renderer->addTriangle(0, $cell / 2, $cell, $cell / 2, 0); + }, function ($renderer, $cell, $index) { + $renderer->addRhombus(0, 0, $cell, $cell); + }, function ($renderer, $cell, $index) { + $m = $cell / 6; + $renderer->addCircle($m, $m, $cell - 2 * $m); + }); + } + private static function createCenterShapes() + { + return array(function ($renderer, $cell, $index) { + $k = $cell * 0.42; + $renderer->addPolygon(array(new Point(0, 0), new Point($cell, 0), new Point($cell, $cell - $k * 2), new Point($cell - $k, $cell), new Point(0, $cell))); + }, function ($renderer, $cell, $index) { + $w = (int) ($cell * 0.5); + $h = (int) ($cell * 0.8); + $renderer->addTriangle($cell - $w, 0, $w, $h, TriangleDirection::NORTH_EAST); + }, function ($renderer, $cell, $index) { + $s = (int) ($cell / 3); + $renderer->addRectangle($s, $s, $cell - $s, $cell - $s); + }, function ($renderer, $cell, $index) { + $tmp = $cell * 0.1; + if ($tmp > 1) { + // large icon => truncate decimals + $inner = (int) $tmp; + } elseif ($tmp > 0.5) { + // medium size icon => fixed width + $inner = 1; + } else { + // small icon => anti-aliased border + $inner = $tmp; + } + // Use fixed outer border widths in small icons to ensure + // the border is drawn + if ($cell < 6) { + $outer = 1; + } elseif ($cell < 8) { + $outer = 2; + } else { + $outer = (int) ($cell / 4); + } + $renderer->addRectangle($outer, $outer, $cell - $inner - $outer, $cell - $inner - $outer); + }, function ($renderer, $cell, $index) { + $m = (int) ($cell * 0.15); + $s = (int) ($cell * 0.5); + $renderer->addCircle($cell - $s - $m, $cell - $s - $m, $s); + }, function ($renderer, $cell, $index) { + $inner = $cell * 0.1; + $outer = $inner * 4; + // Align edge to nearest pixel in large icons + if ($outer > 3) { + $outer = (int) $outer; + } + $renderer->addRectangle(0, 0, $cell, $cell); + $renderer->addPolygon(array(new Point($outer, $outer), new Point($cell - $inner, $outer), new Point($outer + ($cell - $outer - $inner) / 2, $cell - $inner)), \true); + }, function ($renderer, $cell, $index) { + $renderer->addPolygon(array(new Point(0, 0), new Point($cell, 0), new Point($cell, $cell * 0.7), new Point($cell * 0.4, $cell * 0.4), new Point($cell * 0.7, $cell), new Point(0, $cell))); + }, function ($renderer, $cell, $index) { + $renderer->addTriangle($cell / 2, $cell / 2, $cell / 2, $cell / 2, TriangleDirection::SOUTH_EAST); + }, function ($renderer, $cell, $index) { + $renderer->addPolygon(array(new Point(0, 0), new Point($cell, 0), new Point($cell, $cell / 2), new Point($cell / 2, $cell), new Point(0, $cell))); + }, function ($renderer, $cell, $index) { + $tmp = $cell * 0.14; + if ($cell < 8) { + // small icon => anti-aliased border + $inner = $tmp; + } else { + // large icon => truncate decimals + $inner = (int) $tmp; + } + // Use fixed outer border widths in small icons to ensure + // the border is drawn + if ($cell < 4) { + $outer = 1; + } elseif ($cell < 6) { + $outer = 2; + } else { + $outer = (int) ($cell * 0.35); + } + $renderer->addRectangle(0, 0, $cell, $cell); + $renderer->addRectangle($outer, $outer, $cell - $outer - $inner, $cell - $outer - $inner, \true); + }, function ($renderer, $cell, $index) { + $inner = $cell * 0.12; + $outer = $inner * 3; + $renderer->addRectangle(0, 0, $cell, $cell); + $renderer->addCircle($outer, $outer, $cell - $inner - $outer, \true); + }, function ($renderer, $cell, $index) { + $renderer->addTriangle($cell / 2, $cell / 2, $cell / 2, $cell / 2, TriangleDirection::SOUTH_EAST); + }, function ($renderer, $cell, $index) { + $m = $cell * 0.25; + $renderer->addRectangle(0, 0, $cell, $cell); + $renderer->addRhombus($m, $m, $cell - $m, $cell - $m, \true); + }, function ($renderer, $cell, $index) { + $m = $cell * 0.4; + $s = $cell * 1.2; + if ($index != 0) { + $renderer->addCircle($m, $m, $s); + } + }); + } +} diff --git a/vendor-scoped/jdenticon/jdenticon/src/Shapes/ShapePosition.php b/vendor-scoped/jdenticon/jdenticon/src/Shapes/ShapePosition.php new file mode 100644 index 0000000..808d70b --- /dev/null +++ b/vendor-scoped/jdenticon/jdenticon/src/Shapes/ShapePosition.php @@ -0,0 +1,42 @@ +x = $x; + $this->y = $y; + } +} diff --git a/vendor-scoped/level-2/dice/Dice.php b/vendor-scoped/level-2/dice/Dice.php new file mode 100644 index 0000000..8fd8277 --- /dev/null +++ b/vendor-scoped/level-2/dice/Dice.php @@ -0,0 +1,319 @@ + | https://r.je/dice + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version 4.0 */ +namespace Avatar_Privacy\Vendor\Dice; + +class Dice +{ + const CONSTANT = 'Dice::CONSTANT'; + const GLOBAL = 'Dice::GLOBAL'; + const INSTANCE = 'Dice::INSTANCE'; + const CHAIN_CALL = 'Dice::CHAIN_CALL'; + const SELF = 'Dice::SELF'; + /** + * @var array $rules Rules which have been set using addRule() + */ + private $rules = []; + /** + * @var array $cache A cache of closures based on class name so each class is only reflected once + */ + private $cache = []; + /** + * @var array $instances Stores any instances marked as 'shared' so create() can return the same instance + */ + private $instances = []; + /** + * Add a rule $rule to the class $name + * @param string $name The name of the class to add the rule for + * @param array $rule The container can be fully configured using rules provided by associative arrays. See {@link https://r.je/dice.html#example3} for a description of the rules. + */ + public function addRule(string $name, array $rule) : self + { + $dice = clone $this; + $this->addRuleTo($dice, $name, $rule); + return $dice; + } + /** + * Add rules as array. Useful for JSON loading $dice->addRules(json_decode(file_get_contents('foo.json')); + * @param array Rules in a single array [name => $rule] format + */ + public function addRules($rules) : self + { + if (\is_string($rules)) { + $rules = \json_decode(\file_get_contents($rules), \true); + } + $dice = clone $this; + foreach ($rules as $name => $rule) { + $this->addRuleTo($dice, $name, $rule); + } + return $dice; + } + private function addRuleTo(Dice $dice, string $name, array $rule) + { + if (isset($rule['instanceOf']) && (!\array_key_exists('inherit', $rule) || $rule['inherit'] === \true)) { + $rule = \array_replace_recursive($dice->getRule($rule['instanceOf']), $rule); + } + //Allow substitutions rules to be defined with a leading a slash + if (isset($rule['substitutions'])) { + foreach ($rule['substitutions'] as $key => $value) { + $rule['substitutions'][\ltrim($key, '\\')] = $value; + } + } + //Clear any existing instance or cache for this class + unset($dice->instances[$name], $dice->cache[$name]); + $dice->rules[\ltrim(\strtolower($name), '\\')] = \array_replace_recursive($dice->getRule($name), $rule); + } + /** + * Returns the rule that will be applied to the class $name when calling create() + * @param string name The name of the class to get the rules for + * @return array The rules for the specified class + */ + public function getRule(string $name) : array + { + $lcName = \strtolower(\ltrim($name, '\\')); + if (isset($this->rules[$lcName])) { + return $this->rules[$lcName]; + } + foreach ($this->rules as $key => $rule) { + // Find a rule which matches the class described in $name where: + if (empty($rule['instanceOf']) && $key !== '*' && \is_subclass_of($name, $key) && (!\array_key_exists('inherit', $rule) || $rule['inherit'] === \true)) { + // And that rule should be inherited to subclasses + return $rule; + } + } + // No rule has matched, return the default rule if it's set + return isset($this->rules['*']) ? $this->rules['*'] : []; + } + /** + * Returns a fully constructed object based on $name using $args and $share as constructor arguments if supplied + * @param string name The name of the class to instantiate + * @param array $args An array with any additional arguments to be passed into the constructor upon instantiation + * @param array $share a list of defined in shareInstances for objects higher up the object graph, should only be used internally + * @return object A fully constructed object based on the specified input arguments + */ + public function create(string $name, array $args = [], array $share = []) + { + // Is there a shared instance set? Return it. Better here than a closure for this, calling a closure is slower. + if (!empty($this->instances[$name])) { + return $this->instances[$name]; + } + // Create a closure for creating the object if there isn't one already + if (empty($this->cache[$name])) { + $this->cache[$name] = $this->getClosure(\ltrim($name, '\\'), $this->getRule($name)); + } + // Call the cached closure which will return a fully constructed object of type $name + return $this->cache[$name]($args, $share); + } + /** + * Returns a closure for creating object $name based on $rule, caching the reflection object for later use + * @param string $name the Name of the class to get the closure for + * @param array $rule The container can be fully configured using rules provided by associative arrays. See {@link https://r.je/dice.html#example3} for a description of the rules. + * @return callable A closure + */ + private function getClosure(string $name, array $rule) + { + // Reflect the class and constructor, this should only ever be done once per class and get cached + $class = new \ReflectionClass(isset($rule['instanceOf']) ? $rule['instanceOf'] : $name); + $constructor = $class->getConstructor(); + // Create parameter generating function in order to cache reflection on the parameters. This way $reflect->getParameters() only ever gets called once + $params = $constructor ? $this->getParams($constructor, $rule) : null; + //PHP throws a fatal error rather than an exception when trying to instantiate an interface, detect it and throw an exception instead + if ($class->isInterface()) { + $closure = function () { + throw new \InvalidArgumentException('Cannot instantiate interface'); + }; + } else { + if ($params) { + $closure = function (array $args, array $share) use($class, $params) { + // This class has depenencies, call the $params closure to generate them based on $args and $share + return new $class->name(...$params($args, $share)); + }; + } else { + $closure = function () use($class) { + // No constructor arguments, just instantiate the class + return new $class->name(); + }; + } + } + if (!empty($rule['shared'])) { + $closure = function (array $args, array $share) use($class, $name, $constructor, $params, $closure) { + //Internal classes may not be able to be constructed without calling the constructor and will not suffer from #7, construct them normally. + if ($class->isInternal()) { + $this->instances[$class->name] = $this->instances['\\' . $class->name] = $closure($args, $share); + } else { + //Otherwise, create the class without calling the constructor (and write to \$name and $name, see issue #68) + $this->instances[$name] = $this->instances['\\' . $name] = $class->newInstanceWithoutConstructor(); + // Now call this constructor after constructing all the dependencies. This avoids problems with cyclic references (issue #7) + if ($constructor) { + $constructor->invokeArgs($this->instances[$name], $params($args, $share)); + } + } + return $this->instances[$name]; + }; + } + // If there are shared instances, create them and merge them with shared instances higher up the object graph + if (isset($rule['shareInstances'])) { + $closure = function (array $args, array $share) use($closure, $rule) { + foreach ($rule['shareInstances'] as $instance) { + $share[] = $this->create($instance, [], $share); + } + return $closure($args, $share); + }; + } + // When $rule['call'] is set, wrap the closure in another closure which will call the required methods after constructing the object + // By putting this in a closure, the loop is never executed unless call is actually set + return isset($rule['call']) ? function (array $args, array $share) use($closure, $class, $rule, $name) { + // Construct the object using the original closure + $object = $closure($args, $share); + foreach ($rule['call'] as $call) { + // Generate the method arguments using getParams() and call the returned closure + $params = $this->getParams($class->getMethod($call[0]), ['shareInstances' => isset($rule['shareInstances']) ? $rule['shareInstances'] : []])($this->expand(isset($call[1]) ? $call[1] : []), $share); + $return = $object->{$call[0]}(...$params); + if (isset($call[2])) { + if ($call[2] === self::CHAIN_CALL) { + if (!empty($rule['shared'])) { + $this->instances[$name] = $return; + } + if (\is_object($return)) { + $class = new \ReflectionClass(\get_class($return)); + } + $object = $return; + } else { + if (\is_callable($call[2])) { + \call_user_func($call[2], $return); + } + } + } + } + return $object; + } : $closure; + } + /** + * Looks for Dice::INSTANCE, Dice::GLOBAL or Dice::CONSTANT array keys in $param and when found returns an object based on the value see {@link https:// r.je/dice.html#example3-1} + * @param mixed $param Either a string or an array, + * @param array $share Array of instances from 'shareInstances', required for calls to `create` + * @param bool $createFromString + * @return mixed + */ + private function expand($param, array $share = [], bool $createFromString = \false) + { + if (\is_array($param)) { + //if a rule specifies Dice::INSTANCE, look up the relevant instance + if (isset($param[self::INSTANCE])) { + if ($param[self::INSTANCE] === self::SELF) { + return $this; + } + //Check for 'params' which allows parameters to be sent to the instance when it's created + //Either as a callback method or to the constructor of the instance + $args = isset($param['params']) ? $this->expand($param['params']) : []; + //Support Dice::INSTANCE by creating/fetching the specified instance + if (\is_array($param[self::INSTANCE])) { + $param[self::INSTANCE][0] = $this->expand($param[self::INSTANCE][0], $share, \true); + } + if (\is_callable($param[self::INSTANCE])) { + return \call_user_func($param[self::INSTANCE], ...$args); + } else { + return $this->create($param[self::INSTANCE], \array_merge($args, $share)); + } + } else { + if (isset($param[self::GLOBAL])) { + return $GLOBALS[$param[self::GLOBAL]]; + } else { + if (isset($param[self::CONSTANT])) { + return \constant($param[self::CONSTANT]); + } else { + foreach ($param as $name => $value) { + $param[$name] = $this->expand($value, $share); + } + } + } + } + } + return \is_string($param) && $createFromString ? $this->create($param) : $param; + } + /** + * Looks through the array $search for any object which can be used to fulfil $param + The original array $search is modifed so must be passed by reference. + */ + private function matchParam(\ReflectionParameter $param, $class, array &$search) + { + foreach ($search as $i => $arg) { + if ($class && ($arg instanceof $class || $arg === null && $param->allowsNull())) { + // The argument matched, return it and remove it from $search so it won't wrongly match another parameter + return \array_splice($search, $i, 1)[0]; + } + } + return \false; + } + /** + * Returns a closure that generates arguments for $method based on $rule and any $args passed into the closure + * @param object $method An instance of ReflectionMethod (see: {@link http:// php.net/manual/en/class.reflectionmethod.php}) + * @param array $rule The container can be fully configured using rules provided by associative arrays. See {@link https://r.je/dice.html#example3} for a description of the rules. + * @return callable A closure that uses the cached information to generate the arguments for the method + */ + private function getParams(\ReflectionMethod $method, array $rule) + { + // Cache some information about the parameter in $paramInfo so (slow) reflection isn't needed every time + $paramInfo = []; + foreach ($method->getParameters() as $param) { + $type = $param->getType(); + $class = $type instanceof \ReflectionNamedType && !$type->isBuiltIn() ? $type->getName() : null; + $paramInfo[] = [$class, $param, isset($rule['substitutions']) && \array_key_exists($class, $rule['substitutions'])]; + } + // Return a closure that uses the cached information to generate the arguments for the method + return function (array $args, array $share = []) use($paramInfo, $rule) { + // If the rule has construtParams set, construct any classes reference and use them as $args + if (isset($rule['constructParams'])) { + $args = \array_merge($args, $this->expand($rule['constructParams'], $share)); + } + // Array of matched parameters + $parameters = []; + // Fnd a value for each method argument + foreach ($paramInfo as list($class, $param, $sub)) { + // Loop through $args and see whether or not each value can match the current parameter based on type hint + if ($args && ($match = $this->matchParam($param, $class, $args)) !== \false) { + $parameters[] = $match; + } else { + if (($copy = $share) && ($match = $this->matchParam($param, $class, $copy)) !== \false) { + $parameters[] = $match; + } else { + if ($class) { + try { + if ($sub) { + $parameters[] = $this->expand($rule['substitutions'][$class], $share, \true); + } else { + $parameters[] = !$param->allowsNull() ? $this->create($class, [], $share) : null; + } + } catch (\InvalidArgumentException $e) { + } + } else { + if ($args && $param->getType()) { + for ($i = 0; $i < \count($args); $i++) { + if (\call_user_func('is_' . $param->getType()->getName(), $args[$i])) { + $parameters[] = \array_splice($args, $i, 1)[0]; + break; + } + } + } else { + if ($args) { + $parameters[] = $this->expand(\array_shift($args)); + } else { + if ($param->isVariadic()) { + $parameters = \array_merge($parameters, $args); + } else { + $parameters[] = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; + } + } + } + } + } + } + } + return $parameters; + }; + } +} diff --git a/vendor-scoped/level-2/dice/README.md b/vendor-scoped/level-2/dice/README.md new file mode 100644 index 0000000..ae47620 --- /dev/null +++ b/vendor-scoped/level-2/dice/README.md @@ -0,0 +1,365 @@ +[Dice PHP Dependency Injection Container](https://r.je/dice.html) +====================================== + +Dice is a minimalist Dependency Injection Container for PHP with a focus on being lightweight and fast as well as requiring as little configuration as possible. + + +Project Goals +------------- + +1) To be lightweight and not a huge library with dozens of files (Dice is a single 100 line class) yet support all features (and more) offered by much more complex containers + +2) To "just work". Basic functionality should work with zero configuration + +3) Where configuration is required, it should be as minimal and reusable as possible as well as easy to use. + +4) Speed! (See [the section on performance](#performance)) + + +Installation +------------ + +Just include the lightweight `Dice.php` in your project and it's usable without any further configuration: + +Simple example: + +```php +b = $b; + } +} + +class B { + +} + +require_once 'Dice.php'; +$dice = new \Dice\Dice; + +$a = $dice->create('A'); + +var_dump($a->b); //B object + +?> +``` + + +Full Documentation +------------------ + +For complete documentation please see the [Dice PHP Dependency Injection container home page](https://r.je/dice.html) + + +PHP version compatibility +------------------------- + +Dice is compatible with PHP 7.0 and up, there are archived versions of Dice which support PHP 5.6 however this is no longer maintanied. + + +Performance +----------- + +Dice uses reflection which is often wrongly labelled "slow". Reflection is considerably faster than loading and parsing a configuration file. There are a set of benchmarks [here](https://rawgit.com/TomBZombie/php-dependency-injection-benchmarks/master/test1-5_results.html) and [here](https://rawgit.com/TomBZombie/php-dependency-injection-benchmarks/master/test6_results.html) (To download the benchmark tool yourself see [this repository](https://github.com/TomBZombie/php-dependency-injection-benchmarks)) and Dice is faster than the others in most cases. + +In the real world test ([test 6](https://rawgit.com/TomBZombie/php-dependency-injection-benchmarks/master/test6_results.html)) Dice is neck-and-neck with Pimple (which requires writing an awful lot of configuration code) and although Symfony\DependencyInjection is faster at creating objects, it has a larger overhead and you need to create over 500 objects on each page load until it becomes faster than Dice. The same is true of Phalcon, the overhead of loading the Phalcon extension means that unless you're creating well over a thousand objects per HTTP request, the overhead is not worthwhile. + + +Credits +------------ + +Originally developed by Tom Butler (@TomBZombie), with many thanks to daniel-meister (@daniel-meister), Garrett W. (@garrettw), maxwilms (@maxwilms) for bug fixes, suggestions and improvements. + + +Updates +------------ + +### 15/11/2018 4.0 Release - Backwards incompatible + +Dice is now immutable and has better support for other immutable objects. + +**New Features** + +#### 1. Dice is Immutable + +This avoids [issues surrounding mutability](https://www.yegor256.com/2014/06/09/objects-should-be-immutable.html) where a Dice instance is passed around the application and reconfigured. The only difference is that `addRules` and `addRule` return a new Dice instance with the updated rules rather than changing the state of the existing instance. + +```php + +// Pre-4.0 code: +$dice->addRule('PDO', ['shared' => true]); + +$db = $dice->create('PDO'); + +// 4.0 code: +$dice = $dice->addRule('PDO', ['shared' => true]); + +$db = $dice->create('PDO'); +``` + +From a practical perspective in most cases just put `$dice = ` in front of any `$dice->addRule()` call and it will work as before. + +#### 2. Support for Object Method Chaining + +One feature some immutable objects have is they offer object chaining. + +Consider the following Object: + +```php + +$httpRequest = new HTTPRequest(); +$httpRequest = $httpRequest->url('http://example.org')->method('POST')->postdata('foo=bar'); +``` + +It was not possible for Dice to consturuct the configured object in previous versions. As of 4.0 Dice supports chaining method call using the `call` rule and the `Dice::CHAIN_CALL` constant: + +```php +$dice = $dice->addRule('HTTPRequest', + ['call' => [ + ['url', ['http://example.org'], Dice::CHAIN_CALL], + ['method', ['POST'], Dice::CHAIN_CALL ], + ['postdata', ['foo=bar'], Dice::CHAIN_CALL] + ] + ] +); +``` + +Dice will replace the HTTPRequest object with the result of the chained call. This is also useful for factories: + + +```php +$dice = $dice->addRule('MyDatabase', + [ + 'instanceOf' => 'DatabaseFactory', + 'call' => [ + ['get', ['Database'], Dice::CHAIN_CALL] + ] + ] +); + +$database = $dice->create('MyDatabase'); +//Equivalent of: + +$factory = new DatabaseFactory(); +$database = $factory->get('Database'); +``` + + +### 06/03/2018 3.0 Release - Backwards incompatible + +**New Features** + +#### 1. The JSON loader has been removed in favour of a new `addRules` method. + +```php +$dice->addRules([ + '\PDO' => [ + 'shared' => true + ], + 'Framework\Router' => [ + 'constructParams' => ['Foo', 'Bar'] + ] +]); +``` + +The purpose of this addition is to make the JSON loader redundant. Loading of rules from a JSON file can easily be achieved with the code: + +```php +$dice->addRules(json_decode(file_get_contents('rules.json'))); +``` + +#### 2. Better JSON file support: constants and superglobals + +In order to improve support for rules being defined in external JSON files, constants and superglobals can now be passed into objects created by Dice. + +For example, passing the `$_SERVER` superglobal into a router instance and calling PDO's `setAttribute` with `PDO::ATTR_ERRMODE` and `PDO::ERRMODE_EXCEPTION` can be achieved like this in a JSON file: + +_rules.json_ + +```json +{ + "Router": { + "constructParams": [ + {"Dice::GLOBAL": "_SERVER"} + ] + }, + "PDO": { + "shared": true, + "constructParams": [ + "mysql:dbname=testdb;host=127.0.0.1", + "dbuser", + "dbpass" + ], + "call": [ + [ + "setAttribute", + [ + {"Dice::CONSTANT": "PDO::ATTR_ERRMODE"}, + {"Dice::CONSTANT": "PDO::ERRMODE_EXCEPTION"} + ] + ] + ] + } +} +``` + +```php +$dice->addRules(json_decode(file_get_contents('rules.json'))); +``` + +**Backwards incompatible changes** + +1. Dice 3.0 requires PHP 7.0 or above, PHP 5.6 is no longer supported. + +2. Dice no longer supports `'instance'` keys to signify instances. For example: + +```php +$dice->addRule('ClassName', [ + 'constructParams' => ['instance' => '$NamedPDOInstance'] +]); +``` + +As noted in issue #125 this made it impossible to pass an array to a constructor if the array had a key `'instance'`. Instead, the new `\Dice\Dice::INSTANCE` constant should be used: + +```php +$dice->addRule('ClassName', [ + 'constructParams' => [\Dice\Dice::INSTANCE => '$NamedPDOInstance'] +]); +``` +_to make the constant shorter to type out, you can `use \Dice\Dice;` and reference `Dice::INSTANCE`_ + +10/06/2016 + +** Backwards incompatible change ** + +Based on [Issue 110](https://github.com/Level-2/Dice/pull/110) named instances using `instanceOf` will now inherit the rules applied to the class they are instances of: + +```php + +$rule = []; +$rule['shared'] = true; + +$dice->addRule('MyClass', $rule); + +$rule = []; +$rule['instanceOf'] = 'MyClass'; +$rule['constructParams'] = ['Foo', 'Bar']; + +$dice->addRule('$MyNamedInstance', $rule); + + +``` + +`$dice->create('$MyNamedInstance')` will now create a class following the rules applied to both `MyClass` and `$MyNamedInstance` so the instance will be shared. + +Previously only the rules applied to the named instance would be used. + +To restore the old behaviour, set `inherit` to `false` on the named instance: + +```php +$rule = []; +$rule['shared'] = true; + +$dice->addRule('MyClass', $rule); + +$rule = []; +$rule['instanceOf'] = 'MyClass'; +$rule['constructParams'] = ['Foo', 'Bar']; + + +//Prevent the named instance inheriting rules from the class named in `instanceOf`: +$rule['inherit'] = false; + +$dice->addRule('$MyNamedInstance', $rule); + +``` + + + + +29/10/2014 +* Based on [Issue #15](https://github.com/TomBZombie/Dice/issues/15), Dice will now only call closures if they are wrapped in \Dice\Instance. **PLEASE NOTE: THIS IS BACKWARDS INCOMPATIBLE **. + +Previously Dice ran closures that were passed as substitutions, constructParams and when calling methods: + +```php + +$rule->substitutions['A'] = function() { + return new A; +}; + +$rule->call[] = ['someMethod', function() { +// '2' will be provided as the first argument when someMethod is called +return 2; +}]; + +$rule->constructParams[] = function() { + //'abc' will be providedas the first constructor parameter + return 'abc'; +}; +``` + +This behaviour has changed as it makes it impossible to provide a closure as a construct parameter or when calling a method because the closure was always called and executed. + +To overcome this, Dice will now only call a closures if they're wrapped in \Dice\Instance: + +```php +$rule->substitutions['A'] = ['instance' => function() { + return new A; +}]; + +$rule->call[] = ['someMethod', ['instance' => function() { +// '2' will be provided as the first argument when someMethod is called +return 2; +}]]); + +$rule->constructParams[] = ['instance' => function() { { + //'abc' will be providedas the first constructor parameter + return 'abc'; +}]); +``` + + + + + +04/09/2014 +* Pushed PHP5.6 branch live. This is slightly more efficient using PHP5.6 features. For PHP5.4-PHP5.5 please see the relevant branch. This version will be maintained until PHP5.6 is more widespread. + + +26/08/2014 +* Added PHP5.6 branch. Tidied up code by using PHP5.6 features. This will be moved to master when PHP5.6 is released + +28/06/2014 +* Greatly improved efficienty. Dice is now the fastest Dependency Injection Container for PHP! + +06/06/2014 +* Added support for cyclic references ( https://github.com/TomBZombie/Dice/issues/7 ). Please note this is poor design but this fix will stop the infinite loop this design creates. + +27/03/2014 +* Removed assign() method as this duplicated functionality available using $rule->shared +* Removed $callback argument in $dice->create() as the only real use for this feature can be better achieved using $rule->shareInstances +* Tidied up code, removing unused/undocumented features. Dice is now even more lightweight and faster. +* Fixed a bug where when using $rule->call it would use the substitution rules from the constructor on each method called +* Updated [Dice documentation](https://r.je/dice.html) to use shorthand array syntax + +01/03/2014 +* Added test cases for the Xml Loader and Loader Callback classes +* Added a JSON loader + test case +* Added all test cases to a test suite +* Moved to PHP5.4 array syntax. A PHP5.3 compatible version is now available in the PHP5.3 branch. +* Fixed an issue where using named instances would trigger the autoloader with an invalid class name every time a class was created + + +28/02/2014 +* Added basic namespace support. Documentation update will follow shortly. Also moved the XML loader into its own file, you'll need to include it separately if you're using it. +* Please note: CHANGES ARE NOT BACKWARDS COMPATIBLE. However they are easily fixed by doing the following find/replaces: + +```php + new Dice => new \Dice\Dice + new DiceInstance => new \Dice\Instance + new DiceRule => new \Dice\Rule +``` diff --git a/vendor-scoped/mistic100/randomcolor/src/RandomColor.php b/vendor-scoped/mistic100/randomcolor/src/RandomColor.php new file mode 100644 index 0000000..f0ef4cd --- /dev/null +++ b/vendor-scoped/mistic100/randomcolor/src/RandomColor.php @@ -0,0 +1,259 @@ + '', 'hue' => [], 'luminosity' => ''], $options); + $h = self::_pickHue($options); + $s = self::_pickSaturation($h, $options); + $v = self::_pickBrightness($h, $s, $options); + return self::format(\compact('h', 's', 'v'), @$options['format']); + } + public static function many($count, $options = array()) + { + $colors = array(); + for ($i = 0; $i < $count; $i++) { + $colors[] = self::one($options); + } + return $colors; + } + public static function format($hsv, $format = 'hex') + { + switch ($format) { + case 'hsv': + return $hsv; + case 'hsl': + return self::hsv2hsl($hsv); + case 'hslCss': + $hsl = self::hsv2hsl($hsv); + return 'hsl(' . $hsl['h'] . ',' . $hsl['s'] . '%,' . $hsl['l'] . '%)'; + case 'rgb': + return self::hsv2rgb($hsv); + case 'rgbCss': + return 'rgb(' . \implode(',', self::hsv2rgb($hsv)) . ')'; + case 'hex': + default: + return self::hsv2hex($hsv); + } + } + private static function _pickHue($options) + { + $range = self::_getHueRange($options); + if (empty($range)) { + return 0; + } + $hue = self::_rand($range, $options); + // Instead of storing red as two separate ranges, + // we group them, using negative numbers + if ($hue < 0) { + $hue = 360 + $hue; + } + return $hue; + } + private static function _pickSaturation($h, $options) + { + if (@$options['hue'] === 'monochrome') { + return 0; + } + if (@$options['luminosity'] === 'random') { + return self::_rand(array(0, 100), $options); + } + $colorInfo = self::_getColorInfo($h); + $range = $colorInfo['s']; + switch (@$options['luminosity']) { + case 'bright': + $range[0] = 55; + break; + case 'dark': + $range[0] = $range[1] - 10; + break; + case 'light': + $range[1] = 55; + break; + } + return self::_rand($range, $options); + } + private static function _pickBrightness($h, $s, $options) + { + if (@$options['luminosity'] === 'random') { + $range = array(0, 100); + } else { + $range = array(self::_getMinimumBrightness($h, $s), 100); + switch (@$options['luminosity']) { + case 'dark': + $range[1] = $range[0] + 20; + break; + case 'light': + $range[0] = \round(($range[1] + $range[0]) / 2); + break; + } + } + return self::_rand($range, $options); + } + private static function _getHueRange($options) + { + $ranges = array(); + if (isset($options['hue'])) { + if (!\is_array($options['hue'])) { + $options['hue'] = array($options['hue']); + } + foreach ($options['hue'] as $hue) { + if ($hue === 'random') { + $ranges[] = array(0, 360); + } else { + if (isset(self::$dictionary[$hue])) { + $ranges[] = self::$dictionary[$hue]['h']; + } else { + if (\is_numeric($hue)) { + $hue = \intval($hue); + if ($hue <= 360 && $hue >= 0) { + $ranges[] = array($hue, $hue); + } + } + } + } + } + } + if (($l = \count($ranges)) === 0) { + return array(0, 360); + } else { + if ($l === 1) { + return $ranges[0]; + } else { + return $ranges[self::_rand(array(0, $l - 1), $options)]; + } + } + } + private static function _getMinimumBrightness($h, $s) + { + $colorInfo = self::_getColorInfo($h); + $bounds = $colorInfo['bounds']; + for ($i = 0, $l = \count($bounds); $i < $l - 1; $i++) { + $s1 = $bounds[$i][0]; + $v1 = $bounds[$i][1]; + $s2 = $bounds[$i + 1][0]; + $v2 = $bounds[$i + 1][1]; + if ($s >= $s1 && $s <= $s2) { + $m = ($v2 - $v1) / ($s2 - $s1); + $b = $v1 - $m * $s1; + return \round($m * $s + $b); + } + } + return 0; + } + private static function _getColorInfo($h) + { + // Maps red colors to make picking hue easier + if ($h >= 334 && $h <= 360) { + $h -= 360; + } + foreach (self::$dictionary as $color) { + if ($color['h'] !== null && $h >= $color['h'][0] && $h <= $color['h'][1]) { + return $color; + } + } + } + private static function _rand($bounds, $options) + { + if (isset($options['prng'])) { + return $options['prng']($bounds[0], $bounds[1]); + } else { + return \mt_rand($bounds[0], $bounds[1]); + } + } + public static function hsv2hex($hsv) + { + $rgb = self::hsv2rgb($hsv); + $hex = '#'; + foreach ($rgb as $c) { + $hex .= \str_pad(\dechex($c), 2, '0', \STR_PAD_LEFT); + } + return $hex; + } + public static function hsv2hsl($hsv) + { + \extract($hsv); + $s /= 100; + $v /= 100; + $k = (2 - $s) * $v; + return array('h' => $h, 's' => \round($s * $v / ($k < 1 ? $k : 2 - $k), 4) * 100, 'l' => $k / 2 * 100); + } + public static function hsv2rgb($hsv) + { + \extract($hsv); + $h /= 360; + $s /= 100; + $v /= 100; + $i = \floor($h * 6); + $f = $h * 6 - $i; + $m = $v * (1 - $s); + $n = $v * (1 - $s * $f); + $k = $v * (1 - $s * (1 - $f)); + $r = 1; + $g = 1; + $b = 1; + switch ($i) { + case 0: + list($r, $g, $b) = array($v, $k, $m); + break; + case 1: + list($r, $g, $b) = array($n, $v, $m); + break; + case 2: + list($r, $g, $b) = array($m, $v, $k); + break; + case 3: + list($r, $g, $b) = array($m, $n, $v); + break; + case 4: + list($r, $g, $b) = array($k, $m, $v); + break; + case 5: + case 6: + list($r, $g, $b) = array($v, $m, $n); + break; + } + return array('r' => \floor($r * 255), 'g' => \floor($g * 255), 'b' => \floor($b * 255)); + } +} +/* + * h=hueRange + * s=saturationRange : bounds[0][0] ; bounds[-][0] + */ +RandomColor::$dictionary = array('monochrome' => array('bounds' => array(array(0, 0), array(100, 0)), 'h' => NULL, 's' => array(0, 100)), 'red' => array('bounds' => array(array(20, 100), array(30, 92), array(40, 89), array(50, 85), array(60, 78), array(70, 70), array(80, 60), array(90, 55), array(100, 50)), 'h' => array(-26, 18), 's' => array(20, 100)), 'orange' => array('bounds' => array(array(20, 100), array(30, 93), array(40, 88), array(50, 86), array(60, 85), array(70, 70), array(100, 70)), 'h' => array(19, 46), 's' => array(20, 100)), 'yellow' => array('bounds' => array(array(25, 100), array(40, 94), array(50, 89), array(60, 86), array(70, 84), array(80, 82), array(90, 80), array(100, 75)), 'h' => array(47, 62), 's' => array(25, 100)), 'green' => array('bounds' => array(array(30, 100), array(40, 90), array(50, 85), array(60, 81), array(70, 74), array(80, 64), array(90, 50), array(100, 40)), 'h' => array(63, 178), 's' => array(30, 100)), 'blue' => array('bounds' => array(array(20, 100), array(30, 86), array(40, 80), array(50, 74), array(60, 60), array(70, 52), array(80, 44), array(90, 39), array(100, 35)), 'h' => array(179, 257), 's' => array(20, 100)), 'purple' => array('bounds' => array(array(20, 100), array(30, 87), array(40, 79), array(50, 70), array(60, 65), array(70, 59), array(80, 52), array(90, 45), array(100, 42)), 'h' => array(258, 282), 's' => array(20, 100)), 'pink' => array('bounds' => array(array(20, 100), array(30, 90), array(40, 86), array(60, 84), array(80, 80), array(90, 75), array(100, 73)), 'h' => array(283, 334), 's' => array(20, 100))); diff --git a/vendor-scoped/mundschenk-at/check-wp-requirements/LICENSE b/vendor-scoped/mundschenk-at/check-wp-requirements/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/vendor-scoped/mundschenk-at/check-wp-requirements/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/vendor-scoped/mundschenk-at/check-wp-requirements/README.md b/vendor-scoped/mundschenk-at/check-wp-requirements/README.md new file mode 100644 index 0000000..4823b0f --- /dev/null +++ b/vendor-scoped/mundschenk-at/check-wp-requirements/README.md @@ -0,0 +1,59 @@ +# check-wp-requirements + +[![Build Status](https://travis-ci.com/mundschenk-at/check-wp-requirements.svg?branch=master)](https://travis-ci.com/mundschenk-at/check-wp-requirements) +[![Latest Stable Version](https://poser.pugx.org/mundschenk-at/check-wp-requirements/v/stable)](https://packagist.org/packages/mundschenk-at/check-wp-requirements) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/mundschenk-at/check-wp-requirements/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/mundschenk-at/check-wp-requirements/?branch=master) +[![Code Coverage](https://scrutinizer-ci.com/g/mundschenk-at/check-wp-requirements/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/mundschenk-at/check-wp-requirements/?branch=master) +[![License](https://poser.pugx.org/mundschenk-at/check-wp-requirements/license)](https://packagist.org/packages/mundschenk-at/check-wp-requirements) + +A helper class for WordPress plugins to check PHP version and other requirements. + +## Requirements + +* PHP 5.6.0 or above +* WordPress 5.2 or higher. + +## Installation + +The best way to use this package is through Composer: + +```BASH +$ composer require mundschenk-at/check-wp-requirements +``` + +## Basic Usage + +1. Create a `\Mundschenk\WP_Requirements` object and set the requirements in the constructor. +2. Call the `\Mundschenk\WP_Requirements::check()` method and start your plugin normally if it + returns `true`. + +```PHP +// Set up autoloader. +require_once __DIR__ . '/vendor/autoload.php'; + +/** + * Load the plugin after checking for the necessary PHP version. + * + * It's necessary to do this here because main class relies on namespaces. + */ +function run_your_plugin() { + + $requirements = new \Mundschenk\WP_Requirements( 'Your Plugin Name', __FILE__, 'your-textdomain', [ + 'php' => '5.6.0', + 'multibyte' => true, + 'utf-8' => false, + ] ); + + if ( $requirements->check() ) { + // Autoload the rest of your classes. + + // Create and start the plugin. + ... + } +} +run_your_plugin(); +``` + +## License + +check-wp-requirements is licensed under the GNU General Public License 2 or later - see the [LICENSE](LICENSE) file for details. diff --git a/vendor-scoped/mundschenk-at/check-wp-requirements/class-wp-requirements.php b/vendor-scoped/mundschenk-at/check-wp-requirements/class-wp-requirements.php new file mode 100644 index 0000000..9bfa1cc --- /dev/null +++ b/vendor-scoped/mundschenk-at/check-wp-requirements/class-wp-requirements.php @@ -0,0 +1,219 @@ +plugin_name = $name; + $this->plugin_file = $plugin_path; + $this->textdomain = $textdomain; + $this->base_dir = \dirname(__FILE__); + $this->install_requirements = \wp_parse_args($requirements, ['php' => '5.2.0', 'multibyte' => \false, 'utf-8' => \false]); + } + /** + * Checks if all runtime requirements for the plugin are met. + * + * @return bool + */ + public function check() + { + $requirements_met = \true; + foreach ($this->get_requirements() as $requirement) { + if (!empty($this->install_requirements[$requirement['enable_key']]) && !\call_user_func($requirement['check'])) { + $notice = $requirement['notice']; + $requirements_met = \false; + break; + } + } + if (!$requirements_met && !empty($notice) && \is_admin()) { + // Load text domain to ensure translated admin notices. + \load_plugin_textdomain($this->textdomain); + // Add admin notice. + \add_action('admin_notices', $notice); + } + return $requirements_met; + } + /** + * 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. + * } + */ + protected function get_requirements() + { + return [['enable_key' => 'php', 'check' => [$this, 'check_php_support'], 'notice' => [$this, 'admin_notices_php_version_incompatible']], ['enable_key' => 'multibyte', 'check' => [$this, 'check_multibyte_support'], 'notice' => [$this, 'admin_notices_mbstring_incompatible']], ['enable_key' => 'utf-8', 'check' => [$this, 'check_utf8_support'], 'notice' => [$this, 'admin_notices_charset_incompatible']]]; + } + /** + * Deactivates the plugin. + */ + public function deactivate_plugin() + { + \deactivate_plugins(\plugin_basename($this->plugin_file)); + } + /** + * Checks if the PHP version in use is at least equal to the required version. + * + * @return bool + */ + protected function check_php_support() + { + return \version_compare(\PHP_VERSION, $this->install_requirements['php'], '>='); + } + /** + * Checks if multibyte functions are supported. + * + * @return bool + */ + protected function check_multibyte_support() + { + return \function_exists('mb_strlen') && \function_exists('mb_strtolower') && \function_exists('mb_substr') && \function_exists('mb_detect_encoding'); + } + /** + * Checks if the blog charset is set to UTF-8. + * + * @return bool + */ + protected function check_utf8_support() + { + return 'utf-8' === \strtolower(\get_bloginfo('charset')); + } + /** + * Print 'PHP version incompatible' admin notice + */ + public function admin_notices_php_version_incompatible() + { + $this->display_error_notice( + /* translators: 1: plugin name 2: target PHP version number 3: actual PHP version number */ + \__('The activated plugin %1$s requires PHP %2$s or later. Your server is running PHP %3$s. Please deactivate this plugin, or upgrade your server\'s installation of PHP.', $this->textdomain), + "{$this->plugin_name}", + $this->install_requirements['php'], + \PHP_VERSION + ); + } + /** + * Prints 'mbstring extension missing' admin notice + */ + public function admin_notices_mbstring_incompatible() + { + $this->display_error_notice( + /* translators: 1: plugin name 2: mbstring documentation URL */ + \__('The activated plugin %1$s requires the mbstring PHP extension to be enabled on your server. Please deactivate this plugin, or enable the extension.', $this->textdomain), + "{$this->plugin_name}", + /* translators: URL with mbstring PHP extension installation instructions */ + \__('http://www.php.net/manual/en/mbstring.installation.php', $this->textdomain) + ); + } + /** + * Prints 'Charset incompatible' admin notice + */ + public function admin_notices_charset_incompatible() + { + $this->display_error_notice( + /* translators: 1: plugin name 2: current character encoding 3: options URL */ + \__('The activated plugin %1$s requires your blog use the UTF-8 character encoding. You have set your blogs encoding to %2$s. Please deactivate this plugin, or change your character encoding to UTF-8.', $this->textdomain), + "{$this->plugin_name}", + \get_bloginfo('charset'), + '/wp-admin/options-reading.php' + ); + } + /** + * Shows an error message in the admin area. + * + * @param string $format ... An `sprintf` format string, followd by an unspecified number of optional parameters. + */ + protected function display_error_notice($format) + { + if (\func_num_args() < 1 || empty($format)) { + return; + // abort. + } + $args = \func_get_args(); + $format = \array_shift($args); + $message = \vsprintf($format, $args); + require "{$this->base_dir}/partials/requirements-error-notice.php"; + } +} diff --git a/vendor-scoped/mundschenk-at/check-wp-requirements/partials/requirements-error-notice.php b/vendor-scoped/mundschenk-at/check-wp-requirements/partials/requirements-error-notice.php new file mode 100644 index 0000000..518f4fb --- /dev/null +++ b/vendor-scoped/mundschenk-at/check-wp-requirements/partials/requirements-error-notice.php @@ -0,0 +1,31 @@ + +
+

+
+ + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/vendor-scoped/mundschenk-at/wp-data-storage/README.md b/vendor-scoped/mundschenk-at/wp-data-storage/README.md new file mode 100644 index 0000000..6d8de5e --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-data-storage/README.md @@ -0,0 +1,22 @@ +# WordPress Data Storage + +[![Build Status](https://travis-ci.org/mundschenk-at/wp-data-storage.svg?branch=master)](https://travis-ci.org/mundschenk-at/wp-data-storage) +[![Latest Stable Version](https://poser.pugx.org/mundschenk-at/wp-data-storage/v/stable)](https://packagist.org/packages/mundschenk-at/wp-data-storage) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/mundschenk-at/wp-data-storage/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/mundschenk-at/wp-data-storage/?branch=master) +[![Code Coverage](https://scrutinizer-ci.com/g/mundschenk-at/wp-data-storage/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/mundschenk-at/wp-data-storage/?branch=master) +[![License](https://poser.pugx.org/mundschenk-at/wp-data-storage/license)](https://packagist.org/packages/mundschenk-at/wp-data-storage) + +An object oriented library for dealing with WordPress options and caching. + +## Requirements + +* PHP 5.6.0 or above +* WordPress 4.4 or higher (for transient keys longer than 64 characters). + +## Installation + +The best way to use this package is through Composer: + +```BASH +$ composer require mundschenk-at/wp-data-storage +``` diff --git a/vendor-scoped/mundschenk-at/wp-data-storage/src/class-abstract-cache.php b/vendor-scoped/mundschenk-at/wp-data-storage/src/class-abstract-cache.php new file mode 100644 index 0000000..48f55b9 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-data-storage/src/class-abstract-cache.php @@ -0,0 +1,112 @@ + + */ +abstract class Abstract_Cache +{ + /** + * Incrementor for cache invalidation. + * + * @var int + */ + protected $incrementor; + /** + * The prefix added to all keys. + * + * @var string + */ + private $prefix; + /** + * Create new cache instance. + * + * @param string $prefix The prefix automatically added to cache keys. + */ + public function __construct($prefix) + { + $this->prefix = $prefix; + if (empty($this->incrementor)) { + $this->invalidate(); + } + } + /** + * Invalidate all cached elements by reseting the incrementor. + * + * @return void + */ + public abstract function invalidate(); + /** + * Retrieves a cached value. + * + * @param string $key The cache key root. + * + * @return mixed + */ + public abstract function get($key); + /** + * Sets an entry in the cache and stores the key. + * + * @param string $key The cache key root. + * @param mixed $value The value to store. + * @param int $duration Optional. The duration in seconds. Default 0 (no expiration). + * + * @return bool True if the cache could be set successfully. + */ + public abstract function set($key, $value, $duration = 0); + /** + * Deletes an entry from the cache. + * + * @param string $key The cache key root. + * + * @return bool True on successful removal, false on failure. + */ + public abstract function delete($key); + /** + * Retrieves the complete key to use. + * + * @param string $key The cache key root. + * + * @return string + */ + protected function get_key($key) + { + return "{$this->prefix}{$this->incrementor}_{$key}"; + } + /** + * Retrieves the set prefix. + * + * @return string + */ + protected function get_prefix() + { + return $this->prefix; + } +} diff --git a/vendor-scoped/mundschenk-at/wp-data-storage/src/class-cache.php b/vendor-scoped/mundschenk-at/wp-data-storage/src/class-cache.php new file mode 100644 index 0000000..50e0446 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-data-storage/src/class-cache.php @@ -0,0 +1,105 @@ + + */ +class Cache extends Abstract_Cache +{ + /** + * The incrementor cache key. + * + * @var string + */ + private $incrementor_key; + /** + * The cache group. + * + * @var string + */ + private $group; + /** + * Create new cache instance. + * + * @param string $prefix The prefix automatically added to cache keys. + * @param string|null $group Optional. The cache group. Defaults to $prefix. + */ + public function __construct($prefix, $group = null) + { + $this->group = !isset($group) ? $prefix : $group; + $this->incrementor_key = "{$prefix}cache_incrementor"; + $this->incrementor = (int) \wp_cache_get($this->incrementor_key, $this->group); + parent::__construct($prefix); + } + /** + * Invalidate all cached elements by reseting the incrementor. + */ + public function invalidate() + { + $this->incrementor = \time(); + \wp_cache_set($this->incrementor_key, $this->incrementor, $this->group, 0); + } + /** + * Retrieves a cached value. + * + * @param string $key The cache key. + * @param bool|null $found Optional. Whether the key was found in the cache. Disambiguates a return of false as a storable value. Passed by reference. Default null. + * + * @return mixed + */ + public function get($key, &$found = null) + { + return \wp_cache_get($this->get_key($key), $this->group, \false, $found); + } + /** + * Sets an entry in the cache and stores the key. + * + * @param string $key The cache key. + * @param mixed $value The value to store. + * @param int $duration Optional. The duration in seconds. Default 0 (no expiration). + * + * @return bool True if the cache could be set successfully. + */ + public function set($key, $value, $duration = 0) + { + return \wp_cache_set($this->get_key($key), $value, $this->group, $duration); + } + /** + * Deletes an entry from the cache. + * + * @param string $key The cache key root. + * + * @return bool True on successful removal, false on failure. + */ + public function delete($key) + { + return \wp_cache_delete($this->get_key($key), $this->group); + } +} diff --git a/vendor-scoped/mundschenk-at/wp-data-storage/src/class-network-options.php b/vendor-scoped/mundschenk-at/wp-data-storage/src/class-network-options.php new file mode 100644 index 0000000..7df5f5a --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-data-storage/src/class-network-options.php @@ -0,0 +1,97 @@ + + */ +class Network_Options extends Options +{ + /** + * The network ID. + * + * @var int + */ + private $network_id; + /** + * Create new Network Options instance. + * + * @param string $prefix The prefix automatically added to option names. + * @param int|null $network_id Optional. The network ID or null for the current network. Default null. + */ + public function __construct($prefix, $network_id = null) + { + $this->network_id = !empty($network_id) ? $network_id : \get_current_network_id(); + parent::__construct($prefix); + } + /** + * Retrieves an option value. + * + * @param string $option The option name (without the plugin-specific prefix). + * @param mixed $default Optional. Default value to return if the option does not exist. Default null. + * @param bool $raw Optional. Use the raw option name (i.e. don't call get_name). Default false. + * + * @return mixed Value set for the option. + */ + public function get($option, $default = null, $raw = \false) + { + $value = \get_network_option($this->network_id, $raw ? $option : $this->get_name($option), $default); + if (\is_array($default) && '' === $value) { + $value = []; + } + return $value; + } + /** + * Sets or updates an option. + * + * @param string $option The option name (without the plugin-specific prefix). + * @param mixed $value The value to store. + * @param bool $autoload Optional. This value is ignored for network options, + * which are always autoloaded. Default true. + * @param bool $raw Optional. Use the raw option name (i.e. don't call get_name). Default false. + * + * @return bool False if value was not updated and true if value was updated. + */ + public function set($option, $value, $autoload = \true, $raw = \false) + { + return \update_network_option($this->network_id, $raw ? $option : $this->get_name($option), $value); + } + /** + * Deletes an option. + * + * @param string $option The option name (without the plugin-specific prefix). + * @param bool $raw Optional. Use the raw option name (i.e. don't call get_name). Default false. + * + * @return bool True, if option is successfully deleted. False on failure. + */ + public function delete($option, $raw = \false) + { + return \delete_network_option($this->network_id, $raw ? $option : $this->get_name($option)); + } +} diff --git a/vendor-scoped/mundschenk-at/wp-data-storage/src/class-options.php b/vendor-scoped/mundschenk-at/wp-data-storage/src/class-options.php new file mode 100644 index 0000000..1ab49dc --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-data-storage/src/class-options.php @@ -0,0 +1,108 @@ + + */ +class Options +{ + /** + * The prefix added to all keys. + * + * @var string + */ + private $prefix; + /** + * Create new Options instance. + * + * @param string $prefix The prefix automatically added to option names. + */ + public function __construct($prefix) + { + $this->prefix = $prefix; + } + /** + * Retrieves an option value. + * + * @param string $option The option name (without the plugin-specific prefix). + * @param mixed $default Optional. Default value to return if the option does not exist. Default null. + * @param bool $raw Optional. Use the raw option name (i.e. don't call get_name). Default false. + * + * @return mixed Value set for the option. + */ + public function get($option, $default = null, $raw = \false) + { + $value = \get_option($raw ? $option : $this->get_name($option), $default); + if (\is_array($default) && '' === $value) { + $value = []; + } + return $value; + } + /** + * Sets or updates an option. + * + * @param string $option The option name (without the plugin-specific prefix). + * @param mixed $value The value to store. + * @param bool $autoload Optional. Whether to load the option when WordPress + * starts up. For existing options, $autoload can only + * be updated using update_option() if $value is also + * changed. Default true. + * @param bool $raw Optional. Use the raw option name (i.e. don't call get_name). Default false. + * + * @return bool False if value was not updated and true if value was updated. + */ + public function set($option, $value, $autoload = \true, $raw = \false) + { + return \update_option($raw ? $option : $this->get_name($option), $value, $autoload); + } + /** + * Deletes an option. + * + * @param string $option The option name (without the plugin-specific prefix). + * @param bool $raw Optional. Use the raw option name (i.e. don't call get_name). Default false. + * + * @return bool True, if option is successfully deleted. False on failure. + */ + public function delete($option, $raw = \false) + { + return \delete_option($raw ? $option : $this->get_name($option)); + } + /** + * Retrieves the complete option name to use. + * + * @param string $option The option name (without the plugin-specific prefix). + * + * @return string + */ + public function get_name($option) + { + return "{$this->prefix}{$option}"; + } +} diff --git a/vendor-scoped/mundschenk-at/wp-data-storage/src/class-site-transients.php b/vendor-scoped/mundschenk-at/wp-data-storage/src/class-site-transients.php new file mode 100644 index 0000000..d98607f --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-data-storage/src/class-site-transients.php @@ -0,0 +1,96 @@ + + */ +class Site_Transients extends Transients +{ + const TRANSIENT_SQL_PREFIX = '_site_transient_'; + /** + * Retrieves a list of transients set by the plugin from the options table. + * + * @return string[] + */ + public function get_keys_from_database() + { + // If we are not running on multisite, fall back to the parent implementation. + if (!\is_multisite()) { + return parent::get_keys_from_database(); + } + /** + * WordPress database handler. + * + * @var \wpdb + */ + global $wpdb; + $results = $wpdb->get_results($wpdb->prepare("SELECT meta_key FROM {$wpdb->sitemeta} WHERE meta_key like %s and site_id = %d", self::TRANSIENT_SQL_PREFIX . "{$this->get_prefix()}%", \get_current_network_id()), \ARRAY_A); + // WPCS: db call ok, cache ok. + return \str_replace(self::TRANSIENT_SQL_PREFIX, '', \wp_list_pluck($results, 'meta_key')); + } + /** + * Retrieves a cached value. + * + * @param string $key The cache key. + * @param bool $raw Optional. Use the raw key name (i.e. don't call get_key). Default false. + * + * @return mixed + */ + public function get($key, $raw = \false) + { + return \get_site_transient($raw ? $key : $this->get_key($key)); + } + /** + * Sets an entry in the cache and stores the key. + * + * @param string $key The cache key. + * @param mixed $value The value to store. + * @param int $duration Optional. The duration in seconds. Default 0 (no expiration). + * @param bool $raw Optional. Use the raw key name (i.e. don't call get_key). Default false. + * + * @return bool True if the cache could be set successfully. + */ + public function set($key, $value, $duration = 0, $raw = \false) + { + return \set_site_transient($raw ? $key : $this->get_key($key), $value, $duration); + } + /** + * Deletes an entry from the cache. + * + * @param string $key The cache key root. + * @param bool $raw Optional. Use the raw key name (i.e. don't call get_key). Default false. + * + * @return bool True on successful removal, false on failure. + */ + public function delete($key, $raw = \false) + { + return \delete_site_transient($raw ? $key : $this->get_key($key)); + } +} diff --git a/vendor-scoped/mundschenk-at/wp-data-storage/src/class-transients.php b/vendor-scoped/mundschenk-at/wp-data-storage/src/class-transients.php new file mode 100644 index 0000000..be9ecc2 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-data-storage/src/class-transients.php @@ -0,0 +1,183 @@ + + */ +class Transients extends Abstract_Cache +{ + const TRANSIENT_SQL_PREFIX = '_transient_'; + /** + * The incrementor transient key. + * + * @var string + */ + protected $incrementor_key; + /** + * Create new cache instance. + * + * @param string $prefix The prefix automatically added to transient names. + */ + public function __construct($prefix) + { + $this->incrementor_key = $prefix . 'transients_incrementor'; + $this->incrementor = $this->get($this->incrementor_key, \true); + parent::__construct($prefix); + } + /** + * Invalidate all cached elements by reseting the incrementor. + */ + public function invalidate() + { + if (!\wp_using_ext_object_cache()) { + // Clean up old transients. + foreach ($this->get_keys_from_database() as $old_transient) { + $this->delete($old_transient, \true); + } + } + // Update incrementor. + $this->incrementor = \time(); + $this->set($this->incrementor_key, $this->incrementor, 0, \true); + } + /** + * Retrieves a list of transients set by the plugin from the options table. + * + * @return string[] + */ + public function get_keys_from_database() + { + /** + * WordPress database handler. + * + * @var \wpdb + */ + global $wpdb; + $results = $wpdb->get_results($wpdb->prepare("SELECT option_name FROM {$wpdb->options} WHERE option_name like %s", static::TRANSIENT_SQL_PREFIX . "{$this->get_prefix()}%"), \ARRAY_A); + // WPCS: db call ok, cache ok. + return \str_replace(static::TRANSIENT_SQL_PREFIX, '', \wp_list_pluck($results, 'option_name')); + } + /** + * Retrieves a cached value. + * + * @param string $key The cache key. + * @param bool $raw Optional. Use the raw key name (i.e. don't call get_key). Default false. + * + * @return mixed + */ + public function get($key, $raw = \false) + { + return \get_transient($raw ? $key : $this->get_key($key)); + } + /** + * Retrieves a cached large object. + * + * @param string $key The cache key. + * + * @return mixed + */ + public function get_large_object($key) + { + $encoded = $this->get($key); + if (\false === $encoded) { + return \false; + } + $uncompressed = @\gzdecode(\base64_decode($encoded)); + // @codingStandardsIgnoreLine + if (\false === $uncompressed) { + return \false; + } + return $this->maybe_fix_object(\unserialize($uncompressed)); + // @codingStandardsIgnoreLine + } + /** + * Sets an entry in the cache and stores the key. + * + * @param string $key The cache key. + * @param mixed $value The value to store. + * @param int $duration Optional. The duration in seconds. Default 0 (no expiration). + * @param bool $raw Optional. Use the raw key name (i.e. don't call get_key). Default false. + * + * @return bool True if the cache could be set successfully. + */ + public function set($key, $value, $duration = 0, $raw = \false) + { + return \set_transient($raw ? $key : $this->get_key($key), $value, $duration); + } + /** + * Sets a transient for a large PHP object. The object will be stored in + * serialized and gzip encoded form using Base64 encoding to ensure binary safety. + * + * @param string $key The cache key. + * @param mixed $value The value to store. + * @param int $duration Optional. The duration in seconds. Default 0 (no expiration). + * + * @return bool True if the cache could be set successfully. + */ + public function set_large_object($key, $value, $duration = 0) + { + $compressed = \gzencode(\serialize($value)); + // @codingStandardsIgnoreLine + if (\false === $compressed) { + return \false; + // @codeCoverageIgnore + } + return $this->set($key, \base64_encode($compressed), $duration); + } + /** + * Deletes an entry from the cache. + * + * @param string $key The cache key root. + * @param bool $raw Optional. Use the raw key name (i.e. don't call get_key). Default false. + * + * @return bool True on successful removal, false on failure. + */ + public function delete($key, $raw = \false) + { + return \delete_transient($raw ? $key : $this->get_key($key)); + } + /** + * Tries to fix object cache implementations sometimes returning __PHP_Incomplete_Class. + * + * Originally based on http://stackoverflow.com/a/1173769/6646342 and refactored + * for PHP 7.2 compatibility. + * + * @param object $object An object that should have been unserialized, but may be of __PHP_Incomplete_Class. + * + * @return object The object with its real class. + */ + protected function maybe_fix_object($object) + { + if ('__PHP_Incomplete_Class' === \get_class($object)) { + $object = \unserialize(\serialize($object)); + // phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize,WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + } + return $object; + } +} diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/LICENSE b/vendor-scoped/mundschenk-at/wp-settings-ui/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/README.md b/vendor-scoped/mundschenk-at/wp-settings-ui/README.md new file mode 100644 index 0000000..36c0e37 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/README.md @@ -0,0 +1,17 @@ +# WordPress Settings UI + +A library providing an object-oriented interface to the WordPress Settings API. + +## Features + +* A WordPress plugin's settings page can be specified as an array of textual properties. +* A plugin can provide additional custom controls by implementing the `Mundschenk\UI\Control` interface. +* Standard controls included with the library: + - Checkbox (`Mundschenk\UI\Controls\Checkbox_Input`), + - Text field (`Mundschenk\UI\Controls\Text_Input`), + - Number field (`Mundschenk\UI\Controls\Number_Input`), + - Hidden field (`Mundschenk\UI\Controls\Hidden_Input`), + - Submit button (`Mundschenk\UI\Controls\Submit_Input`), + - Select box (`Mundschenk\UI\Controls\Select`), + - Plain text (`Mundschenk\UI\Controls\Display_Text`), and + - Text Area (`Mundschenk\UI\Controls\Textarea`). diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/partials/control.php b/vendor-scoped/mundschenk-at/wp-settings-ui/partials/control.php new file mode 100644 index 0000000..f75a719 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/partials/control.php @@ -0,0 +1,65 @@ +get_outer_html_attributes(); // These are already escaped. +$outer_attributes = empty( $outer_attributes ) ? '' : " {$outer_attributes}"; + +$control_id = $this->get_id(); + +?> +grouped_controls ) ) : ?> + > + short ); ?> + + > + + +label ) ) : ?> + +has_inline_help() ) : ?> + +help_text ) ) : ?> +

help_text, self::ALLOWED_DESCRIPTION_HTML ); ?>

+ + +grouped_controls ) ) : ?> + grouped_controls as $control ) : ?> +
+ render(); ?> + + + + + +` etc.). + * + * @var array { + * Attribute/value pairs. + * + * string $attr Attribute value. + * } + */ + protected $attributes; + /** + * Additional HTML attributes to add to the outer element (either `
` or `
`). + * + * @var array { + * Attribute/value pairs. + * + * string $attr Attribute value. + * } + */ + protected $outer_attributes; + /** + * Grouped controls. + * + * @var array { + * An array of Controls. + * + * Control $control Grouped control. + * } + */ + protected $grouped_controls = []; + /** + * The Control this one is grouped with. + * + * @var Control|null + */ + protected $grouped_with = null; + /** + * An abstraction of the WordPress Options API. + * + * @var Options + */ + protected $options; + /** + * The base path for includes. + * + * @var string + */ + protected $base_path; + /** + * The options key. + * + * @var string + */ + protected $options_key; + /** + * Additional arguments passed to the `add_settings_field` function. + * + * @var array { + * Attribute/value pairs. + * + * string $attr Attribute value. + * } + */ + protected $settings_args; + /** + * A sanitiziation callback. + * + * @var callable|null + */ + protected $sanitize_callback; + const ALLOWED_INPUT_ATTRIBUTES = ['id' => [], 'name' => [], 'value' => [], 'checked' => [], 'type' => [], 'class' => [], 'aria-describedby' => []]; + const ALLOWED_HTML = ['span' => ['class' => []], 'input' => self::ALLOWED_INPUT_ATTRIBUTES, 'select' => self::ALLOWED_INPUT_ATTRIBUTES, 'option' => ['value' => [], 'selected' => []], 'code' => [], 'strong' => [], 'em' => [], 'sub' => [], 'sup' => [], 'br' => []]; + const ALLOWED_DESCRIPTION_HTML = ['code' => [], 'strong' => [], 'em' => [], 'sub' => [], 'sup' => [], 'br' => [], 'span' => ['class' => []]]; + /** + * Create a new UI control object. + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @param string $id Control ID (equivalent to option name). Required. + * @param string $tab_id Tab ID. Required. + * @param string $section Section ID. Required. + * @param string|int $default The default value. Required, but may be an empty string. + * @param string|null $short Optional. Short label. Default null. + * @param string|null $label Optional. Label content with the position of the control marked as %1$s. Default null. + * @param string|null $help_text Optional. Help text. Default null. + * @param bool $inline_help Optional. Display help inline. Default false. + * @param array $attributes Optional. Attributes for the main element of the control. Default []. + * @param array $outer_attributes Optional. Attributes for the outer element (´
` or `
`) of the control. Default []. + * @param array $settings_args Optional. Arguments passed to `add_settings_Field`. Default []. + * @param callable|null $sanitize_callback Optional. A callback to sanitize $_POST data. Default null. + */ + protected function __construct(Options $options, $options_key, $id, $tab_id, $section, $default, $short = null, $label = null, $help_text = null, $inline_help = \false, array $attributes = [], array $outer_attributes = [], $settings_args = [], $sanitize_callback = null) + { + $this->options = $options; + $this->options_key = $options_key; + $this->id = $id; + $this->tab_id = $tab_id; + $this->section = $section; + $this->short = $short ?: ''; + $this->label = $label; + $this->help_text = $help_text; + $this->inline_help = $inline_help; + $this->default = $default; + $this->attributes = $attributes; + $this->outer_attributes = $outer_attributes; + $this->settings_args = $settings_args; + $this->sanitize_callback = $sanitize_callback; + $this->base_path = \dirname(\dirname(__DIR__)); + } + /** + * Prepares keyowrd arguments passed via an array for usage. + * + * @param array $args Arguments. + * @param array $required Required argument names. 'tab_id' is always required. + * + * @return array + * + * @throws \InvalidArgumentException Thrown when a required argument is missing. + */ + protected function prepare_args(array $args, array $required) + { + // Check for required arguments. + $required = \wp_parse_args($required, ['tab_id']); + foreach ($required as $property) { + if (!isset($args[$property])) { + throw new \InvalidArgumentException("Missing argument '{$property}'."); + } + } + // Add default arguments. + $defaults = ['section' => $args['tab_id'], 'short' => null, 'label' => null, 'help_text' => null, 'inline_help' => \false, 'attributes' => [], 'outer_attributes' => [], 'settings_args' => [], 'sanitize_callback' => null]; + $args = \wp_parse_args($args, $defaults); + return $args; + } + /** + * Retrieve the current value for the control. + * May be overridden by subclasses. + * + * @return mixed + */ + public function get_value() + { + $key = $this->options_key ?: $this->id; + $options = $this->options->get($key); + if ($key === $this->id) { + return $options; + } elseif (isset($options[$this->id])) { + return $options[$this->id]; + } else { + return null; + } + } + /** + * Renders control-specific HTML. + * + * @return void + */ + protected function render_element() + { + echo $this->get_element_markup(); + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + /** + * Retrieves the control-specific HTML markup. + * + * @return string + */ + protected abstract function get_element_markup(); + /** + * Render the HTML representation of the control. + */ + public function render() + { + require $this->base_path . '/partials/control.php'; + } + /** + * Retrieves additional HTML attributes as a string ready for inclusion in markup. + * + * @param array $attributes Required. + * + * @return string + */ + protected function get_html_attributes(array $attributes) + { + $html_attributes = ''; + if (!empty($attributes)) { + foreach ($attributes as $attr => $val) { + $html_attributes .= \esc_attr($attr) . '="' . \esc_attr($val) . '" '; + } + } + return $html_attributes; + } + /** + * Retrieves additional HTML attributes for the inner element as a string + * ready for inclusion in markup. + * + * @return string + */ + protected function get_inner_html_attributes() + { + return $this->get_html_attributes($this->attributes); + } + /** + * Retrieves additional HTML attributes for the outer element as a string + * ready for inclusion in markup. + * + * @return string + */ + protected function get_outer_html_attributes() + { + return $this->get_html_attributes($this->outer_attributes); + } + /** + * Retrieve default value. + * + * @return string|int + */ + public function get_default() + { + return $this->default; + } + /** + * Retrieve control ID. + * + * @return string + */ + public function get_id() + { + if (!empty($this->options_key)) { + return "{$this->options->get_name($this->options_key)}[{$this->id}]"; + } else { + return "{$this->options->get_name($this->id)}"; + } + } + /** + * Retrieves the markup for ID, name and class(es). + * Also adds additional attributes if they are set. + * + * @return string + */ + protected function get_id_and_class_markup() + { + $id = \esc_attr($this->get_id()); + $aria = !empty($this->help_text) ? " aria-describedby=\"{$id}-description\"" : ''; + // Set default ID & name, no class (except for submit buttons). + return "id=\"{$id}\" name=\"{$id}\" {$this->get_inner_html_attributes()}{$aria}"; + } + /** + * Determines if the label contains a placeholder for the actual control element(s). + * + * @return bool + */ + protected function label_has_placeholder() + { + return \false !== \strpos($this->label, '%1$s'); + } + /** + * Determines if this control has an inline help text to display. + * + * @return bool + */ + protected function has_inline_help() + { + return $this->inline_help && !empty($this->help_text); + } + /** + * Retrieves the label. If the label text contains a string placeholder, it + * is replaced by the control element markup. + * + * @var string + */ + public function get_label() + { + if ($this->label_has_placeholder()) { + return \sprintf($this->label, $this->get_element_markup()); + } else { + return $this->label; + } + } + /** + * Register the control with the settings API. + * + * @param string $option_group Application-specific prefix. + */ + public function register($option_group) + { + // Register rendering callbacks only for non-grouped controls. + if (empty($this->grouped_with)) { + \add_settings_field($this->get_id(), $this->short, [$this, 'render'], $option_group . $this->tab_id, $this->section, $this->settings_args); + } + } + /** + * Group another control with this one. + * + * @param Control $control Any control. + */ + public function add_grouped_control(Control $control) + { + // Prevent self-references. + if ($this !== $control) { + $this->grouped_controls[] = $control; + $control->group_with($this); + } + } + /** + * Registers this control as grouped with another one. + * + * @param Control $control Any control. + */ + public function group_with(Control $control) + { + // Prevent self-references. + if ($this !== $control) { + $this->grouped_with = $control; + } + } + /** + * Sanitizes an option value. + * + * @param mixed $value The unslashed post variable. + * + * @return mixed The sanitized value. + */ + public function sanitize($value) + { + $sanitize = $this->sanitize_callback; + if (\is_callable($sanitize)) { + return $sanitize($value); + } + return $value; + } +} diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/class-control-factory.php b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/class-control-factory.php new file mode 100644 index 0000000..a3e8cc7 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/class-control-factory.php @@ -0,0 +1,76 @@ + $control_info) { + $controls[$control_id] = $control_info['ui']::create($options, $options_key, $control_id, $control_info); + if (!empty($control_info['grouped_with'])) { + $groups[$control_info['grouped_with']][] = $control_id; + } + } + // Group controls. + foreach ($groups as $group => $control_ids) { + foreach ($control_ids as $control_id) { + $controls[$group]->add_grouped_control($controls[$control_id]); + } + } + return $controls; + } +} diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/class-control.php b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/class-control.php new file mode 100644 index 0000000..120c691 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/class-control.php @@ -0,0 +1,116 @@ + element. + */ +class Checkbox_Input extends Input +{ + /** + * Create a new input control object. + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @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 Optional. Section ID. Default Tab ID. + * @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 [], + * } + * + * @throws \InvalidArgumentException Missing argument. + */ + public function __construct(Options $options, $options_key, $id, array $args) + { + $args['input_type'] = 'checkbox'; + $args['sanitize_callback'] = 'boolval'; + parent::__construct($options, $options_key, $id, $args); + } + /** + * Retrieves the value markup for this input. + * + * @param mixed $value The input value. + * + * @return string + */ + protected function get_value_markup($value) + { + return 'value="1" ' . \checked($value, \true, \false); + } +} diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-display-text.php b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-display-text.php new file mode 100644 index 0000000..71e22c9 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-display-text.php @@ -0,0 +1,121 @@ + [], 'name' => [], 'class' => [], 'aria-describedby' => []]; + const ALLOWED_HTML = ['div' => self::ALLOWED_ATTRIBUTES, 'span' => self::ALLOWED_ATTRIBUTES, 'p' => self::ALLOWED_ATTRIBUTES, 'ul' => self::ALLOWED_ATTRIBUTES, 'ol' => self::ALLOWED_ATTRIBUTES, 'li' => self::ALLOWED_ATTRIBUTES, 'a' => ['class' => [], 'href' => [], 'rel' => [], 'target' => []], 'code' => [], 'strong' => [], 'em' => [], 'sub' => [], 'sup' => []]; + /** + * The HTML elements to display. + * + * @var string[] + */ + protected $elements; + /** + * Create a new input control object. + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @param string $id Control ID (equivalent to option name). Required. + * @param array $args { + * Optional and required arguments. + * + * @type string $input_type HTML input type ('checkbox' etc.). Required. + * @type string $tab_id Tab ID. Required. + * @type string $section Optional. Section ID. Default Tab ID. + * @type array $elements The HTML elements to display (including the outer tag). Required. + * @type string|null $short Optional. Short label. 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 [], + * } + */ + protected function __construct(Options $options, $options_key, $id, array $args) + { + $args = $this->prepare_args($args, ['elements']); + $this->elements = $args['elements']; + $sanitize = function () { + return ''; + }; + parent::__construct($options, $options_key, $id, $args['tab_id'], $args['section'], '', $args['short'], null, $args['help_text'], $args['inline_help'], $args['attributes'], $args['outer_attributes'], $args['settings_args'], $sanitize); + } + /** + * Retrieves the current value for the control. In this case, the method always returns ''. + * + * @return string + */ + public function get_value() + { + return ''; + } + /** + * Retrieves the control-specific HTML markup. + * + * @var string + */ + protected function get_element_markup() + { + return \wp_kses(\implode('', $this->elements), self::ALLOWED_HTML); + } + /** + * Creates a new input control, provided the concrete subclass constructors follow + * this methods signature. + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @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 array $option_values The allowed values. Required. + * @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 [], + * } + * + * @return Control + * + * @throws \InvalidArgumentException Missing argument. + */ + public static function create(Options $options, $options_key, $id, array $args) + { + return new static($options, $options_key, $id, $args); + } +} diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-hidden-input.php b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-hidden-input.php new file mode 100644 index 0000000..52d5d23 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-hidden-input.php @@ -0,0 +1,63 @@ + element. + */ +class Hidden_Input extends Input +{ + /** + * Create a new input control object. + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @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 Optional. Section ID. Default Tab ID. + * @type string|int $default The default value. Required, but may be an empty string. + * @type string|null $short Optional. Short label. Default null. + * @type array $attributes Optional. Default [], + * @type array $outer_attributes Optional. Default [], + * } + * + * @throws \InvalidArgumentException Missing argument. + */ + public function __construct(Options $options, $options_key, $id, array $args) + { + $args['input_type'] = 'hidden'; + $args['label'] = null; + $args['help_text'] = null; + $args['inline_help'] = \false; + parent::__construct($options, $options_key, $id, $args); + } +} diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-input.php b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-input.php new file mode 100644 index 0000000..819ca15 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-input.php @@ -0,0 +1,121 @@ + element. + */ +abstract class Input extends Abstract_Control +{ + /** + * The input type ('checkbox', ...). + * + * @var string + */ + protected $input_type; + /** + * Create a new input control object. + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @param string $id Control ID (equivalent to option name). Required. + * @param array $args { + * Optional and required arguments. + * + * @type string $input_type HTML input type ('checkbox' etc.). Required. + * @type string $tab_id Tab ID. Required. + * @type string $section Optional. Section ID. Default Tab ID. + * @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 [], + * } + */ + protected function __construct(Options $options, $options_key, $id, array $args) + { + $args = $this->prepare_args($args, ['input_type', 'tab_id', 'default']); + $this->input_type = $args['input_type']; + $sanitize = isset($args['sanitize_callback']) ? $args['sanitize_callback'] : 'sanitize_text_field'; + parent::__construct($options, $options_key, $id, $args['tab_id'], $args['section'], $args['default'], $args['short'], $args['label'], $args['help_text'], $args['inline_help'], $args['attributes'], $args['outer_attributes'], $args['settings_args'], $sanitize); + } + /** + * Retrieves the value markup for this input. + * + * @param mixed $value The input value. + * + * @return string + */ + protected function get_value_markup($value) + { + return $value ? 'value="' . \esc_attr($value) . '" ' : ''; + } + /** + * Retrieves the control-specific HTML markup. + * + * @var string + */ + protected function get_element_markup() + { + return 'get_id_and_class_markup()} {$this->get_value_markup($this->get_value())}/>"; + } + /** + * Creates a new input control, provided the concrete subclass constructors follow + * this methods signature. + * + * @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 array $option_values The allowed values. Required. + * @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 [], + * } + * + * @return Control + * + * @throws \InvalidArgumentException Missing argument. + */ + public static function create(Options $options, $options_key, $id, array $args) + { + return new static($options, $options_key, $id, $args); + } +} diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-number-input.php b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-number-input.php new file mode 100644 index 0000000..fc079b3 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-number-input.php @@ -0,0 +1,78 @@ + element. + */ +class Number_Input extends Input +{ + /** + * Create a new input control object. + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @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 Optional. Section ID. Default Tab ID. + * @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 [], + * } + * + * @throws \InvalidArgumentException Missing argument. + */ + public function __construct(Options $options, $options_key, $id, array $args) + { + $args['input_type'] = 'number'; + $args['sanitize_callback'] = function ($value) { + return $value + 0; + }; + parent::__construct($options, $options_key, $id, $args); + } + /** + * Render the value markup for this input. + * + * @param mixed $value The input value. + * + * @return string + */ + protected function get_value_markup($value) + { + // Include 0 values. + return 'value="' . \esc_attr($value) . '" '; + } +} diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-select.php b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-select.php new file mode 100644 index 0000000..ff40821 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-select.php @@ -0,0 +1,152 @@ + element. + */ +class Select extends Abstract_Control +{ + /** + * The selectable values. + * + * @var array + */ + protected $option_values; + /** + * Create a new select control object. + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @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 Optional. Section ID. Default Tab ID. + * @type string|int $default The default value. Required, but may be an empty string. + * @type array $option_values The allowed values. Required. + * @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 [], + * } + * + * @throws \InvalidArgumentException Missing argument. + */ + public function __construct(Options $options, $options_key, $id, array $args) + { + $args = $this->prepare_args($args, ['tab_id', 'default', 'option_values']); + $sanitize = $args['sanitize_callback'] ?: 'sanitize_text_field'; + $this->option_values = $args['option_values']; + parent::__construct($options, $options_key, $id, $args['tab_id'], $args['section'], $args['default'], $args['short'], $args['label'], $args['help_text'], $args['inline_help'], $args['attributes'], $args['outer_attributes'], $args['settings_args'], $sanitize); + } + /** + * Set selectable options. + * + * @param array $option_values An array of VALUE => DISPLAY. + */ + public function set_option_values(array $option_values) + { + $this->option_values = $option_values; + } + /** + * Retrieve the current value for the select control. + * + * @return mixed + */ + public function get_value() + { + $config = $this->options->get($this->options_key); + $value = $config[$this->id]; + // Make sure $value is in $option_values if $option_values is set. + if (isset($this->option_values) && !isset($this->option_values[$value])) { + $value = null; + } + return $value; + } + /** + * Retrieves the control-specific HTML markup. + * + * @return string + */ + protected function get_element_markup() + { + $select_markup = "'; + return $select_markup; + } + /** + * Sanitizes an option value. + * + * @param mixed $value The unslashed post variable. + * + * @return string The sanitized value. + */ + public function sanitize_value($value) + { + return \sanitize_text_field($value); + } + /** + * Creates a new select control + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @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 array $option_values The allowed values. Required. + * @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 [], + * } + * + * @return Control + * + * @throws \InvalidArgumentException Missing argument. + */ + public static function create(Options $options, $options_key, $id, array $args) + { + return new static($options, $options_key, $id, $args); + } +} diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-submit-input.php b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-submit-input.php new file mode 100644 index 0000000..3ecd37e --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-submit-input.php @@ -0,0 +1,100 @@ + element. + */ +class Submit_Input extends Input +{ + /** + * Optional HTML class for buttons. + * + * @var string + */ + protected $button_class; + /** + * Optional button label. + * + * @var string + */ + protected $button_label; + /** + * Create a new input control object. + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @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 Optional. Section ID. Default Tab ID. + * @type string|int $default The default value. Required, but may be an empty string. + * @type string $button_class Required. + * @type string|null $short Optional. Short label. Default null. + * @type string|null $label Optional. The actual button label. Default null (browser dependant). + * @type array $attributes Optional. Default [], + * } + * + * @throws \InvalidArgumentException Missing argument. + */ + public function __construct(Options $options, $options_key, $id, array $args) + { + // Ensure that there is a button class argument. + $args = $this->prepare_args($args, ['button_class']); + // Ensure proper button label handling. + $this->button_label = $args['label']; + $args['label'] = null; + // Force these addtional arguments. + $args['input_type'] = 'submit'; + // Store button class attribute. + $this->button_class = $args['button_class']; + // Call parent. + parent::__construct($options, $options_key, $id, $args); + } + /** + * Retrieve the current button name. + * + * @return string + */ + public function get_value() + { + return $this->button_label; + } + /** + * Markup ID and class(es). + * + * @return string + */ + protected function get_id_and_class_markup() + { + return parent::get_id_and_class_markup() . ' class="' . \esc_attr($this->button_class) . '"'; + } +} diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-text-input.php b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-text-input.php new file mode 100644 index 0000000..8f0901c --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-text-input.php @@ -0,0 +1,63 @@ + element. + */ +class Text_Input extends Input +{ + /** + * Create a new input control object. + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @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 Optional. Section ID. Default Tab ID. + * @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 [], + * } + * + * @throws \InvalidArgumentException Missing argument. + */ + public function __construct(Options $options, $options_key, $id, array $args) + { + $args['input_type'] = 'text'; + parent::__construct($options, $options_key, $id, $args); + } +} diff --git a/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-textarea.php b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-textarea.php new file mode 100644 index 0000000..484e985 --- /dev/null +++ b/vendor-scoped/mundschenk-at/wp-settings-ui/src/ui/controls/class-textarea.php @@ -0,0 +1,104 @@ + element. + */ +class Textarea extends Abstract_Control +{ + /** + * Create a new textarea control object. + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @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 Optional. Section ID. Default Tab ID. + * @type string|int $default The default value. Required, but may be an empty string. + * @type string $short Optional. Short label. + * @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 array $attributes Optional. Default [], + * @type array $outer_attributes Optional. Default [], + * @type array $settings_args Optional. Default [], + * } + * + * @throws \InvalidArgumentException Missing argument. + */ + public function __construct(Options $options, $options_key, $id, array $args) + { + $args = $this->prepare_args($args, ['tab_id', 'default']); + $sanitize = 'sanitize_textarea_field'; + parent::__construct($options, $options_key, $id, $args['tab_id'], $args['section'], $args['default'], $args['short'], $args['label'], $args['help_text'], \false, $args['attributes'], $args['outer_attributes'], $args['settings_args'], $sanitize); + } + /** + * Retrieves the control-specific HTML markup. + * + * @var string + */ + protected function get_element_markup() + { + $value = $this->get_value(); + $value = !empty($value) ? \esc_textarea($value) : ''; + return ""; + } + /** + * Creates a new textarea control + * + * @param Options $options Options API handler. + * @param string $options_key Database key for the options array. Passing '' means that the control ID is used instead. + * @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 array $option_values The allowed values. Required. + * @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 [], + * } + * + * @return Control + * + * @throws \InvalidArgumentException Missing argument. + */ + public static function create(Options $options, $options_key, $id, array $args) + { + return new static($options, $options_key, $id, $args); + } +} diff --git a/vendor-scoped/splitbrain/php-ringicon/LICENSE b/vendor-scoped/splitbrain/php-ringicon/LICENSE new file mode 100644 index 0000000..92400b1 --- /dev/null +++ b/vendor-scoped/splitbrain/php-ringicon/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016 Andreas Gohr + +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. \ No newline at end of file diff --git a/vendor-scoped/splitbrain/php-ringicon/README.md b/vendor-scoped/splitbrain/php-ringicon/README.md new file mode 100644 index 0000000..62dbad9 --- /dev/null +++ b/vendor-scoped/splitbrain/php-ringicon/README.md @@ -0,0 +1,24 @@ +RingIcon - A Indenticon/Glyphicon Library +========================================= + +This minimal library generates identifiers based on an input seed. It can be +used to generate avatar images or visualize other identifying info (like +crypto keys). The result is a PNG or SVG image. + +It has no dependencies except the GD library extension for PHP for PNG images. + +![Multicolor with 3 Rings](sample.png) +![Monochrome with 5 Rings](mono.png) + +Usage +----- + +See the ``demo.html``/``demo.php`` files for examples. + +You can tune the following parameters: + +- The size of the resulting image (via constructor) +- The number of rings to generate (via constructor) +- Multicolor versus monochrome image (via ``setMono()`` method) + +The monochrome version uses a single color and variates the alpha transparency of the rings. diff --git a/vendor-scoped/splitbrain/php-ringicon/src/AbstractRingIcon.php b/vendor-scoped/splitbrain/php-ringicon/src/AbstractRingIcon.php new file mode 100644 index 0000000..e99322d --- /dev/null +++ b/vendor-scoped/splitbrain/php-ringicon/src/AbstractRingIcon.php @@ -0,0 +1,69 @@ + + * @license MIT + * @package splitbrain\RingIcon + */ +abstract class AbstractRingIcon +{ + protected $size; + protected $fullsize; + protected $rings; + protected $center; + protected $ringwidth; + protected $seed; + protected $ismono = \false; + protected $monocolor = null; + /** + * RingIcon constructor. + * @param int $size width and height of the resulting image + * @param int $rings number of rings + */ + public function __construct($size, $rings = 3) + { + $this->size = $size; + $this->fullsize = $this->size * 5; + $this->rings = $rings; + $this->center = \floor($this->fullsize / 2); + $this->ringwidth = \floor($this->fullsize / $rings); + $this->seed = \mt_rand() . \time(); + } + /** + * When set to true a monochrome version is returned + * + * @param bool $ismono + */ + public function setMono($ismono) + { + $this->ismono = $ismono; + } + /** + * Generate number from seed + * + * Each call runs MD5 on the seed again + * + * @param int $min + * @param int $max + * @return int + */ + protected function rand($min, $max) + { + $this->seed = \md5($this->seed); + $rand = \hexdec(\substr($this->seed, 0, 8)); + return $rand % ($max - $min + 1) + $min; + } + /** + * Set a fixed color, which to be later only varied within its alpha channel + */ + protected function generateMonoColor() + { + $this->monocolor = array($this->rand(20, 255), $this->rand(20, 255), $this->rand(20, 255)); + } +} diff --git a/vendor-scoped/splitbrain/php-ringicon/src/RingIconSVG.php b/vendor-scoped/splitbrain/php-ringicon/src/RingIconSVG.php new file mode 100644 index 0000000..c65c98c --- /dev/null +++ b/vendor-scoped/splitbrain/php-ringicon/src/RingIconSVG.php @@ -0,0 +1,145 @@ +center = \floor($this->size / 2); + $this->ringwidth = \floor($this->size / $rings); + } + /** + * Generates an ring image svg suitable for inlining in html + * + * If a seed is given, the image will be based on that seed + * + * @param string $seed + * + * @return string + */ + public function getInlineSVG($seed = '') + { + return $this->generateSVGImage($seed); + } + /** + * Generates an ring image svg + * + * If a seed is given, the image will be based on that seed + * + * @param string $seed initialize the genrator with this string + * @param string $file if given, the image is saved at that path, otherwise is printed as file to browser + */ + public function createImage($seed = '', $file = '') + { + $svg = $this->generateSVGImage($seed, \true); + if ($file) { + \file_put_contents($file, $svg); + } else { + \header("Content-type: image/svg+xml"); + echo $svg; + } + } + /** + * Generates an ring image svg + * + * If a seed is given, the image will be based on that seed + * + * @param string $seed initialize the genrator with this string + * @param bool $file if true, the svg will have the markup suitable for being delivered as file + * + * @return string + */ + protected function generateSVGImage($seed = '', $file = \false) + { + if (!$seed) { + $seed = \mt_rand() . \time(); + } + $this->seed = $seed; + // monochrome wanted? + if ($this->ismono) { + $this->generateMonoColor(); + } + if ($file) { + $svgFileAttributes = array('xmlns' => 'http://www.w3.org/2000/svg', 'version' => '1.1', 'width' => $this->size, 'height' => $this->size); + $svgFileAttributes = \implode(' ', \array_map(function ($k, $v) { + return $k . '="' . \htmlspecialchars($v) . '"'; + }, \array_keys($svgFileAttributes), $svgFileAttributes)); + } else { + $svgFileAttributes = ''; + } + $svg = "size} {$this->size}\">"; + $arcOuterRadious = $this->size / 2; + for ($i = $this->rings; $i > 0; $i--) { + $svg .= $this->createSVGArcPath($arcOuterRadious); + $arcOuterRadious -= $this->ringwidth / 2; + } + $svg .= ''; + if ($file) { + $svgBoilerplate = ' +'; + $svg = $svgBoilerplate . $svg; + } + return $svg; + } + /** + * Draw a single arc + * + * @param float $outerRadius + * + * @return string + */ + protected function createSVGArcPath($outerRadius) + { + $color = $this->randomCssColor(); + $ringThickness = $this->ringwidth / 2; + $startAngle = $this->rand(20, 360); + $stopAngle = $this->rand(20, 360); + if ($stopAngle < $startAngle) { + list($startAngle, $stopAngle) = array($stopAngle, $startAngle); + } + list($xStart, $yStart) = $this->polarToCartesian($outerRadius, $startAngle); + list($xOuterEnd, $yOuterEnd) = $this->polarToCartesian($outerRadius, $stopAngle); + $innerRadius = $outerRadius - $ringThickness; + $SweepFlag = 0; + $innerSweepFlag = 1; + $largeArcFlag = (int) ($stopAngle - $startAngle < 180); + list($xInnerStart, $yInnerStart) = $this->polarToCartesian($outerRadius - $ringThickness, $stopAngle); + list($xInnerEnd, $yInnerEnd) = $this->polarToCartesian($outerRadius - $ringThickness, $startAngle); + $fullPath = ""; + return $fullPath; + } + /** + * Create a random valid css color value + * + * @return string + */ + protected function randomCssColor() + { + if ($this->ismono) { + $alpha = 1 - $this->rand(0, 96) / 100; + return "rgba({$this->monocolor[0]}, {$this->monocolor[1]}, {$this->monocolor[2]}, {$alpha})"; + } + $r = $this->rand(0, 255); + $g = $this->rand(0, 255); + $b = $this->rand(0, 255); + return "rgb({$r}, {$g}, {$b})"; + } + /** + * Calculate the x,y coordinate of a given angle at a given distance from the center + * + * 0 Degree is 3 o'clock + * + * @param float $radius + * @param float $angleInDegrees + * + * @return array + */ + protected function polarToCartesian($radius, $angleInDegrees) + { + $angleInRadians = $angleInDegrees * \M_PI / 180.0; + return array($this->size / 2 + $radius * \cos($angleInRadians), $this->size / 2 + $radius * \sin($angleInRadians)); + } +}