Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @since 10 Feb 2017 */ namespace IPS\Content\Promote; /* To prevent PHP errors (extending class does not exist) revealing path */ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * Facebook Promotion */ class _Facebook extends PromoteAbstract { /** * @brief Icon */ public static $icon = 'facebook'; /** * @brief Default settings */ public $defaultSettings = array( 'token' => NULL, 'owner' => NULL, 'page_name' => NULL, // 'permissions' => NULL, 'members' => NULL, 'page' => NULL, // 'tags' => NULL, 'image' => NULL, 'last_sync' => 0, 'shareable' => array() ); /** * Is connected? * * @return bool */ public function connected() { return ( $this->member->fb_uid and $this->member->fb_token ); } /** * Get image * * @return string */ public function getPhoto() { if ( ! $this->settings['image'] or $this->settings['last_sync'] < time() - 86400 ) { /* Fetch again */ $response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/picture?type=large&appsecret_proof=" . static::appSecretProof( $this->settings['token'] ) )->request()->get(); $extension = str_replace( 'image/', '', $response->httpHeaders['Content-Type'] ); $newFile = \IPS\File::create( 'core_Promote', 'facebook_' . $this->settings['page'] . '.' . $extension, (string) $response, NULL, FALSE, NULL, FALSE ); $this->saveSettings( array( 'image' => (string) $newFile->url, 'last_sync' => time() ) ); } return $this->settings['image']; } /** * Get name * * @param string|NULL $serviceId Specific page/group ID * @return string */ public function getName( $serviceId=NULL ) { if ( $serviceId ) { $realId = mb_substr( $serviceId, 2 ); if ( mb_substr( $serviceId, 0, 2 ) == 'p_' ) { return isset( $this->settings['shareable']['pages'][ $realId ]['name'] ) ? $this->settings['shareable']['pages'][ $realId ]['name'] : \IPS\Member::loggedIn()->language()->addToStack( 'promote_service_name_missing' ); } else { return isset( $this->settings['shareable']['groups'][ $realId ]['name'] ) ? $this->settings['shareable']['groups'][ $realId ]['name'] : \IPS\Member::loggedIn()->language()->addToStack( 'promote_service_name_missing' ); } } } /** * Check publish permissions * * @return boolean */ public function canPostToPage() { $facebook = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth2\Facebook'); $authorizedScopes = $facebook->authorizedScopes( $this->member ); if( $authorizedScopes === NULL ) { return FALSE; } foreach( array( 'user_managed_groups', 'manage_pages', 'publish_pages', 'publish_actions' ) as $perm ) { if ( ! in_array( $perm, $authorizedScopes ) ) { return FALSE; } } return TRUE; } /** * Create the appSecretProof key * * @param string $token The Token * @return string */ protected static function appSecretProof( $token ) { $facebook = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth2\Facebook'); return hash_hmac( 'sha256', $token, $facebook->settings['client_secret'] ); } /** * Get form elements for this share service * * @param string $text Text for the text entry * @param string $link Short or full link (short when available) * @param string $content Additional text content (usually a comment, or the item content) * * @return array of form elements */ public function form( $text, $link=null, $content=null ) { $shareable = array(); $_text = ''; if ( $this->promote and $this->promote->id ) { $_text = $text; if ( isset( $this->promote->form_data['facebook'] ) AND $settings = $this->promote->form_data['facebook'] ) { $shareable = $settings['shareable']; } } else { if ( count( $this->settings['tags'] ) ) { $_text .= '#' . implode( ' #', $this->settings['tags'] ); } } $names = array(); $options = array(); if ( isset( $this->settings['shareable'] ) ) { foreach( $this->settings['shareable']['pages'] as $id => $data ) { $options[ 'p_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_page_prefix', NULL, array( 'sprintf' => array( $data['name'] ) ) ); $names[] = $data['name']; } foreach( $this->settings['shareable']['groups'] as $id => $data ) { $options[ 'g_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_group_prefix', NULL, array( 'sprintf' => array( $data['name'] ) ) ); $names[] = $data['name']; } } return array( new \IPS\Helpers\Form\TextArea( 'promote_social_content_facebook', $_text, FALSE, array( 'maxLength' => 2000, 'rows' => 10 ) ), new \IPS\Helpers\Form\CheckboxSet( 'promote_facebook_shareable', $shareable, FALSE, array( 'options' => $options ) ) ); } /** * Allow for any extra processing * * @param array $values Values from the form isn't it though * @return mixed */ public function processPromoteForm( $values ) { if ( isset( $values['promote_facebook_shareable'] ) ) { return array( 'shareable' => $values['promote_facebook_shareable'] ); } return NULL; } /** * Post to Facebook * * @param \IPS\core\Promote $promote Promote Object * @return void */ public function post( $promote ) { $photos = $promote->imageObjects(); $settings = $promote->form_data['facebook']; $shareable = $this->settings['shareable']; $responses = array(); foreach( $settings['shareable'] as $id ) { $feedId = mb_substr( $id, 2 ); if ( mb_substr( $id, 0, 2 ) == 'p_' ) { $responses[ $id ][] = $this->_post( $promote, $photos, $feedId, $shareable['pages'][ $feedId ]['token'] ); } else { $responses[ $id ][] = $this->_post( $promote, $photos, $feedId, $this->settings['token'] ); } } return $responses; } /** * Actually post the post, but not post post as that would be too late * * @param \IPS\core\Promote $promote Promote Object * @param array $photos The photos * @param int $feedId The feed ID to post to. Honestly, it's not rocket science. * @param string $token I'll give you one guess. Does anyone actually read this? * @return stuff */ protected function _post( $promote, $photos, $feedId, $token=NULL ) { /* Get the last 20 items to see if we have a duplicate */ try { $items = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/feed" )->setQueryString( $this->_addToken( array( 'limit' => 20 ), $token ) )->request()->get()->decodeJson(); if ( isset( $items['data'] ) ) { foreach( $items['data'] as $item ) { $time = new \IPS\DateTime( ( isset( $item['created_time'] ) ? $item['created_time'] : $item['updated_time' ] ) ); $now = new \IPS\DateTime(); /* Only look back past the last 30 mins */ if ( intval( $now->diff( $time )->format('%i') ) > 30 ) { continue; } if ( preg_replace( '#\s{1,}#', " ", $promote->text['facebook'] ) == preg_replace( '#\s{1,}#', " ", $item['message'] ) ) { /* Duplicate */ return $item['id']; } } } } catch( \Exception $e ) { \IPS\Log::log( $e, 'facebook' ); } if ( ! $photos or count( $photos ) === 1 ) { /* Simple message */ try { if ( $photos ) { $thePhoto = array_pop( $photos ); $response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/photos" )->request()->post( $this->_addToken( array( 'message' => $promote->text['facebook'], 'url' => (string) $this->returnUrlWithProtocol( $thePhoto->url ), 'link' => $promote->short_link ), $token ) )->decodeJson(); } else { $response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/feed" )->request()->post( $this->_addToken( array( 'message' => $promote->text['facebook'], 'link' => $promote->short_link ), $token ) )->decodeJson(); } } catch( \Exception $e ) { \IPS\Log::log( $e, 'facebook' ); throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') ); } if ( isset( $response['id'] ) ) { return $response['id']; } else { throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') ); } } else { /* We have multiple photos so we first need to create an album, and then upload into that */ try { /* The item may be a comment, which uses a language string for objectTitle - make sure that is parsed before sending */ $title = $promote->objectTitle; \IPS\Member::loggedIn()->language()->parseOutputForDisplay( $title ); $response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/albums" )->request()->post( $this->_addToken( array( 'name' => $title, ), $token ) )->decodeJson(); if ( ! isset( $response['id'] ) ) { throw new \InvalidArgumentException('Could not create album'); } $newAlbumId = $response['id']; foreach( $photos as $photo ) { $response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $newAlbumId . "/photos" )->request()->post( $this->_addToken( array( 'message' => $promote->text['facebook'], 'url' => (string) $this->returnUrlWithProtocol( $photo->url ), ), $token ) )->decodeJson(); } } catch( \Exception $e ) { \IPS\Log::log( $e, 'facebook' ); throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->get('facebook_publish_exception') ); } if ( isset( $response['id'] ) ) { return $response['id']; } else { throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') ); } } } /** * Adds 'access_token' to query string if token defined (it is for pages, but not for groups * * @param array $values The values of the array * @param string|NULL $token The token obvs * @return array */ protected function _addToken( $values, $token ) { return ( $token ) ? array_merge( $values, array( 'access_token' => $token, 'appsecret_proof' => static::appSecretProof( $token ) ) ) : $values; } /** * Return the published URL * * @param array $data Data returned from a successful POST * @return \IPS\Http\Url * @throws \InvalidArgumentException */ public function getUrl( $data ) { if ( is_array( $data ) and count( $data ) ) { /* Get a random URL from the many it was shared to */ $array = $data[ array_rand( $data ) ]; $data = array_pop( $array ); } if ( $data and preg_match( '#^[0-9_]*$#', $data ) ) { return \IPS\Http\Url::external( 'https://facebook.com/' . $data ); } throw new \InvalidArgumentException(); } /** * Ensure the URL is not protocol relative * * @return \IPS\Http\Url */ protected function returnUrlWithProtocol( \IPS\Http\Url $url ) { if ( ! $url->data['scheme'] ) { $url = $url->setScheme( ( \substr( \IPS\Settings::i()->base_url, 0, 5 ) == 'https' ) ? 'https' : 'http' ); } return $url; } }