PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 260917
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v260917
260917 260913 260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 111220 120213 All 189 releases
← All changes | src/includes/classes/utils-dirs.inc.php +141 -2 260805260917 View file →
@@ -41,8 +41,141 @@
41 41 {
42 42 return rtrim (preg_replace ("/\/+/", "/", str_replace (array(DIRECTORY_SEPARATOR, "\\", "/"), "/", (string)$path)), "/");
43 43 }
44 44 /**
45 + * Resolves and checks a custom template selected by a shortcode.
46 + *
47 + * @package s2Member\Utilities
48 + * @since 260812
49 + *
50 + * @param string $template Template path supplied by a shortcode.
51 + * @param array $legacy_filenames Exact legacy filenames allowed automatically in the content root or active theme roots.
52 + * @param string $shortcode Shortcode name for administrator notices.
53 + * @param int $post_id Post/Page ID where the template was detected, when available.
54 + * @param bool $enforce When true, return only approved templates. Defaults to enforcement.
55 + * @return string Canonical template path, or an empty string if invalid or blocked.
56 + */
57 + public static function shortcode_template ($template = FALSE, $legacy_filenames = array(), $shortcode = '', $post_id = 0, $enforce = TRUE)
58 + {
59 + $template = c_ws_plugin__s2member_utils_dirs::n_dir_seps (trim (str_replace (chr (0), '', (string)$template)));
60 + if (!$template)
61 + return '';
62 +
63 + // Reject parent-directory segments before resolving the path.
64 + if (preg_match ('/(?:^|\/)\.\.(?:\/|$)/', $template))
65 + return '';
66 +
67 + // Preserve the historical lookup precedence and resolve the file to its actual filesystem path.
68 + $candidate = '';
69 + foreach (array(TEMPLATEPATH . '/' . $template, get_stylesheet_directory () . '/' . $template, WP_CONTENT_DIR . '/' . $template) as $_path)
70 + if (is_file ($_path) && ($_realpath = realpath ($_path)))
71 + $candidate = c_ws_plugin__s2member_utils_dirs::n_dir_seps ($_realpath);
72 + unset($_path, $_realpath);
73 +
74 + if (!$candidate || !($_content_dir = realpath (WP_CONTENT_DIR)))
75 + return '';
76 + $content_dir = c_ws_plugin__s2member_utils_dirs::n_dir_seps ($_content_dir);
77 + $is_windows = stripos (PHP_OS, 'WIN') === 0;
78 + $candidate_cmp = ($is_windows) ? strtolower ($candidate) : $candidate;
79 + $content_prefix = $content_dir . '/';
80 + $content_prefix_cmp = ($is_windows) ? strtolower ($content_prefix) : $content_prefix;
81 +
82 + // Require the resolved file to remain under WP_CONTENT_DIR, including after symlink resolution.
83 + if (strpos ($candidate_cmp, $content_prefix_cmp) !== 0)
84 + return '';
85 +
86 + // Keep shortcode templates out of WordPress uploads, where uploaded files can be user-controlled.
87 + $_uploads_dirs = array();
88 + $_uploads = wp_upload_dir();
89 + if (!empty($_uploads['basedir']) && ($_uploads_dir = realpath ($_uploads['basedir'])))
90 + {
91 + $_uploads_dir = c_ws_plugin__s2member_utils_dirs::n_dir_seps ($_uploads_dir);
92 +
93 + //260812 On Multisite, check the shared uploads root instead of only the current site's subdirectory.
94 + if (is_multisite ())
95 + {
96 + foreach (array('/sites/' . get_current_blog_id (), '/' . get_current_blog_id () . '/files', '/' . get_current_blog_id ()) as $_site_suffix)
97 + if (substr ($_uploads_dir, -strlen ($_site_suffix)) === $_site_suffix)
98 + {
99 + $_uploads_dir = substr ($_uploads_dir, 0, -strlen ($_site_suffix));
100 + break;
101 + }
102 + }
103 + $_uploads_dirs[] = $_uploads_dir;
104 + }
105 +
106 + //260812 Also check the standard and legacy Multisite uploads roots.
107 + foreach (array(WP_CONTENT_DIR . '/uploads', (is_multisite () && defined ('UPLOADBLOGSDIR')) ? ABSPATH . ltrim (UPLOADBLOGSDIR, '/\\') : '') as $_uploads_dir)
108 + if ($_uploads_dir && ($_realpath = realpath ($_uploads_dir)))
109 + $_uploads_dirs[] = c_ws_plugin__s2member_utils_dirs::n_dir_seps ($_realpath);
110 +
111 + foreach (array_unique ($_uploads_dirs) as $_uploads_dir)
112 + {
113 + $_uploads_dir_cmp = ($is_windows) ? strtolower ($_uploads_dir) : $_uploads_dir;
114 + if ($candidate_cmp === $_uploads_dir_cmp || strpos ($candidate_cmp, $_uploads_dir_cmp . '/') === 0)
115 + return '';
116 + }
117 + unset($_uploads_dirs, $_uploads, $_uploads_dir, $_uploads_dir_cmp, $_site_suffix, $_realpath);
118 +
119 + $allowed_paths = array();
120 + foreach ((array)$legacy_filenames as $_legacy_filename)
121 + {
122 + $_legacy_filename = ltrim (c_ws_plugin__s2member_utils_dirs::n_dir_seps (trim ((string)$_legacy_filename)), '/');
123 + if (!$_legacy_filename)
124 + continue;
125 +
126 + // Allow only the exact historical filename in the content root or active theme roots.
127 + foreach (array(TEMPLATEPATH . '/' . $_legacy_filename, get_stylesheet_directory () . '/' . $_legacy_filename, WP_CONTENT_DIR . '/' . $_legacy_filename) as $_path)
128 + if (is_file ($_path) && ($_realpath = realpath ($_path)))
129 + $allowed_paths[] = c_ws_plugin__s2member_utils_dirs::n_dir_seps ($_realpath);
130 + }
131 + unset($_legacy_filename, $_path, $_realpath);
132 +
133 + // Add exact custom template files from the whitelist and track whether the current file is listed.
134 + $whitelisted = FALSE;
135 + $_whitelist = apply_filters ('ws_plugin__s2member_shortcode_templates_whitelist', array());
136 + if (is_string ($_whitelist))
137 + $_whitelist = preg_split ("/[\\r\\n]+/", $_whitelist, -1, PREG_SPLIT_NO_EMPTY);
138 + foreach ((array)$_whitelist as $_allowed_file)
139 + {
140 + $_allowed_file = ltrim (c_ws_plugin__s2member_utils_dirs::n_dir_seps (trim ((string)$_allowed_file)), '/');
141 + if ($_allowed_file && is_file (WP_CONTENT_DIR . '/' . $_allowed_file) && ($_realpath = realpath (WP_CONTENT_DIR . '/' . $_allowed_file)))
142 + {
143 + $_realpath = c_ws_plugin__s2member_utils_dirs::n_dir_seps ($_realpath);
144 + $allowed_paths[] = $_realpath;
145 + if ((($is_windows) ? strtolower ($_realpath) : $_realpath) === $candidate_cmp)
146 + $whitelisted = TRUE;
147 + }
148 + }
149 + unset($_whitelist, $_allowed_file, $_realpath);
150 +
151 + $allowed = FALSE;
152 + foreach (array_unique ($allowed_paths) as $_allowed_path)
153 + if ((($is_windows) ? strtolower ($_allowed_path) : $_allowed_path) === $candidate_cmp)
154 + {
155 + $allowed = TRUE;
156 + break;
157 + }
158 + unset($allowed_paths, $_allowed_path);
159 +
160 + $_relative_path = ltrim (substr ($candidate, strlen ($content_dir)), '/');
161 +
162 + if ($allowed)
163 + {
164 + // Clear a saved warning only when the site owner explicitly added this file to the whitelist.
165 + if ($whitelisted)
166 + do_action ('ws_plugin__s2member_shortcode_template_approved', $_relative_path, $shortcode, (int)$post_id);
167 + return $candidate;
168 + }
169 +
170 + // Notify Pro about an unapproved template so it can be included in the admin warning.
171 + if ($_relative_path && $shortcode)
172 + do_action ('ws_plugin__s2member_shortcode_template_unapproved', $_relative_path, $shortcode, (int)$post_id);
173 +
174 + //260915.0134 Block unapproved custom templates; callers retain their existing standard-template fallback.
175 + return ($enforce) ? '' : $candidate;
176 + }
177 + /**
45 178 * Strips a trailing `/app_data/` sub-directory.
46 179 *
47 180 * @package s2Member\Utilities
48 181 * @since 3.5
@@ -128,10 +261,16 @@
128 261 array_unshift ($to, $_jctn_dir);
129 262 }
130 263 else // Else, we should trigger an error in this case. It's NOT possible to generate this.
131 264 {
132 - trigger_error ("Unable to generate a relative path across different Windows drives." .
133 - " Please create a Directory Junction here: " . $_from_drive_jctn . ", pointing to: " . $_to_drive . ":/", E_USER_ERROR);
265 + $error = "Unable to generate a relative path across different Windows drives." .
266 + " Please create a Directory Junction here: " . $_from_drive_jctn . ", pointing to: " . $_to_drive . ":/";
267 +
268 + //260816 PHP 8.4 deprecates trigger_error(..., E_USER_ERROR); preserve the previous fatal-style path on older PHP.
269 + if(PHP_VERSION_ID >= 80400)
270 + throw new \RuntimeException($error);
271 + else
272 + trigger_error($error, E_USER_ERROR);
134 273 }
135 274 }
136 275
137 276 unset($_real_from, $_real_to, $_from_drive, $_to_drive, $_from_drive_jctn, $_sys_temp_dir_jctn, $_jctn, $_from_drive_jctn_exists, $_jctn_dir, $_m);