Just Married: Zend Framework & Flickr
Dienstag, den 9. März 2010Vorweg: Ich glaube dieser Post wird lang…
Also zur Zeit bastel ich an einem Projekt, das aus einigen Social Networks Daten beziehen und diese auf Public Displays anzeigen soll. Da nur-Text etwas oede ist, habe ich mich also an die Flickr API gesetzt. Eigentlich bietet das Zend Framework dafuer die Klasse Zend_Service_Flickr an, aber leider ist die nicht dafuer ausgelegt Applikationen mit Flickr zu verbinden, sondern lediglich fuer eine direkte Benutzerauthentifizierung via Benutzername/Passwort.
Mein erster Ansatz war also erstmal in die API von Flickr zu schauen, denn die bietet ein OAuth aehnliches Verfahren zur Anwendungsauthentifizierung an. Der Trick war nun Zend_Service_Flickr damit zu verheiraten. Und so geht’s:
Als erstes registriert man eine Anwendung bei Flickr und richtet eine Callback URL ein. In meinem Fall wird damit die callbackAction meines FlickrControllers getriggert. Ich brauchte also einen FlickrController…
class FlickrController extends Zend_Controller_Action { public function indexAction() { } public function authenticateAction() { } public function callbackAction() { } }
Die Authentifizierung laeuft so ab:
- Der Benutzer oeffnet die index-Seite. Dort ist ein Link zur authenticate-Seite, wenn alles nicht schon gelaufen ist.
- Die authenticate-Action prueft zunaechst, ob die Anwendung bereits mit Flickr verknuepft ist. Wenn ja, dann wird zur index-Seite weitergeleitet. Wenn nein, dann wird eine Authentication Request URL gebaut und dorthin wird weitergeleitet.
- Auf der Flickr-Seite muss der Benutzer der Anwendung nun den Zugriff erlauben. Ist das geschehen, wird der Benutzer von Flickr direkt zur callback-Seite geleitet. Flickr haengt dabei noch eine Variable mit dem Namen frob an.
- Die callback-Action sendet daraufhin eine Anfrage an Flickr, die u.A. auch die frob-Variable enthaelt. Ausserdem wird eine Signatur angehangen.
- Flickr antwortet mit einem Token, der dann gespeichert wird und fuer zukuenftige Calls verwendet wird.
So, bevor ich alle mit Erklaerungen langweile: Jetzt kommt Code!
class SenScreen_Service_Flickr extends Zend_Service_Flickr { protected $_secret; protected $_perms; protected $_token; const PATH_AUTH = '/services/auth/'; const PATH_REST = '/services/rest/'; public function __construct(array $options = null) { parent::__construct(null); $this->setOptions($options); } public function setOptions(array $options = null) { if ($options == null) { return; } $validOptions = array('api_key', 'secret', 'perms', 'token'); $this->_compareOptions($options, $validOptions); if (isset($options['api_key'])) { $this->apiKey = $options['api_key']; } if (isset($options['secret'])) { $this->_secret = $options['secret']; } if (isset($options['perms'])) { $this->_perms = $options['perms']; } else { $this->_perms = 'read'; } if (isset($options['token']) && $options['token'] instanceof SenScreen_Service_Flickr_AuthToken) { $this->_token = $options['token']; } } public function setApiKey($apiKey) { $this->apiKey = $apiKey; return $this; } public function getApiKey() { return $this->apiKey; } public function setSecret($secret) { $this->_secret = $secret; return $this; } public function getSecret($secret) { return $this->_secret; } public function setPerms($perms) { $this->_perms = $perms; return $this; } public function getPerms() { return $this->_perms; } public function setToken($token) { $this->_token = $token; return $this; } public function getAuthToken() { return $this->_token; } public function getApiSignature(array $params) { $plainSignature = $this->_secret; ksort($params); foreach ($params as $key => $value) { $plainSignature .= $key . $value; } return md5($plainSignature); } private function _getSignedUrl($path, array $params) { $url = self::URI_BASE . $path . '?'; $urlParams = array(); foreach ($params as $key => $value) { $urlParams[] = $key . '=' . $value; } $url .= implode('&', $urlParams); $url .= '&api_sig=' . $this->getApiSignature($params); return $url; } public function redirect() { $params = array( 'api_key' => $this->apiKey, 'perms' => $this->_perms ); $redirectUrl = $this->_getSignedUrl(self::PATH_AUTH, $params); header('Location: ' . $redirectUrl); exit(1); } public function getFrob() { return $_GET['frob']; } public function requestToken() { $params = array( 'method' => 'flickr.auth.getToken', 'api_key' => $this->apiKey, 'frob' => $this->getFrob(), 'perms' => $this->_perms ); $httpClient = new Zend_Http_Client($this->_getSignedUrl(self::PATH_REST, $params)); $httpClient->resetParameters(); $httpClient->setParameterGet($params); $httpClient->setHeaders(array( 'Accept-encoding' => '' )); return $this->_parseToken($httpClient->request('GET')); } private function _parseToken(Zend_Http_Response $response) { $xmlElement = new SimpleXMLElement($response->getBody()); return new SenScreen_Service_Flickr_AuthToken( (string) $xmlElement->auth->token, (string) $xmlElement->auth->user['username'], (string) $xmlElement->auth->user['fullname'] ); } public function isRestrictedMethod($method) { $restrictedMethods = array( 'flickr.photos.getContactsPhotos' ); return in_array($method, $restrictedMethods); } private function _execute($method, $defaultOptions, $options) { $options = $this->_prepareOptions($method, $options, $defaultOptions); $restClient = $this->getRestClient(); $restClient->getHttpClient()->resetParameters(); $response = $restClient->restGet('/services/rest/', $options); if ($response->isError()) { throw new Zend_Service_Exception('An error occurred sending request. Status code: ' . $response->getStatus()); } $dom = new DOMDocument(); $dom->loadXML($response->getBody()); self::_checkErrors($dom); return new Zend_Service_Flickr_ResultSet($dom, $this); } public function getContactsPhotos(array $options = array()) { static $method = 'flickr.photos.getContactsPhotos'; static $defaultOptions = array( 'count' => 100, 'just_friends' => 0, 'single_photo' => 0, 'include_self' => 0, 'extras' => 'license, date_upload, date_taken, owner_name, icon_server, original_format, last_update.' ); return $this->_execute($method, $defaultOptions, $options); } public function getRecent() { static $method = 'flickr.photos.getRecent'; static $defaultOptions = array( 'per_page' => 10, 'page' => 1, 'extras' => 'license, date_upload, date_taken, owner_name, icon_server, original_format, last_update.' ); return $this->_execute($method, $defaultOptions); } }
Diese Klasse erweitert Zend_Service_Flickr, sodass diese auch mit Authentifizierung umgehen kann. Als naechstes muss der Zend_Http_Client ausgetauscht (bzw. erweitert) werden, sodass auch er mit Signaturen und dem Kram umgehen kann:
class SenScreen_Service_Flickr_Http_Client extends Zend_Http_Client { protected $_flickr; public function __construct() { parent::__construct(); $this->setHeaders(array('Accept-Encoding' => '')); } public function setFlickr(SenScreen_Service_Flickr $flickr) { $this->_flickr = $flickr; } public function request($method = null) { if ($this->_flickr->isRestrictedMethod($this->paramsGet['method'])) { $this->paramsGet['auth_token'] = $this->_flickr->getAuthToken()->getToken(); $this->paramsGet = array_merge($this->paramsGet, array( 'api_sig' => $this->_flickr->getApiSignature($this->paramsGet) )); } return parent::request($method); } }
Als naechstes Brauchen wir noch eine Klasse, die unser Token aufbewahrt und serialisierbar in der DB ablegbar ist:
class SenScreen_Service_Flickr_AuthToken { private $_token; private $_username; private $_fullname; public function __construct($token, $username, $fullname) { $this->_token = $token; $this->_username = $username; $this->_fullname = $fullname; } public function getToken() { return $this->_token; } public function getUsername() { return $this->_username; } public function getFullname() { return $this->_fullname; } }
Und dann koennen wir endlich den Controller fuellen und jetzt kommen auch mal Kommentare in den Code, damit ich nicht mehr so viel quarken muss:
class FlickrController extends Zend_Controller_Action { protected $_config = array( 'api_key' => 'myapplicationkey', 'secret' => 'mysecret' ); protected $_flickr; public function init() { $this->_flickr = new SenScreen_Service_Flickr(); $httpClient = new SenScreen_Service_Flickr_Http_Client(); $httpClient->setFlickr($this->_flickr); Zend_Rest_Client::setHttpClient($httpClient); } public function indexAction() { $userId = Zend_Auth::getInstance()->getIdentity()->id; /** * Check whether the current user has an access * token for Flickr. */ $userSettingModel = new Model_UserSetting(); if ($userSettingModel->get($userId, 'flickr_token')) { /** * Get the access token. */ $access_token = unserialize($userSettingModel->getValue()); /** * Configure the Flickr instance. */ $this->_flickr->setOptions(array_merge( $this->_config, array('token' => $access_token) )); $this->view->contactPhotos = $this->_flickr->getContactsPhotos(array('include_self' => 1, 'extras' => '')); $this->view->showAuthenticationButton = false; } else { $this->view->showAuthenticationButton = true; } } public function authenticateAction() { $userId = Zend_Auth::getInstance()->getIdentity()->id; /** * Check whether the current user has an access * token for Flickr. */ $userSettingModel = new Model_UserSetting(); if ($userSettingModel->get($userId, 'flickr_token')) { /** * Get the access token. */ $this->_helper->redirector('index'); } else { /** * The user is not authenticated with Flickr. * Request Flickr auth frob and redirect to * Flickr's authentication page. */ $this->_flickr->setOptions($this->_config); $this->_flickr->redirect(); } } public function callbackAction() { $userId = Zend_Auth::getInstance()->getIdentity()->id; $this->_flickr->setOptions($this->_config); /** * Check whether a frob is present for this user. */ if ($this->_flickr->getFrob()) { /** * A frob is present. Request a token and * save it in the database. */ $token = $this->_flickr->requestToken(); $userSettingModel = new Model_UserSetting(); $userSettingModel->setUserId($userId) ->setKey('flickr_token') ->setValue(serialize($token)) ->save(); /** * Everything is done, redirect to the index * page. */ $this->_helper->redirector('index', 'flickr'); } else { /** * The callback has been called without having * a request token. Redirect to the index page. */ $this->_helper->redirector('index', 'flickr'); } } }
Sollte soweit selbsterklaerend sein. Viel Spass damit.
Update:
Wie ihr vielleicht bemerkt habt, nutze ich das Abstract Data Mapper Design Pattern.









