PluginProbe
Property Hive / 2.3.0
Property Hive v2.3.0
2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 1.4.61 1.4.62 All 260 releases
propertyhive / propertyhive.php

propertyhive.php in Property Hive 2.3.0, at propertyhive.php

720 lines 33.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Property Hive
4 * Plugin URI: https://wordpress.org/plugins/propertyhive/
5 * Description: Property Hive has everything you need to build estate agency websites
6 * Version: 2.3.0
7 * Author: PropertyHive
8 * Author URI: https://wp-property-hive.com
9 * License: GPLv3
10 * License URI: https://www.gnu.org/licenses/gpl-3.0.html
11 * Requires at least: 5.6
12 * Tested up to: 7.1
13 *
14 * Text Domain: propertyhive
15 *
16 * @package PropertyHive
17 * @category Core
18 * @author PropertyHive
19 */
20
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit; // Exit if accessed directly
23 }
24
25 if ( ! class_exists( 'PropertyHive' ) )
26 {
27 /**
28 * Main PropertyHive Class
29 *
30 * @class PropertyHive
31 * @version 2.3.0
32 */
33 final class PropertyHive {
34
35 /**
36 * @var string
37 */
38 public $version = '2.3.0';
39
40 /**
41 * @var PropertyHive The single instance of the class
42 */
43 protected static $_instance = null;
44
45 /**
46 * Query instance.
47 *
48 * @var PH_Query
49 */
50 public $query = null;
51
52 /**
53 * REST API instance.
54 *
55 * @var PH_Rest_Api
56 */
57 public $rest_api = null;
58
59 /**
60 * Email instance.
61 *
62 * @var PH_Emails
63 */
64 public $email = null;
65
66 /**
67 * License instance.
68 *
69 * @var PH_Licenses
70 */
71 public $license = null;
72
73 /**
74 * Countries instance.
75 *
76 * @var PH_Countries
77 */
78 public $countries = null;
79
80 /**
81 * Main PropertyHive Instance
82 *
83 * Ensures only one instance of PropertyHive is loaded or can be loaded.
84 *
85 * @static
86 * @return PropertyHive - Main instance
87 */
88 public static function instance()
89 {
90 if ( is_null( self::$_instance ) )
91 {
92 self::$_instance = new self();
93 }
94 return self::$_instance;
95 }
96
97 /**
98 * Cloning is forbidden.
99 *
100 * @since 1.0.0
101 */
102 public function __clone() {
103 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'propertyhive' ), '1.0.0' );
104 }
105
106 /**
107 * Unserializing instances of this class is forbidden.
108 *
109 * @since 1.0.0
110 */
111 public function __wakeup() {
112 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'propertyhive' ), '1.0.0' );
113 }
114
115 /**
116 * PropertyHive Constructor.
117 * @access public
118 * @return PropertyHive
119 */
120 public function __construct()
121 {
122 // Auto-load classes on demand
123 if ( function_exists( "__autoload" ) ) {
124 spl_autoload_register( "__autoload" );
125 }
126
127 spl_autoload_register( array( $this, 'autoload' ) );
128
129 // Define constants
130 $this->define_constants();
131
132 // Include required files
133 $this->includes();
134
135 // Hooks
136 add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), array( $this, 'action_links' ) );
137 add_filter( 'propertyhive_departments', array( $this, 'setup_custom_departments' ) );
138 //add_action( 'widgets_init', array( $this, 'include_widgets' ) );
139 add_action( 'init', array( $this, 'init' ), 0 );
140 add_action( 'init', array( $this, 'include_template_functions' ) );
141 add_action( 'init', array( $this, 'unsubscribe_contact' ), 0 );
142 add_action( 'init', array( 'PH_Shortcodes', 'init' ) );
143 add_action( 'rest_api_init', array( $this, 'rest_api_includes' ) );
144 add_action( 'after_setup_theme', array( $this, 'setup_environment' ) );
145 add_action( 'wp', array( $this, 'set_cache_constants' ) );
146 add_action( 'wp_update_comment_count', array( $this, 'exclude_notes_from_comment_count' ) );
147
148 // Ensure Template Assistant add on is deactivated now the code is merged into core
149 add_action('plugins_loaded', function () {
150 propertyhive_deactivate_template_assistant();
151 }, 1);
152
153 // Loaded action
154 do_action( 'propertyhive_loaded' );
155 }
156
157 public function set_cache_constants()
158 {
159 $page_ids = array_filter( array( ph_get_page_id( 'my_account' ) ) );
160
161 if ( !empty($page_ids) && is_page( $page_ids ) )
162 {
163 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress/cache-plugin integration constant DONOTCACHEPAGE; cache integrations depend on this established global constant name.
164 if ( !defined('DONOTCACHEPAGE') ) { define('DONOTCACHEPAGE', TRUE); }
165 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress/cache-plugin integration constant DONOTCACHEOBJECT; cache integrations depend on this established global constant name.
166 if ( !defined('DONOTCACHEOBJECT') ) { define('DONOTCACHEOBJECT', TRUE); }
167 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress/cache-plugin integration constant DONOTCACHEDB; cache integrations depend on this established global constant name.
168 if ( !defined('DONOTCACHEDB') ) { define('DONOTCACHEDB', TRUE); }
169 }
170 }
171
172 public function setup_custom_departments( $departments )
173 {
174 $custom_departments = ph_get_custom_departments();
175
176 foreach ( $custom_departments as $key => $custom_department )
177 {
178 $departments[$key] = $custom_department['name'];
179 }
180
181 return $departments;
182 }
183
184 public function exclude_notes_from_comment_count($post_id) {
185 global $wpdb;
186 $post_id = (int)$post_id;
187 if ( !$post_id ) {
188 return false;
189 }
190 if ( !$post = get_post($post_id) ) {
191 return false;
192 }
193
194 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Recount immediately after comment changes, excluding internal CRM notes; a cached count would be stale at this mutation boundary.
195 $new = (int) $wpdb->get_var( $wpdb->prepare("SELECT COUNT(*)
196 FROM $wpdb->comments
197 WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type != 'propertyhive_note' ", $post_id) );
198 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Core comment_count needs the recalculated non-note count; clean_post_cache immediately below invalidates the affected post.
199 $wpdb->update( $wpdb->posts, array('comment_count' => $new), array('ID' => $post_id) );
200
201 clean_post_cache( $post );
202 }
203
204
205 /**
206 * Show action links on the plugin screen
207 *
208 * @param mixed $links
209 * @return array
210 */
211 public function action_links( $links )
212 {
213 return array_merge( array(
214 '<a href="' . admin_url( 'admin.php?page=ph-settings' ) . '">' . __( 'Settings', 'propertyhive' ) . '</a>',
215 '<a href="' . esc_url( apply_filters( 'propertyhive_features_url', admin_url( 'admin.php?page=ph-settings&tab=features' ) ) ) . '">' . __( 'Features', 'propertyhive' ) . '</a>',
216 '<a href="' . esc_url( apply_filters( 'propertyhive_url', 'https://wp-property-hive.com/', 'propertyhive' ) ) . '" target="_blank">' . __( 'Website', 'propertyhive' ) . '</a>',
217 ), $links );
218 }
219
220 /**
221 * Auto-load PH classes on demand to reduce memory consumption.
222 *
223 * @param mixed $class
224 * @return void
225 */
226 public function autoload( $class )
227 {
228 $path = null;
229 $class = strtolower( $class );
230 $file = 'class-' . str_replace( '_', '-', $class ) . '.php';
231
232 if ( strpos( $class, 'ph_shortcode_' ) === 0 ) {
233 $path = $this->plugin_path() . '/includes/shortcodes/';
234 } elseif ( strpos( $class, 'ph_meta_box' ) === 0 ) {
235 $path = $this->plugin_path() . '/includes/admin/post-types/meta-boxes/';
236 } elseif ( strpos( $class, 'ph_admin' ) === 0 ) {
237 $path = $this->plugin_path() . '/includes/admin/';
238 }
239
240 if ( $path && is_readable( $path . $file ) ) {
241 include_once( $path . $file );
242 return;
243 }
244
245 // Fallback
246 if ( strpos( $class, 'ph_' ) === 0 ) {
247 $path = $this->plugin_path() . '/includes/';
248 }
249
250 if ( $path && is_readable( $path . $file ) ) {
251 include_once( $path . $file );
252 return;
253 }
254 }
255
256 /**
257 * Define PH Constants
258 */
259 private function define_constants()
260 {
261 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Legacy public PH_* constants are consumed by existing themes and add-ons.
262 define( 'PH_PLUGIN_FILE', __FILE__ );
263 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Preserve the existing public version constant used by add-ons.
264 define( 'PH_VERSION', $this->version );
265
266 if ( ! defined( 'PH_TEMPLATE_PATH' ) ) {
267 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Preserve the existing theme-overridable template path constant.
268 define( 'PH_TEMPLATE_PATH', $this->template_path() );
269 }
270 }
271
272 /**
273 * Include required core files used in admin and on the frontend.
274 */
275 private function includes() {
276 include_once( 'includes/ph-core-functions.php' );
277 include_once( 'includes/ph-update-functions.php' );
278 include_once( 'includes/class-ph-install.php' );
279 include_once( 'includes/class-ph-comments.php' );
280 include_once( 'includes/class-ph-emails.php' );
281 include_once( 'includes/class-ph-licenses.php' );
282
283 if ( is_admin() ) {
284 include_once( 'includes/admin/class-ph-admin.php' );
285 include_once( 'includes/class-ph-third-party-contacts.php' );
286 }
287
288 if ( defined( 'DOING_AJAX' ) ) {
289 $this->ajax_includes();
290 }
291
292 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only Elementor editor detection selects frontend includes; it does not change saved data.
293 if ( ! is_admin() || defined( 'DOING_AJAX' ) || ( isset( $_GET['action'] ) && is_string( $_GET['action'] ) && 'elementor' === $_GET['action'] ) ) {
294 $this->frontend_includes();
295 }
296
297 include_once( 'includes/ph-form-functions.php' ); // Form Renderers
298 include_once( 'includes/class-ph-form-handler.php' ); // Form Handlers
299 include_once( 'includes/class-ph-shortcodes.php' ); // Shortcodes class
300
301 include( 'includes/class-ph-query.php' ); // The main query class
302
303 include_once( 'includes/class-ph-post-types.php' ); // Registers post types
304 include_once( 'includes/class-ph-countries.php' ); // Manages interaction with countries and currency
305
306 if ( get_option( 'propertyhive_address_keyword_compare', '=' ) == 'polygon' )
307 {
308 include_once( 'includes/class-ph-address-keyword-polygon.php' ); // Manages getting and caching polygons associated with search terms
309 }
310
311 include_once( 'includes/class-ph-user-contacts.php' ); // Handles keeping contacts and users in sync
312
313 include_once( 'includes/class-ph-avada.php' ); // Avada / Fusion Builder
314 include_once( 'includes/class-ph-bricks-builder.php' ); // Bricks Builder
315 include_once( 'includes/class-ph-divi.php' ); // Divi
316 include_once( 'includes/class-ph-elementor.php' ); // Elementor
317 include_once( 'includes/class-ph-salient.php' ); // Salient / WPBakery
318 include_once( 'includes/class-ph-yoast-seo.php' ); // Yoast SEO
319 include_once( 'includes/class-ph-rank-math.php' ); // Rank Math
320 include_once( 'includes/class-ph-aioseo.php' ); // All In One SEO
321 include_once( 'includes/class-ph-duplicate-post.php' ); // Duplicate Post
322
323 include_once( 'includes/class-ph-search-analytics.php' ); // Search Analytics
324
325 include_once( 'includes/class-ph-additional-fields.php' ); // Additional Fields
326 include_once( 'includes/class-ph-text-substitution.php' ); // Text Substitution
327
328 include_once( 'includes/ph-pro-feature-functions.php' ); // Pro Features
329
330 $this->query = new PH_Query();
331 $this->email = new PH_Emails();
332 $this->license = new PH_Licenses();
333 }
334
335 public function rest_api_includes()
336 {
337 include_once( 'includes/class-ph-rest-api.php' );
338 $this->rest_api = new PH_Rest_Api();
339 }
340
341 /**
342 * Include required ajax files.
343 */
344 public function ajax_includes() {
345 include_once( 'includes/class-ph-ajax.php' ); // Ajax functions for admin and the front-end
346 }
347
348 /**
349 * Include required frontend files.
350 */
351 public function frontend_includes() {
352 include_once( 'includes/ph-template-hooks.php' );
353 include_once( 'includes/class-ph-template-loader.php' ); // Template Loader
354 include_once( 'includes/class-ph-frontend-scripts.php' ); // Frontend Scripts
355 }
356
357 /**
358 * Function used to Init PropertyHive Template Functions - This makes them pluggable by plugins and themes.
359 */
360 public function include_template_functions() {
361 include_once( 'includes/ph-template-functions.php' );
362 }
363
364 /**
365 * Include core widgets
366 */
367 public function include_widgets() {
368 /*include_once( 'includes/abstracts/abstract-ph-widget.php' );
369 include_once( 'includes/widgets/class-ph-widget-properties.php' );*/
370 }
371
372 /**
373 * Contacts may store several comma-separated mailbox addresses.
374 */
375 private function contact_unsubscribe_recipients( $email ) {
376 if ( ! is_string( $email ) || '' === $email ) {
377 return array();
378 }
379 $recipients = array_map( 'trim', explode( ',', $email ) );
380 foreach ( $recipients as $recipient ) {
381 if ( ! is_email( $recipient ) ) {
382 return array();
383 }
384 }
385 return array_values( array_unique( $recipients ) );
386 }
387
388 /**
389 * Build a durable, email-bound unsubscribe link for an existing contact.
390 */
391 public function get_contact_unsubscribe_url( $contact_id ) {
392 if ( ( ! is_int( $contact_id ) && ! is_string( $contact_id ) ) || ! ctype_digit( (string) $contact_id ) ) {
393 return '';
394 }
395 $contact_id = (int) $contact_id;
396 $email = get_post_meta( $contact_id, '_email_address', true );
397 if ( ! $contact_id || 'contact' !== get_post_type( $contact_id ) || ! $this->contact_unsubscribe_recipients( $email ) ) {
398 return '';
399 }
400 return $this->contact_unsubscribe_token_url( 'v2|' . $contact_id, $email );
401 }
402
403 /**
404 * Sign the purpose/version, payload and current email with the site's secret.
405 */
406 private function contact_unsubscribe_token_url( $payload, $email ) {
407 $signature = hash_hmac( 'sha256', 'propertyhive-unsubscribe|' . $payload . '|' . $email, wp_salt( 'auth' ) );
408 return add_query_arg( 'ph_unsubscribe', rawurlencode( base64_encode( $payload . '|' . $signature ) ), site_url( '/' ) );
409 }
410
411 /**
412 * Atomically limit mailbox-verification mail, including concurrent requests.
413 */
414 private function contact_unsubscribe_mail_slot( $contact_id ) {
415 global $wpdb;
416 $key = 'propertyhive_unsubscribe_cooldown_' . $contact_id;
417 $now = time();
418 $previous = get_option( $key, false );
419 if ( false !== $previous ) {
420 if ( ! is_numeric( $previous ) || (int) $previous > $now ) {
421 return false;
422 }
423 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Compare-and-delete the expired lock atomically: delete_option could remove a newer request's lock. Invalidate the option cache immediately below.
424 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s", $key, (string) $previous ) );
425 wp_cache_delete( $key, 'options' );
426 }
427 // INSERT IGNORE must not overwrite another request's newly acquired lock (add_option can update duplicate rows).
428 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- The unique option_name index is the cross-request lock; clear positive and negative option caches immediately after the atomic insert.
429 $acquired = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, %s)", $key, (string) ( $now + 5 * MINUTE_IN_SECONDS ), 'no' ) );
430 wp_cache_delete( $key, 'options' );
431 wp_cache_delete( 'notoptions', 'options' );
432 return 1 === $acquired;
433 }
434
435 private function contact_unsubscribe_result( $message, $status = 200 ) {
436 wp_die( esc_html( $message ), esc_html__( 'Unsubscribe', 'propertyhive' ), array( 'response' => (int) $status ) );
437 }
438
439 /**
440 * Signed links authorize unsubscribe; old links first require mailbox proof.
441 */
442 public function unsubscribe_contact() {
443 if ( ! isset( $_GET['ph_unsubscribe'] ) ) {
444 return;
445 }
446 $invalid = __( 'This unsubscribe link is invalid or has expired.', 'propertyhive' );
447 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.NonceVerification.Recommended -- Bound the raw token's size before decoding; no value is used until signature/mailbox verification below.
448 if ( ! is_string( $_GET['ph_unsubscribe'] ) || strlen( $_GET['ph_unsubscribe'] ) > 512 ) {
449 $this->contact_unsubscribe_result( $invalid, 400 );
450 return;
451 }
452 $encoded_token = sanitize_text_field( wp_unslash( $_GET['ph_unsubscribe'] ) );
453 $decoded = base64_decode( $encoded_token, true );
454 $parts = false !== $decoded ? explode( '|', $decoded ) : array();
455 $legacy = count( $parts ) === 2;
456 $version = $legacy ? 'legacy' : ( isset( $parts[0] ) ? $parts[0] : '' );
457 $id_part = $legacy ? $parts[0] : ( isset( $parts[1] ) ? $parts[1] : '' );
458 $contact_id = ctype_digit( $id_part ) ? (int) $id_part : 0;
459 $email = $contact_id ? get_post_meta( $contact_id, '_email_address', true ) : '';
460 if ( ! $contact_id || 'contact' !== get_post_type( $contact_id ) || ! $this->contact_unsubscribe_recipients( $email ) ) {
461 $this->contact_unsubscribe_result( $invalid, 400 );
462 return;
463 }
464
465 if ( $legacy ) {
466 if ( ! hash_equals( md5( $email ), $parts[1] ) ) {
467 $this->contact_unsubscribe_result( $invalid, 400 );
468 return;
469 }
470 $nonce_action = 'propertyhive-unsubscribe-request-' . $contact_id;
471 if ( isset( $_POST['propertyhive_unsubscribe_confirm'] ) ) {
472 if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' !== $_SERVER['REQUEST_METHOD'] || ! is_string( $_POST['propertyhive_unsubscribe_confirm'] ) || '1' !== $_POST['propertyhive_unsubscribe_confirm'] || ! isset( $_POST['_wpnonce'] ) || ! is_string( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), $nonce_action ) ) {
473 $this->contact_unsubscribe_result( $invalid, 400 );
474 return;
475 }
476 if ( $this->contact_unsubscribe_mail_slot( $contact_id ) ) {
477 // Reserve the cooldown before sending, including delivery failures.
478 $nonce = wp_generate_password( 32, false, false );
479 $expires = time() + HOUR_IN_SECONDS;
480 $verify_key = 'propertyhive_unsubscribe_verify_' . $contact_id . '_' . hash( 'sha256', $nonce );
481 set_transient( $verify_key, 1, HOUR_IN_SECONDS );
482 $url = $this->contact_unsubscribe_token_url( 'v3|' . $contact_id . '|' . $expires . '|' . $nonce, $email );
483 $sent = wp_mail(
484 $this->contact_unsubscribe_recipients( $email ),
485 __( 'Confirm your unsubscribe request', 'propertyhive' ),
486 /* translators: %s: Mailbox verification URL. */
487 sprintf( __( "To confirm your unsubscribe request, open this link within one hour:\n\n%s\n\nIf you did not request this, you can ignore this email.", 'propertyhive' ), $url )
488 );
489 if ( ! $sent ) {
490 delete_transient( $verify_key );
491 }
492 }
493 $this->contact_unsubscribe_result( __( 'Please check your inbox for a confirmation link. If you recently requested one, please allow a few minutes before trying again.', 'propertyhive' ) );
494 return;
495 }
496 $legacy_url = add_query_arg( 'ph_unsubscribe', rawurlencode( $encoded_token ), site_url( '/' ) );
497 $form = '<p>' . esc_html__( 'This older unsubscribe link requires email confirmation. Request a confirmation link to continue.', 'propertyhive' ) . '</p>';
498 $form .= '<form method="post" action="' . esc_url( $legacy_url ) . '"><input type="hidden" name="propertyhive_unsubscribe_confirm" value="1"><input type="hidden" name="_wpnonce" value="' . esc_attr( wp_create_nonce( $nonce_action ) ) . '"><button type="submit">' . esc_html__( 'Send confirmation link', 'propertyhive' ) . '</button></form>';
499 wp_die( wp_kses( $form, array( 'p' => array(), 'form' => array( 'method' => true, 'action' => true ), 'input' => array( 'type' => true, 'name' => true, 'value' => true ), 'button' => array( 'type' => true ) ) ), esc_html__( 'Unsubscribe', 'propertyhive' ), array( 'response' => 200 ) );
500 return;
501 }
502
503 $is_verification = 'v3' === $version && count( $parts ) === 5;
504 if ( ! ( 'v2' === $version && count( $parts ) === 3 ) && ! $is_verification ) {
505 $this->contact_unsubscribe_result( $invalid, 400 );
506 return;
507 }
508 $signature = array_pop( $parts );
509 $expected = hash_hmac( 'sha256', 'propertyhive-unsubscribe|' . implode( '|', $parts ) . '|' . $email, wp_salt( 'auth' ) );
510 if ( ! hash_equals( $expected, $signature ) || ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) {
511 $this->contact_unsubscribe_result( $invalid, 400 );
512 return;
513 }
514 if ( $is_verification ) {
515 $verify_key = 'propertyhive_unsubscribe_verify_' . $contact_id . '_' . hash( 'sha256', $parts[3] );
516 if ( ! ctype_digit( $parts[2] ) || (int) $parts[2] <= time() || ! get_transient( $verify_key ) || ! delete_transient( $verify_key ) ) {
517 $this->contact_unsubscribe_result( $invalid, 400 );
518 return;
519 }
520 }
521
522 $methods = get_post_meta( $contact_id, '_forbidden_contact_methods', true );
523 $methods = is_array( $methods ) ? $methods : array();
524 if ( ! in_array( 'email', $methods, true ) ) {
525 $methods[] = 'email';
526 update_post_meta( $contact_id, '_forbidden_contact_methods', wp_slash( array_unique( $methods ) ) );
527 wp_insert_comment( array(
528 'comment_post_ID' => $contact_id,
529 'comment_author' => 'Property Hive',
530 'comment_author_email' => 'propertyhive@noreply.com',
531 'comment_author_url' => '',
532 'comment_date' => gmdate( 'Y-m-d H:i:s' ),
533 'comment_content' => serialize( array( 'note_type' => 'unsubscribe' ) ),
534 'comment_approved' => 1,
535 'comment_type' => 'propertyhive_note',
536 ) );
537 }
538 $this->contact_unsubscribe_result( __( 'You have been unsubscribed successfully. Please allow up to 24 hours for this to take effect.', 'propertyhive' ) );
539 }
540
541 /**
542 * Init PropertyHive when WordPress Initialises.
543 */
544 public function init() {
545 // Before init action
546 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Existing public Property Hive extension hook before_propertyhive_init; changing the established name would detach installed callbacks.
547 do_action( 'before_propertyhive_init' );
548
549 // Set up localisation
550 $this->load_plugin_textdomain();
551
552 // Session class, handles session data for users - can be overwritten if custom handler is needed
553 //$session_class = apply_filters( 'propertyhive_session_handler', 'PH_Session_Handler' );
554
555 // Load class instances
556 //$this->product_factory = new PH_Product_Factory(); // Product Factory to create new product instances
557 $this->countries = new PH_Countries(); // Countries class
558 //$this->integrations = new PH_Integrations(); // Integrations class
559 // $this->session = new $session_class();
560
561 // Email Actions
562 /*$email_actions = array(
563 'propertyhive_low_stock',
564 'propertyhive_no_stock',
565 'propertyhive_product_on_backorder',
566 'propertyhive_order_status_pending_to_processing',
567 'propertyhive_order_status_pending_to_completed',
568 'propertyhive_order_status_pending_to_on-hold',
569 'propertyhive_order_status_failed_to_processing',
570 'propertyhive_order_status_failed_to_completed',
571 'propertyhive_order_status_completed',
572 'propertyhive_new_customer_note',
573 'propertyhive_created_customer'
574 );
575
576 foreach ( $email_actions as $action )
577 add_action( $action, array( $this, 'send_transactional_email' ), 10, 10 );*/
578
579 // Init action
580 do_action( 'propertyhive_init' );
581 }
582
583 /**
584 * Load Localisation files.
585 *
586 * Note: the first-loaded translation file overrides any following ones if the same translation is present
587 */
588 public function load_plugin_textdomain() {
589 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core hook plugin_locale; renaming it would break the core hook contract.
590 $locale = apply_filters( 'plugin_locale', get_locale(), 'propertyhive' );
591
592 // Admin Locale
593 if ( is_admin() ) {
594 load_textdomain( 'propertyhive', WP_LANG_DIR . "/propertyhive/propertyhive-admin-$locale.mo" );
595 load_textdomain( 'propertyhive', dirname( __FILE__ ) . "/i18n/languages/propertyhive-admin-$locale.mo" );
596 }
597
598 // Global + Frontend Locale
599 load_textdomain( 'propertyhive', WP_LANG_DIR . "/propertyhive/propertyhive-$locale.mo" );
600 // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Preserve bundled i18n/languages translations and the plugin_locale override on supported WordPress versions; WordPress.org language packs alone do not cover this legacy custom path.
601 load_plugin_textdomain( 'propertyhive', false, plugin_basename( dirname( __FILE__ ) ) . "/i18n/languages" );
602 }
603
604 /**
605 * Ensure theme and server variable compatibility and setup image sizes..
606 */
607 public function setup_environment() {
608
609 // IIS fallback must preserve encoded URLs and query syntax for WordPress routing.
610 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
611 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This is server-to-server URI compatibility state, not output; validate string shape and remove CR/LF while preserving URL encodings and query delimiters.
612 $php_self = isset( $_SERVER['PHP_SELF'] ) && is_string( $_SERVER['PHP_SELF'] ) ? str_replace( array( "\r", "\n" ), '', wp_unslash( $_SERVER['PHP_SELF'] ) ) : '';
613 $_SERVER['REQUEST_URI'] = substr( $php_self, 1 );
614 if ( isset( $_SERVER['QUERY_STRING'] ) && is_string( $_SERVER['QUERY_STRING'] ) ) {
615 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Preserve the existing query string exactly apart from CR/LF; URL output escaping belongs at its eventual output boundary.
616 $_SERVER['REQUEST_URI'] .= '?' . str_replace( array( "\r", "\n" ), '', wp_unslash( $_SERVER['QUERY_STRING'] ) );
617 }
618 }
619
620 // Legacy NGINX proxy compatibility; only copy syntactically valid IP addresses.
621 if ( ! isset( $_SERVER['REMOTE_ADDR'] ) && isset( $_SERVER['HTTP_REMOTE_ADDR'] ) && is_string( $_SERVER['HTTP_REMOTE_ADDR'] ) ) {
622 $remote_address = sanitize_text_field( wp_unslash( $_SERVER['HTTP_REMOTE_ADDR'] ) );
623 if ( filter_var( $remote_address, FILTER_VALIDATE_IP ) ) {
624 $_SERVER['REMOTE_ADDR'] = $remote_address;
625 }
626 }
627
628 if ( ! isset( $_SERVER['HTTPS'] ) && isset( $_SERVER['HTTP_HTTPS'] ) && is_string( $_SERVER['HTTP_HTTPS'] ) ) {
629 $https = sanitize_text_field( wp_unslash( $_SERVER['HTTP_HTTPS'] ) );
630 if ( '' !== $https && '0' !== $https ) {
631 $_SERVER['HTTPS'] = $https;
632 }
633 }
634
635 // Support hosts which use HTTP_X_FORWARDED_PROTO instead of HTTPS.
636 if ( ! isset( $_SERVER['HTTPS'] ) && isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
637 $_SERVER['HTTPS'] = '1';
638 }
639 }
640
641 /** Helper functions ******************************************************/
642
643 /**
644 * Get the plugin url.
645 *
646 * @return string
647 */
648 public function plugin_url() {
649 return untrailingslashit( plugins_url( '/', __FILE__ ) );
650 }
651
652 /**
653 * Get the plugin path.
654 *
655 * @return string
656 */
657 public function plugin_path() {
658 return untrailingslashit( plugin_dir_path( __FILE__ ) );
659 }
660
661 /**
662 * Get the template path.
663 *
664 * @return string
665 */
666 public function template_path() {
667 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy public template path filter; themes and extensions can customize the plugin template directory through this exact hook.
668 return apply_filters( 'PH_TEMPLATE_PATH', 'propertyhive/' );
669 }
670
671 /**
672 * Get Ajax URL.
673 *
674 * @return string
675 */
676 public function ajax_url() {
677 return admin_url( 'admin-ajax.php', 'relative' );
678 }
679
680 /**
681 * Return the WC API URL for a given request
682 *
683 * @param mixed $request
684 * @param mixed $ssl (default: null)
685 * @return string
686 */
687 public function api_request_url( $request, $ssl = null ) {
688 if ( is_null( $ssl ) ) {
689 $scheme = wp_parse_url( get_option( 'home' ), PHP_URL_SCHEME );
690 } elseif ( $ssl ) {
691 $scheme = 'https';
692 } else {
693 $scheme = 'http';
694 }
695
696 if ( get_option('permalink_structure') ) {
697 return esc_url_raw( trailingslashit( home_url( '/ph-api/' . $request, $scheme ) ) );
698 } else {
699 return esc_url_raw( add_query_arg( 'ph-api', $request, trailingslashit( home_url( '', $scheme ) ) ) );
700 }
701 }
702
703 }
704
705 }
706
707 /**
708 * Returns the main instance of PH to prevent the need to use globals.
709 *
710 * @since 1.0.0
711 * @return PropertyHive
712 */
713 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public global helper PH; the established callable name is part of the plugin/extension API and must remain stable.
714 function PH() {
715 return PropertyHive::instance();
716 }
717
718 // Global for backwards compatibility.
719 $GLOBALS['propertyhive'] = PH();
720