diff --git a/www/protected/components/UsuarioWeb.php b/www/protected/components/UsuarioWeb.php index e6dc058..a2bed73 100644 --- a/www/protected/components/UsuarioWeb.php +++ b/www/protected/components/UsuarioWeb.php @@ -4,11 +4,25 @@ class UsuarioWeb extends CWebUser { private $_model; - function getId_empresa() { + public function getId_empresa() { $usuario = $this->loadUser(Yii::app()->user->id); return $usuario->id_empresa; } + + public function getEsCoordinador() { + $usuario = $this->loadUser(Yii::app()->user->id); + return ($usuario->tipo == Usuario::TIPO_USUARIO_COORDINADOR); + } + + public function getTieneEquipo() { + $subscripcion = $this->loadSubscripcion(Yii::app()->user->id); + return ($subscripcion->producto->n_agentes > 0); + } + public function getSubscripcion() { + return $this->loadSubscripcion(Yii::app()->user->id); + } + // Load user model. protected function loadUser($id = null) { if ($this->_model === null) { @@ -18,6 +32,12 @@ class UsuarioWeb extends CWebUser { return $this->_model; } + // Carga la subscripción activa + protected function loadSubscripcion($idUsuario = null) { + return Subscripcion::model()->activa()->findByAttributes(array('id_usuario' => $idUsuario)); + } + + } ?> \ No newline at end of file diff --git a/www/protected/config/main.php b/www/protected/config/main.php index c26b0e2..3f2580a 100644 --- a/www/protected/config/main.php +++ b/www/protected/config/main.php @@ -27,6 +27,7 @@ $config = array( ), 'modules'=>array( + 'application.modules.socialconnect.SocialConnectModule', ), // application components diff --git a/www/protected/config/mode_development.php b/www/protected/config/mode_development.php index 74d6805..aa2c278 100644 --- a/www/protected/config/mode_development.php +++ b/www/protected/config/mode_development.php @@ -1,5 +1,5 @@ array( 'gii' => array( 'class' => 'system.gii.GiiModule', 'password' => 'password', ), + 'socialConnect' => array( + 'callbackUrl' => 'site/callback', + 'debug_mode' => true, + 'debug_file' => 'socialconnect.log', + 'providers' => array( + 'facebook' => array( + 'enabled' => true, + 'keys' => array('id' => '', 'secret' => ''), + 'scope' => 'email,publish_stream', + ), + 'twitter' => array( + 'enabled' => true, + 'keys' => array( + 'key' => '0aBDNeQOFTPMxHb7TMjHlA', + 'secret' => 'qjVCKdLjRngBUpGnbPw3NXRiIK1BdJWYCnHhZ4pClXk' + ) + ), + 'linkedin' => array( + 'enabled' => true, + 'keys' => array('key' => '', 'secret' => '') + ), + ) + ), ), - + // Application components 'components' => array( // Database @@ -39,8 +58,8 @@ $configSpecific = array( 'password' => '', 'charset' => 'utf8', 'tablePrefix' => '', + 'enableParamLogging' => true, ), - 'mail' => array( 'class' => 'application.extensions.yii-mail.YiiMail', 'transportType' => 'smtp', @@ -52,7 +71,31 @@ $configSpecific = array( 'port' => 25, ), 'viewPath' => 'application.views.mails', - ), + ), + 'socialConnect' => array( + 'class' => 'application.extensions.yii-socialconnect.YiiSocialConnect', + 'callbackUrl' => 'site/callback', + 'debug_mode' => true, + 'debug_file' => dirname(__FILE__) . '/../runtime/socialconnect.log', + 'providers' => array( + 'Facebook' => array( + 'enabled' => true, + 'keys' => array('id' => '', 'secret' => ''), + 'scope' => 'email,publish_stream', + ), + 'Twitter' => array( + 'enabled' => true, + 'keys' => array( + 'key' => '0aBDNeQOFTPMxHb7TMjHlA', + 'secret' => 'qjVCKdLjRngBUpGnbPw3NXRiIK1BdJWYCnHhZ4pClXk' + ) + ), + 'Linkedin' => array( + 'enabled' => true, + 'keys' => array('key' => '', 'secret' => '') + ), + ) + ), // Application Log 'log' => array( @@ -73,13 +116,11 @@ $configSpecific = array( ), ), ), - // application-level parameters that can be accessed // using Yii::app()->params['paramName'] - 'params'=>array( + 'params' => array( 'frontpage' => 'http://localhost/index.php', 'email_remitente' => 'mantenimiento@rodax-software.com', - ), + ), ); - ?> \ No newline at end of file diff --git a/www/protected/config/url_rules.php b/www/protected/config/url_rules.php index 8a9b60a..10b5db1 100644 --- a/www/protected/config/url_rules.php +++ b/www/protected/config/url_rules.php @@ -1,7 +1,6 @@ 'usuario/index', '/'=>'/view', '//'=>'/', diff --git a/www/protected/controllers/EquipoController.php b/www/protected/controllers/EquipoController.php index 428bbe6..a074b71 100644 --- a/www/protected/controllers/EquipoController.php +++ b/www/protected/controllers/EquipoController.php @@ -115,10 +115,10 @@ class EquipoController extends Controller { $invitacion = new FormularioInvitarAgente; // if it is ajax validation request - if (isset($_POST['ajax']) && $_POST['ajax'] === 'invitacion-agente-form') { - echo CActiveForm::validate($invitacion); - Yii::app()->end(); - } +// if (isset($_POST['ajax']) && $_POST['ajax'] === 'invitacion-agente-form') { +// echo CActiveForm::validate($invitacion); +// Yii::app()->end(); +// } if (isset($_POST['FormularioInvitarAgente'])) { $invitacion->attributes = $_POST['FormularioInvitarAgente']; @@ -126,6 +126,7 @@ class EquipoController extends Controller { if ($invitacion->validate()) { $nuevo_usuario = new Usuario('registrar'); $nuevo_usuario->id_empresa = Yii::app()->user->id_empresa; + $nuevo_usuario->tipo = Usuario::TIPO_USUARIO_AGENTE; $nuevo_usuario->nombre = $invitacion->nombre; $nuevo_usuario->email = $invitacion->email; $nuevo_usuario->password = $nuevo_usuario->encrypt(microtime()); @@ -133,14 +134,14 @@ class EquipoController extends Controller { $nuevo_usuario->clave_seguridad = $nuevo_usuario->encrypt(microtime() . $nuevo_usuario->password); if ($nuevo_usuario->save()) { - //$this->enviarMailRegistro($nuevo_usuario); + $this->enviarMailRegistroAgente($nuevo_usuario); Yii::app()->user->setFlash('success', Yii::t('profind', 'Se ha enviado la invitación a la dirección') . ' ' . $invitacion->email); $invitacion = new FormularioInvitarAgente; } else { Yii::app()->user->setFlash('error', Yii::t('profind', 'Se ha producido un error al registrar la invitación')); } } else { - Yii::app()->user->setFlash('error', Yii::t('profind', 'Se ha producido un error al validad la invitación')); + Yii::app()->user->setFlash('error', Yii::t('profind', 'Se ha producido un error al validar la invitación')); } } @@ -151,6 +152,31 @@ class EquipoController extends Controller { )); } + /** + * Envía un mail de registro a un usuario + * con una URL de confirmación. + * @param Usuario $usuario Usuario al que se le enviará el mail de registro + */ + private function enviarMailRegistroAgente($usuario) { + Yii::import('ext.yii-mail.YiiMailMessage'); + + $url_activacion = Yii::app()->params['frontpage'] . '?' . 'key=' . $usuario->clave_seguridad . '&email=' . urlencode($usuario->email) . '&x=1'; + + $mensaje = new YiiMailMessage; + $mensaje->from = Yii::app()->params['email_remitente']; + $mensaje->setTo($usuario->email); + $mensaje->subject = Yii::t('profind', 'Complete su registro de agente en PROFIND'); + $mensaje->view = 'registro_agente'; + $mensaje->setBody(array( + 'url' => $url_activacion, + 'email' => $usuario->email + ), 'text/html' + ); + + Yii::app()->mail->send($mensaje); + } + + /** * Returns the data model based on the primary key given in the GET variable. * If the data model is not found, an HTTP exception will be raised. diff --git a/www/protected/controllers/RegistroUsuarioController.php b/www/protected/controllers/RegistroUsuarioController.php index 1a1ddaa..09483df 100644 --- a/www/protected/controllers/RegistroUsuarioController.php +++ b/www/protected/controllers/RegistroUsuarioController.php @@ -15,7 +15,7 @@ class RegistroUsuarioController extends Controller { public function accessRules() { return array( array('allow', - 'actions' => array('registrar', 'activar'), + 'actions' => array('registrar', 'registrarAgente', 'activar'), 'users' => array('*') ), array('deny'), @@ -47,6 +47,7 @@ class RegistroUsuarioController extends Controller { } $nuevo_usuario = new Usuario('registrar'); + $nuevo_usuario->tipo = Usuario::TIPO_USUARIO_COORDINADOR; $nuevo_usuario->email = $formulario->email; $nuevo_usuario->password = $nuevo_usuario->encrypt($formulario->password); $nuevo_usuario->estado = Usuario::ESTADO_NOACTIVO; @@ -60,6 +61,7 @@ class RegistroUsuarioController extends Controller { Yii::app()->end(); } + // Crear la empresa $nueva_empresa = new Empresa('registrar'); if (!$nueva_empresa->save()) { foreach ($nueva_empresa->getErrors() as $campo => $error) { @@ -68,7 +70,6 @@ class RegistroUsuarioController extends Controller { echo function_exists('json_encode') ? json_encode($resultado) : CJSON::encode($resultado); Yii::app()->end(); } - $nuevo_usuario->id_empresa = $nueva_empresa->id; if (!$nuevo_usuario->save()) { foreach ($nuevo_usuario->getErrors() as $campo => $error) { @@ -77,7 +78,20 @@ class RegistroUsuarioController extends Controller { echo function_exists('json_encode') ? json_encode($resultado) : CJSON::encode($resultado); Yii::app()->end(); } - + + // Crear la subscripción + $nueva_subscripcion = new Subscripcion('registrar'); + $nueva_subscripcion->estado = Subscripcion::ESTADO_ACTIVO; + $nueva_subscripcion->id_usuario = $nuevo_usuario->id; + $nueva_subscripcion->id_producto = 1; + if (!$nueva_subscripcion->save()) { + foreach ($nueva_subscripcion->getErrors() as $campo => $error) { + $resultado[$campo] = $error; + } + echo function_exists('json_encode') ? json_encode($resultado) : CJSON::encode($resultado); + Yii::app()->end(); + } + if ($this->enviarMailRegistro($nuevo_usuario)) { $resultado['status'] = '200'; $resultado['titulo'] = Yii::t('profind', 'Gracias por registrarse en PRODIND'); @@ -95,6 +109,60 @@ class RegistroUsuarioController extends Controller { $this->redirect(Yii::app()->params['frontpage']); } + public function actionRegistrarAgente() { + $formulario = new FormularioRegistroAgente; + $resultado = array(); + + if (isset($_POST['ajax']) && $_POST['ajax'] === 'activar-agente-form-ext') { + $formulario->key = $_POST['FormularioActivarAgente_key']; + $formulario->email = $_POST['FormularioActivarAgente_email']; + $formulario->password = $_POST['FormularioActivarAgente_password']; + $formulario->passwordRepetida = $_POST['FormularioActivarAgente_password_repetida']; + + if ($formulario->validate()) { + $usuario = Usuario::model()->findByAttributes(array('email' => $formulario->email)); + $usuario->estado = Usuario::ESTADO_ACTIVO; + $usuario->save(); + + if ($this->_cambiarPassword($usuario->id, $formulario->password)) { + $this->enviarMailConfirmacionActivacion($usuario); + $resultado['status'] = '200'; + $resultado['titulo'] = Yii::t('profind', 'Cuenta de agente activada'); + $resultado['texto'] = Yii::t('profind', 'Se ha activado su cuenta y se ha establecido su nueva password en PROFIND.'); + echo function_exists('json_encode') ? json_encode($resultado) : CJSON::encode($resultado); + Yii::app()->end(); + } else { + foreach ($formulario->getErrors() as $campo => $error) { + $resultado[$campo] = $error; + } + echo function_exists('json_encode') ? json_encode($resultado) : CJSON::encode($resultado); + Yii::app()->end(); + } + } else { + foreach ($formulario->getErrors() as $campo => $error) { + $resultado[$campo] = $error; + } + echo function_exists('json_encode') ? json_encode($resultado) : CJSON::encode($resultado); + Yii::app()->end(); + } + } + + $this->redirect(Yii::app()->params['frontpage']); + } + + private function _cambiarPassword($id, $nueva_password) { + $usuario = Usuario::model()->findByPk($id); + if (!isset($usuario)) + throw new CHttpException(404, Yii::t('profind', 'La página solicitada no existe.')); + + $usuario->password = $usuario->encrypt($nueva_password); + $usuario->clave_seguridad = $usuario->encrypt(microtime() . $usuario->password); + if ($usuario->save()) { + return true; + } else + return false; + } + /** * Activa la cuenta del usuario a partir de la URL de activación * que se le ha enviado a través de un email. diff --git a/www/protected/controllers/SiteController.php b/www/protected/controllers/SiteController.php index 9d85c55..a998723 100644 --- a/www/protected/controllers/SiteController.php +++ b/www/protected/controllers/SiteController.php @@ -9,7 +9,7 @@ class SiteController extends Controller { public function accessRules() { return array( array('allow', - 'actions' => array('login'), + 'actions' => array('login', 'callback'), 'users' => array('*') ), array('allow', @@ -24,8 +24,6 @@ class SiteController extends Controller { * when an action is not explicitly requested by users. */ public function actionIndex() { - // renders the view file 'protected/views/site/index.php' - // using the default layout 'protected/views/layouts/main.php' $this->render('index'); } @@ -33,7 +31,8 @@ class SiteController extends Controller { * This is the action to handle external exceptions. */ public function actionError() { - if ($error = Yii::app()->errorHandler->error) { + $error = Yii::app()->errorHandler->error; + if ($error) { if (Yii::app()->request->isAjaxRequest) echo $error['message']; else { @@ -93,4 +92,8 @@ class SiteController extends Controller { $this->redirect(Yii::app()->homeUrl); } + public function actionCallback() { + Yii::app()->socialConnect->callback(); + } + } \ No newline at end of file diff --git a/www/protected/controllers/SubcripcionController.php b/www/protected/controllers/SubscripcionController.php similarity index 76% rename from www/protected/controllers/SubcripcionController.php rename to www/protected/controllers/SubscripcionController.php index ec0db21..10bd37e 100644 --- a/www/protected/controllers/SubcripcionController.php +++ b/www/protected/controllers/SubscripcionController.php @@ -1,17 +1,17 @@ loadModel(1); - + + $model = $this->loadModel($id); + // Uncomment the following line if AJAX validation is needed // $this->performAjaxValidation($model); - if (isset($_POST['Subcripcion'])) { - $model->attributes = $_POST['Subcripcion']; + if (isset($_POST['Subscripcion'])) { + $model->attributes = $_POST['Subscripcion']; if ($model->save()) { Yii::app()->user->setFlash('success', Yii::t('profind', 'Se ha actualizado de producto')); $this->redirect(array('modificar', 'id' => $model->id)); @@ -29,7 +29,7 @@ class SubcripcionController extends Controller * @param integer the ID of the model to be loaded */ public function loadModel($id) { - $model = Subcripcion::model()->findByPk($id); + $model = Subscripcion::model()->findByPk($id); if ($model === null) throw new CHttpException(404, Yii::t('profind', 'La página solicitada no existe.')); diff --git a/www/protected/controllers/UsuarioController.php b/www/protected/controllers/UsuarioController.php index 7762917..e1963d2 100644 --- a/www/protected/controllers/UsuarioController.php +++ b/www/protected/controllers/UsuarioController.php @@ -21,7 +21,7 @@ class UsuarioController extends Controller { public function accessRules() { return array( array('allow', // allow admin user to perform 'admin' and 'delete' actions - 'actions' => array('modificar', 'cambiarPassword'), + 'actions' => array('modificar', 'cambiarPassword', 'twitter', 'twitter2'), 'users' => array('@'), ), array('deny', // deny all users @@ -35,12 +35,24 @@ class UsuarioController extends Controller { * If update is successful, the browser will be redirected to the 'view' page. * @param integer $id the ID of the model to be updated */ - public function actionModificar($id) { + public function actionModificar($id, $provider = '') { if ($id != Yii::app()->user->id) throw new CHttpException(404, Yii::t('profind', 'La página solicitada no existe.')); - $usuario = $this->loadModel($id); - + if (($provider != '') && (!isset($_POST['Usuario']))) { + switch ($provider) { + case 'twitter': + case 'facebook': + case 'linkedin': + $usuario = $this->loadModelwithSocialData($id, $provider); + break; + default: + throw new CHttpException(404, Yii::t('profind', 'La página solicitada no existe.')); + } + } + else + $usuario = $this->loadModel($id); + // Uncomment the following line if AJAX validation is needed // $this->performAjaxValidation($usuario); @@ -48,14 +60,14 @@ class UsuarioController extends Controller { $usuario->attributes = $_POST['Usuario']; $ficheroFotografia = CUploadedFile::getInstance($usuario, 'ficheroFotografia'); $quitarFotografia = Yii::app()->request->getParam('quitar_fotografia', '0'); - + if ($usuario->save()) { if (($quitarFotografia == '1') && ($usuario->fotografia->tieneFotografia())) $usuario->fotografia->eliminarFotografia(); if ($ficheroFotografia) $usuario->fotografia->guardarFotografia($ficheroFotografia); - + Yii::app()->user->setFlash('success', Yii::t('profind', 'Se ha actualizado el perfil')); $this->redirect(array('modificar', 'id' => $usuario->id)); } @@ -66,6 +78,20 @@ class UsuarioController extends Controller { )); } + public function loadModelwithSocialData($id, $provider) { + $usuario = $this->loadModel($id); + + $profile = Yii::app()->socialConnect->getUserProfile($provider); + + if ($twitter) { + $usuario->email = $twitter['email']; + $usuario->nombre = $twitter['displayName']; + $usuario->apellidos = $twitter['displayName']; + $usuario->descripcion = $twitter['photoURL']; + } + return $usuario; + } + /** * Returns the data model based on the primary key given in the GET variable. * If the data model is not found, an HTTP exception will be raised. @@ -89,4 +115,5 @@ class UsuarioController extends Controller { Yii::app()->end(); } } + } diff --git a/www/protected/data/tbl_subcripciones.sql b/www/protected/data/tbl_subscripciones.sql similarity index 81% rename from www/protected/data/tbl_subcripciones.sql rename to www/protected/data/tbl_subscripciones.sql index 8bac9e4..5bcbd51 100644 --- a/www/protected/data/tbl_subcripciones.sql +++ b/www/protected/data/tbl_subscripciones.sql @@ -23,10 +23,10 @@ SET time_zone = "+00:00"; -- -------------------------------------------------------- -- --- Estructura de tabla para la tabla `tbl_subcripciones` +-- Estructura de tabla para la tabla `tbl_subscripciones` -- -CREATE TABLE IF NOT EXISTS `tbl_subcripciones` ( +CREATE TABLE IF NOT EXISTS `tbl_subscripciones` ( `id` int(11) NOT NULL AUTO_INCREMENT, `id_usuario` int(11) DEFAULT NULL, `id_producto` int(11) DEFAULT NULL, @@ -37,10 +37,10 @@ CREATE TABLE IF NOT EXISTS `tbl_subcripciones` ( ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ; -- --- Volcado de datos para la tabla `tbl_subcripciones` +-- Volcado de datos para la tabla `tbl_subscripciones` -- -INSERT INTO `tbl_subcripciones` (`id`, `id_usuario`, `id_producto`, `estado`, `fecha_inicio`, `fecha_fin`) VALUES +INSERT INTO `tbl_subscripciones` (`id`, `id_usuario`, `id_producto`, `estado`, `fecha_inicio`, `fecha_fin`) VALUES (1, 2, 2, 'activo', '2012-09-21 00:00:00', '2013-09-21 00:00:00'); /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; diff --git a/www/protected/extensions/yii-socialconnect/YiiSocialConnect.php b/www/protected/extensions/yii-socialconnect/YiiSocialConnect.php new file mode 100644 index 0000000..2c653d7 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/YiiSocialConnect.php @@ -0,0 +1,188 @@ +registerScripts(); + parent::init(); + } + + /** + * Incluir los ficheros de HybridAuth + */ + public function createHybridAuth() { + $this->_hybridAuth = new Hybrid_Auth($this->getConfig()); + } + + /** + * Incluir los ficheros de HybridAuth + */ + public function registerScripts() { + require dirname(__FILE__) . '/vendors/Hybrid/Auth.php'; + require dirname(__FILE__) . '/vendors/Hybrid/Endpoint.php'; //callback + } + + /** + * Devuelve un array con la configuracion adaptada a HybridAuth + * @return array configuración + */ + public function getConfig() { + + return array( + 'baseUrl' => Yii::app()->getBaseUrl(true), + 'base_url' => Yii::app()->createAbsoluteUrl($this->callbackUrl), // URL for Hybrid_Auth callback + 'providers' => $this->providers, + 'debug_file' => $this->debug_file, + 'debug_mode' => $this->debug_mode, + ); + } + + public function callback() { + Hybrid_Endpoint::process(); + } + + /** + * Carga el perfil de un usuario de la red social indicada en el parámetro @param $provider + * El perfil está en la propiedad $userProfile. + * Si hay algún error, está en la propiedad $errorCode. + * @param $provider string nombre de la red social (Twitter, Linkedin, Facebook) + * @return boolean + */ + public function loadUserProfile($provider) { + $this->userProfile = NULL; + + if (!array_key_exists($provider, $this->providers)) { + $this->errorCode = self::ERROR_UNKNOWN_PROVIDER; + } else { + $this->createHybridAuth(); + try { + $social = $this->_hybridAuth->authenticate($provider); + $this->userProfile = $social->getUserProfile(); + $social->logout(); + $this->errorCode = self::ERROR_NONE; + } catch (Exception $e) { + switch ($e->getCode()) { + case 0 : + $this->errorCode = self::ERROR_UNSPECIFIED; + break; + case 1 : + $this->errorCode = self::ERROR_GENERAL_CONFIGURATION; + break; + case 2 : + $this->errorCode = self::ERROR_PROVIDER_CONFIGURATION; + break; + case 3 : + $this->errorCode = self::ERROR_UNKNOWN_PROVIDER; + break; + case 4 : + $this->errorCode = self::ERROR_MISSING_CREDENTIALS; + break; + case 5 : + $this->errorCode = self::ERROR_AUTHENTIFICATION_FAILED; + break; + case 6 : + $this->errorCode = self::ERROR_REQUEST_FAILED; + $social->logout(); + break; + case 7 : + $this->errorCode = self::ERROR_NOT_CONNECTED; + $social->logout(); + break; + case 8 : + $this->errorCode = self::ERROR_FEATURE_NOT_SUPPORTED; + break; + } + } + } + + if ($this->errorCode) + Yii::log($this->getErrorMessage(), CLogger::LEVEL_ERROR); + + return !$this->errorCode; + } + + /** + * Devuelve el texto dek mensaje de error asociado al error que hay en la propiedad errorCode. + * @return string mensaje de error + */ + public function getErrorMessage() { + $message = ''; + switch ($this->errorCode) { + case self::ERROR_UNSPECIFIED: + $message = "Unspecified error."; + break; + case self::ERROR_GENERAL_CONFIGURATION: + $message = "Hybriauth configuration error."; + break; + case self::ERROR_PROVIDER_CONFIGURATION: + $message = "Provider not properly configured."; + break; + case self::ERROR_UNKNOWN_PROVIDER: + $message = "Unknown or disabled provider."; + break; + case self::ERROR_MISSING_CREDENTIALS: + $message = "Missing provider application credentials."; + break; + case self::ERROR_AUTHENTIFICATION_FAILED: + $message = "Authentification failed."; + break; + case self::ERROR_REQUEST_FAILED: + $message = "User profile request failed."; + break; + case self::ERROR_NOT_CONNECTED: + $message = "User not connected to the provider."; + break; + case self::ERROR_FEATURE_NOT_SUPPORTED: + $message = "Provider does not support this feature."; + break; + } + return $message; + } +} + +?> diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Auth.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Auth.php new file mode 100644 index 0000000..7ca4a0c --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Auth.php @@ -0,0 +1,411 @@ +getSessionData() ); + Hybrid_Logger::info( "Hybrid_Auth initialize: check if any error is stored on the endpoint..." ); + + if( Hybrid_Error::hasError() ){ + $m = Hybrid_Error::getErrorMessage(); + $c = Hybrid_Error::getErrorCode(); + $p = Hybrid_Error::getErrorPrevious(); + + Hybrid_Logger::error( "Hybrid_Auth initialize: A stored Error found, Throw an new Exception and delete it from the store: Error#$c, '$m'" ); + + Hybrid_Error::clearError(); + + // try to provide the previous if any + // Exception::getPrevious (PHP 5 >= 5.3.0) http://php.net/manual/en/exception.getprevious.php + if ( version_compare( PHP_VERSION, '5.3.0', '>=' ) && ($p instanceof Exception) ) { + throw new Exception( $m, $c, $p ); + } + else{ + throw new Exception( $m, $c ); + } + } + + Hybrid_Logger::info( "Hybrid_Auth initialize: no error found. initialization succeed." ); + + // Endof initialize + } + + // -------------------------------------------------------------------- + + /** + * Hybrid storage system accessor + * + * Users sessions are stored using HybridAuth storage system ( HybridAuth 2.0 handle PHP Session only) and can be acessed directly by + * Hybrid_Auth::storage()->get($key) to retrieves the data for the given key, or calling + * Hybrid_Auth::storage()->set($key, $value) to store the key => $value set. + */ + public static function storage() + { + return Hybrid_Auth::$store; + } + + // -------------------------------------------------------------------- + + /** + * Get hybridauth session data. + */ + function getSessionData() + { + return Hybrid_Auth::storage()->getSessionData(); + } + + // -------------------------------------------------------------------- + + /** + * restore hybridauth session data. + */ + function restoreSessionData( $sessiondata = NULL ) + { + Hybrid_Auth::storage()->restoreSessionData( $sessiondata ); + } + + // -------------------------------------------------------------------- + + /** + * Try to authenticate the user with a given provider. + * + * If the user is already connected we just return and instance of provider adapter, + * ELSE, try to authenticate and authorize the user with the provider. + * + * $params is generally an array with required info in order for this provider and HybridAuth to work, + * like : + * hauth_return_to: URL to call back after authentication is done + * openid_identifier: The OpenID identity provider identifier + * google_service: can be "Users" for Google user accounts service or "Apps" for Google hosted Apps + */ + public static function authenticate( $providerId, $params = NULL ) + { + Hybrid_Logger::info( "Enter Hybrid_Auth::authenticate( $providerId )" ); + + // if user not connected to $providerId then try setup a new adapter and start the login process for this provider + if( ! Hybrid_Auth::storage()->get( "hauth_session.$providerId.is_logged_in" ) ){ + Hybrid_Logger::info( "Hybrid_Auth::authenticate( $providerId ), User not connected to the provider. Try to authenticate.." ); + + $provider_adapter = Hybrid_Auth::setup( $providerId, $params ); + + $provider_adapter->login(); + } + + // else, then return the adapter instance for the given provider + else{ + Hybrid_Logger::info( "Hybrid_Auth::authenticate( $providerId ), User is already connected to this provider. Return the adapter instance." ); + + return Hybrid_Auth::getAdapter( $providerId ); + } + } + + // -------------------------------------------------------------------- + + /** + * Return the adapter instance for an authenticated provider + */ + public static function getAdapter( $providerId = NULL ) + { + Hybrid_Logger::info( "Enter Hybrid_Auth::getAdapter( $providerId )" ); + + return Hybrid_Auth::setup( $providerId ); + } + + // -------------------------------------------------------------------- + + /** + * Setup an adapter for a given provider + */ + public static function setup( $providerId, $params = NULL ) + { + Hybrid_Logger::debug( "Enter Hybrid_Auth::setup( $providerId )", $params ); + + if( ! $params ){ + $params = Hybrid_Auth::storage()->get( "hauth_session.$providerId.id_provider_params" ); + + Hybrid_Logger::debug( "Hybrid_Auth::setup( $providerId ), no params given. Trying to get the sotred for this provider.", $params ); + } + + if( ! $params ){ + $params = ARRAY(); + + Hybrid_Logger::info( "Hybrid_Auth::setup( $providerId ), no stored params found for this provider. Initialize a new one for new session" ); + } + + if( ! isset( $params["hauth_return_to"] ) ){ + $params["hauth_return_to"] = Hybrid_Auth::getCurrentUrl(); + } + + Hybrid_Logger::debug( "Hybrid_Auth::setup( $providerId ). HybridAuth Callback URL set to: ", $params["hauth_return_to"] ); + + # instantiate a new IDProvider Adapter + $provider = new Hybrid_Provider_Adapter(); + + $provider->factory( $providerId, $params ); + + return $provider; + } + + // -------------------------------------------------------------------- + + /** + * Check if the current user is connected to a given provider + */ + public static function isConnectedWith( $providerId ) + { + return (bool) Hybrid_Auth::storage()->get( "hauth_session.{$providerId}.is_logged_in" ); + } + + // -------------------------------------------------------------------- + + /** + * Return array listing all authenticated providers + */ + public static function getConnectedProviders() + { + $idps = array(); + + foreach( Hybrid_Auth::$config["providers"] as $idpid => $params ){ + if( Hybrid_Auth::isConnectedWith( $idpid ) ){ + $idps[] = $idpid; + } + } + + return $idps; + } + + // -------------------------------------------------------------------- + + /** + * Return array listing all enabled providers as well as a flag if you are connected. + */ + public static function getProviders() + { + $idps = array(); + + foreach( Hybrid_Auth::$config["providers"] as $idpid => $params ){ + if($params['enabled']) { + $idps[$idpid] = array( 'connected' => false ); + + if( Hybrid_Auth::isConnectedWith( $idpid ) ){ + $idps[$idpid]['connected'] = true; + } + } + } + + return $idps; + } + + // -------------------------------------------------------------------- + + /** + * A generic function to logout all connected provider at once + */ + public static function logoutAllProviders() + { + $idps = Hybrid_Auth::getConnectedProviders(); + + foreach( $idps as $idp ){ + $adapter = Hybrid_Auth::getAdapter( $idp ); + + $adapter->logout(); + } + } + + // -------------------------------------------------------------------- + + /** + * Utility function, redirect to a given URL with php header or using javascript location.href + */ + public static function redirect( $url, $mode = "PHP" ) + { + Hybrid_Logger::info( "Enter Hybrid_Auth::redirect( $url, $mode )" ); + + if( $mode == "PHP" ){ + header( "Location: $url" ) ; + } + elseif( $mode == "JS" ){ + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo 'Redirecting, please wait...'; + echo ''; + echo ''; + } + + die(); + } + + // -------------------------------------------------------------------- + + /** + * Utility function, return the current url. TRUE to get $_SERVER['REQUEST_URI'], FALSE for $_SERVER['PHP_SELF'] + */ + public static function getCurrentUrl( $request_uri = true ) + { + if( + isset( $_SERVER['HTTPS'] ) && ( $_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1 ) + || isset( $_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' + ){ + $protocol = 'https://'; + } + else { + $protocol = 'http://'; + } + + $url = $protocol . $_SERVER['SERVER_NAME']; + + // use port if non default + $url .= + isset( $_SERVER['SERVER_PORT'] ) + &&( ($protocol === 'http://' && $_SERVER['SERVER_PORT'] != 80) || ($protocol === 'https://' && $_SERVER['SERVER_PORT'] != 443) ) + ? ':' . $_SERVER['SERVER_PORT'] + : ''; + + if( $request_uri ){ + $url .= $_SERVER['REQUEST_URI']; + } + else{ + $url .= $_SERVER['PHP_SELF']; + } + + // return current url + return $url; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Endpoint.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Endpoint.php new file mode 100644 index 0000000..c271934 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Endpoint.php @@ -0,0 +1,217 @@ +here we need to recreate the $_REQUEST + if ( strrpos( $_SERVER["QUERY_STRING"], '?' ) ) { + $_SERVER["QUERY_STRING"] = str_replace( "?", "&", $_SERVER["QUERY_STRING"] ); + + parse_str( $_SERVER["QUERY_STRING"], $_REQUEST ); + } + + Hybrid_Endpoint::$request = $_REQUEST; + } + + // If openid_policy requested, we return our policy document + if ( isset( Hybrid_Endpoint::$request["get"] ) && Hybrid_Endpoint::$request["get"] == "openid_policy" ) { + Hybrid_Endpoint::processOpenidPolicy(); + } + + // If openid_xrds requested, we return our XRDS document + if ( isset( Hybrid_Endpoint::$request["get"] ) && Hybrid_Endpoint::$request["get"] == "openid_xrds" ) { + Hybrid_Endpoint::processOpenidXRDS(); + } + + // If we get a hauth.start + if ( isset( Hybrid_Endpoint::$request["hauth_start"] ) && Hybrid_Endpoint::$request["hauth_start"] ) { + Hybrid_Endpoint::processAuthStart(); + } + // Else if hauth.done + elseif ( isset( Hybrid_Endpoint::$request["hauth_done"] ) && Hybrid_Endpoint::$request["hauth_done"] ) { + Hybrid_Endpoint::processAuthDone(); + } + // Else we advertise our XRDS document, something supposed to be done from the Realm URL page + else { + Hybrid_Endpoint::processOpenidRealm(); + } + } + + /** + * Process OpenID policy request + */ + public static function processOpenidPolicy() + { + $output = file_get_contents( dirname(__FILE__) . "/resources/openid_policy.html" ); + print $output; + die(); + } + + /** + * Process OpenID XRDS request + */ + public static function processOpenidXRDS() + { + header("Content-Type: application/xrds+xml"); + + $output = str_replace + ( + "{RETURN_TO_URL}", + str_replace( + array("<", ">", "\"", "'", "&"), array("<", ">", """, "'", "&"), + Hybrid_Auth::getCurrentUrl( false ) + ), + file_get_contents( dirname(__FILE__) . "/resources/openid_xrds.xml" ) + ); + print $output; + die(); + } + + /** + * Process OpenID realm request + */ + public static function processOpenidRealm() + { + $output = str_replace + ( + "{X_XRDS_LOCATION}", + htmlentities( Hybrid_Auth::getCurrentUrl( false ), ENT_QUOTES, 'UTF-8' ) . "?get=openid_xrds&v=" . Hybrid_Auth::$version, + file_get_contents( dirname(__FILE__) . "/resources/openid_realm.html" ) + ); + print $output; + die(); + } + + /** + * define:endpoint step 3. + */ + public static function processAuthStart() + { + Hybrid_Endpoint::authInit(); + + $provider_id = trim( strip_tags( Hybrid_Endpoint::$request["hauth_start"] ) ); + + # check if page accessed directly + if( ! Hybrid_Auth::storage()->get( "hauth_session.$provider_id.hauth_endpoint" ) ) { + Hybrid_Logger::error( "Endpoint: hauth_endpoint parameter is not defined on hauth_start, halt login process!" ); + + header( "HTTP/1.0 404 Not Found" ); + die( "You cannot access this page directly." ); + } + + # define:hybrid.endpoint.php step 2. + $hauth = Hybrid_Auth::setup( $provider_id ); + + # if REQUESTed hauth_idprovider is wrong, session not created, etc. + if( ! $hauth ) { + Hybrid_Logger::error( "Endpoint: Invalide parameter on hauth_start!" ); + + header( "HTTP/1.0 404 Not Found" ); + die( "Invalide parameter! Please return to the login page and try again." ); + } + + try { + Hybrid_Logger::info( "Endpoint: call adapter [{$provider_id}] loginBegin()" ); + + $hauth->adapter->loginBegin(); + } + catch ( Exception $e ) { + Hybrid_Logger::error( "Exception:" . $e->getMessage(), $e ); + Hybrid_Error::setError( $e->getMessage(), $e->getCode(), $e->getTraceAsString(), $e ); + + $hauth->returnToCallbackUrl(); + } + + die(); + } + + /** + * define:endpoint step 3.1 and 3.2 + */ + public static function processAuthDone() + { + Hybrid_Endpoint::authInit(); + + $provider_id = trim( strip_tags( Hybrid_Endpoint::$request["hauth_done"] ) ); + + $hauth = Hybrid_Auth::setup( $provider_id ); + + if( ! $hauth ) { + Hybrid_Logger::error( "Endpoint: Invalide parameter on hauth_done!" ); + + $hauth->adapter->setUserUnconnected(); + + header("HTTP/1.0 404 Not Found"); + die( "Invalide parameter! Please return to the login page and try again." ); + } + + try { + Hybrid_Logger::info( "Endpoint: call adapter [{$provider_id}] loginFinish() " ); + + $hauth->adapter->loginFinish(); + } + catch( Exception $e ){ + Hybrid_Logger::error( "Exception:" . $e->getMessage(), $e ); + Hybrid_Error::setError( $e->getMessage(), $e->getCode(), $e->getTraceAsString(), $e ); + + $hauth->adapter->setUserUnconnected(); + } + + Hybrid_Logger::info( "Endpoint: job done. retrun to callback url." ); + + $hauth->returnToCallbackUrl(); + die(); + } + + public static function authInit() + { + if ( ! Hybrid_Endpoint::$initDone) { + Hybrid_Endpoint::$initDone = TRUE; + + # Init Hybrid_Auth + try { + require_once realpath( dirname( __FILE__ ) ) . "/Storage.php"; + + $storage = new Hybrid_Storage(); + + // Check if Hybrid_Auth session already exist + if ( ! $storage->config( "CONFIG" ) ) { + header( "HTTP/1.0 404 Not Found" ); + die( "You cannot access this page directly." ); + } + + Hybrid_Auth::initialize( $storage->config( "CONFIG" ) ); + } + catch ( Exception $e ){ + Hybrid_Logger::error( "Endpoint: Error while trying to init Hybrid_Auth" ); + + header( "HTTP/1.0 404 Not Found" ); + die( "Oophs. Error!" ); + } + } + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Error.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Error.php new file mode 100644 index 0000000..571dfce --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Error.php @@ -0,0 +1,84 @@ +set( "hauth_session.error.status" , 1 ); + Hybrid_Auth::storage()->set( "hauth_session.error.message" , $message ); + Hybrid_Auth::storage()->set( "hauth_session.error.code" , $code ); + Hybrid_Auth::storage()->set( "hauth_session.error.trace" , $trace ); + Hybrid_Auth::storage()->set( "hauth_session.error.previous", $previous ); + } + + /** + * clear the last error + */ + public static function clearError() + { + Hybrid_Logger::info( "Enter Hybrid_Error::clearError()" ); + + Hybrid_Auth::storage()->delete( "hauth_session.error.status" ); + Hybrid_Auth::storage()->delete( "hauth_session.error.message" ); + Hybrid_Auth::storage()->delete( "hauth_session.error.code" ); + Hybrid_Auth::storage()->delete( "hauth_session.error.trace" ); + Hybrid_Auth::storage()->delete( "hauth_session.error.previous" ); + } + + /** + * Checks to see if there is a an error. + * + * @return boolean True if there is an error. + */ + public static function hasError() + { + return (bool) Hybrid_Auth::storage()->get( "hauth_session.error.status" ); + } + + /** + * return error message + */ + public static function getErrorMessage() + { + return Hybrid_Auth::storage()->get( "hauth_session.error.message" ); + } + + /** + * return error code + */ + public static function getErrorCode() + { + return Hybrid_Auth::storage()->get( "hauth_session.error.code" ); + } + + /** + * return string detailled error backtrace as string. + */ + public static function getErrorTrace() + { + return Hybrid_Auth::storage()->get( "hauth_session.error.trace" ); + } + + /** + * @return string detailled error backtrace as string. + */ + public static function getErrorPrevious() + { + return Hybrid_Auth::storage()->get( "hauth_session.error.previous" ); + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Logger.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Logger.php new file mode 100644 index 0000000..8ec18d9 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Logger.php @@ -0,0 +1,68 @@ +format(DATE_ATOM); + + file_put_contents( + Hybrid_Auth::$config["debug_file"], + "DEBUG -- " . $_SERVER['REMOTE_ADDR'] . " -- " . $datetime . " -- " . $message . " -- " . print_r($object, true) . "\n", + FILE_APPEND + ); + } + } + + public static function info( $message ) + { + if( Hybrid_Auth::$config["debug_mode"] ){ + $datetime = new DateTime(); + $datetime = $datetime->format(DATE_ATOM); + + file_put_contents( + Hybrid_Auth::$config["debug_file"], + "INFO -- " . $_SERVER['REMOTE_ADDR'] . " -- " . $datetime . " -- " . $message . "\n", + FILE_APPEND + ); + } + } + + public static function error($message, $object = NULL) + { + if( Hybrid_Auth::$config["debug_mode"] ){ + $datetime = new DateTime(); + $datetime = $datetime->format(DATE_ATOM); + + file_put_contents( + Hybrid_Auth::$config["debug_file"], + "ERROR -- " . $_SERVER['REMOTE_ADDR'] . " -- " . $datetime . " -- " . $message . " -- " . print_r($object, true) . "\n", + FILE_APPEND + ); + } + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Adapter.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Adapter.php new file mode 100644 index 0000000..da4d815 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Adapter.php @@ -0,0 +1,283 @@ +id = $id; + $this->params = $params; + $this->id = $this->getProviderCiId( $this->id ); + $this->config = $this->getConfigById( $this->id ); + + # check the IDp id + if( ! $this->id ){ + throw new Exception( "No provider ID specified.", 2 ); + } + + # check the IDp config + if( ! $this->config ){ + throw new Exception( "Unknown Provider ID, check your configuration file.", 3 ); + } + + # check the IDp adapter is enabled + if( ! $this->config["enabled"] ){ + throw new Exception( "The provider '{$this->id}' is not enabled.", 3 ); + } + + # include the adapter wrapper + if( isset( $this->config["wrapper"] ) && is_array( $this->config["wrapper"] ) ){ + require_once $this->config["wrapper"]["path"]; + + if( ! class_exists( $this->config["wrapper"]["class"] ) ){ + throw new Exception( "Unable to load the adapter class.", 3 ); + } + + $this->wrapper = $this->config["wrapper"]["class"]; + } + else{ + require_once Hybrid_Auth::$config["path_providers"] . $this->id . ".php" ; + + $this->wrapper = "Hybrid_Providers_" . $this->id; + } + + # create the adapter instance, and pass the current params and config + $this->adapter = new $this->wrapper( $this->id, $this->config, $this->params ); + + return $this; + } + + // -------------------------------------------------------------------- + + /** + * Hybrid_Provider_Adapter::login(), prepare the user session and the authentification request + * for index.php + */ + function login() + { + Hybrid_Logger::info( "Enter Hybrid_Provider_Adapter::login( {$this->id} ) " ); + + if( ! $this->adapter ){ + throw new Exception( "Hybrid_Provider_Adapter::login() should not directly used." ); + } + + // clear all unneeded params + foreach( Hybrid_Auth::$config["providers"] as $idpid => $params ){ + Hybrid_Auth::storage()->delete( "hauth_session.{$idpid}.hauth_return_to" ); + Hybrid_Auth::storage()->delete( "hauth_session.{$idpid}.hauth_endpoint" ); + Hybrid_Auth::storage()->delete( "hauth_session.{$idpid}.id_provider_params" ); + } + + // make a fresh start + $this->logout(); + + # get hybridauth base url + $HYBRID_AUTH_URL_BASE = Hybrid_Auth::$config["base_url"]; + + # we make use of session_id() as storage hash to identify the current user + # using session_regenerate_id() will be a problem, but .. + $this->params["hauth_token"] = session_id(); + + # set request timestamp + $this->params["hauth_time"] = time(); + + # for default HybridAuth endpoint url hauth_login_start_url + # auth.start required the IDp ID + # auth.time optional login request timestamp + $this->params["login_start"] = $HYBRID_AUTH_URL_BASE . ( strpos( $HYBRID_AUTH_URL_BASE, '?' ) ? '&' : '?' ) . "hauth.start={$this->id}&hauth.time={$this->params["hauth_time"]}"; + + # for default HybridAuth endpoint url hauth_login_done_url + # auth.done required the IDp ID + $this->params["login_done"] = $HYBRID_AUTH_URL_BASE . ( strpos( $HYBRID_AUTH_URL_BASE, '?' ) ? '&' : '?' ) . "hauth.done={$this->id}"; + + Hybrid_Auth::storage()->set( "hauth_session.{$this->id}.hauth_return_to" , $this->params["hauth_return_to"] ); + Hybrid_Auth::storage()->set( "hauth_session.{$this->id}.hauth_endpoint" , $this->params["login_done"] ); + Hybrid_Auth::storage()->set( "hauth_session.{$this->id}.id_provider_params" , $this->params ); + + // store config to be used by the end point + Hybrid_Auth::storage()->config( "CONFIG", Hybrid_Auth::$config ); + + // move on + Hybrid_Logger::debug( "Hybrid_Provider_Adapter::login( {$this->id} ), redirect the user to login_start URL." ); + + Hybrid_Auth::redirect( $this->params["login_start"] ); + } + + // -------------------------------------------------------------------- + + /** + * let hybridauth forget all about the user for the current provider + */ + function logout() + { + $this->adapter->logout(); + } + + // -------------------------------------------------------------------- + + /** + * return true if the user is connected to the current provider + */ + public function isUserConnected() + { + return $this->adapter->isUserConnected(); + } + + // -------------------------------------------------------------------- + + /** + * handle : + * getUserProfile() + * getUserContacts() + * getUserActivity() + * setUserStatus() + */ + public function __call( $name, $arguments ) + { + Hybrid_Logger::info( "Enter Hybrid_Provider_Adapter::$name(), Provider: {$this->id}" ); + + if ( ! $this->isUserConnected() ){ + throw new Exception( "User not connected to the provider {$this->id}.", 7 ); + } + + if ( ! method_exists( $this->adapter, $name ) ){ + throw new Exception( "Call to undefined function Hybrid_Providers_{$this->id}::$name()." ); + } + + if( count( $arguments ) ){ + return $this->adapter->$name( $arguments[0] ); + } + else{ + return $this->adapter->$name(); + } + } + + // -------------------------------------------------------------------- + + /** + * If the user is connected, then return the access_token and access_token_secret + * if the provider api use oauth + */ + public function getAccessToken() + { + if( ! $this->adapter->isUserConnected() ){ + Hybrid_Logger::error( "User not connected to the provider." ); + + throw new Exception( "User not connected to the provider.", 7 ); + } + + return + ARRAY( + "access_token" => $this->adapter->token( "access_token" ) , // OAuth access token + "access_token_secret" => $this->adapter->token( "access_token_secret" ), // OAuth access token secret + "refresh_token" => $this->adapter->token( "refresh_token" ) , // OAuth refresh token + "expires_in" => $this->adapter->token( "expires_in" ) , // OPTIONAL. The duration in seconds of the access token lifetime + "expires_at" => $this->adapter->token( "expires_at" ) , // OPTIONAL. Timestamp when the access_token expire. if not provided by the social api, then it should be calculated: expires_at = now + expires_in + ); + } + + // -------------------------------------------------------------------- + + /** + * Naive getter of the current connected IDp API client + */ + function api() + { + if( ! $this->adapter->isUserConnected() ){ + Hybrid_Logger::error( "User not connected to the provider." ); + + throw new Exception( "User not connected to the provider.", 7 ); + } + + return $this->adapter->api; + } + + // -------------------------------------------------------------------- + + /** + * redirect the user to hauth_return_to (the callback url) + */ + function returnToCallbackUrl() + { + // get the stored callback url + $callback_url = Hybrid_Auth::storage()->get( "hauth_session.{$this->id}.hauth_return_to" ); + + // remove some unneed'd stored data + Hybrid_Auth::storage()->delete( "hauth_session.{$this->id}.hauth_return_to" ); + Hybrid_Auth::storage()->delete( "hauth_session.{$this->id}.hauth_endpoint" ); + Hybrid_Auth::storage()->delete( "hauth_session.{$this->id}.id_provider_params" ); + + // back to home + Hybrid_Auth::redirect( $callback_url ); + } + + // -------------------------------------------------------------------- + + /** + * return the provider config by id + */ + function getConfigById( $id ) + { + if( isset( Hybrid_Auth::$config["providers"][$id] ) ){ + return Hybrid_Auth::$config["providers"][$id]; + } + + return NULL; + } + + // -------------------------------------------------------------------- + + /** + * return the provider config by id; insensitive + */ + function getProviderCiId( $id ) + { + foreach( Hybrid_Auth::$config["providers"] as $idpid => $params ){ + if( strtolower( $idpid ) == strtolower( $id ) ){ + return $idpid; + } + } + + return NULL; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Model.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Model.php new file mode 100644 index 0000000..276b5cf --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Model.php @@ -0,0 +1,231 @@ +params = Hybrid_Auth::storage()->get( "hauth_session.$providerId.id_provider_params" ); + } + else{ + $this->params = $params; + } + + // idp id + $this->providerId = $providerId; + + // set HybridAuth endpoint for this provider + $this->endpoint = Hybrid_Auth::storage()->get( "hauth_session.$providerId.hauth_endpoint" ); + + // idp config + $this->config = $config; + + // new user instance + $this->user = new Hybrid_User(); + $this->user->providerId = $providerId; + + // initialize the current provider adapter + $this->initialize(); + + Hybrid_Logger::debug( "Hybrid_Provider_Model::__construct( $providerId ) initialized. dump current adapter instance: ", serialize( $this ) ); + } + + // -------------------------------------------------------------------- + + /** + * IDp wrappers initializer + * + * The main job of wrappers initializer is to performs (depend on the IDp api client it self): + * - include some libs nedded by this provider, + * - check IDp key and secret, + * - set some needed parameters (stored in $this->params) by this IDp api client + * - create and setup an instance of the IDp api client on $this->api + */ + abstract protected function initialize(); + + // -------------------------------------------------------------------- + + /** + * begin login + */ + abstract protected function loginBegin(); + + // -------------------------------------------------------------------- + + /** + * finish login + */ + abstract protected function loginFinish(); + + // -------------------------------------------------------------------- + + /** + * generic logout, just erase current provider adapter stored data to let Hybrid_Auth all forget about it + */ + function logout() + { + Hybrid_Logger::info( "Enter [{$this->providerId}]::logout()" ); + + $this->clearTokens(); + + return TRUE; + } + + // -------------------------------------------------------------------- + + /** + * grab the user profile from the IDp api client + */ + function getUserProfile() + { + Hybrid_Logger::error( "HybridAuth do not provide users contats list for {$this->providerId} yet." ); + + throw new Exception( "Provider does not support this feature.", 8 ); + } + + // -------------------------------------------------------------------- + + /** + * load the current logged in user contacts list from the IDp api client + */ + function getUserContacts() + { + Hybrid_Logger::error( "HybridAuth do not provide users contats list for {$this->providerId} yet." ); + + throw new Exception( "Provider does not support this feature.", 8 ); + } + + // -------------------------------------------------------------------- + + /** + * return the user activity stream + */ + function getUserActivity( $stream ) + { + Hybrid_Logger::error( "HybridAuth do not provide user's activity stream for {$this->providerId} yet." ); + + throw new Exception( "Provider does not support this feature.", 8 ); + } + + // -------------------------------------------------------------------- + + /** + * return the user activity stream + */ + function setUserStatus( $status ) + { + Hybrid_Logger::error( "HybridAuth do not provide user's activity stream for {$this->providerId} yet." ); + + throw new Exception( "Provider does not support this feature.", 8 ); + } + + // -------------------------------------------------------------------- + + /** + * return true if the user is connected to the current provider + */ + public function isUserConnected() + { + return (bool) Hybrid_Auth::storage()->get( "hauth_session.{$this->providerId}.is_logged_in" ); + } + + // -------------------------------------------------------------------- + + /** + * set user to connected + */ + public function setUserConnected() + { + Hybrid_Logger::info( "Enter [{$this->providerId}]::setUserConnected()" ); + + Hybrid_Auth::storage()->set( "hauth_session.{$this->providerId}.is_logged_in", 1 ); + } + + // -------------------------------------------------------------------- + + /** + * set user to unconnected + */ + public function setUserUnconnected() + { + Hybrid_Logger::info( "Enter [{$this->providerId}]::setUserUnconnected()" ); + + Hybrid_Auth::storage()->set( "hauth_session.{$this->providerId}.is_logged_in", 0 ); + } + + // -------------------------------------------------------------------- + + /** + * get or set a token + */ + public function token( $token, $value = NULL ) + { + if( $value === NULL ){ + return Hybrid_Auth::storage()->get( "hauth_session.{$this->providerId}.token.$token" ); + } + else{ + Hybrid_Auth::storage()->set( "hauth_session.{$this->providerId}.token.$token", $value ); + } + } + + // -------------------------------------------------------------------- + + /** + * delete a stored token + */ + public function deleteToken( $token ) + { + Hybrid_Auth::storage()->delete( "hauth_session.{$this->providerId}.token.$token" ); + } + + // -------------------------------------------------------------------- + + /** + * clear all existen tokens for this provider + */ + public function clearTokens() + { + Hybrid_Auth::storage()->deleteMatch( "hauth_session.{$this->providerId}." ); + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Model_OAuth1.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Model_OAuth1.php new file mode 100644 index 0000000..e9eee7a --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Model_OAuth1.php @@ -0,0 +1,161 @@ + "OK: Success!", + 304 => "Not Modified: There was no new data to return.", + 400 => "Bad Request: The request was invalid.", + 401 => "Unauthorized.", + 403 => "Forbidden: The request is understood, but it has been refused.", + 404 => "Not Found: The URI requested is invalid or the resource requested does not exists.", + 406 => "Not Acceptable.", + 500 => "Internal Server Error: Something is broken.", + 502 => "Bad Gateway.", + 503 => "Service Unavailable." + ); + + if( ! $code && $this->api ) + $code = $this->api->http_code; + + if( isset( $http_status_codes[ $code ] ) ) + return $code . " " . $http_status_codes[ $code ]; + } + + // -------------------------------------------------------------------- + + /** + * adapter initializer + */ + function initialize() + { + // 1 - check application credentials + if ( ! $this->config["keys"]["key"] || ! $this->config["keys"]["secret"] ){ + throw new Exception( "Your application key and secret are required in order to connect to {$this->providerId}.", 4 ); + } + + // 2 - include OAuth lib and client + require_once Hybrid_Auth::$config["path_libraries"] . "OAuth/OAuth.php"; + require_once Hybrid_Auth::$config["path_libraries"] . "OAuth/OAuth1Client.php"; + + // 3.1 - setup access_token if any stored + if( $this->token( "access_token" ) ){ + $this->api = new OAuth1Client( + $this->config["keys"]["key"], $this->config["keys"]["secret"], + $this->token( "access_token" ), $this->token( "access_token_secret" ) + ); + } + + // 3.2 - setup request_token if any stored, in order to exchange with an access token + elseif( $this->token( "request_token" ) ){ + $this->api = new OAuth1Client( + $this->config["keys"]["key"], $this->config["keys"]["secret"], + $this->token( "request_token" ), $this->token( "request_token_secret" ) + ); + } + + // 3.3 - instanciate OAuth client with client credentials + else{ + $this->api = new OAuth1Client( $this->config["keys"]["key"], $this->config["keys"]["secret"] ); + } + + // Set curl proxy if exist + if( isset( Hybrid_Auth::$config["proxy"] ) ){ + $this->api->curl_proxy = Hybrid_Auth::$config["proxy"]; + } + } + + // -------------------------------------------------------------------- + + /** + * begin login step + */ + function loginBegin() + { + $tokens = $this->api->requestToken( $this->endpoint ); + + // request tokens as recived from provider + $this->request_tokens_raw = $tokens; + + // check the last HTTP status code returned + if ( $this->api->http_code != 200 ){ + throw new Exception( "Authentification failed! {$this->providerId} returned an error. " . $this->errorMessageByStatus( $this->api->http_code ), 5 ); + } + + if ( ! isset( $tokens["oauth_token"] ) ){ + throw new Exception( "Authentification failed! {$this->providerId} returned an invalid oauth token.", 5 ); + } + + $this->token( "request_token" , $tokens["oauth_token"] ); + $this->token( "request_token_secret", $tokens["oauth_token_secret"] ); + + # redirect the user to the provider authentication url + Hybrid_Auth::redirect( $this->api->authorizeUrl( $tokens ) ); + } + + // -------------------------------------------------------------------- + + /** + * finish login step + */ + function loginFinish() + { + $oauth_token = (array_key_exists('oauth_token',$_REQUEST))?$_REQUEST['oauth_token']:""; + $oauth_verifier = (array_key_exists('oauth_verifier',$_REQUEST))?$_REQUEST['oauth_verifier']:""; + + if ( ! $oauth_token || ! $oauth_verifier ){ + throw new Exception( "Authentification failed! {$this->providerId} returned an invalid oauth verifier.", 5 ); + } + + // request an access token + $tokens = $this->api->accessToken( $oauth_verifier ); + + // access tokens as recived from provider + $this->access_tokens_raw = $tokens; + + // check the last HTTP status code returned + if ( $this->api->http_code != 200 ){ + throw new Exception( "Authentification failed! {$this->providerId} returned an error. " . $this->errorMessageByStatus( $this->api->http_code ), 5 ); + } + + // we should have an access_token, or else, something has gone wrong + if ( ! isset( $tokens["oauth_token"] ) ){ + throw new Exception( "Authentification failed! {$this->providerId} returned an invalid access token.", 5 ); + } + + // we no more need to store requet tokens + $this->deleteToken( "request_token" ); + $this->deleteToken( "request_token_secret" ); + + // sotre access_token for later user + $this->token( "access_token" , $tokens['oauth_token'] ); + $this->token( "access_token_secret" , $tokens['oauth_token_secret'] ); + + // set user as logged in to the current provider + $this->setUserConnected(); + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Model_OAuth2.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Model_OAuth2.php new file mode 100644 index 0000000..5d5eb46 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Model_OAuth2.php @@ -0,0 +1,176 @@ + "OK: Success!", + 304 => "Not Modified: There was no new data to return.", + 400 => "Bad Request: The request was invalid.", + 401 => "Unauthorized.", + 403 => "Forbidden: The request is understood, but it has been refused.", + 404 => "Not Found: The URI requested is invalid or the resource requested does not exists.", + 406 => "Not Acceptable.", + 500 => "Internal Server Error: Something is broken.", + 502 => "Bad Gateway.", + 503 => "Service Unavailable." + ); + + if( ! $code && $this->api ) + $code = $this->api->http_code; + + if( isset( $http_status_codes[ $code ] ) ) + return $code . " " . $http_status_codes[ $code ]; + } + + // -------------------------------------------------------------------- + + /** + * adapter initializer + */ + function initialize() + { + if ( ! $this->config["keys"]["id"] || ! $this->config["keys"]["secret"] ){ + throw new Exception( "Your application id and secret are required in order to connect to {$this->providerId}.", 4 ); + } + + // override requested scope + if( isset( $this->config["scope"] ) && ! empty( $this->config["scope"] ) ){ + $this->scope = $this->config["scope"]; + } + + // include OAuth2 client + require_once Hybrid_Auth::$config["path_libraries"] . "OAuth/OAuth2Client.php"; + + // create a new OAuth2 client instance + $this->api = new OAuth2Client( $this->config["keys"]["id"], $this->config["keys"]["secret"], $this->endpoint ); + + // If we have an access token, set it + if( $this->token( "access_token" ) ){ + $this->api->access_token = $this->token( "access_token" ); + $this->api->refresh_token = $this->token( "refresh_token" ); + $this->api->access_token_expires_in = $this->token( "expires_in" ); + $this->api->access_token_expires_at = $this->token( "expires_at" ); + } + + // Set curl proxy if exist + if( isset( Hybrid_Auth::$config["proxy"] ) ){ + $this->api->curl_proxy = Hybrid_Auth::$config["proxy"]; + } + } + + // -------------------------------------------------------------------- + + /** + * begin login step + */ + function loginBegin() + { + // redirect the user to the provider authentication url + Hybrid_Auth::redirect( $this->api->authorizeUrl( array( "scope" => $this->scope ) ) ); + } + + // -------------------------------------------------------------------- + + /** + * finish login step + */ + function loginFinish() + { + $error = (array_key_exists('error',$_REQUEST))?$_REQUEST['error']:""; + + // check for errors + if ( $error ){ + throw new Exception( "Authentification failed! {$this->providerId} returned an error: $error", 5 ); + } + + // try to authenicate user + $code = (array_key_exists('code',$_REQUEST))?$_REQUEST['code']:""; + + try{ + $this->api->authenticate( $code ); + } + catch( Exception $e ){ + throw new Exception( "User profile request failed! {$this->providerId} returned an error: $e", 6 ); + } + + // check if authenticated + if ( ! $this->api->access_token ){ + throw new Exception( "Authentification failed! {$this->providerId} returned an invalid access token.", 5 ); + } + + // store tokens + $this->token( "access_token" , $this->api->access_token ); + $this->token( "refresh_token", $this->api->refresh_token ); + $this->token( "expires_in" , $this->api->access_token_expires_in ); + $this->token( "expires_at" , $this->api->access_token_expires_at ); + + // set user connected locally + $this->setUserConnected(); + } + + function refreshToken() + { + // have an access token? + if( $this->api->access_token ){ + + // have to refresh? + if( $this->api->refresh_token && $this->api->access_token_expires_at ){ + + // expired? + if( $this->api->access_token_expires_at <= time() ){ + $response = $this->api->refreshToken( array( "refresh_token" => $this->api->refresh_token ) ); + + if( ! isset( $response->access_token ) || ! $response->access_token ){ + // set the user as disconnected at this point and throw an exception + $this->setUserUnconnected(); + + throw new Exception( "The Authorization Service has return an invalid response while requesting a new access token. " . (string) $response->error ); + } + + // set new access_token + $this->api->access_token = $response->access_token; + + if( isset( $response->refresh_token ) ) + $this->api->refresh_token = $response->refresh_token; + + if( isset( $response->expires_in ) ){ + $this->api->access_token_expires_in = $response->expires_in; + + // even given by some idp, we should calculate this + $this->api->access_token_expires_at = time() + $response->expires_in; + } + } + } + + // re store tokens + $this->token( "access_token" , $this->api->access_token ); + $this->token( "refresh_token", $this->api->refresh_token ); + $this->token( "expires_in" , $this->api->access_token_expires_in ); + $this->token( "expires_at" , $this->api->access_token_expires_at ); + } + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Model_OpenID.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Model_OpenID.php new file mode 100644 index 0000000..64405e0 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Provider_Model_OpenID.php @@ -0,0 +1,169 @@ +public $openidIdentifier = ""; + * + * Hybrid_Provider_Model_OpenID use LightOpenID lib which can be found on + * Hybrid/thirdparty/OpenID/LightOpenID.php + */ +class Hybrid_Provider_Model_OpenID extends Hybrid_Provider_Model +{ + /* Openid provider identifier */ + public $openidIdentifier = ""; + + // -------------------------------------------------------------------- + + /** + * adapter initializer + */ + function initialize() + { + if( isset( $this->params["openid_identifier"] ) ){ + $this->openidIdentifier = $this->params["openid_identifier"]; + } + + // include LightOpenID lib + require_once Hybrid_Auth::$config["path_libraries"] . "OpenID/LightOpenID.php"; + + $this->api = new LightOpenID( parse_url( Hybrid_Auth::$config["base_url"], PHP_URL_HOST), Hybrid_Auth::$config["proxy"] ); + } + + // -------------------------------------------------------------------- + + /** + * begin login step + */ + function loginBegin() + { + if( empty( $this->openidIdentifier ) ){ + throw new Exception( "OpenID adapter require the identity provider identifier 'openid_identifier' as an extra parameter.", 4 ); + } + + $this->api->identity = $this->openidIdentifier; + $this->api->returnUrl = $this->endpoint; + $this->api->required = ARRAY( + 'namePerson/first' , + 'namePerson/last' , + 'namePerson/friendly' , + 'namePerson' , + + 'contact/email' , + + 'birthDate' , + 'birthDate/birthDay' , + 'birthDate/birthMonth' , + 'birthDate/birthYear' , + + 'person/gender' , + 'pref/language' , + + 'contact/postalCode/home', + 'contact/city/home' , + 'contact/country/home' , + + 'media/image/default' , + ); + + # redirect the user to the provider authentication url + Hybrid_Auth::redirect( $this->api->authUrl() ); + } + + // -------------------------------------------------------------------- + + /** + * finish login step + */ + function loginFinish() + { + # if user don't garant acess of their data to your site, halt with an Exception + if( $this->api->mode == 'cancel'){ + throw new Exception( "Authentification failed! User has canceled authentication!", 5 ); + } + + # if something goes wrong + if( ! $this->api->validate() ){ + throw new Exception( "Authentification failed. Invalid request recived!", 5 ); + } + + # fetch recived user data + $response = $this->api->getAttributes(); + + # sotre the user profile + $this->user->profile->identifier = $this->api->identity; + + $this->user->profile->firstName = (array_key_exists("namePerson/first",$response))?$response["namePerson/first"]:""; + $this->user->profile->lastName = (array_key_exists("namePerson/last",$response))?$response["namePerson/last"]:""; + $this->user->profile->displayName = (array_key_exists("namePerson",$response))?$response["namePerson"]:""; + $this->user->profile->email = (array_key_exists("contact/email",$response))?$response["contact/email"]:""; + $this->user->profile->language = (array_key_exists("pref/language",$response))?$response["pref/language"]:""; + $this->user->profile->country = (array_key_exists("contact/country/home",$response))?$response["contact/country/home"]:""; + $this->user->profile->zip = (array_key_exists("contact/postalCode/home",$response))?$response["contact/postalCode/home"]:""; + $this->user->profile->gender = (array_key_exists("person/gender",$response))?$response["person/gender"]:""; + $this->user->profile->photoURL = (array_key_exists("media/image/default",$response))?$response["media/image/default"]:""; + + $this->user->profile->birthDay = (array_key_exists("birthDate/birthDay",$response))?$response["birthDate/birthDay"]:""; + $this->user->profile->birthMonth = (array_key_exists("birthDate/birthMonth",$response))?$response["birthDate/birthMonth"]:""; + $this->user->profile->birthYear = (array_key_exists("birthDate/birthDate",$response))?$response["birthDate/birthDate"]:""; + + if( ! $this->user->profile->displayName ) { + $this->user->profile->displayName = trim( $this->user->profile->lastName . " " . $this->user->profile->firstName ); + } + + if( isset( $response['namePerson/friendly'] ) && ! empty( $response['namePerson/friendly'] ) && ! $this->user->profile->displayName ) { + $this->user->profile->displayName = (array_key_exists("namePerson/friendly",$response))?$response["namePerson/friendly"]:"" ; + } + + if( isset( $response['birthDate'] ) && ! empty( $response['birthDate'] ) && ! $this->user->profile->birthDay ) { + list( $birthday_year, $birthday_month, $birthday_day ) = (array_key_exists('birthDate',$response))?$response['birthDate']:""; + + $this->user->profile->birthDay = (int) $birthday_day; + $this->user->profile->birthMonth = (int) $birthday_month; + $this->user->profile->birthYear = (int) $birthday_year; + } + + if( ! $this->user->profile->displayName ){ + $this->user->profile->displayName = trim( $this->user->profile->firstName . " " . $this->user->profile->lastName ); + } + + if( $this->user->profile->gender == "f" ){ + $this->user->profile->gender = "female"; + } + + if( $this->user->profile->gender == "m" ){ + $this->user->profile->gender = "male"; + } + + // set user as logged in + $this->setUserConnected(); + + // with openid providers we get the user profile only once, so store it + Hybrid_Auth::storage()->set( "hauth_session.{$this->providerId}.user", $this->user ); + } + + // -------------------------------------------------------------------- + + /** + * load the user profile from the IDp api client + */ + function getUserProfile() + { + // try to get the user profile from stored data + $this->user = Hybrid_Auth::storage()->get( "hauth_session.{$this->providerId}.user" ) ; + + // if not found + if ( ! is_object( $this->user ) ){ + throw new Exception( "User profile request failed! User is not connected to {$this->providerId} or his session has expired.", 6 ); + } + + return $this->user->profile; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/AOL.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/AOL.php new file mode 100644 index 0000000..f8e7d70 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/AOL.php @@ -0,0 +1,16 @@ +config["keys"]["id"] || ! $this->config["keys"]["secret"] ){ + throw new Exception( "Your application id and secret are required in order to connect to {$this->providerId}.", 4 ); + } + + if ( ! class_exists('FacebookApiException') ) { + require_once Hybrid_Auth::$config["path_libraries"] . "Facebook/base_facebook.php"; + require_once Hybrid_Auth::$config["path_libraries"] . "Facebook/facebook.php"; + } + + $this->api = new Facebook( ARRAY( 'appId' => $this->config["keys"]["id"], 'secret' => $this->config["keys"]["secret"] ) ); + + if ( $this->token("access_token") ) { + $access_token = $this->api->extendedAccessToken( $this->token("access_token") ); + + if( $access_token ){ + $this->token("access_token", $access_token ); + $this->api->setAccessToken( $access_token ); + } + + $this->api->setAccessToken( $this->token("access_token") ); + } + + $this->api->getUser(); + } + + /** + * begin login step + * + * simply call Facebook::require_login(). + */ + function loginBegin() + { + $parameters = array("scope" => $this->scope, "redirect_uri" => $this->endpoint, "display" => "page"); + $optionals = array("scope", "redirect_uri", "display"); + + foreach ($optionals as $parameter){ + if( isset( $this->config[$parameter] ) && ! empty( $this->config[$parameter] ) ){ + $parameters[$parameter] = $this->config[$parameter]; + } + } + + // get the login url + $url = $this->api->getLoginUrl( $parameters ); + + // redirect to facebook + Hybrid_Auth::redirect( $url ); + } + + /** + * finish login step + */ + function loginFinish() + { + // in case we get error_reason=user_denied&error=access_denied + if ( isset( $_REQUEST['error'] ) && $_REQUEST['error'] == "access_denied" ){ + throw new Exception( "Authentification failed! The user denied your request.", 5 ); + } + + // try to get the UID of the connected user from fb, should be > 0 + if ( ! $this->api->getUser() ){ + throw new Exception( "Authentification failed! {$this->providerId} returned an invalide user id.", 5 ); + } + + // set user as logged in + $this->setUserConnected(); + + // store facebook access token + $this->token( "access_token", $this->api->getAccessToken() ); + } + + /** + * logout + */ + function logout() + { + $this->api->destroySession(); + + parent::logout(); + } + + /** + * load the user profile from the IDp api client + */ + function getUserProfile() + { + // request user profile from fb api + try{ + $data = $this->api->api('/me'); + } + catch( FacebookApiException $e ){ + throw new Exception( "User profile request failed! {$this->providerId} returned an error: $e", 6 ); + } + + // if the provider identifier is not recived, we assume the auth has failed + if ( ! isset( $data["id"] ) ){ + throw new Exception( "User profile request failed! {$this->providerId} api returned an invalid response.", 6 ); + } + + # store the user profile. + $this->user->profile->identifier = (array_key_exists('id',$data))?$data['id']:""; + $this->user->profile->displayName = (array_key_exists('name',$data))?$data['name']:""; + $this->user->profile->firstName = (array_key_exists('first_name',$data))?$data['first_name']:""; + $this->user->profile->lastName = (array_key_exists('last_name',$data))?$data['last_name']:""; + $this->user->profile->photoURL = "https://graph.facebook.com/" . $this->user->profile->identifier . "/picture?type=square"; + $this->user->profile->profileURL = (array_key_exists('link',$data))?$data['link']:""; + $this->user->profile->webSiteURL = (array_key_exists('website',$data))?$data['website']:""; + $this->user->profile->gender = (array_key_exists('gender',$data))?$data['gender']:""; + $this->user->profile->description = (array_key_exists('bio',$data))?$data['bio']:""; + $this->user->profile->email = (array_key_exists('email',$data))?$data['email']:""; + $this->user->profile->emailVerified = (array_key_exists('email',$data))?$data['email']:""; + $this->user->profile->region = (array_key_exists("hometown",$data)&&array_key_exists("name",$data['hometown']))?$data['hometown']["name"]:""; + + if( array_key_exists('birthday',$data) ) { + list($birthday_month, $birthday_day, $birthday_year) = explode( "/", $data['birthday'] ); + + $this->user->profile->birthDay = (int) $birthday_day; + $this->user->profile->birthMonth = (int) $birthday_month; + $this->user->profile->birthYear = (int) $birthday_year; + } + + return $this->user->profile; + } + + /** + * load the user contacts + */ + function getUserContacts() + { + try{ + $response = $this->api->api('/me/friends'); + } + catch( FacebookApiException $e ){ + throw new Exception( "User contacts request failed! {$this->providerId} returned an error: $e" ); + } + + if( ! $response || ! count( $response["data"] ) ){ + return ARRAY(); + } + + $contacts = ARRAY(); + + foreach( $response["data"] as $item ){ + $uc = new Hybrid_User_Contact(); + + $uc->identifier = (array_key_exists("id",$item))?$item["id"]:""; + $uc->displayName = (array_key_exists("name",$item))?$item["name"]:""; + $uc->profileURL = "https://www.facebook.com/profile.php?id=" . $uc->identifier; + $uc->photoURL = "https://graph.facebook.com/" . $uc->identifier . "/picture?type=square"; + + $contacts[] = $uc; + } + + return $contacts; + } + + /** + * update user status + */ + function setUserStatus( $status ) + { + $parameters = array(); + + if( is_array( $status ) ){ + $parameters = $status; + } + else{ + $parameters["message"] = $status; + } + + try{ + $response = $this->api->api( "/me/feed", "post", $parameters ); + } + catch( FacebookApiException $e ){ + throw new Exception( "Update user status failed! {$this->providerId} returned an error: $e" ); + } + } + + /** + * load the user latest activity + * - timeline : all the stream + * - me : the user activity only + */ + function getUserActivity( $stream ) + { + try{ + if( $stream == "me" ){ + $response = $this->api->api( '/me/feed' ); + } + else{ + $response = $this->api->api('/me/home'); + } + } + catch( FacebookApiException $e ){ + throw new Exception( "User activity stream request failed! {$this->providerId} returned an error: $e" ); + } + + if( ! $response || ! count( $response['data'] ) ){ + return ARRAY(); + } + + $activities = ARRAY(); + + foreach( $response['data'] as $item ){ + if( $stream == "me" && $item["from"]["id"] != $this->api->getUser() ){ + continue; + } + + $ua = new Hybrid_User_Activity(); + + $ua->id = (array_key_exists("id",$item))?$item["id"]:""; + $ua->date = (array_key_exists("created_time",$item))?strtotime($item["created_time"]):""; + + if( $item["type"] == "video" ){ + $ua->text = (array_key_exists("link",$item))?$item["link"]:""; + } + + if( $item["type"] == "link" ){ + $ua->text = (array_key_exists("link",$item))?$item["link"]:""; + } + + if( empty( $ua->text ) && isset( $item["story"] ) ){ + $ua->text = (array_key_exists("link",$item))?$item["link"]:""; + } + + if( empty( $ua->text ) && isset( $item["message"] ) ){ + $ua->text = (array_key_exists("message",$item))?$item["message"]:""; + } + + if( ! empty( $ua->text ) ){ + $ua->user->identifier = (array_key_exists("id",$item["from"]))?$item["from"]["id"]:""; + $ua->user->displayName = (array_key_exists("name",$item["from"]))?$item["from"]["name"]:""; + $ua->user->profileURL = "https://www.facebook.com/profile.php?id=" . $ua->user->identifier; + $ua->user->photoURL = "https://graph.facebook.com/" . $ua->user->identifier . "/picture?type=square"; + + $activities[] = $ua; + } + } + + return $activities; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/Foursquare.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/Foursquare.php new file mode 100644 index 0000000..0ac49bf --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/Foursquare.php @@ -0,0 +1,56 @@ +api->api_base_url = "https://api.foursquare.com/v2/"; + $this->api->authorize_url = "https://foursquare.com/oauth2/authenticate"; + $this->api->token_url = "https://foursquare.com/oauth2/access_token"; + + $this->api->sign_token_name = "oauth_token"; + } + + /** + * load the user profile from the IDp api client + */ + function getUserProfile() + { + $data = $this->api->api( "users/self" ); + + if ( ! isset( $data->response->user->id ) ){ + throw new Exception( "User profile request failed! {$this->providerId} returned an invalide response.", 6 ); + } + + $data = $data->response->user; + + $this->user->profile->identifier = $data->id; + $this->user->profile->firstName = $data->firstName; + $this->user->profile->lastName = $data->lastName; + $this->user->profile->displayName = trim( $this->user->profile->firstName . " " . $this->user->profile->lastName ); + $this->user->profile->photoURL = $data->photo; + $this->user->profile->profileURL = "https://www.foursquare.com/user/" . $data->id; + $this->user->profile->gender = $data->gender; + $this->user->profile->city = $data->homeCity; + $this->user->profile->email = $data->contact->email; + $this->user->profile->emailVerified = $data->contact->email; + + return $this->user->profile; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/Google.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/Google.php new file mode 100644 index 0000000..434cfff --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/Google.php @@ -0,0 +1,119 @@ +api->authorize_url = "https://accounts.google.com/o/oauth2/auth"; + $this->api->token_url = "https://accounts.google.com/o/oauth2/token"; + $this->api->token_info_url = "https://www.googleapis.com/oauth2/v1/tokeninfo"; + } + + /** + * begin login step + */ + function loginBegin() + { + $parameters = array("scope" => $this->scope, "access_type" => "offline"); + $optionals = array("scope", "access_type", "redirect_uri", "approval_prompt"); + + foreach ($optionals as $parameter){ + if( isset( $this->config[$parameter] ) && ! empty( $this->config[$parameter] ) ){ + $parameters[$parameter] = $this->config[$parameter]; + } + } + + Hybrid_Auth::redirect( $this->api->authorizeUrl( $parameters ) ); + } + + /** + * load the user profile from the IDp api client + */ + function getUserProfile() + { + // refresh tokens if needed + $this->refreshToken(); + + // ask google api for user infos + $response = $this->api->api( "https://www.googleapis.com/oauth2/v1/userinfo" ); + + if ( ! isset( $response->id ) || isset( $response->error ) ){ + throw new Exception( "User profile request failed! {$this->providerId} returned an invalide response.", 6 ); + } + + $this->user->profile->identifier = (property_exists($response,'id'))?$response->id:""; + $this->user->profile->firstName = (property_exists($response,'given_name'))?$response->given_name:""; + $this->user->profile->lastName = (property_exists($response,'family_name'))?$response->family_name:""; + $this->user->profile->displayName = (property_exists($response,'name'))?$response->name:""; + $this->user->profile->photoURL = (property_exists($response,'picture'))?$response->picture:""; + $this->user->profile->profileURL = "https://profiles.google.com/" . $this->user->profile->identifier; + $this->user->profile->gender = (property_exists($response,'gender'))?$response->gender:""; + $this->user->profile->email = (property_exists($response,'email'))?$response->email:""; + $this->user->profile->emailVerified = (property_exists($response,'email'))?$response->email:""; + $this->user->profile->language = (property_exists($response,'locale'))?$response->locale:""; + + if( property_exists($response,'birthday') ){ + list($birthday_year, $birthday_month, $birthday_day) = explode( '-', $response->birthday ); + + $this->user->profile->birthDay = (int) $birthday_day; + $this->user->profile->birthMonth = (int) $birthday_month; + $this->user->profile->birthYear = (int) $birthday_year; + } + + return $this->user->profile; + } + + /** + * load the user (Gmail) contacts + * ..toComplete + */ + function getUserContacts() + { + // refresh tokens if needed + $this->refreshToken(); + + if( ! isset( $this->config['contacts_param'] ) ){ + $this->config['contacts_param'] = array( "max-results" => 500 ); + } + + $response = $this->api->api( "https://www.google.com/m8/feeds/contacts/default/full?" + . http_build_query( array_merge( array('alt' => 'json'), $this->config['contacts_param'] ) ) ); + + if( ! $response ){ + return ARRAY(); + } + + $contacts = ARRAY(); + + foreach( $response->feed->entry as $idx => $entry ){ + $uc = new Hybrid_User_Contact(); + + $uc->email = isset($entry->{'gd$email'}[0]->address) ? (string) $entry->{'gd$email'}[0]->address : ''; + $uc->displayName = isset($entry->title->{'$t'}) ? (string) $entry->title->{'$t'} : ''; + $uc->identifier = $uc->email; + + $contacts[] = $uc; + } + + return $contacts; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/LinkedIn.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/LinkedIn.php new file mode 100644 index 0000000..5e755c1 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/LinkedIn.php @@ -0,0 +1,247 @@ +config["keys"]["key"] || ! $this->config["keys"]["secret"] ){ + throw new Exception( "Your application key and secret are required in order to connect to {$this->providerId}.", 4 ); + } + + require_once Hybrid_Auth::$config["path_libraries"] . "OAuth/OAuth.php"; + require_once Hybrid_Auth::$config["path_libraries"] . "LinkedIn/LinkedIn.php"; + + $this->api = new LinkedIn( array( 'appKey' => $this->config["keys"]["key"], 'appSecret' => $this->config["keys"]["secret"], 'callbackUrl' => $this->endpoint ) ); + + if( $this->token( "access_token_linkedin" ) ){ + $this->api->setTokenAccess( $this->token( "access_token_linkedin" ) ); + } + } + + /** + * begin login step + */ + function loginBegin() + { + // send a request for a LinkedIn access token + $response = $this->api->retrieveTokenRequest(); + + if( isset( $response['success'] ) && $response['success'] === TRUE ){ + $this->token( "oauth_token", $response['linkedin']['oauth_token'] ); + $this->token( "oauth_token_secret", $response['linkedin']['oauth_token_secret'] ); + + # redirect user to LinkedIn authorisation web page + Hybrid_Auth::redirect( LINKEDIN::_URL_AUTH . $response['linkedin']['oauth_token'] ); + } + else{ + throw new Exception( "Authentification failed! {$this->providerId} returned an invalid Token.", 5 ); + } + } + + /** + * finish login step + */ + function loginFinish() + { + $oauth_token = $_REQUEST['oauth_token']; + $oauth_verifier = $_REQUEST['oauth_verifier']; + + if ( ! $oauth_verifier ){ + throw new Exception( "Authentification failed! {$this->providerId} returned an invalid Token.", 5 ); + } + + $response = $this->api->retrieveTokenAccess( $oauth_token, $this->token( "oauth_token_secret" ), $oauth_verifier ); + + if( isset( $response['success'] ) && $response['success'] === TRUE ){ + $this->deleteToken( "oauth_token" ); + $this->deleteToken( "oauth_token_secret" ); + + $this->token( "access_token_linkedin", $response['linkedin'] ); + $this->token( "access_token" , $response['linkedin']['oauth_token'] ); + $this->token( "access_token_secret" , $response['linkedin']['oauth_token_secret'] ); + + // set user as logged in + $this->setUserConnected(); + } + else{ + throw new Exception( "Authentification failed! {$this->providerId} returned an invalid Token.", 5 ); + } + } + + /** + * load the user profile from the IDp api client + */ + function getUserProfile() + { + try{ + // http://developer.linkedin.com/docs/DOC-1061 + $response = $this->api->profile('~:(id,first-name,last-name,public-profile-url,picture-url,email-address,date-of-birth,phone-numbers,summary)'); + } + catch( LinkedInException $e ){ + throw new Exception( "User profile request failed! {$this->providerId} returned an error: $e", 6 ); + } + + if( isset( $response['success'] ) && $response['success'] === TRUE ){ + $data = @ new SimpleXMLElement( $response['linkedin'] ); + + if ( ! is_object( $data ) ){ + throw new Exception( "User profile request failed! {$this->providerId} returned an invalide xml data.", 6 ); + } + + $this->user->profile->identifier = (string) $data->{'id'}; + $this->user->profile->firstName = (string) $data->{'first-name'}; + $this->user->profile->lastName = (string) $data->{'last-name'}; + $this->user->profile->displayName = trim( $this->user->profile->firstName . " " . $this->user->profile->lastName ); + + $this->user->profile->email = (string) $data->{'email-address'}; + $this->user->profile->emailVerified = (string) $data->{'email-address'}; + + $this->user->profile->photoURL = (string) $data->{'picture-url'}; + $this->user->profile->profileURL = (string) $data->{'public-profile-url'}; + $this->user->profile->description = (string) $data->{'summary'}; + + $this->user->profile->phone = (string) $data->{'phone-numbers'}->{'phone-number'}->{'phone-number'}; + + if( $data->{'date-of-birth'} ) { + $this->user->profile->birthDay = (string) $data->{'date-of-birth'}->day; + $this->user->profile->birthMonth = (string) $data->{'date-of-birth'}->month; + $this->user->profile->birthYear = (string) $data->{'date-of-birth'}->year; + } + + return $this->user->profile; + } + else{ + throw new Exception( "User profile request failed! {$this->providerId} returned an invalid response.", 6 ); + } + } + + /** + * load the user contacts + */ + function getUserContacts() + { + try{ + $response = $this->api->profile('~/connections:(id,first-name,last-name,picture-url,public-profile-url,summary)'); + } + catch( LinkedInException $e ){ + throw new Exception( "User contacts request failed! {$this->providerId} returned an error: $e" ); + } + + if( ! $response || ! $response['success'] ){ + return ARRAY(); + } + + $connections = new SimpleXMLElement( $response['linkedin'] ); + + $contacts = ARRAY(); + + foreach( $connections->person as $connection ) { + $uc = new Hybrid_User_Contact(); + + $uc->identifier = (string) $connection->id; + $uc->displayName = (string) $connection->{'last-name'} . " " . $connection->{'first-name'}; + $uc->profileURL = (string) $connection->{'public-profile-url'}; + $uc->photoURL = (string) $connection->{'picture-url'}; + $uc->description = (string) $connection->{'summary'}; + + $contacts[] = $uc; + } + + return $contacts; + } + + /** + * update user status + */ + function setUserStatus( $status ) + { + $parameters = array(); + $private = true; // share with your connections only + + if( is_array( $status ) ){ + if( isset( $status[0] ) && ! empty( $status[0] ) ) $parameters["title"] = $status[0]; // post title + if( isset( $status[1] ) && ! empty( $status[1] ) ) $parameters["comment"] = $status[1]; // post comment + if( isset( $status[2] ) && ! empty( $status[2] ) ) $parameters["submitted-url"] = $status[2]; // post url + if( isset( $status[3] ) && ! empty( $status[3] ) ) $parameters["submitted-image-url"] = $status[3]; // post picture url + if( isset( $status[4] ) && ! empty( $status[4] ) ) $private = $status[4]; // true or false + } + else{ + $parameters["comment"] = $status; + } + + try{ + $response = $this->api->share( 'new', $parameters, $private ); + } + catch( LinkedInException $e ){ + throw new Exception( "Update user status update failed! {$this->providerId} returned an error: $e" ); + } + + if ( ! $response || ! $response['success'] ) + { + throw new Exception( "Update user status update failed! {$this->providerId} returned an error." ); + } + } + + /** + * load the user latest activity + * - timeline : all the stream + * - me : the user activity only + */ + function getUserActivity( $stream ) + { + try{ + if( $stream == "me" ){ + $response = $this->api->updates( '?type=SHAR&scope=self&count=25' ); + } + else{ + $response = $this->api->updates( '?type=SHAR&count=25' ); + } + } + catch( LinkedInException $e ){ + throw new Exception( "User activity stream request failed! {$this->providerId} returned an error: $e" ); + } + + if( ! $response || ! $response['success'] ){ + return ARRAY(); + } + + $updates = new SimpleXMLElement( $response['linkedin'] ); + + $activities = ARRAY(); + + foreach( $updates->update as $update ) { + $person = $update->{'update-content'}->person; + $share = $update->{'update-content'}->person->{'current-share'}; + + $ua = new Hybrid_User_Activity(); + + $ua->id = (string) $update->id; + $ua->date = (string) $update->timestamp; + $ua->text = (string) $share->{'comment'}; + + $ua->user->identifier = (string) $person->id; + $ua->user->displayName = (string) $person->{'first-name'} . ' ' . $person->{'last-name'}; + $ua->user->profileURL = (string) $person->{'site-standard-profile-request'}->url; + $ua->user->photoURL = NULL; + + $activities[] = $ua; + } + + return $activities; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/Live.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/Live.php new file mode 100644 index 0000000..a63bbef --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/Live.php @@ -0,0 +1,106 @@ + + * @version 0.2 + * @license BSD License + */ + +/** + * Hybrid_Providers_Live - Windows Live provider adapter based on OAuth2 protocol + */ +class Hybrid_Providers_Live extends Hybrid_Provider_Model_OAuth2 +{ + // default permissions + public $scope = "wl.basic wl.emails wl.signin wl.share wl.birthday"; + + + /** + * IDp wrappers initializer + */ + function initialize() + { + parent::initialize(); + + // Provider api end-points + $this->api->api_base_url = "https://apis.live.net/v5.0/"; + $this->api->authorize_url = "https://oauth.live.com/authorize"; + $this->api->token_url = 'https://oauth.live.com/token'; + + $this->api->curl_authenticate_method = "GET"; + } + + /** + * grab the user profile from the api client + */ + function getUserProfile() + { + $data = $this->api->get( "me" ); + + if ( ! isset( $data->id ) ){ + throw new Exception( "User profile request failed! {$this->providerId} returned an invalide response.", 6 ); + } + + $this->user->profile->identifier = (property_exists($data,'id'))?$data->id:""; + $this->user->profile->firstName = (property_exists($data,'first_name'))?$data->first_name:""; + $this->user->profile->lastName = (property_exists($data,'last_name'))?$data->last_name:""; + $this->user->profile->displayName = (property_exists($data,'name'))?trim( $data->name ):""; + $this->user->profile->gender = (property_exists($data,'gender'))?$data->gender:""; + + //wl.basic + $this->user->profile->profileURL = (property_exists($data,'link'))?$data->link:""; + + //wl.emails + $this->user->profile->email = (property_exists($data,'emails'))?$data->emails->account:""; + $this->user->profile->emailVerified = (property_exists($data,'emails'))?$data->emails->account:""; + + //wl.birthday + $this->user->profile->birthDay = (property_exists($data,'birth_day'))?$data->birth_day:""; + $this->user->profile->birthMonth = (property_exists($data,'birth_month'))?$data->birth_month:""; + $this->user->profile->birthYear = (property_exists($data,'birth_year'))?$data->birth_year:""; + + return $this->user->profile; + } + + + /** + * load the current logged in user contacts list from the IDp api client + */ + + /* Windows Live api does not support retrieval of email addresses (only hashes :/) */ + function getUserContacts() + { + $response = $this->api->get( 'me/contacts' ); + + if ( $this->api->http_code != 200 ) + { + throw new Exception( 'User contacts request failed! ' . $this->providerId . ' returned an error: ' . $this->errorMessageByStatus( $this->api->http_code ) ); + } + + if ( ! $response->data && ( $response->error != 0 ) ) + { + return array(); + } + + $contacts = array(); + + foreach( $response->data as $item ) { + $uc = new Hybrid_User_Contact(); + + $uc->identifier = (property_exists($item,'id'))?$item->id:""; + $uc->displayName = (property_exists($item,'name'))?$item->name:""; + + $contacts[] = $uc; + } + + return $contacts; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/MySpace.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/MySpace.php new file mode 100644 index 0000000..cf3f6a4 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/MySpace.php @@ -0,0 +1,164 @@ +api->api_endpoint_url = "http://api.myspace.com/v1/"; + $this->api->authorize_url = "http://api.myspace.com/authorize"; + $this->api->request_token_url = "http://api.myspace.com/request_token"; + $this->api->access_token_url = "http://api.myspace.com/access_token"; + } + + /** + * get the connected uid from myspace api + */ + public function getCurrentUserId() + { + $response = $this->api->get( 'http://api.myspace.com/v1/user.json' ); + + if ( ! isset( $response->userId ) ){ + throw new Exception( "User id request failed! {$this->providerId} returned an invalide response." ); + } + + return $response->userId; + } + + /** + * load the user profile from the IDp api client + */ + function getUserProfile() + { + $userId = $this->getCurrentUserId(); + + $data = $this->api->get( 'http://api.myspace.com/v1/users/' . $userId . '/profile.json' ); + + if ( ! is_object( $data ) ){ + throw new Exception( "User profile request failed! {$this->providerId} returned an invalide response.", 6 ); + } + + $this->user->profile->identifier = $userId; + $this->user->profile->displayName = $data->basicprofile->name; + $this->user->profile->description = $data->aboutme; + $this->user->profile->gender = $data->basicprofile->gender; + $this->user->profile->photoURL = $data->basicprofile->image; + $this->user->profile->profileURL = $data->basicprofile->webUri; + $this->user->profile->age = $data->age; + $this->user->profile->country = $data->country; + $this->user->profile->region = $data->region; + $this->user->profile->city = $data->city; + $this->user->profile->zip = $data->postalcode; + + return $this->user->profile; + } + + /** + * load the user contacts + */ + function getUserContacts() + { + $userId = $this->getCurrentUserId(); + + $response = $this->api->get( "http://api.myspace.com/v1/users/" . $userId . "/friends.json" ); + + if ( ! is_object( $response ) ){ + throw new Exception( "User profile request failed! {$this->providerId} returned an invalide response.", 6 ); + } + + $contacts = ARRAY(); + + foreach( $response->Friends as $item ){ + $uc = new Hybrid_User_Contact(); + + $uc->identifier = $item->userId; + $uc->displayName = $item->name; + $uc->profileURL = $item->webUri; + $uc->photoURL = $item->image; + $uc->description = $item->status; + + $contacts[] = $uc; + } + + return $contacts; + } + + /** + * update user status + */ + function setUserStatus( $status ) + { + // crappy myspace... gonna see this asaic + $userId = $this->getCurrentUserId(); + + $parameters = array( 'status' => $status ); + + $response = $this->api->api( "http://api.myspace.com/v1/users/" . $userId . "/status", 'PUT', $parameters ); + + // check the last HTTP status code returned + if ( $this->api->http_code != 200 ) + { + throw new Exception( "Update user status failed! {$this->providerId} returned an error. " . $this->errorMessageByStatus( $this->api->http_code ) ); + } + } + + /** + * load the user latest activity + * - timeline : all the stream + * - me : the user activity only + */ + function getUserActivity( $stream ) + { + $userId = $this->getCurrentUserId(); + + if( $stream == "me" ){ + $response = $this->api->get( "http://api.myspace.com/v1/users/" . $userId . "/status.json" ); + } + else{ + $response = $this->api->get( "http://api.myspace.com/v1/users/" . $userId . "/friends/status.json" ); + } + + if ( ! is_object( $response ) ){ + throw new Exception( "User profile request failed! {$this->providerId} returned an invalide response.", 6 ); + } + + $activities = ARRAY(); + + if( $stream == "me" ){ + // todo + } + else{ + foreach( $response->FriendsStatus as $item ){ + $ua = new Hybrid_User_Activity(); + + $ua->id = $item->statusId; + $ua->date = NULL; // to find out!! + $ua->text = $item->status; + + $ua->user->identifier = $item->user->userId; + $ua->user->displayName = $item->user->name; + $ua->user->profileURL = $item->user->uri; + $ua->user->photoURL = $item->user->image; + + $activities[] = $ua; + } + } + + return $activities; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/OpenID.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/OpenID.php new file mode 100644 index 0000000..af5ec1e --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/OpenID.php @@ -0,0 +1,15 @@ +api->api_base_url = "https://api.twitter.com/1/"; + $this->api->authorize_url = "https://api.twitter.com/oauth/authenticate"; + $this->api->request_token_url = "https://api.twitter.com/oauth/request_token"; + $this->api->access_token_url = "https://api.twitter.com/oauth/access_token"; + + $this->api->curl_auth_header = false; + } + + /** + * load the user profile from the IDp api client + */ + function getUserProfile() + { + $response = $this->api->get( 'account/verify_credentials.json' ); + + // check the last HTTP status code returned + if ( $this->api->http_code != 200 ){ + throw new Exception( "User profile request failed! {$this->providerId} returned an error. " . $this->errorMessageByStatus( $this->api->http_code ), 6 ); + } + + if ( ! is_object( $response ) || ! isset( $response->id ) ){ + throw new Exception( "User profile request failed! {$this->providerId} api returned an invalid response.", 6 ); + } + + # store the user profile. + $this->user->profile->identifier = (property_exists($response,'id'))?$response->id:""; + $this->user->profile->displayName = (property_exists($response,'screen_name'))?$response->screen_name:""; + $this->user->profile->description = (property_exists($response,'description'))?$response->description:""; + $this->user->profile->firstName = (property_exists($response,'name'))?$response->name:""; + $this->user->profile->photoURL = (property_exists($response,'profile_image_url'))?$response->profile_image_url:""; + $this->user->profile->profileURL = (property_exists($response,'screen_name'))?("http://twitter.com/".$response->screen_name):""; + $this->user->profile->webSiteURL = (property_exists($response,'url'))?$response->url:""; + $this->user->profile->region = (property_exists($response,'location'))?$response->location:""; + + return $this->user->profile; + } + + /** + * load the user contacts + */ + function getUserContacts() + { + $parameters = array( 'cursor' => '-1' ); + $response = $this->api->get( 'friends/ids.json', $parameters ); + + // check the last HTTP status code returned + if ( $this->api->http_code != 200 ){ + throw new Exception( "User contacts request failed! {$this->providerId} returned an error. " . $this->errorMessageByStatus( $this->api->http_code ) ); + } + + if( ! $response || ! count( $response->ids ) ){ + return ARRAY(); + } + + // 75 id per time should be okey + $contactsids = array_chunk ( $response->ids, 75 ); + + $contacts = ARRAY(); + + foreach( $contactsids as $chunk ){ + $parameters = array( 'user_id' => implode( ",", $chunk ) ); + $response = $this->api->get( 'users/lookup.json', $parameters ); + + // check the last HTTP status code returned + if ( $this->api->http_code != 200 ){ + throw new Exception( "User contacts request failed! {$this->providerId} returned an error. " . $this->errorMessageByStatus( $this->api->http_code ) ); + } + + if( $response && count( $response ) ){ + foreach( $response as $item ){ + $uc = new Hybrid_User_Contact(); + + $uc->identifier = (property_exists($item,'id'))?$item->id:""; + $uc->displayName = (property_exists($item,'name'))?$item->name:""; + $uc->profileURL = (property_exists($item,'screen_name'))?("http://twitter.com/".$item->screen_name):""; + $uc->photoURL = (property_exists($item,'profile_image_url'))?$item->profile_image_url:""; + $uc->description = (property_exists($item,'description'))?$item->description:""; + + $contacts[] = $uc; + } + } + } + + return $contacts; + } + + /** + * update user status + */ + function setUserStatus( $status ) + { + $parameters = array( 'status' => $status ); + $response = $this->api->post( 'statuses/update.json', $parameters ); + + // check the last HTTP status code returned + if ( $this->api->http_code != 200 ){ + throw new Exception( "Update user status failed! {$this->providerId} returned an error. " . $this->errorMessageByStatus( $this->api->http_code ) ); + } + } + + /** + * load the user latest activity + * - timeline : all the stream + * - me : the user activity only + * + * by default return the timeline + */ + function getUserActivity( $stream ) + { + if( $stream == "me" ){ + $response = $this->api->get( 'statuses/user_timeline.json' ); + } + else{ + $response = $this->api->get( 'statuses/home_timeline.json' ); + } + + // check the last HTTP status code returned + if ( $this->api->http_code != 200 ){ + throw new Exception( "User activity stream request failed! {$this->providerId} returned an error. " . $this->errorMessageByStatus( $this->api->http_code ) ); + } + + if( ! $response ){ + return ARRAY(); + } + + $activities = ARRAY(); + + foreach( $response as $item ){ + $ua = new Hybrid_User_Activity(); + + $ua->id = (property_exists($item,'id'))?$item->id:""; + $ua->date = (property_exists($item,'created_at'))?strtotime($item->created_at):""; + $ua->text = (property_exists($item,'text'))?$item->text:""; + + $ua->user->identifier = (property_exists($item->user,'id'))?$item->user->id:""; + $ua->user->displayName = (property_exists($item->user,'name'))?$item->user->name:""; + $ua->user->profileURL = (property_exists($item->user,'screen_name'))?("http://twitter.com/".$item->user->screen_name):""; + $ua->user->photoURL = (property_exists($item->user,'profile_image_url'))?$item->user->profile_image_url:""; + + $activities[] = $ua; + } + + return $activities; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/Yahoo.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/Yahoo.php new file mode 100644 index 0000000..b645214 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Providers/Yahoo.php @@ -0,0 +1,237 @@ + + * @version 0.2 + * @license BSD License + */ + +/** + * Hybrid_Providers_Yahoo - Yahoo provider adapter based on OAuth1 protocol + */ +class Hybrid_Providers_Yahoo extends Hybrid_Provider_Model_OAuth1 +{ + function initialize() + { + parent::initialize(); + + // Provider api end-points + $this->api->api_base_url = 'http://social.yahooapis.com/v1/'; + $this->api->authorize_url = 'https://api.login.yahoo.com/oauth/v2/request_auth'; + $this->api->request_token_url = 'https://api.login.yahoo.com/oauth/v2/get_request_token'; + $this->api->access_token_url = 'https://api.login.yahoo.com/oauth/v2/get_token'; + } + + function getUserProfile() + { + $userId = $this->getCurrentUserId(); + + $parameters = array(); + $parameters['format'] = 'json'; + + $response = $this->api->get( 'user/' . $userId . '/profile', $parameters ); + + if ( ! isset( $response->profile ) ){ + throw new Exception( "User profile request failed! {$this->providerId} returned an invalide response.", 6 ); + } + + $data = $response->profile; + + $this->user->profile->identifier = (property_exists($data,'guid'))?$data->guid:""; + $this->user->profile->firstName = (property_exists($data,'givenName'))?$data->givenName:""; + $this->user->profile->lastName = (property_exists($data,'familyName'))?$data->familyName:""; + $this->user->profile->displayName = (property_exists($data,'nickname'))?trim( $data->nickname ):""; + $this->user->profile->profileURL = (property_exists($data,'profileUrl'))?$data->profileUrl:""; + $this->user->profile->gender = (property_exists($data,'gender'))?$data->gender:""; + + if( $this->user->profile->gender == "F" ){ + $this->user->profile->gender = "female"; + } + + if( $this->user->profile->gender == "M" ){ + $this->user->profile->gender = "male"; + } + + if( isset($data->emails) ){ + $email = ""; + foreach( $data->emails as $v ){ + if( isset($v->primary) && $v->primary ) { + $email = (property_exists($v,'handle'))?$v->handle:""; + + break; + } + } + + $this->user->profile->email = $email; + $this->user->profile->emailVerified = $email; + } + + $this->user->profile->age = (property_exists($data,'displayAge'))?$data->displayAge:""; + $this->user->profile->photoURL = (property_exists($data,'image'))?$data->image->imageUrl:""; + + $this->user->profile->address = (property_exists($data,'location'))?$data->location:""; + $this->user->profile->language = (property_exists($data,'lang'))?$data->lang:""; + + return $this->user->profile; + } + + /** + * load the user contacts + */ + function getUserContacts() + { + $userId = $this->getCurrentUserId(); + + $parameters = array(); + $parameters['format'] = 'json'; + $parameters['count'] = 'max'; + + $response = $this->api->get('user/' . $userId . '/contacts', $parameters); + + if ( $this->api->http_code != 200 ) + { + throw new Exception( 'User contacts request failed! ' . $this->providerId . ' returned an error: ' . $this->errorMessageByStatus( $this->api->http_code ) ); + } + + if ( !$response->contacts->contact && ( $response->errcode != 0 ) ) + { + return array(); + } + + $contacts = array(); + + foreach( $response->contacts->contact as $item ) { + $uc = new Hybrid_User_Contact(); + + $uc->identifier = $this->selectGUID( $item ); + $uc->email = $this->selectEmail( $item->fields ); + $uc->displayName = $this->selectName( $item->fields ); + $uc->photoURL = $this->selectPhoto( $item->fields ); + + $contacts[] = $uc; + } + + return $contacts; + } + + /** + * return the user activity stream + */ + function getUserActivity( $stream ) + { + $userId = $this->getCurrentUserId(); + + $parameters = array(); + $parameters['format'] = 'json'; + $parameters['count'] = 'max'; + + $response = $this->api->get('user/' . $userId . '/updates', $parameters); + + if( ! $response->updates || $this->api->http_code != 200 ) + { + throw new Exception( 'User activity request failed! ' . $this->providerId . ' returned an error: ' . $this->errorMessageByStatus( $this->api->http_code ) ); + } + + $activities = array(); + + foreach( $response->updates as $item ){ + $ua = new Hybrid_User_Activity(); + + $ua->id = (property_exists($item,'collectionID'))?$item->collectionID:""; + $ua->date = (property_exists($item,'lastUpdated'))?$item->lastUpdated:""; + $ua->text = (property_exists($item,'loc_longForm'))?$item->loc_longForm:""; + + $ua->user->identifier = (property_exists($item,'profile_guid'))?$item->profile_guid:""; + $ua->user->displayName = (property_exists($item,'profile_nickname'))?$item->profile_nickname:""; + $ua->user->profileURL = (property_exists($item,'profile_profileUrl'))?$item->profile_profileUrl:""; + $ua->user->photoURL = (property_exists($item,'profile_displayImage'))?$item->profile_displayImage:""; + + $activities[] = $ua; + } + + if( $stream == "me" ){ + $userId = $this->getCurrentUserId(); + $my_activities = array(); + + foreach( $activities as $a ){ + if( $a->user->identifier == $userId ){ + $my_activities[] = $a; + } + } + + return $my_activities; + } + + return $activities; + } + + //-- + + function select($vs, $t) + { + foreach( $vs as $v ){ + if( $v->type == $t ) { + return $v; + } + } + + return NULL; + } + + function selectGUID( $v ) + { + return (property_exists($v,'id'))?$v->id:""; + } + + function selectName( $v ) + { + $s = $this->select($v, 'name'); + + if( ! $s ){ + $s = $this->select($v, 'nickname'); + return ($s)?$s->value:""; + } else { + return ($s)?$s->value->givenName . " " . $s->value->familyName:""; + } + } + + function selectNickame( $v ) + { + $s = $this->select($v, 'nickname'); + return ($s)?$s:""; + } + + function selectPhoto( $v ) + { + $s = $this->select($v, 'guid'); + return ($s)?(property_exists($s,'image')):""; + } + + function selectEmail( $v ) + { + $s = $this->select($v, 'email'); + return ($s)?$s->value:""; + } + + public function getCurrentUserId() + { + $parameters = array(); + $parameters['format'] = 'json'; + + $response = $this->api->get( 'me/guid', $parameters ); + + if ( ! isset( $response->guid->value ) ){ + throw new Exception( "User id request failed! {$this->providerId} returned an invalide response." ); + } + + return $response->guid->value; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Storage.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Storage.php new file mode 100644 index 0000000..1458633 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/Storage.php @@ -0,0 +1,97 @@ +config( "php_session_id", session_id() ); + $this->config( "version", Hybrid_Auth::$version ); + } + + public function config($key, $value=null) + { + $key = strtolower( $key ); + + if( $value ){ + $_SESSION["HA::CONFIG"][$key] = serialize( $value ); + } + elseif( isset( $_SESSION["HA::CONFIG"][$key] ) ){ + return unserialize( $_SESSION["HA::CONFIG"][$key] ); + } + + return NULL; + } + + public function get($key) + { + $key = strtolower( $key ); + + if( isset( $_SESSION["HA::STORE"][$key] ) ){ + return unserialize( $_SESSION["HA::STORE"][$key] ); + } + + return NULL; + } + + public function set( $key, $value ) + { + $key = strtolower( $key ); + + $_SESSION["HA::STORE"][$key] = serialize( $value ); + } + + function clear() + { + $_SESSION["HA::STORE"] = ARRAY(); + } + + function delete($key) + { + $key = strtolower( $key ); + + if( isset( $_SESSION["HA::STORE"][$key] ) ){ + unset( $_SESSION["HA::STORE"][$key] ); + } + } + + function deleteMatch($key) + { + $key = strtolower( $key ); + + if( isset( $_SESSION["HA::STORE"] ) && count( $_SESSION["HA::STORE"] ) ) { + foreach( $_SESSION["HA::STORE"] as $k => $v ){ + if( strstr( $k, $key ) ){ + unset( $_SESSION["HA::STORE"][ $k ] ); + } + } + } + } + + function getSessionData() + { + if( isset( $_SESSION["HA::STORE"] ) ){ + return serialize( $_SESSION["HA::STORE"] ); + } + + return NULL; + } + + function restoreSessionData( $sessiondata = NULL ) + { + $_SESSION["HA::STORE"] = unserialize( $sessiondata ); + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/User.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/User.php new file mode 100644 index 0000000..707999a --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/User.php @@ -0,0 +1,31 @@ +timestamp = time(); + + $this->profile = new Hybrid_User_Profile(); + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/User_Activity.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/User_Activity.php new file mode 100644 index 0000000..3ee08aa --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/User_Activity.php @@ -0,0 +1,39 @@ +user = new stdClass(); + + // typically, we should have a few information about the user who created the event from social apis + $this->user->identifier = NULL; + $this->user->displayName = NULL; + $this->user->profileURL = NULL; + $this->user->photoURL = NULL; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/User_Contact.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/User_Contact.php new file mode 100644 index 0000000..7c6ad8d --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/User_Contact.php @@ -0,0 +1,37 @@ + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + \ No newline at end of file diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/config.php.tpl b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/config.php.tpl new file mode 100644 index 0000000..3d82b68 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/config.php.tpl @@ -0,0 +1,72 @@ + "#GLOBAL_HYBRID_AUTH_URL_BASE#", + + "providers" => array ( + // openid providers + "OpenID" => array ( + "enabled" => #OPENID_ADAPTER_STATUS# + ), + + "AOL" => array ( + "enabled" => #AOL_ADAPTER_STATUS# + ), + + "Yahoo" => array ( + "enabled" => #YAHOO_ADAPTER_STATUS#, + "keys" => array ( "id" => "#YAHOO_APPLICATION_KEY#", "secret" => "#YAHOO_APPLICATION_SECRET#" ) + ), + + "Google" => array ( + "enabled" => #GOOGLE_ADAPTER_STATUS#, + "keys" => array ( "id" => "#GOOGLE_APPLICATION_APP_ID#", "secret" => "#GOOGLE_APPLICATION_SECRET#" ) + ), + + "Facebook" => array ( + "enabled" => #FACEBOOK_ADAPTER_STATUS#, + "keys" => array ( "id" => "#FACEBOOK_APPLICATION_APP_ID#", "secret" => "#FACEBOOK_APPLICATION_SECRET#" ) + ), + + "Twitter" => array ( + "enabled" => #TWITTER_ADAPTER_STATUS#, + "keys" => array ( "key" => "#TWITTER_APPLICATION_KEY#", "secret" => "#TWITTER_APPLICATION_SECRET#" ) + ), + + // windows live + "Live" => array ( + "enabled" => #LIVE_ADAPTER_STATUS#, + "keys" => array ( "id" => "#LIVE_APPLICATION_APP_ID#", "secret" => "#LIVE_APPLICATION_SECRET#" ) + ), + + "MySpace" => array ( + "enabled" => #MYSPACE_ADAPTER_STATUS#, + "keys" => array ( "key" => "#MYSPACE_APPLICATION_KEY#", "secret" => "#MYSPACE_APPLICATION_SECRET#" ) + ), + + "LinkedIn" => array ( + "enabled" => #LINKEDIN_ADAPTER_STATUS#, + "keys" => array ( "key" => "#LINKEDIN_APPLICATION_KEY#", "secret" => "#LINKEDIN_APPLICATION_SECRET#" ) + ), + + "Foursquare" => array ( + "enabled" => #FOURSQUARE_ADAPTER_STATUS#, + "keys" => array ( "id" => "#FOURSQUARE_APPLICATION_APP_ID#", "secret" => "#FOURSQUARE_APPLICATION_SECRET#" ) + ), + ), + + // if you want to enable logging, set 'debug_mode' to true then provide a writable file by the web server on "debug_file" + "debug_mode" => false, + + "debug_file" => "" + ); diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/index.html b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/index.html new file mode 100644 index 0000000..065d2da --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/index.html @@ -0,0 +1,10 @@ + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + \ No newline at end of file diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/openid_policy.html b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/openid_policy.html new file mode 100644 index 0000000..bf5c52c --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/openid_policy.html @@ -0,0 +1,10 @@ + + + OpenID Policy + + + + + \ No newline at end of file diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/openid_realm.html b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/openid_realm.html new file mode 100644 index 0000000..e26a5a1 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/openid_realm.html @@ -0,0 +1,13 @@ + + + HybridAuth Endpoint + + + + +

HybridAuth

+ Open Source Social Sign On PHP Library. +
+ hybridauth.sourceforge.net/ + + diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/openid_xrds.xml b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/openid_xrds.xml new file mode 100644 index 0000000..9d50170 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/resources/openid_xrds.xml @@ -0,0 +1,12 @@ + + + + + http://specs.openid.net/auth/2.0/return_to + {RETURN_TO_URL} + + + \ No newline at end of file diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/Facebook/base_facebook.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/Facebook/base_facebook.php new file mode 100644 index 0000000..5587bee --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/Facebook/base_facebook.php @@ -0,0 +1,1316 @@ + + */ +class FacebookApiException extends Exception +{ + /** + * The result from the API server that represents the exception information. + */ + protected $result; + + /** + * Make a new API Exception with the given result. + * + * @param array $result The result from the API server + */ + public function __construct($result) { + $this->result = $result; + + $code = isset($result['error_code']) ? $result['error_code'] : 0; + + if (isset($result['error_description'])) { + // OAuth 2.0 Draft 10 style + $msg = $result['error_description']; + } else if (isset($result['error']) && is_array($result['error'])) { + // OAuth 2.0 Draft 00 style + $msg = $result['error']['message']; + } else if (isset($result['error_msg'])) { + // Rest server style + $msg = $result['error_msg']; + } else { + $msg = 'Unknown Error. Check getResult()'; + } + + parent::__construct($msg, $code); + } + + /** + * Return the associated result object returned by the API server. + * + * @return array The result from the API server + */ + public function getResult() { + return $this->result; + } + + /** + * Returns the associated type for the error. This will default to + * 'Exception' when a type is not available. + * + * @return string + */ + public function getType() { + if (isset($this->result['error'])) { + $error = $this->result['error']; + if (is_string($error)) { + // OAuth 2.0 Draft 10 style + return $error; + } else if (is_array($error)) { + // OAuth 2.0 Draft 00 style + if (isset($error['type'])) { + return $error['type']; + } + } + } + + return 'Exception'; + } + + /** + * To make debugging easier. + * + * @return string The string representation of the error + */ + public function __toString() { + $str = $this->getType() . ': '; + if ($this->code != 0) { + $str .= $this->code . ': '; + } + return $str . $this->message; + } +} + +/** + * Provides access to the Facebook Platform. This class provides + * a majority of the functionality needed, but the class is abstract + * because it is designed to be sub-classed. The subclass must + * implement the four abstract methods listed at the bottom of + * the file. + * + * @author Naitik Shah + */ +abstract class BaseFacebook +{ + /** + * Version. + */ + const VERSION = '3.1.1'; + + /** + * Default options for curl. + */ + public static $CURL_OPTS = array( + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 60, + CURLOPT_USERAGENT => 'facebook-php-3.1', + ); + + /** + * List of query parameters that get automatically dropped when rebuilding + * the current URL. + */ + protected static $DROP_QUERY_PARAMS = array( + 'code', + 'state', + 'signed_request', + ); + + /** + * Maps aliases to Facebook domains. + */ + public static $DOMAIN_MAP = array( + 'api' => 'https://api.facebook.com/', + 'api_video' => 'https://api-video.facebook.com/', + 'api_read' => 'https://api-read.facebook.com/', + 'graph' => 'https://graph.facebook.com/', + 'graph_video' => 'https://graph-video.facebook.com/', + 'www' => 'https://www.facebook.com/', + ); + + /** + * The Application ID. + * + * @var string + */ + protected $appId; + + /** + * The Application App Secret. + * + * @var string + */ + protected $appSecret; + + /** + * The ID of the Facebook user, or 0 if the user is logged out. + * + * @var integer + */ + protected $user; + + /** + * The data from the signed_request token. + */ + protected $signedRequest; + + /** + * A CSRF state variable to assist in the defense against CSRF attacks. + */ + protected $state; + + /** + * The OAuth access token received in exchange for a valid authorization + * code. null means the access token has yet to be determined. + * + * @var string + */ + protected $accessToken = null; + + /** + * Indicates if the CURL based @ syntax for file uploads is enabled. + * + * @var boolean + */ + protected $fileUploadSupport = false; + + /** + * Initialize a Facebook Application. + * + * The configuration: + * - appId: the application ID + * - secret: the application secret + * - fileUpload: (optional) boolean indicating if file uploads are enabled + * + * @param array $config The application configuration + */ + public function __construct($config) { + $this->setAppId($config['appId']); + $this->setAppSecret($config['secret']); + if (isset($config['fileUpload'])) { + $this->setFileUploadSupport($config['fileUpload']); + } + + $state = $this->getPersistentData('state'); + if (!empty($state)) { + $this->state = $this->getPersistentData('state'); + } + } + + /** + * Set the Application ID. + * + * @param string $appId The Application ID + * @return BaseFacebook + */ + public function setAppId($appId) { + $this->appId = $appId; + return $this; + } + + /** + * Get the Application ID. + * + * @return string the Application ID + */ + public function getAppId() { + return $this->appId; + } + + /** + * Set the App Secret. + * + * @param string $apiSecret The App Secret + * @return BaseFacebook + * @deprecated + */ + public function setApiSecret($apiSecret) { + $this->setAppSecret($apiSecret); + return $this; + } + + /** + * Set the App Secret. + * + * @param string $appSecret The App Secret + * @return BaseFacebook + */ + public function setAppSecret($appSecret) { + $this->appSecret = $appSecret; + return $this; + } + + /** + * Get the App Secret. + * + * @return string the App Secret + * @deprecated + */ + public function getApiSecret() { + return $this->getAppSecret(); + } + + /** + * Get the App Secret. + * + * @return string the App Secret + */ + public function getAppSecret() { + return $this->appSecret; + } + + /** + * Set the file upload support status. + * + * @param boolean $fileUploadSupport The file upload support status. + * @return BaseFacebook + */ + public function setFileUploadSupport($fileUploadSupport) { + $this->fileUploadSupport = $fileUploadSupport; + return $this; + } + + /** + * Get the file upload support status. + * + * @return boolean true if and only if the server supports file upload. + */ + public function getFileUploadSupport() { + return $this->fileUploadSupport; + } + + /** + * DEPRECATED! Please use getFileUploadSupport instead. + * + * Get the file upload support status. + * + * @return boolean true if and only if the server supports file upload. + */ + public function useFileUploadSupport() { + return $this->getFileUploadSupport(); + } + + /** + * Sets the access token for api calls. Use this if you get + * your access token by other means and just want the SDK + * to use it. + * + * @param string $access_token an access token. + * @return BaseFacebook + */ + public function setAccessToken($access_token) { + $this->accessToken = $access_token; + return $this; + } + + /** + * Determines the access token that should be used for API calls. + * The first time this is called, $this->accessToken is set equal + * to either a valid user access token, or it's set to the application + * access token if a valid user access token wasn't available. Subsequent + * calls return whatever the first call returned. + * + * @return string The access token + */ + public function getAccessToken() { + if ($this->accessToken !== null) { + // we've done this already and cached it. Just return. + return $this->accessToken; + } + + // first establish access token to be the application + // access token, in case we navigate to the /oauth/access_token + // endpoint, where SOME access token is required. + $this->setAccessToken($this->getApplicationAccessToken()); + $user_access_token = $this->getUserAccessToken(); + if ($user_access_token) { + $this->setAccessToken($user_access_token); + } + + return $this->accessToken; + } + + /** + * Determines and returns the user access token, first using + * the signed request if present, and then falling back on + * the authorization code if present. The intent is to + * return a valid user access token, or false if one is determined + * to not be available. + * + * @return string A valid user access token, or false if one + * could not be determined. + */ + protected function getUserAccessToken() { + // first, consider a signed request if it's supplied. + // if there is a signed request, then it alone determines + // the access token. + $signed_request = $this->getSignedRequest(); + if ($signed_request) { + // apps.facebook.com hands the access_token in the signed_request + if (array_key_exists('oauth_token', $signed_request)) { + $access_token = $signed_request['oauth_token']; + $this->setPersistentData('access_token', $access_token); + return $access_token; + } + + // the JS SDK puts a code in with the redirect_uri of '' + if (array_key_exists('code', $signed_request)) { + $code = $signed_request['code']; + $access_token = $this->getAccessTokenFromCode($code, ''); + if ($access_token) { + $this->setPersistentData('code', $code); + $this->setPersistentData('access_token', $access_token); + return $access_token; + } + } + + // signed request states there's no access token, so anything + // stored should be cleared. + $this->clearAllPersistentData(); + return false; // respect the signed request's data, even + // if there's an authorization code or something else + } + + $code = $this->getCode(); + if ($code && $code != $this->getPersistentData('code')) { + $access_token = $this->getAccessTokenFromCode($code); + if ($access_token) { + $this->setPersistentData('code', $code); + $this->setPersistentData('access_token', $access_token); + return $access_token; + } + + // code was bogus, so everything based on it should be invalidated. + $this->clearAllPersistentData(); + return false; + } + + // as a fallback, just return whatever is in the persistent + // store, knowing nothing explicit (signed request, authorization + // code, etc.) was present to shadow it (or we saw a code in $_REQUEST, + // but it's the same as what's in the persistent store) + return $this->getPersistentData('access_token'); + } + + /** + * Retrieve the signed request, either from a request parameter or, + * if not present, from a cookie. + * + * @return string the signed request, if available, or null otherwise. + */ + public function getSignedRequest() { + if (!$this->signedRequest) { + if (isset($_REQUEST['signed_request'])) { + $this->signedRequest = $this->parseSignedRequest( + $_REQUEST['signed_request']); + } else if (isset($_COOKIE[$this->getSignedRequestCookieName()])) { + $this->signedRequest = $this->parseSignedRequest( + $_COOKIE[$this->getSignedRequestCookieName()]); + } + } + return $this->signedRequest; + } + + /** + * Get the UID of the connected user, or 0 + * if the Facebook user is not connected. + * + * @return string the UID if available. + */ + public function getUser() { + if ($this->user !== null) { + // we've already determined this and cached the value. + return $this->user; + } + + return $this->user = $this->getUserFromAvailableData(); + } + + /** + * Determines the connected user by first examining any signed + * requests, then considering an authorization code, and then + * falling back to any persistent store storing the user. + * + * @return integer The id of the connected Facebook user, + * or 0 if no such user exists. + */ + protected function getUserFromAvailableData() { + // if a signed request is supplied, then it solely determines + // who the user is. + $signed_request = $this->getSignedRequest(); + if ($signed_request) { + if (array_key_exists('user_id', $signed_request)) { + $user = $signed_request['user_id']; + $this->setPersistentData('user_id', $signed_request['user_id']); + return $user; + } + + // if the signed request didn't present a user id, then invalidate + // all entries in any persistent store. + $this->clearAllPersistentData(); + return 0; + } + + $user = $this->getPersistentData('user_id', $default = 0); + $persisted_access_token = $this->getPersistentData('access_token'); + + // use access_token to fetch user id if we have a user access_token, or if + // the cached access token has changed. + $access_token = $this->getAccessToken(); + if ($access_token && + $access_token != $this->getApplicationAccessToken() && + !($user && $persisted_access_token == $access_token)) { + $user = $this->getUserFromAccessToken(); + if ($user) { + $this->setPersistentData('user_id', $user); + } else { + $this->clearAllPersistentData(); + } + } + + return $user; + } + + /** + * Get a Login URL for use with redirects. By default, full page redirect is + * assumed. If you are using the generated URL with a window.open() call in + * JavaScript, you can pass in display=popup as part of the $params. + * + * The parameters: + * - redirect_uri: the url to go to after a successful login + * - scope: comma separated list of requested extended perms + * + * @param array $params Provide custom parameters + * @return string The URL for the login flow + */ + public function getLoginUrl($params=array()) { + $this->establishCSRFTokenState(); + $currentUrl = $this->getCurrentUrl(); + + // if 'scope' is passed as an array, convert to comma separated list + $scopeParams = isset($params['scope']) ? $params['scope'] : null; + if ($scopeParams && is_array($scopeParams)) { + $params['scope'] = implode(',', $scopeParams); + } + + return $this->getUrl( + 'www', + 'dialog/oauth', + array_merge(array( + 'client_id' => $this->getAppId(), + 'redirect_uri' => $currentUrl, // possibly overwritten + 'state' => $this->state), + $params)); + } + + /** + * Get a Logout URL suitable for use with redirects. + * + * The parameters: + * - next: the url to go to after a successful logout + * + * @param array $params Provide custom parameters + * @return string The URL for the logout flow + */ + public function getLogoutUrl($params=array()) { + return $this->getUrl( + 'www', + 'logout.php', + array_merge(array( + 'next' => $this->getCurrentUrl(), + 'access_token' => $this->getAccessToken(), + ), $params) + ); + } + + /** + * Get a login status URL to fetch the status from Facebook. + * + * The parameters: + * - ok_session: the URL to go to if a session is found + * - no_session: the URL to go to if the user is not connected + * - no_user: the URL to go to if the user is not signed into facebook + * + * @param array $params Provide custom parameters + * @return string The URL for the logout flow + */ + public function getLoginStatusUrl($params=array()) { + return $this->getUrl( + 'www', + 'extern/login_status.php', + array_merge(array( + 'api_key' => $this->getAppId(), + 'no_session' => $this->getCurrentUrl(), + 'no_user' => $this->getCurrentUrl(), + 'ok_session' => $this->getCurrentUrl(), + 'session_version' => 3, + ), $params) + ); + } + + /** + * Make an API call. + * + * @return mixed The decoded response + */ + public function api(/* polymorphic */) { + $args = func_get_args(); + if (is_array($args[0])) { + return $this->_restserver($args[0]); + } else { + return call_user_func_array(array($this, '_graph'), $args); + } + } + + /** + * Constructs and returns the name of the cookie that + * potentially houses the signed request for the app user. + * The cookie is not set by the BaseFacebook class, but + * it may be set by the JavaScript SDK. + * + * @return string the name of the cookie that would house + * the signed request value. + */ + protected function getSignedRequestCookieName() { + return 'fbsr_'.$this->getAppId(); + } + + /** + * Constructs and returns the name of the coookie that potentially contain + * metadata. The cookie is not set by the BaseFacebook class, but it may be + * set by the JavaScript SDK. + * + * @return string the name of the cookie that would house metadata. + */ + protected function getMetadataCookieName() { + return 'fbm_'.$this->getAppId(); + } + + /** + * Get the authorization code from the query parameters, if it exists, + * and otherwise return false to signal no authorization code was + * discoverable. + * + * @return mixed The authorization code, or false if the authorization + * code could not be determined. + */ + protected function getCode() { + if (isset($_REQUEST['code'])) { + if ($this->state !== null && + isset($_REQUEST['state']) && + $this->state === $_REQUEST['state']) { + + // CSRF state has done its job, so clear it + $this->state = null; + $this->clearPersistentData('state'); + return $_REQUEST['code']; + } else { + self::errorLog('CSRF state token does not match one provided.'); + return false; + } + } + + return false; + } + + /** + * Retrieves the UID with the understanding that + * $this->accessToken has already been set and is + * seemingly legitimate. It relies on Facebook's Graph API + * to retrieve user information and then extract + * the user ID. + * + * @return integer Returns the UID of the Facebook user, or 0 + * if the Facebook user could not be determined. + */ + protected function getUserFromAccessToken() { + try { + $user_info = $this->api('/me'); + return $user_info['id']; + } catch (FacebookApiException $e) { + return 0; + } + } + + /** + * Returns the access token that should be used for logged out + * users when no authorization code is available. + * + * @return string The application access token, useful for gathering + * public information about users and applications. + */ + protected function getApplicationAccessToken() { + return $this->appId.'|'.$this->appSecret; + } + + /** + * Lays down a CSRF state token for this process. + * + * @return void + */ + protected function establishCSRFTokenState() { + if ($this->state === null) { + $this->state = md5(uniqid(mt_rand(), true)); + $this->setPersistentData('state', $this->state); + } + } + + /** + * Retrieves an access token for the given authorization code + * (previously generated from www.facebook.com on behalf of + * a specific user). The authorization code is sent to graph.facebook.com + * and a legitimate access token is generated provided the access token + * and the user for which it was generated all match, and the user is + * either logged in to Facebook or has granted an offline access permission. + * + * @param string $code An authorization code. + * @return mixed An access token exchanged for the authorization code, or + * false if an access token could not be generated. + */ + protected function getAccessTokenFromCode($code, $redirect_uri = null) { + if (empty($code)) { + return false; + } + + if ($redirect_uri === null) { + $redirect_uri = $this->getCurrentUrl(); + } + + try { + // need to circumvent json_decode by calling _oauthRequest + // directly, since response isn't JSON format. + $access_token_response = + $this->_oauthRequest( + $this->getUrl('graph', '/oauth/access_token'), + $params = array('client_id' => $this->getAppId(), + 'client_secret' => $this->getAppSecret(), + 'redirect_uri' => $redirect_uri, + 'code' => $code)); + } catch (FacebookApiException $e) { + // most likely that user very recently revoked authorization. + // In any event, we don't have an access token, so say so. + return false; + } + + if (empty($access_token_response)) { + return false; + } + + $response_params = array(); + parse_str($access_token_response, $response_params); + if (!isset($response_params['access_token'])) { + return false; + } + + return $response_params['access_token']; + } + + /** + * Invoke the old restserver.php endpoint. + * + * @param array $params Method call object + * + * @return mixed The decoded response object + * @throws FacebookApiException + */ + protected function _restserver($params) { + // generic application level parameters + $params['api_key'] = $this->getAppId(); + $params['format'] = 'json-strings'; + + $result = json_decode($this->_oauthRequest( + $this->getApiUrl($params['method']), + $params + ), true); + + // results are returned, errors are thrown + if (is_array($result) && isset($result['error_code'])) { + $this->throwAPIException($result); + } + + if ($params['method'] === 'auth.expireSession' || + $params['method'] === 'auth.revokeAuthorization') { + $this->destroySession(); + } + + return $result; + } + + /** + * Return true if this is video post. + * + * @param string $path The path + * @param string $method The http method (default 'GET') + * + * @return boolean true if this is video post + */ + protected function isVideoPost($path, $method = 'GET') { + if ($method == 'POST' && preg_match("/^(\/)(.+)(\/)(videos)$/", $path)) { + return true; + } + return false; + } + + /** + * Invoke the Graph API. + * + * @param string $path The path (required) + * @param string $method The http method (default 'GET') + * @param array $params The query/post data + * + * @return mixed The decoded response object + * @throws FacebookApiException + */ + protected function _graph($path, $method = 'GET', $params = array()) { + if (is_array($method) && empty($params)) { + $params = $method; + $method = 'GET'; + } + $params['method'] = $method; // method override as we always do a POST + + if ($this->isVideoPost($path, $method)) { + $domainKey = 'graph_video'; + } else { + $domainKey = 'graph'; + } + + $result = json_decode($this->_oauthRequest( + $this->getUrl($domainKey, $path), + $params + ), true); + + // results are returned, errors are thrown + if (is_array($result) && isset($result['error'])) { + $this->throwAPIException($result); + } + + return $result; + } + + /** + * Make a OAuth Request. + * + * @param string $url The path (required) + * @param array $params The query/post data + * + * @return string The decoded response object + * @throws FacebookApiException + */ + protected function _oauthRequest($url, $params) { + if (!isset($params['access_token'])) { + $params['access_token'] = $this->getAccessToken(); + } + + // json_encode all params values that are not strings + foreach ($params as $key => $value) { + if (!is_string($value)) { + $params[$key] = json_encode($value); + } + } + + return $this->makeRequest($url, $params); + } + + /** + * Makes an HTTP request. This method can be overridden by subclasses if + * developers want to do fancier things or use something other than curl to + * make the request. + * + * @param string $url The URL to make the request to + * @param array $params The parameters to use for the POST body + * @param CurlHandler $ch Initialized curl handle + * + * @return string The response text + */ + protected function makeRequest($url, $params, $ch=null) { + if (!$ch) { + $ch = curl_init(); + } + + $opts = self::$CURL_OPTS; + if ($this->getFileUploadSupport()) { + $opts[CURLOPT_POSTFIELDS] = $params; + } else { + $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&'); + } + $opts[CURLOPT_URL] = $url; + + // disable the 'Expect: 100-continue' behaviour. This causes CURL to wait + // for 2 seconds if the server does not support this header. + if (isset($opts[CURLOPT_HTTPHEADER])) { + $existing_headers = $opts[CURLOPT_HTTPHEADER]; + $existing_headers[] = 'Expect:'; + $opts[CURLOPT_HTTPHEADER] = $existing_headers; + } else { + $opts[CURLOPT_HTTPHEADER] = array('Expect:'); + } + + curl_setopt_array($ch, $opts); + $result = curl_exec($ch); + + if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT + self::errorLog('Invalid or no certificate authority found, '. + 'using bundled information'); + curl_setopt($ch, CURLOPT_CAINFO, + dirname(__FILE__) . '/fb_ca_chain_bundle.crt'); + $result = curl_exec($ch); + } + + if ($result === false) { + $e = new FacebookApiException(array( + 'error_code' => curl_errno($ch), + 'error' => array( + 'message' => curl_error($ch), + 'type' => 'CurlException', + ), + )); + curl_close($ch); + throw $e; + } + curl_close($ch); + return $result; + } + + /** + * Parses a signed_request and validates the signature. + * + * @param string $signed_request A signed token + * @return array The payload inside it or null if the sig is wrong + */ + protected function parseSignedRequest($signed_request) { + list($encoded_sig, $payload) = explode('.', $signed_request, 2); + + // decode the data + $sig = self::base64UrlDecode($encoded_sig); + $data = json_decode(self::base64UrlDecode($payload), true); + + if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') { + self::errorLog('Unknown algorithm. Expected HMAC-SHA256'); + return null; + } + + // check sig + $expected_sig = hash_hmac('sha256', $payload, + $this->getAppSecret(), $raw = true); + if ($sig !== $expected_sig) { + self::errorLog('Bad Signed JSON signature!'); + return null; + } + + return $data; + } + + /** + * Build the URL for api given parameters. + * + * @param $method String the method name. + * @return string The URL for the given parameters + */ + protected function getApiUrl($method) { + static $READ_ONLY_CALLS = + array('admin.getallocation' => 1, + 'admin.getappproperties' => 1, + 'admin.getbannedusers' => 1, + 'admin.getlivestreamvialink' => 1, + 'admin.getmetrics' => 1, + 'admin.getrestrictioninfo' => 1, + 'application.getpublicinfo' => 1, + 'auth.getapppublickey' => 1, + 'auth.getsession' => 1, + 'auth.getsignedpublicsessiondata' => 1, + 'comments.get' => 1, + 'connect.getunconnectedfriendscount' => 1, + 'dashboard.getactivity' => 1, + 'dashboard.getcount' => 1, + 'dashboard.getglobalnews' => 1, + 'dashboard.getnews' => 1, + 'dashboard.multigetcount' => 1, + 'dashboard.multigetnews' => 1, + 'data.getcookies' => 1, + 'events.get' => 1, + 'events.getmembers' => 1, + 'fbml.getcustomtags' => 1, + 'feed.getappfriendstories' => 1, + 'feed.getregisteredtemplatebundlebyid' => 1, + 'feed.getregisteredtemplatebundles' => 1, + 'fql.multiquery' => 1, + 'fql.query' => 1, + 'friends.arefriends' => 1, + 'friends.get' => 1, + 'friends.getappusers' => 1, + 'friends.getlists' => 1, + 'friends.getmutualfriends' => 1, + 'gifts.get' => 1, + 'groups.get' => 1, + 'groups.getmembers' => 1, + 'intl.gettranslations' => 1, + 'links.get' => 1, + 'notes.get' => 1, + 'notifications.get' => 1, + 'pages.getinfo' => 1, + 'pages.isadmin' => 1, + 'pages.isappadded' => 1, + 'pages.isfan' => 1, + 'permissions.checkavailableapiaccess' => 1, + 'permissions.checkgrantedapiaccess' => 1, + 'photos.get' => 1, + 'photos.getalbums' => 1, + 'photos.gettags' => 1, + 'profile.getinfo' => 1, + 'profile.getinfooptions' => 1, + 'stream.get' => 1, + 'stream.getcomments' => 1, + 'stream.getfilters' => 1, + 'users.getinfo' => 1, + 'users.getloggedinuser' => 1, + 'users.getstandardinfo' => 1, + 'users.hasapppermission' => 1, + 'users.isappuser' => 1, + 'users.isverified' => 1, + 'video.getuploadlimits' => 1); + $name = 'api'; + if (isset($READ_ONLY_CALLS[strtolower($method)])) { + $name = 'api_read'; + } else if (strtolower($method) == 'video.upload') { + $name = 'api_video'; + } + return self::getUrl($name, 'restserver.php'); + } + + /** + * Build the URL for given domain alias, path and parameters. + * + * @param $name string The name of the domain + * @param $path string Optional path (without a leading slash) + * @param $params array Optional query parameters + * + * @return string The URL for the given parameters + */ + protected function getUrl($name, $path='', $params=array()) { + $url = self::$DOMAIN_MAP[$name]; + if ($path) { + if ($path[0] === '/') { + $path = substr($path, 1); + } + $url .= $path; + } + if ($params) { + $url .= '?' . http_build_query($params, null, '&'); + } + + return $url; + } + + /** + * Returns the Current URL, stripping it of known FB parameters that should + * not persist. + * + * @return string The current URL + */ + protected function getCurrentUrl() { + if (isset($_SERVER['HTTPS']) && + ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1) || + isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && + $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') { + $protocol = 'https://'; + } + else { + $protocol = 'http://'; + } + $currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + $parts = parse_url($currentUrl); + + $query = ''; + if (!empty($parts['query'])) { + // drop known fb params + $params = explode('&', $parts['query']); + $retained_params = array(); + foreach ($params as $param) { + if ($this->shouldRetainParam($param)) { + $retained_params[] = $param; + } + } + + if (!empty($retained_params)) { + $query = '?'.implode($retained_params, '&'); + } + } + + // use port if non default + $port = + isset($parts['port']) && + (($protocol === 'http://' && $parts['port'] !== 80) || + ($protocol === 'https://' && $parts['port'] !== 443)) + ? ':' . $parts['port'] : ''; + + // rebuild + return $protocol . $parts['host'] . $port . $parts['path'] . $query; + } + + /** + * Returns true if and only if the key or key/value pair should + * be retained as part of the query string. This amounts to + * a brute-force search of the very small list of Facebook-specific + * params that should be stripped out. + * + * @param string $param A key or key/value pair within a URL's query (e.g. + * 'foo=a', 'foo=', or 'foo'. + * + * @return boolean + */ + protected function shouldRetainParam($param) { + foreach (self::$DROP_QUERY_PARAMS as $drop_query_param) { + if (strpos($param, $drop_query_param.'=') === 0) { + return false; + } + } + + return true; + } + + /** + * Analyzes the supplied result to see if it was thrown + * because the access token is no longer valid. If that is + * the case, then we destroy the session. + * + * @param $result array A record storing the error message returned + * by a failed API call. + */ + protected function throwAPIException($result) { + $e = new FacebookApiException($result); + switch ($e->getType()) { + // OAuth 2.0 Draft 00 style + case 'OAuthException': + // OAuth 2.0 Draft 10 style + case 'invalid_token': + // REST server errors are just Exceptions + case 'Exception': + $message = $e->getMessage(); + if ((strpos($message, 'Error validating access token') !== false) || + (strpos($message, 'Invalid OAuth access token') !== false) || + (strpos($message, 'An active access token must be used') !== false) + ) { + $this->destroySession(); + } + break; + } + + throw $e; + } + + + /** + * Prints to the error log if you aren't in command line mode. + * + * @param string $msg Log message + */ + protected static function errorLog($msg) { + // disable error log if we are running in a CLI environment + // @codeCoverageIgnoreStart + if (php_sapi_name() != 'cli') { + error_log($msg); + } + // uncomment this if you want to see the errors on the page + // print 'error_log: '.$msg."\n"; + // @codeCoverageIgnoreEnd + } + + /** + * Base64 encoding that doesn't need to be urlencode()ed. + * Exactly the same as base64_encode except it uses + * - instead of + + * _ instead of / + * + * @param string $input base64UrlEncoded string + * @return string + */ + protected static function base64UrlDecode($input) { + return base64_decode(strtr($input, '-_', '+/')); + } + + /** + * Destroy the current session + */ + public function destroySession() { + $this->accessToken = null; + $this->signedRequest = null; + $this->user = null; + $this->clearAllPersistentData(); + + // Javascript sets a cookie that will be used in getSignedRequest that we + // need to clear if we can + $cookie_name = $this->getSignedRequestCookieName(); + if (array_key_exists($cookie_name, $_COOKIE)) { + unset($_COOKIE[$cookie_name]); + if (!headers_sent()) { + // The base domain is stored in the metadata cookie if not we fallback + // to the current hostname + $base_domain = '.'. $_SERVER['HTTP_HOST']; + + $metadata = $this->getMetadataCookie(); + if (array_key_exists('base_domain', $metadata) && + !empty($metadata['base_domain'])) { + $base_domain = $metadata['base_domain']; + } + + setcookie($cookie_name, '', 0, '/', $base_domain); + } else { + self::errorLog( + 'There exists a cookie that we wanted to clear that we couldn\'t '. + 'clear because headers was already sent. Make sure to do the first '. + 'API call before outputing anything' + ); + } + } + } + + /** + * Parses the metadata cookie that our Javascript API set + * + * @return an array mapping key to value + */ + protected function getMetadataCookie() { + $cookie_name = $this->getMetadataCookieName(); + if (!array_key_exists($cookie_name, $_COOKIE)) { + return array(); + } + + // The cookie value can be wrapped in "-characters so remove them + $cookie_value = trim($_COOKIE[$cookie_name], '"'); + + if (empty($cookie_value)) { + return array(); + } + + $parts = explode('&', $cookie_value); + $metadata = array(); + foreach ($parts as $part) { + $pair = explode('=', $part, 2); + if (!empty($pair[0])) { + $metadata[urldecode($pair[0])] = + (count($pair) > 1) ? urldecode($pair[1]) : ''; + } + } + + return $metadata; + } + + /** + * Each of the following four methods should be overridden in + * a concrete subclass, as they are in the provided Facebook class. + * The Facebook class uses PHP sessions to provide a primitive + * persistent store, but another subclass--one that you implement-- + * might use a database, memcache, or an in-memory cache. + * + * @see Facebook + */ + + /** + * Stores the given ($key, $value) pair, so that future calls to + * getPersistentData($key) return $value. This call may be in another request. + * + * @param string $key + * @param array $value + * + * @return void + */ + abstract protected function setPersistentData($key, $value); + + /** + * Get the data for $key, persisted by BaseFacebook::setPersistentData() + * + * @param string $key The key of the data to retrieve + * @param boolean $default The default value to return if $key is not found + * + * @return mixed + */ + abstract protected function getPersistentData($key, $default = false); + + /** + * Clear the data with $key from the persistent storage + * + * @param string $key + * @return void + */ + abstract protected function clearPersistentData($key); + + /** + * Clear all data from the persistent storage + * + * @return void + */ + abstract protected function clearAllPersistentData(); + + + /** + * Extending access_token expiration time through fb new endpoint + * returns an new access token which expires in 60 days + * + * http://developers.facebook.com/roadmap/offline-access-removal/#extend_token + * http://stackoverflow.com/a/9035036/1106794 + */ + function extendedAccessToken( $old_access_token ) + { + // Make a OAuth Request. + try { + $params = array( + 'client_id' => $this->getAppId(), + 'client_secret' => $this->getAppSecret(), + 'grant_type' => 'fb_exchange_token', + 'fb_exchange_token' => $old_access_token, + ); + + $response = $this->_oauthRequest( $this->getUrl( 'graph', '/oauth/access_token' ), $params ); + + // print_r( array( $this->getUrl( 'graph', '/oauth/access_token' ), $params, $response ) ); + } + catch ( FacebookApiException $e ) { + // most likely that user very recently revoked authorization. + // In any event, we don't have an access token, so say so. + return false; + } + + if (empty($response)) { + return false; + } + + $response_params = array(); + + parse_str($response, $response_params); + + if (!isset($response_params['access_token'])) { + return false; + } + + return $response_params['access_token']; + } + +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/Facebook/facebook.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/Facebook/facebook.php new file mode 100644 index 0000000..c577c2a --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/Facebook/facebook.php @@ -0,0 +1,93 @@ +constructSessionVariableName($key); + $_SESSION[$session_var_name] = $value; + } + + protected function getPersistentData($key, $default = false) { + if (!in_array($key, self::$kSupportedKeys)) { + self::errorLog('Unsupported key passed to getPersistentData.'); + return $default; + } + + $session_var_name = $this->constructSessionVariableName($key); + return isset($_SESSION[$session_var_name]) ? + $_SESSION[$session_var_name] : $default; + } + + protected function clearPersistentData($key) { + if (!in_array($key, self::$kSupportedKeys)) { + self::errorLog('Unsupported key passed to clearPersistentData.'); + return; + } + + $session_var_name = $this->constructSessionVariableName($key); + unset($_SESSION[$session_var_name]); + } + + protected function clearAllPersistentData() { + foreach (self::$kSupportedKeys as $key) { + $this->clearPersistentData($key); + } + } + + protected function constructSessionVariableName($key) { + return implode('_', array('fb', + $this->getAppId(), + $key)); + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/Facebook/fb_ca_chain_bundle.crt b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/Facebook/fb_ca_chain_bundle.crt new file mode 100644 index 0000000..b92d719 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/Facebook/fb_ca_chain_bundle.crt @@ -0,0 +1,121 @@ +-----BEGIN CERTIFICATE----- +MIIFgjCCBGqgAwIBAgIQDKKbZcnESGaLDuEaVk6fQjANBgkqhkiG9w0BAQUFADBm +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSUwIwYDVQQDExxEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBDQS0zMB4XDTEwMDExMzAwMDAwMFoXDTEzMDQxMTIzNTk1OVowaDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEX +MBUGA1UEChMORmFjZWJvb2ssIEluYy4xFzAVBgNVBAMUDiouZmFjZWJvb2suY29t +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9rzj7QIuLM3sdHu1HcI1VcR3g +b5FExKNV646agxSle1aQ/sJev1mh/u91ynwqd2BQmM0brZ1Hc3QrfYyAaiGGgEkp +xbhezyfeYhAyO0TKAYxPnm2cTjB5HICzk6xEIwFbA7SBJ2fSyW1CFhYZyo3tIBjj +19VjKyBfpRaPkzLmRwIDAQABo4ICrDCCAqgwHwYDVR0jBBgwFoAUUOpzidsp+xCP +nuUBINTeeZlIg/cwHQYDVR0OBBYEFPp+tsFBozkjrHlEnZ9J4cFj2eM0MA4GA1Ud +DwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMF8GA1UdHwRYMFYwKaAnoCWGI2h0dHA6 +Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9jYTMtZmIuY3JsMCmgJ6AlhiNodHRwOi8vY3Js +NC5kaWdpY2VydC5jb20vY2EzLWZiLmNybDCCAcYGA1UdIASCAb0wggG5MIIBtQYL +YIZIAYb9bAEDAAEwggGkMDoGCCsGAQUFBwIBFi5odHRwOi8vd3d3LmRpZ2ljZXJ0 +LmNvbS9zc2wtY3BzLXJlcG9zaXRvcnkuaHRtMIIBZAYIKwYBBQUHAgIwggFWHoIB +UgBBAG4AeQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQByAHQAaQBmAGkA +YwBhAHQAZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEA +bgBjAGUAIABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMA +UABTACAAYQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkA +IABBAGcAcgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBtAGkAdAAgAGwA +aQBhAGIAaQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBjAG8AcgBwAG8A +cgBhAHQAZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBlAHIAZQBuAGMA +ZQAuMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0BAQUF +AAOCAQEACOkTIdxMy11+CKrbGNLBSg5xHaTvu/v1wbyn3dO/mf68pPfJnX6ShPYy +4XM4Vk0x4uaFaU4wAGke+nCKGi5dyg0Esg7nemLNKEJaFAJZ9enxZm334lSCeARy +wlDtxULGOFRyGIZZPmbV2eNq5xdU/g3IuBEhL722mTpAye9FU/J8Wsnw54/gANyO +Gzkewigua8ip8Lbs9Cht399yAfbfhUP1DrAm/xEcnHrzPr3cdCtOyJaM6SRPpRqH +ITK5Nc06tat9lXVosSinT3KqydzxBYua9gCFFiR3x3DgZfvXkC6KDdUlDrNcJUub +a1BHnLLP4mxTHL6faAXYd05IxNn/IA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGVTCCBT2gAwIBAgIQCFH5WYFBRcq94CTiEsnCDjANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA3MDQwMzAwMDAwMFoXDTIyMDQwMzAwMDAwMFowZjEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTElMCMGA1UEAxMcRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +Q0EtMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9hCikQH17+NDdR +CPge+yLtYb4LDXBMUGMmdRW5QYiXtvCgFbsIYOBC6AUpEIc2iihlqO8xB3RtNpcv +KEZmBMcqeSZ6mdWOw21PoF6tvD2Rwll7XjZswFPPAAgyPhBkWBATaccM7pxCUQD5 +BUTuJM56H+2MEb0SqPMV9Bx6MWkBG6fmXcCabH4JnudSREoQOiPkm7YDr6ictFuf +1EutkozOtREqqjcYjbTCuNhcBoz4/yO9NV7UfD5+gw6RlgWYw7If48hl66l7XaAs +zPw82W3tzPpLQ4zJ1LilYRyyQLYoEt+5+F/+07LJ7z20Hkt8HEyZNp496+ynaF4d +32duXvsCAwEAAaOCAvcwggLzMA4GA1UdDwEB/wQEAwIBhjCCAcYGA1UdIASCAb0w +ggG5MIIBtQYLYIZIAYb9bAEDAAIwggGkMDoGCCsGAQUFBwIBFi5odHRwOi8vd3d3 +LmRpZ2ljZXJ0LmNvbS9zc2wtY3BzLXJlcG9zaXRvcnkuaHRtMIIBZAYIKwYBBQUH +AgIwggFWHoIBUgBBAG4AeQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQBy +AHQAaQBmAGkAYwBhAHQAZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBj +AGUAcAB0AGEAbgBjAGUAIABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAg +AEMAUAAvAEMAUABTACAAYQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQ +AGEAcgB0AHkAIABBAGcAcgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBt +AGkAdAAgAGwAaQBhAGIAaQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBj +AG8AcgBwAG8AcgBhAHQAZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBl +AHIAZQBuAGMAZQAuMA8GA1UdEwEB/wQFMAMBAf8wNAYIKwYBBQUHAQEEKDAmMCQG +CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wgY8GA1UdHwSBhzCB +hDBAoD6gPIY6aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0SGlnaEFz +c3VyYW5jZUVWUm9vdENBLmNybDBAoD6gPIY6aHR0cDovL2NybDQuZGlnaWNlcnQu +Y29tL0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUVWUm9vdENBLmNybDAfBgNVHSMEGDAW +gBSxPsNpA/i/RwHUmCYaCALvY2QrwzAdBgNVHQ4EFgQUUOpzidsp+xCPnuUBINTe +eZlIg/cwDQYJKoZIhvcNAQEFBQADggEBAF1PhPGoiNOjsrycbeUpSXfh59bcqdg1 +rslx3OXb3J0kIZCmz7cBHJvUV5eR13UWpRLXuT0uiT05aYrWNTf58SHEW0CtWakv +XzoAKUMncQPkvTAyVab+hA4LmzgZLEN8rEO/dTHlIxxFVbdpCJG1z9fVsV7un5Tk +1nq5GMO41lJjHBC6iy9tXcwFOPRWBW3vnuzoYTYMFEuFFFoMg08iXFnLjIpx2vrF +EIRYzwfu45DC9fkpx1ojcflZtGQriLCnNseaIGHr+k61rmsb5OPs4tk8QUmoIKRU +9ZKNu8BVIASm2LAXFszj0Mi0PeXZhMbT9m5teMl5Q+h6N/9cNUm/ocU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEQjCCA6ugAwIBAgIEQoclDjANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEy +MjIxNTI3MjdaFw0xNDA3MjIxNTU3MjdaMGwxCzAJBgNVBAYTAlVTMRUwEwYDVQQK +EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xKzApBgNV +BAMTIkRpZ2lDZXJ0IEhpZ2ggQXNzdXJhbmNlIEVWIFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGzOVz5vvUu+UtLTKm3+WBP8nNJUm2cSrD +1ZQ0Z6IKHLBfaaZAscS3so/QmKSpQVk609yU1jzbdDikSsxNJYL3SqVTEjju80lt +cZF+Y7arpl/DpIT4T2JRvvjF7Ns4kuMG5QiRDMQoQVX7y1qJFX5x6DW/TXIJPb46 +OFBbdzEbjbPHJEWap6xtABRaBLe6E+tRCphBQSJOZWGHgUFQpnlcid4ZSlfVLuZd +HFMsfpjNGgYWpGhz0DQEE1yhcdNafFXbXmThN4cwVgTlEbQpgBLxeTmIogIRfCdm +t4i3ePLKCqg4qwpkwr9mXZWEwaElHoddGlALIBLMQbtuC1E4uEvLAgMBAAGjggET +MIIBDzASBgNVHRMBAf8ECDAGAQH/AgEBMCcGA1UdJQQgMB4GCCsGAQUFBwMBBggr +BgEFBQcDAgYIKwYBBQUHAwQwMwYIKwYBBQUHAQEEJzAlMCMGCCsGAQUFBzABhhdo +dHRwOi8vb2NzcC5lbnRydXN0Lm5ldDAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8v +Y3JsLmVudHJ1c3QubmV0L3NlcnZlcjEuY3JsMB0GA1UdDgQWBBSxPsNpA/i/RwHU +mCYaCALvY2QrwzALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8BdiE1U9s/8KAGv7 +UISX8+1i0BowGQYJKoZIhvZ9B0EABAwwChsEVjcuMQMCAIEwDQYJKoZIhvcNAQEF +BQADgYEAUuVY7HCc/9EvhaYzC1rAIo348LtGIiMduEl5Xa24G8tmJnDioD2GU06r +1kjLX/ktCdpdBgXadbjtdrZXTP59uN0AXlsdaTiFufsqVLPvkp5yMnqnuI3E2o6p +NpAkoQSbB6kUCNnXcW26valgOjDLZFOnr241QiwdBAJAAE/rRa8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 +MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE +ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j +b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg +U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ +I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 +wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC +AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb +oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 +MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi +E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa +MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI +hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN +95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd +2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/LinkedIn/LinkedIn.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/LinkedIn/LinkedIn.php new file mode 100644 index 0000000..05b8cfe --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/LinkedIn/LinkedIn.php @@ -0,0 +1,2641 @@ + + * @copyright Copyright 2011, fiftyMission Inc. + * @license http://www.opensource.org/licenses/mit-license.php The MIT License + */ + +/** + * 'LinkedInException' class declaration. + * + * This class extends the base 'Exception' class. + * + * @access public + * @package classpackage + */ +class LinkedInException extends Exception {} + +/** + * 'LinkedIn' class declaration. + * + * This class provides generalized LinkedIn oauth functionality. + * + * @access public + * @package classpackage + */ +class LinkedIn { + // api/oauth settings + const _API_OAUTH_REALM = 'http://api.linkedin.com'; + const _API_OAUTH_VERSION = '1.0'; + + // the default response format from LinkedIn + const _DEFAULT_RESPONSE_FORMAT = 'xml'; + + // helper constants used to standardize LinkedIn <-> API communication. See demo page for usage. + const _GET_RESPONSE = 'lResponse'; + const _GET_TYPE = 'lType'; + + // Invitation API constants. + const _INV_SUBJECT = 'Invitation to connect'; + const _INV_BODY_LENGTH = 200; + + // API methods + const _METHOD_TOKENS = 'POST'; + + // Network API constants. + const _NETWORK_LENGTH = 1000; + const _NETWORK_HTML = ''; + + // response format type constants, see http://developer.linkedin.com/docs/DOC-1203 + const _RESPONSE_JSON = 'JSON'; + const _RESPONSE_JSONP = 'JSONP'; + const _RESPONSE_XML = 'XML'; + + // Share API constants + const _SHARE_COMMENT_LENGTH = 700; + const _SHARE_CONTENT_TITLE_LENGTH = 200; + const _SHARE_CONTENT_DESC_LENGTH = 400; + + // LinkedIn API end-points + const _URL_ACCESS = 'https://api.linkedin.com/uas/oauth/accessToken'; + const _URL_API = 'https://api.linkedin.com'; + const _URL_AUTH = 'https://www.linkedin.com/uas/oauth/authenticate?oauth_token='; + // const _URL_REQUEST = 'https://api.linkedin.com/uas/oauth/requestToken'; + const _URL_REQUEST = 'https://api.linkedin.com/uas/oauth/requestToken?scope=r_basicprofile+r_emailaddress'; + const _URL_REVOKE = 'https://api.linkedin.com/uas/oauth/invalidateToken'; + + // Library version + const _VERSION = '3.2.0'; + + // oauth properties + protected $callback; + protected $token = NULL; + + // application properties + protected $application_key, + $application_secret; + + // the format of the data to return + protected $response_format = self::_DEFAULT_RESPONSE_FORMAT; + + // last request fields + public $last_request_headers, + $last_request_url; + + /** + * Create a LinkedIn object, used for OAuth-based authentication and + * communication with the LinkedIn API. + * + * @param arr $config + * The 'start-up' object properties: + * - appKey => The application's API key + * - appSecret => The application's secret key + * - callbackUrl => [OPTIONAL] the callback URL + * + * @return obj + * A new LinkedIn object. + */ + public function __construct($config) { + if(!is_array($config)) { + // bad data passed + throw new LinkedInException('LinkedIn->__construct(): bad data passed, $config must be of type array.'); + } + $this->setApplicationKey($config['appKey']); + $this->setApplicationSecret($config['appSecret']); + $this->setCallbackUrl($config['callbackUrl']); + } + + /** + * The class destructor. + * + * Explicitly clears LinkedIn object from memory upon destruction. + */ + public function __destruct() { + unset($this); + } + + /** + * Bookmark a job. + * + * Calling this method causes the current user to add a bookmark for the + * specified job: + * + * http://developer.linkedin.com/docs/DOC-1323 + * + * @param str $jid + * Job ID you want to bookmark. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function bookmarkJob($jid) { + // check passed data + if(!is_string($jid)) { + // bad data passed + throw new LinkedInException('LinkedIn->bookmarkJob(): bad data passed, $jid must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/job-bookmarks'; + $response = $this->fetch('POST', $query, '' . trim($jid) . ''); + + /** + * Check for successful request (a 201 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(201, $response); + } + + /** + * Get list of jobs you have bookmarked. + * + * Returns a list of jobs the current user has bookmarked, per: + * + * http://developer.linkedin.com/docs/DOC-1323 + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function bookmarkedJobs() { + // construct and send the request + $query = self::_URL_API . '/v1/people/~/job-bookmarks'; + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Custom addition to make code compatible with PHP 5.2 + */ + private function intWalker($value, $key) { + if(!is_int($value)) { + throw new LinkedInException('LinkedIn->checkResponse(): $http_code_required must be an integer or an array of integer values'); + } + } + + /** + * Used to check whether a response LinkedIn object has the required http_code or not and + * returns an appropriate LinkedIn object. + * + * @param var $http_code_required + * The required http response from LinkedIn, passed in either as an integer, + * or an array of integers representing the expected values. + * @param arr $response + * An array containing a LinkedIn response. + * + * @return boolean + * TRUE or FALSE depending on if the passed LinkedIn response matches the expected response. + */ + private function checkResponse($http_code_required, $response) { + // check passed data + if(is_array($http_code_required)) { + array_walk($http_code_required, array($this, 'intWalker')); + } else { + if(!is_int($http_code_required)) { + throw new LinkedInException('LinkedIn->checkResponse(): $http_code_required must be an integer or an array of integer values'); + } else { + $http_code_required = array($http_code_required); + } + } + if(!is_array($response)) { + throw new LinkedInException('LinkedIn->checkResponse(): $response must be an array'); + } + + // check for a match + if(in_array($response['info']['http_code'], $http_code_required)) { + // response found + $response['success'] = TRUE; + } else { + // response not found + $response['success'] = FALSE; + $response['error'] = 'HTTP response from LinkedIn end-point was not code ' . implode(', ', $http_code_required); + } + return $response; + } + + /** + * Close a job. + * + * Calling this method causes the passed job to be closed, per: + * + * http://developer.linkedin.com/docs/DOC-1151 + * + * @param str $jid + * Job ID you want to close. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function closeJob($jid) { + // check passed data + if(!is_string($jid)) { + // bad data passed + throw new LinkedInException('LinkedIn->closeJob(): bad data passed, $jid must be of string value.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/jobs/partner-job-id=' . trim($jid); + $response = $this->fetch('DELETE', $query); + + /** + * Check for successful request (a 204 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(204, $response); + } + + /** + * Share comment posting method. + * + * Post a comment on an existing connections shared content. API details can + * be found here: + * + * http://developer.linkedin.com/docs/DOC-1043 + * + * @param str $uid + * The LinkedIn update ID. + * @param str $comment + * The share comment to be posted. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function comment($uid, $comment) { + // check passed data + if(!is_string($uid)) { + // bad data passed + throw new LinkedInException('LinkedIn->comment(): bad data passed, $uid must be of type string.'); + } + if(!is_string($comment)) { + // nothing/non-string passed, raise an exception + throw new LinkedInException('LinkedIn->comment(): bad data passed, $comment must be a non-zero length string.'); + } + + /** + * Share comment rules: + * + * 1) No HTML permitted. + * 2) Comment cannot be longer than 700 characters. + */ + $comment = substr(trim(htmlspecialchars(strip_tags($comment))), 0, self::_SHARE_COMMENT_LENGTH); + $data = ' + + ' . $comment . ' + '; + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/network/updates/key=' . $uid . '/update-comments'; + $response = $this->fetch('POST', $query, $data); + + /** + * Check for successful request (a 201 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(201, $response); + } + + /** + * Share comment retrieval. + * + * Return all comments associated with a given network update: + * + * http://developer.linkedin.com/docs/DOC-1043 + * + * @param str $uid + * The LinkedIn update ID. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function comments($uid) { + // check passed data + if(!is_string($uid)) { + // bad data passed + throw new LinkedInException('LinkedIn->comments(): bad data passed, $uid must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/network/updates/key=' . $uid . '/update-comments'; + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Company profile retrieval function. + * + * Takes a string of parameters as input and requests company profile data + * from the LinkedIn Company Profile API. See the official documentation for + * $options 'field selector' formatting: + * + * http://developer.linkedin.com/docs/DOC-1014 + * http://developer.linkedin.com/docs/DOC-1259 + * + * @param str $options + * Data retrieval options. + * @param bool $by_email + * [OPTIONAL] Search by email domain? + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function company($options, $by_email = FALSE) { + // check passed data + if(!is_string($options)) { + // bad data passed + throw new LinkedInException('LinkedIn->company(): bad data passed, $options must be of type string.'); + } + if(!is_bool($by_email)) { + // bad data passed + throw new LinkedInException('LinkedIn->company(): bad data passed, $by_email must be of type boolean.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/companies' . ($by_email ? '' : '/') . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Company products and their associated recommendations. + * + * The product data type contains details about a company's product or + * service, including recommendations from LinkedIn members, and replies from + * company representatives. + * + * http://developer.linkedin.com/docs/DOC-1327 + * + * @param str $cid + * Company ID you want the producte for. + * @param str $options + * [OPTIONAL] Data retrieval options. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function companyProducts($cid, $options = '') { + // check passed data + if(!is_string($cid)) { + // bad data passed + throw new LinkedInException('LinkedIn->companyProducts(): bad data passed, $cid must be of type string.'); + } + if(!is_string($options)) { + // bad data passed + throw new LinkedInException('LinkedIn->companyProducts(): bad data passed, $options must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/companies/' . trim($cid) . '/products' . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Connection retrieval function. + * + * Takes a string of parameters as input and requests connection-related data + * from the Linkedin Connections API. See the official documentation for + * $options 'field selector' formatting: + * + * http://developer.linkedin.com/docs/DOC-1014 + * + * @param str $options + * [OPTIONAL] Data retrieval options. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function connections($options = '~/connections') { + // check passed data + if(!is_string($options)) { + // bad data passed + throw new LinkedInException('LinkedIn->connections(): bad data passed, $options must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/' . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * This creates a post in the specified group with the specified title and specified summary. + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $gid + * The group id. + * @param str $title + * The title of the post. This must be non-empty. + * @param str $summary + * [OPTIONAL] The content or summary of the post. This can be empty. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function createPost($gid, $title, $summary = '') { + if(!is_string($gid)) { + throw new LinkedInException('LinkedIn->createPost(): bad data passed, $gid must be of type string.'); + } + if(!is_string($title) || empty($title)) { + throw new LinkedInException('LinkedIn->createPost(): bad data passed, $title must be a non-empty string.'); + } + if(!is_string($summary)) { + throw new LinkedInException('LinkedIn->createPost(): bad data passed, $summary must be of type string.'); + } + + // construct the XML + $data = ' + + '. $title . ' + ' . $summary . ' + '; + + // construct and send the request + $query = self::_URL_API . '/v1/groups/' . trim($gid) . '/posts'; + $response = $this->fetch('POST', $query, $data); + + /** + * Check for successful request (a 201 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(201, $response); + } + + /** + * This deletes the specified post if you are the owner or moderator that post. + * Otherwise, it just flags the post as inappropriate. + * + * https://developer.linkedin.com/documents/groups-api + * + * @param str $pid + * The post id. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function deletePost($pid) { + if(!is_string($pid)) { + throw new LinkedInException('LinkedIn->deletePost(): bad data passed, $pid must be of type string'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/posts/' . trim($pid); + $response = $this->fetch('DELETE', $query); + + /** + * Check for successful request (a 204 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(204, $response); + } + + /** + * Edit a job. + * + * Calling this method causes the passed job to be edited, with the passed + * XML instructing which fields to change, per: + * + * http://developer.linkedin.com/docs/DOC-1154 + * http://developer.linkedin.com/docs/DOC-1142 + * + * @param str $jid + * Job ID you want to renew. + * @param str $xml + * The XML containing the job fields to edit. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function editJob($jid, $xml) { + // check passed data + if(!is_string($jid)) { + // bad data passed + throw new LinkedInException('LinkedIn->editJob(): bad data passed, $jid must be of string value.'); + } + if(is_string($xml)) { + $xml = trim(stripslashes($xml)); + } else { + // bad data passed + throw new LinkedInException('LinkedIn->editJob(): bad data passed, $xml must be of string value.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/jobs/partner-job-id=' . trim($jid); + $response = $this->fetch('PUT', $query, $xml); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * General data send/request method. + * + * @param str $method + * The data communication method. + * @param str $url + * The Linkedin API endpoint to connect with. + * @param str $data + * [OPTIONAL] The data to send to LinkedIn. + * @param arr $parameters + * [OPTIONAL] Addition OAuth parameters to send to LinkedIn. + * + * @return arr + * Array containing: + * + * array( + * 'info' => Connection information, + * 'linkedin' => LinkedIn response, + * 'oauth' => The OAuth request string that was sent to LinkedIn + * ) + */ + protected function fetch($method, $url, $data = NULL, $parameters = array()) { + // check for cURL + if(!extension_loaded('curl')) { + // cURL not present + throw new LinkedInException('LinkedIn->fetch(): PHP cURL extension does not appear to be loaded/present.'); + } + + try { + // generate OAuth values + $oauth_consumer = new OAuthConsumer($this->getApplicationKey(), $this->getApplicationSecret(), $this->getCallbackUrl()); + $oauth_token = $this->getToken(); + $oauth_token = (!is_null($oauth_token)) ? new OAuthToken($oauth_token['oauth_token'], $oauth_token['oauth_token_secret']) : NULL; + $defaults = array( + 'oauth_version' => self::_API_OAUTH_VERSION + ); + $parameters = array_merge($defaults, $parameters); + + // generate OAuth request + $oauth_req = OAuthRequest::from_consumer_and_token($oauth_consumer, $oauth_token, $method, $url, $parameters); + $oauth_req->sign_request(new OAuthSignatureMethod_HMAC_SHA1(), $oauth_consumer, $oauth_token); + + // start cURL, checking for a successful initiation + if(!$handle = curl_init()) { + // cURL failed to start + throw new LinkedInException('LinkedIn->fetch(): cURL did not initialize properly.'); + } + + // set cURL options, based on parameters passed + curl_setopt($handle, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($handle, CURLOPT_RETURNTRANSFER, TRUE); + curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, FALSE); + curl_setopt($handle, CURLOPT_URL, $url); + curl_setopt($handle, CURLOPT_VERBOSE, FALSE); + + // configure the header we are sending to LinkedIn - http://developer.linkedin.com/docs/DOC-1203 + $header = array($oauth_req->to_header(self::_API_OAUTH_REALM)); + if(is_null($data)) { + // not sending data, identify the content type + $header[] = 'Content-Type: text/plain; charset=UTF-8'; + switch($this->getResponseFormat()) { + case self::_RESPONSE_JSON: + $header[] = 'x-li-format: json'; + break; + case self::_RESPONSE_JSONP: + $header[] = 'x-li-format: jsonp'; + break; + } + } else { + $header[] = 'Content-Type: text/xml; charset=UTF-8'; + curl_setopt($handle, CURLOPT_POSTFIELDS, $data); + } + curl_setopt($handle, CURLOPT_HTTPHEADER, $header); + + // set the last url, headers + $this->last_request_url = $url; + $this->last_request_headers = $header; + + // gather the response + $return_data['linkedin'] = curl_exec($handle); + $return_data['info'] = curl_getinfo($handle); + $return_data['oauth']['header'] = $oauth_req->to_header(self::_API_OAUTH_REALM); + $return_data['oauth']['string'] = $oauth_req->base_string; + + // check for throttling + if(self::isThrottled($return_data['linkedin'])) { + throw new LinkedInException('LinkedIn->fetch(): throttling limit for this user/application has been reached for LinkedIn resource - ' . $url); + } + + //TODO - add check for NO response (http_code = 0) from cURL + + // close cURL connection + curl_close($handle); + + // no exceptions thrown, return the data + return $return_data; + } catch(OAuthException $e) { + // oauth exception raised + throw new LinkedInException('OAuth exception caught: ' . $e->getMessage()); + } + } + + /** + * This flags a specified post as specified by type. + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $pid + * The post id. + * @param str $type + * The type to flag the post as. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function flagPost($pid, $type) { + if(!is_string($pid)) { + throw new LinkedInException('LinkedIn->flagPost(): bad data passed, $pid must be of type string'); + } + if(!is_string($type)) { + throw new LinkedInException('LinkedIn->flagPost(): bad data passed, $like must be of type string'); + } + //Constructing the xml + $data = ''; + switch($type) { + case 'promotion': + $data .= 'promotion'; + break; + case 'job': + $data .= 'job'; + break; + default: + throw new LinkedInException('LinkedIn->flagPost(): invalid value for $type, must be one of: "promotion", "job"'); + break; + } + + // construct and send the request + $query = self::_URL_API . '/v1/posts/' . $pid . '/category/code'; + $response = $this->fetch('PUT', $query, $data); + + /** + * Check for successful request (a 204 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(204, $response); + } + + /** + * Follow a company. + * + * Calling this method causes the current user to start following the + * specified company, per: + * + * http://developer.linkedin.com/docs/DOC-1324 + * + * @param str $cid + * Company ID you want to follow. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function followCompany($cid) { + // check passed data + if(!is_string($cid)) { + // bad data passed + throw new LinkedInException('LinkedIn->followCompany(): bad data passed, $cid must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/following/companies'; + $response = $this->fetch('POST', $query, '' . trim($cid) . ''); + + /** + * Check for successful request (a 201 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(201, $response); + } + + /** + * Follows/Unfollows the specified post. + * + * https://developer.linkedin.com/documents/groups-api + * + * @param str $pid + * The post id. + * @param bool $follow + * Determines whether to follow or unfollow the post. TRUE = follow, FALSE = unfollow + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + + public function followPost($pid, $follow) { + if(!is_string($pid)) { + throw new LinkedInException('LinkedIn->followPost(): bad data passed, $pid must be of type string'); + } + if(!($follow === TRUE || $follow === FALSE)) { + throw new LinkedInException('LinkedIn->followPost(): bad data passed, $follow must be of type boolean'); + } + + // construct the XML + $data = ' + '. (($follow) ? 'true' : 'false'). ''; + + // construct and send the request + $query = self::_URL_API . '/v1/posts/' . trim($pid) . '/relation-to-viewer/is-following'; + $response = $this->fetch('PUT', $query, $data); + + /** + * Check for successful request (a 204 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(204, $response); + } + + /** + * Get list of companies you follow. + * + * Returns a list of companies the current user is currently following, per: + * + * http://developer.linkedin.com/docs/DOC-1324 + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function followedCompanies() { + // construct and send the request + $query = self::_URL_API . '/v1/people/~/following/companies'; + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Get the application_key property. + * + * @return str + * The application key. + */ + public function getApplicationKey() { + return $this->application_key; + } + + /** + * Get the application_secret property. + * + * @return str + * The application secret. + */ + public function getApplicationSecret() { + return $this->application_secret; + } + + /** + * Get the callback property. + * + * @return str + * The callback url. + */ + public function getCallbackUrl() { + return $this->callback; + } + + /** + * Get the response_format property. + * + * @return str + * The response format. + */ + public function getResponseFormat() { + return $this->response_format; + } + + /** + * Get the token_access property. + * + * @return arr + * The access token. + */ + public function getToken() { + return $this->token; + } + + /** + * [DEPRECATED] Get the token_access property. + * + * @return arr + * The access token. + */ + public function getTokenAccess() { + return $this->getToken(); + } + + /** + * + * Get information about a specific group. + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $gid + * The group id. + * + * @param str $options + * [OPTIONAL] Field selectors for the group. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + + public function group($gid, $options = '') { + if(!is_string($gid)){ + throw new LinkedInException('LinkedIn->group(): bad data passed, $gid must be of type string.'); + } + if(!is_string($options)) { + throw new LinkedInException('LinkedIn->group(): bad data passed, $options must be of type string'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/groups/' . trim($gid) . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * This returns all the groups the user is a member of. + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $options + * [OPTIONAL] Field selectors for the groups. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function groupMemberships($options = '') { + if(!is_string($options)) { + throw new LinkedInException('LinkedIn->groupMemberships(): bad data passed, $options must be of type string'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/group-memberships' . trim($options) . '?membership-state=member'; + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * This gets a specified post made within a group. + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $pid + * The post id. + * @param str $options + * [OPTIONAL] Field selectors for the post. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function groupPost($pid, $options = '') { + if(!is_string($pid)) { + throw new LinkedInException('LinkedIn->groupPost(): bad data passed, $pid must be of type string.'); + } + if(!is_string($options)) { + throw new LinkedInException('LinkedIn->groupPost(): bad data passed, $options must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/posts/' . trim($pid) . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * This returns all the comments made on the specified post within a group. + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $pid + * The post id. + * @param str $options + * [OPTIONAL] Field selectors for the post comments. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function groupPostComments($pid, $options = ''){ + if(!is_string($pid)){ + throw new LinkedInException('LinkedIn->groupPostComments(): bad data passed, $pid must be of type string.'); + } + if(!is_string($options)) { + throw new LinkedInException('LinkedIn->groupPostComments(): bad data passed, $options must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/posts/' . trim($pid) . '/comments' . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * This returns all the posts within a group. + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $gid + * The group id. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function groupPosts($gid, $options = '') { + if(!is_string($gid)){ + throw new LinkedInException('LinkedIn->groupPosts(): bad data passed, $gid must be of type string'); + } + if(!is_string($options)){ + throw new LinkedInException('LinkedIn->groupPosts(): bad data passed, $options must be of type string'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/groups/' . trim($gid) .'/posts' . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * This returns the group settings of the specified group + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $gid + * The group id. + * @param str $options + * [OPTIONAL] Field selectors for the group. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function groupSettings($gid, $options = '') { + if(!is_string($gid)) { + throw new LinkedInException('LinkedIn->groupSettings(): bad data passed, $gid must be of type string'); + } + if(!is_string($options)) { + throw new LinkedInException('LinkedIn->groupSettings(): bad data passed, $options must be of type string'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/group-memberships/' . trim($gid) . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Send connection invitations. + * + * Send an invitation to connect to your network, either by email address or + * by LinkedIn ID. Details on the API here: + * + * http://developer.linkedin.com/docs/DOC-1012 + * + * @param str $method + * The invitation method to process. + * @param str $recipient + * The email/id to send the invitation to. + * @param str $subject + * The subject of the invitation to send. + * @param str $body + * The body of the invitation to send. + * @param str $type + * [OPTIONAL] The invitation request type (only friend is supported at this time by the Invite API). + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function invite($method, $recipient, $subject, $body, $type = 'friend') { + /** + * Clean up the passed data per these rules: + * + * 1) Message must be sent to one recipient (only a single recipient permitted for the Invitation API) + * 2) No HTML permitted + * 3) 200 characters max in the invitation subject + * 4) Only able to connect as a friend at this point + */ + // check passed data + if(empty($recipient)) { + throw new LinkedInException('LinkedIn->invite(): you must provide an invitation recipient.'); + } + switch($method) { + case 'email': + if(is_array($recipient)) { + $recipient = array_map('trim', $recipient); + } else { + // bad format for recipient for email method + throw new LinkedInException('LinkedIn->invite(): invitation recipient email/name array is malformed.'); + } + break; + case 'id': + $recipient = trim($recipient); + if(!self::isId($recipient)) { + // bad format for recipient for id method + throw new LinkedInException('LinkedIn->invite(): invitation recipient ID does not match LinkedIn format.'); + } + break; + default: + throw new LinkedInException('LinkedIn->invite(): bad invitation method, must be one of: email, id.'); + break; + } + if(!empty($subject)) { + $subject = trim(htmlspecialchars(strip_tags(stripslashes($subject)))); + } else { + throw new LinkedInException('LinkedIn->invite(): message subject is empty.'); + } + if(!empty($body)) { + $body = trim(htmlspecialchars(strip_tags(stripslashes($body)))); + if(strlen($body) > self::_INV_BODY_LENGTH) { + throw new LinkedInException('LinkedIn->invite(): message body length is too long - max length is ' . self::_INV_BODY_LENGTH . ' characters.'); + } + } else { + throw new LinkedInException('LinkedIn->invite(): message body is empty.'); + } + switch($type) { + case 'friend': + break; + default: + throw new LinkedInException('LinkedIn->invite(): bad invitation type, must be one of: friend.'); + break; + } + + // construct the xml data + $data = ' + + + '; + switch($method) { + case 'email': + // email-based invitation + $data .= ' + ' . htmlspecialchars($recipient['first-name']) . ' + ' . htmlspecialchars($recipient['last-name']) . ' + '; + break; + case 'id': + // id-based invitation + $data .= ''; + break; + } + $data .= ' + + ' . $subject . ' + ' . $body . ' + + + '; + switch($type) { + case 'friend': + $data .= 'friend'; + break; + } + $data .= ' '; + switch($method) { + case 'id': + // id-based invitation, we need to get the authorization information + $query = 'id=' . $recipient . ':(api-standard-profile-request)'; + $response = self::profile($query); + if($response['info']['http_code'] == 200) { + $response['linkedin'] = self::xmlToArray($response['linkedin']); + if($response['linkedin'] === FALSE) { + // bad XML data + throw new LinkedInException('LinkedIn->invite(): LinkedIn returned bad XML data.'); + } + $authentication = explode(':', $response['linkedin']['person']['children']['api-standard-profile-request']['children']['headers']['children']['http-header']['children']['value']['content']); + + // complete the xml + $data .= ' + ' . $authentication[0] . ' + ' . $authentication[1] . ' + '; + } else { + // bad response from the profile request, not a valid ID? + throw new LinkedInException('LinkedIn->invite(): could not send invitation, LinkedIn says: ' . print_r($response['linkedin'], TRUE)); + } + break; + } + $data .= ' + + '; + + // send request + $query = self::_URL_API . '/v1/people/~/mailbox'; + $response = $this->fetch('POST', $query, $data); + + /** + * Check for successful request (a 201 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(201, $response); + } + + /** + * LinkedIn ID validation. + * + * Checks the passed string $id to see if it has a valid LinkedIn ID format, + * which is, as of October 15th, 2010: + * + * 10 alpha-numeric mixed-case characters, plus underscores and dashes. + * + * @param str $id + * A possible LinkedIn ID. + * + * @return bool + * TRUE/FALSE depending on valid ID format determination. + */ + public static function isId($id) { + // check passed data + if(!is_string($id)) { + // bad data passed + throw new LinkedInException('LinkedIn->isId(): bad data passed, $id must be of type string.'); + } + + $pattern = '/^[a-z0-9_\-]{10}$/i'; + if($match = preg_match($pattern, $id)) { + // we have a match + $return_data = TRUE; + } else { + // no match + $return_data = FALSE; + } + return $return_data; + } + + /** + * Throttling check. + * + * Checks the passed LinkedIn response to see if we have hit a throttling + * limit: + * + * http://developer.linkedin.com/docs/DOC-1112 + * + * @param arr $response + * The LinkedIn response. + * + * @return bool + * TRUE/FALSE depending on content of response. + */ + public static function isThrottled($response) { + $return_data = FALSE; + + // check the variable + if(!empty($response) && is_string($response)) { + // we have an array and have a properly formatted LinkedIn response + + // store the response in a temp variable + $temp_response = self::xmlToArray($response); + if($temp_response !== FALSE) { + // check to see if we have an error + if(array_key_exists('error', $temp_response) && ($temp_response['error']['children']['status']['content'] == 403) && preg_match('/throttle/i', $temp_response['error']['children']['message']['content'])) { + // we have an error, it is 403 and we have hit a throttle limit + $return_data = TRUE; + } + } + } + return $return_data; + } + + /** + * Job posting detail info retrieval function. + * + * The Jobs API returns detailed information about job postings on LinkedIn. + * Find the job summary, description, location, and apply our professional graph + * to present the relationship between the current member and the job poster or + * hiring manager. + * + * http://developer.linkedin.com/docs/DOC-1322 + * + * @param str $jid + * ID of the job you want to look up. + * @param str $options + * [OPTIONAL] Data retrieval options. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function job($jid, $options = '') { + // check passed data + if(!is_string($jid)) { + // bad data passed + throw new LinkedInException('LinkedIn->job(): bad data passed, $jid must be of type string.'); + } + if(!is_string($options)) { + // bad data passed + throw new LinkedInException('LinkedIn->job(): bad data passed, $options must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/jobs/' . trim($jid) . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Join the specified group, per: + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $gid + * The group id. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function joinGroup($gid) { + if(!is_string($gid)) { + throw new LinkedInException('LinkedIn->joinGroup(): bad data passed, $gid must be of type string.'); + } + + // constructing the XML + $data = ' + + + member + + '; + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/group-memberships/' . trim($gid); + $response = $this->fetch('PUT', $query, $data); + + /** + * Check for successful request (a 200 or 201 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(array(200, 201), $response); + } + + /** + * Returns the last request header from the previous call to the + * LinkedIn API. + * + * @returns str + * The header, in string format. + */ + public function lastRequestHeader() { + return $this->last_request_headers; + } + + /** + * Returns the last request url from the previous call to the + * LinkedIn API. + * + * @returns str + * The url, in string format. + */ + public function lastRequestUrl() { + return $this->last_request_url; + } + + /** + * Leave the specified group, per:. + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $gid + * The group id. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function leaveGroup($gid){ + if(!is_string($gid)) { + throw new LinkedInException('LinkedIn->leaveGroup(): bad data passed, $gid must be of type string'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/group-memberships/' .trim($gid); + $response = $this->fetch('DELETE', $query); + + /** + * Check for successful request (a 204 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(204, $response); + } + + /** + * Like another user's network update, per: + * + * http://developer.linkedin.com/docs/DOC-1043 + * + * @param str $uid + * The LinkedIn update ID. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function like($uid) { + // check passed data + if(!is_string($uid)) { + // bad data passed + throw new LinkedInException('LinkedIn->like(): bad data passed, $uid must be of type string.'); + } + + // construct the XML + $data = ' + true'; + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/network/updates/key=' . $uid . '/is-liked'; + $response = $this->fetch('PUT', $query, $data); + + /** + * Check for successful request (a 201 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(201, $response); + } + + /** + * Likes/unlikes the specified post, per: + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $pid + * The post id. + * @param bool $like + * Determines whether to like or unlike. TRUE = like, FALSE = unlike. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function likePost($pid, $like) { + if(!is_string($pid)) { + throw new LinkedInException ('LinkedIn->likePost(): bad data passed, $pid must be of type string'); + } + if(!($like === TRUE || $like === FALSE)) { + throw new LinkedInException('LinkedIn->likePost(): bad data passed, $like must be of type boolean'); + } + + // construct the XML + $data = ' + '.(($like) ? 'true': 'false').''; + + // construct and send the request + $query = self::_URL_API . '/v1/posts/' . trim($pid) . '/relation-to-viewer/is-liked'; + $response = $this->fetch('PUT', $query, $data); + + /** + * Check for successful request (a 204 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(204, $response); + } + + /** + * Retrieve network update likes. + * + * Return all likes associated with a given network update: + * + * http://developer.linkedin.com/docs/DOC-1043 + * + * @param str $uid + * The LinkedIn update ID. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function likes($uid) { + // check passed data + if(!is_string($uid)) { + // bad data passed + throw new LinkedInException('LinkedIn->likes(): bad data passed, $uid must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/network/updates/key=' . $uid . '/likes'; + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Connection messaging method. + * + * Send a message to your network connection(s), optionally copying yourself. + * Full details from LinkedIn on this functionality can be found here: + * + * http://developer.linkedin.com/docs/DOC-1044 + * + * @param arr $recipients + * The connection(s) to send the message to. + * @param str $subject + * The subject of the message to send. + * @param str $body + * The body of the message to send. + * @param bool $copy_self + * [OPTIONAL] Also update the teathered Twitter account. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function message($recipients, $subject, $body, $copy_self = FALSE) { + /** + * Clean up the passed data per these rules: + * + * 1) Message must be sent to at least one recipient + * 2) No HTML permitted + */ + if(!empty($subject) && is_string($subject)) { + $subject = trim(strip_tags(stripslashes($subject))); + } else { + throw new LinkedInException('LinkedIn->message(): bad data passed, $subject must be of type string.'); + } + if(!empty($body) && is_string($body)) { + $body = trim(strip_tags(stripslashes($body))); + } else { + throw new LinkedInException('LinkedIn->message(): bad data passed, $body must be of type string.'); + } + if(!is_array($recipients) || count($recipients) < 1) { + // no recipients, and/or bad data + throw new LinkedInException('LinkedIn->message(): at least one message recipient required.'); + } + + // construct the xml data + $data = ' + + '; + $data .= ($copy_self) ? '' : ''; + for($i = 0; $i < count($recipients); $i++) { + if(is_string($recipients[$i])) { + $data .= ''; + } else { + throw new LinkedInException ('LinkedIn->message(): bad data passed, $recipients must be an array of type string.'); + } + } + $data .= ' + ' . htmlspecialchars($subject) . ' + ' . htmlspecialchars($body) . ' + '; + + // send request + $query = self::_URL_API . '/v1/people/~/mailbox'; + $response = $this->fetch('POST', $query, $data); + + /** + * Check for successful request (a 201 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(201, $response); + } + + /** + * Job posting method. + * + * Post a job to LinkedIn, assuming that you have access to this feature. + * Full details from LinkedIn on this functionality can be found here: + * + * http://developer.linkedin.com/community/jobs?view=documents + * + * @param str $xml + * The XML defining a job to post. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function postJob($xml) { + // check passed data + if(is_string($xml)) { + $xml = trim(stripslashes($xml)); + } else { + throw new LinkedInException('LinkedIn->postJob(): bad data passed, $xml must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/jobs'; + $response = $this->fetch('POST', $query, $xml); + + /** + * Check for successful request (a 201 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(201, $response); + } + + /** + * General profile retrieval function. + * + * Takes a string of parameters as input and requests profile data from the + * Linkedin Profile API. See the official documentation for $options + * 'field selector' formatting: + * + * http://developer.linkedin.com/docs/DOC-1014 + * http://developer.linkedin.com/docs/DOC-1002 + * + * @param str $options + * [OPTIONAL] Data retrieval options. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function profile($options = '~') { + // check passed data + if(!is_string($options)) { + // bad data passed + throw new LinkedInException('LinkedIn->profile(): bad data passed, $options must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/' . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Manual API call method, allowing for support for un-implemented API + * functionality to be supported. + * + * @param str $method + * The data communication method. + * @param str $url + * The Linkedin API endpoint to connect with - should NOT include the + * leading https://api.linkedin.com/v1. + * @param str $body + * [OPTIONAL] The URL-encoded body data to send to LinkedIn with the request. + * + * @return arr + * Array containing retrieval information, LinkedIn response. Note that you + * must manually check the return code and compare this to the expected + * API response to determine if the raw call was successful. + */ + public function raw($method, $url, $body = NULL) { + if(!is_string($method)) { + // bad data passed + throw new LinkedInException('LinkedIn->raw(): bad data passed, $method must be of string value.'); + } + if(!is_string($url)) { + // bad data passed + throw new LinkedInException('LinkedIn->raw(): bad data passed, $url must be of string value.'); + } + if(!is_null($body) && !is_string($url)) { + // bad data passed + throw new LinkedInException('LinkedIn->raw(): bad data passed, $body must be of string value.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1' . trim($url); + return $this->fetch($method, $query, $body); + } + + /** + * This removes the specified group from the group suggestions, per: + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $gid + * The group id. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function removeSuggestedGroup($gid) { + if(!is_string($gid)) { + throw new LinkedInException('LinkedIn->removeSuggestedGroup(): bad data passed, $gid must be of type string'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/suggestions/groups/' .trim($gid); + $response = $this->fetch('DELETE', $query); + + /** + * Check for successful request (a 204 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(204, $response); + } + + /** + * Renew a job. + * + * Calling this method causes the passed job to be renewed, per: + * + * http://developer.linkedin.com/docs/DOC-1154 + * + * @param str $jid + * Job ID you want to renew. + * @param str $cid + * Contract ID that covers the passed Job ID. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function renewJob($jid, $cid) { + // check passed data + if(!is_string($jid)) { + // bad data passed + throw new LinkedInException('LinkedIn->renewJob(): bad data passed, $jid must be of string value.'); + } + if(!is_string($cid)) { + // bad data passed + throw new LinkedInException('LinkedIn->renewJob(): bad data passed, $cid must be of string value.'); + } + + // construct the xml data + $data = ' + + ' . trim($cid) . ' + + '; + + // construct and send the request + $query = self::_URL_API . '/v1/jobs/partner-job-id=' . trim($jid); + $response = $this->fetch('PUT', $query, $data); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Access token retrieval. + * + * Request the user's access token from the Linkedin API. + * + * @param str $token + * The token returned from the user authorization stage. + * @param str $secret + * The secret returned from the request token stage. + * @param str $verifier + * The verification value from LinkedIn. + * + * @return arr + * The Linkedin OAuth/http response, in array format. + */ + public function retrieveTokenAccess($token, $secret, $verifier) { + // check passed data + if(!is_string($token) || !is_string($secret) || !is_string($verifier)) { + // nothing passed, raise an exception + throw new LinkedInException('LinkedIn->retrieveTokenAccess(): bad data passed, string type is required for $token, $secret and $verifier.'); + } + + // start retrieval process + $this->setToken(array('oauth_token' => $token, 'oauth_token_secret' => $secret)); + $parameters = array( + 'oauth_verifier' => $verifier + ); + $response = $this->fetch(self::_METHOD_TOKENS, self::_URL_ACCESS, NULL, $parameters); + parse_str($response['linkedin'], $response['linkedin']); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + if($response['info']['http_code'] == 200) { + // tokens retrieved + $this->setToken($response['linkedin']); + + // set the response + $return_data = $response; + $return_data['success'] = TRUE; + } else { + // error getting the request tokens + $this->setToken(NULL); + + // set the response + $return_data = $response; + $return_data['error'] = 'HTTP response from LinkedIn end-point was not code 200'; + $return_data['success'] = FALSE; + } + return $return_data; + } + + /** + * Request token retrieval. + * + * Get the request token from the Linkedin API. + * + * @return arr + * The Linkedin OAuth/http response, in array format. + */ + public function retrieveTokenRequest() { + $parameters = array( + 'oauth_callback' => $this->getCallbackUrl() + ); + $response = $this->fetch(self::_METHOD_TOKENS, self::_URL_REQUEST, NULL, $parameters); + parse_str($response['linkedin'], $response['linkedin']); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + if(($response['info']['http_code'] == 200) && (array_key_exists('oauth_callback_confirmed', $response['linkedin'])) && ($response['linkedin']['oauth_callback_confirmed'] == 'true')) { + // tokens retrieved + $this->setToken($response['linkedin']); + + // set the response + $return_data = $response; + $return_data['success'] = TRUE; + } else { + // error getting the request tokens + $this->setToken(NULL); + + // set the response + $return_data = $response; + if((array_key_exists('oauth_callback_confirmed', $response['linkedin'])) && ($response['linkedin']['oauth_callback_confirmed'] == 'true')) { + $return_data['error'] = 'HTTP response from LinkedIn end-point was not code 200'; + } else { + $return_data['error'] = 'OAuth callback URL was not confirmed by the LinkedIn end-point'; + } + $return_data['success'] = FALSE; + } + return $return_data; + } + + /** + * User authorization revocation. + * + * Revoke the current user's access token, clear the access token's from + * current LinkedIn object. The current documentation for this feature is + * found in a blog entry from April 29th, 2010: + * + * http://developer.linkedin.com/community/apis/blog/2010/04/29/oauth--now-for-authentication + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function revoke() { + // construct and send the request + $response = $this->fetch('GET', self::_URL_REVOKE); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * [DEPRECATED] General people search function. + * + * Takes a string of parameters as input and requests profile data from the + * Linkedin People Search API. See the official documentation for $options + * querystring formatting: + * + * http://developer.linkedin.com/docs/DOC-1191 + * + * @param str $options + * [OPTIONAL] Data retrieval options. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function search($options = NULL) { + return searchPeople($options); + } + + /** + * Company search. + * + * Uses the Company Search API to find companies using keywords, industry, + * location, or some other criteria. It returns a collection of matching + * companies. + * + * http://developer.linkedin.com/docs/DOC-1325 + * + * @param str $options + * [OPTIONAL] Search options. + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function searchCompanies($options = '') { + // check passed data + if(!is_string($options)) { + // bad data passed + throw new LinkedInException('LinkedIn->searchCompanies(): bad data passed, $options must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/company-search' . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Jobs search. + * + * Use the Job Search API to find jobs using keywords, company, location, + * or some other criteria. It returns a collection of matching jobs. Each + * entry can contain much of the information available on the job listing. + * + * http://developer.linkedin.com/docs/DOC-1321 + * + * @param str $options + * [OPTIONAL] Data retrieval options. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function searchJobs($options = '') { + // check passed data + if(!is_string($options)) { + // bad data passed + throw new LinkedInException('LinkedIn->jobsSearch(): bad data passed, $options must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/job-search' . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * General people search function. + * + * Takes a string of parameters as input and requests profile data from the + * Linkedin People Search API. See the official documentation for $options + * querystring formatting: + * + * http://developer.linkedin.com/docs/DOC-1191 + * + * @param str $options + * [OPTIONAL] Data retrieval options. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function searchPeople($options = NULL) { + // check passed data + if(!is_null($options) && !is_string($options)) { + // bad data passed + throw new LinkedInException('LinkedIn->search(): bad data passed, $options must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people-search' . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Set the application_key property. + * + * @param str $key + * The application key. + */ + public function setApplicationKey($key) { + $this->application_key = $key; + } + + /** + * Set the application_secret property. + * + * @param str $secret + * The application secret. + */ + public function setApplicationSecret($secret) { + $this->application_secret = $secret; + } + + /** + * Set the callback property. + * + * @param str $url + * The callback url. + */ + public function setCallbackUrl($url) { + $this->callback = $url; + } + + /** + * This sets the group settings of the specified group. + * + * http://developer.linkedin.com/documents/groups-api + * + * @param str $gid + * The group id. + * @param str $xml + * The group settings to set. The settings are: + * - + * - + * - + * - + * - + * - + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function setGroupSettings($gid, $xml) { + if(!is_string ($gid)) { + throw new LinkedInException('LinkedIn->setGroupSettings(): bad data passed, $token_access should be in array format.'); + } + if(!is_string ($xml)) { + throw new LinkedInException('LinkedIn->setGroupSettings(): bad data passed, $token_access should be in array format.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/group-memberships/' . trim($gid); + $response = $this->fetch('PUT', $query, $xml); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Set the response_format property. + * + * @param str $format + * [OPTIONAL] The response format to specify to LinkedIn. + */ + public function setResponseFormat($format = self::_DEFAULT_RESPONSE_FORMAT) { + $this->response_format = $format; + } + + /** + * Set the token property. + * + * @return arr $token + * The LinkedIn OAuth token. + */ + public function setToken($token) { + // check passed data + if(!is_null($token) && !is_array($token)) { + // bad data passed + throw new LinkedInException('LinkedIn->setToken(): bad data passed, $token_access should be in array format.'); + } + + // set token + $this->token = $token; + } + + /** + * [DEPRECATED] Set the token_access property. + * + * @return arr $token_access + * [OPTIONAL] The LinkedIn OAuth access token. + */ + public function setTokenAccess($token_access) { + $this->setToken($token_access); + } + + /** + * Post a share. + * + * Create a new or reshare another user's shared content. Full details from + * LinkedIn on this functionality can be found here: + * + * http://developer.linkedin.com/docs/DOC-1212 + * + * $action values: ('new', 'reshare') + * $content format: + * $action = 'new'; $content => ('comment' => 'xxx', 'title' => 'xxx', 'submitted-url' => 'xxx', 'submitted-image-url' => 'xxx', 'description' => 'xxx') + * $action = 'reshare'; $content => ('comment' => 'xxx', 'id' => 'xxx') + * + * @param str $action + * The sharing action to perform. + * @param str $content + * The share content. + * @param bool $private + * [OPTIONAL] Should we restrict this shared item to connections only? + * @param bool $twitter + * [OPTIONAL] Also update the teathered Twitter account. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function share($action, $content, $private = TRUE, $twitter = FALSE) { + // check the status itself + if(!empty($action) && !empty($content)) { + /** + * Status is not empty, wrap a cleaned version of it in xml. Status + * rules: + * + * 1) Comments are 700 chars max (if this changes, change _SHARE_COMMENT_LENGTH constant) + * 2) Content/title 200 chars max (if this changes, change _SHARE_CONTENT_TITLE_LENGTH constant) + * 3) Content/description 400 chars max (if this changes, change _SHARE_CONTENT_DESC_LENGTH constant) + * 4a) New shares must contain a comment and/or (content/title and content/submitted-url) + * 4b) Reshared content must contain an attribution id. + * 4c) Reshared content must contain actual content, not just a comment. + * 5) No HTML permitted in comment, content/title, content/description. + */ + + // prepare the share data per the rules above + $share_flag = FALSE; + $content_xml = NULL; + switch($action) { + case 'new': + // share can be an article + if(array_key_exists('title', $content) && array_key_exists('submitted-url', $content)) { + // we have shared content, format it as needed per rules above + $content_title = trim(htmlspecialchars(strip_tags(stripslashes($content['title'])))); + if(strlen($content_title) > self::_SHARE_CONTENT_TITLE_LENGTH) { + throw new LinkedInException('LinkedIn->share(): title length is too long - max length is ' . self::_SHARE_CONTENT_TITLE_LENGTH . ' characters.'); + } + $content_xml .= ' + ' . $content_title . ' + ' . trim(htmlspecialchars($content['submitted-url'])) . ''; + if(array_key_exists('submitted-image-url', $content)) { + $content_xml .= '' . trim(htmlspecialchars($content['submitted-image-url'])) . ''; + } + if(array_key_exists('description', $content)) { + $content_desc = trim(htmlspecialchars(strip_tags(stripslashes($content['description'])))); + if(strlen($content_desc) > self::_SHARE_CONTENT_DESC_LENGTH) { + throw new LinkedInException('LinkedIn->share(): description length is too long - max length is ' . self::_SHARE_CONTENT_DESC_LENGTH . ' characters.'); + } + $content_xml .= '' . $content_desc . ''; + } + $content_xml .= ''; + + $share_flag = TRUE; + } + + // share can be just a comment + if(array_key_exists('comment', $content)) { + // comment located + $comment = htmlspecialchars(trim(strip_tags(stripslashes($content['comment'])))); + if(strlen($comment) > self::_SHARE_COMMENT_LENGTH) { + throw new LinkedInException('LinkedIn->share(): comment length is too long - max length is ' . self::_SHARE_COMMENT_LENGTH . ' characters.'); + } + $content_xml .= '' . $comment . ''; + + $share_flag = TRUE; + } + break; + case 'reshare': + if(array_key_exists('id', $content)) { + // put together the re-share attribution XML + $content_xml .= ' + + ' . trim($content['id']) . ' + + '; + + // optional additional comment + if(array_key_exists('comment', $content)) { + // comment located + $comment = htmlspecialchars(trim(strip_tags(stripslashes($content['comment'])))); + if(strlen($comment) > self::_SHARE_COMMENT_LENGTH) { + throw new LinkedInException('LinkedIn->share(): comment length is too long - max length is ' . self::_SHARE_COMMENT_LENGTH . ' characters.'); + } + $content_xml .= '' . $comment . ''; + } + + $share_flag = TRUE; + } + break; + default: + // bad action passed + throw new LinkedInException('LinkedIn->share(): share action is an invalid value, must be one of: share, reshare.'); + break; + } + + // should we proceed? + if($share_flag) { + // put all of the xml together + $visibility = ($private) ? 'connections-only' : 'anyone'; + $data = ' + + ' . $content_xml . ' + + ' . $visibility . ' + + '; + + // create the proper url + $share_url = self::_URL_API . '/v1/people/~/shares'; + if($twitter) { + // update twitter as well + $share_url .= '?twitter-post=true'; + } + + // send request + $response = $this->fetch('POST', $share_url, $data); + } else { + // data contraints/rules not met, raise an exception + throw new LinkedInException('LinkedIn->share(): sharing data constraints not met; check that you have supplied valid content and combinations of content to share.'); + } + } else { + // data missing, raise an exception + throw new LinkedInException('LinkedIn->share(): sharing action or shared content is missing.'); + } + + /** + * Check for successful request (a 201 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(201, $response); + } + + /** + * Network statistics. + * + * General network statistics retrieval function, returns the number of connections, + * second-connections an authenticated user has. More information here: + * + * http://developer.linkedin.com/docs/DOC-1006 + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function statistics() { + // construct and send the request + $query = self::_URL_API . '/v1/people/~/network/network-stats'; + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Companies you may want to follow. + * + * Returns a list of companies the current user may want to follow, per: + * + * http://developer.linkedin.com/docs/DOC-1324 + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function suggestedCompanies() { + // construct and send the request + $query = self::_URL_API . '/v1/people/~/suggestions/to-follow/companies'; + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Retrieves suggested groups for the user, per: + * + * http://developer.linkedin.com/documents/groups-api + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function suggestedGroups() { + // construct and send the request + $query = self::_URL_API . '/v1/people/~/suggestions/groups:(id,name,is-open-to-non-members)'; + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse (200, $response); + } + + /** + * Jobs you may be interested in. + * + * Returns a list of jobs the current user may be interested in, per: + * + * http://developer.linkedin.com/docs/DOC-1323 + * + * @param str $options + * [OPTIONAL] Data retrieval options. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function suggestedJobs($options = ':(jobs)') { + // check passed data + if(!is_string($options)) { + // bad data passed + throw new LinkedInException('LinkedIn->suggestedJobs(): bad data passed, $options must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/suggestions/job-suggestions' . trim($options); + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Unbookmark a job. + * + * Calling this method causes the current user to remove a bookmark for the + * specified job: + * + * http://developer.linkedin.com/docs/DOC-1323 + * + * @param str $jid + * Job ID you want to unbookmark. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function unbookmarkJob($jid) { + // check passed data + if(!is_string($jid)) { + // bad data passed + throw new LinkedInException('LinkedIn->unbookmarkJob(): bad data passed, $jid must be of type string.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/job-bookmarks/' . trim($jid); + $response = $this->fetch('DELETE', $query); + + /** + * Check for successful request (a 204 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(204, $response); + } + + /** + * Unfollow a company. + * + * Calling this method causes the current user to stop following the specified + * company, per: + * + * http://developer.linkedin.com/docs/DOC-1324 + * + * @param str $cid + * Company ID you want to unfollow. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function unfollowCompany($cid) { + // check passed data + if(!is_string($cid)) { + // bad data passed + throw new LinkedInException('LinkedIn->unfollowCompany(): bad data passed, $cid must be of string value.'); + } + + // construct and send the request + $query = self::_URL_API . '/v1/people/~/following/companies/id=' . trim($cid); + $response = $this->fetch('DELETE', $query); + + /** + * Check for successful request (a 204 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(204, $response); + } + + /** + * Unlike a network update. + * + * Unlike another user's network update: + * + * http://developer.linkedin.com/docs/DOC-1043 + * + * @param str $uid + * The LinkedIn update ID. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function unlike($uid) { + // check passed data + if(!is_string($uid)) { + // bad data passed + throw new LinkedInException('LinkedIn->unlike(): bad data passed, $uid must be of type string.'); + } + + // construct the xml data + $data = ' + false'; + + // send request + $query = self::_URL_API . '/v1/people/~/network/updates/key=' . $uid . '/is-liked'; + $response = $this->fetch('PUT', $query, $data); + + /** + * Check for successful request (a 201 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(201, $response); + } + + /** + * Post network update. + * + * Update the user's Linkedin network status. Full details from LinkedIn + * on this functionality can be found here: + * + * http://developer.linkedin.com/docs/DOC-1009 + * http://developer.linkedin.com/docs/DOC-1009#comment-1077 + * + * @param str $update + * The network update. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function updateNetwork($update) { + // check passed data + if(!is_string($update)) { + // nothing/non-string passed, raise an exception + throw new LinkedInException('LinkedIn->updateNetwork(): bad data passed, $update must be a non-zero length string.'); + } + + /** + * Network update is not empty, wrap a cleaned version of it in xml. + * Network update rules: + * + * 1) No HTML permitted except those found in _NETWORK_HTML constant + * 2) Update cannot be longer than 140 characters. + */ + // get the user data + $response = self::profile('~:(first-name,last-name,site-standard-profile-request)'); + if($response['success'] === TRUE) { + /** + * We are converting response to usable data. I'd use SimpleXML here, but + * to keep the class self-contained, we will use a portable XML parsing + * routine, self::xmlToArray. + */ + $person = self::xmlToArray($response['linkedin']); + if($person === FALSE) { + // bad xml data + throw new LinkedInException('LinkedIn->updateNetwork(): LinkedIn returned bad XML data.'); + } + $fields = $person['person']['children']; + + // prepare user data + $first_name = trim($fields['first-name']['content']); + $last_name = trim($fields['last-name']['content']); + $profile_url = trim($fields['site-standard-profile-request']['children']['url']['content']); + + // create the network update + $update = trim(htmlspecialchars(strip_tags($update, self::_NETWORK_HTML))); + if(strlen($update) > self::_NETWORK_LENGTH) { + throw new LinkedInException('LinkedIn->share(): update length is too long - max length is ' . self::_NETWORK_LENGTH . ' characters.'); + } + $user = htmlspecialchars('' . $first_name . ' ' . $last_name . ''); + $data = ' + linkedin-html + ' . $user . ' ' . $update . ' + '; + + // send request + $query = self::_URL_API . '/v1/people/~/person-activities'; + $response = $this->fetch('POST', $query, $data); + + /** + * Check for successful request (a 201 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(201, $response); + } else { + // profile retrieval failed + throw new LinkedInException('LinkedIn->updateNetwork(): profile data could not be retrieved.'); + } + } + + /** + * General network update retrieval function. + * + * Takes a string of parameters as input and requests update-related data + * from the Linkedin Network Updates API. See the official documentation for + * $options parameter formatting: + * + * http://developer.linkedin.com/docs/DOC-1006 + * + * For getting more comments, likes, etc, see here: + * + * http://developer.linkedin.com/docs/DOC-1043 + * + * @param str $options + * [OPTIONAL] Data retrieval options. + * @param str $id + * [OPTIONAL] The LinkedIn ID to restrict the updates for. + * + * @return arr + * Array containing retrieval success, LinkedIn response. + */ + public function updates($options = NULL, $id = NULL) { + // check passed data + if(!is_null($options) && !is_string($options)) { + // bad data passed + throw new LinkedInException('LinkedIn->updates(): bad data passed, $options must be of type string.'); + } + if(!is_null($id) && !is_string($id)) { + // bad data passed + throw new LinkedInException('LinkedIn->updates(): bad data passed, $id must be of type string.'); + } + + // construct and send the request + if(!is_null($id) && self::isId($id)) { + $query = self::_URL_API . '/v1/people/' . $id . '/network/updates' . trim($options); + } else { + $query = self::_URL_API . '/v1/people/~/network/updates' . trim($options); + } + $response = $this->fetch('GET', $query); + + /** + * Check for successful request (a 200 response from LinkedIn server) + * per the documentation linked in method comments above. + */ + return $this->checkResponse(200, $response); + } + + /** + * Converts passed XML data to an array. + * + * @param str $xml + * The XML to convert to an array. + * + * @return arr + * Array containing the XML data. + * @return bool + * FALSE if passed data cannot be parsed to an array. + */ + public static function xmlToArray($xml) { + // check passed data + if(!is_string($xml)) { + // bad data possed + throw new LinkedInException('LinkedIn->xmlToArray(): bad data passed, $xml must be a non-zero length string.'); + } + + $parser = xml_parser_create(); + xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); + xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1); + if(xml_parse_into_struct($parser, $xml, $tags)) { + $elements = array(); + $stack = array(); + foreach($tags as $tag) { + $index = count($elements); + if($tag['type'] == 'complete' || $tag['type'] == 'open') { + $elements[$tag['tag']] = array(); + $elements[$tag['tag']]['attributes'] = (array_key_exists('attributes', $tag)) ? $tag['attributes'] : NULL; + $elements[$tag['tag']]['content'] = (array_key_exists('value', $tag)) ? $tag['value'] : NULL; + if($tag['type'] == 'open') { + $elements[$tag['tag']]['children'] = array(); + $stack[count($stack)] = &$elements; + $elements = &$elements[$tag['tag']]['children']; + } + } + if($tag['type'] == 'close') { + $elements = &$stack[count($stack) - 1]; + unset($stack[count($stack) - 1]); + } + } + $return_data = $elements; + } else { + // not valid xml data + $return_data = FALSE; + } + xml_parser_free($parser); + return $return_data; + } +} + +?> diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/OAuth/OAuth.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/OAuth/OAuth.php new file mode 100644 index 0000000..e712c47 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/OAuth/OAuth.php @@ -0,0 +1,896 @@ +key = $key; + $this->secret = $secret; + $this->callback_url = $callback_url; + } + + function __toString() { + return "OAuthConsumer[key=$this->key,secret=$this->secret]"; + } +} + +class OAuthToken { + // access tokens and request tokens + public $key; + public $secret; + + /** + * key = the token + * secret = the token secret + */ + function __construct($key, $secret) { + $this->key = $key; + $this->secret = $secret; + } + + /** + * generates the basic string serialization of a token that a server + * would respond to request_token and access_token calls with + */ + function to_string() { + return "oauth_token=" . + OAuthUtil::urlencode_rfc3986($this->key) . + "&oauth_token_secret=" . + OAuthUtil::urlencode_rfc3986($this->secret); + } + + function __toString() { + return $this->to_string(); + } +} + +/** + * A class for implementing a Signature Method + * See section 9 ("Signing Requests") in the spec + */ +abstract class OAuthSignatureMethod { + /** + * Needs to return the name of the Signature Method (ie HMAC-SHA1) + * @return string + */ + abstract public function get_name(); + + /** + * Build up the signature + * NOTE: The output of this function MUST NOT be urlencoded. + * the encoding is handled in OAuthRequest when the final + * request is serialized + * @param OAuthRequest $request + * @param OAuthConsumer $consumer + * @param OAuthToken $token + * @return string + */ + abstract public function build_signature($request, $consumer, $token); + + /** + * Verifies that a given signature is correct + * @param OAuthRequest $request + * @param OAuthConsumer $consumer + * @param OAuthToken $token + * @param string $signature + * @return bool + */ + public function check_signature($request, $consumer, $token, $signature) { + $built = $this->build_signature($request, $consumer, $token); + + // Check for zero length, although unlikely here + if (strlen($built) == 0 || strlen($signature) == 0) { + return false; + } + + if (strlen($built) != strlen($signature)) { + return false; + } + + // Avoid a timing leak with a (hopefully) time insensitive compare + $result = 0; + for ($i = 0; $i < strlen($signature); $i++) { + $result |= ord($built{$i}) ^ ord($signature{$i}); + } + + return $result == 0; + } +} + +/** + * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104] + * where the Signature Base String is the text and the key is the concatenated values (each first + * encoded per Parameter Encoding) of the Consumer Secret and Token Secret, separated by an '&' + * character (ASCII code 38) even if empty. + * - Chapter 9.2 ("HMAC-SHA1") + */ +class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod { + function get_name() { + return "HMAC-SHA1"; + } + + public function build_signature($request, $consumer, $token) { + $base_string = $request->get_signature_base_string(); + $request->base_string = $base_string; + + $key_parts = array( + $consumer->secret, + ($token) ? $token->secret : "" + ); + + $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); + $key = implode('&', $key_parts); + + return base64_encode(hash_hmac('sha1', $base_string, $key, true)); + } +} + +/** + * The PLAINTEXT method does not provide any security protection and SHOULD only be used + * over a secure channel such as HTTPS. It does not use the Signature Base String. + * - Chapter 9.4 ("PLAINTEXT") + */ +class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod { + public function get_name() { + return "PLAINTEXT"; + } + + /** + * oauth_signature is set to the concatenated encoded values of the Consumer Secret and + * Token Secret, separated by a '&' character (ASCII code 38), even if either secret is + * empty. The result MUST be encoded again. + * - Chapter 9.4.1 ("Generating Signatures") + * + * Please note that the second encoding MUST NOT happen in the SignatureMethod, as + * OAuthRequest handles this! + */ + public function build_signature($request, $consumer, $token) { + $key_parts = array( + $consumer->secret, + ($token) ? $token->secret : "" + ); + + $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); + $key = implode('&', $key_parts); + $request->base_string = $key; + + return $key; + } +} + +/** + * The RSA-SHA1 signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in + * [RFC3447] section 8.2 (more simply known as PKCS#1), using SHA-1 as the hash function for + * EMSA-PKCS1-v1_5. It is assumed that the Consumer has provided its RSA public key in a + * verified way to the Service Provider, in a manner which is beyond the scope of this + * specification. + * - Chapter 9.3 ("RSA-SHA1") + */ +abstract class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod { + public function get_name() { + return "RSA-SHA1"; + } + + // Up to the SP to implement this lookup of keys. Possible ideas are: + // (1) do a lookup in a table of trusted certs keyed off of consumer + // (2) fetch via http using a url provided by the requester + // (3) some sort of specific discovery code based on request + // + // Either way should return a string representation of the certificate + protected abstract function fetch_public_cert(&$request); + + // Up to the SP to implement this lookup of keys. Possible ideas are: + // (1) do a lookup in a table of trusted certs keyed off of consumer + // + // Either way should return a string representation of the certificate + protected abstract function fetch_private_cert(&$request); + + public function build_signature($request, $consumer, $token) { + $base_string = $request->get_signature_base_string(); + $request->base_string = $base_string; + + // Fetch the private key cert based on the request + $cert = $this->fetch_private_cert($request); + + // Pull the private key ID from the certificate + $privatekeyid = openssl_get_privatekey($cert); + + // Sign using the key + $ok = openssl_sign($base_string, $signature, $privatekeyid); + + // Release the key resource + openssl_free_key($privatekeyid); + + return base64_encode($signature); + } + + public function check_signature($request, $consumer, $token, $signature) { + $decoded_sig = base64_decode($signature); + + $base_string = $request->get_signature_base_string(); + + // Fetch the public key cert based on the request + $cert = $this->fetch_public_cert($request); + + // Pull the public key ID from the certificate + $publickeyid = openssl_get_publickey($cert); + + // Check the computed signature against the one passed in the query + $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); + + // Release the key resource + openssl_free_key($publickeyid); + + return $ok == 1; + } +} + +class OAuthRequest { + protected $parameters; + protected $http_method; + protected $http_url; + // for debug purposes + public $base_string; + public static $version = '1.0'; + public static $POST_INPUT = 'php://input'; + + function __construct($http_method, $http_url, $parameters=NULL) { + $parameters = ($parameters) ? $parameters : array(); + $parameters = array_merge( OAuthUtil::parse_parameters(parse_url($http_url, PHP_URL_QUERY)), $parameters); + $this->parameters = $parameters; + $this->http_method = $http_method; + $this->http_url = $http_url; + } + + + /** + * attempt to build up a request from what was passed to the server + */ + public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) { + $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") + ? 'http' + : 'https'; + $http_url = ($http_url) ? $http_url : $scheme . + '://' . $_SERVER['SERVER_NAME'] . + ':' . + $_SERVER['SERVER_PORT'] . + $_SERVER['REQUEST_URI']; + $http_method = ($http_method) ? $http_method : $_SERVER['REQUEST_METHOD']; + + // We weren't handed any parameters, so let's find the ones relevant to + // this request. + // If you run XML-RPC or similar you should use this to provide your own + // parsed parameter-list + if (!$parameters) { + // Find request headers + $request_headers = OAuthUtil::get_headers(); + + // Parse the query-string to find GET parameters + $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']); + + // It's a POST request of the proper content-type, so parse POST + // parameters and add those overriding any duplicates from GET + if ($http_method == "POST" + && isset($request_headers['Content-Type']) + && strstr($request_headers['Content-Type'], + 'application/x-www-form-urlencoded') + ) { + $post_data = OAuthUtil::parse_parameters( + file_get_contents(self::$POST_INPUT) + ); + $parameters = array_merge($parameters, $post_data); + } + + // We have a Authorization-header with OAuth data. Parse the header + // and add those overriding any duplicates from GET or POST + if (isset($request_headers['Authorization']) && substr($request_headers['Authorization'], 0, 6) == 'OAuth ') { + $header_parameters = OAuthUtil::split_header( + $request_headers['Authorization'] + ); + $parameters = array_merge($parameters, $header_parameters); + } + + } + + return new OAuthRequest($http_method, $http_url, $parameters); + } + + /** + * pretty much a helper function to set up the request + */ + public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) { + $parameters = ($parameters) ? $parameters : array(); + $defaults = array("oauth_version" => OAuthRequest::$version, + "oauth_nonce" => OAuthRequest::generate_nonce(), + "oauth_timestamp" => OAuthRequest::generate_timestamp(), + "oauth_consumer_key" => $consumer->key); + if ($token) + $defaults['oauth_token'] = $token->key; + + $parameters = array_merge($defaults, $parameters); + + return new OAuthRequest($http_method, $http_url, $parameters); + } + + public function set_parameter($name, $value, $allow_duplicates = true) { + if ($allow_duplicates && isset($this->parameters[$name])) { + // We have already added parameter(s) with this name, so add to the list + if (is_scalar($this->parameters[$name])) { + // This is the first duplicate, so transform scalar (string) + // into an array so we can add the duplicates + $this->parameters[$name] = array($this->parameters[$name]); + } + + $this->parameters[$name][] = $value; + } else { + $this->parameters[$name] = $value; + } + } + + public function get_parameter($name) { + return isset($this->parameters[$name]) ? $this->parameters[$name] : null; + } + + public function get_parameters() { + return $this->parameters; + } + + public function unset_parameter($name) { + unset($this->parameters[$name]); + } + + /** + * The request parameters, sorted and concatenated into a normalized string. + * @return string + */ + public function get_signable_parameters() { + // Grab all parameters + $params = $this->parameters; + + // Remove oauth_signature if present + // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") + if (isset($params['oauth_signature'])) { + unset($params['oauth_signature']); + } + + return OAuthUtil::build_http_query($params); + } + + /** + * Returns the base string of this request + * + * The base string defined as the method, the url + * and the parameters (normalized), each urlencoded + * and the concated with &. + */ + public function get_signature_base_string() { + $parts = array( + $this->get_normalized_http_method(), + $this->get_normalized_http_url(), + $this->get_signable_parameters() + ); + + $parts = OAuthUtil::urlencode_rfc3986($parts); + + return implode('&', $parts); + } + + /** + * just uppercases the http method + */ + public function get_normalized_http_method() { + return strtoupper($this->http_method); + } + + /** + * parses the url and rebuilds it to be + * scheme://host/path + */ + public function get_normalized_http_url() { + $parts = parse_url($this->http_url); + + $scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http'; + $port = (isset($parts['port'])) ? $parts['port'] : (($scheme == 'https') ? '443' : '80'); + $host = (isset($parts['host'])) ? strtolower($parts['host']) : ''; + $path = (isset($parts['path'])) ? $parts['path'] : ''; + + if (($scheme == 'https' && $port != '443') + || ($scheme == 'http' && $port != '80')) { + $host = "$host:$port"; + } + return "$scheme://$host$path"; + } + + /** + * builds a url usable for a GET request + */ + public function to_url() { + $post_data = $this->to_postdata(); + $out = $this->get_normalized_http_url(); + if ($post_data) { + $out .= '?'.$post_data; + } + return $out; + } + + /** + * builds the data one would send in a POST request + */ + public function to_postdata() { + return OAuthUtil::build_http_query($this->parameters); + } + + /** + * builds the Authorization: header + */ + public function to_header($realm=null) { + $first = true; + if($realm) { + $out = 'Authorization: OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"'; + $first = false; + } else + $out = 'Authorization: OAuth'; + + $total = array(); + foreach ($this->parameters as $k => $v) { + if (substr($k, 0, 5) != "oauth") continue; + if (is_array($v)) { + throw new OAuthException('Arrays not supported in headers'); + } + $out .= ($first) ? ' ' : ','; + $out .= OAuthUtil::urlencode_rfc3986($k) . + '="' . + OAuthUtil::urlencode_rfc3986($v) . + '"'; + $first = false; + } + return $out; + } + + public function __toString() { + return $this->to_url(); + } + + + public function sign_request($signature_method, $consumer, $token) { + $this->set_parameter( + "oauth_signature_method", + $signature_method->get_name(), + false + ); + $signature = $this->build_signature($signature_method, $consumer, $token); + $this->set_parameter("oauth_signature", $signature, false); + } + + public function build_signature($signature_method, $consumer, $token) { + $signature = $signature_method->build_signature($this, $consumer, $token); + return $signature; + } + + /** + * util function: current timestamp + */ + private static function generate_timestamp() { + return time(); + } + + /** + * util function: current nonce + */ + private static function generate_nonce() { + $mt = microtime(); + $rand = mt_rand(); + + return md5($mt . $rand); // md5s look nicer than numbers + } +} + +class OAuthServer { + protected $timestamp_threshold = 300; // in seconds, five minutes + protected $version = '1.0'; // hi blaine + protected $signature_methods = array(); + + protected $data_store; + + function __construct($data_store) { + $this->data_store = $data_store; + } + + public function add_signature_method($signature_method) { + $this->signature_methods[$signature_method->get_name()] = + $signature_method; + } + + // high level functions + + /** + * process a request_token request + * returns the request token on success + */ + public function fetch_request_token(&$request) { + $this->get_version($request); + + $consumer = $this->get_consumer($request); + + // no token required for the initial token request + $token = NULL; + + $this->check_signature($request, $consumer, $token); + + // Rev A change + $callback = $request->get_parameter('oauth_callback'); + $new_token = $this->data_store->new_request_token($consumer, $callback); + + return $new_token; + } + + /** + * process an access_token request + * returns the access token on success + */ + public function fetch_access_token(&$request) { + $this->get_version($request); + + $consumer = $this->get_consumer($request); + + // requires authorized request token + $token = $this->get_token($request, $consumer, "request"); + + $this->check_signature($request, $consumer, $token); + + // Rev A change + $verifier = $request->get_parameter('oauth_verifier'); + $new_token = $this->data_store->new_access_token($token, $consumer, $verifier); + + return $new_token; + } + + /** + * verify an api call, checks all the parameters + */ + public function verify_request(&$request) { + $this->get_version($request); + $consumer = $this->get_consumer($request); + $token = $this->get_token($request, $consumer, "access"); + $this->check_signature($request, $consumer, $token); + return array($consumer, $token); + } + + // Internals from here + /** + * version 1 + */ + private function get_version(&$request) { + $version = $request->get_parameter("oauth_version"); + if (!$version) { + // Service Providers MUST assume the protocol version to be 1.0 if this parameter is not present. + // Chapter 7.0 ("Accessing Protected Ressources") + $version = '1.0'; + } + if ($version !== $this->version) { + throw new OAuthException("OAuth version '$version' not supported"); + } + return $version; + } + + /** + * figure out the signature with some defaults + */ + private function get_signature_method($request) { + $signature_method = $request instanceof OAuthRequest + ? $request->get_parameter("oauth_signature_method") + : NULL; + + if (!$signature_method) { + // According to chapter 7 ("Accessing Protected Ressources") the signature-method + // parameter is required, and we can't just fallback to PLAINTEXT + throw new OAuthException('No signature method parameter. This parameter is required'); + } + + if (!in_array($signature_method, + array_keys($this->signature_methods))) { + throw new OAuthException( + "Signature method '$signature_method' not supported " . + "try one of the following: " . + implode(", ", array_keys($this->signature_methods)) + ); + } + return $this->signature_methods[$signature_method]; + } + + /** + * try to find the consumer for the provided request's consumer key + */ + private function get_consumer($request) { + $consumer_key = $request instanceof OAuthRequest + ? $request->get_parameter("oauth_consumer_key") + : NULL; + + if (!$consumer_key) { + throw new OAuthException("Invalid consumer key"); + } + + $consumer = $this->data_store->lookup_consumer($consumer_key); + if (!$consumer) { + throw new OAuthException("Invalid consumer"); + } + + return $consumer; + } + + /** + * try to find the token for the provided request's token key + */ + private function get_token($request, $consumer, $token_type="access") { + $token_field = $request instanceof OAuthRequest + ? $request->get_parameter('oauth_token') + : NULL; + + $token = $this->data_store->lookup_token( + $consumer, $token_type, $token_field + ); + if (!$token) { + throw new OAuthException("Invalid $token_type token: $token_field"); + } + return $token; + } + + /** + * all-in-one function to check the signature on a request + * should guess the signature method appropriately + */ + private function check_signature($request, $consumer, $token) { + // this should probably be in a different method + $timestamp = $request instanceof OAuthRequest + ? $request->get_parameter('oauth_timestamp') + : NULL; + $nonce = $request instanceof OAuthRequest + ? $request->get_parameter('oauth_nonce') + : NULL; + + $this->check_timestamp($timestamp); + $this->check_nonce($consumer, $token, $nonce, $timestamp); + + $signature_method = $this->get_signature_method($request); + + $signature = $request->get_parameter('oauth_signature'); + $valid_sig = $signature_method->check_signature( + $request, + $consumer, + $token, + $signature + ); + + if (!$valid_sig) { + throw new OAuthException("Invalid signature"); + } + } + + /** + * check that the timestamp is new enough + */ + private function check_timestamp($timestamp) { + if( ! $timestamp ) + throw new OAuthException( + 'Missing timestamp parameter. The parameter is required' + ); + + // verify that timestamp is recentish + $now = time(); + if (abs($now - $timestamp) > $this->timestamp_threshold) { + throw new OAuthException( + "Expired timestamp, yours $timestamp, ours $now" + ); + } + } + + /** + * check that the nonce is not repeated + */ + private function check_nonce($consumer, $token, $nonce, $timestamp) { + if( ! $nonce ) + throw new OAuthException( + 'Missing nonce parameter. The parameter is required' + ); + + // verify that the nonce is uniqueish + $found = $this->data_store->lookup_nonce( + $consumer, + $token, + $nonce, + $timestamp + ); + if ($found) { + throw new OAuthException("Nonce already used: $nonce"); + } + } + +} + +class OAuthDataStore { + function lookup_consumer($consumer_key) { + // implement me + } + + function lookup_token($consumer, $token_type, $token) { + // implement me + } + + function lookup_nonce($consumer, $token, $nonce, $timestamp) { + // implement me + } + + function new_request_token($consumer, $callback = null) { + // return a new token attached to this consumer + } + + function new_access_token($token, $consumer, $verifier = null) { + // return a new access token attached to this consumer + // for the user associated with this token if the request token + // is authorized + // should also invalidate the request token + } + +} + +class OAuthUtil { + public static function urlencode_rfc3986($input) { + if (is_array($input)) { + return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input); + } else if (is_scalar($input)) { + return str_replace( + '+', + ' ', + str_replace('%7E', '~', rawurlencode($input)) + ); + } else { + return ''; + } +} + + + // This decode function isn't taking into consideration the above + // modifications to the encoding process. However, this method doesn't + // seem to be used anywhere so leaving it as is. + public static function urldecode_rfc3986($string) { + return urldecode($string); + } + + // Utility function for turning the Authorization: header into + // parameters, has to do some unescaping + // Can filter out any non-oauth parameters if needed (default behaviour) + // May 28th, 2010 - method updated to tjerk.meesters for a speed improvement. + // see http://code.google.com/p/oauth/issues/detail?id=163 + public static function split_header($header, $only_allow_oauth_parameters = true) { + $params = array(); + if (preg_match_all('/('.($only_allow_oauth_parameters ? 'oauth_' : '').'[a-z_-]*)=(:?"([^"]*)"|([^,]*))/', $header, $matches)) { + foreach ($matches[1] as $i => $h) { + $params[$h] = OAuthUtil::urldecode_rfc3986(empty($matches[3][$i]) ? $matches[4][$i] : $matches[3][$i]); + } + if (isset($params['realm'])) { + unset($params['realm']); + } + } + return $params; + } + + // helper to try to sort out headers for people who aren't running apache + public static function get_headers() { + if (function_exists('apache_request_headers')) { + // we need this to get the actual Authorization: header + // because apache tends to tell us it doesn't exist + $headers = apache_request_headers(); + + // sanitize the output of apache_request_headers because + // we always want the keys to be Cased-Like-This and arh() + // returns the headers in the same case as they are in the + // request + $out = array(); + foreach ($headers AS $key => $value) { + $key = str_replace( + " ", + "-", + ucwords(strtolower(str_replace("-", " ", $key))) + ); + $out[$key] = $value; + } + } else { + // otherwise we don't have apache and are just going to have to hope + // that $_SERVER actually contains what we need + $out = array(); + if( isset($_SERVER['CONTENT_TYPE']) ) + $out['Content-Type'] = $_SERVER['CONTENT_TYPE']; + if( isset($_ENV['CONTENT_TYPE']) ) + $out['Content-Type'] = $_ENV['CONTENT_TYPE']; + + foreach ($_SERVER as $key => $value) { + if (substr($key, 0, 5) == "HTTP_") { + // this is chaos, basically it is just there to capitalize the first + // letter of every word that is not an initial HTTP and strip HTTP + // code from przemek + $key = str_replace( + " ", + "-", + ucwords(strtolower(str_replace("_", " ", substr($key, 5)))) + ); + $out[$key] = $value; + } + } + } + return $out; + } + + // This function takes a input like a=b&a=c&d=e and returns the parsed + // parameters like this + // array('a' => array('b','c'), 'd' => 'e') + public static function parse_parameters( $input ) { + if (!isset($input) || !$input) return array(); + + $pairs = explode('&', $input); + + $parsed_parameters = array(); + foreach ($pairs as $pair) { + $split = explode('=', $pair, 2); + $parameter = OAuthUtil::urldecode_rfc3986($split[0]); + $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : ''; + + if (isset($parsed_parameters[$parameter])) { + // We have already recieved parameter(s) with this name, so add to the list + // of parameters with this name + + if (is_scalar($parsed_parameters[$parameter])) { + // This is the first duplicate, so transform scalar (string) into an array + // so we can add the duplicates + $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]); + } + + $parsed_parameters[$parameter][] = $value; + } else { + $parsed_parameters[$parameter] = $value; + } + } + return $parsed_parameters; + } + + public static function build_http_query($params) { + if (!$params) return ''; + + // Urlencode both keys and values + $keys = OAuthUtil::urlencode_rfc3986(array_keys($params)); + $values = OAuthUtil::urlencode_rfc3986(array_values($params)); + $params = array_combine($keys, $values); + + // Parameters are sorted by name, using lexicographical byte value ordering. + // Ref: Spec: 9.1.1 (1) + uksort($params, 'strcmp'); + + $pairs = array(); + foreach ($params as $parameter => $value) { + if (is_array($value)) { + // If two or more parameters share the same name, they are sorted by their value + // Ref: Spec: 9.1.1 (1) + // June 12th, 2010 - changed to sort because of issue 164 by hidetaka + sort($value, SORT_STRING); + foreach ($value as $duplicate_value) { + $pairs[] = $parameter . '=' . $duplicate_value; + } + } else { + $pairs[] = $parameter . '=' . $value; + } + } + // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61) + // Each name-value pair is separated by an '&' character (ASCII code 38) + return implode('&', $pairs); + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/OAuth/OAuth1Client.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/OAuth/OAuth1Client.php new file mode 100644 index 0000000..34bad87 --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/OAuth/OAuth1Client.php @@ -0,0 +1,229 @@ +sha1_method = new OAuthSignatureMethod_HMAC_SHA1(); + $this->consumer = new OAuthConsumer( $consumer_key, $consumer_secret ); + $this->token = null; + + if ( $oauth_token && $oauth_token_secret ){ + $this->token = new OAuthConsumer( $oauth_token, $oauth_token_secret ); + } + } + + /** + * Build authorize url + * + * @return string + */ + function authorizeUrl( $token, $extras =array() ) + { + if ( is_array( $token ) ){ + $token = $token['oauth_token']; + } + + $parameters = array( "oauth_token" => $token ); + + if( count($extras) ) + foreach( $extras as $k=>$v ) + $parameters[$k] = $v; + + return $this->authorize_url . "?" . http_build_query( $parameters ); + } + + /** + * Get a request_token from provider + * + * @return array a key/value array containing oauth_token and oauth_token_secret + */ + function requestToken( $callback = null ) + { + $parameters = array(); + + if ( $callback ) { + $this->redirect_uri = $parameters['oauth_callback'] = $callback; + } + + $request = $this->signedRequest( $this->request_token_url, $this->request_token_method, $parameters ); + $token = OAuthUtil::parse_parameters( $request ); + $this->token = new OAuthConsumer( $token['oauth_token'], $token['oauth_token_secret'] ); + + return $token; + } + + /** + * Exchange the request token and secret for an access token and secret, to sign API calls. + * + * @return array array('oauth_token' => the access token, 'oauth_token_secret' => the access secret) + */ + function accessToken( $oauth_verifier = false, $oauth_token = false ) + { + $parameters = array(); + + // 1.0a + if ( $oauth_verifier ) { + $parameters['oauth_verifier'] = $oauth_verifier; + } + + $request = $this->signedRequest( $this->access_token_url, $this->access_token_method, $parameters ); + $token = OAuthUtil::parse_parameters( $request ); + $this->token = new OAuthConsumer( $token['oauth_token'], $token['oauth_token_secret'] ); + + return $token; + } + + /** + * GET wrappwer for provider apis request + */ + function get($url, $parameters = array()) + { + return $this->api($url, 'GET', $parameters); + } + + /** + * POST wreapper for provider apis request + */ + function post($url, $parameters = array()) + { + return $this->api($url, 'POST', $parameters); + } + + /** + * Format and sign an oauth for provider api + */ + function api( $url, $method = 'GET', $parameters = array() ) + { + if ( strrpos($url, 'http://') !== 0 && strrpos($url, 'https://') !== 0 ) { + $url = $this->api_base_url . $url; + } + + $response = $this->signedRequest( $url, $method, $parameters ); + + if( $this->decode_json ){ + $response = json_decode( $response ); + } + + return $response; + } + + /** + * Make signed request + */ + function signedRequest( $url, $method, $parameters ) + { + $request = OAuthRequest::from_consumer_and_token($this->consumer, $this->token, $method, $url, $parameters); + $request->sign_request($this->sha1_method, $this->consumer, $this->token); + switch ($method) { + case 'GET': return $this->request( $request->to_url(), 'GET' ); + default : return $this->request( $request->get_normalized_http_url(), $method, $request->to_postdata(), $request->to_header() ) ; + } + } + + /** + * Make http request + */ + function request( $url, $method, $postfields = NULL, $auth_header = null ) + { + Hybrid_Logger::info( "Enter OAuth1Client::request( $method, $url )" ); + Hybrid_Logger::debug( "OAuth1Client::request(). dump post fields: ", serialize( $postfields ) ); + + $this->http_info = array(); + $ci = curl_init(); + + /* Curl settings */ + curl_setopt( $ci, CURLOPT_USERAGENT , $this->curl_useragent ); + curl_setopt( $ci, CURLOPT_CONNECTTIMEOUT, $this->curl_connect_time_out ); + curl_setopt( $ci, CURLOPT_TIMEOUT , $this->curl_time_out ); + curl_setopt( $ci, CURLOPT_RETURNTRANSFER, TRUE ); + curl_setopt( $ci, CURLOPT_HTTPHEADER , array('Expect:') ); + curl_setopt( $ci, CURLOPT_SSL_VERIFYPEER, $this->curl_ssl_verifypeer ); + curl_setopt( $ci, CURLOPT_HEADERFUNCTION, array($this, 'getHeader') ); + curl_setopt( $ci, CURLOPT_HEADER , FALSE ); + + if($this->curl_proxy){ + curl_setopt( $ci, CURLOPT_PROXY , $this->curl_proxy); + } + + switch ($method){ + case 'POST': + curl_setopt( $ci, CURLOPT_POST, TRUE ); + + if ( !empty($postfields) ){ + curl_setopt( $ci, CURLOPT_POSTFIELDS, $postfields ); + } + + if ( !empty($auth_header) && $this->curl_auth_header ){ + curl_setopt( $ci, CURLOPT_HTTPHEADER, array( 'Content-Type: application/atom+xml', $auth_header ) ); + } + break; + case 'DELETE': + curl_setopt( $ci, CURLOPT_CUSTOMREQUEST, 'DELETE' ); + if ( !empty($postfields) ){ + $url = "{$url}?{$postfields}"; + } + } + + curl_setopt($ci, CURLOPT_URL, $url); + $response = curl_exec($ci); + + Hybrid_Logger::debug( "OAuth1Client::request(). dump request info: ", serialize( curl_getinfo($ci) ) ); + Hybrid_Logger::debug( "OAuth1Client::request(). dump request result: ", serialize( $response ) ); + + $this->http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE); + $this->http_info = array_merge($this->http_info, curl_getinfo($ci)); + + curl_close ($ci); + + return $response; + } + + /** + * Get the header info to store. + */ + function getHeader($ch, $header) { + $i = strpos($header, ':'); + + if ( !empty($i) ){ + $key = str_replace('-', '_', strtolower(substr($header, 0, $i))); + $value = trim(substr($header, $i + 2)); + $this->http_header[$key] = $value; + } + + return strlen($header); + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/OAuth/OAuth2Client.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/OAuth/OAuth2Client.php new file mode 100644 index 0000000..1642faf --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/OAuth/OAuth2Client.php @@ -0,0 +1,243 @@ +client_id = $client_id; + $this->client_secret = $client_secret; + $this->redirect_uri = $redirect_uri; + } + + public function authorizeUrl( $extras = array() ) + { + $params = array( + "client_id" => $this->client_id, + "redirect_uri" => $this->redirect_uri, + "response_type" => "code" + ); + + if( count($extras) ) + foreach( $extras as $k=>$v ) + $params[$k] = $v; + + return $this->authorize_url . "?" . http_build_query( $params ); + } + + public function authenticate( $code ) + { + $params = array( + "client_id" => $this->client_id, + "client_secret" => $this->client_secret, + "grant_type" => "authorization_code", + "redirect_uri" => $this->redirect_uri, + "code" => $code + ); + + $response = $this->request( $this->token_url, $params, $this->curl_authenticate_method ); + + $response = $this->parseRequestResult( $response ); + + if( ! $response || ! isset( $response->access_token ) ){ + throw new Exception( "The Authorization Service has return: " . $response->error ); + } + + if( isset( $response->access_token ) ) $this->access_token = $response->access_token; + if( isset( $response->refresh_token ) ) $this->refresh_token = $response->refresh_token; + if( isset( $response->expires_in ) ) $this->access_token_expires_in = $response->expires_in; + + // calculate when the access token expire + $this->access_token_expires_at = time() + $response->expires_in; + + return $response; + } + + public function authenticated() + { + if ( $this->access_token ){ + if ( $this->token_info_url && $this->refresh_token ){ + // check if this access token has expired, + $tokeninfo = $this->tokenInfo( $this->access_token ); + + // if yes, access_token has expired, then ask for a new one + if( $tokeninfo && isset( $tokeninfo->error ) ){ + $response = $this->refreshToken( $this->refresh_token ); + + // if wrong response + if( ! isset( $response->access_token ) || ! $response->access_token ){ + throw new Exception( "The Authorization Service has return an invalid response while requesting a new access token. given up!" ); + } + + // set new access_token + $this->access_token = $response->access_token; + } + } + + return true; + } + + return false; + } + + /** + * Format and sign an oauth for provider api + */ + public function api( $url, $method = "GET", $parameters = array() ) + { + if ( strrpos($url, 'http://') !== 0 && strrpos($url, 'https://') !== 0 ) { + $url = $this->api_base_url . $url; + } + + $parameters[$this->sign_token_name] = $this->access_token; + $response = null; + + switch( $method ){ + case 'GET' : $response = $this->request( $url, $parameters, "GET" ); break; + case 'POST' : $response = $this->request( $url, $parameters, "POST" ); break; + } + + if( $response && $this->decode_json ){ + $response = json_decode( $response ); + } + + return $response; + } + + /** + * GET wrappwer for provider apis request + */ + function get( $url, $parameters = array() ) + { + return $this->api( $url, 'GET', $parameters ); + } + + /** + * POST wreapper for provider apis request + */ + function post( $url, $parameters = array() ) + { + return $this->api( $url, 'POST', $parameters ); + } + + // -- tokens + + public function tokenInfo($accesstoken) + { + $params['access_token'] = $this->access_token; + $response = $this->request( $this->token_info_url, $params ); + return $this->parseRequestResult( $response ); + } + + public function refreshToken( $parameters = array() ) + { + $params = array( + "client_id" => $this->client_id, + "client_secret" => $this->client_secret, + "grant_type" => "refresh_token" + ); + + foreach($parameters as $k=>$v ){ + $params[$k] = $v; + } + + $response = $this->request( $this->token_url, $params, "POST" ); + return $this->parseRequestResult( $response ); + } + + // -- utilities + + private function request( $url, $params=false, $type="GET" ) + { + Hybrid_Logger::info( "Enter OAuth2Client::request( $url )" ); + Hybrid_Logger::debug( "OAuth2Client::request(). dump request params: ", serialize( $params ) ); + + if( $type == "GET" ){ + $url = $url . ( strpos( $url, '?' ) ? '&' : '?' ) . http_build_query( $params ); + } + + $this->http_info = array(); + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL , $url ); + curl_setopt($ch, CURLOPT_RETURNTRANSFER , 1 ); + curl_setopt($ch, CURLOPT_TIMEOUT , $this->curl_time_out ); + curl_setopt($ch, CURLOPT_USERAGENT , $this->curl_useragent ); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT , $this->curl_connect_time_out ); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER , $this->curl_ssl_verifypeer ); + curl_setopt($ch, CURLOPT_HTTPHEADER , $this->curl_header ); + + if($this->curl_proxy){ + curl_setopt( $ch, CURLOPT_PROXY , $this->curl_proxy); + } + + if( $type == "POST" ){ + curl_setopt($ch, CURLOPT_POST, 1); + if($params) curl_setopt( $ch, CURLOPT_POSTFIELDS, $params ); + } + + $response = curl_exec($ch); + Hybrid_Logger::debug( "OAuth2Client::request(). dump request info: ", serialize( curl_getinfo($ch) ) ); + Hybrid_Logger::debug( "OAuth2Client::request(). dump request result: ", serialize( $response ) ); + + $this->http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $this->http_info = array_merge($this->http_info, curl_getinfo($ch)); + + curl_close ($ch); + + return $response; + } + + private function parseRequestResult( $result ) + { + if( json_decode( $result ) ) return json_decode( $result ); + + parse_str( $result, $ouput ); + + $result = new StdClass(); + + foreach( $ouput as $k => $v ) + $result->$k = $v; + + return $result; + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/OpenID/LightOpenID.php b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/OpenID/LightOpenID.php new file mode 100644 index 0000000..2bb935c --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/OpenID/LightOpenID.php @@ -0,0 +1,803 @@ += 5.1.2 with curl or http/https stream wrappers enabled. + * @author Mewp + * @copyright Copyright (c) 2010, Mewp + * @license http://www.opensource.org/licenses/mit-license.php MIT + */ +class LightOpenID +{ + public $returnUrl + , $required = array() + , $optional = array() + , $verify_peer = null + , $capath = null + , $cainfo = null + , $data; + private $identity, $claimed_id; + protected $server, $version, $trustRoot, $aliases, $identifier_select = false + , $ax = false, $sreg = false, $setup_url = null, $headers = array(), $proxy = null; + static protected $ax_to_sreg = array( + 'namePerson/friendly' => 'nickname', + 'contact/email' => 'email', + 'namePerson' => 'fullname', + 'birthDate' => 'dob', + 'person/gender' => 'gender', + 'contact/postalCode/home' => 'postcode', + 'contact/country/home' => 'country', + 'pref/language' => 'language', + 'pref/timezone' => 'timezone', + ); + + function __construct($host, $proxy) + { + $this->proxy = $proxy; + $this->trustRoot = (strpos($host, '://') ? $host : 'http://' . $host); + if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') + || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) + && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') + ) { + $this->trustRoot = (strpos($host, '://') ? $host : 'https://' . $host); + } + + if(($host_end = strpos($this->trustRoot, '/', 8)) !== false) { + $this->trustRoot = substr($this->trustRoot, 0, $host_end); + } + + $uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?'); + $this->returnUrl = $this->trustRoot . $uri; + + $this->data = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET; + + if(!function_exists('curl_init') && !in_array('https', stream_get_wrappers())) { + throw new ErrorException('You must have either https wrappers or curl enabled.'); + } + } + + function __set($name, $value) + { + switch ($name) { + case 'identity': + if (strlen($value = trim((String) $value))) { + if (preg_match('#^xri:/*#i', $value, $m)) { + $value = substr($value, strlen($m[0])); + } elseif (!preg_match('/^(?:[=@+\$!\(]|https?:)/i', $value)) { + $value = "http://$value"; + } + if (preg_match('#^https?://[^/]+$#i', $value, $m)) { + $value .= '/'; + } + } + $this->$name = $this->claimed_id = $value; + break; + case 'trustRoot': + case 'realm': + $this->trustRoot = trim($value); + } + } + + function __get($name) + { + switch ($name) { + case 'identity': + # We return claimed_id instead of identity, + # because the developer should see the claimed identifier, + # i.e. what he set as identity, not the op-local identifier (which is what we verify) + return $this->claimed_id; + case 'trustRoot': + case 'realm': + return $this->trustRoot; + case 'mode': + return empty($this->data['openid_mode']) ? null : $this->data['openid_mode']; + } + } + + /** + * Checks if the server specified in the url exists. + * + * @param $url url to check + * @return true, if the server exists; false otherwise + */ + function hostExists($url) + { + if (strpos($url, '/') === false) { + $server = $url; + } else { + $server = @parse_url($url, PHP_URL_HOST); + } + + if (!$server) { + return false; + } + + return !!gethostbynamel($server); + } + + protected function request_curl($url, $method='GET', $params=array(), $update_claimed_id) + { + $params = http_build_query($params, '', '&'); + $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : '')); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_HEADER, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*')); + if($this->proxy){ + curl_setopt( $curl, CURLOPT_PROXY, $this->proxy); + } + if($this->verify_peer !== null) { + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer); + if($this->capath) { + curl_setopt($curl, CURLOPT_CAPATH, $this->capath); + } + + if($this->cainfo) { + curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo); + } + } + + if ($method == 'POST') { + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $params); + } elseif ($method == 'HEAD') { + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_NOBODY, true); + } else { + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_HTTPGET, true); + } + $response = curl_exec($curl); + + if($method == 'HEAD' && curl_getinfo($curl, CURLINFO_HTTP_CODE) == 405) { + curl_setopt($curl, CURLOPT_HTTPGET, true); + $response = curl_exec($curl); + $response = substr($response, 0, strpos($response, "\r\n\r\n")); + } + + if($method == 'HEAD' || $method == 'GET') { + $header_response = $response; + + # If it's a GET request, we want to only parse the header part. + if($method == 'GET') { + $header_response = substr($response, 0, strpos($response, "\r\n\r\n")); + } + + $headers = array(); + foreach(explode("\n", $header_response) as $header) { + $pos = strpos($header,':'); + if ($pos !== false) { + $name = strtolower(trim(substr($header, 0, $pos))); + $headers[$name] = trim(substr($header, $pos+1)); + } + } + + if($update_claimed_id) { + # Updating claimed_id in case of redirections. + $effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); + if($effective_url != $url) { + $this->identity = $this->claimed_id = $effective_url; + } + } + + if($method == 'HEAD') { + return $headers; + } else { + $this->headers = $headers; + } + } + + if (curl_errno($curl)) { + throw new ErrorException(curl_error($curl), curl_errno($curl)); + } + + return $response; + } + + protected function parse_header_array($array, $update_claimed_id) + { + $headers = array(); + foreach($array as $header) { + $pos = strpos($header,':'); + if ($pos !== false) { + $name = strtolower(trim(substr($header, 0, $pos))); + $headers[$name] = trim(substr($header, $pos+1)); + + # Following possible redirections. The point is just to have + # claimed_id change with them, because the redirections + # are followed automatically. + # We ignore redirections with relative paths. + # If any known provider uses them, file a bug report. + if($name == 'location' && $update_claimed_id) { + if(strpos($headers[$name], 'http') === 0) { + $this->identity = $this->claimed_id = $headers[$name]; + } elseif($headers[$name][0] == '/') { + $parsed_url = parse_url($this->claimed_id); + $this->identity = + $this->claimed_id = $parsed_url['scheme'] . '://' + . $parsed_url['host'] + . $headers[$name]; + } + } + } + } + return $headers; + } + + protected function request_streams($url, $method='GET', $params=array(), $update_claimed_id) + { + if(!$this->hostExists($url)) { + throw new ErrorException("Could not connect to $url.", 404); + } + + $params = http_build_query($params, '', '&'); + switch($method) { + case 'GET': + $opts = array( + 'http' => array( + 'method' => 'GET', + 'header' => 'Accept: application/xrds+xml, */*', + 'ignore_errors' => true, + ), 'ssl' => array( + 'CN_match' => parse_url($url, PHP_URL_HOST), + ), + ); + $url = $url . ($params ? '?' . $params : ''); + if($this->proxy){ + $opts['http']['proxy'] = 'http://' . $this->proxy; + } + break; + case 'POST': + $opts = array( + 'http' => array( + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => $params, + 'ignore_errors' => true, + ), 'ssl' => array( + 'CN_match' => parse_url($url, PHP_URL_HOST), + ), + ); + if($this->proxy){ + $opts['http']['proxy'] = 'http://' . $this->proxy; + } + break; + case 'HEAD': + # We want to send a HEAD request, + # but since get_headers doesn't accept $context parameter, + # we have to change the defaults. + $default = stream_context_get_options(stream_context_get_default()); + stream_context_get_default( + array( + 'http' => array( + 'method' => 'HEAD', + 'header' => 'Accept: application/xrds+xml, */*', + 'ignore_errors' => true, + ), 'ssl' => array( + 'CN_match' => parse_url($url, PHP_URL_HOST), + ), + ) + ); + + $url = $url . ($params ? '?' . $params : ''); + $headers = get_headers ($url); + if(!$headers) { + return array(); + } + + if(intval(substr($headers[0], strlen('HTTP/1.1 '))) == 405) { + # The server doesn't support HEAD, so let's emulate it with + # a GET. + $args = func_get_args(); + $args[1] = 'GET'; + call_user_func_array(array($this, 'request_streams'), $args); + return $this->headers; + } + + $headers = $this->parse_header_array($headers, $update_claimed_id); + + # And restore them. + stream_context_get_default($default); + return $headers; + } + + if($this->verify_peer) { + $opts['ssl'] += array( + 'verify_peer' => true, + 'capath' => $this->capath, + 'cafile' => $this->cainfo, + ); + } + + $context = stream_context_create ($opts); + $data = file_get_contents($url, false, $context); + # This is a hack for providers who don't support HEAD requests. + # It just creates the headers array for the last request in $this->headers. + if(isset($http_response_header)) { + $this->headers = $this->parse_header_array($http_response_header, $update_claimed_id); + } + + return $data; + } + + protected function request($url, $method='GET', $params=array(), $update_claimed_id=false) + { + if (function_exists('curl_init') + && (!in_array('https', stream_get_wrappers()) || !ini_get('safe_mode') && !ini_get('open_basedir')) + ) { + return $this->request_curl($url, $method, $params, $update_claimed_id); + } + return $this->request_streams($url, $method, $params, $update_claimed_id); + } + + protected function build_url($url, $parts) + { + if (isset($url['query'], $parts['query'])) { + $parts['query'] = $url['query'] . '&' . $parts['query']; + } + + $url = $parts + $url; + $url = $url['scheme'] . '://' + . (empty($url['username'])?'' + :(empty($url['password'])? "{$url['username']}@" + :"{$url['username']}:{$url['password']}@")) + . $url['host'] + . (empty($url['port'])?'':":{$url['port']}") + . (empty($url['path'])?'':$url['path']) + . (empty($url['query'])?'':"?{$url['query']}") + . (empty($url['fragment'])?'':"#{$url['fragment']}"); + return $url; + } + + /** + * Helper function used to scan for / tags and extract information + * from them + */ + protected function htmlTag($content, $tag, $attrName, $attrValue, $valueName) + { + preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1); + preg_match_all("#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2); + + $result = array_merge($matches1[1], $matches2[1]); + return empty($result)?false:$result[0]; + } + + /** + * Performs Yadis and HTML discovery. Normally not used. + * @param $url Identity URL. + * @return String OP Endpoint (i.e. OpenID provider address). + * @throws ErrorException + */ + function discover($url) + { + if (!$url) throw new ErrorException('No identity supplied.'); + # Use xri.net proxy to resolve i-name identities + if (!preg_match('#^https?:#', $url)) { + $url = "https://xri.net/$url"; + } + + # We save the original url in case of Yadis discovery failure. + # It can happen when we'll be lead to an XRDS document + # which does not have any OpenID2 services. + $originalUrl = $url; + + # A flag to disable yadis discovery in case of failure in headers. + $yadis = true; + + # We'll jump a maximum of 5 times, to avoid endless redirections. + for ($i = 0; $i < 5; $i ++) { + if ($yadis) { + $headers = $this->request($url, 'HEAD', array(), true); + + $next = false; + if (isset($headers['x-xrds-location'])) { + $url = $this->build_url(parse_url($url), parse_url(trim($headers['x-xrds-location']))); + $next = true; + } + + if (isset($headers['content-type']) + && (strpos($headers['content-type'], 'application/xrds+xml') !== false + || strpos($headers['content-type'], 'text/xml') !== false) + ) { + # Apparently, some providers return XRDS documents as text/html. + # While it is against the spec, allowing this here shouldn't break + # compatibility with anything. + # --- + # Found an XRDS document, now let's find the server, and optionally delegate. + $content = $this->request($url, 'GET'); + + preg_match_all('#(.*?)#s', $content, $m); + foreach($m[1] as $content) { + $content = ' ' . $content; # The space is added, so that strpos doesn't return 0. + + # OpenID 2 + $ns = preg_quote('http://specs.openid.net/auth/2.0/', '#'); + if(preg_match('#\s*'.$ns.'(server|signon)\s*#s', $content, $type)) { + if ($type[1] == 'server') $this->identifier_select = true; + + preg_match('#(.*)#', $content, $server); + preg_match('#<(Local|Canonical)ID>(.*)#', $content, $delegate); + if (empty($server)) { + return false; + } + # Does the server advertise support for either AX or SREG? + $this->ax = (bool) strpos($content, 'http://openid.net/srv/ax/1.0'); + $this->sreg = strpos($content, 'http://openid.net/sreg/1.0') + || strpos($content, 'http://openid.net/extensions/sreg/1.1'); + + $server = $server[1]; + if (isset($delegate[2])) $this->identity = trim($delegate[2]); + $this->version = 2; + + $this->server = $server; + return $server; + } + + # OpenID 1.1 + $ns = preg_quote('http://openid.net/signon/1.1', '#'); + if (preg_match('#\s*'.$ns.'\s*#s', $content)) { + + preg_match('#(.*)#', $content, $server); + preg_match('#<.*?Delegate>(.*)#', $content, $delegate); + if (empty($server)) { + return false; + } + # AX can be used only with OpenID 2.0, so checking only SREG + $this->sreg = strpos($content, 'http://openid.net/sreg/1.0') + || strpos($content, 'http://openid.net/extensions/sreg/1.1'); + + $server = $server[1]; + if (isset($delegate[1])) $this->identity = $delegate[1]; + $this->version = 1; + + $this->server = $server; + return $server; + } + } + + $next = true; + $yadis = false; + $url = $originalUrl; + $content = null; + break; + } + if ($next) continue; + + # There are no relevant information in headers, so we search the body. + $content = $this->request($url, 'GET', array(), true); + + if (isset($this->headers['x-xrds-location'])) { + $url = $this->build_url(parse_url($url), parse_url(trim($this->headers['x-xrds-location']))); + continue; + } + + $location = $this->htmlTag($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content'); + if ($location) { + $url = $this->build_url(parse_url($url), parse_url($location)); + continue; + } + } + + if (!$content) $content = $this->request($url, 'GET'); + + # At this point, the YADIS Discovery has failed, so we'll switch + # to openid2 HTML discovery, then fallback to openid 1.1 discovery. + $server = $this->htmlTag($content, 'link', 'rel', 'openid2.provider', 'href'); + $delegate = $this->htmlTag($content, 'link', 'rel', 'openid2.local_id', 'href'); + $this->version = 2; + + if (!$server) { + # The same with openid 1.1 + $server = $this->htmlTag($content, 'link', 'rel', 'openid.server', 'href'); + $delegate = $this->htmlTag($content, 'link', 'rel', 'openid.delegate', 'href'); + $this->version = 1; + } + + if ($server) { + # We found an OpenID2 OP Endpoint + if ($delegate) { + # We have also found an OP-Local ID. + $this->identity = $delegate; + } + $this->server = $server; + return $server; + } + + throw new ErrorException("No OpenID Server found at $url", 404); + } + throw new ErrorException('Endless redirection!', 500); + } + + protected function sregParams() + { + $params = array(); + # We always use SREG 1.1, even if the server is advertising only support for 1.0. + # That's because it's fully backwards compatibile with 1.0, and some providers + # advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com + $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1'; + if ($this->required) { + $params['openid.sreg.required'] = array(); + foreach ($this->required as $required) { + if (!isset(self::$ax_to_sreg[$required])) continue; + $params['openid.sreg.required'][] = self::$ax_to_sreg[$required]; + } + $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']); + } + + if ($this->optional) { + $params['openid.sreg.optional'] = array(); + foreach ($this->optional as $optional) { + if (!isset(self::$ax_to_sreg[$optional])) continue; + $params['openid.sreg.optional'][] = self::$ax_to_sreg[$optional]; + } + $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']); + } + return $params; + } + + protected function axParams() + { + $params = array(); + if ($this->required || $this->optional) { + $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; + $params['openid.ax.mode'] = 'fetch_request'; + $this->aliases = array(); + $counts = array(); + $required = array(); + $optional = array(); + foreach (array('required','optional') as $type) { + foreach ($this->$type as $alias => $field) { + if (is_int($alias)) $alias = strtr($field, '/', '_'); + $this->aliases[$alias] = 'http://axschema.org/' . $field; + if (empty($counts[$alias])) $counts[$alias] = 0; + $counts[$alias] += 1; + ${$type}[] = $alias; + } + } + foreach ($this->aliases as $alias => $ns) { + $params['openid.ax.type.' . $alias] = $ns; + } + foreach ($counts as $alias => $count) { + if ($count == 1) continue; + $params['openid.ax.count.' . $alias] = $count; + } + + # Don't send empty ax.requied and ax.if_available. + # Google and possibly other providers refuse to support ax when one of these is empty. + if($required) { + $params['openid.ax.required'] = implode(',', $required); + } + if($optional) { + $params['openid.ax.if_available'] = implode(',', $optional); + } + } + return $params; + } + + protected function authUrl_v1($immediate) + { + $returnUrl = $this->returnUrl; + # If we have an openid.delegate that is different from our claimed id, + # we need to somehow preserve the claimed id between requests. + # The simplest way is to just send it along with the return_to url. + if($this->identity != $this->claimed_id) { + $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->claimed_id; + } + + $params = array( + 'openid.return_to' => $returnUrl, + 'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup', + 'openid.identity' => $this->identity, + 'openid.trust_root' => $this->trustRoot, + ) + $this->sregParams(); + + return $this->build_url(parse_url($this->server) + , array('query' => http_build_query($params, '', '&'))); + } + + protected function authUrl_v2($immediate) + { + $params = array( + 'openid.ns' => 'http://specs.openid.net/auth/2.0', + 'openid.mode' => $immediate ? 'checkid_immediate' : 'checkid_setup', + 'openid.return_to' => $this->returnUrl, + 'openid.realm' => $this->trustRoot, + ); + if ($this->ax) { + $params += $this->axParams(); + } + if ($this->sreg) { + $params += $this->sregParams(); + } + if (!$this->ax && !$this->sreg) { + # If OP doesn't advertise either SREG, nor AX, let's send them both + # in worst case we don't get anything in return. + $params += $this->axParams() + $this->sregParams(); + } + + if ($this->identifier_select) { + $params['openid.identity'] = $params['openid.claimed_id'] + = 'http://specs.openid.net/auth/2.0/identifier_select'; + } else { + $params['openid.identity'] = $this->identity; + $params['openid.claimed_id'] = $this->claimed_id; + } + + return $this->build_url(parse_url($this->server) + , array('query' => http_build_query($params, '', '&'))); + } + + /** + * Returns authentication url. Usually, you want to redirect your user to it. + * @return String The authentication url. + * @param String $select_identifier Whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1. + * @throws ErrorException + */ + function authUrl($immediate = false) + { + if ($this->setup_url && !$immediate) return $this->setup_url; + if (!$this->server) $this->discover($this->identity); + + if ($this->version == 2) { + return $this->authUrl_v2($immediate); + } + return $this->authUrl_v1($immediate); + } + + /** + * Performs OpenID verification with the OP. + * @return Bool Whether the verification was successful. + * @throws ErrorException + */ + function validate() + { + # If the request was using immediate mode, a failure may be reported + # by presenting user_setup_url (for 1.1) or reporting + # mode 'setup_needed' (for 2.0). Also catching all modes other than + # id_res, in order to avoid throwing errors. + if(isset($this->data['openid_user_setup_url'])) { + $this->setup_url = $this->data['openid_user_setup_url']; + return false; + } + if($this->mode != 'id_res') { + return false; + } + + $this->claimed_id = isset($this->data['openid_claimed_id'])?$this->data['openid_claimed_id']:$this->data['openid_identity']; + $params = array( + 'openid.assoc_handle' => $this->data['openid_assoc_handle'], + 'openid.signed' => $this->data['openid_signed'], + 'openid.sig' => $this->data['openid_sig'], + ); + + if (isset($this->data['openid_ns'])) { + # We're dealing with an OpenID 2.0 server, so let's set an ns + # Even though we should know location of the endpoint, + # we still need to verify it by discovery, so $server is not set here + $params['openid.ns'] = 'http://specs.openid.net/auth/2.0'; + } elseif (isset($this->data['openid_claimed_id']) + && $this->data['openid_claimed_id'] != $this->data['openid_identity'] + ) { + # If it's an OpenID 1 provider, and we've got claimed_id, + # we have to append it to the returnUrl, like authUrl_v1 does. + $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?') + . 'openid.claimed_id=' . $this->claimed_id; + } + + if ($this->data['openid_return_to'] != $this->returnUrl) { + # The return_to url must match the url of current request. + # I'm assuing that noone will set the returnUrl to something that doesn't make sense. + return false; + } + + $server = $this->discover($this->claimed_id); + + foreach (explode(',', $this->data['openid_signed']) as $item) { + # Checking whether magic_quotes_gpc is turned on, because + # the function may fail if it is. For example, when fetching + # AX namePerson, it might containg an apostrophe, which will be escaped. + # In such case, validation would fail, since we'd send different data than OP + # wants to verify. stripslashes() should solve that problem, but we can't + # use it when magic_quotes is off. + $value = $this->data['openid_' . str_replace('.','_',$item)]; + $params['openid.' . $item] = function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc() ? stripslashes($value) : $value; + + } + + $params['openid.mode'] = 'check_authentication'; + + $response = $this->request($server, 'POST', $params); + + return preg_match('/is_valid\s*:\s*true/i', $response); + } + + protected function getAxAttributes() + { + $alias = null; + if (isset($this->data['openid_ns_ax']) + && $this->data['openid_ns_ax'] != 'http://openid.net/srv/ax/1.0' + ) { # It's the most likely case, so we'll check it before + $alias = 'ax'; + } else { + # 'ax' prefix is either undefined, or points to another extension, + # so we search for another prefix + foreach ($this->data as $key => $val) { + if (substr($key, 0, strlen('openid_ns_')) == 'openid_ns_' + && $val == 'http://openid.net/srv/ax/1.0' + ) { + $alias = substr($key, strlen('openid_ns_')); + break; + } + } + } + if (!$alias) { + # An alias for AX schema has not been found, + # so there is no AX data in the OP's response + return array(); + } + + $attributes = array(); + foreach (explode(',', $this->data['openid_signed']) as $key) { + $keyMatch = $alias . '.value.'; + if (substr($key, 0, strlen($keyMatch)) != $keyMatch) { + continue; + } + $key = substr($key, strlen($keyMatch)); + if (!isset($this->data['openid_' . $alias . '_type_' . $key])) { + # OP is breaking the spec by returning a field without + # associated ns. This shouldn't happen, but it's better + # to check, than cause an E_NOTICE. + continue; + } + $value = $this->data['openid_' . $alias . '_value_' . $key]; + $key = substr($this->data['openid_' . $alias . '_type_' . $key], + strlen('http://axschema.org/')); + + $attributes[$key] = $value; + } + return $attributes; + } + + protected function getSregAttributes() + { + $attributes = array(); + $sreg_to_ax = array_flip(self::$ax_to_sreg); + foreach (explode(',', $this->data['openid_signed']) as $key) { + $keyMatch = 'sreg.'; + if (substr($key, 0, strlen($keyMatch)) != $keyMatch) { + continue; + } + $key = substr($key, strlen($keyMatch)); + if (!isset($sreg_to_ax[$key])) { + # The field name isn't part of the SREG spec, so we ignore it. + continue; + } + $attributes[$sreg_to_ax[$key]] = $this->data['openid_sreg_' . $key]; + } + return $attributes; + } + + /** + * Gets AX/SREG attributes provided by OP. should be used only after successful validaton. + * Note that it does not guarantee that any of the required/optional parameters will be present, + * or that there will be no other attributes besides those specified. + * In other words. OP may provide whatever information it wants to. + * * SREG names will be mapped to AX names. + * * @return Array Array of attributes with keys being the AX schema names, e.g. 'contact/email' + * @see http://www.axschema.org/types/ + */ + function getAttributes() + { + if (isset($this->data['openid_ns']) + && $this->data['openid_ns'] == 'http://specs.openid.net/auth/2.0' + ) { # OpenID 2.0 + # We search for both AX and SREG attributes, with AX taking precedence. + return $this->getAxAttributes() + $this->getSregAttributes(); + } + return $this->getSregAttributes(); + } +} diff --git a/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/index.html b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/index.html new file mode 100644 index 0000000..065d2da --- /dev/null +++ b/www/protected/extensions/yii-socialconnect/vendors/Hybrid/thirdparty/index.html @@ -0,0 +1,10 @@ + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + \ No newline at end of file diff --git a/www/protected/migrations/m120927_143321_tbl_productos.php b/www/protected/migrations/m120927_143321_tbl_productos.php index 852c471..83919b2 100644 --- a/www/protected/migrations/m120927_143321_tbl_productos.php +++ b/www/protected/migrations/m120927_143321_tbl_productos.php @@ -2,8 +2,7 @@ class m120927_143321_tbl_productos extends CDbMigration { - public function up() - { + public function safeUp() { $this->createTable('tbl_productos', array( 'id' => 'pk', 'titulo' => 'string', @@ -12,7 +11,7 @@ class m120927_143321_tbl_productos extends CDbMigration )); - $this->createTable('tbl_subcripciones', array( + $this->createTable('tbl_subscripciones', array( 'id' => 'pk', 'id_usuario' => 'integer', 'id_producto' => 'integer', @@ -20,14 +19,11 @@ class m120927_143321_tbl_productos extends CDbMigration 'fecha_inicio' => 'datetime', 'fecha_fin' => 'datetime', )); - - $this->addColumn('tbl_usuarios', 'id_producto', 'integer'); } - public function down() + public function safeDown() { $this->dropTable('tbl_productos'); - $this->dropTable('tbl_subcripciones'); - $this->dropColumn('tbl_usuarios', 'id_producto'); + $this->dropTable('tbl_subscripciones'); } } \ No newline at end of file diff --git a/www/protected/models/FormularioInvitarAgente.php b/www/protected/models/FormularioInvitarAgente.php index cd70040..d7f99fa 100644 --- a/www/protected/models/FormularioInvitarAgente.php +++ b/www/protected/models/FormularioInvitarAgente.php @@ -20,7 +20,7 @@ class FormularioInvitarAgente extends CFormModel { array('nombre, email', 'required'), array('nombre, email, mensaje', 'safe'), array('email', 'email'), - array('email', 'comprobarEmailRepetido', 'message' => Yii::t('profind', 'Ya existe un agente con el mismo email')), + //array('email', 'comprobarEmailRepetido', 'message' => Yii::t('profind', 'Ya existe un agente con el mismo email')), ); } diff --git a/www/protected/models/FormularioRegistroAgente.php b/www/protected/models/FormularioRegistroAgente.php new file mode 100644 index 0000000..3d49155 --- /dev/null +++ b/www/protected/models/FormularioRegistroAgente.php @@ -0,0 +1,8 @@ + array(self::HAS_MANY, 'Subscripcion', 'id_producto'), ); } diff --git a/www/protected/models/Subcripcion.php b/www/protected/models/Subcripcion.php deleted file mode 100644 index 217ba58..0000000 --- a/www/protected/models/Subcripcion.php +++ /dev/null @@ -1,100 +0,0 @@ -true), - array('estado', 'length', 'max'=>255), - array('fecha_inicio, fecha_fin', 'safe'), - // The following rule is used by search(). - // Please remove those attributes that should not be searched. - array('id, id_usuario, id_producto, estado, fecha_inicio, fecha_fin', 'safe', 'on'=>'search'), - ); - } - - /** - * @return array relational rules. - */ - public function relations() - { - // NOTE: you may need to adjust the relation name and the related - // class name for the relations automatically generated below. - return array( - 'producto' => array(self::HAS_ONE, 'Producto', 'id'), - ); - } - - /** - * @return array customized attribute labels (name=>label) - */ - public function attributeLabels() - { - return array( - 'id' => 'ID', - 'id_usuario' => 'Id Usuario', - 'id_producto' => 'Id Producto', - 'estado' => 'Estado', - 'fecha_inicio' => 'Fecha Inicio', - 'fecha_fin' => 'Fecha Fin', - ); - } - - /** - * Retrieves a list of models based on the current search/filter conditions. - * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions. - */ - public function search() - { - // Warning: Please modify the following code to remove attributes that - // should not be searched. - - $criteria=new CDbCriteria; - - $criteria->compare('id',$this->id); - $criteria->compare('id_usuario',$this->id_usuario); - $criteria->compare('id_producto',$this->id_producto); - $criteria->compare('estado',$this->estado,true); - $criteria->compare('fecha_inicio',$this->fecha_inicio,true); - $criteria->compare('fecha_fin',$this->fecha_fin,true); - - return new CActiveDataProvider($this, array( - 'criteria'=>$criteria, - )); - } -} \ No newline at end of file diff --git a/www/protected/models/Subscripcion.php b/www/protected/models/Subscripcion.php new file mode 100644 index 0000000..0031331 --- /dev/null +++ b/www/protected/models/Subscripcion.php @@ -0,0 +1,108 @@ + true), + array('estado', 'length', 'max' => 255), + array('fecha_inicio, fecha_fin', 'safe'), + // The following rule is used by search(). + // Please remove those attributes that should not be searched. + array('id, id_usuario, id_producto, estado, fecha_inicio, fecha_fin', 'safe', 'on' => 'search'), + ); + } + + /** + * @return array relational rules. + */ + public function relations() { + // NOTE: you may need to adjust the relation name and the related + // class name for the relations automatically generated below. + return array( + 'producto' => array(self::BELONGS_TO, 'Producto', 'id_producto'), + ); + } + + /** + * @return array customized attribute labels (name=>label) + */ + public function attributeLabels() { + return array( + 'id' => 'ID', + 'id_usuario' => 'Id Usuario', + 'id_producto' => 'Id Producto', + 'estado' => 'Estado', + 'fecha_inicio' => 'Fecha Inicio', + 'fecha_fin' => 'Fecha Fin', + ); + } + + public function scopes() { + return array( + 'activa' => array( + 'condition' => 'estado = ' . self::ESTADO_ACTIVO, + 'limit' => 1 + ), + ); + } + + /** + * Retrieves a list of models based on the current search/filter conditions. + * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions. + */ + public function search() { + // Warning: Please modify the following code to remove attributes that + // should not be searched. + + $criteria = new CDbCriteria; + + $criteria->compare('id', $this->id); + $criteria->compare('id_usuario', $this->id_usuario); + $criteria->compare('id_producto', $this->id_producto); + $criteria->compare('estado', $this->estado, true); + $criteria->compare('fecha_inicio', $this->fecha_inicio, true); + $criteria->compare('fecha_fin', $this->fecha_fin, true); + + return new CActiveDataProvider($this, array( + 'criteria' => $criteria, + )); + } + +} \ No newline at end of file diff --git a/www/protected/models/Usuario.php b/www/protected/models/Usuario.php index 5820e0b..cc27c35 100644 --- a/www/protected/models/Usuario.php +++ b/www/protected/models/Usuario.php @@ -30,6 +30,9 @@ class Usuario extends CActiveRecord { const ESTADO_ACTIVO = 1; const ESTADO_DENEGADO = 2; + const TIPO_USUARIO_COORDINADOR = 'C'; + const TIPO_USUARIO_AGENTE = 'A'; + public $ficheroFotografia; public $fotografia; @@ -65,6 +68,7 @@ class Usuario extends CActiveRecord { array('email', 'email'), array('email', 'unique'), array('descripcion', 'safe'), + array('tipo', 'default', 'value' => self::TIPO_USUARIO_COORDINADOR), array('email, nombre, apellidos, password, tipo, titulo, localidad, telefono', 'length', 'max' => 255), array('ficheroFotografia', 'file', 'types' => 'jpg', @@ -84,7 +88,7 @@ class Usuario extends CActiveRecord { // NOTE: you may need to adjust the relation name and the related // class name for the relations automatically generated below. return array( - 'empresa' => array(self::HAS_ONE, 'Empresa', 'id'), + 'empresa' => array(self::BELONGS_TO, 'Empresa', 'id_empresa'), ); } diff --git a/www/protected/views/mails/registro_agente.php b/www/protected/views/mails/registro_agente.php new file mode 100644 index 0000000..f6cd163 --- /dev/null +++ b/www/protected/views/mails/registro_agente.php @@ -0,0 +1,18 @@ + + + Active su cuenta de agente en PROFIND + + +

Active su cuenta de agente en PROFIND

+

+ Para completar el registro, pulse en el siguiente enlace:
+ +

+

+ Este correo se ha enviado desde http://www.profindtic.com. + Usted ha recibido este correo porque han utilizado su dirección para registrarle como agente en PROFIND. +

+

No responda a este correo ya que ha sido generado automáticamente para su información.

+

El equipo de PROFIND

+ + \ No newline at end of file diff --git a/www/protected/views/subcripcion/update.php b/www/protected/views/subcripcion/update.php index a2f50c5..c24eeab 100644 --- a/www/protected/views/subcripcion/update.php +++ b/www/protected/views/subcripcion/update.php @@ -1,8 +1,8 @@ breadcrumbs=array( - 'Subcripcion'=>array('/subcripcion'), + 'Subscripcion'=>array('/subscripcion'), 'Update', ); ?>