PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.2
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.2
4.6.2 4.6.1 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 2 months ago Elementor 4 weeks ago Ends 2 weeks ago Gutenberg 4 weeks ago Includes 1 week ago Plugins 1 year ago Providers 1 month ago ThirdParty 4 weeks ago AutoLoader.php 3 years ago Compatibility.php 3 years ago Core.php 1 week ago CoreLegacy.php 2 weeks ago DisablerLegacy.php 3 years ago Loader.php 3 years ago MilestoneNotification.php 4 weeks ago RestAPI.php 1 month ago Shortcode.php 4 weeks ago index.html 7 years ago simple_html_dom.php 5 years ago
Core.php
1039 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 // This endpoint fetches an arbitrary user-supplied URL server-side
437 // (oEmbed discovery). Its only legitimate caller is the block editor
438 // preview, which always runs as a logged-in editor. Requiring
439 // edit_posts prevents unauthenticated SSRF via this route.
440 'permission_callback' => function () {
441 return current_user_can('edit_posts');
442 },
443 ]
444 );
445 register_rest_route(
446 'embedpress/v1',
447 '/oembed/(?P<provider>[a-zA-Z0-9\-]+)',
448 [
449 'methods' => \WP_REST_Server::CREATABLE,
450 'callback' => ['\\EmbedPress\\RestAPI', 'oembed'],
451 'permission_callback' => function () {
452 return current_user_can('edit_posts');
453 },
454 ]
455 );
456
457 // Queue infinite-scroll: returns a batch of rendered <li.ep-yt-queue__item>
458 // for the next page of a YouTube playlist.
459 register_rest_route(
460 'embedpress/v1',
461 '/youtube-playlist-items',
462 [
463 'methods' => \WP_REST_Server::READABLE,
464 'callback' => ['\\EmbedPress\\RestAPI', 'youtube_playlist_items'],
465 'permission_callback' => '__return_true',
466 'args' => [
467 'playlist_id' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
468 'page_token' => ['required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
469 'page_size' => ['required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint'],
470 'offset' => ['required' => false, 'type' => 'integer', 'sanitize_callback' => 'absint'],
471 'layout' => ['required' => false, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
472 ],
473 ]
474 );
475
476 // Dynamic-source field enumerator (fbs-81736). Inspector drop-down
477 // calls this to populate field choices for the selected provider
478 // instead of asking the user to type the field key by hand.
479 register_rest_route(
480 'embedpress/v1',
481 '/dynamic-fields',
482 [
483 'methods' => \WP_REST_Server::READABLE,
484 'callback' => ['\\EmbedPress\\Includes\\Classes\\DynamicFieldResolver', 'rest_list_fields'],
485 'permission_callback' => function () {
486 return current_user_can('edit_posts')
487 && \EmbedPress\Includes\Classes\Helper::is_pro_features_enabled();
488 },
489 'args' => [
490 'source' => ['type' => 'string', 'required' => true],
491 ],
492 ]
493 );
494
495 // Dynamic-source value resolver (fbs-81736). The block editor calls this
496 // to live-preview the resolved custom-field URL instead of the saved
497 // placeholder, so the canvas matches the front-end render.
498 register_rest_route(
499 'embedpress/v1',
500 '/dynamic-resolve',
501 [
502 'methods' => \WP_REST_Server::READABLE,
503 'callback' => ['\\EmbedPress\\Includes\\Classes\\DynamicFieldResolver', 'rest_resolve_field'],
504 'permission_callback' => function () {
505 return current_user_can('edit_posts')
506 && \EmbedPress\Includes\Classes\Helper::is_pro_features_enabled();
507 },
508 'args' => [
509 'source' => ['type' => 'string', 'required' => true],
510 'field' => ['type' => 'string', 'required' => true],
511 'post_id' => ['type' => 'integer', 'required' => false],
512 ],
513 ]
514 );
515 }
516
517 public function send_user_feedback_email($request)
518 {
519 // Ensure we have a valid REST request object
520 if (!($request instanceof \WP_REST_Request)) {
521 return new \WP_REST_Response(['message' => 'Invalid request'], 400);
522 }
523
524 // CRITICAL: Check if feedback was already sent FIRST to prevent spam
525 $is_feedback_already_sent = get_option('embedpress_feedback_submited');
526 if ($is_feedback_already_sent) {
527 return new \WP_REST_Response(['message' => 'Feedback already submitted'], 200);
528 }
529
530 // Rate limiting: Only allow one request per IP per minute
531 $user_ip = $this->get_user_ip();
532 $rate_limit_key = 'embedpress_feedback_rate_limit_' . md5($user_ip);
533 if (get_transient($rate_limit_key)) {
534 return new \WP_REST_Response(['message' => 'Too many requests. Please try again later.'], 429);
535 }
536 set_transient($rate_limit_key, 2, MINUTE_IN_SECONDS);
537
538 // Verify user is logged in and has admin capabilities
539 if (!is_user_logged_in()) {
540 return new \WP_REST_Response(['message' => 'Unauthorized. You must be logged in.'], 401);
541 }
542
543 if (!current_user_can('manage_options')) {
544 return new \WP_REST_Response(['message' => 'Forbidden. Insufficient permissions.'], 403);
545 }
546
547 // Verify nonce for CSRF protection
548 $nonce = $request->get_header('X-WP-Nonce');
549
550 if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
551 return new \WP_REST_Response(['message' => 'Invalid security token. Please refresh the page and try again.'], 403);
552 }
553
554
555 $params = $request->get_params();
556
557 // Safely extract and sanitize incoming params to avoid undefined index notices
558 $params = is_array($params) ? $params : [];
559 $user_email = isset($params['email']) ? sanitize_email($params['email']) : '';
560 $user_name = isset($params['name']) ? sanitize_text_field($params['name']) : '';
561 $user_rating = isset($params['rating']) ? intval($params['rating']) : 0;
562 $user_msg = isset($params['message']) ? sanitize_textarea_field($params['message']) : '';
563
564 // Validate rating is within acceptable range
565 if ($user_rating < 1 || $user_rating > 5) {
566 return new \WP_REST_Response(['message' => 'Invalid rating value. Must be between 1 and 5.'], 400);
567 }
568
569 // Prevent submissions with empty/invalid user data (prevents N/A spam)
570 if (empty($user_email) || !is_email($user_email)) {
571 return new \WP_REST_Response(['message' => 'Valid email address is required.'], 400);
572 }
573
574 if (empty($user_name) || strlen(trim($user_name)) < 2) {
575 return new \WP_REST_Response(['message' => 'Valid name is required.'], 400);
576 }
577
578 // If the payload is completely empty, ignore to prevent blank/spam emails
579 $has_meaningful_input = false;
580
581 if ($user_rating > 0) {
582 if ($user_rating < 5) {
583 // description required
584 if (trim($user_msg) !== '') {
585 $has_meaningful_input = true;
586 }
587 } else {
588 // rating is 5, description not required
589 $has_meaningful_input = true;
590 }
591 }
592
593 if (!$has_meaningful_input) {
594 return new \WP_REST_Response(['message' => 'No feedback content provided; ignored.'], 200);
595 }
596
597 // Prevent accidental duplicate submissions (double-clicks, quick retries)
598 $payload_hash = md5(json_encode([$user_email, $user_name, $user_rating, $user_msg]));
599 if (get_transient('embedpress_feedback_dupe_' . $payload_hash)) {
600 return new \WP_REST_Response(['message' => 'Duplicate feedback detected; already processed.'], 200);
601 }
602 set_transient('embedpress_feedback_dupe_' . $payload_hash, 1, 5 * MINUTE_IN_SECONDS);
603
604 $email_html = $user_email ? '<a href="mailto:' . esc_attr($user_email) . '">' . esc_html($user_email) . '</a>' : 'N/A';
605 $rating_html = $user_rating ? esc_html($user_rating) . ' ⭐️' : 'N/A';
606 $message_html = $user_msg !== '' ? nl2br(esc_html($user_msg)) : 'N/A';
607
608
609 $site_name = get_bloginfo('name');
610 $site_url = get_site_url();
611 $admin_email = get_option('admin_email');
612 $wp_version = get_bloginfo('version');
613
614 $admin_user = get_user_by('ID', 1);
615 if ($admin_user) {
616 $first_name = get_user_meta($admin_user->ID, 'first_name', true);
617 $last_name = get_user_meta($admin_user->ID, 'last_name', true);
618
619 $admin_full_name = trim("$first_name $last_name");
620
621 // Fallback to display name if full name is not set
622 if (empty($admin_full_name)) {
623 $admin_full_name = $admin_user->display_name;
624 }
625 } else {
626 $admin_full_name = 'Unknown';
627 }
628
629 $to = 'akash@wpdeveloper.com, rasel@wpdeveloper.com, nahid@wpdeveloper.com, md-nahid-hasan@wpdeveloper.com'; // Replace with the recipient's email
630 $subject = '[IMPORTANT] New feedback received from an EmbedPress user.';
631
632 // HTML Email Template
633 $message = '<html><body style="font-family: Arial, sans-serif; padding: 20px;">';
634 $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;">';
635 $message .= '<div style="text-align: center; padding-bottom: 20px; border-bottom: 1px solid #ddd">';
636 $message .= '<img src="https://embedpress.com/wp-content/uploads/2025/03/logo.png" alt="EmbedPress" style="max-width: 150px;">';
637 $message .= '</div>';
638 $message .= '<h2 style="font-family: system-ui; color: #333; text-align: center;">Feedback Overview</h2>';
639 $message .= '<table style="font-family: system-ui; width: 100%; border-collapse: collapse; border: 1px solid #ddd">';
640
641 // Email
642 $message .= '<tr><td style="padding: 10px; font-weight: bold; width: 100px; border-bottom: 1px solid #ddd;">Email :</td>';
643 $message .= '<td style="padding: 10px; border-bottom: 1px solid #ddd;">' . $email_html . '</td></tr>';
644
645 // Rating
646 $message .= '<tr><td style="padding: 10px; font-weight: bold; width: 100px; border-bottom: 1px solid #ddd;">Rating :</td>';
647 $message .= '<td style="padding: 10px; border-bottom: 1px solid #ddd;">' . $rating_html . '</td></tr>';
648
649 // User
650 $message .= '<tr><td style="padding: 10px; font-weight: bold; width: 100px; border-bottom: 1px solid #ddd;">Name :</td>';
651 $message .= '<td style="padding: 10px; border-bottom: 1px solid #ddd; font-weight: 500;">' . esc_html($admin_full_name) . '</td></tr>';
652
653 // Pack
654 $message .= '<tr><td style="padding: 10px; font-weight: bold; width: 100px; border-bottom: 1px solid #ddd;">Site Url :</td>';
655 $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>';
656
657 // Feedback
658 $message .= '<tr><td style="padding: 10px; font-weight: bold; width: 100px; display: flex;">Feedback :</td>';
659 $message .= '<td style="padding: 10px;">' . $message_html . '</td></tr>';
660
661 $message .= '</table>';
662 $message .= '</div></body></html>';
663
664 $headers = [
665 'Content-Type: text/html; charset=UTF-8',
666 'From: ' . esc_html($site_name) . ' <' . sanitize_email($admin_email) . '>'
667 ];
668 if ($user_email) {
669 $reply_to = $user_name ? esc_html($user_name) . ' <' . sanitize_email($user_email) . '>' : sanitize_email($user_email);
670 $headers[] = 'Reply-To: ' . $reply_to;
671 }
672
673 // Send the email
674 $sent = wp_mail($to, $subject, $message, $headers);
675
676 if ($sent) {
677 update_option('embedpress_feedback_submited', true);
678
679 return new \WP_REST_Response(['message' => 'Email sent successfully!'], 200);
680 } else {
681 // Retrieve last error
682 $last_error = get_transient('embedpress_last_mail_error');
683 $error_message = 'Failed to send email.';
684 if ($last_error instanceof \WP_Error) {
685 $error_message = $last_error->get_error_message();
686 }
687
688 return new \WP_REST_Response([
689 'message' => $error_message
690 ], 422); // using 422 instead of 500
691 }
692 }
693
694 public function capture_mail_error($wp_error)
695 {
696 if ($wp_error instanceof \WP_Error) {
697 set_transient('embedpress_last_mail_error', $wp_error, 60);
698 }
699 }
700
701 /**
702 * Get user IP address safely
703 *
704 * @return string
705 */
706 private function get_user_ip()
707 {
708 $ip_keys = [
709 'HTTP_CF_CONNECTING_IP', // CloudFlare
710 'HTTP_X_FORWARDED_FOR',
711 'HTTP_X_REAL_IP',
712 'REMOTE_ADDR'
713 ];
714
715 foreach ($ip_keys as $key) {
716 if (!empty($_SERVER[$key])) {
717 $ip = sanitize_text_field(wp_unslash($_SERVER[$key]));
718 // Handle comma-separated IPs (from proxies)
719 if (strpos($ip, ',') !== false) {
720 $ip = trim(explode(',', $ip)[0]);
721 }
722 if (filter_var($ip, FILTER_VALIDATE_IP)) {
723 return $ip;
724 }
725 }
726 }
727
728 return '0.0.0.0';
729 }
730
731 /**
732 * Permission callback for feedback endpoint
733 *
734 * We return true here to allow the request to reach our handler,
735 * where we perform detailed authentication and authorization checks
736 * with custom error messages.
737 *
738 * @return bool
739 */
740 public function feedback_permission_callback()
741 {
742 // Return true to allow request to reach handler
743 // Actual auth checks are done in send_user_feedback_email()
744 return true;
745 }
746
747
748 public function register_feedback_email_endpoint()
749 {
750 register_rest_route('embedpress/v1', '/send-feedback', [
751 'methods' => 'POST',
752 'callback' => [$this, 'send_user_feedback_email'],
753 'permission_callback' => [$this, 'feedback_permission_callback'],
754 'args' => [
755 'email' => [
756 'required' => false,
757 'type' => 'string',
758 'sanitize_callback' => 'sanitize_email',
759 ],
760 'name' => [
761 'required' => false,
762 'type' => 'string',
763 'sanitize_callback' => 'sanitize_text_field',
764 ],
765 'rating' => [
766 'required' => true,
767 'type' => 'integer',
768 'validate_callback' => function($param) {
769 return is_numeric($param) && $param >= 1 && $param <= 5;
770 }
771 ],
772 'message' => [
773 'required' => false,
774 'type' => 'string',
775 'sanitize_callback' => 'sanitize_textarea_field',
776 ],
777 ]
778 ]);
779 }
780
781
782
783 /**
784 * Callback called right after the plugin has been activated.
785 *
786 * @return void
787 * @since 1.0.0
788 * @static
789 *
790 */
791 public static function onPluginActivationCallback()
792 {
793 $dirname = wp_get_upload_dir()['basedir'] . '/embedpress';
794 if (!file_exists($dirname)) {
795 mkdir($dirname, 0777);
796 }
797 flush_rewrite_rules();
798 embedpress_schedule_cache_cleanup();
799
800 // Trigger the setup wizard redirect only on a true fresh install. The
801 // 'embedpress_install_type' marker is set in EmbedpressSettings::__construct
802 // before any default settings are written, so it reliably distinguishes
803 // first-ever installs from re-activations after a plugin update.
804 $pro_active = apply_filters( 'embedpress/is_allow_rander', false );
805 $install_type = get_option( 'embedpress_install_type', false );
806
807 if ( ! $pro_active && $install_type !== 'existing' ) {
808 $settings = get_option( EMBEDPRESS_PLG_NAME, [] );
809 $settings['need_first_time_redirect'] = true;
810 update_option( EMBEDPRESS_PLG_NAME, $settings );
811
812 // Clear any previous redirect done flag
813 delete_option( 'embedpress_activation_redirect_done' );
814
815 }
816
817 // "What's New" modal: show ONLY after an update from an older version,
818 // never on a brand-new install (a first-time user has nothing "new").
819 //
820 // Trust the 'embedpress_install_type' marker here — do NOT re-derive
821 // "prior data" from EMBEDPRESS_PLG_NAME. EmbedpressSettings::__construct
822 // runs during initialize() EARLIER in this same activation request and
823 // (a) computes install_type before writing anything, then (b) seeds
824 // default settings (turn_off_rating_help / turn_off_milestone / …) into
825 // EMBEDPRESS_PLG_NAME. So by the time this callback runs that option is
826 // ALWAYS non-empty — the old !empty() check made every fresh install look
827 // "existing" and wrongly fired the modal on top of the onboarding wizard.
828 //
829 // On a fresh install we stamp the current version as already seen so both
830 // the modal and the menu "New" badge stay suppressed; on an update we
831 // leave the markers unset and the modal fires once.
832 $seen_option = \EmbedPress\Includes\Classes\FeaturePreviewModal::SEEN_VERSION_OPTION;
833 if ( get_option( $seen_option, false ) === false ) {
834 if ( $install_type === 'existing' ) {
835 // Known existing user (update / re-activate) → let the modal fire once.
836 $is_fresh_install = false;
837 } elseif ( $install_type === 'fresh' ) {
838 // Known fresh install → suppress.
839 $is_fresh_install = true;
840 } else {
841 // install_type not computed this request (e.g. WP-CLI activation,
842 // where EmbedpressSettings didn't run and so has NOT polluted
843 // EMBEDPRESS_PLG_NAME) → direct data detection is reliable here.
844 $is_fresh_install = ! (
845 (bool) get_option( 'embedpress_elements_updated', false )
846 || ! empty( get_option( EMBEDPRESS_PLG_NAME, [] ) )
847 || ! empty( get_option( EMBEDPRESS_PLG_NAME . ':elements', [] ) )
848 );
849 }
850
851 if ( $is_fresh_install ) {
852 $current_version = defined( 'EMBEDPRESS_VERSION' ) ? EMBEDPRESS_VERSION : '0.0.0';
853 // Suppress the modal (seen) AND the menu "New" badge (opened) — a
854 // first-time user has nothing "new" to be announced.
855 update_option( $seen_option, $current_version );
856 update_option(
857 \EmbedPress\Includes\Classes\FeaturePreviewModal::OPENED_VERSION_OPTION,
858 $current_version
859 );
860 }
861 }
862 }
863
864 /**
865 * Callback called right after the plugin has been deactivated.
866 *
867 * @return void
868 * @since 1.0.0
869 * @static
870 *
871 */
872 public static function onPluginDeactivationCallback()
873 {
874 flush_rewrite_rules();
875 embedpress_cache_cleanup();
876 $timestamp = wp_next_scheduled('embedpress_backup_cleanup_action');
877 if ($timestamp) {
878 wp_unschedule_event($timestamp, 'embedpress_backup_cleanup_action');
879 }
880 }
881
882 /**
883 * Method that retrieves all additional service providers defined in the ~<plugin_root_path>/providers.php file.
884 *
885 * @return array
886 * @since 1.0.0
887 * @static
888 *
889 */
890 public static function getAdditionalServiceProviders()
891 {
892 $additionalProvidersFilePath = EMBEDPRESS_PATH_BASE . 'providers.php';
893 if (file_exists($additionalProvidersFilePath)) {
894 include $additionalProvidersFilePath;
895
896 if (isset($additionalServiceProviders)) {
897 return apply_filters('embedpress_additional_service_providers', $additionalServiceProviders);
898 }
899 }
900
901 return apply_filters('embedpress_additional_service_providers', []);
902 }
903
904 /**
905 * Method that checks if an embed of a given service provider can be responsive.
906 *
907 * @param string $serviceProviderAlias The service's slug.
908 *
909 * @return boolean
910 * @since 1.0.0
911 * @static
912 *
913 */
914 public static function canServiceProviderBeResponsive($serviceProviderAlias)
915 {
916 return in_array($serviceProviderAlias, [
917 "dailymotion",
918 "kickstarter",
919 "rutube",
920 "ted",
921 "vimeo",
922 "youtube",
923 "ustream",
924 "google-docs",
925 "animatron",
926 "amcharts",
927 "on-aol-com",
928 "animoto",
929 "videojug",
930 'issuu',
931 ]);
932 }
933
934 /**
935 * Method that retrieves the plugin settings defined by the user.
936 *
937 * @return object
938 * @since 1.0.0
939 * @static
940 *
941 */
942 public static function getSettings()
943 {
944 // Fetch settings from the database
945 $settings = get_option(EMBEDPRESS_PLG_NAME);
946
947 // If the settings are not an array (it might return false), initialize as an empty array
948 if (!is_array($settings)) {
949 $settings = [];
950 }
951
952 // Default values if settings are missing
953 if (!isset($settings['enablePluginInAdmin'])) {
954 $settings['enablePluginInAdmin'] = true;
955 }
956
957 if (!isset($settings['enablePluginInFront'])) {
958 $settings['enablePluginInFront'] = true;
959 }
960
961 if (!isset($settings['enableGlobalEmbedResize'])) {
962 $settings['enableGlobalEmbedResize'] = false;
963 }
964
965 if (!isset($settings['enableEmbedResizeHeight'])) {
966 $settings['enableEmbedResizeHeight'] = 550; // old 552
967 }
968
969 if (!isset($settings['enableEmbedResizeWidth'])) {
970 $settings['enableEmbedResizeWidth'] = 600; // old 652
971 }
972
973 return (object) $settings;
974 }
975
976 /**
977 * Retrieve all registered plugins.
978 *
979 * @return array
980 * @since 1.4.0
981 * @static
982 *
983 */
984 public static function getPlugins()
985 {
986 return self::$plugins;
987 }
988
989 /**
990 * Handle links displayed below the plugin name in the WordPress Installed Plugins page.
991 *
992 * @return array
993 * @since 1.4.0
994 * @static
995 *
996 */
997 public static function handleActionLinks($links, $file)
998 {
999 $settingsLink = '<a href="' . admin_url('admin.php?page=embedpress') . '" aria-label="' . __(
1000 'Open settings page',
1001 'embedpress'
1002 ) . '">' . __('Settings', 'embedpress') . '</a>';
1003
1004 array_unshift($links, $settingsLink);
1005 if (!apply_filters('embedpress/is_allow_rander', false)) {
1006 $links[] = '<a href="https://wpdeveloper.com/in/upgrade-embedpress" target="_blank" class="embedpress-go-pro-action" style="color: green">' . __('Go Pro', 'embedpress') . '</a>';
1007 }
1008 return $links;
1009 }
1010
1011
1012 /**
1013 * Method that ensures the API's url are whitelisted to WordPress external requests.
1014 *
1015 * @param boolean $isAllowed
1016 * @param string $host
1017 * @param string $url
1018 *
1019 * @return boolean
1020 * @since 1.4.0
1021 * @static
1022 *
1023 */
1024 public static function allowApiHost($isAllowed, $host, $url)
1025 {
1026 if ($host === EMBEDPRESS_LICENSES_API_HOST) {
1027 $isAllowed = true;
1028 }
1029
1030 return $isAllowed;
1031 }
1032
1033 public function extended_mime_types($mimes)
1034 {
1035 $mimes['ppsx'] = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
1036 return $mimes;
1037 }
1038 }
1039