Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit Site: Add theme exporter. #22922

Merged
merged 2 commits into from
Jun 9, 2020
Merged

Conversation

epiqueras
Copy link
Contributor

Closes #19260

Description

This PR adds a theme exporter button to the site editor header's "More" dropdown.

It is like #21958 but implements the zip creation in the server to avoid loading a large zip management library in the client.

How to test this?

Try exporting templates/parts with different modifications and verify that the exported files match what you have on the editor.

Screenshots

Screen Shot 2020-06-04 at 4 22 31 PM

Types of Changes

New Feature: The site editor now has a theme exporter button for exporting customized templates and template parts in a theme zip file.

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR.

@github-actions
Copy link

github-actions bot commented Jun 4, 2020

Size Change: +4.91 kB (0%)

Total Size: 1.13 MB

Filename Size Change
build/a11y/index.js 1.14 kB +1 B
build/annotations/index.js 3.62 kB +2 B (0%)
build/api-fetch/index.js 3.4 kB +1 B
build/autop/index.js 2.83 kB +1 B
build/block-directory/index.js 6.77 kB +21 B (0%)
build/block-editor/index.js 106 kB +357 B (0%)
build/block-editor/style-rtl.css 11.4 kB +2 B (0%)
build/block-editor/style.css 11.4 kB +2 B (0%)
build/block-library/editor-rtl.css 7.88 kB +10 B (0%)
build/block-library/editor.css 7.89 kB +10 B (0%)
build/block-library/index.js 127 kB +707 B (0%)
build/block-library/style-rtl.css 7.72 kB +33 B (0%)
build/block-library/style.css 7.72 kB +38 B (0%)
build/block-serialization-default-parser/index.js 1.88 kB -2 B (0%)
build/block-serialization-spec-parser/index.js 3.1 kB +1 B
build/blocks/index.js 48.1 kB -34 B (0%)
build/components/index.js 194 kB +776 B (0%)
build/components/style-rtl.css 19.5 kB +15 B (0%)
build/components/style.css 19.5 kB +10 B (0%)
build/compose/index.js 9.31 kB +2 B (0%)
build/core-data/index.js 11.4 kB +1 B
build/data-controls/index.js 1.29 kB -1 B
build/data/index.js 8.45 kB -4 B (0%)
build/date/index.js 5.47 kB -1 B
build/deprecated/index.js 772 B +1 B
build/dom-ready/index.js 569 B +1 B
build/dom/index.js 3.17 kB -1 B
build/edit-navigation/index.js 8.25 kB +1 B
build/edit-post/index.js 303 kB +311 B (0%)
build/edit-post/style-rtl.css 5.6 kB +168 B (3%)
build/edit-post/style.css 5.6 kB +168 B (3%)
build/edit-site/index.js 16.7 kB +1.68 kB (10%) ⚠️
build/edit-widgets/index.js 9.34 kB +512 B (5%) 🔍
build/editor/index.js 44.8 kB +120 B (0%)
build/format-library/index.js 7.72 kB +4 B (0%)
build/hooks/index.js 2.13 kB -3 B (0%)
build/html-entities/index.js 622 B +1 B
build/i18n/index.js 3.56 kB +1 B
build/is-shallow-equal/index.js 712 B +1 B
build/media-utils/index.js 5.3 kB +1 B
build/notices/index.js 1.79 kB +1 B
build/plugins/index.js 2.56 kB -1 B
build/redux-routine/index.js 2.85 kB -1 B
build/rich-text/index.js 14.8 kB -3 B (0%)
build/server-side-render/index.js 2.68 kB +3 B (0%)
build/token-list/index.js 1.28 kB -1 B
build/url/index.js 4.06 kB -1 B
build/viewport/index.js 1.85 kB -1 B
ℹ️ View Unchanged
Filename Size Change
build/blob/index.js 620 B 0 B
build/block-directory/style-rtl.css 892 B 0 B
build/block-directory/style.css 892 B 0 B
build/block-library/theme-rtl.css 684 B 0 B
build/block-library/theme.css 686 B 0 B
build/edit-navigation/style-rtl.css 918 B 0 B
build/edit-navigation/style.css 919 B 0 B
build/edit-site/style-rtl.css 2.96 kB 0 B
build/edit-site/style.css 2.96 kB 0 B
build/edit-widgets/style-rtl.css 2.4 kB 0 B
build/edit-widgets/style.css 2.4 kB 0 B
build/editor/editor-styles-rtl.css 425 B 0 B
build/editor/editor-styles.css 428 B 0 B
build/editor/style-rtl.css 4.26 kB 0 B
build/editor/style.css 4.27 kB 0 B
build/element/index.js 4.65 kB 0 B
build/escape-html/index.js 733 B 0 B
build/format-library/style-rtl.css 502 B 0 B
build/format-library/style.css 502 B 0 B
build/keyboard-shortcuts/index.js 2.52 kB 0 B
build/keycodes/index.js 1.94 kB 0 B
build/list-reusable-blocks/index.js 3.12 kB 0 B
build/list-reusable-blocks/style-rtl.css 226 B 0 B
build/list-reusable-blocks/style.css 226 B 0 B
build/nux/index.js 3.4 kB 0 B
build/nux/style-rtl.css 616 B 0 B
build/nux/style.css 613 B 0 B
build/primitives/index.js 1.5 kB 0 B
build/priority-queue/index.js 789 B 0 B
build/shortcode/index.js 1.7 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

lib/template-loader.php Show resolved Hide resolved
} )
.then( ( res ) => res.blob() )
.then( ( blob ) =>
downloadjs(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there not a built-in way to download a file from WordPress? I don't like that we have to add a whole new dependency for it 😕

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I know of. It's a tiny browser-compat shim.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just "link" to the download URL instead of using apiFetch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need to persist the file in a public directory. This seemed cleaner.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure that's not needed since the request already sends a zip file but maybe there's something I'm missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you mean a link to the URL using an href. I thought you meant having the API return the link to the file.

That won't work either, because we need the nonce middleware and all that for authentication. Unless you think there is another way to authenticate there?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe if you use a link, you might not need the nonces and things like that as it's a "GET" request and it uses the session right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried it, but it's still needed.

The API fetch is also a "GET" request.

Copy link
Contributor

@mcsf mcsf Jun 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely weird to bring in downloadjs just for this, even if it's not a huge lib.

The limitation with directly accessing the endpoint lies not in the nonce requirement, since that can be satisfied with the _wpnonce GET parameter, but in the authentication itself.

My reflex here was that we should try to make this work with plain URLs, but after digging a bit I don't think it's very practical.

However, depending on downloadjs is a little unfortunate, and looking at its source it seems like it does a lot of legwork to support older browsers that we don't need to care about anymore, not to mention that it supports different download methods (e.g. URL-based). Could we replace it with something dead-simple? The following works for me in my browser console:

function downloadBlob( data, filename, mimeType ) {
  const blob = new Blob( [ data ], { type: mimeType } );
  const url = URL.createObjectURL( blob );
  const anchor = document.createElement( 'a' );
  anchor.download = filename;
  anchor.href = url;
  document.body.appendChild( anchor );
  anchor.click();
  document.body.removeChild( anchor );
}

downloadBlob( '<html>...</html>', 'home.html', 'text/html' );

If this is worthwhile I can open a PR.

$filename = tempnam( get_temp_dir(), 'edit-site-export' );
$zip = new ZipArchive();
$zip->open( $filename, ZipArchive::OVERWRITE );
$zip->addEmptyDir( 'theme' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not put block-templates and block-template-parts at the top-level? (Unless I'm wrong about typical theme .zip files having these directories on the top-level)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because unzipping a zip file yields its contents, not its contents wrapped in an extra directory. Try downloading the file to see what I mean.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe make a constant for the theme directory name? We might later want to add an option for the user to give a name to the theme 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can change it then. I don't see much value in complicating this very simple code with lots of string joins when it's not needed yet.

Copy link
Member

@TimothyBJacobs TimothyBJacobs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally the REST API would support serving these non-JSON responses using a WP_REST_Response object so you didn’t need to send headers yourself and die, but this is fine to me for the time being. I’ll try and work on that for 5.6.

@epiqueras
Copy link
Contributor Author

Thanks!

@epiqueras epiqueras merged commit 848b493 into master Jun 9, 2020
@epiqueras epiqueras deleted the add/theme-exporter-to-edit-site branch June 9, 2020 23:41
@github-actions github-actions bot added this to the Gutenberg 8.4 milestone Jun 9, 2020
Comment on lines +41 to +46
$zip->close();
header( 'Content-Type: application/zip' );
header( 'Content-Disposition: attachment; filename=edit-site-export.zip' );
header( 'Content-Length: ' . filesize( $filename ) );
flush();
echo readfile( $filename );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we clean up, delete the file? Especially since it's using temporary filenames, so we're not just overwriting the same file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an issue with keeping it around? Deleting it would technically be more work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do make changes here we should also switch to wp_tempnam which I missed the first time round.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious if that's just for consistency/style or is there some benefit to it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its mostly consistency and style, but it does appear to have some edge case handling.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an issue with keeping it around?

I don't think we can make assumptions on how often the location returned by get_temp_dir() is flushed. We must assume that ZIP files would proliferate over time.

Deleting it would technically be more work.

How so?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How so?

You always have to create it/overwrite it. Deleting it is another set of operations we could avoid, although I doubt it makes a measurable difference.

I think we can be safe here by using the same file name every time. That way it just overwrites the same file if it hasn't been cleaned up already.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, @epiqueras and I talked about this over Slack, and my recommendation was to ensure good random filenames (WordPress's wp_tempnam might be better at avoiding naming conflicts) and ensure we delete the file in the moment.

This stands in contrast with adopting a fixed filename so as to overwrite the file over time. This might be enough in simple sites, but is bound to cause race-condition-type problems in any larger installation, and especially in multisite or highly concurrent scenarios.

} )
.then( ( res ) => res.blob() )
.then( ( blob ) =>
downloadjs(
Copy link
Contributor

@mcsf mcsf Jun 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely weird to bring in downloadjs just for this, even if it's not a huge lib.

The limitation with directly accessing the endpoint lies not in the nonce requirement, since that can be satisfied with the _wpnonce GET parameter, but in the authentication itself.

My reflex here was that we should try to make this work with plain URLs, but after digging a bit I don't think it's very practical.

However, depending on downloadjs is a little unfortunate, and looking at its source it seems like it does a lot of legwork to support older browsers that we don't need to care about anymore, not to mention that it supports different download methods (e.g. URL-based). Could we replace it with something dead-simple? The following works for me in my browser console:

function downloadBlob( data, filename, mimeType ) {
  const blob = new Blob( [ data ], { type: mimeType } );
  const url = URL.createObjectURL( blob );
  const anchor = document.createElement( 'a' );
  anchor.download = filename;
  anchor.href = url;
  document.body.appendChild( anchor );
  anchor.click();
  document.body.removeChild( anchor );
}

downloadBlob( '<html>...</html>', 'home.html', 'text/html' );

If this is worthwhile I can open a PR.

@epiqueras
Copy link
Contributor Author

@mcsf

If this is worthwhile I can open a PR.

I saw lots of variations of that in StackOverflow, but most comments recommended using downloadjs because browser compatibility was hard.

I guess we can make our own package that targets the browsers WordPress cares about and test it thoroughly.

@mcsf
Copy link
Contributor

mcsf commented Jun 15, 2020

I saw lots of variations of that in StackOverflow, but most comments recommended using downloadjs because browser compatibility was hard.

If this sort of StackOverflow feedback is recent, then I guess we shouldn't be reinventing the wheel. If not, well… I don't care enough about the issue, I just would like to think that we don't need special libraries to download blobs in 2020. :)

@epiqueras
Copy link
Contributor Author

I just would like to think that we don't need special libraries to download blobs in 2020. :)

I know, classic Web.

@ellatrix ellatrix mentioned this pull request Jun 16, 2020
12 tasks
@noisysocks noisysocks mentioned this pull request Jun 24, 2020
@mcsf
Copy link
Contributor

mcsf commented Jul 2, 2020

Looking at something totally unrelated, I stumbled upon this:

export function download( fileName, content, contentType ) {
const file = new window.Blob( [ content ], { type: contentType } );
// IE11 can't use the click to download technique
// we use a specific IE11 technique instead.
if ( window.navigator.msSaveOrOpenBlob ) {
window.navigator.msSaveOrOpenBlob( file, fileName );
} else {
const a = document.createElement( 'a' );
a.href = URL.createObjectURL( file );
a.download = fileName;
a.style.display = 'none';
document.body.appendChild( a );
a.click();
document.body.removeChild( a );
}
}

@epiqueras
Copy link
Contributor Author

Mmm, @youknowriad should we use that instead?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Type] Experimental Experimental feature or API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Edit Site: Export all templates from wp_templates
6 participants