PluginProbe
CryptX / 4.2.0
CryptX v4.2.0
4.2.0 4.1.1 trunk 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.9 2.0 2.1 2.2 2.3 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 All 92 releases
cryptx / src / block / index.js

index.js in CryptX 4.2.0, at src/block/index.js

270 lines 8.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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