PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.0
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.0
4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / EmbedPress / Core.php
embedpress / EmbedPress Last commit date
AMP 3 years ago Analytics 1 month ago Elementor 4 days ago Ends 4 days ago Gutenberg 4 days ago Includes 4 days ago Plugins 1 year ago Providers 1 month ago ThirdParty 4 days ago AutoLoader.php 3 years ago Compatibility.php 3 years ago Core.php 4 days ago CoreLegacy.php 2 months ago DisablerLegacy.php 3 years ago Loader.php 3 years ago MilestoneNotification.php 4 days ago RestAPI.php 1 month ago Shortcode.php 4 days ago index.html 7 years ago simple_html_dom.php 4 years ago
Core.php
1008 lines
1 <?php
2
3 namespace EmbedPress;
4
5 use EmbedPress\Ends\Back\Handler as EndHandlerAdmin;
6 use EmbedPress\Ends\Back\Settings\EmbedpressSettings;
7 use EmbedPress\Ends\Front\Handler as EndHandlerPublic;
8 use EmbedPress\Includes\Traits\Shared;
9 use EmbedPress\Includes\Classes\FeatureNotices;
10 use EmbedPress\Includes\Classes\FeaturePreviewModal;
11
12
13 (defined('ABSPATH') && defined('EMBEDPRESS_IS_LOADED')) or die("No direct script access allowed.");
14
15 /**
16 * Entity that glues together all pieces that the plugin is made of, for WordPress 5+.
17 *
18 * @package EmbedPress
19 * @author EmbedPress <help@embedpress.com>
20 * @copyright Copyright (C) 2021 WPDeveloper. All rights reserved.
21 * @license GPLv3 or later
22 * @since 1.0.0
23 */
24 class Core
25 {
26 use Shared;
27
28 /**
29 * The name of the plugin.
30 *
31 * @since 1.0.0
32 * @access protected
33 *
34 * @var string $pluginName The name of the plugin.
35 */
36 protected $pluginName;
37
38 /**
39 * The version of the plugin.
40 *
41 * @since 1.0.0
42 * @access protected
43 *
44 * @var string $pluginVersion The version of the plugin.
45 */
46 protected $pluginVersion;
47
48 /**
49 * An instance of the plugin loader.
50 *
51 * @since 1.0.0
52 * @access protected
53 *
54 * @var Loader $pluginVersion The version of the plugin.
55 */
56 protected $loaderInstance;
57
58 /**
59 * An associative array storing all registered/active EmbedPress plugins and their namespaces.
60 *
61 * @since 1.4.0
62 * @access private
63 * @static
64 *
65 * @var array
66 */
67 private static $plugins = [];
68
69 /**
70 * Initialize the plugin and set its properties.
71 *
72 * @return void
73 * @since 1.0.0
74 *
75 */
76 public function __construct()
77 {
78 $this->pluginName = EMBEDPRESS_PLG_NAME;
79 $this->pluginVersion = EMBEDPRESS_VERSION;
80
81 $this->loaderInstance = new Loader();
82
83 add_action('in_admin_header', [$this, 'remove_admin_notice'], 99);
84 add_action('ep_admin_notices', [$this, 'embedpress_admin_notice']);
85 add_action('ep_admin_notices', [$this, 'admin_notice']);
86
87 add_filter('upload_mimes', [$this, 'extended_mime_types']);
88
89 add_action('wp_mail_failed', [$this, 'capture_mail_error'], 10, 1);
90 }
91
92 /**
93 * Method that retrieves the plugin name.
94 *
95 * @return string
96 * @since 1.0.0
97 *
98 */
99 public function getPluginName()
100 {
101 return $this->pluginName;
102 }
103
104 /**
105 * Method that retrieves the plugin version.
106 *
107 * @return string
108 * @since 1.0.0
109 *
110 */
111 public function getPluginVersion()
112 {
113 return $this->pluginVersion;
114 }
115
116 /**
117 * Method that retrieves the loader instance.
118 *
119 * @return Loader
120 * @since 1.0.0
121 *
122 */
123 public function getLoader()
124 {
125 return $this->loaderInstance;
126 }
127
128 /**
129 * Method responsible to connect all required hooks in order to make the plugin work.
130 *
131 * @return void
132 * @since 1.0.0
133 *
134 */
135 public function initialize()
136 {
137 global $wp_actions;
138 add_filter('oembed_providers', [$this, 'addOEmbedProviders']);
139 add_action('rest_api_init', [$this, 'registerOEmbedRestRoutes']);
140 add_action('rest_api_init', ['\\EmbedPress\\Includes\\Classes\\GoogleReviewsRestController', 'register']);
141 add_action('enqueue_block_editor_assets', ['\\EmbedPress\\Includes\\Classes\\GoogleReviewsRenderer', 'enqueue_editor_assets']);
142 add_action('elementor/preview/enqueue_styles', ['\\EmbedPress\\Includes\\Classes\\GoogleReviewsRenderer', 'enqueue_editor_assets']);
143 \EmbedPress\Includes\Classes\GoogleReviewsAdminPage::register();
144 // Apify-backed review fetching (the heavy "fetch all reviews" job + the
145 // sync ≤5 fallback) lives in free so the picker → refetch flow works
146 // without Pro. Pro extends this via filters (multi-place merge,
147 // advanced layouts, filtering, theme, schema) — see
148 // embedpress-pro/includes/Filters/Google_Reviews_Pro.php.
149 new \EmbedPress\Includes\Classes\GoogleReviewsApify();
150 // Hosted-proxy review fetch (api.embedpress.com/google-reviews/v1).
151 // Lowest-priority handler on the same filters — Apify (user token) wins
152 // when configured; managed runs as the zero-setup fallback.
153 new \EmbedPress\Includes\Classes\GoogleReviewsManaged();
154
155 // just disabled rating and feedback
156 // add_action('rest_api_init', [$this, 'register_feedback_email_endpoint']);
157
158
159 $this->start_plugin_tracking();
160
161 if (is_admin()) {
162 new EmbedpressSettings();
163
164 add_action('init', [$this, 'admin_notice']);
165
166 // Initialize Feature Notices from separate file
167 FeatureNotices::get_instance();
168
169 // Initialize the post-update "What's New" feature preview modal.
170 // Its dismiss AJAX + enqueue/render hooks self-register; release
171 // feature sets are registered from FeatureNotices::register_all_notices().
172 FeaturePreviewModal::get_instance();
173
174 add_filter(
175 'plugin_action_links_embedpress/embedpress.php',
176 ['\\EmbedPress\\Core', 'handleActionLinks'],
177 10,
178 2
179 );
180
181 // Old enqueue handlers removed - now handled by AssetManager
182 add_action('wp_ajax_embedpress_notice_dismiss', ['\\EmbedPress\\Ends\\Back\\Handler', 'embedpress_notice_dismiss']);
183 new EndHandlerAdmin($this->getPluginName(), $this->getPluginVersion());
184 // Asset enqueuing now handled by AssetManager - keeping only non-asset functionality
185 } else {
186 // Asset enqueuing now handled by AssetManager - keeping only non-asset functionality
187 new EndHandlerPublic($this->getPluginName(), $this->getPluginVersion());
188 }
189
190 // Add support for embeds on AMP pages
191 add_filter('pp_embed_parsed_content', ['\\EmbedPress\\AMP\\EmbedHandler', 'processParsedContent'], 10, 3);
192
193 // Add support for our embeds on Beaver Builder. Without this it only run the native embeds.
194 add_filter(
195 'fl_builder_before_render_shortcodes',
196 ['\\EmbedPress\\ThirdParty\\BeaverBuilder', 'before_render_shortcodes']
197 );
198 $this->loaderInstance->run();
199 }
200
201 /**
202 * Initialize minimal plugin functionality without script handlers
203 * Used when the new block system is active to avoid conflicts
204 *
205 * @return void
206 * @since 4.2.7
207 */
208 public function initialize_minimal()
209 {
210
211 add_filter('oembed_providers', [$this, 'addOEmbedProviders']);
212 add_action('rest_api_init', [$this, 'registerOEmbedRestRoutes']);
213
214 // just disabled rating and feedback
215 // add_action('rest_api_init', [$this, 'register_feedback_email_endpoint']);
216
217 $this->start_plugin_tracking();
218
219 // Skip the admin and frontend handlers that enqueue scripts
220 // Only initialize core functionality
221
222
223 // Add support for embeds on AMP pages
224 add_filter('pp_embed_parsed_content', ['\\EmbedPress\\AMP\\EmbedHandler', 'processParsedContent'], 10, 3);
225
226 // Add support for our embeds on Beaver Builder
227 add_filter(
228 'fl_builder_before_render_shortcodes',
229 ['\\EmbedPress\\ThirdParty\\BeaverBuilder', 'before_render_shortcodes']
230 );
231
232 $this->loaderInstance->run();
233 }
234
235 /**
236 * @param $providers
237 *
238 * @return mixed
239 */
240 public function addOEmbedProviders($providers)
241 {
242 $newProviders = [
243 // Viddler
244 '#https?://(.+\.)?viddler\.com/v/.+#i' => 'viddler',
245
246 // Deviantart.com (http://www.deviantart.com)
247 // '#https?://(.+\.)?deviantart\.com/art/.+#i' => 'devianart',
248 // '#https?://(.+\.)?deviantart\.com/.+#i' => 'devianart',
249 // '#https?://(.+\.)?deviantart\.com/.*/d.+#i' => 'devianart',
250 // '#https?://(.+\.)?fav\.me/.+#i' => 'devianart',
251 // '#https?://(.+\.)?sta\.sh/.+#i' => 'devianart',
252
253 // chirbit.com (http://www.chirbit.com/)
254 //'#https?://(.+\.)?chirb\.it/.+#i' => 'chirbit',
255
256
257 // nfb.ca (http://www.nfb.ca/)
258 //'#https?://(.+\.)?nfb\.ca/film/.+#i' => 'nfb',
259
260 // Dotsub (http://dotsub.com/)
261 //'#https?://(.+\.)?dotsub\.com/view/.+#i' => 'dotsub',
262
263 // Rdio (http://rdio.com/)
264 '#https?://(.+\.)?rdio\.com/(artist|people)/.+#i' => 'rdio',
265
266 // Sapo Videos (http://videos.sapo.pt)
267 //'#https?://(.+\.)?videos\.sapo\.pt/.+#i' => 'sapo',
268
269 // Official FM (http://official.fm)
270 '#https?://(.+\.)?official\.fm/(tracks|playlists)/.+#i' => 'officialfm',
271
272 // HuffDuffer (http://huffduffer.com)
273 //'#https?://(.+\.)?huffduffer\.com/.+#i' => 'huffduffer',
274
275 // Shoudio (http://shoudio.com)
276 //'#https?://(.+\.)?shoudio\.(com|io)/.+#i' => 'shoudio',
277
278 // Moby Picture (http://www.mobypicture.com)
279 '#https?://(.+\.)?mobypicture\.com/user/.+/view/.+#i' => 'mobypicture',
280 '#https?://(.+\.)?moby\.to/.+#i' => 'mobypicture',
281
282 // 23HQ (http://www.23hq.com)
283 //'#https?://(.+\.)?23hq\.com/.+/photo/.+#i' => '23hq',
284
285 // Cacoo (https://cacoo.com)
286 '#https?://(.+\.)?cacoo\.com/diagrams/.+#i' => 'cacoo',
287
288 // Dipity (http://www.dipity.com)
289 '#https?://(.+\.)?dipity\.com/.+#i' => 'dipity',
290
291 // Roomshare (http://roomshare.jp)
292 //'#https?://(.+\.)?roomshare\.jp/(en/)?post/.+#i' => 'roomshare',
293
294 // Crowd Ranking (http://crowdranking.com)
295 '#https?://(.+\.)?c9ng\.com/.+#i' => 'crowd',
296
297 // CircuitLab (https://www.circuitlab.com/)
298 //'#https?://(.+\.)?circuitlab\.com/circuit/.+#i' => 'circuitlab',
299
300 // Coub (http://coub.com/)
301 //'#https?://(.+\.)?coub\.com/(view|embed)/.+#i' => 'coub',
302
303 // Ustream (http://www.ustream.tv)
304 //'#https?://(.+\.)?ustream\.(tv|com)/.+#i' => 'ustream',
305
306 // Daily Mile (http://www.dailymile.com)
307 '#https?://(.+\.)?dailymile\.com/people/.+/entries/.+#i' => 'daily',
308
309 // Sketchfab (http://sketchfab.com)
310 '#https?://(.+\.)?sketchfab\.com/models/.+#i' => 'sketchfab',
311 '#https?://(.+\.)?sketchfab\.com/.+/folders/.+#i' => 'sketchfab',
312
313 // AudioSnaps (http://audiosnaps.com)
314 '#https?://(.+\.)?audiosnaps\.com/k/.+#i' => 'audiosnaps',
315
316 // RapidEngage (https://rapidengage.com)
317 '#https?://(.+\.)?rapidengage\.com/s/.+#i' => 'rapidengage',
318
319 // Getty Images (http://www.gettyimages.com/)
320 //'#https?://(.+\.)?gty\.im/.+#i' => 'gettyimages',
321 //'#https?://(.+\.)?gettyimages\.com/detail/photo/.+#i' => 'gettyimages',
322
323 // amCharts Live Editor (http://live.amcharts.com/)
324 //'#https?://(.+\.)?live\.amcharts\.com/.+#i' => 'amcharts',
325
326 // Infogram (https://infogr.am/)
327 //'#https?://(.+\.)?infogr\.am/.+#i' => 'infogram',
328 //(https://infogram.com/)
329 //'#https?://(.+\.)?infogram\.com/.+#i' => 'infogram',
330
331 // ChartBlocks (http://www.chartblocks.com/)
332 //'#https?://(.+\.)?public\.chartblocks\.com/c/.+#i' => 'chartblocks',
333
334 // ReleaseWire (http://www.releasewire.com/)
335 //'#https?://(.+\.)?rwire\.com/.+#i' => 'releasewire',
336
337 // ShortNote (https://www.shortnote.jp/)
338 //'#https?://(.+\.)?shortnote\.jp/view/notes/.+#i' => 'shortnote',
339
340 // EgliseInfo (http://egliseinfo.catholique.fr/)
341 '#https?://(.+\.)?egliseinfo\.catholique\.fr/.+#i' => 'egliseinfo',
342
343 // Silk (http://www.silk.co/)
344 '#https?://(.+\.)?silk\.co/explore/.+#i' => 'silk',
345 '#https?://(.+\.)?silk\.co/s/embed/.+#i' => 'silk',
346
347 // http://bambuser.com
348 '#https?://(.+\.)?bambuser\.com/v/.+#i' => 'bambuser',
349
350 // https://clyp.it
351 //'#https?://(.+\.)?clyp\.it/.+#i' => 'clyp',
352
353 // https://gist.github.com
354 // '#https?://(.+\.)?gist\.github\.com/.+#i' => 'github',
355
356 // https://portfolium.com
357 //'#https?://(.+\.)?portfolium\.com/.+#i' => 'portfolium',
358
359 // http://rutube.ru
360 '#https?://(.+\.)?rutube\.ru/video/.+#i' => 'rutube',
361
362 // http://www.videojug.com
363 '#https?://(.+\.)?videojug\.com/.+#i' => 'videojug',
364
365 // https://vine.com
366 //'#https?://(.+\.)?vine\.co/v/.+#i' => 'vine',
367
368 // Google Shortened Url
369 '#https?://(.+\.)?goo\.gl/.+#i' => 'google',
370
371 // Google Maps
372 //'#https?://(.+\.)?google\.com/maps/.+#i' => 'googlemaps',
373 //'#https?://(.+\.)?maps\.google\.com/.+#i' => 'googlemaps',
374
375 // Google Docs
376 //'#https?://(.+\.)?docs\.google\.com/(.+/)?(document|presentation|spreadsheets|forms|drawings)/.+#i' => 'googledocs',
377
378 // Twitch.tv
379 //'#https?://(.+\.)?twitch\.tv/.+#i' => 'twitch',
380
381 // Giphy
382 //'#https?://(.+\.)?giphy\.com/gifs/.+#i' => 'giphy',
383 //'#https?://(.+\.)?i\.giphy\.com/.+#i' => 'giphy',
384 //'#https?://(.+\.)?gph\.is/.+#i' => 'giphy',
385
386 // Wistia
387 //'#https?://(.+\.)?wistia\.com/medias/.+#i' => 'wistia',
388 //'#https?://(.+\.)?fast\.wistia\.com/embed/medias/.+#i\.jsonp' => 'wistia',
389 ];
390
391 /**
392 * ========================================
393 * Make sure the $wp_write global is set.
394 * This fix compatibility with JetPack, Classical Editor and Disable Gutenberg. JetPack makes
395 * the oembed_providers filter be called and this activates our class too, but one dependency
396 * of the rest_url method is not loaded yet.
397 */
398 global $wp_rewrite;
399
400 if (!class_exists('\\WP_Rewrite')) {
401 $path = ABSPATH . WPINC . '/class-wp-rewrite.php';
402 if (file_exists($path)) {
403 require_once $path;
404 }
405 }
406
407 if (!is_object($wp_rewrite)) {
408 $wp_rewrite = new \WP_Rewrite();
409 $_GLOBALS['wp_write'] = $wp_rewrite;
410 }
411 /*========================================*/
412
413 foreach ($newProviders as $url => &$data) {
414 $data = [
415 rest_url('embedpress/v1/oembed/' . $data),
416 true,
417 ];
418 }
419
420 $providers = array_merge($providers, $newProviders);
421
422 return $providers;
423 }
424
425 /**
426 * Register OEmbed Rest Routes
427 */
428 public function registerOEmbedRestRoutes()
429 {
430 register_rest_route(
431 'embedpress/v1',
432 '/oembed/(?P<provider>[a-zA-Z0-9\-]+)',
433 [
434 'methods' => \WP_REST_Server::READABLE,
435 'callback' => ['\\EmbedPress\\RestAPI', 'oembed'],
436 'permission_callback' => '__return_true',
437 ]
438 );
439 register_rest_route(
440 'embedpress/v1',
441 '/oembed/(?P<provider>[a-zA-Z0-9\-]+)',
442 [
443 'methods' => \WP_REST_Server::CREATABLE,
444 'callback' => ['\\EmbedPress\\RestAPI', 'oembed'],
445 'permission_callback' => '__return_true',
446 ]
447 );
448
449 // Queue infinite-scroll: returns a batch of rendered <li.ep-yt-queue__item>
450 // for the next page of a YouTube playlist.
451 register_rest_route(
452 'embedpress/v1',
453 '/youtube-playlist-items',
454 [
455 'methods' => \WP_REST_Server::READABLE,
456 'callback' => ['\\EmbedPress\\RestAPI', 'youtube_playlist_items'],
457 'permission_callback' => '__return_true',
458 'args' => [
459 'playlist_id' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
460 'page_token' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
461 'page_size' => ['required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint'],
462 'offset' => ['required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint'],
463 'layout' => ['required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
464 ],
465 ]
466 );
467
468 // Dynamic-source field enumerator (fbs-81736). Inspector drop-down
469 // calls this to populate field choices for the selected provider
470 // instead of asking the user to type the field key by hand.
471 register_rest_route(
472 'embedpress/v1',
473 '/dynamic-fields',
474 [
475 'methods' => \WP_REST_Server::READABLE,
476 'callback' => ['\\EmbedPress\\Includes\\Classes\\DynamicFieldResolver', 'rest_list_fields'],
477 'permission_callback' => function () {
478 return current_user_can('edit_posts')
479 && \EmbedPress\Includes\Classes\Helper::is_pro_features_enabled();
480 },
481 'args' => [
482 'source' => ['type' => 'string', 'required' => true],
483 ],
484 ]
485 );
486
487 // Dynamic-source value resolver (fbs-81736). The block editor calls this
488 // to live-preview the resolved custom-field URL instead of the saved
489 // placeholder, so the canvas matches the front-end render.
490 register_rest_route(
491 'embedpress/v1',
492 '/dynamic-resolve',
493 [
494 'methods' => \WP_REST_Server::READABLE,
495 'callback' => ['\\EmbedPress\\Includes\\Classes\\DynamicFieldResolver', 'rest_resolve_field'],
496 'permission_callback' => function () {
497 return current_user_can('edit_posts')
498 && \EmbedPress\Includes\Classes\Helper::is_pro_features_enabled();
499 },
500 'args' => [
501 'source' => ['type' => 'string', 'required' => true],
502 'field' => ['type' => 'string', 'required' => true],
503 'post_id' => ['type' => 'integer', 'required' => false],
504 ],
505 ]
506 );
507 }
508
509 public function send_user_feedback_email($request)
510 {
511 // Ensure we have a valid REST request object
512 if (!($request instanceof \WP_REST_Request)) {
513 return new \WP_REST_Response(['message' => 'Invalid request'], 400);
514 }
515
516 // CRITICAL: Check if feedback was already sent FIRST to prevent spam
517 $is_feedback_already_sent = get_option('embedpress_feedback_submited');
518 if ($is_feedback_already_sent) {
519 return new \WP_REST_Response(['message' => 'Feedback already submitted'], 200);
520 }
521
522 // Rate limiting: Only allow one request per IP per minute
523 $user_ip = $this->get_user_ip();
524 $rate_limit_key = 'embedpress_feedback_rate_limit_' . md5($user_ip);
525 if (get_transient($rate_limit_key)) {
526 return new \WP_REST_Response(['message' => 'Too many requests. Please try again later.'], 429);
527 }
528 set_transient($rate_limit_key, 2, MINUTE_IN_SECONDS);
529
530 // Verify user is logged in and has admin capabilities
531 if (!is_user_logged_in()) {
532 return new \WP_REST_Response(['message' => 'Unauthorized. You must be logged in.'], 401);
533 }
534
535 if (!current_user_can('manage_options')) {
536 return new \WP_REST_Response(['message' => 'Forbidden. Insufficient permissions.'], 403);
537 }
538
539 // Verify nonce for CSRF protection
540 $nonce = $request->get_header('X-WP-Nonce');
541
542 if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
543 return new \WP_REST_Response(['message' => 'Invalid security token. Please refresh the page and try again.'], 403);
544 }
545
546
547 $params = $request->get_params();
548
549 // Safely extract and sanitize incoming params to avoid undefined index notices
550 $params = is_array($params) ? $params : [];
551 $user_email = isset($params['email']) ? sanitize_email($params['email']) : '';
552 $user_name = isset($params['name']) ? sanitize_text_field($params['name']) : '';
553 $user_rating = isset($params['rating']) ? intval($params['rating']) : 0;
554 $user_msg = isset($params['message']) ? sanitize_textarea_field($params['message']) : '';
555
556 // Validate rating is within acceptable range
557 if ($user_rating < 1 || $user_rating > 5) {
558 return new \WP_REST_Response(['message' => 'Invalid rating value. Must be between 1 and 5.'], 400);
559 }
560
561 // Prevent submissions with empty/invalid user data (prevents N/A spam)
562 if (empty($user_email) || !is_email($user_email)) {
563 return new \WP_REST_Response(['message' => 'Valid email address is required.'], 400);
564 }
565
566 if (empty($user_name) || strlen(trim($user_name)) < 2) {
567 return new \WP_REST_Response(['message' => 'Valid name is required.'], 400);
568 }
569
570 // If the payload is completely empty, ignore to prevent blank/spam emails
571 $has_meaningful_input = false;
572
573 if ($user_rating > 0) {
574 if ($user_rating < 5) {
575 // description required
576 if (trim($user_msg) !== '') {
577 $has_meaningful_input = true;
578 }
579 } else {
580 // rating is 5, description not required
581 $has_meaningful_input = true;
582 }
583 }
584
585 if (!$has_meaningful_input) {
586 return new \WP_REST_Response(['message' => 'No feedback content provided; ignored.'], 200);
587 }
588
589 // Prevent accidental duplicate submissions (double-clicks, quick retries)
590 $payload_hash = md5(json_encode([$user_email, $user_name, $user_rating, $user_msg]));
591 if (get_transient('embedpress_feedback_dupe_' . $payload_hash)) {
592 return new \WP_REST_Response(['message' => 'Duplicate feedback detected; already processed.'], 200);
593 }
594 set_transient('embedpress_feedback_dupe_' . $payload_hash, 1, 5 * MINUTE_IN_SECONDS);
595
596 $email_html = $user_email ? '<a href="mailto:' . esc_attr($user_email) . '">' . esc_html($user_email) . '</a>' : 'N/A';
597 $rating_html = $user_rating ? esc_html($user_rating) . ' ⭐️' : 'N/A';
598 $message_html = $user_msg !== '' ? nl2br(esc_html($user_msg)) : 'N/A';
599
600
601 $site_name = get_bloginfo('name');
602 $site_url = get_site_url();
603 $admin_email = get_option('admin_email');
604 $wp_version = get_bloginfo('version');
605
606 $admin_user = get_user_by('ID', 1);
607 if ($admin_user) {
608 $first_name = get_user_meta($admin_user->ID, 'first_name', true);
609 $last_name = get_user_meta($admin_user->ID, 'last_name', true);
610
611 $admin_full_name = trim("$first_name $last_name");
612
613 // Fallback to display name if full name is not set
614 if (empty($admin_full_name)) {
615 $admin_full_name = $admin_user->display_name;
616 }
617 } else {
618 $admin_full_name = 'Unknown';
619 }
620
621 $to = 'akash@wpdeveloper.com, rasel@wpdeveloper.com, nahid@wpdeveloper.com, md-nahid-hasan@wpdeveloper.com'; // Replace with the recipient's email
622 $subject = '[IMPORTANT] New feedback received from an EmbedPress user.';
623
624 // HTML Email Template
625 $message = '<html><body style="font-family: Arial, sans-serif; padding: 20px;">';
626 $message .= '<div style="max-width: 600px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: auto;">';
627 $message .= '<div style="text-align: center; padding-bottom: 20px; border-bottom: 1px solid #ddd">';
628 $message .= '<img src="https://embedpress.com/wp-content/uploads/2025/03/logo.png" alt="EmbedPress" style="max-width: 150px;">';
629 $message .= '</div>';
630 $message .= '<h2 style="font-family: system-ui; color: #333; text-align: center;">Feedback Overview</h2>';
631 $message .= '<table style="font-family: system-ui; width: 100%; border-collapse: collapse; border: 1px solid #ddd">';
632
633 // Email
634 $message .= '<tr><td style="padding: 10px; font-weight: bold; width: 100px; border-bottom: 1px solid #ddd;">Email :</td>';
635 $message .= '<td style="padding: 10px; border-bottom: 1px solid #ddd;">' . $email_html . '</td></tr>';
636
637 // Rating
638 $message .= '<tr><td style="padding: 10px; font-weight: bold; width: 100px; border-bottom: 1px solid #ddd;">Rating :</td>';
639 $message .= '<td style="padding: 10px; border-bottom: 1px solid #ddd;">' . $rating_html . '</td></tr>';
640
641 // User
642 $message .= '<tr><td style="padding: 10px; font-weight: bold; width: 100px; border-bottom: 1px solid #ddd;">Name :</td>';
643 $message .= '<td style="padding: 10px; border-bottom: 1px solid #ddd; font-weight: 500;">' . esc_html($admin_full_name) . '</td></tr>';
644
645 // Pack
646 $message .= '<tr><td style="padding: 10px; font-weight: bold; width: 100px; border-bottom: 1px solid #ddd;">Site Url :</td>';
647 $message .= '<td style="padding: 10px; border-bottom: 1px solid #ddd;"><a target="_blank" href="' . esc_url($site_url) . '" style="color: blue;">' . esc_html($site_url) . '</a></td></tr>';
648
649 // Feedback
650 $message .= '<tr><td style="padding: 10px; font-weight: bold; width: 100px; display: flex;">Feedback :</td>';
651 $message .= '<td style="padding: 10px;">' . $message_html . '</td></tr>';
652
653 $message .= '</table>';
654 $message .= '</div></body></html>';
655
656 $headers = [
657 'Content-Type: text/html; charset=UTF-8',
658 'From: ' . esc_html($site_name) . ' <' . sanitize_email($admin_email) . '>'
659 ];
660 if ($user_email) {
661 $reply_to = $user_name ? esc_html($user_name) . ' <' . sanitize_email($user_email) . '>' : sanitize_email($user_email);
662 $headers[] = 'Reply-To: ' . $reply_to;
663 }
664
665 // Send the email
666 $sent = wp_mail($to, $subject, $message, $headers);
667
668 if ($sent) {
669 update_option('embedpress_feedback_submited', true);
670
671 return new \WP_REST_Response(['message' => 'Email sent successfully!'], 200);
672 } else {
673 // Retrieve last error
674 $last_error = get_transient('embedpress_last_mail_error');
675 $error_message = 'Failed to send email.';
676 if ($last_error instanceof \WP_Error) {
677 $error_message = $last_error->get_error_message();
678 }
679
680 return new \WP_REST_Response([
681 'message' => $error_message
682 ], 422); // using 422 instead of 500
683 }
684 }
685
686 public function capture_mail_error($wp_error)
687 {
688 if ($wp_error instanceof \WP_Error) {
689 set_transient('embedpress_last_mail_error', $wp_error, 60);
690 }
691 }
692
693 /**
694 * Get user IP address safely
695 *
696 * @return string
697 */
698 private function get_user_ip()
699 {
700 $ip_keys = [
701 'HTTP_CF_CONNECTING_IP', // CloudFlare
702 'HTTP_X_FORWARDED_FOR',
703 'HTTP_X_REAL_IP',
704 'REMOTE_ADDR'
705 ];
706
707 foreach ($ip_keys as $key) {
708 if (!empty($_SERVER[$key])) {
709 $ip = sanitize_text_field(wp_unslash($_SERVER[$key]));
710 // Handle comma-separated IPs (from proxies)
711 if (strpos($ip, ',') !== false) {
712 $ip = trim(explode(',', $ip)[0]);
713 }
714 if (filter_var($ip, FILTER_VALIDATE_IP)) {
715 return $ip;
716 }
717 }
718 }
719
720 return '0.0.0.0';
721 }
722
723 /**
724 * Permission callback for feedback endpoint
725 *
726 * We return true here to allow the request to reach our handler,
727 * where we perform detailed authentication and authorization checks
728 * with custom error messages.
729 *
730 * @return bool
731 */
732 public function feedback_permission_callback()
733 {
734 // Return true to allow request to reach handler
735 // Actual auth checks are done in send_user_feedback_email()
736 return true;
737 }
738
739
740 public function register_feedback_email_endpoint()
741 {
742 register_rest_route('embedpress/v1', '/send-feedback', [
743 'methods' => 'POST',
744 'callback' => [$this, 'send_user_feedback_email'],
745 'permission_callback' => [$this, 'feedback_permission_callback'],
746 'args' => [
747 'email' => [
748 'required' => false,
749 'type' => 'string',
750 'sanitize_callback' => 'sanitize_email',
751 ],
752 'name' => [
753 'required' => false,
754 'type' => 'string',
755 'sanitize_callback' => 'sanitize_text_field',
756 ],
757 'rating' => [
758 'required' => true,
759 'type' => 'integer',
760 'validate_callback' => function($param) {
761 return is_numeric($param) && $param >= 1 && $param <= 5;
762 }
763 ],
764 'message' => [
765 'required' => false,
766 'type' => 'string',
767 'sanitize_callback' => 'sanitize_textarea_field',
768 ],
769 ]
770 ]);
771 }
772
773
774
775 /**
776 * Callback called right after the plugin has been activated.
777 *
778 * @return void
779 * @since 1.0.0
780 * @static
781 *
782 */
783 public static function onPluginActivationCallback()
784 {
785 $dirname = wp_get_upload_dir()['basedir'] . '/embedpress';
786 if (!file_exists($dirname)) {
787 mkdir($dirname, 0777);
788 }
789 flush_rewrite_rules();
790 embedpress_schedule_cache_cleanup();
791
792 // Trigger the setup wizard redirect only on a true fresh install. The
793 // 'embedpress_install_type' marker is set in EmbedpressSettings::__construct
794 // before any default settings are written, so it reliably distinguishes
795 // first-ever installs from re-activations after a plugin update.
796 $pro_active = apply_filters( 'embedpress/is_allow_rander', false );
797 $install_type = get_option( 'embedpress_install_type', false );
798
799 if ( ! $pro_active && $install_type !== 'existing' ) {
800 $settings = get_option( EMBEDPRESS_PLG_NAME, [] );
801 $settings['need_first_time_redirect'] = true;
802 update_option( EMBEDPRESS_PLG_NAME, $settings );
803
804 // Clear any previous redirect done flag
805 delete_option( 'embedpress_activation_redirect_done' );
806
807 }
808
809 // "What's New" modal: show ONLY after an update from an older version,
810 // never on a brand-new install (a first-time user has nothing "new").
811 // The install_type marker is written later (on admin_init), so at
812 // activation time we detect a fresh site directly by the absence of any
813 // prior EmbedPress data — mirroring EmbedpressSettings' own check. On a
814 // fresh site we stamp the current version as already seen so the modal
815 // stays suppressed; on an update there IS prior data, so we leave the
816 // seen-version alone and the modal fires once.
817 $seen_option = \EmbedPress\Includes\Classes\FeaturePreviewModal::SEEN_VERSION_OPTION;
818 if ( get_option( $seen_option, false ) === false ) {
819 $had_prior_data = (bool) get_option( 'embedpress_elements_updated', false )
820 || ! empty( get_option( EMBEDPRESS_PLG_NAME, [] ) )
821 || ! empty( get_option( EMBEDPRESS_PLG_NAME . ':elements', [] ) )
822 || $install_type === 'existing';
823
824 if ( ! $had_prior_data ) {
825 update_option(
826 $seen_option,
827 defined( 'EMBEDPRESS_VERSION' ) ? EMBEDPRESS_VERSION : '0.0.0'
828 );
829 }
830 }
831 }
832
833 /**
834 * Callback called right after the plugin has been deactivated.
835 *
836 * @return void
837 * @since 1.0.0
838 * @static
839 *
840 */
841 public static function onPluginDeactivationCallback()
842 {
843 flush_rewrite_rules();
844 embedpress_cache_cleanup();
845 $timestamp = wp_next_scheduled('embedpress_backup_cleanup_action');
846 if ($timestamp) {
847 wp_unschedule_event($timestamp, 'embedpress_backup_cleanup_action');
848 }
849 }
850
851 /**
852 * Method that retrieves all additional service providers defined in the ~<plugin_root_path>/providers.php file.
853 *
854 * @return array
855 * @since 1.0.0
856 * @static
857 *
858 */
859 public static function getAdditionalServiceProviders()
860 {
861 $additionalProvidersFilePath = EMBEDPRESS_PATH_BASE . 'providers.php';
862 if (file_exists($additionalProvidersFilePath)) {
863 include $additionalProvidersFilePath;
864
865 if (isset($additionalServiceProviders)) {
866 return apply_filters('embedpress_additional_service_providers', $additionalServiceProviders);
867 }
868 }
869
870 return apply_filters('embedpress_additional_service_providers', []);
871 }
872
873 /**
874 * Method that checks if an embed of a given service provider can be responsive.
875 *
876 * @param string $serviceProviderAlias The service's slug.
877 *
878 * @return boolean
879 * @since 1.0.0
880 * @static
881 *
882 */
883 public static function canServiceProviderBeResponsive($serviceProviderAlias)
884 {
885 return in_array($serviceProviderAlias, [
886 "dailymotion",
887 "kickstarter",
888 "rutube",
889 "ted",
890 "vimeo",
891 "youtube",
892 "ustream",
893 "google-docs",
894 "animatron",
895 "amcharts",
896 "on-aol-com",
897 "animoto",
898 "videojug",
899 'issuu',
900 ]);
901 }
902
903 /**
904 * Method that retrieves the plugin settings defined by the user.
905 *
906 * @return object
907 * @since 1.0.0
908 * @static
909 *
910 */
911 public static function getSettings()
912 {
913 // Fetch settings from the database
914 $settings = get_option(EMBEDPRESS_PLG_NAME);
915
916 // If the settings are not an array (it might return false), initialize as an empty array
917 if (!is_array($settings)) {
918 $settings = [];
919 }
920
921 // Default values if settings are missing
922 if (!isset($settings['enablePluginInAdmin'])) {
923 $settings['enablePluginInAdmin'] = true;
924 }
925
926 if (!isset($settings['enablePluginInFront'])) {
927 $settings['enablePluginInFront'] = true;
928 }
929
930 if (!isset($settings['enableGlobalEmbedResize'])) {
931 $settings['enableGlobalEmbedResize'] = false;
932 }
933
934 if (!isset($settings['enableEmbedResizeHeight'])) {
935 $settings['enableEmbedResizeHeight'] = 550; // old 552
936 }
937
938 if (!isset($settings['enableEmbedResizeWidth'])) {
939 $settings['enableEmbedResizeWidth'] = 600; // old 652
940 }
941
942 return (object) $settings;
943 }
944
945 /**
946 * Retrieve all registered plugins.
947 *
948 * @return array
949 * @since 1.4.0
950 * @static
951 *
952 */
953 public static function getPlugins()
954 {
955 return self::$plugins;
956 }
957
958 /**
959 * Handle links displayed below the plugin name in the WordPress Installed Plugins page.
960 *
961 * @return array
962 * @since 1.4.0
963 * @static
964 *
965 */
966 public static function handleActionLinks($links, $file)
967 {
968 $settingsLink = '<a href="' . admin_url('admin.php?page=embedpress') . '" aria-label="' . __(
969 'Open settings page',
970 'embedpress'
971 ) . '">' . __('Settings', 'embedpress') . '</a>';
972
973 array_unshift($links, $settingsLink);
974 if (!apply_filters('embedpress/is_allow_rander', false)) {
975 $links[] = '<a href="https://wpdeveloper.com/in/upgrade-embedpress" target="_blank" class="embedpress-go-pro-action" style="color: green">' . __('Go Pro', 'embedpress') . '</a>';
976 }
977 return $links;
978 }
979
980
981 /**
982 * Method that ensures the API's url are whitelisted to WordPress external requests.
983 *
984 * @param boolean $isAllowed
985 * @param string $host
986 * @param string $url
987 *
988 * @return boolean
989 * @since 1.4.0
990 * @static
991 *
992 */
993 public static function allowApiHost($isAllowed, $host, $url)
994 {
995 if ($host === EMBEDPRESS_LICENSES_API_HOST) {
996 $isAllowed = true;
997 }
998
999 return $isAllowed;
1000 }
1001
1002 public function extended_mime_types($mimes)
1003 {
1004 $mimes['ppsx'] = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
1005 return $mimes;
1006 }
1007 }
1008