632636fb by Jeff Balicki

home content

1 parent e4dc7cb8
Showing 104 changed files with 5032 additions and 0 deletions
1 <svg fillRule="evenodd" clipRule="evenodd" strokeLinecap="round" strokeLinejoin="round" strokeMiterlimit="1.5"
2 width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
3 <path
4 id="Block_Lab_Icon"
5 d="M2.255,4.02c0.032,-0.042 0.071,-0.08 0.119,-0.111l4.937,-2.85c0.08,-0.042 0.169,-0.063 0.263,-0.058c0.054,0.007 0.077,0.002 0.184,0.058l4.938,2.85l0,0l4.937,2.851l0.026,0.016l0.026,0.019l0.023,0.02l0.023,0.021l0.021,0.023l0.019,0.025l0.017,0.026l0.016,0.027l0.014,0.028l0.011,0.029l0.01,0.03l0.008,0.03l0.005,0.031l0.003,0.031l0.001,0.031l0,0l0,5.701c-0.012,0.235 -0.035,0.265 -0.223,0.387l-9.837,5.679c-0.13,0.099 -0.3,0.12 -0.485,0.022l0,0l-4.937,-2.85l0,0c-0.025,-0.016 -0.046,-0.031 -0.065,-0.044c-0.118,-0.083 -0.143,-0.132 -0.154,-0.271c-0.009,-0.048 -0.011,-0.095 -0.004,-0.141l0,-11.334c0.005,-0.104 0.043,-0.2 0.104,-0.276Zm4.833,13.754l0,-10.369l-4.043,-2.334l0,10.368l4.043,2.335Zm5.831,-8.551l3.596,-2.076l-3.596,-2.076l0,4.152Z"
6 fill="#82878c"
7 />
8 </svg>
1 <?php
2 /**
3 * Block Lab
4 *
5 * @package Block_Lab_Custom_Pro
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 *
9 * Plugin Name: Block Lab Custom Pro
10 * Plugin URI:
11 * Description: The easy way to build custom blocks for Gutenberg.
12 * Version: 200
13 * Author:
14 * Author URI:
15 * License: GPL2
16 * License URI:
17 * Text Domain: block-lab
18 * Domain Path: languages
19 */
20
21 // Exit if accessed directly.
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 // Setup the plugin auto loader.
27 require_once 'php/autoloader.php';
28
29 /**
30 * Admin notice for incompatible versions of PHP.
31 */
32 function block_lab_php_version_error() {
33 printf( '<div class="error"><p>%s</p></div>', esc_html( block_lab_php_version_text() ) );
34 }
35
36 /**
37 * String describing the minimum PHP version.
38 *
39 * "Namespace" is a PHP 5.3 introduced feature. This is a hard requirement
40 * for the plugin structure.
41 *
42 * "Traits" is a PHP 5.4 introduced feature. Remove "Traits" support from
43 * php/autoloader if you want to support a lower PHP version.
44 * Remember to update the checked version below if you do.
45 *
46 * @return string
47 */
48 function block_lab_php_version_text() {
49 return __( 'Block Lab plugin error: Your version of PHP is too old to run this plugin. You must be running PHP 5.4 or higher.', 'block-lab' );
50 }
51
52 // If the PHP version is too low, show warning and return.
53 if ( version_compare( phpversion(), '5.4', '<' ) ) {
54 if ( defined( 'WP_CLI' ) ) {
55 WP_CLI::warning( block_lab_php_version_text() );
56 } else {
57 add_action( 'admin_notices', 'block_lab_php_version_error' );
58 }
59
60 return;
61 }
62
63 /**
64 * Admin notice for incompatible versions of WordPress or missing Gutenberg Plugin.
65 */
66 function block_lab_wp_version_error() {
67 printf( '<div class="error"><p>%s</p></div>', esc_html( block_lab_wp_version_text() ) );
68 }
69
70 /**
71 * String describing the minimum WP version or Gutenberg Plugin requirement.
72 *
73 * "Blocks" are a feature of WordPress 5.0+ or require the Gutenberg plugin.
74 *
75 * @return string
76 */
77 function block_lab_wp_version_text() {
78 return __( 'Block Lab plugin error: Your version of WordPress is too old. You must be running WordPress 5.0 to use Block Lab.', 'block-lab' );
79 }
80
81 // If the WordPress version is too low, show warning and return.
82 if ( ! function_exists( 'register_block_type' ) ) {
83 if ( defined( 'WP_CLI' ) ) {
84 WP_CLI::warning( block_lab_wp_version_text() );
85 } else {
86 add_action( 'admin_notices', 'block_lab_wp_version_error' );
87 }
88 }
89
90 /**
91 * Get the plugin object.
92 *
93 * @return \Block_Lab\Plugin
94 */
95 function block_lab() {
96 static $instance;
97
98 if ( null === $instance ) {
99 $instance = new \Block_Lab\Plugin();
100 }
101
102 return $instance;
103 }
104
105 /**
106 * Setup the plugin instance.
107 */
108 block_lab()
109 ->set_basename( plugin_basename( __FILE__ ) )
110 ->set_directory( plugin_dir_path( __FILE__ ) )
111 ->set_file( __FILE__ )
112 ->set_slug( 'block-lab' )
113 ->set_url( plugin_dir_url( __FILE__ ) )
114 ->set_version( __FILE__ )
115 ->init();
116
117 // Sometimes we need to do some things after the plugin is loaded, so call the Plugin_Interface::plugin_loaded().
118 add_action( 'plugins_loaded', [ block_lab(), 'plugin_loaded' ] );
119
120 // Require helpers at 11, so if GCB is active, its helpers will be required first and prevent a PHP error.
121 add_action( 'plugins_loaded', [ block_lab(), 'require_helpers' ], 11 );
122
123
1 .post-type-block_lab .tablenav.top,
2 .post-type-block_lab .search-box,
3 .post-type-block_lab .inline-edit-date,
4 .post-type-block_lab .inline-edit-group {
5 display: none;
6 }
7 .post-type-block_lab .column-template code {
8 background: none;
9 font-size: 12px;
10 padding-left: 0;
11 }
12 .post-type-block_lab .fixed .column-icon {
13 width: 10%;
14 }
15 .post-type-block_lab .fixed td.column-icon {
16 color: #72777c;
17 }
18 .post-type-block_lab .fixed td.column-icon .icon {
19 background: #ffffff;
20 border: 1px solid #aaa;
21 border-radius: 4px;
22 padding: 4px;
23 width: 24px;
24 height: 24px;
25 display: inline-block;
26 }
27 .post-type-block_lab .fixed .column-fields {
28 width: 10%;
29 }
1 .bl-notice-conflict {
2 display: flex;
3 padding-top: 0.2rem;
4 padding-bottom: 0.2rem;
5 }
6
7 .bl-notice-conflict .bl-conflict-copy {
8 margin-right: auto;
9 }
10
11 .bl-notice-conflict .bl-link-deactivate {
12 margin: auto 0.2rem;
13 }
1 #adminmenu ul > li > a[href="edit.php?post_type=block_lab&page=block-lab-pro"] {
2 color: #00b9eb;
3 }
...\ No newline at end of file ...\ No newline at end of file
1 .bl-notice-migration {
2 display: flex;
3 }
4
5 .bl-notice-migration .bl-migration-copy {
6 margin-right: auto;
7 }
8
9 .bl-notice-migration__learn-more {
10 display: block;
11 }
12
13 .bl-notice-migration .bl-notice-option {
14 margin: auto 0.2rem;
15 }
16
17 .bl-notice-migration.bl-hidden {
18 display: none;
19 }
20
21 #bl-notice-not-now {
22 margin-left: 16px;
23 }
1 :root {
2 --color-brand: #5C34E8;
3 --color-pink: #FF227E;
4 --color-orange: #FF7C53;
5 --color-heading: #000;
6 --color-body: rgb(51, 51, 51);
7 --color-white: #fff;
8 --color-black: #000;
9 --color-gray: #edf2f7;
10 --color-green: #48bb78;
11
12 --color-purple: #422E62;
13 --color-purple-100: #FAF5FF;
14 --color-purple-200: #E9D8FD;
15 --color-purple-300: #D6BCFA;
16 --color-purple-400: #B794F4;
17 --color-purple-500: #9F7AEA;
18 --color-purple-600: #805AD5;
19 --color-purple-700: #6B46C1;
20 --color-purple-800: #553C9A;
21 --color-purple-900: #44337A;
22
23 --color-gray-100: #F7FAFC;
24 --color-gray-200: #EDF2F7;
25 --color-gray-300: #E2E8F0;
26 --color-gray-400: #CBD5E0;
27 --color-gray-500: #A0AEC0;
28 --color-gray-600: #718096;
29 --color-gray-700: #4A5568;
30 --color-gray-800: #2D3748;
31 --color-gray-900: #1A202C;
32
33 --box-shadow: 10px 10px 20px 0px rgba(121,110,255,0.1);
34 --box-shadow-small: 0 1px 3px 0 rgba(0,0,0,.1), 0 1px 2px 0 rgba(0,0,0,.06);
35 }
36
37 #wpcontent {
38 padding-left: 0;
39 }
40
41 .bl-migration__content {
42 font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
43 line-height: 1.5;
44 }
45
46 .bl-migration__content {
47 color: var(--color-body);
48 font-size: 16px;
49 }
50
51 .bl-migration__content .get-genesis-pro {
52 border-top: solid 1px var(--color-gray-300);
53 padding-top: 2rem;
54 font-size: 18px;
55 }
56
57 .bl-migration__content p {
58 margin-bottom: 1em;
59 font-size: 16px;
60 }
61
62 .bl-migration__content h1,
63 .bl-migration__content h2,
64 .bl-migration__content h3,
65 .bl-migration__content h4,
66 .bl-migration__content h5,
67 .bl-migration__content h6 {
68 font-weight: 500;
69 margin-bottom: 1em;
70 }
71
72 .bl-migration__content h1 {
73 font-size: 1.65rem;
74 }
75
76 .bl-migration__content a {
77 color: var(--color-brand);
78 text-decoration: underline;
79 }
80
81 .bl-migration__content {
82 display: flex;
83 flex-grow: 1;
84 flex-direction: column;
85 background-color: var(--color-gray);
86 }
87
88 .bl-migration__content ul {
89 list-style-type: disc;
90 padding-left: 2rem;
91 margin-bottom: 2rem;
92 }
93
94 .bl-migration__content-wrapper {
95 padding: 1rem;
96 overflow: auto;
97 flex-grow: 1;
98 }
99
100 .bl-migration__content-container {
101 box-shadow: var(--box-shadow);
102 padding: 2.5rem;
103 background-color: var(--color-white);
104
105 }
106
107 .bl-migration__content .container {
108 max-width: 1200px;
109 margin-left: auto;
110 margin-right: auto;
111 }
112
113 .bl-migration__content .btn {
114 appearance: none;
115 border: none;
116 display: flex;
117 color: var(--color-purple-100);
118 cursor: pointer;
119 box-shadow: var(--box-shadow-small);
120 padding-left: 1.25rem;
121 padding-right: 1.25rem;
122 font-size: .875rem;
123 height: 2.25rem;
124 line-height: 1;
125 font-weight: 500;
126 align-items: center;
127 border-radius: .25rem;
128 background-color: var(--color-purple-700);
129 text-decoration: none;
130 }
131
132 .bl-migration__content .btn:hover {
133 cursor: pointer;
134 background-color: var(--color-purple-800);
135 }
136
137 .bl-migration__content .btn[disabled],
138 .genesis-pro-form button[disabled] {
139 color: var(--color-gray-500);
140 background-color: var(--color-gray-200);
141 box-shadow: none;
142 cursor: not-allowed;
143 }
144
145 .bl-migration__content .get-genesis-pro .btn {
146 display: inline-flex;
147 margin-right: 1rem;
148 }
149
150 .bl-migration__content .btn-secondary {
151 color: var(--color-purple-600);
152 border-color: var(--color-purple-200);
153 background-color: var(--color-purple-100);
154 box-shadow: none;
155 border-width: 1px;
156 border-style: solid;
157 }
158
159 .bl-migration__content .btn-secondary:hover {
160 color: var(--color-purple-700);
161 background-color: var(--color-purple-200);
162 }
163
164 .help-text {
165 opacity: 0.7;
166 font-style: italic;
167 margin-top: .5rem;
168 font-size: .875rem;
169 }
170
171 .dev-notice {
172 display: flex;
173 padding-left: .5rem;
174 padding-right: .5rem;
175 height: 3rem;
176 justify-content: flex-start;
177 align-items: center;
178 border-width: 1px;
179 border-radius: .25rem;
180 border-color: var(--color-brand);
181 background-color: var(--color-purple-100);
182 }
183
184 .dev-notice svg {
185 color: var(--color-purple-600);
186 fill: currentColor;
187 height: 1.25rem;
188 width: 1.25rem;
189 margin-left: 0.25rem;
190 }
191 .dev-notice>span {
192 color: var(--color-purple-700);
193 font-size: .875rem;
194 margin-left: .5rem;
195 line-height: 1;
196 font-weight: 500;
197 }
198
199 .dev-notice .btn {
200 color: var(--color-purple-700);
201 background-color: var(--color-purple-200);
202 height: 2rem;
203 padding-right: 0.75rem;
204 padding-left: 0.75rem;
205 margin-left: auto;
206 }
207
208 .dev-notice .btn:hover,
209 .genesis-pro-form button:hover {
210 color: var(--color-purple-800);
211 background-color: var(--color-purple-300);
212 }
213
214 .step {
215 background-color: var(--color-gray-100);
216 display: flex;
217 justify-content: flex-start;
218 padding-top: 1.1rem;
219 padding-bottom: 1rem;
220 padding-left: 1rem;
221 padding-right: 1rem;
222 border-top-width: 1px;
223 border-top-style: solid;
224 border-top-color: var(--color-gray-400);
225 max-height: 2.4rem;
226 overflow: hidden;
227 transition-duration: 200ms;
228 }
229
230 .step:last-child {
231 border-bottom: 1px solid var(--color-gray-400);
232 }
233
234 .step-icon {
235 display: flex;
236 flex-shrink: 0;
237 align-items: center;
238 justify-content: center;
239 height: 2.25rem;
240 width: 2.25rem;
241 border-radius: 99999px;
242 border-color: var(--color-gray-300);
243 border-width: 2px;
244 border-style: solid;
245 color: var(--color-gray-500)
246 }
247
248 .step-icon svg {
249 width: 1.5rem;
250 height: 1.5rem;
251 fill: currentColor;
252 }
253
254 .step-icon span {
255 font-size: 1.25rem;
256 font-weight: 600;
257 }
258
259 .step h3 {
260 color: var(--color-gray-500);
261 margin-top: 6px;
262 }
263
264 .step--active {
265 overflow: hidden;
266 transition: max-height 1s ease-in-out;
267 max-height: 200rem;
268 background-color: var(--color-white);
269 padding-top: 2rem;
270 padding-bottom: 2rem;
271 }
272
273 .step--active h3 {
274 color: var(--color-body);
275 }
276
277 .step--active .step-icon {
278 border-color: var(--color-green);
279 color: var(--color-green);
280 }
281
282 .step--complete .step-icon {
283 background-color: var(--color-green);
284 border-color: var(--color-green);
285 color: var(--color-white);
286 }
287
288 .step--complete h3 {
289 color: var(--color-body);
290 }
291
292 .step-content {
293 flex-grow: 1;
294 margin-left: 2rem;
295 }
296
297 .step-footer {
298 display: flex;
299 align-items: center;
300 justify-content: space-between;
301 margin-top: 2rem;
302 border-top: solid 1px var(--color-gray-300);
303 padding-top: 1rem;
304 }
305
306 .step-footer form {
307 display: flex;
308 align-items: center;
309 margin-left: auto;
310 margin-bottom: 0;
311 margin-right: 1rem;
312 }
313
314 .step-footer button:only-child {
315 margin-left: auto;
316 }
317
318 .step-footer input[type="checkbox"] {
319 margin: 0;
320 }
321
322 label {
323 font-size: .875rem;
324 margin-left: .25rem;
325 font-weight: 500;
326 color: var(--color-gray-700);
327 }
328
329 .genesis-pro-form {
330 display: flex;
331 align-items: center;
332 }
333
334 .genesis-pro-form form {
335 display: flex;
336 margin-bottom: 0;
337 }
338
339 .genesis-pro-form input {
340 appearance: none;
341 width: 20rem;
342 box-shadow: var(--box-shadow-small);
343 padding-left: 1rem;
344 padding-right: 1rem;
345 font-size: .875rem;
346 height: 2.5rem;
347 border-style: solid;
348 border-width: 1px;
349 border-radius: 0.25rem 0 0 0.25rem;
350 border-color: var(--color-gray-400)
351 }
352
353 .genesis-pro-form button {
354 color: var(--color-purple-600);
355 box-shadow: var(--box-shadow-small);
356 padding-left: 1rem;
357 padding-right: 1rem;
358 font-size: .875rem;
359 height: 2.5rem;
360 margin-left: -1px;
361 font-weight: 500;
362 border-style: solid;
363 border-width: 1px;
364 border-bottom-right-radius: .25rem;
365 border-top-right-radius: .25rem;
366 border-color: var(--color-purple-300);
367 background-color: var(--color-purple-200);
368 cursor: pointer;
369 }
370
371 .genesis-pro-form p {
372 margin-left: 1rem;
373 margin-right: 1rem;
374 margin-bottom: 0;
375 margin-top: 0;
376 font-weight: 500;
377 }
378
379 .pro-submission-message {
380 font-style: italic;
381 color: var(--color-purple-600);
382 margin-top: 0.5rem;
383 }
384
385 .pro-box {
386 background-color: var(--color-gray-800);
387 color: var(--color-gray-200);
388 padding: 2.5rem 2.5rem 1rem 2.5rem;
389 margin-top: 2.5rem;
390 border-radius: .25rem;
391 box-shadow: var(--box-shadow);
392 margin-bottom: 2rem;;
393 }
394
395 .pro-box h3 {
396 color: var(--color-gray-200);
397 }
398
399 .pro-box-tiles {
400 display: flex;
401 margin-top: 1rem;
402 margin-left: -.5rem;
403 margin-right: -.5rem;
404 }
405
406 .pro-box-tile {
407 background-color: var(--color-gray-900);
408 padding: 2rem;
409 border-radius: .25rem;
410 margin-left: .5rem;
411 margin-right: .5rem;
412 width: 0;
413 flex-grow: 1;
414 }
415
416 .pro-box-tile__icon {
417 display: flex;
418 align-items: center;
419 justify-content: center;
420 border-radius: .25rem;
421 background-color: var(--color-purple-700);
422 height: 3rem;
423 width: 3rem;
424 margin-bottom: .75rem;
425 }
426
427 .pro-box-tile__icon svg {
428 width: 2rem;
429 height: 2rem;
430 color: var(--color-purple-300);
431 fill: currentColor;
432 }
433
434 .bl-migration__error {
435 background-color: var(--color-purple-100);
436 padding: 0.1rem 1rem;
437 margin-bottom: 1rem;
438 }
439
440 .bl-migration__error p:first-child {
441 color: var(--color-purple-700);
442 font-weight: 500;
443 }
444
445 .message-future {
446 font-size: 12px;
447 }
448
449 /*
450 * Copied from Gutenberg.
451 * https://github.com/WordPress/gutenberg/blob/eb856ef0892d6b9d15f39483ac2f45673d68f2d4/packages/components/src/spinner/style.scss
452 */
453 .components-spinner {
454 display: inline-block;
455 background-color: #7e8993;
456 width: 18px;
457 height: 18px;
458 opacity: 0.7;
459 margin: 5px 11px 0;
460 border-radius: 100%;
461 position: relative;
462 }
463
464 .components-spinner::before {
465 content: "";
466 position: absolute;
467 background-color: #fff;
468 top: 3px;
469 left: 3px;
470 width: 4px;
471 height: 4px;
472 border-radius: 100%;
473 transform-origin: 6px 6px;
474 animation: components-spinner__animation 1s infinite linear;
475 }
476
477 @keyframes components-spinner__animation {
478 from {
479 transform: rotate(0deg);
480 }
481 to {
482 transform: rotate(360deg);
483 }
484 }
485
486 @media (max-width: 1000px) {
487 .step .pro-box-tiles {
488 flex-direction: column;
489 }
490
491 .step .pro-box-tile {
492 width: auto;
493 }
494 }
1
2 .block-lab-notice {
3 border-left: 4px solid #7D5DEC;
4 padding: 10px 20px;
5 background: #fff;
6 box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
7 margin: 5px 0 15px;
8 }
9 .block-lab-notice h2 {
10 margin: 0.5em 0 1em;
11 }
12 .block-lab-notice p {
13 font-size: 1em;
14 }
15 .block-lab-notice .intro {
16 font-size: 1.2em;
17 line-height: 1.5em;
18 }
19 .block-lab-notice p.ps {
20 font-size: smaller;
21 }
22 .block-lab-notice .button {
23 display: inline-block;
24 position: relative;
25 background-color: #7D5DEC;
26 padding: 10px 12px;
27 margin: 1em 0;
28 height: auto;
29 border-radius: 4px;
30 border: 2px solid #7D5DEC;
31 color: #fff;
32 font-size: 13px;
33 text-decoration: none;
34 line-height: 13px;
35 cursor: pointer;
36 box-shadow: none;
37 }
38 .block-lab-notice .button:active,
39 .block-lab-notice .button:focus,
40 .block-lab-notice .button:visited {
41 color: #fff;
42 }
43 .block-lab-notice .button:hover {
44 color: #fff;
45 background-color: #6444d3;
46 border-color: #6444d3;
47 }
48 .block-lab-notice .button:nth-of-type( 2 ) {
49 margin-left: 10px;
50 }
51 .block-lab-notice .button--white {
52 background-color: #fff;
53 border-color: #fff;
54 color: #7D5DEC;
55 }
56 .block-lab-notice .button--white:active,
57 .block-lab-notice .button--white:focus,
58 .block-lab-notice .button--white:visited {
59 color: #7D5DEC;
60 }
61 .block-lab-notice .button--white:hover {
62 color: #7D5DEC;
63 background-color: #f4f4f4;
64 border-color: #f4f4f4;
65 }
66 .block-lab-notice .button_cta {
67 font-weight: 600;
68 padding: 16px 15px 15px;
69 box-shadow: 10px 10px 20px 0 rgba( 0, 0, 0, 0.2 );
70 }
71 .block-lab-welcome {
72 background-color: #7D5DEC;
73 background-image: url('https://getblocklab.com/wp-content/uploads/2019/02/Block-Lab-Pro-Hero-Background-1.svg');
74 background-size: cover;
75 background-position: center;
76 color: #fff;
77 padding: 32px 32px 6px;
78 border: none;
79 box-shadow: 10px 10px 20px 0px rgba( 121, 110, 255, 0.05 );
80 border-radius: 3px;
81 }
82 .block-lab-welcome h2 {
83 color: #fff;
84 line-height: 1em;
85 font-size: 2em;
86 font-weight: 700;
87 margin-bottom: 0.5em;
88 margin-top: 0;
89 font-style: italic;
90 }
91 .block-lab-welcome p {
92 font-size: 1.1em;
93 }
94 .block-lab-welcome .intro {
95 font-size: 1.3em;
96 }
97 .block-lab-notice p.ps {
98 opacity: 0.5;
99 }
100 .block-lab-welcome .notice-dismiss:before {
101 color: #372182;
102 }
103 .block-lab-welcome .notice-dismiss:hover:before {
104 color: #f4f4f4;
105 }
106 .block-lab-edit-block a.trash {
107 color: #444;
108 }
109 .block-lab-edit-block a.trash:hover {
110 color: #dc3232;
111 }
112 .block-lab-add-fields {
113 position: relative;
114 }
115 .block-lab-add-fields h2 {
116 font-size: 1.3em !important;
117 padding: 0 !important;
118 margin: 0.5em 0 1em !important;
119 font-weight: 600 !important;
120 }
121 .block-lab-publish {
122 margin: 1em 0 0;
123 }
124 .block-lab-publish h2 {
125 font-size: 1.3em !important;
126 padding: 0 !important;
127 margin: 0.5em 0 1em !important;
128 }
129
130 @keyframes block-lab-edit-block-point {
131 from {
132 transform: rotate(230deg) translateX(0px) translateY(0px);
133 }
134 to {
135 transform: rotate(220deg) translateX(10px) translateY(-40px);
136 }
137 }
138 @keyframes block-lab-add-fields-point {
139 from {
140 transform: rotate(30deg) translateX(0px) translateY(0px);
141 }
142 to {
143 transform: rotate(40deg) translateX(40px) translateY(20px);
144 }
145 }
1 .block-lab-settings .nav-tab-wrapper .dashicons-before:before {
2 padding: 2px 3px 0 0;
3 }
...\ No newline at end of file ...\ No newline at end of file
1 .block-lab-pro {
2 padding: 20px 20px 0 0;
3 margin: 0 auto;
4 max-width: 1280px;
5 }
6 .block-lab-pro .container {
7 margin-top: 20px;
8 display:grid;
9 grid-template-columns: repeat( 6, 1fr );
10 grid-gap: 20px;
11 }
12 .block-lab-pro .tile {
13 box-shadow: 10px 10px 20px 0px rgba( 121, 110, 255, 0.05 );
14 border-radius: 3px;
15 background-color: #fff;
16 }
17 .block-lab-pro .tile p {
18 font-size: 16px;
19 }
20 .block-lab-pro .tile_header {
21 padding: 40px 40px 0;
22 font-weight: 600;
23 display: flex;
24 justify-content: space-between;
25 align-items: center;
26 }
27 .block-lab-pro .tile_header span {
28 font-size: 16px;
29 line-height: 16px;
30 }
31 .block-lab-pro .tile_body {
32 padding: 0 40px 20px;
33 }
34 .block-lab-pro .tile_body h4 {
35 font-size: 24px;
36 line-height: 24px;
37 }
38 .block-lab-pro .tile_footer {
39 border-top: 2px solid #F7F9FC;
40 padding: 30px 40px 30px;
41 display: flex;
42 justify-content: space-between;
43 align-items: center;
44 }
45 .block-lab-pro .tile_footer.tile_footer_email {
46 background-color: #F7F9FC;
47 }
48 .block-lab-pro .tile_icon {
49 width: 180px;
50 display: block;
51 margin: auto;
52 max-width: 100%;
53 }
54 .block-lab-pro .tile_icon_wrapper {
55 width: 180px;
56 height: 180px;
57 display: block;
58 background-size: contain;
59 background-position: center;
60 background-repeat: no-repeat;
61 margin: auto;
62 }
63 .block-lab-pro .button {
64 display: inline-block;
65 position: relative;
66 background-color: #5c34e8;
67 padding: 10px 12px;
68 height: auto;
69 border-radius: 4px;
70 border: 2px solid #5c34e8;
71 color: #fff;
72 font-size: 13px;
73 text-decoration: none;
74 line-height: 13px;
75 cursor: pointer;
76 box-shadow: none;
77 }
78 .block-lab-pro .button:active,
79 .block-lab-pro .button:focus,
80 .block-lab-pro .button:visited {
81 color: #fff;
82 background-color: #5c34e8;
83 }
84 .block-lab-pro .button:hover {
85 color: #fff;
86 background-color: #5c34e8;
87 border-color: #5c34e8;
88 opacity: 0.9;
89 }
90 .block-lab-pro .button:nth-of-type( 2 ) {
91 margin-left: 10px;
92 }
93 .block-lab-pro .button--secondary {
94 background-color: #ff237e;
95 border-color: #ff237e;
96 color: #fff;
97 }
98 .block-lab-pro .button--secondary:active,
99 .block-lab-pro .button--secondary:focus,
100 .block-lab-pro .button--secondary:visited {
101 color: #fff;
102 background-color: #ff237e;
103 }
104 .block-lab-pro .button--secondary:hover {
105 background-color: #ff237e;
106 border-color: #ff237e;
107 opacity: 0.9;
108 }
109 .block-lab-pro .button--secondary-stroke {
110 border-color: #fff;
111 background-color: transparent;
112 }
113 .block-lab-pro .button--secondary-stroke:active,
114 .block-lab-pro .button--secondary-stroke:focus,
115 .block-lab-pro .button--secondary-stroke:visited {
116 color: #fff;
117 }
118 .block-lab-pro .button--secondary-stroke:hover {
119 color: #5c34e8;
120 background-color: #fff;
121 border-color: #fff;
122 }
123 .block-lab-pro .button_close {
124 background-color: rgba( 0, 0, 0, 0.1 );
125 border-radius: 50%;
126 line-height: 1.6em;
127 display: flex;
128 justify-content: center;
129 border: none;
130 color: red;
131 font-weight: 700;
132 }
133 .block-lab-pro .section_heading {
134 grid-column-start: 1;
135 grid-column-end: 7;
136 }
137 .block-lab-pro .dashboard_welcome {
138 grid-column-start: 1;
139 grid-column-end: 7;
140 background-color: #fff;
141 background-image: url('http://getblocklab.com/wp-content/uploads/2019/08/block_lab_hero_bg.svg');
142 background-size: cover;
143 background-position: top;
144 color: #000;
145 padding: 32px;
146 }
147 .block-lab-pro .dashboard_welcome .notice p {
148 color: #444;
149 font-size: 13px;
150 margin: 0.5em 0;
151 padding: 2px;
152 }
153 .block-lab-pro .dashboard_welcome h1 {
154 color: #000;
155 font-weight: 700;
156 font-size: 52px;
157 line-height: 52px;
158 margin-top: 10px;
159 text-shadow: 0 0 10px rgba(255,255,255,0.6);
160 }
161 .block-lab-pro .dashboard_welcome h1 .pro-pill {
162 font-size: .6em;
163 line-height: 1.4;
164 color: #fff;
165 background-color: #5c34e8;
166 display: inline-block;
167 border-radius: 8px;
168 padding: 0 11px;
169 top: -5px;
170 position: relative;
171 box-shadow: 0 0 10px rgba(255,255,255,0.6);
172 text-shadow: none;
173 }
174 .block-lab-pro .dashboard_welcome .tile_body {
175 display: flex;
176 justify-content: left;
177 align-items: center;
178 }
179 .block-lab-pro .dashboard_welcome p.description {
180 font-size: 18px;
181 margin-bottom: 30px;
182 max-width: 480px;
183 color: #000;
184 padding-top: 12px;
185 text-shadow: 0 0 4px #fff;
186 font-style: normal;
187 }
188 /* Tile sizing */
189 .block-lab-pro .tile_2 {
190 grid-column: span 2;
191 }
192 .block-lab-pro .tile_3 {
193 grid-column: span 3;
194 }
195 .block-lab-pro .tile_4 {
196 grid-column: span 4;
197 }
198 .block-lab-pro .tile_5 {
199 grid-column: span 5;
200 }
201 .block-lab-pro .tile_6 {
202 grid-column: span 6;
203 }
204 .block-lab-pro .ul_pills {
205 display: flex;
206 flex-wrap: wrap;
207 justify-content: flex-start;
208 }
209 .block-lab-pro .ul_pills li {
210 display: block;
211 padding: 3px 6px;
212 margin-right: 16px;
213 margin-bottom: 16px;
214 border-radius: 3px;
215 box-shadow: 0 0 0 4px rgba( 121, 110, 255, 0.1 );
216 font-weight: 600;
217 }
218 .block-lab-pro .align_center {
219 text-align: center;
220 }
221 /* Mailchimp Styling */
222 #mc_embed_signup {
223 width: 100%;
224 }
225 #mc_embed_signup #mc_embed_signup_scroll {
226 display: flex;
227 }
228 .block-lab-pro input[type=email].input {
229 background-color: #fff;
230 border: 1px solid rgba( 0, 0, 0, 0.1 );
231 border-radius: 3px;
232 padding: 9px 10px;
233 width: 100%;
234 }
235 .block-lab-pro label.input_label {
236 display: none;
237 }
238 .mc-field-group {
239 flex-grow: 1;
240 padding-right: 10px
241 }
242 .block-lab-pro .cta_license_form_wrapper {
243 display: flex;
244 align-items: center;
245 }
246 .block-lab-pro .button_cta {
247 padding: 16px 15px 15px;
248 box-shadow: 10px 10px 20px 0 rgba( 0, 0, 0, 0.2 );
249 }
250 .block-lab-pro .license_key_form {
251 display: flex;
252 background-color: rgba( 0, 0, 0, 0.06 );
253 border-radius: 3px;
254 padding: 4px;
255 }
256 .block-lab-pro .license_key_text {
257 font-size: 14px !important;
258 font-style: italic !important;
259 margin-bottom: 14px !important;
260 margin-right: 10px;
261 margin-left: 10px;
262 }
263 .block-lab-pro input[type="text"].input_text {
264 border-radius: 3px;
265 padding: 11px 10px 10px;
266 width: 100%;
267 box-shadow: none;
268 border: none;
269 background-color: rgba( 255,255,255,1 );
270 margin-right: 4px;
271 }
272 @media ( max-width: 1200px ) {
273 .block-lab-pro .cta_license_form_wrapper {
274 display: block;
275 }
276 }
277 @media ( max-width: 720px ) {
278 .block-lab-pro .container {
279 display: block;
280 }
281 .block-lab-pro .tile {
282 margin-bottom: 20px;
283 }
284 .block-lab-pro .dashboard_welcome {
285 display: block;
286 }
287 .block-lab-pro .tile_body {
288 padding-top: 40px;
289 }
290 }
1 <?php return array('dependencies' => array(), 'version' => '734085aee55b820c6613aebadcd335c2');
...\ No newline at end of file ...\ No newline at end of file
1 /**
2 * Colors
3 *
4 * The variables below are taken from Gutenberg's styling in _colors.scss.
5 *
6 * @see https://github.com/WordPress/gutenberg/blob/master/assets/stylesheets/_colors.scss
7 */
8
9 // Hugo's new WordPress shades of gray, from http://codepen.io/hugobaeta/pen/grJjVp.
10 $black: #000;
11 $dark-gray-900: #191e23;
12 $dark-gray-800: #23282d;
13 $dark-gray-700: #32373c;
14 $dark-gray-600: #40464d;
15 $dark-gray-500: #555d66; // Use this most of the time for dark items.
16 $dark-gray-400: #606a73;
17 $dark-gray-300: #6c7781; // Lightest gray that can be used for AA text contrast.
18 $dark-gray-200: #7e8993;
19 $dark-gray-150: #8d96a0; // Lightest gray that can be used for AA non-text contrast.
20 $dark-gray-100: #8f98a1;
21 $light-gray-900: #a2aab2;
22 $light-gray-800: #b5bcc2;
23 $light-gray-700: #ccd0d4;
24 $light-gray-600: #d7dade;
25 $light-gray-500: #e2e4e7; // Good for "grayed" items and borders.
26 $light-gray-400: #e8eaeb; // Good for "readonly" input fields and special text selection.
27 $light-gray-300: #edeff0;
28 $light-gray-200: #f3f4f5;
29 $light-gray-100: #f8f9f9;
30 $white: #fff;
31
32 // Dark opacities, for use with light themes.
33 $dark-opacity-900: rgba(#000510, 0.9);
34 $dark-opacity-800: rgba(#00000a, 0.85);
35 $dark-opacity-700: rgba(#06060b, 0.8);
36 $dark-opacity-600: rgba(#000913, 0.75);
37 $dark-opacity-500: rgba(#0a1829, 0.7);
38 $dark-opacity-400: rgba(#0a1829, 0.65);
39 $dark-opacity-300: rgba(#0e1c2e, 0.62);
40 $dark-opacity-200: rgba(#162435, 0.55);
41 $dark-opacity-100: rgba(#223443, 0.5);
42 $dark-opacity-light-900: rgba(#304455, 0.45);
43 $dark-opacity-light-800: rgba(#425863, 0.4);
44 $dark-opacity-light-700: rgba(#667886, 0.35);
45 $dark-opacity-light-600: rgba(#7b86a2, 0.3);
46 $dark-opacity-light-500: rgba(#9197a2, 0.25);
47 $dark-opacity-light-400: rgba(#95959c, 0.2);
48 $dark-opacity-light-300: rgba(#829493, 0.15);
49 $dark-opacity-light-200: rgba(#8b8b96, 0.1);
50 $dark-opacity-light-100: rgba(#747474, 0.05);
51 $dark-opacity-background-fill: rgba($dark-gray-700, 0.7); // Similar to $dark-opacity-light-200, but more opaque.
52
53 // Light opacities, for use with dark themes.
54 $light-opacity-900: rgba($white, 1);
55 $light-opacity-800: rgba($white, 0.9);
56 $light-opacity-700: rgba($white, 0.85);
57 $light-opacity-600: rgba($white, 0.8);
58 $light-opacity-500: rgba($white, 0.75);
59 $light-opacity-400: rgba($white, 0.7);
60 $light-opacity-300: rgba($white, 0.65);
61 $light-opacity-200: rgba($white, 0.6);
62 $light-opacity-100: rgba($white, 0.55);
63 $light-opacity-light-900: rgba($white, 0.5);
64 $light-opacity-light-800: rgba($white, 0.45);
65 $light-opacity-light-700: rgba($white, 0.4);
66 $light-opacity-light-600: rgba($white, 0.35);
67 $light-opacity-light-500: rgba($white, 0.3);
68 $light-opacity-light-400: rgba($white, 0.25);
69 $light-opacity-light-300: rgba($white, 0.2);
70 $light-opacity-light-200: rgba($white, 0.15);
71 $light-opacity-light-100: rgba($white, 0.1);
72 $light-opacity-background-fill: rgba($light-gray-300, 0.8); // Similar to $light-opacity-light-200, but more opaque.
73
74 // Additional colors.
75 // Some are from https://make.wordpress.org/design/handbook/foundations/colors/.
76 $blue-wordpress-700: #00669b;
77 $blue-dark-900: #0071a1;
78
79 $blue-medium-900: #006589;
80 $blue-medium-800: #00739c;
81 $blue-medium-700: #007fac;
82 $blue-medium-600: #008dbe;
83 $blue-medium-500: #00a0d2;
84 $blue-medium-400: #33b3db;
85 $blue-medium-300: #66c6e4;
86 $blue-medium-200: #bfe7f3;
87 $blue-medium-100: #e5f5fa;
88 $blue-medium-highlight: #b3e7fe;
89 $blue-medium-focus: #007cba;
90
91 // Alert colors.
92 $alert-yellow: #f0b849;
93 $alert-red: #d94f4f;
94 $alert-green: #4ab866;
...\ No newline at end of file ...\ No newline at end of file
1 /* global XMLHttpRequest, FormData, ajaxurl */
2
3 document.addEventListener( 'DOMContentLoaded', function() {
4 const hiddenClass = 'bl-hidden';
5
6 // In the main migration notice, on clicking 'Not Now',
7 // make an AJAX request to store the user meta to not display the notice again.
8 // Also, remove this notice and display another.
9 document.querySelector( '#bl-notice-not-now' ).addEventListener( 'click', function() {
10 const request = new XMLHttpRequest();
11 const data = new FormData();
12 data.append( 'action', 'bl_dismiss_migration_notice' );
13 data.append( 'bl-migration-nonce-name', document.querySelector( '#bl-migration-nonce-name' ).value );
14
15 request.open( 'POST', ajaxurl, true );
16 request.send( data );
17
18 // Remove this notice.
19 const notice = document.querySelector( '#bl-migration-notice' );
20 notice.parentNode.removeChild( notice );
21
22 // Display the 'Not Now' notice.
23 document.querySelector( '#bl-not-now-notice' ).classList.remove( hiddenClass );
24 } );
25
26 // In the 'Not Now' notice, on clicking 'OK', hide the notice.
27 document.querySelector( '#bl-notice-ok' ).addEventListener( 'click', function() {
28 document.querySelector( '#bl-not-now-notice' ).classList.add( hiddenClass );
29 } );
30 } );
1 <?php return array('dependencies' => array('react', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-element', 'wp-polyfill'), 'version' => 'd880838e7a15ba0cee2603309a597941');
...\ No newline at end of file ...\ No newline at end of file
1 <?php return array('dependencies' => array('lodash', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-deprecated', 'wp-dom', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-keycodes'), 'version' => '71acd0fd9d80bf1688a4e97931170ca5');
...\ No newline at end of file ...\ No newline at end of file
This diff could not be displayed because it is too large.
1 /* global blockLabMigration */
2 // @ts-check
3
4 /**
5 * External dependencies
6 */
7 import * as React from 'react';
8
9 /**
10 * WordPress dependencies
11 */
12 import { useState } from '@wordpress/element';
13
14 /**
15 * Internal dependencies
16 */
17 import { Intro } from './';
18 import { BackUpSite, GetGenesisPro, InstallActivateGcb, MigrateBlocks, UpdateHooks } from './steps';
19 import { FIRST_STEP_NUMBER } from '../constants';
20
21 /**
22 * The migration admin page.
23 *
24 * @return {React.ReactElement} The component for the admin page.
25 */
26 const App = () => {
27 const [ currentStepIndex, setStepIndex ] = useState( FIRST_STEP_NUMBER );
28
29 /**
30 * Sets the step index to the previous step.
31 */
32 const goToPrevious = () => {
33 setStepIndex( currentStepIndex - 1 );
34 };
35
36 /**
37 * Sets the step index to the next step.
38 */
39 const goToNext = () => {
40 setStepIndex( currentStepIndex + 1 );
41 };
42
43 const steps = [
44 BackUpSite,
45 UpdateHooks,
46 InstallActivateGcb,
47 MigrateBlocks,
48 ];
49
50 // Conditionally add the step to get Genesis Pro.
51 // @ts-ignore
52 if ( blockLabMigration.isPro ) {
53 steps.unshift( GetGenesisPro );
54 }
55
56 return (
57 <div className="bl-migration__content-wrapper">
58 <div className="container bl-migration__content-container">
59 <Intro />
60 {
61 steps.map( ( MigrationStep, index ) => {
62 const stepIndex = FIRST_STEP_NUMBER + index;
63 const isStepActive = currentStepIndex === stepIndex;
64 const isStepComplete = currentStepIndex > stepIndex;
65
66 return (
67 <MigrationStep
68 key={ `bl-migration-step-${ stepIndex }` }
69 { ...{ currentStepIndex, goToNext, goToPrevious, isStepActive, isStepComplete, stepIndex } }
70 />
71 );
72 } )
73 }
74 </div>
75 </div>
76 );
77 };
78
79 export default App;
1 // @ts-check
2
3 /**
4 * External dependencies
5 */
6 import * as React from 'react';
7
8 /**
9 * WordPress dependencies
10 */
11 import { __ } from '@wordpress/i18n';
12 import { useState } from '@wordpress/element';
13
14 /**
15 * @typedef ButtonNextProps
16 * @property {React.EventHandler<React.MouseEvent<HTMLButtonElement, MouseEvent>>} onClick The click handler.
17 * @property {string} [checkboxLabel] The label of the checkbox, if there should be one.
18 * @property {number} stepIndex The index of this button's step.
19 */
20
21 /**
22 * The next button.
23 *
24 * @param {ButtonNextProps} props The component props.
25 * @return {React.ReactElement} The component for the step content.
26 */
27 const ButtonNext = ( { onClick, checkboxLabel, stepIndex } ) => {
28 const [ isCheckboxChecked, setCheckboxChecked ] = useState( false );
29
30 // If there's no label for the 'confirmation' checkbox, return a simple button.
31 if ( ! checkboxLabel ) {
32 return <button className="btn" onClick={ onClick }>{ __( 'Next Step', 'block-lab' ) }</button>;
33 }
34
35 const inputId = `bl-migration-check-${ stepIndex }`;
36 return (
37 <>
38 <form>
39 <input
40 id={ inputId }
41 type="checkbox"
42 onClick={ () => {
43 setCheckboxChecked( ! isCheckboxChecked );
44 } }
45 />
46 <label htmlFor={ inputId } className="ml-2 font-medium">{ checkboxLabel }</label>
47 </form>
48 <button
49 className="btn"
50 onClick={ onClick }
51 disabled={ ! isCheckboxChecked }
52 >
53 { __( 'Next Step', 'block-lab' ) }
54 </button>
55 </>
56
57 );
58 };
59
60 export default ButtonNext;
1 // @ts-check
2
3 /**
4 * External dependencies
5 */
6 import * as React from 'react';
7
8 /**
9 * WordPress dependencies
10 */
11 import { __ } from '@wordpress/i18n';
12
13 /**
14 * @typedef ButtonPreviousProps
15 * @property {React.EventHandler<React.MouseEvent<HTMLButtonElement, MouseEvent>>} onClick The click handler.
16 */
17
18 /**
19 * The previous button.
20 *
21 * @param {ButtonPreviousProps} props The component props.
22 * @return {React.ReactElement} The component for the step content.
23 */
24 const ButtonPrevious = ( { onClick } ) => {
25 return (
26 <button className="btn btn-secondary" onClick={ onClick }>
27 { __( 'Previous', 'block-lab' ) }
28 </button>
29 );
30 };
31
32 export default ButtonPrevious;
1 export { default as App } from './app';
2 export { default as ButtonNext } from './button-next';
3 export { default as ButtonPrevious } from './button-previous';
4 export { default as Intro } from './intro';
5 export { default as Step } from './step';
6 export { default as StepContent } from './step-content';
7 export { default as StepFooter } from './step-footer';
8 export { default as StepIcon } from './step-icon';
1 // @ts-check
2
3 /**
4 * External dependencies
5 */
6 import * as React from 'react';
7
8 /**
9 * WordPress dependencies
10 */
11 import { __ } from '@wordpress/i18n';
12
13 /**
14 * The introduction to the migration.
15 *
16 * @return {React.ReactElement} The introduction to the migration.
17 */
18 const Intro = () => {
19 const developerNoticeUrl = 'https://getblocklab.com/migrating-to-genesis-custom-blocks/';
20 const announcementUrl = 'https://getblocklab.com/the-block-lab-team-are-joining-wp-engine/';
21
22 return (
23 <>
24 <div>
25 <h1>{ __( 'Migrating to Genesis Custom Blocks', 'block-lab' ) }</h1>
26 <p>{ __( 'In April, the Block Lab team joined the Genesis team at WP Engine. With our full-time focus, we’re very excited about the future of custom block tooling in WordPress. You can read more about that moment in this', 'block-lab' ) } <a target="_blank" rel="noopener noreferrer" className="text-purple-600 underline hover:text-purple-700" href={ announcementUrl }>{ __( 'announcement post', 'block-lab' ) }.</a></p>
27 <p>
28 { __( 'As part of this move, we have been working on a new plugin that is based on what we developed at Block Lab.', 'block-lab' ) }
29 &nbsp;
30 { __( 'Genesis Custom Blocks is now the home of all our custom block efforts and what we have planned is very very cool!', 'block-lab' ) }
31 &nbsp;
32 { __( 'Version 1.0 of this plugin is now released and has full feature parity with Block Lab.', 'block-lab' ) }
33 </p>
34 <p>{ __( 'To continue receiving the best of what our team is building, we encourage you to migrate over. Our migration tool makes this nice and easy, and for the majority of use cases, completely automated.', 'block-lab' ) }</p>
35 <div className="dev-notice">
36 <svg fill="currentColor" viewBox="0 0 20 20">
37 <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd"></path>
38 </svg>
39 <span>{ __( 'Need to let the developer for this site know about this? Send them this link.', 'block-lab' ) }</span>
40 <a href={ developerNoticeUrl } target="_blank" rel="noopener noreferrer" className="btn">
41 <span>{ __( 'Developer Notice', 'block-lab' ) }</span>
42 <svg fill="currentColor" viewBox="0 0 20 20">
43 <path d="M11 3a1 1 0 100 2h3.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"></path>
44 <path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"></path>
45 </svg>
46 </a>
47 </div>
48 </div>
49 <h2>{ __( "Let's Migrate", 'block-lab' ) }</h2>
50 </>
51 );
52 };
53
54 export default Intro;
1 // @ts-check
2
3 /**
4 * External dependencies
5 */
6 import * as React from 'react';
7
8 /**
9 * @typedef StepContentProps
10 * @property {React.ReactNode} children The component's children.
11 * @property {string} heading The step heading.
12 * @property {boolean} isStepActive Whether this step is active.
13 */
14
15 /**
16 * The content of the step.
17 *
18 * @param {StepContentProps} props The component props.
19 * @return {React.ReactElement} The component for the step content.
20 */
21 const StepContent = ( { children, heading, isStepActive } ) => {
22 return (
23 <div className="step-content">
24 <h3>{ heading }</h3>
25 { isStepActive && children }
26 </div>
27 );
28 };
29
30 export default StepContent;
1 // @ts-check
2
3 /**
4 * External dependencies
5 */
6 import * as React from 'react';
7
8 /**
9 * @typedef StepFooterProps
10 * @property {React.ReactNode} children The component's children.
11 */
12
13 /**
14 * The footer of the step.
15 *
16 * @param {StepFooterProps} props The component props.
17 * @return {React.ReactElement} The component for the step content.
18 */
19 const StepFooter = ( { children } ) => {
20 return (
21 <div className="step-footer">
22 { children }
23 </div>
24 );
25 };
26
27 export default StepFooter;
1 // @ts-check
2
3 /**
4 * External dependencies
5 */
6 import * as React from 'react';
7
8 /**
9 * WordPress dependencies
10 */
11 import { __ } from '@wordpress/i18n';
12
13 /**
14 * @typedef StepIconProps
15 * @property {number} index The index of this icon's step.
16 * @property {boolean} isComplete Whether this icon's step is active.
17 */
18
19 /**
20 * The icon of the step number.
21 *
22 * @param {StepIconProps} props The component props.
23 * @return {React.ReactElement} props The icon component.
24 */
25 const StepIcon = ( { index, isComplete } ) => {
26 const titleId = `bl-migration-icon-${ index }`;
27 const svg = (
28 <svg fill="currentColor" viewBox="0 0 20 20" aria-labelledby={ titleId }>
29 <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd"></path>
30 <title id={ titleId }>{ __( 'Step completed', 'block-lab' ) }</title>
31 </svg>
32 );
33
34 return (
35 <div className="step-icon">
36 { isComplete ? svg : index }
37 </div>
38 );
39 };
40
41 export default StepIcon;
1 // @ts-check
2
3 /**
4 * External dependencies
5 */
6 import classNames from 'classnames';
7 import * as React from 'react';
8
9 /**
10 * @typedef StepProps
11 * @property {boolean} isActive Whether this step is active.
12 * @property {boolean} isComplete Whether this step is complete.
13 * @property {React.ReactNode} children The children of the component.
14 */
15
16 /**
17 * Migration step.
18 *
19 * @param {StepProps} props The component props.
20 */
21 const Step = ( { isActive, isComplete, children } ) => {
22 return (
23 <div
24 className={ classNames( 'step', {
25 'step--active': isActive,
26 'step--complete': isComplete,
27 } ) }
28 >
29 { children }
30 </div>
31 );
32 };
33
34 export default Step;
1 // @ts-check
2
3 /**
4 * External dependencies
5 */
6 import * as React from 'react';
7
8 /**
9 * WordPress dependencies
10 */
11 import { __ } from '@wordpress/i18n';
12
13 /**
14 * Internal dependencies
15 */
16 import { ButtonNext, ButtonPrevious, Step, StepContent, StepFooter, StepIcon } from '../';
17 import { FIRST_STEP_NUMBER } from '../../constants';
18
19 /**
20 * @typedef {Object} BackUpSiteProps The component props.
21 * @property {boolean} isStepActive Whether this step is active.
22 * @property {boolean} isStepComplete Whether this step is complete.
23 * @property {number} stepIndex The step index of this step.
24 * @property {React.EventHandler<React.MouseEvent<HTMLButtonElement, MouseEvent>>} goToNext Goes to the next step.
25 * @property {React.EventHandler<React.MouseEvent<HTMLButtonElement, MouseEvent>>} goToPrevious Goes to the previous step.
26 */
27
28 /**
29 * The step that prompts to back up the site.
30 *
31 * @param {BackUpSiteProps} Props The component props.
32 * @return {React.ReactElement} The component to prompt to back up the site.
33 */
34 const BackUpSite = ( { isStepActive, isStepComplete, goToNext, goToPrevious, stepIndex } ) => {
35 const isFirstStep = FIRST_STEP_NUMBER === stepIndex;
36
37 return (
38 <Step isActive={ isStepActive } isComplete={ isStepComplete }>
39 <StepIcon
40 index={ stepIndex }
41 isComplete={ isStepComplete }
42 />
43 <StepContent
44 heading={ __( 'Back Up Your Site', 'block-lab' ) }
45 isStepActive={ isStepActive }
46 >
47 <p>{ __( 'Migrating from Block Lab to Genesis Custom Blocks is a one-way action. It can’t be undone. Please back up your site before you begin, just in case you need to roll it back.', 'block-lab' ) }</p>
48 <StepFooter>
49 { ! isFirstStep && <ButtonPrevious onClick={ goToPrevious } /> }
50 <ButtonNext
51 checkboxLabel={ __( 'I have backed up my site.', 'block-lab' ) }
52 onClick={ goToNext }
53 stepIndex={ stepIndex }
54 />
55 </StepFooter>
56 </StepContent>
57 </Step>
58 );
59 };
60
61 export default BackUpSite;
1 export { default as BackUpSite } from './back-up-site';
2 export { default as GetGenesisPro } from './get-genesis-pro';
3 export { default as InstallActivateGcb } from './install-activate-gcb';
4 export { default as MigrateBlocks } from './migrate-blocks';
5 export { default as UpdateHooks } from './update-hooks';
1 // @ts-check
2
3 /**
4 * External dependencies
5 */
6 import * as React from 'react';
7
8 /**
9 * WordPress dependencies
10 */
11 import { speak } from '@wordpress/a11y';
12 import apiFetch from '@wordpress/api-fetch';
13 import { Spinner } from '@wordpress/components';
14 import { useState } from '@wordpress/element';
15 import { __ } from '@wordpress/i18n';
16
17 /**
18 * Internal dependencies
19 */
20 import { ButtonNext, Step, StepContent, StepFooter, StepIcon } from '../';
21
22 /**
23 * @typedef {Object} InstallActivateGcbProps The component props.
24 * @property {React.EventHandler<React.MouseEvent<HTMLButtonElement, MouseEvent>>} goToNext Goes to the next step.
25 * @property {boolean} isStepActive Whether this step is active.
26 * @property {boolean} isStepComplete Whether this step is complete.
27 * @property {number} stepIndex The step index of this step.
28 */
29
30 /**
31 * Installs and activates GCB.
32 *
33 * @param {InstallActivateGcbProps} Props The component props.
34 * @return {React.ReactElement} The component to activate Genesis Custom Blocks.
35 */
36 const InstallActivateGcb = ( { goToNext, isStepActive, isStepComplete, stepIndex } ) => {
37 const [ isInProgress, setIsInProgress ] = useState( false );
38 const [ isError, setIsError ] = useState( false );
39 const [ errorMessage, setErrorMessage ] = useState( '' );
40 const [ isSuccess, setIsSuccess ] = useState( false );
41
42 /**
43 * Installs and activates Genesis Custom Blocks.
44 */
45 const installAndActivateGcb = async () => {
46 speak( __( 'The installation is now in progress', 'block-lab' ) );
47 setIsInProgress( true );
48 setIsError( false );
49 setErrorMessage( '' );
50
51 await apiFetch( {
52 path: '/block-lab/install-activate-gcb',
53 method: 'POST',
54 } ).then( () => {
55 speak( __( 'Success! Genesis Custom Blocks is installed and activated.', 'block-lab' ) );
56 setIsSuccess( true );
57 } ).catch( ( result ) => {
58 speak( __( 'The installation and activation failed with the following error:', 'block-lab' ) );
59 if ( result.hasOwnProperty( 'message' ) ) {
60 speak( result.message );
61 setErrorMessage( result.message );
62 }
63 setIsSuccess( false );
64 setIsError( true );
65 } );
66
67 setIsInProgress( false );
68 };
69
70 return (
71 <Step isActive={ isStepActive } isComplete={ isStepComplete }>
72 <StepIcon
73 index={ stepIndex }
74 isComplete={ isStepComplete }
75 />
76 <StepContent
77 heading={ __( 'Install And Activate Genesis Custom Blocks', 'block-lab' ) }
78 isStepActive={ isStepActive }
79 >
80 { isInProgress && (
81 <>
82 <Spinner />
83 <p>{ __( 'Installing and activating Genesis Custom Blocks…', 'block-lab' ) }</p>
84 </>
85 ) }
86 { !! errorMessage && (
87 <div className="bl-migration__error">
88 <p>{ __( 'The following error ocurred:', 'block-lab' ) }</p>
89 <p>{ errorMessage }</p>
90 </div>
91 ) }
92 { ! isInProgress && ! isSuccess && (
93 <button
94 className="btn"
95 onClick={ installAndActivateGcb }
96 >
97 { isError ? __( 'Try Again', 'block-lab' ) : __( 'Install and activate', 'block-lab' ) }
98 </button>
99 ) }
100 { isSuccess && (
101 <>
102 <p>{ __( 'Success! Genesis Custom Blocks is installed and activated.', 'block-lab' ) }</p>
103 <StepFooter>
104 <ButtonNext
105 onClick={ goToNext }
106 stepIndex={ stepIndex }
107 />
108 </StepFooter>
109 </>
110 ) }
111 </StepContent>
112 </Step>
113 );
114 };
115
116 export default InstallActivateGcb;
1 // @ts-check
2 /* global blockLabMigration */
3
4 /**
5 * External dependencies
6 */
7 import * as React from 'react';
8
9 /**
10 * WordPress dependencies
11 */
12 import { speak } from '@wordpress/a11y';
13 import apiFetch from '@wordpress/api-fetch';
14 import { Spinner } from '@wordpress/components';
15 import { useState } from '@wordpress/element';
16 import { __ } from '@wordpress/i18n';
17
18 /**
19 * Internal dependencies
20 */
21 import { Step, StepContent, StepFooter, StepIcon } from '../';
22
23 /**
24 * @typedef {Object} MigrateBlocksProps The component props.
25 * @property {Function} goToNext Goes to the next step.
26 * @property {boolean} isStepActive Whether this step is active.
27 * @property {boolean} isStepComplete Whether this step is complete.
28 * @property {number} stepIndex The step index of this step.
29 */
30
31 /**
32 * The step that migrates the blocks.
33 *
34 * @param {MigrateBlocksProps} Props The component props.
35 * @return {React.ReactElement} The component to prompt to migrate the post content.
36 */
37 const MigrateBlocks = ( { isStepActive, isStepComplete, stepIndex } ) => {
38 const [ currentBlockMigrationStep, setCurrentBlockMigrationStep ] = useState( 0 );
39 const [ isInProgress, setIsInProgress ] = useState( false );
40 const [ isError, setIsError ] = useState( false );
41 const [ errorMessage, setErrorMessage ] = useState( '' );
42 const [ isSuccess, setIsSuccess ] = useState( false );
43
44 const migrationLabels = [
45 __( 'Migrating your blocks…', 'block-lab' ),
46 __( 'Migrating your post content…', 'block-lab' ),
47 ];
48
49 /**
50 * Migrates the custom post type, then chains post content migration to the callback.
51 */
52 const migrateCpt = async () => {
53 await apiFetch( {
54 path: '/block-lab/migrate-post-type',
55 method: 'POST',
56 } ).then( async () => {
57 setCurrentBlockMigrationStep( 1 );
58 await migratePostContent();
59 } ).catch( ( result ) => {
60 if ( result.hasOwnProperty( 'message' ) ) {
61 setErrorMessage( result.message );
62 }
63 speak( __( 'The migration failed in the CPT migration', 'block-lab' ) );
64 setIsError( true );
65 setIsInProgress( false );
66 } );
67 };
68
69 /**
70 * Migrates the post content.
71 */
72 const migratePostContent = async () => {
73 // Used for a 504 Gateway Timeout Error, but could also be for other errors.
74 const timeoutErrorCode = 'invalid_json';
75
76 await apiFetch( {
77 path: '/block-lab/migrate-post-content',
78 method: 'POST',
79 } ).then( () => {
80 speak( __( 'The migration was successful!', 'block-lab' ) );
81 setIsSuccess( true );
82 } ).catch( async ( result ) => {
83 if ( result.hasOwnProperty( 'code' ) && timeoutErrorCode === result.code ) {
84 await migratePostContent();
85 return;
86 } else if ( result.hasOwnProperty( 'message' ) ) {
87 setErrorMessage( result.message );
88 }
89
90 speak( __( 'The migration failed in the post content migration', 'block-lab' ) );
91 setIsError( true );
92 } );
93 };
94
95 /**
96 * Handles all of the migration for this step.
97 */
98 const migrate = async () => {
99 speak( __( 'The migration is now in progress', 'block-lab' ) );
100 setErrorMessage( '' );
101 setIsInProgress( true );
102
103 // The post content migration is chained to the callback in then().
104 await migrateCpt();
105
106 setIsInProgress( false );
107 };
108
109 return (
110 <Step isActive={ isStepActive } isComplete={ isStepComplete }>
111 <StepIcon
112 index={ stepIndex }
113 isComplete={ isStepComplete }
114 />
115 <StepContent
116 heading={ __( 'Migrate Your Blocks', 'block-lab' ) }
117 isStepActive={ isStepActive }
118 >
119 { ! isSuccess && <p>{ __( "Okay! Everything is ready. Let's do this. While the migration is underway, don't leave this page.", 'block-lab' ) }</p> }
120 { !! errorMessage && (
121 <div className="bl-migration__error">
122 <p>{ __( 'The following error ocurred:', 'block-lab' ) }</p>
123 <p>{ errorMessage }</p>
124 </div>
125 ) }
126 { isInProgress && (
127 <>
128 <Spinner />
129 <p>{ migrationLabels[ currentBlockMigrationStep ] }</p>
130 </>
131 ) }
132 { ! isInProgress && ! isSuccess && (
133 <button
134 className="btn"
135 onClick={ migrate }
136 >
137 { isError ? __( 'Try Again', 'block-lab' ) : __( 'Migrate Now', 'block-lab' ) }
138 </button>
139 ) }
140 { isSuccess && (
141 <>
142 <p>
143 <span role="img" aria-label={ __( 'party emoji', 'block-lab' ) }>🎉</span>
144 &nbsp;
145 { __( 'The migration completed successfully! Time to say goodbye to Block Lab (it’s been fun!) and step into the FUTURE', 'block-lab' ) }
146 &nbsp;
147 <span className="message-future">{ __( 'FUTURE', 'block-lab' ) }</span>
148 &nbsp;
149 <sub>{ __( 'FUTURE', 'block-lab' ) }</sub>.
150 </p>
151 <StepFooter>
152 { /* @ts-ignore */ }
153 <a href={ blockLabMigration.gcbUrl } className="btn">
154 { __( 'Go To Genesis Custom Blocks', 'block-lab' ) }
155 </a>
156 </StepFooter>
157 </>
158 ) }
159 </StepContent>
160 </Step>
161 );
162 };
163
164 export default MigrateBlocks;
1 /**
2 * External dependencies
3 */
4 import { render } from '@testing-library/react';
5 import user from '@testing-library/user-event';
6
7 /**
8 * Internal dependencies
9 */
10 import { BackUpSite } from '../';
11
12 test( 'back up site migration step', async () => {
13 const props = {
14 goToNext: jest.fn(),
15 isStepActive: true,
16 isStepComplete: false,
17 stepIndex: 1,
18 };
19 const { getByLabelText, getByText } = render( <BackUpSite { ...props } /> );
20
21 getByText( /back up your site/ );
22 getByText( props.stepIndex.toString() );
23
24 // Because the 'confirm' checkbox isn't checked, the 'next' button should be disabled.
25 user.click( getByText( 'Next Step' ) );
26 expect( props.goToNext ).not.toHaveBeenCalled();
27
28 // Now that the 'confirm' checkbox is checked, the 'next' button should work.
29 user.click( getByLabelText( 'I have backed up my site.' ) );
30 user.click( getByText( 'Next Step' ) );
31 expect( props.goToNext ).toHaveBeenCalled();
32 } );
1 /**
2 * External dependencies
3 */
4 import { fireEvent, render, waitFor } from '@testing-library/react';
5 import user from '@testing-library/user-event';
6
7 /**
8 * WordPress dependencies
9 */
10 import apiFetch from '@wordpress/api-fetch';
11
12 /**
13 * Internal dependencies
14 */
15 import { GetGenesisPro } from '../';
16
17 jest.mock( '@wordpress/api-fetch' );
18 window.blockLabMigration = {};
19
20 test( 'get Genesis Pro migration step', async () => {
21 apiFetch.mockImplementation( () => new Promise( ( resolve ) => resolve( { success: true } ) ) );
22
23 const props = {
24 goToNext: jest.fn(),
25 goToPrevious: jest.fn(),
26 isStepActive: true,
27 isStepComplete: false,
28 stepIndex: 1,
29 };
30 const { getByText, getByRole } = render( <GetGenesisPro { ...props } /> );
31
32 // Because the checkbox isn't checked, the 'next' button should be disabled.
33 user.click( getByText( 'Next Step' ) );
34 expect( props.goToNext ).not.toHaveBeenCalled();
35
36 fireEvent.change(
37 getByRole( 'textbox' ),
38 { target: { value: '1234567' } }
39 );
40
41 await waitFor( () =>
42 user.click( getByText( 'Save' ) )
43 );
44 getByText( 'Thanks! Your key is valid, and has been saved.' );
45
46 user.click( getByText( 'Next Step' ) );
47 expect( props.goToNext ).toHaveBeenCalled();
48 } );
1 /**
2 * External dependencies
3 */
4 import { render } from '@testing-library/react';
5
6 /**
7 * Internal dependencies
8 */
9 import { InstallActivateGcb } from '../';
10
11 global.blockLabMigration = {
12 activateUrl: 'https://example.com',
13 };
14
15 test( 'activate gcb migration step', async () => {
16 const props = {
17 isStepActive: true,
18 isStepComplete: false,
19 stepIndex: 5,
20 };
21 const { getByText } = render( <InstallActivateGcb { ...props } /> );
22
23 expect( getByText( props.stepIndex.toString() ) ).toBeInTheDocument();
24 } );
1 /**
2 * External dependencies
3 */
4 import { render, waitFor } from '@testing-library/react';
5 import user from '@testing-library/user-event';
6
7 /**
8 * WordPress dependencies
9 */
10 import apiFetch from '@wordpress/api-fetch';
11
12 /**
13 * Internal dependencies
14 */
15 import { MigrateBlocks } from '../';
16
17 jest.mock( '@wordpress/api-fetch' );
18 global.blockLabMigration = {
19 gcbUrl: 'https://example.com',
20 };
21
22 test( 'migrate blocks step', async () => {
23 apiFetch.mockImplementation( () => new Promise( ( resolve ) => resolve( { success: true } ) ) );
24 const props = {
25 currentStepIndex: 4,
26 goToNext: jest.fn(),
27 isStepActive: true,
28 isStepComplete: false,
29 stepIndex: 4,
30 };
31
32 const { getByText } = render( <MigrateBlocks { ...props } /> );
33
34 getByText( /migrate your blocks/i );
35 getByText( props.stepIndex.toString() );
36
37 await waitFor( () =>
38 user.click( getByText( 'Migrate Now' ) )
39 );
40
41 expect( getByText( 'The migration was successful!' ) ).toBeInTheDocument();
42 } );
1 /**
2 * External dependencies
3 */
4 import { render, screen } from '@testing-library/react';
5 import user from '@testing-library/user-event';
6
7 /**
8 * Internal dependencies
9 */
10 import { UpdateHooks } from '../';
11
12 describe( 'update hooks migration step', () => {
13 it( 'displays step content when this step is active', async () => {
14 const props = {
15 goToNext: jest.fn(),
16 goToPrevious: jest.fn(),
17 isStepActive: true,
18 isStepComplete: false,
19 stepIndex: 2,
20 };
21 const { getByText } = render( <UpdateHooks { ...props } /> );
22
23 getByText( 'Update Hooks & API' );
24 getByText( props.stepIndex.toString() );
25
26 // It should always be possible to click the 'previous' button.
27 user.click( getByText( 'Previous' ) );
28 expect( props.goToPrevious ).toHaveBeenCalled();
29 } );
30
31 it( 'does not display content when this step is not active', async () => {
32 const props = {
33 goToNext: jest.fn(),
34 goToPrevious: jest.fn(),
35 isStepActive: false,
36 isStepComplete: false,
37 stepIndex: 2,
38 };
39 const { getByText } = render( <UpdateHooks { ...props } /> );
40
41 // The heading should still display.
42 getByText( 'Update Hooks & API' );
43 getByText( props.stepIndex.toString() );
44
45 // The content of the step should now display, as it's not active.
46 expect( screen.queryByText( 'Previous' ) ).not.toBeInTheDocument();
47 } );
48 } );
1 // @ts-check
2
3 /**
4 * External dependencies
5 */
6 import * as React from 'react';
7
8 /**
9 * WordPress dependencies
10 */
11 import { __ } from '@wordpress/i18n';
12
13 /**
14 * Internal dependencies
15 */
16 import { ButtonNext, ButtonPrevious, Step, StepContent, StepFooter, StepIcon } from '../';
17
18 /**
19 * @typedef {Object} UpdateHooksProps The component props.
20 * @property {boolean} isStepActive Whether this step is active.
21 * @property {boolean} isStepComplete Whether this step is complete.
22 * @property {number} stepIndex The step index of this step.
23 * @property {React.EventHandler<React.MouseEvent<HTMLButtonElement, MouseEvent>>} goToNext Goes to the next step.
24 * @property {React.EventHandler<React.MouseEvent<HTMLButtonElement, MouseEvent>>} goToPrevious Goes to the next step.
25 */
26
27 /**
28 * The step that prompts to update hooks.
29 *
30 * @param {UpdateHooksProps} Props The component props.
31 * @return {React.ReactElement} The component to prompt to back up the site.
32 */
33 const UpdateHooks = ( { isStepActive, isStepComplete, stepIndex, goToNext, goToPrevious } ) => {
34 const hooksDetailsUrl = 'https://developer.wpengine.com/genesis-custom-blocks/block-lab-hook-compatibility/';
35 const phpApiDetailsUrl = 'https://developer.wpengine.com/genesis-custom-blocks/block-lab-php-api-compatibility/';
36
37 return (
38 <Step isActive={ isStepActive } isComplete={ isStepComplete }>
39 <StepIcon
40 index={ stepIndex }
41 isComplete={ isStepComplete }
42 />
43 <StepContent
44 heading={ __( 'Update Hooks & API', 'block-lab' ) }
45 isStepActive={ isStepActive }
46 >
47 <p>{ __( 'In most cases, you won’t have to worry about this step. However, there are some instances that will require manual edits to your custom block related files. These are:', 'block-lab' ) }</p>
48 <ul className="list-disc list-inside mt-2">
49 <li>
50 <b>{ __( 'Hooks', 'block-lab' ) }</b> - { __( 'The Block Lab hook names have changed. If you’ve extended Block Lab with custom functionality using these, you’ll need to make some small changes.', 'block-lab' ) }
51 &nbsp;
52 <a
53 href={ hooksDetailsUrl }
54 target="_blank"
55 rel="noopener noreferrer"
56 aria-label={ __( 'More details on the hooks', 'genesis-custom-blocks' ) }
57 >
58 { __( 'More details here.', 'block-lab' ) }
59 </a>
60 </li>
61 <li>
62 <b>{ __( 'API', 'block-lab' ) }</b> - { __( 'If you use Block Lab’s PHP API or JSON API to register and configure your custom blocks, you’ll need to make some small changes.', 'block-lab' ) }
63 &nbsp;
64 <a
65 href={ phpApiDetailsUrl }
66 target="_blank"
67 rel="noopener noreferrer"
68 aria-label={ __( 'More details on the PHP API', 'genesis-custom-blocks' ) }
69 >
70 { __( 'More details here.', 'block-lab' ) }
71 </a>
72 </li>
73 </ul>
74 <StepFooter>
75 <ButtonPrevious onClick={ goToPrevious } />
76 <ButtonNext
77 checkboxLabel={ __( "I'm all okay on the hooks and API front.", 'block-lab' ) }
78 onClick={ goToNext }
79 stepIndex={ stepIndex }
80 />
81 </StepFooter>
82 </StepContent>
83 </Step>
84 );
85 };
86
87 export default UpdateHooks;
1 /**
2 * External dependencies
3 */
4 import { render } from '@testing-library/react';
5
6 /**
7 * Internal dependencies
8 */
9 import App from '../app';
10
11 global.blockLabMigration = {
12 isPro: true,
13 };
14
15 test( 'migration app', async () => {
16 const { getByText } = render( <App /> );
17
18 expect( getByText( 'Migrating to Genesis Custom Blocks' ) ).toBeInTheDocument();
19 expect( getByText( 'Need to let the developer for this site know about this? Send them this link.' ) ).toBeInTheDocument();
20 } );
1 /**
2 * WordPress dependencies
3 */
4 import domReady from '@wordpress/dom-ready';
5 import { render } from '@wordpress/element';
6
7 /**
8 * Internal dependencies
9 */
10 import { App } from './components';
11
12 // Renders the app in the container.
13 domReady( () => {
14 render(
15 <App />,
16 document.querySelector( '.bl-migration__content' )
17 );
18 } );
1 <?php
2 /**
3 * WP Admin resources.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Admin;
11
12 use Block_Lab\Component_Abstract;
13 use Block_Lab\Admin\Migration\Api;
14 use Block_Lab\Admin\Migration\Subscription_Api;
15 use Block_Lab\Admin\Migration\Notice;
16 use Block_Lab\Admin\Migration\Submenu;
17
18 /**
19 * Class Admin
20 */
21 class Admin extends Component_Abstract {
22
23 /**
24 * JSON import.
25 *
26 * @var Import
27 */
28 public $import;
29
30 /**
31 * Plugin license.
32 *
33 * @var License
34 */
35 public $license;
36
37 /**
38 * User onboarding.
39 *
40 * @var Onboarding
41 */
42 public $onboarding;
43
44 /**
45 * Plugin settings.
46 *
47 * @var Settings
48 */
49 public $settings;
50
51 /**
52 * Plugin upgrade.
53 *
54 * @var Upgrade
55 */
56 public $upgrade;
57
58 /**
59 * The migration API.
60 *
61 * @var Api
62 */
63 private $api;
64
65 /**
66 * THe subscription API for the migration.
67 *
68 * @var Subscription_Api
69 */
70 private $subscription_api;
71
72 /**
73 * The migration notice.
74 *
75 * @var Notice
76 */
77 private $notice;
78
79 /**
80 * The migration submenu under the Block Lab menu item.
81 *
82 * @var Submenu
83 */
84 private $submenu;
85
86 /**
87 * Initialise the Admin component.
88 */
89 public function init() {
90 $this->settings = new Settings();
91 block_lab()->register_component( $this->settings );
92
93 $this->license = new License();
94 block_lab()->register_component( $this->license );
95
96 $this->onboarding = new Onboarding();
97 block_lab()->register_component( $this->onboarding );
98
99 $this->api = new Api();
100 block_lab()->register_component( $this->api );
101
102 $this->subscription_api = new Subscription_Api();
103 block_lab()->register_component( $this->subscription_api );
104
105 $this->notice = new Notice();
106 block_lab()->register_component( $this->notice );
107
108 $this->submenu = new Submenu();
109 block_lab()->register_component( $this->submenu );
110
111 $show_pro_nag = apply_filters( 'block_lab_show_pro_nag', false );
112 if ( $show_pro_nag && ! block_lab()->is_pro() ) {
113 $this->upgrade = new Upgrade();
114 block_lab()->register_component( $this->upgrade );
115 } else {
116 $this->maybe_settings_redirect();
117 }
118
119 if ( defined( 'WP_LOAD_IMPORTERS' ) && WP_LOAD_IMPORTERS ) {
120 $this->import = new Import();
121 block_lab()->register_component( $this->import );
122 }
123 }
124
125 /**
126 * Register any hooks that this component needs.
127 */
128 public function register_hooks() {
129 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
130 }
131
132 /**
133 * Enqueue scripts and styles used globally in the WP Admin.
134 *
135 * @return void
136 */
137 public function enqueue_scripts() {
138 wp_enqueue_style(
139 'block-lab',
140 $this->plugin->get_url( 'css/admin.css' ),
141 [],
142 $this->plugin->get_version()
143 );
144 }
145
146 /**
147 * Redirect to the Settings screen if the license is being saved.
148 */
149 public function maybe_settings_redirect() {
150 $page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_STRING );
151
152 if ( 'block-lab-pro' === $page ) {
153 wp_safe_redirect(
154 add_query_arg(
155 [
156 'post_type' => 'block_lab',
157 'page' => 'block-lab-settings',
158 'tab' => 'license',
159 ],
160 admin_url( 'edit.php' )
161 )
162 );
163
164 die();
165 }
166 }
167 }
1 <?php
2 /**
3 * Block Lab Importer.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Admin;
11
12 use Block_Lab\Component_Abstract;
13
14 /**
15 * Class Import
16 */
17 class Import extends Component_Abstract {
18
19 /**
20 * Importer slug.
21 *
22 * @var string
23 */
24 public $slug = 'block-lab';
25
26 /**
27 * Register any hooks that this component needs.
28 */
29 public function register_hooks() {
30 add_action( 'admin_init', [ $this, 'register_importer' ] );
31 }
32
33 /**
34 * Register the importer for the Tools > Import admin screen
35 */
36 public function register_importer() {
37 register_importer(
38 $this->slug,
39 __( 'Block Lab', 'block-lab' ),
40 __( 'Import custom blocks created with Block Lab.', 'block-lab' ),
41 [ $this, 'render_page' ]
42 );
43 }
44
45 /**
46 * Render the import page. Manages the three separate stages of the JSON import process.
47 */
48 public function render_page() {
49 $step = filter_input( INPUT_GET, 'step', FILTER_SANITIZE_NUMBER_INT );
50
51 ob_start();
52
53 $this->render_page_header();
54
55 switch ( $step ) {
56 case 0:
57 default:
58 $this->render_welcome();
59 break;
60 case 1:
61 check_admin_referer( 'import-upload' );
62
63 $upload_dir = wp_get_upload_dir();
64
65 if ( ! isset( $upload_dir['basedir'] ) ) {
66 $this->render_import_error(
67 __( 'Sorry, there was an error uploading the file.', 'block-lab' ),
68 __( 'Upload base directory not set.', 'block-lab' )
69 );
70 }
71
72 $cache_dir = $upload_dir['basedir'] . '/block-lab';
73 $file = wp_import_handle_upload();
74
75 if ( $this->validate_upload( $file ) ) {
76 if ( ! file_exists( $cache_dir ) ) {
77 mkdir( $cache_dir, 0777, true );
78 }
79
80 // This is on the local filesystem, so file_get_contents() is ok to use here.
81 file_put_contents( $cache_dir . '/import.json', file_get_contents( $file['file'] ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions
82
83 $json = file_get_contents( $file['file'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions
84 $blocks = json_decode( $json, true );
85
86 $this->render_choose_blocks( $blocks );
87 }
88 break;
89 case 2:
90 $cache_dir = wp_get_upload_dir()['basedir'] . '/block-lab';
91 $file = [ 'file' => $cache_dir . '/import.json' ];
92
93 if ( $this->validate_upload( $file ) ) {
94 // This is on the local filesystem, so file_get_contents() is ok to use here.
95 $json = file_get_contents( $file['file'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions
96 $blocks = json_decode( $json, true );
97
98 $import_blocks = [];
99 foreach ( $blocks as $block_namespace => $block ) {
100 if ( 'on' === filter_input( INPUT_GET, $block_namespace, FILTER_SANITIZE_STRING ) ) {
101 $import_blocks[ $block_namespace ] = $block;
102 }
103 }
104
105 $this->import_blocks( $import_blocks );
106 }
107
108 break;
109 }
110
111 $html = ob_get_clean();
112 echo '<div class="wrap block-lab-import">' . $html . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
113 }
114
115 /**
116 * Render the Import page header.
117 */
118 public function render_page_header() {
119 ?>
120 <h2><?php esc_html_e( 'Import Block Lab Content Blocks', 'block-lab' ); ?></h2>
121 <?php
122 }
123
124 /**
125 * Render the welcome message.
126 */
127 public function render_welcome() {
128 ?>
129 <p><?php esc_html_e( 'Welcome! This importer processes Block Lab JSON files, adding custom blocks to this site.', 'block-lab' ); ?></p>
130 <p><?php esc_html_e( 'Choose a JSON (.json) file to upload, then click Upload file and import.', 'block-lab' ); ?></p>
131 <p>
132 <?php
133 echo wp_kses(
134 sprintf(
135 /* translators: %1$s: an opening anchor tag, %2$s: a closing anchor tag */
136 __( 'This JSON file should come from the export link or bulk action in the %1$sContent Blocks screen%2$s, not from the main Export tool.', 'block-lab' ),
137 sprintf(
138 '<a href="%1$s">',
139 esc_url(
140 admin_url(
141 add_query_arg(
142 [ 'post_type' => block_lab()->get_post_type_slug() ],
143 'edit.php'
144 )
145 )
146 )
147 ),
148 '</a>'
149 ),
150 [ 'a' => [ 'href' => [] ] ]
151 );
152 ?>
153 </p>
154
155 <?php
156 wp_import_upload_form(
157 add_query_arg(
158 [
159 'import' => $this->slug,
160 'step' => 1,
161 ]
162 )
163 );
164 }
165
166 /**
167 * Render the currently importing block title.
168 *
169 * @param string $title The title of the block.
170 */
171 public function render_import_success( $title ) {
172 echo wp_kses_post(
173 sprintf(
174 '<p>%s</p>',
175 sprintf(
176 // translators: placeholder refers to title of custom block.
177 __( 'Successfully imported %1$s.', 'block-lab' ),
178 '<strong>' . esc_html( $title ) . '</strong>'
179 )
180 )
181 );
182 }
183
184 /**
185 * Render the currently importing block title.
186 *
187 * @param string $title The title of the block.
188 * @param string $error The error being reported.
189 */
190 public function render_import_error( $title, $error ) {
191 echo wp_kses_post(
192 sprintf( '<p><strong>%s</strong></p><p>%s</p>', $title, $error )
193 );
194 }
195
196 /**
197 * Render the successful import message.
198 */
199 public function render_done() {
200 ?>
201 <p><?php esc_html_e( 'All done!', 'block-lab' ); ?></p>
202 <?php
203 }
204
205 /**
206 * Render the interface for choosing blocks to update.
207 *
208 * @param array $blocks An array of block names to choose from.
209 */
210 public function render_choose_blocks( $blocks ) {
211 ?>
212 <p><?php esc_html_e( 'Please select the blocks to import:', 'block-lab' ); ?></p>
213 <form>
214 <?php
215 foreach ( $blocks as $block_namespace => $block ) {
216 $action = __( 'Import', 'block-lab' );
217 if ( $this->block_exists( $block_namespace ) ) {
218 $action = __( 'Replace', 'block-lab' );
219 }
220 ?>
221 <p>
222 <input type="checkbox" name="<?php echo esc_attr( $block_namespace ); ?>" id="<?php echo esc_attr( $block_namespace ); ?>" checked>
223 <label for="<?php echo esc_attr( $block_namespace ); ?>">
224 <?php echo esc_html( $action ); ?> <strong><?php echo esc_attr( $block['title'] ); ?></strong>
225 </label>
226 </p>
227 <?php
228 }
229 wp_nonce_field();
230 ?>
231 <input type="hidden" name="import" value="block-lab">
232 <input type="hidden" name="step" value="2">
233 <p class="submit"><input type="submit" value="<?php esc_attr_e( 'Import Selected', 'block-lab' ); ?>" class="button button-primary"></p>
234 </form>
235 <?php
236 }
237
238 /**
239 * Handles the JSON upload and initial parsing of the file.
240 *
241 * @param array $file The file.
242 * @return bool False if error uploading or invalid file, true otherwise.
243 */
244 public function validate_upload( $file ) {
245 if ( isset( $file['error'] ) ) {
246 $this->render_import_error(
247 __( 'Sorry, there was an error uploading the file.', 'block-lab' ),
248 $file['error']
249 );
250 return false;
251 } elseif ( ! file_exists( $file['file'] ) ) {
252 $this->render_import_error(
253 __( 'Sorry, there was an error uploading the file.', 'block-lab' ),
254 sprintf(
255 // translators: placeholder refers to a file directory.
256 __( 'The export file could not be found at %1$s. It is likely that this was caused by a permissions problem.', 'block-lab' ),
257 '<code>' . esc_html( $file['file'] ) . '</code>'
258 )
259 );
260 return false;
261 }
262
263 // This is on the local filesystem, so file_get_contents() is ok to use here.
264 $json = file_get_contents( $file['file'] ); // @codingStandardsIgnoreLine
265 $data = json_decode( $json, true );
266
267 if ( ! is_array( $data ) ) {
268 $this->render_import_error(
269 __( 'Sorry, there was an error processing the file.', 'block-lab' ),
270 __( 'Invalid JSON.', 'block-lab' )
271 );
272 return false;
273 }
274
275 return true;
276 }
277
278 /**
279 * Import data into new Block Lab posts.
280 *
281 * @param array $blocks An array of Block Lab content blocks.
282 */
283 public function import_blocks( $blocks ) {
284 foreach ( $blocks as $block_namespace => $block ) {
285 if ( ! isset( $block['title'] ) || ! isset( $block['name'] ) ) {
286 continue;
287 }
288
289 $post_id = false;
290
291 if ( $this->block_exists( $block_namespace ) ) {
292 $post = get_page_by_path( $block['name'], OBJECT, block_lab()->get_post_type_slug() );
293 if ( $post ) {
294 $post_id = $post->ID;
295 }
296 }
297
298 $json = wp_json_encode( [ $block_namespace => $block ], JSON_UNESCAPED_UNICODE );
299
300 $post_data = [
301 'post_title' => $block['title'],
302 'post_name' => $block['name'],
303 'post_content' => wp_slash( $json ),
304 'post_status' => 'publish',
305 'post_type' => block_lab()->get_post_type_slug(),
306 ];
307
308 if ( $post_id ) {
309 $post_data['ID'] = $post_id;
310 }
311 $post = wp_insert_post( $post_data );
312
313 if ( is_wp_error( $post ) ) {
314 $this->render_import_error(
315 sprintf(
316 // translators: placeholder refers to title of custom block.
317 __( 'Error importing %s.', 'block-lab' ),
318 $block['title']
319 ),
320 $post->get_error_message()
321 );
322 } else {
323 $this->render_import_success( $block['title'] );
324 }
325 }
326
327 $this->render_done();
328 }
329
330 /**
331 * Check if block already exists.
332 *
333 * @param string $block_namespace The JSON key for the block. e.g. block-lab/foo.
334 *
335 * @return bool
336 */
337 private function block_exists( $block_namespace ) {
338 $registered_blocks = get_dynamic_block_names();
339 if ( in_array( $block_namespace, $registered_blocks, true ) ) {
340 return true;
341 }
342
343 return false;
344 }
345 }
1 <?php
2 /**
3 * Enable and validate Pro version licensing.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Admin;
11
12 use Block_Lab\Component_Abstract;
13
14 /**
15 * Class License
16 */
17 class License extends Component_Abstract {
18
19 /**
20 * Option name of the license key.
21 *
22 * @var string
23 */
24 const LICENSE_KEY_OPTION_NAME = 'block_lab_license_key';
25
26 /**
27 * URL of the Block Lab store.
28 *
29 * @var string
30 */
31 public $store_url;
32
33 /**
34 * Product slug of the Pro version on the Block Lab store.
35 *
36 * @var string
37 */
38 public $product_slug;
39
40 /**
41 * The name of the license key transient.
42 *
43 * @var string
44 */
45 const TRANSIENT_NAME = 'block_lab_license';
46
47 /**
48 * The transient 'license' value for when the request to validate the Pro license failed.
49 *
50 * This is for when the actual POST request fails,
51 * not for when it returns that the license is invalid.
52 *
53 * @var string
54 */
55 const REQUEST_FAILED = 'request_failed';
56
57 /**
58 * Initialise the Pro component.
59 */
60 public function init() {
61 $this->store_url = 'https://getblocklab.com';
62 $this->product_slug = 'block-lab-pro';
63 }
64
65 /**
66 * Register any hooks that this component needs.
67 */
68 public function register_hooks() {
69 add_filter( 'pre_update_option_block_lab_license_key', [ $this, 'save_license_key' ] );
70 }
71
72 /**
73 * Check that the license key is valid before saving.
74 *
75 * @param string $key The license key that was submitted.
76 *
77 * @return string
78 */
79 public function save_license_key( $key ) {
80 $this->activate_license( $key );
81 $license = get_transient( self::TRANSIENT_NAME );
82
83 if ( ! $this->is_valid() ) {
84 $key = '';
85 if ( isset( $license['license'] ) && self::REQUEST_FAILED === $license['license'] ) {
86 block_lab()->admin->settings->prepare_notice( $this->license_request_failed_message() );
87 } else {
88 block_lab()->admin->settings->prepare_notice( $this->license_invalid_message() );
89 }
90 } else {
91 block_lab()->admin->settings->prepare_notice( $this->license_success_message() );
92 }
93
94 return $key;
95 }
96
97 /**
98 * Check if the license if valid.
99 *
100 * @return bool
101 */
102 public function is_valid() {
103 $license = $this->get_license();
104
105 if ( isset( $license['license'] ) && 'valid' === $license['license'] ) {
106 if ( isset( $license['expires'] ) && time() < strtotime( $license['expires'] ) ) {
107 return true;
108 }
109 }
110
111 return false;
112 }
113
114 /**
115 * Retrieve the license data.
116 *
117 * @return mixed
118 */
119 public function get_license() {
120 $license = get_transient( self::TRANSIENT_NAME );
121
122 if ( ! $license ) {
123 $key = get_option( self::LICENSE_KEY_OPTION_NAME );
124 if ( ! empty( $key ) ) {
125 $this->activate_license( $key );
126 $license = get_transient( self::TRANSIENT_NAME );
127 }
128 }
129
130 return $license;
131 }
132
133 /**
134 * Try to activate the license.
135 *
136 * @param string $key The license key to activate.
137 */
138 public function activate_license( $key ) {
139 // Data to send in our API request.
140 $api_params = [
141 'edd_action' => 'activate_license',
142 'license' => $key,
143 'item_name' => rawurlencode( $this->product_slug ),
144 'url' => home_url(),
145 ];
146
147 // Call the Block Lab store's API.
148 $response = wp_remote_post(
149 $this->store_url,
150 [
151 'timeout' => 10,
152 'sslverify' => true,
153 'body' => $api_params,
154 ]
155 );
156
157 if ( is_wp_error( $response ) ) {
158 $license = [ 'license' => self::REQUEST_FAILED ];
159 } else {
160 $license = json_decode( wp_remote_retrieve_body( $response ), true );
161 }
162
163 $expiration = DAY_IN_SECONDS;
164
165 set_transient( self::TRANSIENT_NAME, $license, $expiration );
166 }
167
168 /**
169 * Admin notice for correct license details.
170 *
171 * @return string
172 */
173 public function license_success_message() {
174 $message = __( 'Your Block Lab license was successfully activated!', 'block-lab' );
175 return sprintf( '<div class="notice notice-success"><p>%s</p></div>', esc_html( $message ) );
176 }
177
178 /**
179 * Admin notice for the license request failing.
180 *
181 * This is for when the validation request fails entirely, like with a 404.
182 * Not for when it returns that the license is invalid.
183 *
184 * @return string
185 */
186 public function license_request_failed_message() {
187 $message = sprintf(
188 /* translators: %s is an HTML link to contact support */
189 __( 'There was a problem activating the license, but it may not be invalid. If the problem persists, please %s.', 'block-lab' ),
190 sprintf(
191 '<a href="%1$s">%2$s</a>',
192 'mailto:hi@getblocklab.com?subject=There was a problem activating my Block Lab Pro license',
193 esc_html__( 'contact support', 'block-lab' )
194 )
195 );
196
197 return sprintf( '<div class="notice notice-error"><p>%s</p></div>', wp_kses_post( $message ) );
198 }
199
200 /**
201 * Admin notice for incorrect license details.
202 *
203 * @return string
204 */
205 public function license_invalid_message() {
206 $message = __( 'There was a problem activating your Block Lab license.', 'block-lab' );
207 return sprintf( '<div class="notice notice-error"><p>%s</p></div>', esc_html( $message ) );
208 }
209 }
1 <?php
2 /**
3 * Block Lab Settings.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Admin;
11
12 use Block_Lab\Component_Abstract;
13
14 /**
15 * Class Settings
16 */
17 class Settings extends Component_Abstract {
18
19 /**
20 * Page slug.
21 *
22 * @var string
23 */
24 public $slug = 'block-lab-settings';
25
26 /**
27 * Register any hooks that this component needs.
28 */
29 public function register_hooks() {
30 add_action( 'admin_menu', [ $this, 'add_submenu_pages' ] );
31 add_action( 'admin_init', [ $this, 'register_settings' ] );
32 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
33 add_action( 'admin_notices', [ $this, 'show_notices' ] );
34 }
35
36 /**
37 * Enqueue scripts and styles used by the Settings screen.
38 *
39 * @return void
40 */
41 public function enqueue_scripts() {
42 $page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_STRING );
43
44 // Enqueue scripts and styles on the edit screen of the Block post type.
45 if ( $this->slug === $page ) {
46 wp_enqueue_style(
47 $this->slug,
48 $this->plugin->get_url( 'css/admin.settings.css' ),
49 [],
50 $this->plugin->get_version()
51 );
52 }
53 }
54
55 /**
56 * Add submenu pages to the Block Lab menu.
57 */
58 public function add_submenu_pages() {
59 add_submenu_page(
60 'edit.php?post_type=' . block_lab()->get_post_type_slug(),
61 __( 'Block Lab Settings', 'block-lab' ),
62 __( 'Settings', 'block-lab' ),
63 'manage_options',
64 $this->slug,
65 [ $this, 'render_page' ]
66 );
67 }
68
69 /**
70 * Register Block Lab settings.
71 */
72 public function register_settings() {
73 register_setting( 'block-lab-license-key', 'block_lab_license_key' );
74 }
75
76 /**
77 * Render the Settings page.
78 */
79 public function render_page() {
80 ?>
81 <div class="wrap block-lab-settings">
82 <?php
83 $this->render_page_header();
84 include block_lab()->get_path() . 'php/views/license.php';
85 ?>
86 </div>
87 <?php
88 }
89
90 /**
91 * Render the Settings page header.
92 */
93 public function render_page_header() {
94 ?>
95 <h2><?php echo esc_html( get_admin_page_title() ); ?></h2>
96 <h2 class="nav-tab-wrapper">
97 <a href="<?php echo esc_url( add_query_arg( 'tab', 'license' ) ); ?>" title="<?php esc_attr_e( 'License', 'block-lab' ); ?>" class="nav-tab nav-tab-active dashicons-before dashicons-nametag">
98 <?php esc_html_e( 'License', 'block-lab' ); ?>
99 </a>
100 <a href="https://getblocklab.com/docs/" target="_blank" class="nav-tab dashicons-before dashicons-info">
101 <?php esc_html_e( 'Documentation', 'block-lab' ); ?>
102 </a>
103 <a href="https://wordpress.org/support/plugin/block-lab/" target="_blank" class="nav-tab dashicons-before dashicons-sos">
104 <?php esc_html_e( 'Help', 'block-lab' ); ?>
105 </a>
106 </h2>
107 <?php
108 }
109
110 /**
111 * Prepare notices to be displayed after saving the settings.
112 *
113 * @param string $notice The notice text to display.
114 */
115 public function prepare_notice( $notice ) {
116 $notices = get_option( 'block_lab_notices', [] );
117 $notices[] = $notice;
118 update_option( 'block_lab_notices', $notices );
119 }
120
121 /**
122 * Show any admin notices after saving the settings.
123 */
124 public function show_notices() {
125 $notices = get_option( 'block_lab_notices', [] );
126
127 if ( empty( $notices ) || ! is_array( $notices ) ) {
128 return;
129 }
130
131 foreach ( $notices as $notice ) {
132 echo wp_kses_post( $notice );
133 }
134
135 delete_option( 'block_lab_notices' );
136 }
137 }
1 <?php
2 /**
3 * Block Lab Upgrade Page.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Admin;
11
12 use Block_Lab\Component_Abstract;
13
14 /**
15 * Class Upgrade
16 */
17 class Upgrade extends Component_Abstract {
18
19 /**
20 * Page slug.
21 *
22 * @var string
23 */
24 public $slug = 'block-lab-pro';
25
26 /**
27 * Register any hooks that this component needs.
28 */
29 public function register_hooks() {
30 add_action( 'admin_menu', [ $this, 'add_submenu_pages' ] );
31 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
32 }
33
34 /**
35 * Enqueue scripts and styles used by the Upgrade screen.
36 *
37 * @return void
38 */
39 public function enqueue_scripts() {
40 $page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_STRING );
41
42 // Enqueue scripts and styles on the edit screen of the Block post type.
43 if ( $this->slug === $page ) {
44 wp_enqueue_style(
45 $this->slug,
46 $this->plugin->get_url( 'css/admin.upgrade.css' ),
47 [],
48 $this->plugin->get_version()
49 );
50 }
51 }
52
53 /**
54 * Add submenu pages to the Block Lab menu.
55 */
56 public function add_submenu_pages() {
57 add_submenu_page(
58 'edit.php?post_type=block_lab',
59 __( 'Block Lab Pro', 'block-lab' ),
60 __( 'Go Pro', 'block-lab' ),
61 'manage_options',
62 $this->slug,
63 [ $this, 'render_page' ]
64 );
65 }
66
67 /**
68 * Render the Upgrade page.
69 */
70 public function render_page() {
71 ?>
72 <div class="wrap block-lab-pro">
73 <h2 class="screen-reader-text"><?php echo esc_html( get_admin_page_title() ); ?></h2>
74 <?php include block_lab()->get_path() . 'php/views/upgrade.php'; ?>
75 </div>
76 <?php
77 }
78 }
1 <?php
2 /**
3 * Migration REST API endpoints.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Admin\Migration;
11
12 use Plugin_Upgrader;
13 use WP_Ajax_Upgrader_Skin;
14 use WP_Error;
15 use WP_REST_Response;
16 use Block_Lab\Component_Abstract;
17
18 /**
19 * Class Post_Type
20 */
21 class Api extends Component_Abstract {
22
23 /**
24 * Adds the actions.
25 */
26 public function register_hooks() {
27 add_action( 'rest_api_init', [ $this, 'register_route_install_gcb' ] );
28 add_action( 'rest_api_init', [ $this, 'register_route_migrate_post_content' ] );
29 add_action( 'rest_api_init', [ $this, 'register_route_migrate_post_type' ] );
30 }
31
32 /**
33 * Registers a route to install and activate the plugin Genesis Custom Blocks.
34 */
35 public function register_route_install_gcb() {
36 register_rest_route(
37 block_lab()->get_slug(),
38 'install-activate-gcb',
39 [
40 'methods' => 'POST',
41 'callback' => [ $this, 'get_install_gcb_response' ],
42 'permission_callback' => function() {
43 return current_user_can( 'install_plugins' ) && current_user_can( 'activate_plugins' );
44 },
45 ]
46 );
47 }
48
49 /**
50 * Installs and activates Genesis Custom Blocks, and returns the result.
51 *
52 * @param array $data Data sent in the POST request.
53 * @return WP_REST_Response|WP_Error Response to the request.
54 */
55 public function get_install_gcb_response( $data ) {
56 unset( $data );
57
58 $installation_result = $this->install_plugin();
59 if ( is_wp_error( $installation_result ) ) {
60 return $installation_result;
61 }
62
63 $activation_result = $this->activate_plugin();
64 if ( is_wp_error( $activation_result ) ) {
65 return $activation_result;
66 }
67
68 return rest_ensure_response( [ 'message' => __( 'Plugin installed and activated', 'block-lab' ) ] );
69 }
70
71 /**
72 * Installs the new plugin.
73 *
74 * Mainly copied from Gutenberg, with slight changes.
75 * The main change being that it returns true
76 * if the plugin is already downloaded, not a WP_Error.
77 *
78 * @see https://github.com/WordPress/gutenberg/blob/fef0445bf47adc6c8d8b69e19616feb8b6de8c2e/lib/class-wp-rest-plugins-controller.php#L271-L369
79 * @return true|WP_Error True on success, WP_Error on failure.
80 */
81 private function install_plugin() {
82 global $wp_filesystem;
83
84 require_once ABSPATH . 'wp-admin/includes/file.php';
85 require_once ABSPATH . 'wp-admin/includes/plugin.php';
86 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
87 require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
88 require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
89
90 // Check if the plugin is already installed.
91 if ( array_key_exists( $this->get_new_plugin_file(), get_plugins() ) ) {
92 return true;
93 }
94
95 // Verify filesystem is accessible first.
96 $filesystem_available = $this->is_filesystem_available();
97 if ( is_wp_error( $filesystem_available ) ) {
98 return $filesystem_available;
99 }
100
101 $download_link = $this->get_download_link();
102 if ( is_wp_error( $download_link ) ) {
103 return $download_link;
104 }
105
106 $skin = new WP_Ajax_Upgrader_Skin();
107 $upgrader = new Plugin_Upgrader( $skin );
108
109 $result = $upgrader->install( $download_link );
110
111 if ( is_wp_error( $result ) ) {
112 $result->add_data( [ 'status' => 500 ] );
113
114 return $result;
115 }
116
117 // This should be the same as $result above.
118 if ( is_wp_error( $skin->result ) ) {
119 $skin->result->add_data( [ 'status' => 500 ] );
120
121 return $skin->result;
122 }
123
124 if ( $skin->get_errors()->has_errors() ) {
125 $error = $skin->get_errors();
126 $error->add_data( [ 'status' => 500 ] );
127
128 return $error;
129 }
130
131 if ( is_null( $result ) ) {
132 // Pass through the error from WP_Filesystem if one was raised.
133 if ( $wp_filesystem instanceof WP_Filesystem_Base && isset( $wp_filesystem->errors ) && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
134 return new WP_Error( 'unable_to_connect_to_filesystem', $wp_filesystem->errors->get_error_message(), [ 'status' => 500 ] );
135 }
136
137 return new WP_Error( 'unable_to_connect_to_filesystem', __( 'Unable to connect to the filesystem. Please confirm your credentials.', 'block-lab' ), [ 'status' => 500 ] );
138 }
139
140 $file = $upgrader->plugin_info();
141 if ( ! $file ) {
142 return new WP_Error( 'unable_to_determine_installed_plugin', __( 'Unable to determine what plugin was installed.', 'block-lab' ), [ 'status' => 500 ] );
143 }
144
145 return true;
146 }
147
148 /**
149 * Determines if the filesystem is available.
150 *
151 * Only the 'Direct' filesystem transport, and SSH/FTP when credentials are stored are supported at present.
152 * Copied from Gutenberg.
153 *
154 * @see https://github.com/WordPress/gutenberg/blob/8d64aa3092d5d9e841895bf2d495565c9a770238/lib/class-wp-rest-plugins-controller.php#L799-L815
155 *
156 * @return true|WP_Error True if filesystem is available, WP_Error otherwise.
157 */
158 private function is_filesystem_available() {
159 if ( 'direct' === get_filesystem_method() ) {
160 return true;
161 }
162
163 ob_start();
164 $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
165 ob_end_clean();
166
167 if ( $filesystem_credentials_are_stored ) {
168 return true;
169 }
170
171 return new WP_Error( 'fs_unavailable', __( 'The filesystem is currently unavailable for managing plugins.', 'block-lab' ), [ 'status' => 500 ] );
172 }
173
174 /**
175 * Gets the GCB Pro download link.
176 *
177 * @return string|WP_Error The download link, or a WP_Error.
178 */
179 public function get_download_link() {
180 if ( ! empty( get_transient( Subscription_Api::TRANSIENT_NAME_GCB_PRO_DOWNLOAD_LINK ) ) ) {
181 return get_transient( Subscription_Api::TRANSIENT_NAME_GCB_PRO_DOWNLOAD_LINK );
182 } else {
183 $api = plugins_api(
184 'plugin_information',
185 [
186 'slug' => 'genesis-custom-blocks',
187 'fields' => [
188 'sections' => false,
189 ],
190 ]
191 );
192
193 if ( is_wp_error( $api ) ) {
194 if ( false !== strpos( $api->get_error_message(), 'Plugin not found.' ) ) {
195 $api->add_data( [ 'status' => 404 ] );
196 } else {
197 $api->add_data( [ 'status' => 500 ] );
198 }
199
200 return $api;
201 }
202
203 if ( empty( $api->download_link ) ) {
204 return new WP_Error(
205 'no_download_link',
206 __( 'There was no download_link in the API', 'block-lab' )
207 );
208 }
209
210 return $api->download_link;
211 }
212 }
213
214 /**
215 * Activates the new plugin.
216 *
217 * Mainly copied from Gutenberg's WP_REST_Plugins_Controller::handle_plugin_status().
218 *
219 * @see https://github.com/WordPress/gutenberg/blob/fef0445bf47adc6c8d8b69e19616feb8b6de8c2e/lib/class-wp-rest-plugins-controller.php#L679-L709
220 *
221 * @return true|WP_Error True on success, WP_Error on failure.
222 */
223 private function activate_plugin() {
224 $activation_result = activate_plugin( $this->get_new_plugin_file(), '', false, true );
225 if ( is_wp_error( $activation_result ) ) {
226 $activation_result->add_data( [ 'status' => 500 ] );
227 return $activation_result;
228 }
229
230 return true;
231 }
232
233 /**
234 * Registers a route to migrate the post content to the new namespace.
235 */
236 public function register_route_migrate_post_content() {
237 register_rest_route(
238 block_lab()->get_slug(),
239 'migrate-post-content',
240 [
241 'methods' => 'POST',
242 'callback' => [ $this, 'get_migrate_post_content_response' ],
243 'permission_callback' => function() {
244 return current_user_can( Submenu::MIGRATION_CAPABILITY );
245 },
246 ]
247 );
248 }
249
250 /**
251 * Gets the REST API response for the post content migration.
252 *
253 * @return WP_REST_Response The response to the request.
254 */
255 public function get_migrate_post_content_response() {
256 return rest_ensure_response( ( new Post_Content( 'block-lab', 'genesis-custom-blocks' ) )->migrate_all() );
257 }
258
259 /**
260 * Registers a route to migrate the post type.
261 */
262 public function register_route_migrate_post_type() {
263 register_rest_route(
264 block_lab()->get_slug(),
265 'migrate-post-type',
266 [
267 'methods' => 'POST',
268 'callback' => [ $this, 'get_migrate_post_type_response' ],
269 'permission_callback' => function() {
270 return current_user_can( Submenu::MIGRATION_CAPABILITY );
271 },
272 ]
273 );
274 }
275
276 /**
277 * Gets the REST API response for the post type migration.
278 *
279 * @return WP_REST_Response The response to the request.
280 */
281 public function get_migrate_post_type_response() {
282 return rest_ensure_response( ( new Post_Type( 'block_lab', 'block-lab', 'block_lab', 'genesis_custom_block', 'genesis-custom-blocks', 'genesis_custom_blocks' ) )->migrate_all() );
283 }
284
285 /**
286 * Gets the directory and file of the new plugin to install.
287 *
288 * @return string The plugin file.
289 */
290 public function get_new_plugin_file() {
291 if ( ! empty( get_transient( Subscription_Api::TRANSIENT_NAME_GCB_PRO_DOWNLOAD_LINK ) ) ) {
292 return 'genesis-custom-blocks-pro/genesis-custom-blocks-pro.php';
293 }
294
295 return 'genesis-custom-blocks/genesis-custom-blocks.php';
296 }
297 }
1 <?php
2 /**
3 * Displays a migration notice.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Admin\Migration;
11
12 use Block_Lab\Component_Abstract;
13
14 /**
15 * Class Notice
16 */
17 class Notice extends Component_Abstract {
18
19 /**
20 * The AJAX action to dismiss the migration notice.
21 *
22 * @var string
23 */
24 const NOTICE_AJAX_ACTION = 'bl_dismiss_migration_notice';
25
26 /**
27 * The action of the migration notice nonce.
28 *
29 * @var string
30 */
31 const NOTICE_NONCE_ACTION = 'bl-migration-nonce';
32
33 /**
34 * The name of the migration notice nonce.
35 *
36 * @var string
37 */
38 const NOTICE_NONCE_NAME = 'bl-migration-nonce-name';
39
40 /**
41 * The slug of the stylesheet for the migration notice.
42 *
43 * @var string
44 */
45 const NOTICE_STYLE_SLUG = 'block-lab-migration-notice-style';
46
47 /**
48 * The slug of the script for the migration notice.
49 *
50 * @var string
51 */
52 const NOTICE_SCRIPT_SLUG = 'block-lab-migration-notice-script';
53
54 /**
55 * The user meta key to store whether a user has dismissed the migration notice.
56 *
57 * @var string
58 */
59 const NOTICE_USER_META_KEY = 'block_lab_show_migration_notice';
60
61 /**
62 * The user meta value stored if a user has dismissed the migration notice.
63 *
64 * @var string
65 */
66 const NOTICE_DISMISSED_META_VALUE = 'dismissed';
67
68 /**
69 * The capability required to see the notice.
70 *
71 * @var string
72 */
73 const NOTICE_CAPABILITY = 'install_plugins';
74
75 /**
76 * Adds an action for the notice.
77 */
78 public function register_hooks() {
79 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
80 add_action( 'wp_ajax_' . self::NOTICE_AJAX_ACTION, [ $this, 'ajax_handler_migration_notice' ] );
81 }
82
83 /**
84 * Outputs the migration notice if this is on the right page and the user has the right permission.
85 */
86 public function render_migration_notice() {
87 if ( ! $this->should_display_migration_notice() ) {
88 return;
89 }
90
91 // @todo: verify that this doc page exists, or change it to one that does exist.
92 $learn_more_link = 'https://getblocklab.com/docs/genesis-custom-blocks';
93 $migration_url = add_query_arg(
94 [
95 'post_type' => block_lab()->get_post_type_slug(),
96 'page' => 'block-lab-migration',
97 ],
98 admin_url( 'edit.php' )
99 );
100
101 ?>
102 <div id="bl-migration-notice" class="notice notice-info bl-notice-migration">
103 <?php wp_nonce_field( self::NOTICE_NONCE_ACTION, self::NOTICE_NONCE_NAME, false ); ?>
104 <div class="bl-migration-copy">
105 <p>
106 <?php
107 printf(
108 /* translators: %1$s: the plugin name */
109 esc_html__( 'The Block Lab team have moved. For future updates and improvements, migrate now to the new home of custom blocks: %1$s.', 'block-lab' ),
110 sprintf(
111 '<strong>%1$s</strong>',
112 esc_html__( 'Genesis Custom Blocks', 'block-lab' )
113 )
114 );
115 ?>
116 <a target="_blank" rel="noopener noreferrer" class="bl-notice-migration__learn-more" href="<?php echo esc_url( $learn_more_link ); ?>">
117 <?php esc_html_e( 'Learn more', 'block-lab' ); ?>
118 </a>
119 </p>
120 </div>
121 <button id="bl-notice-not-now" href="#" class="bl-notice-option button button-secondary">
122 <?php esc_html_e( 'Not Now', 'block-lab' ); ?>
123 </button>
124 <a href="<?php echo esc_url( $migration_url ); ?>" class="bl-notice-option button button-primary">
125 <?php esc_html_e( 'Migrate', 'block-lab' ); ?>
126 </a>
127 </div>
128 <div id="bl-not-now-notice" class="notice notice-info bl-notice-migration bl-hidden">
129 <div class="bl-migration-copy">
130 <p><?php esc_html_e( "When you're ready, our migration tool is available in the main menu, under Block Lab > Migrate.", 'block-lab' ); ?></p>
131 </div>
132 <a href="#" id="bl-notice-ok" class="bl-notice-option">
133 <?php esc_html_e( 'Okay', 'block-lab' ); ?>
134 </a>
135 </div>
136 <?php
137 }
138
139 /**
140 * Enqueues the migration notice assets.
141 */
142 public function enqueue_assets() {
143 if ( ! $this->should_display_migration_notice() ) {
144 return;
145 }
146
147 wp_enqueue_style(
148 self::NOTICE_STYLE_SLUG,
149 $this->plugin->get_url( 'css/admin.migration-notice.css' ),
150 [],
151 $this->plugin->get_version()
152 );
153
154 wp_enqueue_script(
155 self::NOTICE_SCRIPT_SLUG,
156 $this->plugin->get_url( 'js/admin.migration-notice.js' ),
157 [],
158 $this->plugin->get_version(),
159 true
160 );
161 }
162
163 /**
164 * Gets whether the migration notice should display.
165 *
166 * This should display on Block Lab > Content Blocks,
167 * /wp-admin/plugins.php, the Dashboard, and Block Lab > Settings.
168 *
169 * @return bool Whether the migration notice should display.
170 */
171 public function should_display_migration_notice() {
172 if ( ! current_user_can( self::NOTICE_CAPABILITY ) ) {
173 return false;
174 }
175
176 // If the user has dismissed the notice, it shouldn't appear again.
177 if ( self::NOTICE_DISMISSED_META_VALUE === get_user_meta( get_current_user_id(), self::NOTICE_USER_META_KEY, true ) ) {
178 return false;
179 }
180
181 $screen = get_current_screen();
182 return (
183 ( isset( $screen->base, $screen->post_type ) && 'edit' === $screen->base && 'block_lab' === $screen->post_type )
184 ||
185 ( isset( $screen->base ) && in_array( $screen->base, [ 'plugins', 'dashboard', 'block_lab_page_block-lab-settings' ], true ) )
186 );
187 }
188
189 /**
190 * Handles an AJAX request to not display the notice.
191 *
192 * This stores in the user meta the fact that the notice was dismissed,
193 * so it's not displayed again.
194 */
195 public function ajax_handler_migration_notice() {
196 check_ajax_referer( self::NOTICE_NONCE_ACTION, self::NOTICE_NONCE_NAME );
197
198 if ( ! current_user_can( self::NOTICE_CAPABILITY ) ) {
199 wp_send_json_error();
200 }
201
202 update_user_meta( get_current_user_id(), self::NOTICE_USER_META_KEY, self::NOTICE_DISMISSED_META_VALUE );
203
204 wp_send_json_success();
205 }
206 }
1 <?php
2 /**
3 * Post_Content.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Admin\Migration;
11
12 use WP_Error;
13
14 /**
15 * Class Post_Content
16 */
17 class Post_Content {
18
19 /**
20 * The previous namespace of the block.
21 *
22 * @var string
23 */
24 private $previous_block_namespace;
25
26 /**
27 * The new namespace of the block.
28 *
29 * @var string
30 */
31 private $new_block_namespace;
32
33 /**
34 * Post_Content constructor.
35 *
36 * @param string $previous_block_namespace Previous namespace of the blocks.
37 * @param string $new_block_namespace New namespace of the blocks.
38 */
39 public function __construct( $previous_block_namespace, $new_block_namespace ) {
40 $this->previous_block_namespace = $previous_block_namespace;
41 $this->new_block_namespace = $new_block_namespace;
42 }
43
44 /**
45 * Migrates all of the block namespaces in all of the posts that have Block Lab blocks.
46 *
47 * @return array|WP_Error The result of the migration, or a WP_Error if it failed.
48 */
49 public function migrate_all() {
50 $success_count = 0;
51 $error_count = 0;
52 $errors = new WP_Error();
53 $max_allowed_errors = 20;
54 $posts = $this->query_for_posts();
55
56 while ( ! empty( $posts ) && $error_count < $max_allowed_errors ) {
57 foreach ( $posts as $post ) {
58 if ( isset( $post->ID ) ) {
59 $migrated_post = $this->migrate_single( $post->ID );
60 if ( is_wp_error( $migrated_post ) ) {
61 $error_count++;
62 $errors->add( $migrated_post->get_error_code(), $migrated_post->get_error_message() );
63 } else {
64 $success_count++;
65 }
66 }
67 }
68
69 $posts = $this->query_for_posts();
70 }
71
72 $is_success = $error_count < $max_allowed_errors;
73 if ( ! $is_success ) {
74 return $errors;
75 }
76
77 $results = [
78 'successCount' => $success_count,
79 'errorCount' => $error_count,
80 ];
81
82 if ( $errors->has_errors() ) {
83 $results['errorMessage'] = $errors->get_error_message();
84 }
85
86 return $results;
87 }
88
89 /**
90 * Migrates the block namespaces in post_content.
91 *
92 * Blocks are stored in the post_content of a post with a namespace,
93 * like '<!-- wp:block-lab/test-image {"example-image":8} /-->'.
94 * In that case, 'block-lab' needs to be changed to the new namespace.
95 * But nothing else in the block should be changed.
96 * The block pattern is mainly taken from Core.
97 *
98 * @see https://github.com/WordPress/wordpress-develop/blob/78d1ab2ed40093a5bd2a75b01ceea37811739f55/src/wp-includes/class-wp-block-parser.php#L413
99 *
100 * @param int $post_id The ID of the post to convert.
101 * @return int|WP_Error The post ID that was changed, or a WP_Error on failure.
102 */
103 public function migrate_single( $post_id ) {
104 $post = get_post( $post_id );
105 if ( ! isset( $post->ID ) ) {
106 return new WP_Error(
107 'invalid_post_id',
108 __( 'Invalid post ID', 'block-lab' )
109 );
110 }
111
112 $new_post_content = preg_replace(
113 '#(<!--\s+wp:)(' . sanitize_key( $this->previous_block_namespace ) . ')(/[a-z][a-z0-9_-]*)#s',
114 '$1' . sanitize_key( $this->new_block_namespace ) . '$3',
115 $post->post_content
116 );
117
118 return wp_update_post(
119 [
120 'ID' => $post->ID,
121 'post_content' => wp_slash( $new_post_content ),
122 ],
123 true
124 );
125 }
126
127 /**
128 * Gets posts that have Block Lab blocks in their post_content.
129 *
130 * Queries for posts that have wp:block-lab/ in the post content,
131 * meaning they probably have a Block Lab block.
132 * Excludes revision posts, as this could overwrite the entire history.
133 * This will allow users to go back to the content before it was migrated.
134 *
135 * @return array The posts that were found.
136 */
137 private function query_for_posts() {
138 global $wpdb;
139
140 $query_limit = 10;
141 return $wpdb->get_results(
142 $wpdb->prepare(
143 "SELECT * FROM {$wpdb->posts} WHERE post_type != %s AND post_content LIKE %s LIMIT %d",
144 'revision',
145 '%' . $wpdb->esc_like( 'wp:' . $this->previous_block_namespace . '/' ) . '%',
146 absint( $query_limit )
147 )
148 );
149 }
150 }
1 <?php
2 /**
3 * Post_Type.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Admin\Migration;
11
12 use WP_Error;
13 use WP_Post;
14 use WP_Query;
15
16 /**
17 * Class Post_Type
18 */
19 class Post_Type {
20
21 /**
22 * The previous slug of the custom post type (in Block Lab).
23 *
24 * @var string
25 */
26 private $previous_post_type_slug;
27
28 /**
29 * The previous namespace of the block.
30 *
31 * @var string
32 */
33 private $previous_block_namespace;
34
35 /**
36 * The previous default block icon.
37 *
38 * @var string
39 */
40 private $previous_default_icon;
41
42 /**
43 * The new namespace of the block.
44 *
45 * @var string
46 */
47 private $new_block_namespace;
48
49 /**
50 * The new slug of the custom post type (not in Block Lab).
51 *
52 * @var string
53 */
54 private $new_post_type_slug;
55
56 /**
57 * The new default block icon.
58 *
59 * @var string
60 */
61 private $new_default_icon;
62
63 /**
64 * Post_Type constructor.
65 *
66 * @param string $previous_post_type_slug Previous slug of the custom post type.
67 * @param string $previous_block_namespace Previous block namespace.
68 * @param string $previous_default_icon Previous default block icon.
69 * @param string $new_post_type_slug New slug of the custom post type.
70 * @param string $new_block_namespace New namespace of the block.
71 * @param string $new_default_icon New default block icon.
72 */
73 public function __construct( $previous_post_type_slug, $previous_block_namespace, $previous_default_icon, $new_post_type_slug, $new_block_namespace, $new_default_icon ) {
74 $this->previous_post_type_slug = $previous_post_type_slug;
75 $this->previous_block_namespace = $previous_block_namespace;
76 $this->previous_default_icon = $previous_default_icon;
77 $this->new_post_type_slug = $new_post_type_slug;
78 $this->new_block_namespace = $new_block_namespace;
79 $this->new_default_icon = $new_default_icon;
80 }
81
82 /**
83 * Migrates all of the custom post type posts to the new post_type slug and block namespace.
84 *
85 * These each store a config for a custom block,
86 * they aren't blocks that users entered into the block editor.
87 *
88 * @return array|WP_Error An array on success, a WP_Error on failure.
89 */
90 public function migrate_all() {
91 $posts = $this->query_for_posts();
92 $success_count = 0;
93 $error_count = 0;
94 $max_allowed_errors = 20;
95 $errors = new WP_Error();
96
97 while ( ! empty( $posts ) && $error_count < $max_allowed_errors ) {
98 foreach ( $posts as $post ) {
99 $migration_result = $this->migrate_single( $post );
100 if ( is_wp_error( $migration_result ) ) {
101 $error_count++;
102 $errors->add( $migration_result->get_error_code(), $migration_result->get_error_message() );
103 } else {
104 $success_count++;
105 }
106 }
107
108 $posts = $this->query_for_posts();
109 }
110
111 $is_success = ! empty( $success_count ) || ( empty( $success_count ) && empty( $error_count ) );
112
113 if ( ! $is_success ) {
114 return $errors;
115 }
116
117 return [
118 'successCount' => $success_count,
119 'errorCount' => $error_count,
120 ];
121 }
122
123 /**
124 * Migrates the custom post type post to the new post_type slug and block namespace.
125 *
126 * Inspired by the work of Weston Ruter: https://github.com/ampproject/amp-wp/blob/4880f0f58daaf07685854be8574ff25d76ff583e/includes/validation/class-amp-validated-url-post-type.php#L165-L170
127 * The post_content of the CPT has a configuration for a block like:
128 * '{"block-lab\/test-image":{"name":"test-image","title":"Test Image","excluded":[],"icon":"block_lab","category":{"slug":"common","title":"Common Blocks","icon":null},"keywords":[""],"fields":{"image":{"name":"image","label":"Image","control":"image","type":"integer","order":0,"location":"editor","width":"50","help":"Here is some help text"}}}}'
129 * The beginning of this has the 'block-lab' namespace, which this changes to the new namespace.
130 *
131 * @param WP_Post $post The post to convert.
132 * @return true|WP_Error True on success, WP_Error on failure.
133 */
134 public function migrate_single( WP_Post $post ) {
135 global $wpdb;
136
137 $block = json_decode( $post->post_content, true );
138 if ( JSON_ERROR_NONE !== json_last_error() || empty( $block ) ) {
139 return new WP_Error(
140 'block_invalid_json',
141 __( 'The block looks to be invalid JSON', 'block-lab' )
142 );
143 }
144
145 $block_keys = array_keys( $block );
146 $old_block_name = reset( $block_keys );
147 if ( empty( $block[ $old_block_name ] ) ) {
148 return new WP_Error(
149 'invalid_block_name',
150 __( 'The block name looks to be invalid', 'block-lab' )
151 );
152 }
153
154 $block_contents = $block[ $old_block_name ];
155 if ( isset( $block_contents['icon'] ) && $this->previous_default_icon === $block_contents['icon'] ) {
156 $block_contents['icon'] = $this->new_default_icon;
157 }
158
159 if ( empty( $block_contents['icon'] ) ) {
160 $block_contents['icon'] = $this->new_default_icon;
161 }
162
163 $new_block_name = preg_replace( '#^' . $this->previous_block_namespace . '(?=/)#', $this->new_block_namespace, $old_block_name );
164 $new_block = [ $new_block_name => $block_contents ];
165
166 $rows_updated = $wpdb->update(
167 $wpdb->posts,
168 [
169 'post_type' => sanitize_key( $this->new_post_type_slug ),
170 'post_content' => wp_json_encode( $new_block ),
171 ],
172 [
173 'ID' => $post->ID,
174 ]
175 );
176 clean_post_cache( $post->ID );
177
178 if ( empty( $rows_updated ) ) {
179 return new WP_Error(
180 'post_not_updated',
181 __( 'The post was not updated', 'block-lab' )
182 );
183 }
184
185 return true;
186 }
187
188 /**
189 * Gets the posts of the previous post_type.
190 *
191 * This query won't find posts that were already migrated, as the migration changes the 'post_type'.
192 * So this doesn't need an 'offset' parameter.
193 *
194 * @return WP_Post[] The posts that were found.
195 */
196 private function query_for_posts() {
197 $query = new WP_Query(
198 [
199 'post_type' => $this->previous_post_type_slug,
200 'posts_per_page' => 10,
201 'post_status' => 'any',
202 ]
203 );
204
205 return $query->posts;
206 }
207 }
1 <?php
2 /**
3 * Migration submenu.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Admin\Migration;
11
12 use Block_Lab\Component_Abstract;
13 use Block_Lab\Admin\License;
14
15 /**
16 * Class Post_Type
17 */
18 class Submenu extends Component_Abstract {
19
20 /**
21 * The menu slug for the migration menu.
22 *
23 * @var string
24 */
25 const MENU_SLUG = 'block-lab-migration';
26
27 /**
28 * The user capability to migrate posts and post content.
29 *
30 * @var string
31 */
32 const MIGRATION_CAPABILITY = 'edit_others_posts';
33
34 /**
35 * The query var to deactivate this plugin and activate the new one.
36 *
37 * @var string
38 */
39 const QUERY_VAR_DEACTIVATE_AND_GCB_PAGE = 'bl_deactivate_and_gcb';
40
41 /**
42 * The query var to deactivate this plugin and activate the new one.
43 *
44 * @var string
45 */
46 const NONCE_ACTION_DEACTIVATE = 'deactivate_bl_and_activate_new';
47
48 /**
49 * Adds the actions.
50 */
51 public function register_hooks() {
52 add_action( 'admin_menu', [ $this, 'add_submenu_page' ], 9 );
53 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
54 add_action( 'admin_bar_init', [ $this, 'maybe_activate_plugin' ] );
55 }
56
57 /**
58 * Adds the submenu page for migration.
59 */
60 public function add_submenu_page() {
61 if ( $this->user_can_view_migration_page() ) {
62 add_submenu_page(
63 'edit.php?post_type=block_lab',
64 __( 'Migrate to Genesis Custom Blocks', 'block-lab' ),
65 __( 'Migrate', 'block-lab' ),
66 'manage_options',
67 self::MENU_SLUG,
68 [ $this, 'render_page' ]
69 );
70 }
71 }
72
73 /**
74 * Adds the scripts for the submenu.
75 */
76 public function enqueue_scripts() {
77 $page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_STRING );
78
79 // Only enqueue if on the migration page.
80 if ( self::MENU_SLUG === $page && $this->user_can_view_migration_page() ) {
81 wp_enqueue_style(
82 self::MENU_SLUG,
83 block_lab()->get_url( 'css/admin.migration.css' ),
84 [],
85 block_lab()->get_version()
86 );
87
88 $script_config = require block_lab()->get_path( 'js/admin.migration.asset.php' );
89 wp_enqueue_script(
90 self::MENU_SLUG,
91 block_lab()->get_url( 'js/admin.migration.js' ),
92 $script_config['dependencies'],
93 $script_config['version'],
94 true
95 );
96
97 $gcb_url = add_query_arg(
98 [
99 self::QUERY_VAR_DEACTIVATE_AND_GCB_PAGE => true,
100 '_wpnonce' => wp_create_nonce( self::NONCE_ACTION_DEACTIVATE ),
101 ],
102 admin_url()
103 );
104
105 $is_pro = block_lab()->is_pro();
106 $genesis_pro_subscription_key = get_option( Subscription_Api::OPTION_NAME_GENESIS_PRO_SUBSCRIPTION_KEY );
107 $script_data = [
108 'isPro' => $is_pro,
109 'gcbUrl' => $gcb_url,
110 ];
111
112 if ( $genesis_pro_subscription_key ) {
113 $script_data['genesisProKey'] = $genesis_pro_subscription_key;
114 }
115
116 wp_add_inline_script(
117 self::MENU_SLUG,
118 'const blockLabMigration = ' . wp_json_encode( $script_data ) . ';',
119 'before'
120 );
121 }
122 }
123
124 /**
125 * Gets whether the current user can view the migration page.
126 *
127 * @return bool Whether the user can view the migration page.
128 */
129 public function user_can_view_migration_page() {
130 return current_user_can( 'install_plugins' ) && current_user_can( self::MIGRATION_CAPABILITY );
131 }
132
133 /**
134 * Renders the submenu page.
135 */
136 public function render_page() {
137 echo '<div class="bl-migration__content"></div>';
138 }
139
140 /**
141 * Conditionally deactivates this plugin and goes to the Genesis Custom Blocks page.
142 *
143 * The logic to deactivate the plugin was mainly copied from Core.
144 * https://github.com/WordPress/wordpress-develop/blob/61803a37a41eca95efe964c7e02c768de6df75fa/src/wp-admin/plugins.php#L196-L221
145 */
146 public function maybe_activate_plugin() {
147 $previous_plugin_file = 'block-lab/block-lab.php';
148
149 if ( empty( $_GET[ self::QUERY_VAR_DEACTIVATE_AND_GCB_PAGE ] ) ) {
150 return;
151 }
152
153 if ( ! current_user_can( 'deactivate_plugin', $previous_plugin_file ) ) {
154 wp_die( esc_html__( 'Sorry, you are not allowed to deactivate this plugin.', 'block-lab' ) );
155 }
156
157 check_admin_referer( self::NONCE_ACTION_DEACTIVATE );
158
159 if ( ! is_network_admin() && is_plugin_active_for_network( $previous_plugin_file ) ) {
160 wp_die( esc_html__( 'Sorry, you are not allowed to deactivate this network-active plugin.', 'block-lab' ) );
161 }
162
163 deactivate_plugins( $previous_plugin_file, false, is_network_admin() );
164
165 if ( ! is_network_admin() ) {
166 update_option( 'recently_activated', [ $previous_plugin_file => time() ] + (array) get_option( 'recently_activated' ) );
167 } else {
168 update_site_option( 'recently_activated', [ $previous_plugin_file => time() ] + (array) get_site_option( 'recently_activated' ) );
169 }
170
171 // Go to the Genesis Custom Blocks page.
172 wp_safe_redirect(
173 add_query_arg(
174 'post_type',
175 'genesis_custom_block',
176 admin_url( 'edit.php' )
177 )
178 );
179 }
180 }
1 <?php
2 /**
3 * Verifies the Genesis Pro subscription, and saves the link to download GCB Pro.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Admin\Migration;
11
12 use WP_Error;
13 use WP_REST_Request;
14 use WP_REST_Response;
15 use Block_Lab\Component_Abstract;
16
17 /**
18 * Class Subscription_Api
19 */
20 class Subscription_Api extends Component_Abstract {
21
22 /**
23 * Option name where the subscription key is stored for Genesis Pro plugins.
24 *
25 * @var string
26 */
27 const OPTION_NAME_GENESIS_PRO_SUBSCRIPTION_KEY = 'genesis_pro_subscription_key';
28
29 /**
30 * Transient name where the subscription endpoint response is stored.
31 *
32 * @var string
33 */
34 const TRANSIENT_NAME_GCB_PRO_DOWNLOAD_LINK = 'genesis_custom_blocks_pro_download_link';
35
36 /**
37 * Adds the component action.
38 */
39 public function register_hooks() {
40 add_action( 'rest_api_init', [ $this, 'register_route_update_subscription_key' ] );
41 }
42
43 /**
44 * Registers a route to update the subscription key.
45 */
46 public function register_route_update_subscription_key() {
47 register_rest_route(
48 block_lab()->get_slug(),
49 'update-subscription-key',
50 [
51 'methods' => 'POST',
52 'callback' => [ $this, 'get_update_subscription_key_response' ],
53 'permission_callback' => function() {
54 return current_user_can( 'manage_options' );
55 },
56 'accept_json' => true,
57 ]
58 );
59 }
60
61 /**
62 * Gets the REST API response to the request to update the subscription key.
63 *
64 * @param WP_REST_Request $data Data sent in the POST request.
65 * @return WP_REST_Response|WP_Error A WP_REST_Response on success, WP_Error on failure.
66 */
67 public function get_update_subscription_key_response( $data ) {
68 $key = $data->get_param( 'subscriptionKey' );
69
70 if ( empty( $key ) ) {
71 $this->delete_subscription_data();
72 return new WP_Error( 'empty_subscription_key', __( 'Empty subscription key', 'block-lab' ) );
73 }
74
75 $sanitized_key = $this->sanitize_subscription_key( $key );
76 $subscription_response = $this->get_subscription_response( $sanitized_key );
77 if ( $subscription_response->is_valid() && ! empty( $subscription_response->get_product_info()->download_link ) ) {
78 $was_option_update_successful = update_option( self::OPTION_NAME_GENESIS_PRO_SUBSCRIPTION_KEY, $sanitized_key );
79
80 if ( ! $was_option_update_successful ) {
81 $existing_option = get_option( self::OPTION_NAME_GENESIS_PRO_SUBSCRIPTION_KEY );
82
83 // update_option() will return false when trying to save the same option that's already saved.
84 // In that case, there's no need for an error, but any other failure should be an error.
85 if ( $sanitized_key !== $existing_option ) {
86 $this->delete_subscription_data();
87 return new WP_Error( 'option_not_updated', __( 'The option was not updated', 'block-lab' ) );
88 }
89 }
90
91 set_transient(
92 self::TRANSIENT_NAME_GCB_PRO_DOWNLOAD_LINK,
93 esc_url_raw( $subscription_response->get_product_info()->download_link )
94 );
95
96 return rest_ensure_response( [ 'success' => true ] );
97 } else {
98 $this->delete_subscription_data();
99 return new WP_Error(
100 $subscription_response->get_error_code(),
101 $this->get_subscription_invalid_message( $subscription_response->get_error_code() )
102 );
103 }
104 }
105
106 /**
107 * Deletes the stored Genesis Pro key and the GCB Pro download link.
108 */
109 public function delete_subscription_data() {
110 delete_option( self::OPTION_NAME_GENESIS_PRO_SUBSCRIPTION_KEY );
111 delete_transient( self::TRANSIENT_NAME_GCB_PRO_DOWNLOAD_LINK );
112 }
113
114 /**
115 * Gets a new subscription response.
116 *
117 * @param string $key The subscription key to check.
118 * @return Subscription_Response The subscription response.
119 */
120 public function get_subscription_response( $key ) {
121 return new Subscription_Response( $key );
122 }
123
124 /**
125 * Admin message for incorrect subscription details.
126 *
127 * @param string $error_code The error code from the endpoint.
128 * @return string The error message to display.
129 */
130 public function get_subscription_invalid_message( $error_code ) {
131 switch ( $error_code ) {
132 case 'key-unknown':
133 return esc_html__( 'The subscription key you entered appears to be invalid or is not associated with this product. Please verify the key you have saved here matches the key in your WP Engine Account Portal.', 'block-lab' );
134
135 case 'key-invalid':
136 return esc_html__( 'The subscription key you entered is invalid. Get your subscription key in the WP Engine Account Portal.', 'block-lab' );
137
138 case 'key-deleted':
139 return esc_html__( 'Your subscription key was regenerated in the WP Engine Account Portal but was not updated in this settings page. Update your subscription key here to receive updates.', 'block-lab' );
140
141 case 'subscription-expired':
142 return esc_html__( 'Your Genesis Pro subscription has expired. Please renew it.', 'block-lab' );
143
144 case 'subscription-notfound':
145 return esc_html__( 'A valid subscription for your subscription key was not found. Please contact support.', 'block-lab' );
146
147 case 'product-unknown':
148 return esc_html__( 'The product you requested information for is unknown. Please contact support.', 'block-lab' );
149
150 default:
151 return esc_html__( 'There was an unknown error connecting to the update service. Please ensure the key you have saved here matches the key in your WP Engine Account Portal. This issue could be temporary. Please contact support if this error persists.', 'block-lab' );
152 }
153 }
154
155 /**
156 * Gets the sanitized subscription key.
157 *
158 * @param string $subscription_key The subscription key.
159 * @return string The sanitized key.
160 */
161 public function sanitize_subscription_key( $subscription_key ) {
162 return preg_replace( '/[^A-Za-z0-9_-]/', '', $subscription_key );
163 }
164 }
1 <?php
2 /**
3 * The Genesis Pro subscription response.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Admin\Migration;
11
12 use stdClass;
13
14 /**
15 * Class Subscription_Response
16 */
17 class Subscription_Response {
18
19 /**
20 * Endpoint to validate the Genesis Pro subscription key.
21 *
22 * @var string
23 */
24 const ENDPOINT = 'https://wp-product-info.wpesvc.net/v1/plugins/genesis-custom-blocks-pro/subscriptions/';
25
26 /**
27 * The code expected in a success response.
28 *
29 * @var string
30 */
31 const SUCCESS_CODE = 200;
32
33 /**
34 * Whether the subscription key is valid.
35 *
36 * @var bool
37 */
38 private $is_valid = false;
39
40 /**
41 * The error code, if any.
42 *
43 * @var string|null
44 */
45 private $error_code;
46
47 /**
48 * The product info.
49 *
50 * @var stdClass|null
51 */
52 private $product_info;
53
54 /**
55 * Constructs the class.
56 *
57 * @param string $subscription_key The subscription key to check.
58 */
59 public function __construct( $subscription_key ) {
60 $this->evaluate( $subscription_key );
61 }
62
63 /**
64 * Evaluates the response, storing the response body and a possible error message.
65 *
66 * @param string $subscription_key The subscription key to check.
67 */
68 public function evaluate( $subscription_key ) {
69 $response = wp_remote_get(
70 self::ENDPOINT . $subscription_key,
71 [
72 'timeout' => defined( 'DOING_CRON' ) && DOING_CRON ? 30 : 3,
73 'user-agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ),
74 'body' => [
75 'version' => block_lab()->get_version(),
76 ],
77 ]
78 );
79
80 if ( is_wp_error( $response ) || self::SUCCESS_CODE !== wp_remote_retrieve_response_code( $response ) ) {
81 if ( is_wp_error( $response ) ) {
82 $this->error_code = $response->get_error_code();
83 } else {
84 $response_body = json_decode( wp_remote_retrieve_body( $response ), false );
85 $this->error_code = ! empty( $response_body->error_code ) ? $response_body->error_code : 'unknown';
86 }
87
88 return;
89 }
90
91 $this->is_valid = true;
92 $this->product_info = new stdClass();
93 $response_body = json_decode( wp_remote_retrieve_body( $response ) );
94
95 if ( ! is_object( $response_body ) ) {
96 $response_body = new stdClass();
97 }
98
99 $this->product_info = $response_body;
100 }
101
102 /**
103 * Gets whether the subscription key is valid.
104 *
105 * @return bool
106 */
107 public function is_valid() {
108 return $this->is_valid;
109 }
110
111 /**
112 * Gets the error code, if any.
113 *
114 * @return string|null
115 */
116 public function get_error_code() {
117 return $this->error_code;
118 }
119
120 /**
121 * Gets the product info, or null if there isn't any.
122 *
123 * @return stdClass|null
124 */
125 public function get_product_info() {
126 return $this->product_info;
127 }
128 }
1 <?php
2 /**
3 * Plugin Autoloader
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 /**
11 * Register Autoloader
12 */
13 spl_autoload_register(
14 function ( $class ) {
15 // Assume we're using namespaces (because that's how the plugin is structured).
16 $namespace = explode( '\\', $class );
17 $root = array_shift( $namespace );
18
19 // If a class ends with "Trait" then prefix the filename with 'trait-', else use 'class-'.
20 $class_trait = preg_match( '/Trait$/', $class ) ? 'trait-' : 'class-';
21
22 // If we're not in the plugin's namespace then just return.
23 if ( 'Block_Lab' !== $root ) {
24 return;
25 }
26
27 // Class name is the last part of the FQN.
28 $class_name = array_pop( $namespace );
29
30 // Remove "Trait" from the class name.
31 if ( 'trait-' === $class_trait ) {
32 $class_name = str_replace( 'Trait', '', $class_name );
33 }
34
35 $filename = $class_trait . $class_name . '.php';
36
37 // For file naming, the namespace is everything but the class name and the root namespace.
38 $namespace = trim( implode( DIRECTORY_SEPARATOR, $namespace ) );
39
40 // Because WordPress file naming conventions are odd.
41 $filename = strtolower( str_replace( '_', '-', $filename ) );
42 $namespace = strtolower( str_replace( '_', '-', $namespace ) );
43
44 // Get the path to our files.
45 $directory = dirname( __FILE__ );
46 if ( ! empty( $namespace ) ) {
47 $directory .= DIRECTORY_SEPARATOR . $namespace;
48 }
49
50 $file = $directory . DIRECTORY_SEPARATOR . $filename;
51
52 if ( file_exists( $file ) ) {
53 require_once $file;
54 }
55 }
56 );
1 <?php
2 /**
3 * Block.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Blocks;
11
12 /**
13 * Class Block
14 */
15 class Block {
16
17 /**
18 * Block name (slug).
19 *
20 * @var string
21 */
22 public $name = '';
23
24 /**
25 * Block title.
26 *
27 * @var string
28 */
29 public $title = '';
30
31 /**
32 * Exclude the block in these post types.
33 *
34 * @var array
35 */
36 public $excluded = [];
37
38 /**
39 * Icon.
40 *
41 * @var string
42 */
43 public $icon = '';
44
45 /**
46 * Category. An array containing the keys slug, title, and icon.
47 *
48 * @var array
49 */
50 public $category = [
51 'slug' => '',
52 'title' => '',
53 'icon' => '',
54 ];
55
56 /**
57 * Block keywords.
58 *
59 * @var string[]
60 */
61 public $keywords = [];
62
63 /**
64 * Block fields.
65 *
66 * @var Field[]
67 */
68 public $fields = [];
69
70 /**
71 * Block constructor.
72 *
73 * @param int|bool $post_id Post ID.
74 *
75 * @return void
76 */
77 public function __construct( $post_id = false ) {
78 if ( ! $post_id ) {
79 return;
80 }
81
82 $post = get_post( $post_id );
83
84 if ( ! $post instanceof \WP_Post ) {
85 return;
86 }
87
88 $this->name = $post->post_name;
89 $this->from_json( $post->post_content );
90 }
91
92 /**
93 * Construct the Block from a JSON blob.
94 *
95 * @param string $json JSON blob.
96 *
97 * @return void
98 */
99 public function from_json( $json ) {
100 $json = json_decode( $json, true );
101
102 if ( ! isset( $json[ 'block-lab/' . $this->name ] ) ) {
103 return;
104 }
105
106 $config = $json[ 'block-lab/' . $this->name ];
107
108 $this->from_array( $config );
109 }
110
111 /**
112 * Construct the Block from a config array.
113 *
114 * @param array $config An array containing field parameters.
115 *
116 * @return void
117 */
118 public function from_array( $config ) {
119 if ( isset( $config['name'] ) ) {
120 $this->name = $config['name'];
121 }
122
123 if ( isset( $config['title'] ) ) {
124 $this->title = $config['title'];
125 }
126
127 if ( isset( $config['excluded'] ) ) {
128 $this->excluded = $config['excluded'];
129 }
130
131 if ( isset( $config['icon'] ) ) {
132 $this->icon = $config['icon'];
133 }
134
135 if ( isset( $config['category'] ) ) {
136 $this->category = $config['category'];
137 if ( ! is_array( $this->category ) ) {
138 $this->category = $this->get_category_array_from_slug( $this->category );
139 }
140 }
141
142 if ( isset( $config['keywords'] ) ) {
143 $this->keywords = $config['keywords'];
144 }
145
146 if ( isset( $config['fields'] ) ) {
147 foreach ( $config['fields'] as $key => $field ) {
148 $this->fields[ $key ] = new Field( $field );
149 }
150 }
151 }
152
153 /**
154 * Get the Block as a JSON blob.
155 *
156 * @return string
157 */
158 public function to_json() {
159 $config['name'] = $this->name;
160 $config['title'] = $this->title;
161 $config['excluded'] = $this->excluded;
162 $config['icon'] = $this->icon;
163 $config['category'] = $this->category;
164 $config['keywords'] = $this->keywords;
165
166 $config['fields'] = [];
167 foreach ( $this->fields as $key => $field ) {
168 $config['fields'][ $key ] = $field->to_array();
169 }
170
171 return wp_json_encode( [ 'block-lab/' . $this->name => $config ], JSON_UNESCAPED_UNICODE );
172 }
173
174 /**
175 * This is a backwards compatibility fix.
176 *
177 * Block categories used to be saved as strings, but were always included in
178 * the default list of categories, so we can find them.
179 *
180 * It's not possible to use get_block_categories() here, as Block's are
181 * sometimes instantiated before that function is available.
182 *
183 * @param string $slug The category slug to find.
184 *
185 * @return array
186 */
187 public function get_category_array_from_slug( $slug ) {
188 return [
189 'slug' => $slug,
190 'title' => ucwords( $slug, '-' ),
191 'icon' => null,
192 ];
193 }
194 }
1 <?php
2 /**
3 * Block Field.
4 *
5 * @package Block_Lab
6 * @copyright Copyright(c) 2020, Block Lab
7 * @license http://opensource.org/licenses/GPL-2.0 GNU General Public License, version 2 (GPL-2.0)
8 */
9
10 namespace Block_Lab\Blocks;
11
12 /**
13 * Class Field
14 */
15 class Field {
16
17 /**
18 * Field name (slug).
19 *
20 * @var string
21 */
22 public $name = '';
23
24 /**
25 * Field label.
26 *
27 * @var string
28 */
29 public $label = '';
30
31 /**
32 * Field control type.
33 *
34 * @var string
35 */
36 public $control = 'text';
37
38 /**
39 * Field variable type.
40 *
41 * @var string
42 */
43 public $type = 'string';
44
45 /**
46 * Field order.
47 *
48 * @var int
49 */
50 public $order = 0;
51
52 /**
53 * Field settings.
54 *
55 * @var array
56 */
57 public $settings = [];
58
59 /**
60 * Field constructor.
61 *
62 * @param array $config An associative array with keys corresponding to the Field's properties.
63 */
64 public function __construct( $config = [] ) {
65 $this->from_array( $config );
66 }
67
68 /**
69 * Get field properties as an array, ready to be stored as JSON.
70 *
71 * @return array
72 */
73 public function to_array() {
74 $config = [
75 'name' => $this->name,
76 'label' => $this->label,
77 'control' => $this->control,
78 'type' => $this->type,
79 'order' => $this->order,
80 ];
81
82 $config = array_merge(
83 $config,
84 $this->settings
85 );
86
87 // Handle the sub-fields setting used by the Repeater.
88 if ( isset( $this->settings['sub_fields'] ) ) {
89 /**
90 * Recursively loop through sub-fields.
91 *
92 * @var string $key The name of the sub-field's parent.
93 * @var Field $field The sub-field.
94 */
95 foreach ( $this->settings['sub_fields'] as $key => $field ) {
96 $config['sub_fields'][ $key ] = $field->to_array();
97 }
98 }
99
100 return $config;
101 }
102
103 /**
104 * Set field properties from an array, after being stored as JSON.
105 *
106 * @param array $config An array containing field parameters.
107 */
108 public function from_array( $config ) {
109 if ( isset( $config['name'] ) ) {
110 $this->name = $config['name'];
111 }
112 if ( isset( $config['label'] ) ) {
113 $this->label = $config['label'];
114 }
115 if ( isset( $config['control'] ) ) {
116 $this->control = $config['control'];
117 }
118 if ( isset( $config['type'] ) ) {
119 $this->type = $config['type'];
120 }
121 if ( isset( $config['order'] ) ) {
122 $this->order = $config['order'];
123 }
124 if ( isset( $config['settings'] ) ) {
125 $this->settings = $config['settings'];
126 }
127
128 if ( ! isset( $config['type'] ) ) {
129 $control_class_name = 'Block_Lab\\Blocks\\Controls\\';
130 $control_class_name .= ucwords( $this->control, '_' );
131 if ( class_exists( $control_class_name ) ) {
132 /**
133 * An instance of the control, to retrieve the correct type.
134 *
135 * @var Control_Abstract $control_class
136 */
137 $control_class = new $control_class_name();
138 $this->type = $control_class->type;
139 }
140 }
141
142 // Add any other non-default keys to the settings array.
143 $field_defaults = [ 'name', 'label', 'control', 'type', 'order', 'settings' ];
144 $field_settings = array_diff( array_keys( $config ), $field_defaults );
145
146 foreach ( $field_settings as $settings_key ) {
147 $this->settings[ $settings_key ] = $config[ $settings_key ];
148 }
149
150 // Handle the sub-fields setting used by the Repeater.
151 if ( isset( $this->settings['sub_fields'] ) ) {
152 /**
153 * Recursively loop through sub-fields.
154 */
155 foreach ( $this->settings['sub_fields'] as $key => $field ) {
156 $this->settings['sub_fields'][ $key ] = new Field( $field );
157 }
158 }
159 }
160
161 /**
162 * Return the value with the correct variable type.
163 *
164 * @param mixed $value The value to typecast.
165 * @return mixed
166 */
167 public function cast_value( $value ) {
168 switch ( $this->type ) {
169 case 'string':
170 $value = strval( $value );
171 break;
172 case 'textarea':
173 $value = strval( $value );
174 if ( isset( $this->settings['new_lines'] ) ) {
175 if ( 'autop' === $this->settings['new_lines'] ) {
176 $value = wpautop( $value );
177 }
178 if ( 'autobr' === $this->settings['new_lines'] ) {
179 $value = nl2br( $value );
180 }
181 }
182 break;
183 case 'boolean':
184 if ( 1 === $value ) {
185 $value = true;
186 }
187 break;
188 case 'integer':
189 $value = intval( $value );
190 break;
191 case 'array':
192 if ( ! $value ) {
193 $value = [];
194 } else {
195 $value = (array) $value;
196 }
197 break;
198 }
199
200 return $value;
201 }
202
203 /**
204 * Gets the field value as a string.
205 *
206 * @param mixed $value The field value.
207 *
208 * @return string $value The value to echo.
209 */
210 public function cast_value_to_string( $value ) {
211 if ( is_array( $value ) ) {
212 return implode( ', ', $value );
213 }
214
215 if ( true === $value ) {
216 return __( 'Yes', 'block-lab' );
217 }
218
219 if ( false === $value ) {
220 return __( 'No', 'block-lab' );
221 }
222
223 return strval( $value );
224 }
225 }
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.