| @@ -1,12 +1,12 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | /** |
| 3 | 3 | * @package FireBox |
| 4 | - * @version 3.1.13 Free | |
| 4 | + * @version 2.1.23 Free | |
| 5 | 5 | * |
| 6 | 6 | * @author FirePlugins <info@fireplugins.com> |
| 7 | 7 | * @link https://www.fireplugins.com |
| 8 | - * @copyright Copyright © 2026 FirePlugins All Rights Reserved | |
| 8 | + * @copyright Copyright © 2024 FirePlugins All Rights Reserved | |
| 9 | 9 | * @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later |
| 10 | 10 | */ |
| 11 | 11 | |
| 12 | 12 | namespace FireBox\Core\Helpers; |
| @@ -20,405 +20,56 @@ | ||
| 20 | 20 | |
| 21 | 21 | class BoxHelper |
| 22 | 22 | { |
| 23 | 23 | /** |
| 24 | - * Option holding the cached campaign lists, keyed by status/limit. | |
| 25 | - * | |
| 26 | - * An autoloaded option rather than a transient on purpose: a transient with an expiry is | |
| 27 | - * stored non-autoloaded, so reading it costs 2 queries per request on stock WordPress — | |
| 28 | - * exactly what the campaign query it replaces costs. Autoloaded means WordPress has | |
| 29 | - * already loaded it by the time Boxes::render() runs. There is no TTL either: the option | |
| 30 | - * is deleted on every campaign write, so the cache is always current. | |
| 31 | - * | |
| 32 | - * @var string | |
| 33 | - */ | |
| 34 | - const BOXES_CACHE_OPTION = 'firebox_boxes_cache'; | |
| 35 | - | |
| 36 | - /** | |
| 37 | - * Returns the prefix every FireBox cookie name is built from. | |
| 38 | - * | |
| 39 | - * Cookies are set with path=/, so on a subdirectory network every site of | |
| 40 | - * example.com/store, example.com/blog … shares one cookie namespace. Post | |
| 41 | - * IDs restart per site, so campaign 42 exists on both and one campaign's | |
| 42 | - * "already seen" cookie would suppress an unrelated campaign on another | |
| 43 | - * site. The blog ID in the name is what keeps them apart. | |
| 44 | - * | |
| 45 | - * Single sites — and the main site of a network, which owns the unprefixed | |
| 46 | - * namespace already — keep the original prefix, so upgrading does not show | |
| 47 | - * a campaign again to visitors who had dismissed it. | |
| 48 | - * | |
| 49 | - * @return string | |
| 50 | - */ | |
| 51 | - public static function cookiePrefix() | |
| 52 | - { | |
| 53 | - if (!is_multisite() || is_main_site()) | |
| 54 | - { | |
| 55 | - return 'firebox_'; | |
| 56 | - } | |
| 57 | - | |
| 58 | - return 'firebox_' . get_current_blog_id() . '_'; | |
| 59 | - } | |
| 60 | - | |
| 61 | - /** | |
| 62 | - * The path FireBox cookies are written to. | |
| 63 | - * | |
| 64 | - * WordPress derives COOKIEPATH from the site's home URL, so it already | |
| 65 | - * points at exactly the part of the domain this site occupies — the whole | |
| 66 | - * domain for a normal install, /store/ for that site of a subdirectory | |
| 67 | - * network. It is what WooCommerce and AffiliateWP scope their cookies with, | |
| 68 | - * and it keeps a network's sites from writing over each other. | |
| 69 | - * | |
| 70 | - * @return string | |
| 71 | - */ | |
| 72 | - public static function cookiePath() | |
| 73 | - { | |
| 74 | - return \FPFramework\Helpers\CookieScope::path(); | |
| 75 | - } | |
| 76 | - | |
| 77 | - /** | |
| 78 | - * The domain FireBox cookies are written to. | |
| 79 | - * | |
| 80 | - * Empty unless the site defines COOKIE_DOMAIN, which is how a site tells | |
| 81 | - * WordPress its cookies belong to something other than the current host — | |
| 82 | - * shared between www and non-www, or across the subdomains of a network. | |
| 83 | - * Ignoring it means writing a cookie the site cannot read back. | |
| 84 | - * | |
| 85 | - * @return string | |
| 86 | - */ | |
| 87 | - public static function cookieDomain() | |
| 88 | - { | |
| 89 | - return \FPFramework\Helpers\CookieScope::domain(); | |
| 90 | - } | |
| 91 | - | |
| 92 | - /** | |
| 93 | - * Paths a cookie of ours may have been written to before. | |
| 94 | - * | |
| 95 | - * Cookies are keyed by name *and* path, so a cookie written at '/' is a | |
| 96 | - * different cookie from the same name at '/store/'. FireBox wrote every | |
| 97 | - * cookie to '/' until it started honouring COOKIEPATH, and expiring only | |
| 98 | - * the new path would leave the old one behind — still sent, still | |
| 99 | - * suppressing the campaign it belongs to. | |
| 100 | - * | |
| 101 | - * @return array | |
| 102 | - */ | |
| 103 | - public static function cookiePathsToClear() | |
| 104 | - { | |
| 105 | - return \FPFramework\Helpers\CookieScope::pathsToClear(); | |
| 106 | - } | |
| 107 | - | |
| 108 | - /** | |
| 109 | 24 | * Get box meta |
| 110 | - * | |
| 25 | + * | |
| 111 | 26 | * @param int $id |
| 112 | - * | |
| 27 | + * | |
| 113 | 28 | * @return array |
| 114 | 29 | */ |
| 115 | 30 | public static function getMeta($id) |
| 116 | 31 | { |
| 117 | - $meta = get_post_meta($id, 'firebox_meta', false); | |
| 118 | - $meta = isset($meta[0]) && is_array($meta[0]) ? $meta[0] : []; | |
| 119 | - | |
| 120 | - return $meta; | |
| 32 | + return get_post_meta($id, 'fpframework_meta_settings', true); | |
| 121 | 33 | } |
| 122 | 34 | |
| 123 | 35 | /** |
| 124 | - * Attaches inline JavaScript that runs once the FireBox runtime has created all | |
| 125 | - * campaign instances. | |
| 126 | - * | |
| 127 | - * The script is attached in the "before" position on purpose: WordPress downgrades a | |
| 128 | - * defer-strategy script to a blocking one as soon as it carries an "after" inline | |
| 129 | - * script, so a single campaign with custom code would make the whole runtime | |
| 130 | - * parse-blocking. Printing before the runtime keeps defer intact. | |
| 131 | - * | |
| 132 | - * Waiting for DOMContentLoaded is what makes that safe: firebox-main is deferred, so | |
| 133 | - * the browser executes it once parsing is done — at which point FireBox.onReady() | |
| 134 | - * runs immediately (readyState is already "interactive") and builds every campaign | |
| 135 | - * instance. DOMContentLoaded fires after all deferred scripts, so by the time this | |
| 136 | - * code runs the instances exist. | |
| 137 | - * | |
| 138 | - * @param string $js | |
| 139 | - * | |
| 140 | - * @return void | |
| 141 | - */ | |
| 142 | - public static function addInlineScript($js) | |
| 143 | - { | |
| 144 | - if (!is_string($js) || trim($js) === '') | |
| 145 | - { | |
| 146 | - return; | |
| 147 | - } | |
| 148 | - | |
| 149 | - $wrapped = 'document.addEventListener("DOMContentLoaded", function(){' . $js . '});'; | |
| 150 | - | |
| 151 | - wp_add_inline_script('firebox-main', $wrapped, 'before'); | |
| 152 | - } | |
| 153 | - | |
| 154 | - /** | |
| 155 | - * Whether any of the given campaigns uses session-dependent behavior: | |
| 156 | - * the Pageviews/Time on Site display conditions, or an impressions | |
| 157 | - * limit scoped to the visitor's session (fpfsid). | |
| 158 | - * | |
| 159 | - * These are stamped and evaluated against JS-set session cookies, so | |
| 160 | - * when in use the frontend runtime must be enqueued on every page. | |
| 161 | - * Skipped entirely when no campaign needs them, so other sites load no | |
| 162 | - * extra JS or cookies. | |
| 163 | - * | |
| 164 | - * @param \WP_Query $boxes | |
| 165 | - * | |
| 166 | - * @return bool | |
| 167 | - */ | |
| 168 | - public static function sessionConditionsInUse($boxes) | |
| 169 | - { | |
| 170 | - // The answer only changes when a campaign is written to, so keep it in an | |
| 171 | - // autoloaded option instead of re-scanning every campaign's meta per page view. | |
| 172 | - $cached = get_option('firebox_session_conditions_in_use'); | |
| 173 | - | |
| 174 | - if ($cached === 'yes' || $cached === 'no') | |
| 175 | - { | |
| 176 | - return $cached === 'yes'; | |
| 177 | - } | |
| 178 | - | |
| 179 | - $inUse = self::computeSessionConditionsInUse($boxes); | |
| 180 | - | |
| 181 | - update_option('firebox_session_conditions_in_use', $inUse ? 'yes' : 'no', true); | |
| 182 | - | |
| 183 | - return $inUse; | |
| 184 | - } | |
| 185 | - | |
| 186 | - /** | |
| 187 | - * Drops the cached session-conditions flag so it is recomputed on the next | |
| 188 | - * frontend request. Hooked on every write path that can change the answer. | |
| 189 | - * | |
| 190 | - * @return void | |
| 191 | - */ | |
| 192 | - public static function invalidateSessionConditionsFlag() | |
| 193 | - { | |
| 194 | - delete_option('firebox_session_conditions_in_use'); | |
| 195 | - } | |
| 196 | - | |
| 197 | - /** | |
| 198 | - * Drops every campaign-derived cache: the session-conditions flag and the | |
| 199 | - * cached box list. Hooked on each write path that can change either answer. | |
| 200 | - * | |
| 201 | - * @return void | |
| 202 | - */ | |
| 203 | - public static function invalidateCaches() | |
| 204 | - { | |
| 205 | - self::invalidateSessionConditionsFlag(); | |
| 206 | - self::invalidateBoxesCache(); | |
| 207 | - } | |
| 208 | - | |
| 209 | - /** | |
| 210 | - * Registers the invalidation hooks for the campaign-derived caches. | |
| 211 | - * | |
| 212 | - * @return void | |
| 213 | - */ | |
| 214 | - public static function registerCacheInvalidation() | |
| 215 | - { | |
| 216 | - add_action('save_post_firebox', [self::class, 'invalidateCaches']); | |
| 217 | - | |
| 218 | - // These fire for every post type, so only react to campaigns — otherwise | |
| 219 | - // ordinary content edits would keep busting the campaign caches. | |
| 220 | - $onPostChange = function($post_id, $post = null) | |
| 221 | - { | |
| 222 | - if (self::isCampaign($post_id, $post)) | |
| 223 | - { | |
| 224 | - self::invalidateCaches(); | |
| 225 | - } | |
| 226 | - }; | |
| 227 | - | |
| 228 | - add_action('deleted_post', $onPostChange, 10, 2); | |
| 229 | - add_action('trashed_post', $onPostChange, 10, 1); | |
| 230 | - add_action('untrashed_post', $onPostChange, 10, 1); | |
| 231 | - | |
| 232 | - // firebox_meta is also written outside the save_post flow (Gutenberg REST meta | |
| 233 | - // updates, the campaign importer, duplication). | |
| 234 | - $onMetaWrite = function($meta_id, $post_id, $meta_key) | |
| 235 | - { | |
| 236 | - if ($meta_key === 'firebox_meta') | |
| 237 | - { | |
| 238 | - self::invalidateCaches(); | |
| 239 | - } | |
| 240 | - }; | |
| 241 | - | |
| 242 | - add_action('added_postmeta', $onMetaWrite, 10, 3); | |
| 243 | - add_action('updated_postmeta', $onMetaWrite, 10, 3); | |
| 244 | - } | |
| 245 | - | |
| 246 | - /** | |
| 247 | - * Whether the given post is a campaign. `deleted_post` passes the post object | |
| 248 | - * along because the row is already gone by the time it fires. | |
| 249 | - * | |
| 250 | - * @param int $post_id | |
| 251 | - * @param \WP_Post $post | |
| 252 | - * | |
| 253 | - * @return bool | |
| 254 | - */ | |
| 255 | - private static function isCampaign($post_id, $post = null) | |
| 256 | - { | |
| 257 | - if ($post instanceof \WP_Post) | |
| 258 | - { | |
| 259 | - return $post->post_type === 'firebox'; | |
| 260 | - } | |
| 261 | - | |
| 262 | - return get_post_type($post_id) === 'firebox'; | |
| 263 | - } | |
| 264 | - | |
| 265 | - /** | |
| 266 | - * Scans the given campaigns for session-dependent behavior. | |
| 267 | - * | |
| 268 | - * @param \WP_Query $boxes | |
| 269 | - * | |
| 270 | - * @return bool | |
| 271 | - */ | |
| 272 | - private static function computeSessionConditionsInUse($boxes) | |
| 273 | - { | |
| 274 | - if (!$boxes || empty($boxes->posts)) | |
| 275 | - { | |
| 276 | - return false; | |
| 277 | - } | |
| 278 | - | |
| 279 | - foreach ($boxes->posts as $box) | |
| 280 | - { | |
| 281 | - $meta = self::getMeta($box->ID); | |
| 282 | - | |
| 283 | - if (!$meta) | |
| 284 | - { | |
| 285 | - continue; | |
| 286 | - } | |
| 287 | - | |
| 288 | - // Impressions limit scoped to the browser session | |
| 289 | - $impressionsPeriod = isset($meta['assign_impressions_param_type']) ? $meta['assign_impressions_param_type'] : ''; | |
| 290 | - | |
| 291 | - if ($impressionsPeriod === 'custom') | |
| 292 | - { | |
| 293 | - $impressionsPeriod = isset($meta['assign_impressions_param_custom_period']) ? $meta['assign_impressions_param_custom_period'] : ''; | |
| 294 | - } | |
| 295 | - | |
| 296 | - if ($impressionsPeriod === 'session') | |
| 297 | - { | |
| 298 | - return true; | |
| 299 | - } | |
| 300 | - | |
| 301 | - // Session-dependent display conditions | |
| 302 | - $metaJson = wp_json_encode($meta); | |
| 303 | - | |
| 304 | - if (strpos($metaJson, 'Pageviews') !== false || strpos($metaJson, 'TimeOnSite') !== false) | |
| 305 | - { | |
| 306 | - return true; | |
| 307 | - } | |
| 308 | - } | |
| 309 | - | |
| 310 | - return false; | |
| 311 | - } | |
| 312 | - | |
| 313 | - /** | |
| 314 | 36 | * Gets all Boxes. |
| 315 | - * | |
| 316 | - * Only the campaign IDs are cached, in an autoloaded option so the list also | |
| 317 | - * survives across requests on hosts without a persistent object cache — where | |
| 318 | - * wp_cache is per-request and the query therefore ran on every page view. The | |
| 319 | - * autoloaded options are already in memory by the time this runs, so a cache hit | |
| 320 | - * costs no query of its own. | |
| 321 | - * | |
| 322 | - * There is no TTL: the cache is dropped on every campaign write, which also means | |
| 323 | - * editors see their changes immediately. | |
| 324 | - * | |
| 37 | + * | |
| 325 | 38 | * @param array $status |
| 326 | 39 | * @param int $limit |
| 327 | - * | |
| 328 | - * @return \WP_Query | |
| 40 | + * | |
| 41 | + * @return array | |
| 329 | 42 | */ |
| 330 | 43 | public static function getAllBoxes($status = ['publish'], $limit = -1) |
| 331 | 44 | { |
| 332 | - $key = self::getBoxesCacheKey($status, $limit); | |
| 333 | - $cache = get_option(self::BOXES_CACHE_OPTION, []); | |
| 334 | - $cache = is_array($cache) ? $cache : []; | |
| 45 | + // cache key | |
| 46 | + $hash = md5('firebox_getAllBoxes_' . implode(',', $status)); | |
| 335 | 47 | |
| 336 | - if (isset($cache[$key]) && is_array($cache[$key])) | |
| 48 | + // check cache | |
| 49 | + if ($data = wp_cache_get($hash)) | |
| 337 | 50 | { |
| 338 | - return self::hydrateBoxesQuery($cache[$key]); | |
| 339 | - } | |
| 340 | - | |
| 51 | + return $data; | |
| 52 | + } | |
| 53 | + | |
| 341 | 54 | $args = [ |
| 342 | 55 | 'post_status' => $status, |
| 343 | 56 | 'post_type' => 'firebox', |
| 344 | - 'posts_per_page' => $limit, | |
| 345 | - 'no_found_rows' => true, | |
| 346 | - 'update_post_term_cache' => false, | |
| 347 | - 'cache_results' => true | |
| 57 | + 'posts_per_page' => $limit | |
| 348 | 58 | ]; |
| 349 | - | |
| 59 | + | |
| 350 | 60 | // Get the query. |
| 351 | 61 | $query = new \WP_Query($args); |
| 352 | 62 | |
| 353 | 63 | wp_reset_postdata(); |
| 354 | 64 | |
| 355 | - $cache[$key] = wp_list_pluck($query->posts, 'ID'); | |
| 65 | + // set cache | |
| 66 | + wp_cache_set($hash, $query); | |
| 356 | 67 | |
| 357 | - update_option(self::BOXES_CACHE_OPTION, $cache, true); | |
| 358 | - | |
| 359 | 68 | return $query; |
| 360 | 69 | } |
| 361 | 70 | |
| 362 | 71 | /** |
| 363 | - * Rebuilds a WP_Query from a cached list of campaign IDs. | |
| 364 | - * | |
| 365 | - * @param array $ids | |
| 366 | - * | |
| 367 | - * @return \WP_Query | |
| 368 | - */ | |
| 369 | - private static function hydrateBoxesQuery($ids) | |
| 370 | - { | |
| 371 | - $query = new \WP_Query(); | |
| 372 | - | |
| 373 | - // A hand-built query has no query vars of its own, and the loop reads several | |
| 374 | - // while setting up each post. "fields => all" states that $posts already holds | |
| 375 | - // full post objects, which is what it takes for the loop to use them as-is. | |
| 376 | - $query->query_vars = $query->fill_query_vars(['fields' => 'all']); | |
| 377 | - | |
| 378 | - $posts = []; | |
| 379 | - | |
| 380 | - if ($ids) | |
| 381 | - { | |
| 382 | - // Two queries, both by primary key, priming the same post and postmeta | |
| 383 | - // caches the original WP_Query did. | |
| 384 | - _prime_post_caches($ids, false, true); | |
| 385 | - | |
| 386 | - // array_filter drops campaigns deleted since the cache was written. | |
| 387 | - $posts = array_values(array_filter(array_map('get_post', $ids))); | |
| 388 | - } | |
| 389 | - | |
| 390 | - $query->posts = $posts; | |
| 391 | - $query->post_count = count($posts); | |
| 392 | - $query->found_posts = $query->post_count; | |
| 393 | - | |
| 394 | - return $query; | |
| 395 | - } | |
| 396 | - | |
| 397 | - /** | |
| 398 | - * The cache entry a box list is stored under. | |
| 399 | - * | |
| 400 | - * @param array $status | |
| 401 | - * @param int $limit | |
| 402 | - * | |
| 403 | - * @return string | |
| 404 | - */ | |
| 405 | - private static function getBoxesCacheKey($status, $limit) | |
| 406 | - { | |
| 407 | - return md5(implode(',', (array) $status) . '_' . (int) $limit); | |
| 408 | - } | |
| 409 | - | |
| 410 | - /** | |
| 411 | - * Drops every cached box list. | |
| 412 | - * | |
| 413 | - * @return void | |
| 414 | - */ | |
| 415 | - public static function invalidateBoxesCache() | |
| 416 | - { | |
| 417 | - delete_option(self::BOXES_CACHE_OPTION); | |
| 418 | - } | |
| 419 | - | |
| 420 | - /** | |
| 421 | 72 | * Retrieves all boxes in a key => value array of ID => title |
| 422 | 73 | * |
| 423 | 74 | * @return array |
| 424 | 75 | */ |
| @@ -470,9 +121,9 @@ | ||
| 470 | 121 | } |
| 471 | 122 | |
| 472 | 123 | $boxes = firebox()->tables->box->getResults([ |
| 473 | 124 | 'where' => [ |
| 474 | - 'ID' => ' NOT IN (' . absint($id) . ')', | |
| 125 | + 'ID' => ' NOT IN (' . esc_sql($id) . ')', | |
| 475 | 126 | 'post_status' => " = 'publish'", |
| 476 | 127 | 'post_type' => " = 'firebox'" |
| 477 | 128 | ] |
| 478 | 129 | ]); |
| @@ -504,9 +155,9 @@ | ||
| 504 | 155 | $box = (int) $box; |
| 505 | 156 | |
| 506 | 157 | $box = firebox()->tables->box->getResults([ |
| 507 | 158 | 'where' => [ |
| 508 | - 'ID' => " = '" . absint($box) . "'" | |
| 159 | + 'ID' => " = '" . esc_sql($box) . "'" | |
| 509 | 160 | ] |
| 510 | 161 | ]); |
| 511 | 162 | |
| 512 | 163 | return isset($box[0]) ? $box[0] : []; |
| @@ -529,9 +180,9 @@ | ||
| 529 | 180 | $box = (int) $box; |
| 530 | 181 | |
| 531 | 182 | $box = firebox()->tables->box->getResults([ |
| 532 | 183 | 'where' => [ |
| 533 | - 'ID' => " = '" . absint($box) . "'" | |
| 184 | + 'ID' => " = '" . esc_sql($box) . "'" | |
| 534 | 185 | ] |
| 535 | 186 | ]); |
| 536 | 187 | |
| 537 | 188 | if (!$box) |
| @@ -582,9 +233,9 @@ | ||
| 582 | 233 | // cache key |
| 583 | 234 | $cache_key = md5('fboxSettings'); |
| 584 | 235 | |
| 585 | 236 | // check cache |
| 586 | - if ($params = wp_cache_get($cache_key, 'firebox')) | |
| 237 | + if ($params = wp_cache_get($cache_key)) | |
| 587 | 238 | { |
| 588 | 239 | return $params; |
| 589 | 240 | } |
| 590 | 241 | |
| @@ -591,12 +242,65 @@ | ||
| 591 | 242 | // get params |
| 592 | 243 | $params = get_option('firebox_settings'); |
| 593 | 244 | |
| 594 | 245 | // set cache |
| 595 | - wp_cache_set($cache_key, $params, 'firebox', 5 * MINUTE_IN_SECONDS); | |
| 246 | + wp_cache_set($cache_key, $params); | |
| 596 | 247 | |
| 597 | 248 | return $params; |
| 598 | 249 | } |
| 250 | + | |
| 251 | + /** | |
| 252 | + * Returns the box template from the URL using the "template" query string. | |
| 253 | + * | |
| 254 | + * @return array | |
| 255 | + */ | |
| 256 | + public static function getBoxFromTemplateURL() | |
| 257 | + { | |
| 258 | + if (!isset($_GET['fpf_use_template'])) //phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 259 | + { | |
| 260 | + return; | |
| 261 | + } | |
| 262 | + | |
| 263 | + // $template must be an integer | |
| 264 | + $template = isset($_GET['template']) ? intval($_GET['template']) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 265 | + if (empty($template) || $template === 'blank') | |
| 266 | + { | |
| 267 | + return; | |
| 268 | + } | |
| 269 | + | |
| 270 | + $local_template_path = \FPFramework\Helpers\WPHelper::getPluginUploadsDirectory('firebox', 'templates') . '/template.json'; | |
| 271 | + if (!file_exists($local_template_path)) | |
| 272 | + { | |
| 273 | + return; | |
| 274 | + } | |
| 275 | + | |
| 276 | + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents | |
| 277 | + $local_template = file_get_contents($local_template_path); | |
| 278 | + if (!$local_template = json_decode($local_template, true)) | |
| 279 | + { | |
| 280 | + return; | |
| 281 | + } | |
| 282 | + | |
| 283 | + if (!isset($local_template['id'])) | |
| 284 | + { | |
| 285 | + return; | |
| 286 | + } | |
| 287 | + | |
| 288 | + if (!isset($local_template['template'])) | |
| 289 | + { | |
| 290 | + return; | |
| 291 | + } | |
| 292 | + | |
| 293 | + if ((int) $local_template['id'] !== (int) $template) | |
| 294 | + { | |
| 295 | + return; | |
| 296 | + } | |
| 297 | + | |
| 298 | + // Find images and save them locally. | |
| 299 | + $local_template['template']['post_content'] = \FPFramework\Helpers\WPHelper::downloadAndReplaceImages($local_template['template']['post_content']); | |
| 300 | + | |
| 301 | + return $local_template; | |
| 302 | + } | |
| 599 | 303 | |
| 600 | 304 | /** |
| 601 | 305 | * Duplicates a box |
| 602 | 306 | * |
| @@ -608,10 +312,10 @@ | ||
| 608 | 312 | { |
| 609 | 313 | // get box |
| 610 | 314 | $box = firebox()->tables->box->getResults([ |
| 611 | 315 | 'where' => [ |
| 612 | - 'ID' => " = '" . absint($box_id) . "'", | |
| 613 | - 'post_status' => " = '" . sanitize_key(get_post_status($box_id)) . "'", | |
| 316 | + 'ID' => " = '" . esc_sql($box_id) . "'", | |
| 317 | + 'post_status' => " = '" . esc_sql(get_post_status($box_id)) . "'", | |
| 614 | 318 | 'post_type' => " = 'firebox'" |
| 615 | 319 | ], |
| 616 | 320 | 'limit' => 1 |
| 617 | 321 | ]); |
| @@ -626,17 +330,8 @@ | ||
| 626 | 330 | $box->ID = ''; |
| 627 | 331 | $box->post_title = 'Copy of ' . $box->post_title; |
| 628 | 332 | $box->post_status = 'draft'; |
| 629 | 333 | |
| 630 | - /** | |
| 631 | - * The copy belongs to whoever duplicates, never the source author — the same | |
| 632 | - * rule the importer applies. The PHP and custom-JavaScript gates are a | |
| 633 | - * function of the post author, so keeping the source author would let a copy | |
| 634 | - * of a higher-privileged user's campaign keep running code its new owner is | |
| 635 | - * not allowed to write. | |
| 636 | - */ | |
| 637 | - $box->post_author = get_current_user_id(); | |
| 638 | - | |
| 639 | 334 | $factory = new \FPFramework\Base\Factory(); |
| 640 | 335 | |
| 641 | 336 | $tz = wp_timezone(); |
| 642 | 337 | $date_without_tz = $factory->getDate(); |
| @@ -653,14 +348,11 @@ | ||
| 653 | 348 | // insert new box |
| 654 | 349 | $new_box_id = firebox()->tables->box->insert($box); |
| 655 | 350 | |
| 656 | 351 | // add meta options for new box |
| 657 | - // TODO: In the future, use "firebox_meta". This is a temporary fix for backwards compatibility. | |
| 658 | - $checkMeta = (array) $meta; | |
| 659 | - $meta_key = isset($checkMeta['width']) ? 'firebox_meta' : 'fpframework_meta_settings'; | |
| 660 | - update_post_meta($new_box_id, $meta_key, wp_slash($meta)); | |
| 352 | + update_post_meta($new_box_id, 'fpframework_meta_settings', wp_slash($meta)); | |
| 661 | 353 | |
| 662 | - return $new_box_id; | |
| 354 | + return true; | |
| 663 | 355 | } |
| 664 | 356 | |
| 665 | 357 | /** |
| 666 | 358 | * Reset Box Stats |
| @@ -681,22 +373,20 @@ | ||
| 681 | 373 | firebox()->tables->boxlog->deleteRaw('WHERE box IN (' . implode(',', $box_ids) . ')'); |
| 682 | 374 | } |
| 683 | 375 | |
| 684 | 376 | /** |
| 685 | - * Builds the export payload (boxes + meta) and a suggested filename for the given box ids, | |
| 686 | - * without emitting any headers or output. Shared by exportBoxes() (legacy file-download | |
| 687 | - * action) and the Campaigns REST export endpoint. | |
| 688 | - * | |
| 377 | + * Exports boxes | |
| 378 | + * | |
| 689 | 379 | * @param array $box_ids |
| 690 | - * | |
| 691 | - * @return array|null ['filename' => string, 'exported' => array] or null if no boxes matched | |
| 380 | + * | |
| 381 | + * @return string | |
| 692 | 382 | */ |
| 693 | - public static function getExportPayload($box_ids) | |
| 383 | + public static function exportBoxes($box_ids) | |
| 694 | 384 | { |
| 695 | 385 | // get boxes |
| 696 | 386 | $boxes = firebox()->tables->box->getResults([ |
| 697 | 387 | 'where' => [ |
| 698 | - 'ID' => ' IN (' . implode(',', array_map('intval', $box_ids)) . ')', | |
| 388 | + 'ID' => ' IN (' . implode(',', esc_sql($box_ids)) . ')', | |
| 699 | 389 | 'post_type' => " = 'firebox'" |
| 700 | 390 | ] |
| 701 | 391 | ]); |
| 702 | 392 | |
| @@ -703,13 +393,13 @@ | ||
| 703 | 393 | $boxes = (array) $boxes; |
| 704 | 394 | |
| 705 | 395 | if (!count($boxes)) |
| 706 | 396 | { |
| 707 | - return null; | |
| 397 | + return; | |
| 708 | 398 | } |
| 709 | 399 | |
| 710 | 400 | $exported = []; |
| 711 | - | |
| 401 | + | |
| 712 | 402 | $filename = firebox()->_('FB_PLUGIN_NAME') . ' Items'; |
| 713 | 403 | |
| 714 | 404 | // name for 1 box |
| 715 | 405 | if (count($boxes) == 1) |
| @@ -714,12 +404,10 @@ | ||
| 714 | 404 | // name for 1 box |
| 715 | 405 | if (count($boxes) == 1) |
| 716 | 406 | { |
| 717 | 407 | $name = mb_strtolower(html_entity_decode($boxes['0']->post_title)); |
| 718 | - // preg_replace() returns null when PCRE fails (backtrack limit, invalid UTF-8), | |
| 719 | - // and passing null on is deprecated since PHP 8.1. Keep the previous value. | |
| 720 | - $name = preg_replace('#[^a-z0-9_-]#', '_', $name) ?? $name; | |
| 721 | - $name = trim(preg_replace('#__+#', '_', $name) ?? $name, '_-'); | |
| 408 | + $name = preg_replace('#[^a-z0-9_-]#', '_', $name); | |
| 409 | + $name = trim(preg_replace('#__+#', '_', $name), '_-'); | |
| 722 | 410 | |
| 723 | 411 | $filename = firebox()->_('FB_PLUGIN_NAME') . ' Item (' . $name . ')'; |
| 724 | 412 | } |
| 725 | 413 | |
| @@ -732,31 +420,8 @@ | ||
| 732 | 420 | 'meta' => $meta |
| 733 | 421 | ]; |
| 734 | 422 | } |
| 735 | 423 | |
| 736 | - return [ | |
| 737 | - 'filename' => $filename, | |
| 738 | - 'exported' => $exported, | |
| 739 | - ]; | |
| 740 | - } | |
| 741 | - | |
| 742 | - /** | |
| 743 | - * Exports boxes | |
| 744 | - * | |
| 745 | - * @param array $box_ids | |
| 746 | - * | |
| 747 | - * @return string | |
| 748 | - */ | |
| 749 | - public static function exportBoxes($box_ids) | |
| 750 | - { | |
| 751 | - if (!$payload = self::getExportPayload($box_ids)) | |
| 752 | - { | |
| 753 | - return; | |
| 754 | - } | |
| 755 | - | |
| 756 | - $filename = $payload['filename']; | |
| 757 | - $exported = $payload['exported']; | |
| 758 | - | |
| 759 | 424 | // SET DOCUMENT HEADER |
| 760 | 425 | $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; |
| 761 | 426 | if (preg_match('#Opera(/| )([0-9].[0-9]{1,2})#', $userAgent)) |
| 762 | 427 | { |
| @@ -790,13 +455,8 @@ | ||
| 790 | 455 | } |
| 791 | 456 | |
| 792 | 457 | // PRINT STRING |
| 793 | 458 | echo wp_json_encode($exported); |
| 794 | - | |
| 795 | - if (ob_get_level()) | |
| 796 | - { | |
| 797 | - @ob_end_flush(); | |
| 798 | - } | |
| 799 | 459 | die(); |
| 800 | 460 | } |
| 801 | 461 | |
| 802 | 462 | /** |
| @@ -817,9 +477,9 @@ | ||
| 817 | 477 | 'select' => [ |
| 818 | 478 | 'date as last_date_viewed' |
| 819 | 479 | ], |
| 820 | 480 | 'where' => [ |
| 821 | - 'box' => ' = ' . absint($id), | |
| 481 | + 'box' => ' = ' . esc_sql($id), | |
| 822 | 482 | ], |
| 823 | 483 | 'orderby' => ' date desc', |
| 824 | 484 | 'limit' => 1 |
| 825 | 485 | ]); |
| @@ -825,7 +485,5 @@ | ||
| 825 | 485 | ]); |
| 826 | 486 | |
| 827 | 487 | return isset($last_date_viewed[0]->last_date_viewed) ? get_date_from_gmt($last_date_viewed[0]->last_date_viewed) : null; |
| 828 | 488 | } |
| 829 | - | |
| 830 | - | |
| 831 | -} | |
| 489 | +} | |