first commit

This commit is contained in:
Sophia Atkinson 2023-09-01 00:37:57 -07:00
commit 9caab8ce68
602 changed files with 33485 additions and 0 deletions

View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('wp-blocks', 'wp-components', 'wp-data', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-server-side-render'), 'version' => '2226b124181394cb78e7');

View File

@ -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<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{};t%2?w(Object(r),!0).forEach((function(t){n(e,t,r[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):w(Object(r)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(r,t))}))}return e}[t,r].forEach((function(e){if(e){var t=e.metadata,r=e.settings,a=e.name;(0,i.registerBlockType)(a,g(g({},t),r))}}))}();

View File

@ -0,0 +1,29 @@
{
"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
}
}
}

View File

@ -0,0 +1,101 @@
/**
* Avatar 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 <github@mundschenk.at>
* @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 (
<Fragment>
<InspectorControls>
<PanelBody title={ __( 'Avatar', 'avatar-privacy' ) }>
<PanelRow>
<SelectControl
label={ __( 'User', 'avatar-privacy' ) }
value={ attributes.user_id }
options={ users.map( ( user ) => ( {
label: user.name,
value: user.id,
} ) ) }
onChange={ ( newUser ) =>
setAttributes( {
user_id: parseInt( newUser ),
user: findUser( newUser ),
} )
}
/>
</PanelRow>
<PanelRow>
<RangeControl
label={ __( 'Avatar Size', 'avatar-privacy' ) }
value={ attributes.avatar_size }
initialPosition={ attributes.avatar_size }
onChange={ ( newSize ) =>
setAttributes( { avatar_size: newSize } )
}
min={ 48 }
max={ 240 }
/>
</PanelRow>
</PanelBody>
</InspectorControls>
<img
width={ attributes.avatar_size }
src={ attributes.user.avatar_urls[ 96 ] }
alt={ sprintf(
/* translators: user display name */
__( 'Avatar of %s', 'avatar-privacy' ),
attributes.user.name
) }
/>
</Fragment>
);
} );

View File

@ -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 <github@mundschenk.at>
* @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
},
};

View File

@ -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 <github@mundschenk.at>
* @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,
} );
} );

View File

@ -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
}
}
}

View File

@ -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 <github@mundschenk.at>
* @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 (
<Fragment>
<InspectorControls>
<PanelBody title={ __( 'Form', 'avatar-privacy' ) }>
<PanelRow>
<RangeControl
label={ __( 'Avatar Size', 'avatar-privacy' ) }
value={ attributes.avatar_size }
initialPosition={ attributes.avatar_size }
onChange={ ( newSize ) =>
setAttributes( { avatar_size: newSize } )
}
min={ 48 }
max={ 240 }
/>
</PanelRow>
<PanelRow>
<ToggleControl
label={ __(
'Show Descriptions',
'avatar-privacy'
) }
checked={ !! attributes.show_descriptions }
onChange={ () =>
setAttributes( {
show_descriptions:
! attributes.show_descriptions,
} )
}
/>
</PanelRow>
</PanelBody>
</InspectorControls>
<ServerSideRender
block="avatar-privacy/form"
attributes={ {
avatar_size: attributes.avatar_size,
show_descriptions: attributes.show_descriptions,
className,
preview: true,
} }
/>
</Fragment>
);
};

View File

@ -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 <github@mundschenk.at>
* @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
},
};

4
admin/css/blocks.css Normal file
View File

@ -0,0 +1,4 @@
.components-panel__row .components-range-control .components-base-control__label {
max-width: 100%;
}
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImJsb2Nrcy5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7RUFDRSxlQUFlO0FBQ2pCIiwiZmlsZSI6ImJsb2Nrcy5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyIuY29tcG9uZW50cy1wYW5lbF9fcm93IC5jb21wb25lbnRzLXJhbmdlLWNvbnRyb2wgLmNvbXBvbmVudHMtYmFzZS1jb250cm9sX19sYWJlbCB7XG4gIG1heC13aWR0aDogMTAwJTtcbn0iXX0= */

1
admin/css/blocks.min.css vendored Normal file
View File

@ -0,0 +1 @@
.components-panel__row .components-range-control .components-base-control__label{max-width:100%}

14
admin/css/settings.css Normal file
View File

@ -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 */

1
admin/css/settings.min.css vendored Normal file
View File

@ -0,0 +1 @@
.submit .aux-buttons{display:block;float:right}select,input{margin-top:-0.1em}input[type=number]{width:5em}

View File

@ -0,0 +1,30 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
if ( ! empty( $description ) ) : ?>
<p><?php echo \esc_html( $description ); ?></p>
<?php endif; ?>
<?php

View File

@ -0,0 +1,42 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
use Avatar_Privacy\Components\Network_Settings_Page;
?><div class='wrap'>
<h1><?php \esc_html_e( 'Avatar Privacy Network Settings', 'avatar-privacy' ); ?></h1>
<form method="post" action="<?php echo \esc_url( 'edit.php?action=' . Network_Settings_Page::ACTION ); ?>">
<?php \settings_fields( Network_Settings_Page::OPTION_GROUP ); ?>
<?php \do_settings_sections( Network_Settings_Page::OPTION_GROUP ); ?>
<p class="submit">
<?php \submit_button( \__( 'Save Changes', 'avatar-privacy' ), 'primary', 'save_changes', false, [ 'tabindex' => 1 ] ); ?>
</p><!-- .submit -->
</form>
</div><!-- .wrap -->
<?php

View File

@ -0,0 +1,53 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
/**
* Required template variables:
*
* @var string $nonce The nonce itself.
* @var string $action The nonce action.
* @var string $field_name The name of the checkbox `<input>` element.
* @var string $value The checkbox value.
*/
?>
<tr class="avatar-privacy-allow-anonymous">
<th scope="row"><?php \esc_html_e( 'Logged-out Commenting', 'avatar-privacy' ); ?></th>
<td>
<?php \wp_nonce_field( $action, $nonce ); ?>
<input
id="<?php echo \esc_attr( $field_name ); ?>"
name="<?php echo \esc_attr( $field_name ); ?>"
type="checkbox"
value="true"
<?php \checked( $value ); ?>
/>
<label for="<?php echo \esc_attr( $field_name ); ?>"><?php \esc_html_e( 'Allow logged-out comments with my profile picture.', 'avatar-privacy' ); ?></label><br />
<p class="description">
<?php \esc_html_e( 'Check this box if you want to be able to use your profile picture while logged-out.', 'avatar-privacy' ); ?>
</p>
</td>
</tr>
<?php

View File

@ -0,0 +1,58 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2021 Peter Putzer.
* Copyright 2012-2013 Johannes Freudendahl.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
use Avatar_Privacy\Tools\Template as T;
/**
* Required template variables:
*
* @var T $template The templating helper.
* @var string $nonce The nonce itself.
* @var string $action The nonce action.
* @var string $field_name The name of the checkbox `<input>` element.
* @var string $value The checkbox value.
*/
?>
<tr class="avatar-privacy-use-gravatar">
<th scope="row"><?php \esc_html_e( 'Gravatars', 'avatar-privacy' ); ?></th>
<td>
<?php \wp_nonce_field( $action, $nonce ); ?>
<input
id="<?php echo \esc_attr( $field_name ); ?>"
name="<?php echo \esc_attr( $field_name ); ?>"
type="checkbox"
value="true"
<?php \checked( $value ); ?>
/>
<label for="<?php echo \esc_attr( $field_name ); ?>"><?php echo \wp_kses( $template->get_use_gravatar_label( 'user' ), T::ALLOWED_HTML_LABEL ); ?></label><br />
<p class="description">
<?php \esc_html_e( "Uncheck this box if you don't want to display the gravatar for your e-mail address (or don't have an account on Gravatar.com).", 'avatar-privacy' ); ?>
<?php \esc_html_e( 'This setting will only take effect if you have not uploaded a local profile picture.', 'avatar-privacy' ); ?>
</p>
</td>
</tr>
<?php

View File

@ -0,0 +1,69 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2021 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
use Avatar_Privacy\Core;
use Avatar_Privacy\Tools\Template as T;
/**
* Required template variables:
*
* @var int $user_id The ID of the edited user.
* @var T $template The templating helper.
* @var string $nonce The nonce itself.
* @var string $action The nonce action.
* @var string $upload_field The name of the uploader `<input>` element.
* @var string $erase_field The name of the erase checkbox `<input>` 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.
*/
?>
<tr class="avatar-privacy-user-avatar-upload">
<th scope="row"><?php \esc_html_e( 'Profile Picture', 'avatar-privacy' ); ?></th>
<td>
<?php echo \get_avatar( $user_id, $size, '', '', [ 'upload_timestamp' => true ] ); ?>
<?php if ( $can_upload ) : ?>
<p class="avatar-privacy-upload-fields">
<?php \wp_nonce_field( $action, $nonce ); ?>
<input type="file" id="<?php echo \esc_attr( $upload_field ); ?>" name="<?php echo \esc_attr( $upload_field ); ?>" accept="image/*" />
<?php if ( $has_local_avatar ) : ?>
<input type="checkbox" id="<?php echo \esc_attr( $erase_field ); ?>" name="<?php echo \esc_attr( $erase_field ); ?>" value="true" />
<label for="<?php echo \esc_attr( $erase_field ); ?>"><?php \esc_html_e( 'Delete local avatar picture.', 'avatar-privacy' ); ?></label><br />
<?php endif; ?>
</p>
<?php endif; ?>
<?php if ( ! $uploads_disabled && $show_description ) : ?>
<p class="description">
<?php echo \wp_kses( $template->get_uploader_description( $can_upload, $has_local_avatar ), T::ALLOWED_HTML_LABEL ); ?>
</p>
<?php endif; ?>
</td>
</tr>
<?php

View File

@ -0,0 +1,39 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
?>
<script type="text/javascript">
(function($){
var parent = $( '#show_avatars' ),
hideChildren = $( '.avatar-settings-disabled' ),
showChildren = $( '.avatar-settings-enabled' );
parent.change(function(){
hideChildren.toggleClass( 'hide-if-js', this.checked );
showChildren.toggleClass( 'hide-if-js', ! this.checked );
});
})(jQuery);
</script>
<?php

View File

@ -0,0 +1,39 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2019 Peter Putzer.
* Copyright 2012-2013 Johannes Freudendahl.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
$show_avatars_class = empty( $show_avatars ) ? '' : ' hide-if-js';
?>
<div class="avatar-settings-disabled<?php echo $show_avatars_class; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>">
<p>
<?php \esc_html_e( "The Avatar Privacy plugin modifies the display of avatars. You have not enabled avatars, so this plugin can't do anything for you. :-)", 'avatar-privacy' ); ?>
</p>
<p>
<?php \esc_html_e( 'You can enable profile pictures for users and commenters by selecting "Show Avatars" above. Then you will also see the features enabled by the Avatar Privacy plugin here.', 'avatar-privacy' ); ?>
</p>
</div>
<?php

View File

@ -0,0 +1,42 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2019 Peter Putzer.
* Copyright 2012-2013 Johannes Freudendahl.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
$show_avatars_class = ! empty( $show_avatars ) ? '' : ' hide-if-js';
$allowed_html = [ 'strong' => [] ];
?>
<div class="avatar-settings-enabled<?php echo $show_avatars_class; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>">
<p>
<?php \esc_html_e( 'To protect the privacy of your users, visitors and commenters, the Avatar Privacy plugin has enabled the following features:', 'avatar-privacy' ); ?>
</p>
<ul class="ul-disc">
<li><strong><?php \esc_html_e( 'Consent:', 'avatar-privacy' ); ?></strong> <?php \esc_html_e( 'We only try to load an avatar from Gravatar.com when users and commenters have explicitly consented.', 'avatar-privacy' ); ?></li>
<li><strong><?php \esc_html_e( 'Check:', 'avatar-privacy' ); ?></strong> <?php \esc_html_e( 'Even when people opt-in, we check if they really have a gravatar associated with their e-mail address.', 'avatar-privacy' ); ?></li>
<li><strong><?php \esc_html_e( 'Cache:', 'avatar-privacy' ); ?></strong> <?php \esc_html_e( 'To prevent tracking of your visitors, gravatars are cached locally and all default images are hosted on your server.', 'avatar-privacy' ); ?></li>
</ul>
</div>
<?php

84
avatar-privacy.php Normal file
View File

@ -0,0 +1,84 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
* Copyright 2012-2013 Johannes Freudendahl.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*
* @wordpress-plugin
* Plugin Name: Avatar Privacy
* Plugin URI: https://code.mundschenk.at/avatar-privacy/
* Description: Adds options to enhance the privacy when using avatars.
* Author: Peter Putzer
* Author URI: https://code.mundschenk.at
* Version: 2.7.0
* Requires at least: 5.6
* Requires PHP: 7.4
* License: GNU General Public License v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: avatar-privacy
*/
namespace Avatar_Privacy;
use Avatar_Privacy\Controller;
use Avatar_Privacy\Factory;
use Avatar_Privacy\Requirements;
// Don't do anything if called directly.
if ( ! \defined( 'ABSPATH' ) || ! \defined( 'WPINC' ) ) {
die;
}
// Make plugin file path available globally.
if ( ! \defined( 'AVATAR_PRIVACY_PLUGIN_FILE' ) ) {
\define( 'AVATAR_PRIVACY_PLUGIN_FILE', __FILE__ );
}
if ( ! \defined( 'AVATAR_PRIVACY_PLUGIN_PATH' ) ) {
\define( 'AVATAR_PRIVACY_PLUGIN_PATH', __DIR__ );
}
// Initialize autoloader.
require_once __DIR__ . '/vendor-scoped/autoload.php';
/**
* Load the plugin after checking for the necessary PHP version (& other requirements).
*
* @since 2.4.0 Moved to Avatar_Privacy\run_avatar_privacy.
*
* @return void
*/
function run_avatar_privacy() {
// Check plugin requirements.
$requirements = new Requirements();
if ( $requirements->check() ) {
/**
* Create and start the plugin.
*
* @var Controller
*/
$plugin = Factory::get()->create( Controller::class );
$plugin->run();
}
}
run_avatar_privacy();

View File

@ -0,0 +1,54 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2022 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
#use function Avatar_Privacy\get_gravatar_checkbox;
#if ( ! \function_exists( 'avapr_get_avatar_checkbox' ) ) {
/**
* Returns the 'use gravatar' checkbox for the comment form.
*
* This is intended as a template function for older or highly-customized
* themes. Output the result with echo or print.
*
* @deprecated 2.3.0 Use \Avatar_Privacy\get_gravatar_checkbox instead.
*
* @return string The HTML code for the checkbox or an empty string.
*/
#function avapr_get_avatar_checkbox() {
#\_deprecated_function( __FUNCTION__, '2.3.0', 'Avatar_Privacy\get_gravatar_checkbox' );
#return get_gravatar_checkbox();
#}
#}
/**
* PHP 5.2 compatibility layer.
*
* Will be removed once the minimum requireemnt is WordPress 5.2.
*/
class_alias( \Avatar_Privacy\Factory::class, \Avatar_Privacy_Factory::class );

View File

@ -0,0 +1,84 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers;
/**
* Specifies an interface for handling avatar retrieval and caching.
*
* @since 2.0.0
* @since 2.4.0 Internal constants removed.
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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();
}

View File

@ -0,0 +1,200 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers;
use Avatar_Privacy\Avatar_Handlers\Avatar_Handler;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Icon_Provider; // phpcs:ignore ImportDetection.Imports.RequireImports.Import -- needed for PHPDoc
use Avatar_Privacy\Tools\Network\Remote_Image_Service;
/**
* Handles image caching for default icons.
*
* @since 2.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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 '';
}
}

View File

@ -0,0 +1,249 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers;
use Avatar_Privacy\Core;
use Avatar_Privacy\Avatar_Handlers\Avatar_Handler;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
use Avatar_Privacy\Data_Storage\Options;
use Avatar_Privacy\Tools\Images\Image_File;
use Avatar_Privacy\Tools\Network\Gravatar_Service;
/**
* Handles retrieving and caching Gravatar.com images.
*
* @since 1.0.0
* @since 2.0.0 Class was moved to Avatar_Privacy\Avatar_Handlers and now implements the new Avatar_Handler interface.
* @since 2.4.0 Internal constants and property $type_mapping removed.
*
* @author Peter Putzer <github@mundschenk.at>
*
* @phpstan-type AvatarArguments array{
* email?: string,
* rating?: key-of<self::GRAVATAR_RATING>,
* 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<self::GRAVATAR_RATING>
*/
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
}
}

View File

@ -0,0 +1,185 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2020-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers;
use Avatar_Privacy\Avatar_Handlers\Avatar_Handler;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
use Avatar_Privacy\Tools\Images\Image_File;
use Avatar_Privacy\Tools\Network\Remote_Image_Service;
/**
* Handles retrieving and caching legacy icons (including remote images).
*
* @since 2.4.0
*
* @author Peter Putzer <giGrthub@mundschenk.at>
*
* @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';
}
}

View File

@ -0,0 +1,221 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers;
use Avatar_Privacy\Avatar_Handlers\Avatar_Handler;
use Avatar_Privacy\Core\User_Fields;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
use Avatar_Privacy\Tools\Images;
use Avatar_Privacy\Tools\Images\Image_File;
/**
* Handles image caching for uploaded user avatars.
*
* @since 2.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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';
}
}

View File

@ -0,0 +1,127 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Icon_Provider;
/**
* An abstract implementation of the Default_Icon_Provider interface.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 '';
}
}

View File

@ -0,0 +1,139 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Abstract_Icon_Provider;
use Avatar_Privacy\Core\Default_Avatars;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
use Avatar_Privacy\Tools\Images;
use Avatar_Privacy\Tools\Images\Image_File;
/**
* An icon provider for uploaded custom default icons.
*
* @since 2.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons
*
* @author Peter Putzer <github@mundschenk.at>
*/
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' );
}
}

View File

@ -0,0 +1,116 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Abstract_Icon_Provider;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generator;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
/**
* An Icon_Provider that generates dynamic icons.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 );
}

View File

@ -0,0 +1,48 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons;
/**
* An icon generator.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 );
}

View File

@ -0,0 +1,87 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons;
/**
* Specifies an interface for default icon providers.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons
*
* @author Peter Putzer <github@mundschenk.at>
*/
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();
}

View File

@ -0,0 +1,79 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Abstract_Icon_Provider;
/**
* A default icon provider implementation using static images.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 );
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Static_Icon_Provider;
/**
* A default icon provider implementation using static SVG images.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 );
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generating_Icon_Provider;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
/**
* An icon provider for "birds" style icons.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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' );
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generating_Icon_Provider;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
/**
* An icon provider for "cats" style icons.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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' );
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generating_Icon_Provider;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
/**
* An icon provider for "aleavatar" icons.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons
* @since 2.1.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons
*
* @author Peter Putzer <github@mundschenk.at>
*/
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";
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generating_Icon_Provider;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
/**
* An icon provider for "monsterid" style icons.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons
* @since 2.1.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons
*
* @author Peter Putzer <github@mundschenk.at>
*/
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";
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generating_Icon_Provider;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
/**
* An icon provider for "retro" 8-bit style icons.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons
* @since 2.1.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons
*
* @author Peter Putzer <github@mundschenk.at>
*/
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";
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generating_Icon_Provider;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
/**
* An icon provider for "rings" icons.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons
* @since 2.1.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons
*
* @author Peter Putzer <github@mundschenk.at>
*/
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' );
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generating_Icon_Provider;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
/**
* An icon provider for "robohash" style icons.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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' );
}
}

View File

@ -0,0 +1,66 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generating_Icon_Provider;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
/**
* An icon provider for "wavatar" style icons.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons
* @since 2.1.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons
*
* @author Peter Putzer <github@mundschenk.at>
*/
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";
}
}

View File

@ -0,0 +1,109 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @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\Generators\PNG_Parts_Generator;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Tools\Images;
use Avatar_Privacy\Tools\Number_Generator;
use GdImage; // phpcs:ignore ImportDetection.Imports -- PHP 8.0 compatibility.
/**
* A bird avatar generator for the images created by David Revoy.
*
* See https://www.peppercarrot.com/extras/html/2019_bird-generator/
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @phpstan-type PartType value-of<self::PARTS>
* @phpstan-type PartsTemplate array<PartType, array{}>
* @phpstan-type AllPossibleParts array<PartType, string[]>
* @phpstan-type RandomizedParts array<PartType, string>
* @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;
}
}

View File

@ -0,0 +1,109 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @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\Generators\PNG_Parts_Generator;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Tools\Images;
use Avatar_Privacy\Tools\Number_Generator;
use GdImage; // phpcs:ignore ImportDetection.Imports -- PHP 8.0 compatibility.
/**
* A cat avatar generator for the images created by David Revoy.
*
* See https://www.davidrevoy.com/article591/cat-avatar-generator
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @phpstan-type PartType value-of<self::PARTS>
* @phpstan-type PartsTemplate array<PartType, array{}>
* @phpstan-type AllPossibleParts array<PartType, string[]>
* @phpstan-type RandomizedParts array<PartType, string>
* @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;
}
}

View File

@ -0,0 +1,74 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2019 Peter Putzer.
*
* 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.
*
* ***
*
* @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;
/**
* Generates an SVG icon based on a hash.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators
*
* @author Peter Putzer <github@mundschenk.at>
*/
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' );
}
}

View File

@ -0,0 +1,460 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
* Copyright 2007-2014 Scott Sherrill-Mix.
*
* 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.
*
* ***
*
* @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\Generators\PNG_Parts_Generator;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Tools\Images;
use Avatar_Privacy\Tools\Number_Generator;
use GdImage; // phpcs:ignore ImportDetection.Imports -- PHP 8.0 compatibility.
/**
* A monster generator based on the WordPress implementation by Scott Sherrill-Mix
* and the original algorithm designed by Andreas Gohr, based on an idea by Don Park.
*
* Artwork by Katherine Garner (Lemm).
*
* @link http://scott.sherrillmix.com/blog/blogger/wp_monsterid-update-hand-drawn-monsters/
* @link https://www.splitbrain.org/projects/monsterid
* @link http://kathgarner.com
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators
* @since 2.3.0 Refactored to use standard parts mechanisms, various obsolete
* constants removed.
*
* @author Peter Putzer <github@mundschenk.at>
* @author Scott Sherrill-Mix
*
* @phpstan-type PartType value-of<self::PARTS>
* @phpstan-type PartsTemplate array<PartType, array{}>
* @phpstan-type AllPossibleParts array<PartType, string[]>
* @phpstan-type RandomizedParts array<PartType, string>
*
* @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 );
}
}

View File

@ -0,0 +1,321 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @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\Data_Storage\Site_Transients;
use Avatar_Privacy\Exceptions\Part_Files_Not_Found_Exception;
use Avatar_Privacy\Tools\Number_Generator;
/**
* A base class for parts-based icon generators independent of the used image
* format. Part definitions are chached using WordPress' transient mechanism.
*
* The algorithm for building icons is conceptually based on Andreas Gohr's
* MonsterId library.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @phpstan-type PartType string
* @phpstan-type PartsTemplate array<PartType, array{}>
* @phpstan-type AllPossibleParts array<PartType, string[]>
* @phpstan-type RandomizedParts array<PartType, string>
* @phpstan-type AdditionalArguments array<string, mixed>
*/
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<string, string[]>
*/
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<PartType, 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 );
}

View File

@ -0,0 +1,383 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
* Copyright 2007-2014 Scott Sherrill-Mix.
*
* 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.
*
* ***
*
* @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\Generators\Parts_Generator;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Tools\Images;
use Avatar_Privacy\Tools\Number_Generator;
use GdImage; // phpcs:ignore ImportDetection.Imports -- PHP 8.0 compatibility.
/**
* A base class for parts-based PNG icon generators.
*
* The class includes some functions created for Scott Sherrill-Mix' WordPress
* plugin of MonsterID.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
* @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<string, array{ 0: BoundsTuple, 1: BoundsTuple }>
*/
protected function get_parts_dimensions() {
/**
* An array of boundary coordinates indexed by filename.
*
* @phpstan-var array<string, array{ 0: BoundsTuple, 1: BoundsTuple }> $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;
}
}

View File

@ -0,0 +1,198 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright (c) 2013, 2014, 2016 Benjamin Laugueux <benjamin@yzalis.com>
* Copyright (c) 2015 Grummfy <grummfy@gmail.com>
* 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 <github@mundschenk.at>
* @author Grummfy <grummfy@gmail.com>
*/
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<int, array<int, bool>>
*/
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<int, array<int, bool>> $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 );
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2021 Peter Putzer.
*
* 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.
*
* ***
*
* @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\Vendor\splitbrain\RingIcon\RingIconSVG;
/**
* An icon generator.
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators
* @since 2.3.0 Made subclass of RingIconSVG as Avatar_Privacy\Vendor\splitbrain\RingIcon is stable.
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 );
}
}

View File

@ -0,0 +1,275 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @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\Generators\Parts_Generator;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Tools\Number_Generator;
use Avatar_Privacy\Tools\Template;
/**
* An icon generator based on the Robohash SVG library by Nimiq.
*
* The code is a new implementation based on the general idea (only color
* pre-selections have been reused).
*
* @link https://github.com/nimiq/robohash
*
* @since 2.3.0
* @since 2.4.0 Internal method render_svg removed.
*
* @author Peter Putzer <github@mundschenk.at>
*
* @phpstan-type PartType value-of<self::PARTS>
* @phpstan-type PartsTemplate array<PartType, array{}>
* @phpstan-type AllPossibleParts array<PartType, string[]>
* @phpstan-type RandomizedParts array<PartType, string>
* @phpstan-type AdditionalArguments array{ color: value-of<self::COLORS>, bg_color: value-of<self::BG_COLORS> }
*/
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(
[
'#<svg[^>]+>(.*)</svg>#',
'/#26a9e0/',
], [
'$1',
'currentColor',
],
$svg
);
return "<g transform=\"translate(0,20)\">{$svg}</g>";
}
}

View File

@ -0,0 +1,279 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
* Copyright 2007-2008 Shamus Young.
*
* 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.
*
* ***
*
* @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\Generators\PNG_Parts_Generator;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Tools\Images;
use Avatar_Privacy\Tools\Number_Generator;
use GdImage; // phpcs:ignore ImportDetection.Imports -- PHP 8.0 compatibility.
/**
* A Wavatar generator, based on the original WordPress plugin by Shamus Young.
*
* @link https://www.shamusyoung.com/twentysidedtale/?p=1462
*
* @since 1.0.0
* @since 2.0.0 Moved to Avatar_Privacy\Avatar_Handlers\Default_Icons\Generators
* @since 2.3.0 Refactored to use standard parts mechanisms, various obsolete
* constants removed.
*
* @author Peter Putzer <github@mundschenk.at>
* @author Shamus Young <shamus@shamusyoung.com>
*
* @phpstan-import-type HueDegree from Images\Color
* @phpstan-import-type NormalizedHue from Images\Color
*
* @phpstan-type PartType value-of<self::PARTS>
* @phpstan-type PartsTemplate array<PartType, array{}>
* @phpstan-type AllPossibleParts array<PartType, string[]>
* @phpstan-type RandomizedParts array<PartType, string>
* @phpstan-type AdditionalArguments array<self::HUE_*, NormalizedHue>
*/
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<string, int>
*/
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 );
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Static_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\SVG_Icon_Provider;
/**
* An icon provider for the "bowling pin" icon.
*
* @since 2.1.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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' );
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Static_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\SVG_Icon_Provider;
/**
* An icon provider for the "mystery" icon.
*
* @since 2.1.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
class Mystery_Icon_Provider extends SVG_Icon_Provider {
/**
* Creates a new instance.
*/
public function __construct() {
parent::__construct( [ 'mystery', 'mystery-man', 'mm' ], 'mystery' );
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Static_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\SVG_Icon_Provider;
/**
* An icon provider for the "silhouette" icon.
*
* @since 2.1.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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' );
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Avatar_Handlers\Default_Icons\Static_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\SVG_Icon_Provider;
/**
* An icon provider for the "speech bubble" icon.
*
* @since 2.1.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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' );
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy;
/**
* Implements an interface for plugin components.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
interface Component {
/**
* Sets up the various hooks for the plugin component.
*
* @return void
*/
public function run();
}

View File

@ -0,0 +1,82 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2020 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy;
use Avatar_Privacy\Component; // phpcs:ignore ImportDetection.Imports.RequireImports.Import -- used in annotation.
use Avatar_Privacy\Core;
/**
* Initialize Avatar Privacy plugin.
*
* @since 1.0.0
* @since 2.1.0 Renamed to Avatar_Privacy\Controller.
*
* @author Peter Putzer <github@mundschenk.at>
*/
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();
}
}
}

View File

@ -0,0 +1,490 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
* Copyright 2012-2013 Johannes Freudendahl.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy;
use Avatar_Privacy\Core\Comment_Author_Fields;
use Avatar_Privacy\Core\Default_Avatars;
use Avatar_Privacy\Core\User_Fields;
use Avatar_Privacy\Core\Settings;
use Avatar_Privacy\Tools\Hasher;
use Avatar_Privacy\Exceptions\File_Deletion_Exception; // phpcs:ignore ImportDetection.Imports.RequireImports -- needed for PHPDoc annotation.
use Avatar_Privacy\Exceptions\Upload_Handling_Exception; // phpcs:ignore ImportDetection.Imports.RequireImports -- needed for PHPDoc annotation.
/**
* The core database API of the Avatar Privacy plugin.
*
* @author Peter Putzer <github@mundschenk.at>
* @author Johannes Freudendahl <wordpress@freudendahl.net>
*
* @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 );
}
}

View File

@ -0,0 +1,578 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy;
use Avatar_Privacy\Vendor\Dice\Dice;
use Avatar_Privacy\Core;
use Avatar_Privacy\Core\API;
use Avatar_Privacy\Core\Settings;
use Avatar_Privacy\Component;
use Avatar_Privacy\Components;
use Avatar_Privacy\Controller;
use Avatar_Privacy\CLI;
use Avatar_Privacy\Upload_Handlers\Upload_Handler;
use Avatar_Privacy\Avatar_Handlers\Avatar_Handler;
use Avatar_Privacy\Avatar_Handlers\Default_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons_Handler;
use Avatar_Privacy\Avatar_Handlers\Gravatar_Cache_Handler;
use Avatar_Privacy\Avatar_Handlers\Legacy_Icon_Handler;
use Avatar_Privacy\Avatar_Handlers\User_Avatar_Handler;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Generated_Icons;
use Avatar_Privacy\Avatar_Handlers\Default_Icons\Static_Icons;
use Avatar_Privacy\Data_Storage\Cache;
use Avatar_Privacy\Data_Storage\Database;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
use Avatar_Privacy\Data_Storage\Options;
use Avatar_Privacy\Data_Storage\Network_Options;
use Avatar_Privacy\Data_Storage\Transients;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Exceptions\Object_Factory_Exception;
use Avatar_Privacy\Integrations;
use Avatar_Privacy\Tools;
use Avatar_Privacy\Tools\HTML\User_Form;
/**
* A factory for creating Avatar_Privacy instances via dependency injection.
*
* @since 1.0.0
* @since 2.1.0 Class made concrete.
* @since 2.3.0 Moved to Avatar_Privacy\Factory.
* @since 2.4.0 Named instances converted to use class constants.
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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<class-string|string,mixed[]>
*/
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<int, array<string, class-string<Component>>>
*/
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<array<self::INSTANCE, class-string<Default_Icons\Icon_Provider>>>
*/
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<array<self::INSTANCE, class-string<Integrations\Plugin_Integration>>>
*/
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<array<self::INSTANCE, class-string<CLI\Command>>>
*/
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<array<self::INSTANCE, class-string<Database\Table>>>
*/
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<array<self::INSTANCE, class-string<Avatar_Handler>>>
*/
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 ];
}
}

View File

@ -0,0 +1,137 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy;
/**
* A custom requirements class to check for additional PHP packages and other
* prerequisites.
*
* @since 1.0.0
* @since 2.4.0 Moved to \Avatar_Privacy\Requirements.
*
* @author Peter Putzer <github@mundschenk.at>
*/
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<int, array{ enable_key: string, check: callable, notice: callable }>
*/
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 <a href="%2$s">enable the extension</a>.', 'avatar-privacy' ),
'<strong>Avatar Privacy</strong>',
/* 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' ),
'<strong>Avatar Privacy</strong>'
);
}
}

View File

@ -0,0 +1,106 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\CLI;
use Avatar_Privacy\CLI\Command;
/**
* An abstract base class for CLI command implementations.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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<TKey,TValue>
*
* @phpstan-param \Iterator<TKey,TValue> $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;
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2020 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\CLI;
/**
* An interface for CLI command implementations.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
interface Command {
/**
* Registers the command (and any optional subcommands).
*
* The method assumes that `\WP_CLI` is available.
*
* @return void
*/
public function register();
}

View File

@ -0,0 +1,104 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2020 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\CLI;
use Avatar_Privacy\CLI\Abstract_Command;
use Avatar_Privacy\Components\Image_Proxy;
use WP_CLI;
/**
* CLI commands for working with Avatar Privacy cron jobs.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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)." ) );
}
}
}

View File

@ -0,0 +1,324 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\CLI;
use Avatar_Privacy\CLI\Abstract_Command;
use Avatar_Privacy\Core;
use Avatar_Privacy\Data_Storage\Database\Comment_Author_Table;
use WP_CLI;
use WP_CLI\Formatter;
use WP_CLI\Iterators\Table as Table_Iterator;
use function WP_CLI\Utils\format_items;
use function WP_CLI\Utils\get_flag_value;
/**
* CLI commands for accessing the Avatar Privacy database tables.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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
*
* [--<field>=<value>]
* : Filter by one or more fields (see "Available Fields" section).
*
* [--field=<field>]
* : Prints the value of a single field for each row.
*
* [--fields=<fields>]
* : Comma-separated list of fields to show.
*
* [--format=<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<string,object> $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}." );
}
}
}

View File

@ -0,0 +1,209 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2020-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\CLI;
use Avatar_Privacy\CLI\Abstract_Command;
use Avatar_Privacy\Core\Default_Avatars;
use WP_CLI;
use function WP_CLI\Utils\get_flag_value;
/**
* CLI commands for setting Avatar Privacy defaults.
*
* @since 2.4.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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
*
* <image_url>
* : 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.' );
}
}
}

View File

@ -0,0 +1,239 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\CLI;
use Avatar_Privacy\CLI\Abstract_Command;
use Avatar_Privacy\Components\Setup;
use Avatar_Privacy\Components\Uninstallation;
use Avatar_Privacy\Data_Storage\Database\Comment_Author_Table as Database;
use WP_CLI;
use function WP_CLI\Utils\get_flag_value;
/**
* CLI commands for reoving data added by Avatar Privacy.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 <ID>").
* @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 <ID>").
* @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}." );
}
}
}

View File

@ -0,0 +1,206 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2020-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\CLI;
use Avatar_Privacy\CLI\Abstract_Command;
use Avatar_Privacy\Core\User_Fields;
use WP_CLI;
use function WP_CLI\Utils\get_flag_value;
/**
* CLI commands for working with the Avatar Privacy user data API.
*
* @since 2.4.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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
*
* <user_id>
* : The ID of the user whose local avatar should be set.
*
* <image_url>
* : 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
*
* <user_id>
* : 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.' );
}
}
}

View File

@ -0,0 +1,682 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
* Copyright 2012-2013 Johannes Freudendahl.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Core\Comment_Author_Fields;
use Avatar_Privacy\Core\User_Fields;
use Avatar_Privacy\Core\Settings;
use Avatar_Privacy\Exceptions\Avatar_Comment_Type_Exception;
use Avatar_Privacy\Tools\Images\Image_File;
use Avatar_Privacy\Tools\Network\Gravatar_Service;
use Avatar_Privacy\Tools\Network\Remote_Image_Service;
/**
* Handles the display of avatars in WordPress.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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 );
}
}

View File

@ -0,0 +1,249 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Tools\Template;
use Avatar_Privacy\Tools\HTML\Dependencies;
use Avatar_Privacy\Tools\HTML\User_Form;
/**
* The component providing our Gutenberg blocks.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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( '/<input[^>]+type=["\']hidden[^>]+>/Si', '', $markup );
// Also remove links.
$markup = (string) \preg_replace( '/(<a[^>]+)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 );
}
}

View File

@ -0,0 +1,76 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2020 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\CLI\Command; // phpcs:ignore ImportDetection.Imports.RequireImports.Import -- needed for type hinting.
/**
* The component providing CLI commands.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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();
}
}
}

View File

@ -0,0 +1,334 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2021 Peter Putzer.
* Copyright 2012-2013 Johannes Freudendahl.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Factory;
use Avatar_Privacy\Core\Comment_Author_Fields;
use Avatar_Privacy\Tools\Template;
/**
* Handles comment posting in WordPress.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 );
}
}

View File

@ -0,0 +1,349 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Avatar_Handlers\Avatar_Handler; // phpcs:ignore ImportDetection.Imports.RequireImports -- used by annotations
use Avatar_Privacy\Avatar_Handlers\Default_Icons_Handler;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Tools\Images\Image_File;
/**
* Handles the creation and caching of avatar images. Default icons are created by
* Icon_Provider instances, remote Gravatar.com avatars by a Gravatar_Cache_Handler.
*
* @since 1.0.0
* @since 2.0.0 Renamed to Image_Proxy.
*
* @author Peter Putzer <github@mundschenk.at>
*
* @phpstan-type HandlerHooks array<string, Avatar_Handler>
*/
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
}
}

View File

@ -0,0 +1,80 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2022 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Integrations\Plugin_Integration; // phpcs:ignore ImportDetection.Imports.RequireImports.Import -- necessary for type hints.
/**
* A registry for plugin integrations.
*
* @since 1.1.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 );
}
}

View File

@ -0,0 +1,376 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Core\Settings;
use Avatar_Privacy\Data_Storage\Network_Options;
use Avatar_Privacy\Data_Storage\Transients;
use Avatar_Privacy\Tools\Multisite;
use Avatar_Privacy\Tools\Template;
use Avatar_Privacy\Tools\HTML\Dependencies;
use Avatar_Privacy\Vendor\Mundschenk\UI\Control; // phpcs:ignore ImportDetection.Imports.RequireImports.Import -- necessary for type hints.
use Avatar_Privacy\Vendor\Mundschenk\UI\Controls;
use Avatar_Privacy\Vendor\Mundschenk\UI\Control_Factory;
/**
* Handles the network settings page on multisite installations.
*
* @since 2.1.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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, [] );
}
}
}

View File

@ -0,0 +1,355 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Core\Comment_Author_Fields;
use Avatar_Privacy\Core\User_Fields;
/**
* Integrates with the new privacy tools added in WordPress 4.9.6.
*
* @since 1.1.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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 = '<strong class="privacy-policy-tutorial">' . \__( 'Suggested text:' ) . ' </strong>'; // phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- Missing text domain is intentional to use Core translation.
$content = '<h3>' . \__( 'Comments', 'avatar-privacy' ) . '</h3>';
$content .= '<p class="privacy-policy-tutorial">' . \__( 'The information in this subsection supersedes the paragraph on Gravatar in the default "Comments" subsection provided by WordPress.', 'avatar-privacy' ) . '</p>';
$content .= "<p>{$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' ) . '</p>';
$content .= '<h3>' . \__( 'Cookies', 'avatar-privacy' ) . '</h3>';
$content .= '<p class="privacy-policy-tutorial">' . \__( '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' ) . '</p>';
$content .= "<p>{$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' ) . '</p>';
\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,
];
}
}

View File

@ -0,0 +1,229 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
* Copyright 2012-2013 Johannes Freudendahl.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Core\Settings;
use Avatar_Privacy\Data_Storage\Options;
use Avatar_Privacy\Tools\Template;
use Avatar_Privacy\Upload_Handlers\Custom_Default_Icon_Upload_Handler as Upload;
use Avatar_Privacy\Vendor\Mundschenk\UI\Control_Factory;
use Avatar_Privacy\Vendor\Mundschenk\UI\Controls;
/**
* Handles privacy-specific additions to the "Discussion" settings page.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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( '#(<form method="post") (action="options.php">)#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<key-of<SettingsFields>, 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;
}
}

View File

@ -0,0 +1,485 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
* Copyright 2012-2013 Johannes Freudendahl.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Core\Comment_Author_Fields;
use Avatar_Privacy\Core\Settings;
use Avatar_Privacy\Core\User_Fields;
use Avatar_Privacy\Components\Image_Proxy;
use Avatar_Privacy\Data_Storage\Database\Hashes_Table;
use Avatar_Privacy\Data_Storage\Database\Table; // phpcs:ignore ImportDetection.Imports.RequireImports.Import -- needed for type annotations.
use Avatar_Privacy\Data_Storage\Network_Options;
use Avatar_Privacy\Data_Storage\Options;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Data_Storage\Transients;
use Avatar_Privacy\Tools\Multisite;
/**
* Handles plugin activation and deactivation.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @phpstan-import-type SettingsFields from \Avatar_Privacy\Core\Settings
*
* @phpstan-type ObsoleteSettingsFields array<self::OBSOLETE_SETTINGS_FILES, mixed>
*/
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;
}
}

View File

@ -0,0 +1,151 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2019-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Tools\HTML\User_Form;
/**
* The component providing the `[avatar-privacy-avatar-upload]` shortcode.
*
* @since 2.3.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @phpstan-type FrontendFormAttributes array{ avatar_size?: int }
*/
class Shortcodes implements Component {
/**
* The shortcode attributes for `[avatar-privacy-form]`.
*
* @var array<string, int>
*/
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;
}
}

View File

@ -0,0 +1,288 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2020 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Core\User_Fields;
use Avatar_Privacy\Core\Settings;
use Avatar_Privacy\Data_Storage\Database\Comment_Author_Table as Database;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
use Avatar_Privacy\Data_Storage\Network_Options;
use Avatar_Privacy\Data_Storage\Options;
use Avatar_Privacy\Data_Storage\Site_Transients;
use Avatar_Privacy\Data_Storage\Transients;
use function Avatar_Privacy\Tools\delete_file;
/**
* Handles plugin activation and deactivation.
*
* @since 1.0.0
* @since 2.1.0 Class is now instantiated from `uninstall.php` all methods have been made non-static.
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 );
}
}
}

View File

@ -0,0 +1,171 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2020 Peter Putzer.
* Copyright 2012-2013 Johannes Freudendahl.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Components;
use Avatar_Privacy\Component;
use Avatar_Privacy\Tools\HTML\User_Form;
/**
* Handles privacy-specific updates of the user profile.
*
* @since 1.0.0
* @since 2.3.0 Public methods save_use_gravatar_checkbox,
* save_allow_anonymous_checkbox and save_user_profile_fields have
* been removed. The obsolete class constants have also been removed.
*
* @author Peter Putzer <github@mundschenk.at>
*/
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( '#<tr class="user-profile-picture">.*</tr>#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 );
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2020 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Core;
/**
* A marker interface for core API implementations.
*
* @since 2.4.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
interface API {}

View File

@ -0,0 +1,413 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
* Copyright 2012-2013 Johannes Freudendahl.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Core;
use Avatar_Privacy\Core\API;
use Avatar_Privacy\Data_Storage\Cache;
use Avatar_Privacy\Data_Storage\Database\Comment_Author_Table;
use Avatar_Privacy\Data_Storage\Database\Hashes_Table;
use Avatar_Privacy\Tools\Hasher;
use const MINUTE_IN_SECONDS;
/**
* The API for handling (anonymous) comment author data as part of the
* Avatar Privacy Core API.
*
* @since 2.4.0
*
* @author Peter Putzer <github@mundschenk.at>
* @author Johannes Freudendahl <wordpress@freudendahl.net>
*
* @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<string,string>
*/
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;
}
}

View File

@ -0,0 +1,270 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2020-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Core;
use Avatar_Privacy\Core\API;
use Avatar_Privacy\Core\Settings;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
use Avatar_Privacy\Data_Storage\Options;
use Avatar_Privacy\Exceptions\File_Deletion_Exception;
use Avatar_Privacy\Exceptions\Upload_Handling_Exception;
use Avatar_Privacy\Tools\Hasher;
use Avatar_Privacy\Tools\Images\Image_File;
use Avatar_Privacy\Upload_Handlers\Custom_Default_Icon_Upload_Handler as Upload_Handler;
use function Avatar_Privacy\Tools\delete_file;
/**
* The API for handling data attached to registered users as part of the
* Avatar Privacy Core API.
*
* @since 2.4.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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}" );
}
}

View File

@ -0,0 +1,449 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Core;
use Avatar_Privacy\Core\API;
use Avatar_Privacy\Components\Network_Settings_Page;
use Avatar_Privacy\Data_Storage\Options;
use Avatar_Privacy\Data_Storage\Network_Options;
use Avatar_Privacy\Upload_Handlers\Custom_Default_Icon_Upload_Handler;
use Avatar_Privacy\Vendor\Mundschenk\UI\Controls;
/**
* Default configuration for Avatar Privacy.
*
* @internal
*
* @since 2.0.0
* @since 2.1.0 Class made concrete and marked internal.
* @since 2.4.0 Moved to Avatar_Privacy\Core.
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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<SettingsFieldMeta>
*/
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<int, SettingsFields>
*/
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<SettingsFields> $setting
* @phpstan-return value-of<SettingsFields>
*/
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<SettingsFields> $setting
* @phpstan-param value-of<SettingsFields> $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<SettingsFieldMeta>
*/
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;
}
}

View File

@ -0,0 +1,525 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Core;
use Avatar_Privacy\Core\API;
use Avatar_Privacy\Data_Storage\Filesystem_Cache;
use Avatar_Privacy\Exceptions\Upload_Handling_Exception; // phpcs:ignore ImportDetection.Imports.RequireImports -- needed for PHPDoc annotation.
use Avatar_Privacy\Tools\Hasher;
use Avatar_Privacy\Tools\Images\Image_File;
use Avatar_Privacy\Upload_Handlers\User_Avatar_Upload_Handler;
use function Avatar_Privacy\Tools\delete_file;
/**
* The API for handling data attached to registered users as part of the
* Avatar Privacy Core API.
*
* @since 2.4.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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<string,\WP_User|null>
*/
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'] );
}
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Data_Storage;
/**
* A plugin-specific cache handler.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 );
}
}

View File

@ -0,0 +1,278 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Data_Storage;
use Avatar_Privacy\Exceptions\Filesystem_Exception;
use function Avatar_Privacy\Tools\delete_file;
/**
* A filesystem caching handler.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*
* @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<string,\SplFileInfo>
*/
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<string,\SplFileInfo,\RecursiveIteratorIterator<\RecursiveDirectoryIterator<string,\SplFileInfo>>> $files
*/
$files = new \RegexIterator( $files, $regex, \RecursiveRegexIterator::MATCH );
}
return $files;
}
}

View File

@ -0,0 +1,134 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2019 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Data_Storage;
/**
* A plugin-specific network options handler.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 );
}
}

View File

@ -0,0 +1,82 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2020 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Data_Storage;
/**
* A plugin-specific options handler.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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;
}
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Data_Storage;
/**
* A plugin-specific site transients handler.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 );
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Data_Storage;
/**
* A plugin-specific transients handler.
*
* @since 1.0.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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 );
}
}

View File

@ -0,0 +1,454 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2022 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Data_Storage\Database;
use Avatar_Privacy\Data_Storage\Database\Table;
use Avatar_Privacy\Data_Storage\Network_Options;
/**
* The database table used for storing (anonymous) comment author data.
*
* @since 2.4.0 Extracted from Avatar_Privacy\Data_Storage\Database\Table.
*
* @author Peter Putzer <github@mundschenk.at>
*/
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;
}
}

View File

@ -0,0 +1,162 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2020-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Data_Storage\Database;
use Avatar_Privacy\Data_Storage\Database\Table;
/**
* The new database table used for storing and looking up hashed identifiers
* (e-mail addresses mostly, but might be URLs for trackbacks and other special
* comment types).
*
* @since 2.4.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
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;
}
}

View File

@ -0,0 +1,694 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2018-2023 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Data_Storage\Database;
use Avatar_Privacy\Exceptions\Database_Exception;
/**
* A plugin-specific database handler.
*
* @since 2.1.0
* @since 2.4.0 Renamed to Avatar_Privacy\Data_Storage\Database\Table and
* refactored as abstract base class.
*
* @author Peter Putzer <github@mundschenk.at>
*
* @phpstan-type SQLValue int|string|null
* @phpstan-type ColumnValueTuples array<string,SQLValue>
* @phpstan-type ColumnFormats array<string,string>
*/
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<column,int>).
*
* @since 2.6.0
*
* @var array<string,int>
*/
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<int|string>
*/
protected function prepare_values( array $prepared_rows ) {
$values = [];
foreach ( $prepared_rows as $row ) {
foreach ( $row as $value ) {
if ( null !== $value ) {
$values[] = $value;
}
}
}
return $values;
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2020 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Exceptions;
/**
* An exception indicating that the comment was not of a type that we should
* display an avatar for.
*
* @since 2.3.4
*
* @author Peter Putzer <github@mundschenk.at>
*/
class Avatar_Comment_Type_Exception extends \RuntimeException {
}

View File

@ -0,0 +1,37 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2020 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Exceptions;
/**
* An exception indicating that an error occured during a database operation.
*
* @since 2.4.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
class Database_Exception extends \RuntimeException {
}

View File

@ -0,0 +1,39 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2020 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Exceptions;
use Avatar_Privacy\Exceptions\Filesystem_Exception;
/**
* An exception indicating that a file could not be deleted.
*
* @since 2.4.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
class File_Deletion_Exception extends Filesystem_Exception {
}

View File

@ -0,0 +1,37 @@
<?php
/**
* This file is part of Avatar Privacy.
*
* Copyright 2020 Peter Putzer.
*
* 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.
*
* ***
*
* @package mundschenk-at/avatar-privacy
* @license http://www.gnu.org/licenses/gpl-2.0.html
*/
namespace Avatar_Privacy\Exceptions;
/**
* An exception indicating that a filesystem operation failed.
*
* @since 2.4.0
*
* @author Peter Putzer <github@mundschenk.at>
*/
class Filesystem_Exception extends \RuntimeException {
}

Some files were not shown because too many files have changed in this diff Show More