config = $config; } /** * AJAX hooks for various feed data related functionality * * @since 6.0 */ public static function hooks() { \add_action('wp_ajax_sbi_source_builder_update', array(self::class, 'builder_update')); \add_action('wp_ajax_sbi_source_builder_update_multiple', array(self::class, 'builder_update_multiple')); \add_action('wp_ajax_sbi_source_get_page', array(self::class, 'get_page')); \add_action('wp_ajax_sbi_source_get_featured_post_preview', array(self::class, 'get_featured_post_preview')); \add_action('wp_ajax_sbi_source_get_playlist_post_preview', array(self::class, 'get_playlist_post_preview')); \add_action('admin_init', array(self::class, 'batch_process_legacy_source_queue')); } /** * Used in an AJAX call to update sources based on selections or * input from a user. Makes an API request to add additiona info * about the connected source. * * @since 6.0 */ public static function builder_update() { if (!\check_ajax_referer('sbi_admin_nonce', 'nonce', \false) && !\check_ajax_referer('sby-admin', 'nonce', \false)) { \wp_send_json_error(); } if (!sbi_current_user_can('manage_instagram_feed_options')) { \wp_send_json_error(); } $source_data = array('access_token' => \sanitize_text_field($_POST['access_token']), 'id' => \sanitize_text_field($_POST['id']), 'type' => \sanitize_text_field($_POST['type']), 'username' => isset($_POST['username']) ? \sanitize_text_field($_POST['username']) : ''); $return = sbi_connect_new_account($source_data['access_token'], $source_data['id']); if (empty($return['error'])) { \wp_send_json_success(\Smashballoon\Customizer\SBI_Feed_Builder::get_source_list()); } \wp_send_json_error(array('message' => $return['error'])); } /** * Add our update a source from raw API data. * * @param $source_data * * @return string */ public static function process_connecting_source_data($source_data) { $connected_account = array('id' => $source_data['id'], 'user_id' => $source_data['id'], 'type' => $source_data['type'], 'account_type' => $source_data['type'], 'username' => $source_data['username'], 'access_token' => $source_data['access_token'], 'privilege' => $source_data['privilege']); $single_source = self::update_single_source($connected_account, \false); if (!empty($single_source['error'])) { $message = !empty($single_source['error']['error']['message']) ? \esc_html($single_source['error']['error']['message']) : ''; $code = !empty($single_source['error']['error']['code']) ? \esc_html($single_source['error']['error']['code']) : ''; if (isset($single_source['error']['response']) && \is_wp_error($single_source['error']['response'])) { $response = $single_source['error']['response']; $message = \sprintf(\__('Error connecting to %s.', 'feeds-for-youtube'), $single_source['error']['url']); if (isset($response) && isset($response->errors)) { foreach ($response->errors as $key => $item) { $code = $key; $message .= $item[0]; } } } $message .= ' ' . \__('Directions on how to resolve this issue', 'feeds-for-youtube') . ''; $return_html = '
'; $return_html .= ''; $return_html .= ''; $return_html .= ''; $return_html .= '' . \sprintf(\__('Connection Error: %s ', 'feeds-for-youtube'), $code) . '
' . $message . '
'; $return = array('success' => \false, 'message' => $return_html); return \json_encode($return); } else { global $sb_instagram_posts_manager; $sb_instagram_posts_manager->remove_error('connection'); } $manager = new \SmashBalloon\YoutubeFeed\Vendor\SB_Instagram_Data_Manager(); $manager->update_last_used(); return \json_encode(\Smashballoon\Customizer\SBI_Feed_Builder::get_source_list()); } /** * Used in an AJAX call to update Multiple sources based on selections or * input from a user. Makes an API request to add additiona info * about the connected source. * * @since 6.0 */ public static function builder_update_multiple() { if (!\check_ajax_referer('sbi_admin_nonce', 'nonce', \false) && !\check_ajax_referer('sby-admin', 'nonce', \false)) { \wp_send_json_error(); } if (!sbi_current_user_can('manage_instagram_feed_options')) { \wp_send_json_error(); } if (isset($_POST['sourcesList']) && !empty($_POST['sourcesList']) && \is_array($_POST['sourcesList'])) { foreach ($_POST['sourcesList'] as $single_source) { $source_data = array('access_token' => \sanitize_text_field($single_source['access_token']), 'id' => \sanitize_text_field($single_source['id']), 'type' => \sanitize_text_field($single_source['type']), 'username' => isset($single_source['username']) ? \sanitize_text_field($single_source['username']) : ''); if ($single_source['type'] === 'business') { $source_data['privilege'] = 'tagged'; } if (!empty($single_source['name'])) { $source_data['name'] = \sanitize_text_field($single_source['name']); } self::process_connecting_source_data($source_data); } } echo \json_encode(\Smashballoon\Customizer\SBI_Feed_Builder::get_source_list()); \wp_die(); } /** * Get a list of sources with a limit and offset like a page * * @since 6.0 */ public static function get_page() { if (!\check_ajax_referer('sbi_admin_nonce', 'nonce', \false) && !\check_ajax_referer('sby-admin', 'nonce', \false)) { \wp_send_json_error(); } if (!sbi_current_user_can('manage_instagram_feed_options')) { \wp_send_json_error(); } $args = array('page' => $_POST['page']); $source_data = \Smashballoon\Customizer\SBI_Db::source_query($args); echo \json_encode($source_data); \wp_die(); } /** * Connection URLs are based on the website connecting accounts so that is * configured here and returned * * @param bool $is_settings * * @return array * * @since 6.0 */ public function get_connection_urls($is_settings = \false) { $urls = array(); $admin_url_state = $is_settings ? \admin_url(\sprintf('admin.php?page=%s-settings', $this->config->plugin_slug)) : \admin_url(\sprintf('admin.php?page=%s-feed-builder', $this->config->plugin_slug)); //If the admin_url isn't returned correctly then use a fallback if ($admin_url_state === \sprintf('/wp-admin/admin.php?page=%s-feed-builder', $this->config->plugin_slug) || $admin_url_state === \sprintf('/wp-admin/admin.php?page=%s-feed-builder&tab=configuration', $this->config->plugin_slug)) { $admin_url_state = "http://{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}"; } #$urls['personal'] ='https://connect.smashballoon.com/auth/ig/?wordpress_user=&v=pro&vn=' . SBYVER . '&state='; #$urls['business'] = 'https://connect.smashballoon.com/auth/ig/?wordpress_user=&v=pro&vn=' . SBYVER . '&state='; $urls['personal'] = 'https://connect.smashballoon.com/auth/ig/?wordpress_user=&v=pro&vn=' . SBYVER; $urls['business'] = 'https://connect.smashballoon.com/auth/ig/?wordpress_user=&v=pro&vn=' . SBYVER; $urls['stateURL'] = $admin_url_state; return $urls; } /** * Used as a listener for the account connection process. If * data is returned from the account connection processed it's used * to generate the list of possible sources to chose from. * * @return array|bool * * @since 6.0 */ public static function maybe_source_connection_data() { if (isset($_GET['sbi_access_token']) && isset($_GET['sbi_graph_api'])) { $return = self::retrieve_available_business_accounts(); return $return; } elseif (isset($_GET['sbi_access_token']) && isset($_GET['sbi_account_type'])) { $return = self::retrieve_available_personal_accounts(); return $return; } return \false; } /** * Uses the Instagram Basic Display API to get available personal * accounts * * @return array|bool * * @since 6.0 */ public static function retrieve_available_personal_accounts() { $encryption = new \SmashBalloon\YoutubeFeed\Vendor\SB_Instagram_Data_Encryption(); $return = array('type' => 'personal', 'unconnectedAccounts' => array(), 'matchingExistingAccounts' => array(), 'didQuickUpdate' => \false); $access_token = \sanitize_text_field($_GET['sbi_access_token']); if (empty($access_token)) { return array(); } $user_id = \sanitize_text_field($_GET['sbi_id']); $user_name = \sanitize_text_field($_GET['sbi_username']); $expires_in = (int) $_GET['sbi_expires_in']; $expires_timestamp = \time() + $expires_in; $source_data = array('access_token' => $access_token, 'id' => $user_id, 'user_id' => $user_id, 'type' => 'basic', 'username' => $user_name, 'privilege' => '', 'expires' => \date('Y-m-d H:i:s', $expires_timestamp)); $connection = new \SmashBalloon\YoutubeFeed\Vendor\SB_Instagram_API_Connect($source_data, 'header', array()); $connection->connect(); $header_details = '{}'; $source_data['error'] = ''; if (!$connection->is_wp_error() && !$connection->is_instagram_error()) { $header_details_array = $connection->get_data(); $header_details_array = self::merge_account_details($header_details_array, $source_data); $source_data['username'] = $header_details_array['username']; $header_details = \json_encode($header_details_array); } else { $source_data['error'] = $connection; if ($connection->is_wp_error()) { $page_error = $connection->get_wp_error(); if (!empty($page_error) && isset($page_error['response']->errors)) { $error_message = ''; foreach ($page_error['response']->errors as $key => $item) { $error_message .= $key . ': ' . $item[0] . ' '; } return array('error' => array('code' => 'HTTP Request', 'message' => $error_message, 'details' => $error_message)); } } else { $error = $connection->get_data(); return array('error' => array('code' => $error['error']['code'], 'message' => $error['error']['message'], 'details' => $error['error']['message'])); } } $source_data['info'] = $header_details; $return['unconnectedAccounts'][] = $source_data; $args = array('id' => $user_id); $results = \Smashballoon\Customizer\SBI_Db::source_query($args); $already_connected_as_business_account = isset($results[0]) && $results[0]['account_type'] === 'business'; $matches_existing_personal = isset($results[0]) && $results[0]['account_type'] === 'personal'; if ($already_connected_as_business_account) { $return['matchingExistingAccounts'] = $results[0]; $instagram_account_data = \json_decode($encryption->decrypt($results[0]['info']), \true); $return['matchingExistingAccounts']['avatar'] = isset($instagram_account_data['profile_picture_url']) ? $instagram_account_data['profile_picture_url'] : \false; $return['notice'] = \__('The Instagram account you are logged into is already connected as a "business" account. Remove the business account if you\'d like to connect as a basic account instead (not recommended).', 'feeds-for-youtube'); } elseif ($matches_existing_personal) { $return['matchingExistingAccounts'] = $results[0]; self::update_or_insert($source_data); $return['notice'] = ''; $return['didQuickUpdate'] = \true; } else { self::update_or_insert($source_data); $return['didQuickUpdate'] = \true; } return $return; } /** * Uses the Facebook API to retrieve a list of business accounts * * @return array|bool * * @since 6.0 */ public static function retrieve_available_business_accounts() { $return = array('type' => 'business', 'unconnectedAccounts' => array(), 'matchingExistingAccounts' => array(), 'didQuickUpdate' => \false); $access_token = sbi_maybe_clean(\urldecode($_GET['sbi_access_token'])); if (empty($access_token)) { return array(); } $url = 'https://graph.facebook.com/me/accounts?fields=instagram_business_account,access_token&limit=500&access_token=' . $access_token; $args = array('timeout' => 60); $result = \wp_remote_get($url, $args); $pages_data = '{}'; if (!\is_wp_error($result)) { $pages_data = $result['body']; } else { $page_error = $result; } if (isset($page_error) && isset($page_error->errors)) { $error_message = ''; foreach ($page_error->errors as $key => $item) { $error_message .= $key . ': ' . $item[0] . ' '; } return array('error' => array('code' => 'HTTP Request', 'message' => \__('Your server could not complete a remote request to Facebook\'s API. Your host may be blocking access or there may be a problem with your server.', 'feeds-for-youtube'), 'details' => $error_message)); } $pages_data_arr = \json_decode($pages_data, \true); if (empty($pages_data_arr['data'])) { return array('error' => array('code' => 'No Accounts Found', 'message' => \__('Couldn\'t find Business Profile', 'feeds-for-youtube'), 'details' => \sprintf(\__('Uh oh. It looks like this Facebook account is not currently connected to an Instagram Business profile. Please check that you are logged into the %1$sFacebook account%2$s in this browser which is associated with your Instagram Business Profile.', 'feeds-for-youtube'), '', ''))); } $user_url = 'https://graph.facebook.com/me?fields=name,id,picture&access_token=' . $access_token; $args = array('timeout' => 60); $result = \wp_remote_get($user_url, $args); if (!\is_wp_error($result)) { $user_data = $result['body']; $user_data_arr = \json_decode($user_data, \true); $return['user'] = $user_data_arr; } $return['numFound'] = \count($pages_data_arr['data']); foreach ($pages_data_arr['data'] as $page_data) { if (isset($page_data['instagram_business_account'])) { $instagram_business_id = $page_data['instagram_business_account']['id']; $page_access_token = isset($page_data['access_token']) ? $page_data['access_token'] : ''; $instagram_account_url = 'https://graph.facebook.com/' . $instagram_business_id . '?fields=name,username,profile_picture_url&access_token=' . $access_token; $args = array('timeout' => 60); $result = \wp_remote_get($instagram_account_url, $args); if (!\is_wp_error($result)) { $instagram_account_info = $result['body']; $instagram_account_data = \json_decode($instagram_account_info, \true); $instagram_biz_img = isset($instagram_account_data['profile_picture_url']) ? $instagram_account_data['profile_picture_url'] : \false; $source_data = array('access_token' => $access_token, 'id' => $instagram_business_id, 'user_id' => $instagram_business_id, 'type' => 'business', 'username' => $instagram_account_data['username'], 'avatar' => $instagram_biz_img, 'privilege' => 'tagged'); $source_data['info'] = \json_encode($instagram_account_data); $return['unconnectedAccounts'][] = $source_data; $args = array('id' => $instagram_business_id); $results = \Smashballoon\Customizer\SBI_Db::source_query($args); $already_connected_as_business_account = isset($results[0]) && $results[0]['account_type'] === 'business'; $matches_existing_personal = isset($results[0]) && $results[0]['account_type'] === 'personal'; if ($already_connected_as_business_account) { self::update_or_insert($source_data); } elseif ($matches_existing_personal && $return['numFound'] === 1) { $return['didQuickUpdate'] = \true; self::update_or_insert($source_data); } } else { $page_error = $result; } } } if (empty($return['unconnectedAccounts'])) { return array('error' => array('code' => 'No Accounts Found', 'message' => \__('Couldn\'t find Business Profile', 'feeds-for-youtube'), 'details' => \sprintf(\__('Uh oh. It looks like this Facebook account is not currently connected to an Instagram Business profile. Please check that you are logged into the %1$sFacebook account%2$s in this browser which is associated with your Instagram Business Profile. If you are, in fact, logged-in to the correct account please make sure you have Instagram accounts connected with your Facebook account by following %3$sthis FAQ%4$s', 'feeds-for-youtube'), '', '', '', ''))); } return $return; } /** * Used to update or insert connected accounts (sources) * * @param array $source_data * * @return bool * * @since 6.0 */ public static function update_or_insert($source_data) { if (!isset($source_data['id'])) { return \false; } if (isset($source_data['info'])) { // data from an API request related to the source is saved as a JSON string if (\is_object($source_data['info']) || \is_array($source_data['info'])) { $source_data['info'] = \json_encode($source_data['info']); } } if (self::exists_in_database($source_data)) { $source_data['last_updated'] = \date('Y-m-d H:i:s'); self::update($source_data, \false); } else { if (!isset($source_data['access_token'])) { return \false; } self::insert($source_data); } return \true; } /** * Whether or not the source exists in the database * * @param array $args * * @return bool * * @since 6.0 */ public static function exists_in_database($args) { $results = \Smashballoon\Customizer\SBI_Db::source_query($args); return isset($results[0]); } /** * Add a new source as a row in the sbi_sources table * * @param array $source_data * * @return false|int * * @since 6.0 */ public static function insert($source_data) { if (isset($source_data['name'])) { $source_data['username'] = $source_data['name']; } $data = $source_data; return \Smashballoon\Customizer\SBI_Db::source_insert($data); } /** * Update info in rows that match the source data * * @param array $source_data * * @return false|int * * @since 6.0 */ public static function update($source_data, $where_privilige = \true) { $where = array('id' => $source_data['id']); unset($source_data['id']); if ($where_privilige && isset($source_data['privilege'])) { $where['privilege'] = $source_data['privilege']; } // usernames are more common in the other plugins so // that is the name of the column that is used as the // page or group "name" data if (isset($source_data['name'])) { $source_data['username'] = $source_data['name']; } $data = $source_data; return \Smashballoon\Customizer\SBI_Db::source_update($data, $where); } /** * Creates a queue of connected accounts that need to be added to * the sources table * * @since 6.0 */ public static function set_legacy_source_queue() { $sbi_statuses_option = \get_option('sbi_statuses', array()); $options = \get_option('sb_instagram_settings', array()); $connected_accounts = isset($options['connected_accounts']) ? $options['connected_accounts'] : array(); $sbi_statuses_option['legacy_source_queue'] = \array_chunk(\array_keys($connected_accounts), self::BATCH_SIZE); \update_option('sbi_statuses', $sbi_statuses_option); return $sbi_statuses_option['legacy_source_queue']; } /** * Whether or not there are still sources in the queue and * this isn't disabled * * @return bool * * @since 6.0 */ public static function should_do_source_updates() { $sbi_statuses_option = \get_option('sbi_statuses', array()); $should_do_source_updates = isset($sbi_statuses_option['legacy_source_queue']) ? !empty($sbi_statuses_option['legacy_source_queue']) : \false; return \apply_filters('should_do_source_updates', $should_do_source_updates); } /** * Processes one set of connected accounts * * @since 6.0 */ public static function batch_process_legacy_source_queue() { if (!self::should_do_source_updates()) { return; } $sbi_statuses_option = \get_option('sbi_statuses', array()); $batch = \array_shift($sbi_statuses_option['legacy_source_queue']); \update_option('sbi_statuses', $sbi_statuses_option); // updated early just in case there is a fatal error if (empty($batch)) { return; } $options = \get_option('sb_instagram_settings', array()); $connected_accounts = isset($options['connected_accounts']) ? $options['connected_accounts'] : array(); foreach ($batch as $account_key) { $connected_account = isset($connected_accounts[$account_key]) ? $connected_accounts[$account_key] : \false; if ($connected_account) { self::update_single_source($connected_account); } } return $sbi_statuses_option['legacy_source_queue']; } /** * Transfer data from a connected account to the sources table * after it's been validated with an API call * * @param array $connected_account * @param bool $connect_if_error * * @return array * * @since 6.0 */ public static function update_single_source($connected_account, $connect_if_error = \true) { $account_type = isset($connected_account['account_type']) ? $connected_account['account_type'] : 'business'; $connection = new \SmashBalloon\YoutubeFeed\Vendor\SB_Instagram_API_Connect($connected_account, 'header', array()); $connection->connect(); if (isset($connected_account['privilege']) && $connected_account['privilege'] === 'tagged') { $connected_account['use_tagged'] = \true; } $source_data = array('access_token' => $connected_account['access_token'], 'id' => $connected_account['user_id'], 'type' => $account_type, 'username' => $connected_account['username'], 'privilege' => !empty($connected_account['use_tagged']) ? 'tagged' : ''); if (!empty($connected_account['expires_timestamp'])) { $source_data['expires'] = \date('Y-m-d H:i:s', $connected_account['expires_timestamp']); } if ($connected_account['local_avatar']) { \SmashBalloon\YoutubeFeed\Vendor\SB_Instagram_Connected_Account::update_local_avatar_status($connected_account['username'], \true); } $header_details = '{}'; $source_data['error'] = ''; if (!$connection->is_wp_error() && !$connection->is_instagram_error()) { $header_details_array = $connection->get_data(); $header_details_array = self::merge_account_details($header_details_array, $connected_account); $cdn_avatar_url = \SmashBalloon\YoutubeFeed\Vendor\SB_Instagram_Parse::get_avatar($header_details_array, array(), \true); if (!empty($cdn_avatar_url)) { $created = \SmashBalloon\YoutubeFeed\Vendor\SB_Instagram_Connected_Account::create_local_avatar($header_details_array['username'], $cdn_avatar_url); \SmashBalloon\YoutubeFeed\Vendor\SB_Instagram_Connected_Account::update_local_avatar_status($header_details_array['username'], $created); } $source_data['username'] = $header_details_array['username']; $header_details = \json_encode($header_details_array); } else { $source_data['error'] = $connection; if ($connection->is_wp_error()) { $source_data['error'] = $connection->get_wp_error(); } else { $source_data['error'] = $connection->get_data(); } } $source_data['info'] = $header_details; if (!empty($connected_account['private'])) { $source_data['info']['private'] = $connected_account['private']; } if (empty($source_data['error']) || $connect_if_error) { self::update_or_insert($source_data); } $source_data['record_id'] = 0; $source_data['account_id'] = $connected_account['user_id']; $source_data['account_type'] = $account_type; return $source_data; } /** * Creates a source from the access token and * source ID saved in 3.x settings * * @since 6.0 */ public static function update_source_from_legacy_settings() { // not needed } public static function merge_account_details($header_details_array, $connected_account) { $header_details_array['local_avatar'] = !empty($connected_account['local_avatar']); $header_details_array['name'] = !empty($connected_account['name']) ? $connected_account['name'] : '{}'; $header_details_array['page_access_token'] = !empty($connected_account['page_access_token']) ? $connected_account['page_access_token'] : ''; return $header_details_array; } /** * If the plugin is still updating legacy sources this function * can be used to udpate a single source if needed before * the update is done. * * @param string $slug_or_id * * @return array|bool */ public static function maybe_one_off_connected_account_update($slug_or_id) { if (!self::should_do_source_updates()) { return \false; } $connected_accounts = (array) \json_decode(\stripcslashes(\get_option('sbi_connected_accounts')), \true); $connected_account = isset($connected_accounts[$slug_or_id]) ? $connected_accounts[$slug_or_id] : \false; if ($connected_account) { return self::update_single_source($connected_account); } return \false; } /** * Clears the "error" column in the sbi_sources table for a specific * account * * @param string $account_id * * @return bool * * @since 6.0 */ public static function clear_error($account_id) { $source_data = array('id' => $account_id, 'error' => ''); return self::update_or_insert($source_data); } /** * Adds an error to the error table by account ID * * @param string $account_id * @param string|object|array $error * * @return bool * * @since 6.0 */ public static function add_error($account_id, $error) { $source_data = array('id' => $account_id, 'error' => \is_string($error) ? $error : \json_encode($error)); return self::update_or_insert($source_data); } /** * Uses query results from the sbi_sources table to convert them * into connected account data and return them as a connected account * array as would be used in versions 5.x and below * * @param array $source_data * * @return array * * @since 6.0 */ public static function convert_sources_to_connected_accounts($source_data) { $encryption = new \SmashBalloon\YoutubeFeed\Vendor\SB_Instagram_Data_Encryption(); $connected_accounts = array(); foreach ($source_data as $source_datum) { $info = !empty($source_datum['info']) ? \json_decode($encryption->decrypt($source_datum['info']), \true) : array(); $settings = array('gdpr' => 'no'); $avatar = \SmashBalloon\YoutubeFeed\Vendor\SB_Instagram_Parse::get_avatar($info, $settings, \true); $connected_account = array('id' => $source_datum['account_id'], 'user_id' => $source_datum['account_id'], 'type' => $source_datum['account_type'], 'account_type' => $source_datum['account_type'], 'username' => $source_datum['username'], 'access_token' => sbi_maybe_clean($source_datum['access_token']), 'privilege' => $source_datum['privilege'], 'expires_timestamp' => \strtotime($source_datum['expires']), 'is_valid' => empty($source_datum['error']), 'profile_picture' => $avatar, 'last_checked' => isset($source_datum['last_updated']) ? \strtotime($source_datum['last_updated']) : \time()); if (!empty($info['private'])) { $connected_account['private'] = $info['private']; } $connected_account['local_avatar_url'] = \SmashBalloon\YoutubeFeed\Vendor\SB_Instagram_Connected_Account::maybe_local_avatar($source_datum['username'], $avatar); $connected_accounts[$source_datum['account_id']] = $connected_account; } return $connected_accounts; } /** * Returns a batch of accounts that have expiring access tokens * * @return array|bool * * @since 6.0 */ public static function get_expiring() { $args = array('expiring' => \true); $results = \Smashballoon\Customizer\SBI_Db::source_query($args); return $results; } }