PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / modules / pro-plugin-provisioning / Archive.php

Archive.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.8.0, at modules/pro-plugin-provisioning/Archive.php

118 lines 4.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Is this actually an installable WordPress plugin archive?
4 *
5 * The download service's response shape is UNKNOWN and cannot be probed — the qualifying
6 * subscription plans do not exist on the cloud yet, and this repository forbids firing
7 * speculative requests at production to find out. So nothing here parses. It VALIDATES:
8 * one shape is accepted and everything else is refused, which is correct whatever the
9 * service turns out to answer with, and which is what lets the live source replace the
10 * mock without a single test changing (spec 059 FR-023).
11 *
12 * The bytes are untrusted third-party input. They are inspected in the temp directory and
13 * never extracted here — `Plugin_Upgrader` does the installation, because it is
14 * WordPress's own vetted path and it is the one that knows about filesystem credentials,
15 * ownership, and rollback.
16 *
17 * @package Templately\Modules\ProPluginProvisioning
18 */
19
20 namespace Templately\Modules\ProPluginProvisioning;
21
22 use ZipArchive;
23
24 class Archive {
25
26 /**
27 * Whether a file on disk is a ZIP containing at least one WordPress plugin.
28 *
29 * A JSON error body, an HTML error page, an empty response, a truncated download, a
30 * ZIP of something that is not a plugin — all of them fail here, and all of them mean
31 * the same thing to the caller: not available.
32 *
33 * When `$expected_dir` is given, the headed file must ALSO sit in exactly that folder.
34 * The service can hand back a different product than the one asked for — a
35 * `platform=elementor` request has been observed answering with Essential Blocks Pro
36 * (2026-09-08) — and "a valid plugin" is not the same question as "the plugin the
37 * catalog pinned". Without this the wrong licensed plugin installs silently.
38 *
39 * @param string $path Absolute path to a downloaded file.
40 * @param string|null $expected_dir Plugin folder the archive must contain, or null to accept any.
41 * @return bool
42 */
43 public static function is_installable_plugin( $path, $expected_dir = null ): bool {
44 if ( ! is_string( $path ) || '' === $path || ! is_readable( $path ) ) {
45 return false;
46 }
47
48 if ( filesize( $path ) < 100 ) {
49 // Smaller than any real plugin zip; almost certainly an error body.
50 return false;
51 }
52
53 if ( ! class_exists( 'ZipArchive' ) ) {
54 // Without the extension we cannot inspect the archive, and installing an
55 // unverified one is not an acceptable substitute for checking.
56 return false;
57 }
58
59 $zip = new ZipArchive();
60
61 if ( true !== $zip->open( $path ) ) {
62 return false;
63 }
64
65 $has_plugin_header = false;
66
67 for ( $i = 0; $i < $zip->numFiles; $i++ ) {
68 $name = $zip->getNameIndex( $i );
69
70 if ( ! is_string( $name ) || substr( $name, -4 ) !== '.php' ) {
71 continue;
72 }
73
74 // Only `<folder>/<file>.php`. WordPress's own `Plugin_Upgrader::check_package()`
75 // looks for the header at the top level of the extracted folder and rejects an
76 // archive whose only headed file is nested deeper — so accepting one here would
77 // pass the validator and still hard-fail the install.
78 $parts = explode( '/', trim( $name, '/' ) );
79
80 if ( 2 !== count( $parts ) ) {
81 continue;
82 }
83
84 // Right shape, wrong product — see the docblock.
85 if ( is_string( $expected_dir ) && '' !== $expected_dir && $parts[0] !== $expected_dir ) {
86 continue;
87 }
88
89 // Plugin headers must be within the first 8KB for WordPress to read them, so
90 // there is no reason to pull a whole file into memory to look for one.
91 $contents = $zip->getFromIndex( $i, 8192 );
92
93 if ( is_string( $contents ) && preg_match( '/^[ \t\/*#@]*Plugin Name:\s*\S/mi', $contents ) ) {
94 $has_plugin_header = true;
95 break;
96 }
97 }
98
99 $zip->close();
100
101 return $has_plugin_header;
102 }
103
104 /**
105 * Delete a transient archive, ignoring a file that is already gone.
106 *
107 * Nothing licensed may survive the request that downloaded it (FR-024), so this is
108 * called on BOTH the success and the failure branch of installation.
109 *
110 * @param string|null $path
111 */
112 public static function discard( $path ): void {
113 if ( is_string( $path ) && '' !== $path && file_exists( $path ) ) {
114 @unlink( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a vanished temp file is not an error.
115 }
116 }
117 }
118