| 1 |
/** |
| 2 |
* The editor side of the protected-address block. |
| 3 |
* |
| 4 |
* What the canvas shows is deliberately NOT what the visitor gets. The front end |
| 5 |
* is rendered on the server -- encrypted, per request -- and showing that here |
| 6 |
* would be a wall of base64 that tells the author nothing about the thing they |
| 7 |
* are editing. So the canvas shows the readable form, and says once, quietly, |
| 8 |
* that the address is hidden on the published page. |
| 9 |
* |
| 10 |
* There is no ServerSideRender for the same reason: it would cost a REST round |
| 11 |
* trip on every keystroke to render something unreadable. |
| 12 |
*/ |
| 13 |
|
| 14 |
import { registerBlockType } from '@wordpress/blocks'; |
| 15 |
import { |
| 16 |
useBlockProps, |
| 17 |
InspectorControls, |
| 18 |
BlockControls, |
| 19 |
} from '@wordpress/block-editor'; |
| 20 |
// No __experimental* imports here on purpose. The settings screen carries one |
| 21 |
// already, and an API that WordPress reserves the right to remove is a poor |
| 22 |
// thing to hang an editor block on: when it goes, the block stops loading and |
| 23 |
// the post it sits in cannot be edited. The controls below keep their default |
| 24 |
// margins instead, which is what stacks them in an inspector panel anyway. |
| 25 |
import { |
| 26 |
PanelBody, |
| 27 |
Placeholder, |
| 28 |
TextControl, |
| 29 |
TextareaControl, |
| 30 |
ToolbarButton, |
| 31 |
ToolbarGroup, |
| 32 |
Button, |
| 33 |
Notice, |
| 34 |
} from '@wordpress/components'; |
| 35 |
import { useState } from '@wordpress/element'; |
| 36 |
import { __ } from '@wordpress/i18n'; |
| 37 |
|
| 38 |
import metadata from './block.json'; |
| 39 |
import './index.css'; |
| 40 |
|
| 41 |
/** |
| 42 |
* Strips whatever a paste dragged in with it. |
| 43 |
* |
| 44 |
* An address never contains whitespace, so removing all of it is safe and |
| 45 |
* saves the author a puzzle: copy one out of Word or a PDF and it usually |
| 46 |
* arrives with a non-breaking space attached. PHP's trim() does not remove |
| 47 |
* that one, so the address would be rejected on the server and the block would |
| 48 |
* be absent from the published page -- over a character nobody can see. |
| 49 |
* |
| 50 |
* Cleaning it away here means the stored value is always one both sides agree |
| 51 |
* about, whichever field it was typed or pasted into. The warning below then |
| 52 |
* stays for addresses that are genuinely the wrong shape. |
| 53 |
* |
| 54 |
* @param {string} value The address as it arrived. |
| 55 |
* |
| 56 |
* @return {string} The address without invisible characters. |
| 57 |
*/ |
| 58 |
// Written as escapes, not as the characters themselves: a literal |
| 59 |
// non-breaking space in the source is invisible to the next reader and |
| 60 |
// survives exactly one careless reformat. |
| 61 |
const cleanAddress = ( value ) => |
| 62 |
value.replace( /[\s\u00A0\uFEFF\u200B-\u200D\u2060]+/g, '' ); |
| 63 |
|
| 64 |
/** |
| 65 |
* The address as the author should see it while writing. |
| 66 |
* |
| 67 |
* @param {Object} props Component props. |
| 68 |
* @param {Object} props.attributes The block attributes. |
| 69 |
* @param {Function} props.setAttributes Attribute setter. |
| 70 |
* |
| 71 |
* @return {Element} The editor markup. |
| 72 |
*/ |
| 73 |
function Edit( { attributes, setAttributes } ) { |
| 74 |
const { address, linkText, subject, body, cc, bcc } = attributes; |
| 75 |
const blockProps = useBlockProps(); |
| 76 |
|
| 77 |
// Kept separate from the attribute so a half-typed address is not stored on |
| 78 |
// every keystroke -- and so the placeholder does not vanish mid-word. |
| 79 |
const [ draft, setDraft ] = useState( '' ); |
| 80 |
const [ editing, setEditing ] = useState( false ); |
| 81 |
|
| 82 |
// PHP refuses to render anything it cannot recognise as an address, which |
| 83 |
// is the right call -- there is nothing to protect, and printing it would |
| 84 |
// only publish whatever was typed. But refusing quietly means the block is |
| 85 |
// simply absent from the published page, with the editor still showing it. |
| 86 |
// "My block is gone" is a bad way to find that out, so it is said here. |
| 87 |
// |
| 88 |
// The plugin's own pattern, character for character -- the one in |
| 89 |
// CryptX\Exposure, which is what PHP gates on. Not is_email(): that accepts |
| 90 |
// quotes and slashes in a local part, so it would stay silent about an |
| 91 |
// address the server then refuses. Not a looser guess either, which would |
| 92 |
// warn about addresses that work perfectly well. Both sides have to agree, |
| 93 |
// or the warning is worse than none. |
| 94 |
// |
| 95 |
// cleanAddress() rather than a trim: both fields store through it, so an |
| 96 |
// invisible character pasted in with the address never survives to be |
| 97 |
// judged differently by the two sides. |
| 98 |
const looksLikeAddress = |
| 99 |
address === '' || |
| 100 |
/^[_a-zA-Z0-9-+]+(\.[_a-zA-Z0-9-+]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(\.[a-zA-Z]{2,})$/.test( |
| 101 |
cleanAddress( address ) |
| 102 |
); |
| 103 |
|
| 104 |
const warning = ! looksLikeAddress && ( |
| 105 |
<Notice status="warning" isDismissible={ false }> |
| 106 |
{ __( |
| 107 |
'This does not look like an email address, so nothing will appear on the published page. Addresses with accented or non-Latin characters in the domain cannot be protected; write the domain in its punycode form (xn--…) instead.', |
| 108 |
'cryptx' |
| 109 |
) } |
| 110 |
</Notice> |
| 111 |
); |
| 112 |
|
| 113 |
const inspector = ( |
| 114 |
<InspectorControls> |
| 115 |
<PanelBody title={ __( 'Address', 'cryptx' ) }> |
| 116 |
<div className="cryptx-block-fields"> |
| 117 |
<TextControl |
| 118 |
__nextHasNoMarginBottom |
| 119 |
__next40pxDefaultSize |
| 120 |
type="email" |
| 121 |
label={ __( 'Email address', 'cryptx' ) } |
| 122 |
value={ address } |
| 123 |
onChange={ ( next ) => |
| 124 |
setAttributes( { address: cleanAddress( next ) } ) |
| 125 |
} |
| 126 |
/> |
| 127 |
<TextControl |
| 128 |
__nextHasNoMarginBottom |
| 129 |
__next40pxDefaultSize |
| 130 |
label={ __( 'Link text', 'cryptx' ) } |
| 131 |
help={ __( |
| 132 |
'Leave empty to use whatever the CryptX settings say. Fill it in to show something else, such as "Write to us".', |
| 133 |
'cryptx' |
| 134 |
) } |
| 135 |
value={ linkText } |
| 136 |
onChange={ ( next ) => |
| 137 |
setAttributes( { linkText: next } ) |
| 138 |
} |
| 139 |
/> |
| 140 |
</div> |
| 141 |
</PanelBody> |
| 142 |
<PanelBody |
| 143 |
title={ __( 'Prefilled message', 'cryptx' ) } |
| 144 |
initialOpen={ false } |
| 145 |
> |
| 146 |
<div className="cryptx-block-fields"> |
| 147 |
<TextControl |
| 148 |
__nextHasNoMarginBottom |
| 149 |
__next40pxDefaultSize |
| 150 |
label={ __( 'Subject', 'cryptx' ) } |
| 151 |
value={ subject } |
| 152 |
onChange={ ( next ) => |
| 153 |
setAttributes( { subject: next } ) |
| 154 |
} |
| 155 |
/> |
| 156 |
<TextareaControl |
| 157 |
__nextHasNoMarginBottom |
| 158 |
label={ __( 'Message', 'cryptx' ) } |
| 159 |
rows={ 3 } |
| 160 |
value={ body } |
| 161 |
onChange={ ( next ) => setAttributes( { body: next } ) } |
| 162 |
/> |
| 163 |
<TextControl |
| 164 |
__nextHasNoMarginBottom |
| 165 |
__next40pxDefaultSize |
| 166 |
label={ __( 'Cc', 'cryptx' ) } |
| 167 |
value={ cc } |
| 168 |
onChange={ ( next ) => setAttributes( { cc: next } ) } |
| 169 |
/> |
| 170 |
<TextControl |
| 171 |
__nextHasNoMarginBottom |
| 172 |
__next40pxDefaultSize |
| 173 |
label={ __( 'Bcc', 'cryptx' ) } |
| 174 |
help={ __( |
| 175 |
'These travel inside the encrypted link, so they are not readable in the page either.', |
| 176 |
'cryptx' |
| 177 |
) } |
| 178 |
value={ bcc } |
| 179 |
onChange={ ( next ) => setAttributes( { bcc: next } ) } |
| 180 |
/> |
| 181 |
</div> |
| 182 |
</PanelBody> |
| 183 |
</InspectorControls> |
| 184 |
); |
| 185 |
|
| 186 |
if ( ! address || editing ) { |
| 187 |
return ( |
| 188 |
<div { ...blockProps }> |
| 189 |
{ inspector } |
| 190 |
<Placeholder |
| 191 |
icon="email-alt" |
| 192 |
label={ __( 'Protected email address', 'cryptx' ) } |
| 193 |
instructions={ __( |
| 194 |
'The address is replaced before the page is delivered, so a spam bot reading the source finds nothing to collect.', |
| 195 |
'cryptx' |
| 196 |
) } |
| 197 |
> |
| 198 |
<form |
| 199 |
onSubmit={ ( event ) => { |
| 200 |
event.preventDefault(); |
| 201 |
setAttributes( { |
| 202 |
address: cleanAddress( draft ), |
| 203 |
} ); |
| 204 |
setEditing( false ); |
| 205 |
} } |
| 206 |
> |
| 207 |
<TextControl |
| 208 |
__nextHasNoMarginBottom |
| 209 |
__next40pxDefaultSize |
| 210 |
type="email" |
| 211 |
label={ __( 'Email address', 'cryptx' ) } |
| 212 |
hideLabelFromVision |
| 213 |
placeholder={ __( 'info@example.com', 'cryptx' ) } |
| 214 |
value={ draft || address } |
| 215 |
onChange={ setDraft } |
| 216 |
/> |
| 217 |
<Button |
| 218 |
__next40pxDefaultSize |
| 219 |
variant="primary" |
| 220 |
type="submit" |
| 221 |
disabled={ ! ( draft || address ).trim() } |
| 222 |
accessibleWhenDisabled |
| 223 |
> |
| 224 |
{ __( 'Use this address', 'cryptx' ) } |
| 225 |
</Button> |
| 226 |
</form> |
| 227 |
{ warning } |
| 228 |
</Placeholder> |
| 229 |
</div> |
| 230 |
); |
| 231 |
} |
| 232 |
|
| 233 |
return ( |
| 234 |
<div { ...blockProps }> |
| 235 |
{ inspector } |
| 236 |
<BlockControls> |
| 237 |
<ToolbarGroup> |
| 238 |
<ToolbarButton |
| 239 |
icon="edit" |
| 240 |
title={ __( 'Change the address', 'cryptx' ) } |
| 241 |
onClick={ () => { |
| 242 |
setDraft( address ); |
| 243 |
setEditing( true ); |
| 244 |
} } |
| 245 |
/> |
| 246 |
</ToolbarGroup> |
| 247 |
</BlockControls> |
| 248 |
{ warning } |
| 249 |
<a |
| 250 |
href="#cryptx-preview" |
| 251 |
onClick={ ( event ) => event.preventDefault() } |
| 252 |
> |
| 253 |
{ /* Through the cleaner, so the canvas shows the string the |
| 254 |
page will show. Attributes can reach a block without |
| 255 |
passing a setter -- pasted markup, the post code editor, |
| 256 |
an import -- and then the two would differ. */ } |
| 257 |
{ linkText || cleanAddress( address ) } |
| 258 |
</a> |
| 259 |
</div> |
| 260 |
); |
| 261 |
} |
| 262 |
|
| 263 |
registerBlockType( metadata.name, { |
| 264 |
edit: Edit, |
| 265 |
// Nothing is saved but the attributes. The markup is built on the server on |
| 266 |
// every request, because it is encrypted with the site's secret -- a saved |
| 267 |
// ciphertext would stop resolving the moment that secret changed. |
| 268 |
save: () => null, |
| 269 |
} ); |
| 270 |
|