nyroBlog
Bannière NyroBlog, par Nyro
Image par Nyro - ?

Tag : PHP


Connexions OAuth Multiple avec Symfony 2.3

Voici un long tutorial pour Symfony 2.3 permettant d'ajouter des boutons dec onnexion à des services proposant de l'Oauth (comme Facebook, twitter, google, etc..)

Il existe un très bon Bundle, HWIOAuthBundle qui a toutes les fonctionnalités de connexion à OAuth, mais qui manque cruellement de documentations claires.

Ce tutorial part du principe que vous avez déjà en place :

  • Un firewall dans lequel on peut se connecter via un formulaire de connexion (form_login)
  • Un formulaire d'inscription
  • Les utilisateurs sont enregistrés en base de données (via une entity, nommé User dans ce tuto)
  • La gestion des roles et sécurité restera exactement la même

 

Les fonctionnalités que nosu allons mettre en place vont se greffer à l'existant. Toute la sécurité, gestion d'accès et autre reste à mettre en place (ou le sont peut-être déjà)
L'ajout de la fonctionnalité Connexion Oauth permettra :

  • Pré-remplir le formulaire d'inscription si l'utilisateur n'existe pas
  • Connexion en un seul clic via un service externe
  • Ajout d'un service externe pour un compte déjà existant : Ainsi un utilisateur pourra utiliser plusieurs boutons de connexion externe pour se connecter au même compte de votre site



Installation de HWIOAuthBundle

La première étape consiste donc à installer HWIOAuthBundle
Dans le composer.json, il faut ajouter la ligne suivante de le require:

"hwi/oauth-bundle": "0.3.*@dev"

Un update, et on ajoute le Bundle dans app/AppKernel.php :

public function registerBundles()
{
    $bundles = array(
        // ...
        new HWI\Bundle\OAuthBundle\HWIOAuthBundle(),
    );
}

Jusque là, rien d'anormal. Si on essaie de recharger Symfony à ce moment là, on aura une erreur car le Bundle n'est pas configuré.

 

Configuration du bundle

Donc dans le fichier config.yml, on peut ajouter cette configuration :

#HWIOAuthBundle
hwi_oauth:
    firewall_name: oauth
    resource_owners:
        facebook:
            type: facebook
            client_id: %facebook_client_id%
            client_secret: %facebook_client_secret%
            scope: email
            infos_url:     "https://graph.facebook.com/me?fields=username,name,email,picture.type(large)"
            paths:
                email:          email
                profilepicture: picture.data.url
            options:
                display: popup #dialog is optimized for popup window
        twitter:
            type: twitter
            client_id: %twitter_client_id%
            client_secret: %twitter_client_secret%
            paths:
                profilepicture: profile_image_url
        google:
            type: google
            client_id: %google_client_id%
            client_secret: %google_client_secret%
            scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"
            paths:
                email:           email
                profilepicture:  picture

Les services facebook, twitter et google sont paramétré pour se connecter, mais aussi récupérer l'email et l'avatar de l'utilisateur

Le paramètre firewall_name est important ; c'est le nom que l'on va donner au firewall spécifique à la connexion oAuth

Vous aurez noté qu'on utilise des paramètres pour les client_id et client_secret. A vous de les placer correctement dans votre fichier parameters.
Les options pour chaque app sur les services externes et la récupération de ces infos diffèrent selon les services, mais on s'y retrouve assez facilement.

 

Configuration du firewall

Il est temps d'ajouter ce firewall.
Notre but est de ne rien touché aux firewall acuels et de créer un espace spécial pour tout ce qui concerne le bundle que l'on vient d'ajouter.
Dans votre fichier app/config/security.yml, ajoter les informations suivantes :

security:
    # Probably other stuff here

    providers:
		# Probably other providers here
        oauth:
            id: my_oauth_members

    firewalls:
		# Probably other firewalls here

        oauth:
            pattern:    ˆ/oauth/.*
            anonymous: ~
            provider: oauth
            oauth:
                resource_owners:
                    facebook: "/oauth/login/check-facebook"
                    twitter: "/oauth/login/check-twitter"
                    google: "/oauth/login/check-google"
                login_path: /oauth/login
                failure_path: /oauth/login
                check_path: /oauth/login_check
                default_target_path: /oauth/target
                oauth_user_provider:
                    service: my_oauth_members

    access_control:
		# Probably other access_control here
        - { path: ˆ/oauth/target, roles: ROLE_OAUTH_USER }
        - { path: ˆ/oauth/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
		# Probably other access_control here

A la vue de ce fichier, vous aurez compris que toutes les URLs commenceront par oauth.
Vous pouvez évidemment changer cela pour ce que vous voulez, en changeant les fichiers routings.
Soyez sûr que l'URL /oauthRegister est accessible derrière votre firewall déjà existant, en étant connecté ou non. C'est cette URL qui fera le pont entre les deux firewalls.

 

Configuration du routing

Pour mettre en place le routing, il suffit d'importer dans un fichier routing extsiant un autre routing, avec le préfixe oauth. On ajouter aussi la route pour le oauthRegister

oauth_register:
    pattern: /oauthRegister
    defaults: { _controller: AcmeMainBundle:Security:oauthRegister }
my_oauth:
    resource: "@AcmeMainBundle/Resources/config/routingSecurityOAuth.yml"
    prefix:   /oauth/

Et le fichier routingSecurityOAuth.yml :

hwi_oauth_login:
    resource: "@HWIOAuthBundle/Resources/config/routing/login.xml"
    prefix:   /login
hwi_oauth_redirect:
    resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
    prefix:   /connect
facebook_login:
    pattern: /login/check-facebook
twitter_login:
    pattern: /login/check-twitter
google_login:
    pattern: /login/check-google
oauth_target:
    pattern: /target
    defaults: { _controller: AcmeMainBundle:Security:oauthTarget }

On commence par importer les routes du bundle, avec des préfixes spécifiques. Pour chaque service, on doit créé une route avec seulement son pattern. C'est le bundle qui intercepttera ces URLs pour réaliser les actions nécessaires. La route target permettra de recevoir les infos de l'utilisateur connecté, puis de l'envoyer vers oauthRegister et faire le pont avec l'autre firewall.

 

Configuration de l'user provider (service)

Dans le firewall, vous avez probablement remarqué que l'on a configuré le provider my_oauth_members. Il est temps de paramétrer ce provider (avant de le coder), dans un fichier services.yml. :

parameters:
    acme_main.oauth_members.class: Acme\MainBundle\Services\OAuthMembersService

services:
    my_oauth_members:
        class: %acme_main.oauth_members.class%

 

Classe OAuthUser

La classe OAuthUser sera l'objet utilisé par Symfony pour connecter l'utilisateur après l'autorisation sur un service externe. Dans notre cas, cet objet est très simple et permet simplement de conserver toutes les informations de l'utilisateur afin de les passer à oauthRegister ensuite.

// Acme/MainBundle/Security/OAuthUser.php
<?php 

namespace Acme\MainBundle\Security;

use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthUser as BaseOAuthUser;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;

class OAuthUser extends BaseOAuthUser {
	
	protected $data;
	
	public function __construct(UserResponseInterface $response) {
		parent::__construct($response->getUsername());
		$this->data = array(
			'provider'=>$response->getResourceOwner()->getName(),
			'providerId'=>$response->getUsername()
		);
		$vars = array(
			'nickname',
			'realname',
			'email',
			'profilePicture',
			'accessToken',
			'refreshToken',
			'tokenSecret',
			'expiresIn',
		);
		foreach($vars as $v) {
			$fct = 'get'.ucfirst($v);
			$this->data[$v] = $response->$fct();
		}
	}
	
	public function getData() {
		return $this->data;
	}
	
	/**
     * {@inheritDoc}
     */
    public function getRoles() {
        return array('ROLE_OAUTH_USER');
    }

}

 

Classe OAuthMembersService

Cette classe est l'user provider de notre firewall est n'est utilisé que lorsque l'utilisateur se connecte depuis un service externe. Les autres fonctions sont alors inutile:

// Acme/MainBundle/Services/OAuthMembersService.php
<?php 

namespace Acme\MainBundle\Services;

use Symfony\Component\Security\Core\User\UserProviderInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\OAuthAwareUserProviderInterface;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use Acme\MainBundle\Security\OAuthUser;

class OAuthMembersService implements UserProviderInterface, OAuthAwareUserProviderInterface {

    public function loadUserByUsername($username) {
		throw new Exception('loadByUsername not implemented');
    }
	
    public function supportsClass($class) {
		return $class === 'Acme\\MainBundle\\Security\\OAuthUser';
    }
	
	public function refreshUser(\Symfony\Component\Security\Core\User\UserInterface $user) {
		if (!$this->supportsClass(get_class($user))) {
			throw new UnsupportedUserException(sprintf('Unsupported user class "%s"', get_class($user)));
		}
		return $user;
	}
	
    public function loadUserByOAuthUserResponse(UserResponseInterface $response) {
		return new OAuthUser($response);
    }
	
}

 

Entity UserOauth

Jusqu'à maintenant, on a tout configuré, et préparé pour envoyer les requêtes oauth sur un controller AcmeMainBundle:Security. Avant de voir ce controller, il faut préparer l'entité responsable de sauvegarder les informations de l'utilisateur en base de données, afin d'être capable de le retrouver lorsqu'il se connectera à nouveau.

L'Entity UserOauth, qui à une relation vers User, la table qui contient vos utilisateurs normaux :

// Acme/MainBundle/Entity/UserOauth.php
<?php 

namespace Acme\MainBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * UserOauth
 *
 * @ORM\Table(name="user_oauth")
 * @ORM\Entity
 */
class UserOauth
{
    /**
     * @var string
     *
     * @ORM\Column(name="provider", type="string", length=250, nullable=false)
     */
    private $provider;

    /**
     * @var string
     *
     * @ORM\Column(name="provider_id", type="string", length=250, nullable=false)
     */
    private $providerId;

    /**
     * @var string
     *
     * @ORM\Column(name="nickname", type="string", length=250, nullable=true)
     */
    private $nickname;

    /**
     * @var string
     *
     * @ORM\Column(name="realname", type="string", length=250, nullable=true)
     */
    private $realname;

    /**
     * @var string
     *
     * @ORM\Column(name="email", type="string", length=250, nullable=true)
     */
    private $email;

    /**
     * @var string
     *
     * @ORM\Column(name="profile_picture", type="string", length=250, nullable=true)
     */
    private $profilePicture;

    /**
     * @var string
     *
     * @ORM\Column(name="access_token", type="string", length=250, nullable=true)
     */
    private $accessToken;

    /**
     * @var string
     *
     * @ORM\Column(name="refresh_token", type="string", length=250, nullable=true)
     */
    private $refreshToken;

    /**
     * @var string
     *
     * @ORM\Column(name="token_secret", type="string", length=250, nullable=true)
     */
    private $tokenSecret;

    /**
     * @var string
     *
     * @ORM\Column(name="expires_in", type="string", length=250, nullable=true)
     */
    private $expiresIn;

    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="bigint")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var \Acme\MainBundle\Entity\User
     *
     * @ORM\ManyToOne(targetEntity="Acme\MainBundle\Entity\User")
     * @ORM\JoinColumns({
     *   @ORM\JoinColumn(name="user_id", referencedColumnName="id")
     * })
     */
    private $user;



    /**
     * Set provider
     *
     * @param string $provider
     * @return UserOauth
     */
    public function setProvider($provider)
    {
        $this->provider = $provider;
    
        return $this;
    }

    /**
     * Get provider
     *
     * @return string 
     */
    public function getProvider()
    {
        return $this->provider;
    }

    /**
     * Set providerId
     *
     * @param string $providerId
     * @return UserOauth
     */
    public function setProviderId($providerId)
    {
        $this->providerId = $providerId;
    
        return $this;
    }

    /**
     * Get providerId
     *
     * @return string 
     */
    public function getProviderId()
    {
        return $this->providerId;
    }

    /**
     * Set token
     *
     * @param string $token
     * @return UserOauth
     */
    public function setToken($token)
    {
        $this->token = $token;
    
        return $this;
    }

    /**
     * Get token
     *
     * @return string 
     */
    public function getToken()
    {
        return $this->token;
    }

    /**
     * Set nickname
     *
     * @param string $nickname
     * @return UserOauth
     */
    public function setNickname($nickname)
    {
        $this->nickname = $nickname;
    
        return $this;
    }

    /**
     * Get nickname
     *
     * @return string 
     */
    public function getNickname()
    {
        return $this->nickname;
    }

    /**
     * Set realname
     *
     * @param string $realname
     * @return UserOauth
     */
    public function setRealname($realname)
    {
        $this->realname = $realname;
    
        return $this;
    }

    /**
     * Get realname
     *
     * @return string 
     */
    public function getRealname()
    {
        return $this->realname;
    }

    /**
     * Set email
     *
     * @param string $email
     * @return UserOauth
     */
    public function setEmail($email)
    {
        $this->email = $email;
    
        return $this;
    }

    /**
     * Get email
     *
     * @return string 
     */
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * Set profilePicture
     *
     * @param string $profilePicture
     * @return UserOauth
     */
    public function setProfilePicture($profilePicture)
    {
        $this->profilePicture = $profilePicture;
    
        return $this;
    }

    /**
     * Get profilePicture
     *
     * @return string 
     */
    public function getProfilePicture()
    {
        return $this->profilePicture;
    }

    /**
     * Set accessToken
     *
     * @param string $accessToken
     * @return UserOauth
     */
    public function setAccessToken($accessToken)
    {
        $this->accessToken = $accessToken;
    
        return $this;
    }

    /**
     * Get accessToken
     *
     * @return string 
     */
    public function getAccessToken()
    {
        return $this->accessToken;
    }

    /**
     * Set refreshToken
     *
     * @param string $refreshToken
     * @return UserOauth
     */
    public function setRefreshToken($refreshToken)
    {
        $this->refreshToken = $refreshToken;
    
        return $this;
    }

    /**
     * Get refreshToken
     *
     * @return string 
     */
    public function getRefreshToken()
    {
        return $this->refreshToken;
    }

    /**
     * Set tokenSecret
     *
     * @param string $tokenSecret
     * @return UserOauth
     */
    public function setTokenSecret($tokenSecret)
    {
        $this->tokenSecret = $tokenSecret;
    
        return $this;
    }

    /**
     * Get tokenSecret
     *
     * @return string 
     */
    public function getTokenSecret()
    {
        return $this->tokenSecret;
    }

    /**
     * Set expiresIn
     *
     * @param string $expiresIn
     * @return UserOauth
     */
    public function setExpiresIn($expiresIn)
    {
        $this->expiresIn = $expiresIn;
    
        return $this;
    }

    /**
     * Get expiresIn
     *
     * @return string 
     */
    public function getExpiresIn()
    {
        return $this->expiresIn;
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set user
     *
     * @param \Acme\MainBundle\Entity\User $user
     * @return UserOauth
     */
    public function setUser(\Acme\MainBundle\Entity\User $user = null)
    {
        $this->user = $user;
    
        return $this;
    }

    /**
     * Get user
     *
     * @return \Acme\MainBundle\Entity\User 
     */
    public function getUser()
    {
        return $this->user;
    }
}

Les 2 champs importants et obligatoires sont provider et providerId. Provider contiendra le nom du service (facebook ou twitter par exemple) et providerId sera l'identifiant unique de l'utilisateur sur le service externe.

 

Le controller

Le controller est la clé du succès pour faire le lien entre le firewall classique et le firewall oauth.
Ce controller devra sans doute être le même que celui qui vous permet aujourd'hui d'enregistré un utilisateur (même si ce n'est pas obligatoire, c'est plus simple)
Son fonctionnement est le suivant :

  • On reçoit une requête sur targetAction, l'utilisateur vient de se connecter via un service externe
  • On enregistre les informations de la classe OAuthUser en session Flash les infos de l'utilisateur externe
  • On le redirige vers oauthRegister (qui est derrière le firewall normal). Et là, plusieurs possibilités :
    • Le UserOauth n'existe pas et l'utilisateur n'est pas connecté : On l'envoie sur le formulaire d'inscription en enregistrant en session Flash les infos de l'utilisateur externe.
    • Le UserOauth n'existe pas et l'utilisateur est connecté : On enregistre ces infos en Base de données et on le redirige vers la home (il vient d'ajouter un nouveau service à son compte)
    • Le UserOauth existe : On le met à jour et on connecte l'utilisateur correspondant (il vient de se reconnecter)

Le point qui reste à éclairer est le formulaire d'inscription, qui est pour moi dans le même controller. C'est le seul point qui devra être repris sur un projet existant pour bien prendre en comtpe le Oauth.
Son fonctionnement est assez simple :

  • On vérifie si on a des informations en session Flash provenant de l'Oauth.
  • Si ce n'est pas le cas, on est dans une inscription classique et on ne fera rien de plus
  • Si c'est le cas, avant de créer le formulaire, on prérempli le champ email et username que l'on a peut-être provenant de l'Oauth
  • On réenregistre en session Flash les infos de l'oauth pour les avoir de nouveau au post du formulaire
  • Lorsque le formulaire est valide et que l'on peut créer l'utilisateur, on persiste aussi une ligne dans userOauth et le tour est joué

Et voici la classe :

// Acme/MainBundle/Controller/SecurityController.php
<?php 

namespace Acme\MainBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken,
	Symfony\Component\Security\Core\AuthenticationEvents,
	Symfony\Component\Security\Core\Event\AuthenticationEvent;

class SecurityController extends Controller {
	
	protected $oauthDataKey = 'oAuthData';
	
	public function oauthTargetAction() {
		$user = $this->getUser();
		$this->get('session')->getFlashBag()->set($this->oauthDataKey, $user->getData());
		return $this->redirect($this->generateUrl('oauth_register'));
	}
	
	public function oauthRegisterAction() {
		$oAuthData = $this->get('session')->getFlashBag()->get($this->oauthDataKey);
		
		if (!$oAuthData || !is_array($oAuthData) || !isset($oAuthData['provider']) || !isset($oAuthData['providerId']))
			return $this->redirect($this->generateUrl('hwi_oauth_connect'));
		
		// Search for userOauth
		$userOauth = $this->getDoctrine()->getRepository('AcmeMainBundle:UserOauth')->findOneBy(array(
			'provider'=>$oAuthData['provider'],
			'providerId'=>$oAuthData['providerId'],
		));
		if ($userOauth) {
			// We found it, update user oauth data
			$this->setUserOauthData($userOauth, $oAuthData);
			$this->getDoctrine()->getManager()->flush();
			
			// Log the user
			$user = $userOauth->getUser();
			$this->logUser($user);
		} else {
			$user = $this->getUser();
			if ($user && is_object($user)) {
				// User is already connected, just add the UserOauth
				$userOauth = $this->getNewUserOauth($user, $oAuthData);
				$this->getDoctrine()->getManager()->flush();
			} else {
				// Not logged and not existing, redirect to register page
				$this->get('session')->getFlashBag()->set($this->oauthDataKey, $oAuthData);
				return $this->redirect($this->generateUrl('register'));
			}
		}
		return $this->redirect($this->generateUrl('_homepage'));
	}
	
	/**
	 * Get a new UserOauth entity with persisting it
	 *
	 * @param \Acme\MainBundle\Entity\User $user
	 * @param array $oAuthData
	 * @return \Acme\MainBundle\Entity\UserOauth
	 */
	protected function getNewUserOauth($user, $oAuthData) {
		$userOauth = new \Acme\MainBundle\Entity\UserOauth();
		$userOauth->setUser($user);
		$this->setUserOauthData($userOauth, $oAuthData);
		$this->getDoctrine()->getManager()->persist($userOauth);
		return $userOauth;
	}
	
	protected function setUserOauthData($userOauth, $oAuthData) {
		foreach($oAuthData as $k=>$v) {
			$fct = 'set'.ucfirst($k);
			$userOauth->$fct($v);
		}
	}
	
	public function logUser(\Acme\MainBundle\Entity\User $user) {
		// Here, "main" is the name of the firewall in your security.yml
		$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
		$this->get('security.context')->setToken($token);

		// Fire the login event
		$this->get('event_dispatcher')->dispatch(AuthenticationEvents::AUTHENTICATION_SUCCESS, new AuthenticationEvent($token));
	}
	
	public function registerAction() {
		$user = new \Acme\MainBundle\Entity\User();
		
		$oAuthData = $this->get('session')->getFlashBag()->get($this->oauthDataKey);
		if ($oAuthData && is_array($oAuthData)) {
			if (isset($oAuthData['email']) && $oAuthData['email'])
				$user->setEmail($oAuthData['email']);
			if (isset($oAuthData['nickname']) && $oAuthData['nickname'])
				$user->setUsername($oAuthData['nickname']);
		} else {
			$oAuthData = false;
		}
		
		$form = $this->createFormBuilder($user)
					// Create your form normally
					->getForm();
		
		$form->handleRequest($this->getRequest());
		if ($form->isValid()) {
			// Handle the user normally, preparing for persistence
			$this->getDoctrine()->getManager()->persist($user);
			
			if ($oAuthData && is_array($oAuthData))
				$userOauth = $this->getNewUserOauth($user, $oAuthData);
				
			$this->getDoctrine()->getManager()->flush();
			$this->logUser($user);
			return $this->redirect($this->generateUrl('_homepage'));
		}
		
		// Keep oAuthData in flashbag
		if ($oAuthData)
			$this->get('session')->getFlashBag()->set($this->oauthDataKey, $oAuthData);
		
        return $this->render(
            'AcmeMainBundle:Security:register.html.php',
            array(
				'form'=>$form->createView()
			)
        );
	}
	
}

Et voilà, vous avez tout, en place. Il ne reste maintenant qu'à créer des liens vers les connexions paramétrées

 

Liens vers les connect

Pour créer les liens vers les différents services externes de connexion, il faut récupérer les existants, puis créer les bonnes URLs. Dans un controller, on peut faire ça très simplement :

protected function getOauthUrls() {
	$ret = array();
	foreach($this->get('hwi_oauth.security.oauth_utils')->getResourceOwners() as $name)
		$ret[$name] = $this->generateUrl('hwi_oauth_service_redirect', array('service'=>$name));
	return $ret;
}



Hé bien ce fut bien long... Mais après une absence de près d'un an et demi, il fallait au moins ça !

 

Ca vous a plus ? Des remarques ? Des questions ? Les commentaires sotn là pour ça.

Embed.ly and his so great customer service

You probably know embed.ly. It is a service which, as its name suggests it, lets embed external content on your own website pretty easily.

The idea of their API is simple: send a media URL of a sharing website (youtube, dailymotion, twitter, google maps, etc... and many others) and the API returns many informations about the media: author, inserted dat, and more important the HTML code to embed the content on your website such as flash player, iframe or others things regarding the needs.

In one word, a real advantage with big time savings for the developper who may propose in a few lines of code a huge number of content sharing sites.

 

Yeah ok, but why are these guys better than others?

I'm coming to it.

 

Since it'sversion 2, nyroModal uses embed.ly to display these kind of content directly on modal windows. From only youtube, my open-source project is passed to a multitide of différent video sites, as well as tweets, google maps, and so on...

So great!

the Embed.ly API was at this time free and without limits, requires no registration and no account on their website.

 

Then Embed.ly evolved. To monetiez their API, they set up various plans and prices. There is still a free API with a minimum of services - same as before - with a 10 000 queries limit per month.

In order to keep nyroModal working, I updated source code on September 17th. I created an API key on embed.ly and use it on my demo page

The commit on GitHub.

 

Ok good. But... nyroModal runs at an average of 1,000 visits per day. A rate of 3 urls that send request to embed.ly, we arrive at the limit in 3 days. Moreover, there is a embed.ly demo page with almost 170 demo links.

In his great goodness, embed.ly sent me an email on September 25 telling me that I came close to this limite (around 8,000). So I decided to place a PHP script to cache embed.ly queries to solve this problem.

On September 26, the commit was sent and I thougt I definitvely solved this problem. Some tests later, it's working great.

 

Good, and embed.ly is when they are friendly and the best of the world?

I'm there.

September, I've got an email telling me I exceeded the limit... Neither one nor two, I plunged back into the code, I analyze and I draw what I send to embed.ly and how I create the file name cache. In fact, jQuery adds a parameter to the query, simply named "_"  to not cache the response.

I immediately coorectedthe bug, commit it, set it up and test it again.

It does not work at all! The answers to embed. Ly are simply empty.

Normal, I exceeded the quota and have to go with a pay plan to continue using the API.

 

I read the email from embed.ly and see : "Please reply to this email with questions."

Without much hope, I write an email explaining the situation and that I did not pay because I do not make money with.

 

Then I click around on the Embed.ly dashboard, read their FAQ, prices, etc...

And only 9 minutes later, I received two answers from embed.ly employees:

Hey Cedric,

If you are just using the key as a demo for nyroModal, then there is no
reason for us to charge you. I've updated your account, but I ask that you
add the IPs that your proxy server is going to hit Embedly from here:
https://app.embed.ly/organizations/nyrodev--nyromodal/ip.

This way we can assure that the key is only being used by the demo site.

Thanks,

Sean

Sean just set my API key to a 50 000 queries par month. Free and without further requests for explanations. I quickly my server IP address as requested, and another answer from another employee came up:

Cédric,

I've reset your monthly counter. Enjoy my friend!

Bob Corsaro

Bob put my account of queries to zero for the current month. Exactly what I expected too.

And voilà. These people got it all. Where some companies would surely seek to charge or request futher explanattions, embed.ly, in 10 minutes solved my probelm and encourage me to make open-source:

We love it when people incorporate Embedly into open source projects and we are happy to help out the developers.

I'm confident that this flexibility in their management is because they are only 4 in the team. They can react quickly as they wish.

 

Finally, use embed.ly! This service works really rellay good and people behing it are at the top of the developper mountain.

Embed.ly, ou comment être au top

Vous connaissez sûrement embed.ly. C'est un service qui, comme son nom l'indique à tout anglo-développeur qui soit, permet d'incroporer des contenus externes sur son site facilement.

L'idée de leur API est simple : on envoie une URL d'un média sur un site de partage de contenu (youtube, dailymotion, twitter, google Maps, etc etc... et de nombreux autres) et l'API nous renvoie un maximum d'informations sur cette vidéo : auteur, date d'ajout, mais surtout, le code HTML pour embeder la vidéo sur son site ; c'est à dire e player Flash, l'iFrame ou autre selon les besoins.

Bref, un réel avantage et gain de temps pour le développeur qui peut proposer, en quelques lignes de code un nombre impressionnant de sites de partage de contenus.

 

Oui bon ok, mais en quoi ils sont géniaux ces gens ?

J'y viens.

 

Depuis sa version 2, nyroModal utilise embed.ly pour afficher les contenus de partage directement dans une modale. De simplement youtube auparavant, le projet open-source est donc passé à une multitude de sites de vidéos différents, mais aussi des tweets, des cartes google, etc...

Un must!

L'API d'Embed.ly était alors gratuite et sans aucune limite, ne nécessitant aucun inscription et compte chez eux.

 

Puis, Embed.ly a évolué. Pour monétiser leur API, ils ont mis en place différents plans et prix pour leur API. Il existe toujours une API libre avec un minimum de service - les mêmes qu'avant- avec une limite de 10 000 requêtes par mois.

Pour que nyroModal continue de fonctionner, j'ai donc mis à jour le code source le 17 septembre en créant une clé chez Embed.ly et en l'incluant sur ma page de démo.

Le commit en question sur GitHub.

 

Oui mais voilà. nyroModal tourne à une moyenne de 1 000 visites par jours. A raison de 3 urls qui envoient des requêtes vers embed.ly, on arrive à la fameuse limite gratuite de 10 000 en 3 jours. Qui plus est, sur la page de démo spéciale embed.ly, on a quelques 170 liens de démo.

 

Dans sa grande bonté, embed.ly m'a envoyé un email le 25 septembre m'indiquant que j'arrivais près de cette limite (environ 8 000). J'ai donc décider de mettre en place un cache en PHP des requêtes faites à embed.ly pour régler le problème.

Le 26 septembre, le commit était envoyé et je pensais avori réglé le problème et être tranquille de ce côté là. Quelques tests, ça marche.

 

Bon, et embed.ly, c'est à quel moment qu'ils sont sympa et les best of the world ?

J'y suis.

Le 27 septembre, je reçois un nouvel email m'indiquant que j'ai dépassé la limite. Ni une ni deux, je replonge dans le code, j'analyse et je trace ce que j'envois à embed.ly et comment je créer le nom du fichier du cache. En réalité, jQuery ajoute un paramètre à la requête, nommé simplement "_" qui permet de ne pas mettre en cache la réponse.

Je corrige immédiatement le bug, le commit, le met en place et teste à nouveau.

Ça ne marche plus du tout ! Les réponses d'embed.ly sont simplement vide.

Normal, j'ai dépassé le quota et dois passer à un plan payant pour continuer à utiliser l'API.

 

Je relis l'email d'embed.ly et je vois : "Please reply to this email with questions."

Sans trop d'espoirs, j'écris un email leur expliquant la situation et que je ne voulais pas payer car je ne gagne pas d'argent avec.

 

Puis je me balade sur le dashboard d'Embed.ly, à lire leur FAQ, leurs prix, etc...

Et seulement 9 minutes plus tard, je reçois 2 réponses d'employés d'embed.ly.

Hey Cedric,

If you are just using the key as a demo for nyroModal, then there is no
reason for us to charge you. I've updated your account, but I ask that you
add the IPs that your proxy server is going to hit Embedly from here:
https://app.embed.ly/organizations/nyrodev--nyromodal/ip.

This way we can assure that the key is only being used by the demo site.

Thanks,

Sean

Ce Sean a simplement passé mon abonnement à 50 000 requêtes par mois. Gratuitement et sans autres demandes d'explications. Je m'exécute à ajouter l'adresse IP de mon serveur comme demandée, et une autre réponse d'un autre employé :

Cédric,

I've reset your monthly counter. Enjoy my friend!

Bob Corsaro

Lui a remis mon compte à zéro pour le mois en cours. Niquel aussi.

Alors voilà. Ces gens ont tout compris. Là où certaines entreprises auraient sûrement chercher à faire payer ou demander plus d'explications, recherche un peu plus que ça, eux ont, en 10 minutes seulement, résolu mon problème et même encourager à faire de l'open-source :

We love it when people incorporate Embedly into open source projects and we are happy to help out the developers.

Je suis persuadé que cette souplesse dans leur gestion est dû au fait qu'il ne soit que 4 dans l'équipe. Ils peuvent réagir rapidement et comme ils le veulent.

 

Pour conclure, utilisez embed.ly ! Ce service fonctionne du feu de dieu et les gens qui sont derrière sont au top.

The return...

Last post: october 2010. (and maybe earlier in English...)

This has to stop! nyroBlog come back in a new version, "powered by nyroFwk".

Presentation stays the same but it should be faster.

 

New features in this version:

  • Server-side optimisations to cache MySQL and views (nyroFwk native)
  • Client-side optimisations (YSlow and PageSpeed) (natif nyroFwk)
  • Spam control with Akismet (usage of a native nyroFwk class)
  • Indexing of posts with Zend_Search_Lucene (might be included soon in nyroFwk)

New it's up to me to write new posts about PHP, frameworks, JavaScript, jQuery, and so one...

 

If you see anything wrong or some bugs in this new version, don't hesitate to tell me!

Le retour...

Dernier billet : octobre 2010.

Cela n'a que trop duré ! nyroBlog revient dans une nouvelle version "powered by nyroFwk".

La présentation reste la même, mais il devrait être plus rapide.

 

Les nouveautés de cette version :

  • Optimisations côté serveur avec du cache MySQL et vues (natif nyroFwk)
  • Optimisations côté client (YSlow et PageSpeed) (natif nyroFwk)
  • Contrôle des spams avec Akismet (utilisation classe native nyroFwk)
  • Indexation des billets avec un Zend_Search_Lucene (probablement bientôt inclus dans nyroFwk)

Maintenant, à moi de me remettre à écrire des billets à propos de PHP, de framework, de JavaScript, jQuery, etc...

 

Si vous voyez des bugs ou des choses étranges sur ce blog, n'hésitez pas !