| @@ -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); |