Textpattern CMS support forum
You are not logged in. Register | Login | Help
- Topics: Active | Unanswered
ZIP plugin file requires archive subfolder, throws error otherwise.
I tried building a plugin as a zip file. The initial contents of the zip file were:
- wet_example.php
- help.textile
- textpack.txt
- manifest.json
Upon installation, I encountered a “Badly formed or empty plugin code.” error.
After some back-and-forth, I learned that the zip file must be an archive including the plugin sub-folder name. In other words, these files must be contained in a subfolder named after the plugin:
- wet_example/wet_example.php
- wet_example/help.textile
- wet_example/textpack.txt
- wet_example/manifest.json
As I have not thoroughly read the code, this is merely anecdotal evidence and might be taken with a pinch of salt.
To make matters worse, I am building on Windows 24H2. The built-in Compress-Archive cmdlet struggles to produce archives containing relative folder structures. Yes, it’s 2025 now, in case you were wondering ;)
Nevertheless, I am curious about the reasons why this specific subfolder structure is a requirement when, in my humble naïve opinion, a flat archive containing all the files comprising the plugin might also suffice.
Please shed a light!
Offline
Re: ZIP plugin file requires archive subfolder, throws error otherwise.
wet wrote #340229:
why this specific subfolder structure is a requirement
Hmm, it isn’t. Look at com_article_image for an example. Whether you download and import the Release .zip file (or the Code zip file, extract it and drag the enclosed .zip up to a 4.9.0 installation), it shows the three parts correctly in the Preview step – code, help and textpack. There’s no plugin-named subfolder. But if it had other files or subfolders, it would install those too in the plugin’s dir, mimicking the structure of the zip.
The only stipulations, as far as I recall, are that the zip file must be exactly named as the name of your plugin and must match the name of your .php file in the root, because it uses those to assume the name of your plugin and its subfolder inside PLUGINPATH
.
That said, some caveats for those of us on non-*nix systems:
- If you try and right-click-Compress a folder on macOS and drag the resulting zip up, it may fail to install. macOS tends to add the hidden resource forks and bundles to it, plus
.DS_Store
files, which get installed alongside. You don’t want to burden the zip with those (even though we strip anything inside__MACOSX
folders internally). A safer bet is to use the command line to build the zip file and add files to it (albeit it’s a bit more cumbersome to exclude certain directories like .git if you’re developing locally under version control). - Windows is persnickety. Won’t handle symlinks. Usually hard-codes full paths (as you found) but we specifically handle that because it was discovered during development and I coded around it in the importer.
tl;dr If your zip file name and/or .php file don’t match, it will likely not install. Otherwise, it should work. If not, please drop me a zip file and I’ll see if I can diagnose why it’s not working.
The smd plugin menagerie — for when you need one more gribble of power from Textpattern. Bleeding-edge code available on GitHub.
Txp Builders – finely-crafted code, design and Txp
Offline
Re: ZIP plugin file requires archive subfolder, throws error otherwise.
Thanks for the helpful pointers, Stef.
Nevertheless, I am stuck.
Here’s the zip file I am dealing with.
Single-stepping through textpattern/vendors/Textpattern/Plugin/Plugin.php, this is what I encountered – please go along with me:
At #330, the content of variables at this stage is:
$keyFiles | Array ( [code] => wet_textfilter_markdown.php [manifest] => manifest.json [help] => help.html [help_raw] => help.textile [textpack] => textpack.txp [data] => data.txp ) |
$filename | ‘wet_textfilter_markdown’ |
$key | ‘code’ |
$fn | ‘wet_textfilter_markdown.php’ |
$keyFile | ‘wet_textfilter_markdown/wet_textfilter_markdown.php’ |
It seems to me that the expression $keyFile = $filename.'/'.$fn
in #331 prepends $filename
as a directory name, followed by a directory separator and the names of the various key files. This results in $keyContent
being empty when the loop terminates.
At least, this is what I assume results in the error message I am experiencing.
NB: ‘com_article_image-v.1.0.beta.zip’ does contain a subfolder named ‘com_article_image-v.1.0.beta’ carrying all files. It seems that the ZIP installation routine is tailored to ZIP files exactly as they are built by GitHub.
Offline
Re: ZIP plugin file requires archive subfolder, throws error otherwise.
Ah, okay, I see the issue. And it’s not because we’re bending to GitHub at all; we’re bending to Windows.
On the occasions when I zip up a plugin for distribution, I issue this type of command inside my plugins directory (which has a separate folder for each plugin I own):
zip -r smd_token.zip smd_token -x '**/.*' -x '**/__MACOS'
That includes the folder name at the top level, so when it’s unzipped, it can just be dropped verbatim into the PLUGINPATH without us having to assume anything. Convention over configuration… sort of!
When unzipping (e.g. double-clicking), I was originally under the mistaken belief that the reason it dumped the files into a subfolder was because it used the zip filename. I changed that opinion based on an irritating experience I had when I downloaded a plugin and already had a zip file of the same name in that directory. macOS helpfully called the file smd_token 2.zip. When I dragged that to my Plugins installation panel, all hell broke loose and it wouldn’t install, even though the contents of the zip file contained the correctly-named top-level directory and all files beneath.
Perform this test, for example:
- Download com_article_image from the releases page.
- In your local file system, duplicate the .zip file and rename it com_article_image2.zip if necessary.
- Inspect the contents of both archives:
unzip -l com_article_image.zip
vsunzip -l com_article_image2.zip
. Note both are internally identical. - Drag com_article_image.zip to the Txp Plugins panel’s Browse widget and Upload. Note the three separate panes in the Preview step. Cancel the installation.
- Repeat the above with com_article_image2.zip. Note the single pane with the list of files in the archive.
Conclusion: the zip file name is important and is used as the basis of the unzipping process, even though the contents also contain the subfolder.
This was an unfortunate by-product of getting round the annoying hard-coded full path shenanigans of Windows. Since we couldn’t rely on the internal subdirectory being relative to itself, we simply assume the zip file name is the intended subfolder name, find the corresponding portion of the path name inside each file in the archive tree, ignore the path up to that point and unzip its contents into the PLUGINPATH/subfolder-taken-from-the-zip-filename
directory, creating it if it doesn’t exist. Recursively for any subfolders inside the archive too.
Now, consider your wishes. If we have a bunch of ‘loose’ files in the archive without a directory containing them, and you had them stored locally before you zipped them up on your Windows machine in C:\Users\rwetzlmayr\plugins\dev\markdown-filter, your archive contains every file with that path in its internal tree. When we extract each file, we compare it with the zip filename to find where the relative path begins… and don’t find it. So the installer panics and just shows you the files as-is under the directory taken from the zip filename. But as it can’t “find” the necessary .textile help, manifest.json, .php file inside the archive, it doesn’t load its internal arrays and thus doesn’t install correctly.
[EDIT: side note that this also helps protect us against haf-assed attempts to upload borked or malicious zip files that don’t adhere to the expected internal folder structure]
It’s frustrating. But it was the only way at the time I could figure to get round the full-paths-in-the-archive issue. We chose convention, however limiting that may be.
Conceivably, we could be more clever and presume that if it can’t find a matching subfolder path, then every file must be destined to live in the top-level dir, named as per the zip filename. Perhaps we can somehow locate the parts of each internal filename that are identical and assume that bit must be the hard-coded path, roll our coding eyes at Windows, and strip it off. The same would occur for regular relative-based file trees, but that shouldn’t cause an issue.
It does, however, rely on inspecting all the files first to find commonality and I don’t know how much control we have over the PHP unzipping process. Don’t really like the idea of combing an archive twice (can we even do that once it’s open?) but it might be doable. Certainly happy to investigate, unless you fancy playing to try and find a way round it?
Last edited by Bloke (2025-08-20 10:37:26)
The smd plugin menagerie — for when you need one more gribble of power from Textpattern. Bleeding-edge code available on GitHub.
Txp Builders – finely-crafted code, design and Txp
Offline
Re: ZIP plugin file requires archive subfolder, throws error otherwise.
Bloke wrote #340237:
On the occasions when I zip up a plugin for distribution, I issue this type of command inside my plugins directory (which has a separate folder for each plugin I own):
zip -r smd_token.zip smd_token -x '**/.*' -x '**/__MACOS'...
I have a little droplet made as an automator action to do that.
I changed that opinion based on an irritating experience I had when I downloaded a plugin and already had a zip file of the same name in that directory. macOS helpfully called the file smd_token 2.zip. When I dragged that to my Plugins installation panel, all hell broke loose and it wouldn’t install, even though the contents of the zip file contained the correctly-named top-level directory and all files beneath.
Yes, I can testify to that on the mac.
TXP Builders – finely-crafted code, design and txp
Offline
Re: ZIP plugin file requires archive subfolder, throws error otherwise.
Thanks for all the info. I will pack my plugins according to the expected structure.
I think my initial confusion stemmed from the surprise effect of this prerequisite which I hadn’t yet learned. Would RTFM have helped?
Offline
Re: ZIP plugin file requires archive subfolder, throws error otherwise.
wet wrote #340242:
Thanks for all the info. I will pack my plugins according to the expected structure.
Cool. I agree it would be nice to be a little less opinionated about how zip files are constructed. So if you do have any brainwaves on how to go about it, please throw them this way.
Would RTFM have helped?
Sadly not yet. Zip file support and plugin compilation have yet to be added to that doc (since 4.9.0 still isn’t released yet).
Elsewhere, the Plugins panel has not yet been documented to mention them, nor with the ability to upload directly from URL (e.g from a repo). Nor the plugin auto-updater and compiler.
When I have some time away from my current crop of projects, I’ll get back to playing in the test docs site with a view to updating all this stuff.
The smd plugin menagerie — for when you need one more gribble of power from Textpattern. Bleeding-edge code available on GitHub.
Txp Builders – finely-crafted code, design and Txp
Offline
Re: ZIP plugin file requires archive subfolder, throws error otherwise.
So if you do have any brainwaves on how to go about it, please throw them this way.
I’ve published a small tool to build Textpattern plugins on Windows. Look here if interested.1 Even with usage docs.
1 txp:veterans might notice that I named this tool after the author of the original plugin template, whose enmattification happened in 2007.
Offline
Re: ZIP plugin file requires archive subfolder, throws error otherwise.
Bloke wrote #340237:
I changed that opinion based on an irritating experience I had when I downloaded a plugin and already had a zip file of the same name in that directory. macOS helpfully called the file smd_token 2.zip. When I dragged that to my Plugins installation panel, all hell broke loose
Methinks, one way to handle these naming errors caused by external factors, such as file names and whatnot, is:
- Eliminate these external factors.
- Start at a well-known location. This well-known location is manifest.json. ‘Well-known’ being an euphemism for ‘you decide where it has to be and the others follow your rule’.
- manifest.json optionally carries the plugin’s name in an element appropriately called ‘name’, just like the corresponding
txp_plugin
table column. If present, the content of this element overrides a preliminary plugin name derived from the uploaded file’s name. - The entire contents of the ZIP file go into a folder named after this JSON element and, eventually, into any subfolders present in the ZIP file (bonus points here!).
- Any options resulting in ambiguity must be resolvable by either convention, or configuration using another element in manifest.json.
Offline
Re: ZIP plugin file requires archive subfolder, throws error otherwise.
wet wrote #340253:
Start at a well-known location. This well-known location is manifest.json.
Good idea. If it’s in the zip bundle, that’s potentially a neat way out. It’s not a stipulation (yet) to include that file.
The sticking point is if we can access the contents of the file during the unzipping process. If the unzipper iterates through the files and finds help.textile first, or a subdirectory that it traverses, where does it unzip it to, waiting for the moment it gets to manifest.json and can then know for sure?
The only way I can see of reliably doing this is to unzip everything into a temporary folder, scan the contents, read the manifest.json if it’s there, and act accordingly to relocate the bundle (after sanitizing it to remove the possibiliy of directory traversal). Bit faffier but certainly not impossible. We already silo the upload to perform the verification step, so this might not be that much of a leap.
Last edited by Bloke (2025-08-21 08:26:06)
The smd plugin menagerie — for when you need one more gribble of power from Textpattern. Bleeding-edge code available on GitHub.
Txp Builders – finely-crafted code, design and Txp
Offline
Offline
Re: ZIP plugin file requires archive subfolder, throws error otherwise.
If memory serves, the initial purpose of fs plugin storage was to avoid loading them from db via eval()
, blocked by many hosts. The zip distribution came after, inheriting the limitations of the initial format, so it certainly can be improved.
Offline