portfolio: MDL-16417 - Adding googledocs/picasa plugins
authorpoltawski <poltawski>
Sat, 29 Nov 2008 20:24:25 +0000 (20:24 +0000)
committerpoltawski <poltawski>
Sat, 29 Nov 2008 20:24:25 +0000 (20:24 +0000)
* Added simple library for googleapi things
* Respository plugins to follow

lang/en_utf8/portfolio_googledocs.php [new file with mode: 0644]
lang/en_utf8/portfolio_picasa.php [new file with mode: 0644]
lib/googleapi.php [new file with mode: 0644]
portfolio/type/googledocs/db/events.php [new file with mode: 0644]
portfolio/type/googledocs/lib.php [new file with mode: 0644]
portfolio/type/googledocs/version.php [new file with mode: 0644]
portfolio/type/picasa/db/events.php [new file with mode: 0644]
portfolio/type/picasa/lib.php [new file with mode: 0644]
portfolio/type/picasa/version.php [new file with mode: 0644]

diff --git a/lang/en_utf8/portfolio_googledocs.php b/lang/en_utf8/portfolio_googledocs.php
new file mode 100644 (file)
index 0000000..488e826
--- /dev/null
@@ -0,0 +1,6 @@
+<?php
+$string['pluginname'] = 'Google Docs';
+$string['noauthtoken'] = 'An authentication token has not been recieved from google. Please ensure you are allowing moodle to access your google account';
+$string['nosessiontoken'] = 'A session token does not exist preventing export to google.';
+$string['sendfailed'] = 'The file $a failed to transfer to google';
+?>
diff --git a/lang/en_utf8/portfolio_picasa.php b/lang/en_utf8/portfolio_picasa.php
new file mode 100644 (file)
index 0000000..df72d5c
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+$string['pluginname'] = 'Picasa';
+$string['noauthtoken'] = 'An authentication token has not been recieved from google. Please ensure you are allowing moodle to access your google account';
+$string['sendfailed'] = 'The file $a failed to transfer to picasa';
+?>
diff --git a/lib/googleapi.php b/lib/googleapi.php
new file mode 100644 (file)
index 0000000..69ba61a
--- /dev/null
@@ -0,0 +1,381 @@
+<?php // $Id$
+/**
+ * Moodle - Modular Object-Oriented Dynamic Learning Environment
+ *          http://moodle.org
+ * Copyright (C) 1999 onwards Martin Dougiamas  http://dougiamas.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @package    moodle
+ * @subpackage lib
+ * @author     Dan Poltawski <talktodan@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
+ *
+ * Simple implementation of some Google API functions for Moodle.
+ */
+
+require_once($CFG->libdir.'/filelib.php');
+
+/**
+ * Base class for google authenticated http requests 
+ * 
+ * Most Google API Calls required that requests are sent with an 
+ * Authorization header + token. This class extends the curl class
+ * to aid this
+ */
+abstract class google_auth_request extends curl{
+    protected $token = '';
+
+    // Must be overriden with the authorization header name
+    public abstract static function get_auth_header_name();
+
+    protected function request($url, $options = array()){
+        if($this->token){
+            // Adds authorisation head to a request so that it can be authentcated
+            $this->setHeader('Authorization: '. $this->get_auth_header_name().'"'.$this->token.'"');
+        }
+
+        $ret = parent::request($url, $options);
+        // reset headers for next request
+        $this->header = array();
+        return $ret;
+    }
+
+    public function get_sessiontoken(){
+        return $this->token;
+    }
+}
+
+/*******
+ * The following two classes are usd to implement AuthSub google 
+ * authtentication, as documented here:
+ * http://code.google.com/apis/accounts/docs/AuthSub.html
+ *******/
+
+/**
+ * Used to uprade a google AuthSubRequest one-time token into
+ * a session token which can be used long term.
+ */
+class google_authsub_request extends google_auth_request {
+    const AUTHSESSION_URL = 'https://www.google.com/accounts/AuthSubSessionToken';
+
+    /**
+     * Constructor. Calls constructor of its parents 
+     *
+     * @param string $authtoken The token to upgrade to a session token
+     */
+    public function __construct($authtoken){
+        parent::__construct();
+        $this->token = $authtoken;
+    }
+
+    /**
+     * Requests a long-term session token from google based on the 
+     *
+     * @return string Sub-Auth token 
+     */
+    public function get_session_token(){
+        $content = $this->get(google_authsub_request::AUTHSESSION_URL);
+
+        if( preg_match('/token=(.*)/i', $content, $matches) ){
+            return $matches[1];
+        }else{
+            throw new moodle_exception('could not upgrade google authtoken to session token');
+        }
+    }
+
+    public static function get_auth_header_name(){
+        return 'AuthSub token=';
+    }
+}
+
+/**
+ * Allows http calls using google subauth authorisation
+ */
+class google_authsub extends google_auth_request {
+    const LOGINAUTH_URL    = 'https://www.google.com/accounts/AuthSubRequest';
+    const VERIFY_TOKEN_URL = 'https://www.google.com/accounts/AuthSubTokenInfo';
+    const REVOKE_TOKEN_URL = 'https://www.google.com/accounts/AuthSubRevokeToken';
+
+    /**
+     * Constructor, allows subauth requests using the response from an initial 
+     * AuthSubRequest or with the subauth long-term token. Note that constructing
+     * this object without a valid token will cause an exception to be thrown.
+     *
+     * @param string $sessiontoken A long-term subauth session token
+     * @param string $authtoken A one-time auth token wich is used to upgrade to session token
+     * @param mixed  @options Options to pass to the base curl object
+     */
+    public function __construct($sessiontoken = '', $authtoken = '', $options = array()){
+        parent::__construct($options);
+
+        if( $authtoken ){
+            $gauth = new google_authsub_request($authtoken);
+            $sessiontoken = $gauth->get_session_token();
+        }
+
+        $this->token = $sessiontoken;
+        if(! $this->valid_token() ){
+            throw new moodle_exception('Invalid subauth token');
+        }
+    }
+
+    /**
+     * Tests if a subauth token used is valid
+     *
+     * @return boolean true if token valid
+     */
+    public function valid_token(){
+        $this->get(google_authsub::VERIFY_TOKEN_URL);
+
+        if($this->info['http_code'] === 200){
+            return true;
+        }else{
+            return false;
+        }
+    }
+
+    /**
+     * Calls googles api to revoke the subauth token
+     *
+     * @return boolean Returns true if token succesfully revoked
+     */
+    public function revoke_session_token(){
+        $this->get(google_authsub::REVOKE_TOKEN_URL);
+
+        if($this->info['http_code'] === 200){
+            $this->token = '';
+            return true;
+        }else{
+            return false;
+        }
+    }
+
+    /**
+     * Creates a login url for subauth request
+     *
+     * @param string $returnaddr The address which the user should be redirected to recieve the token
+     * @param string $realm The google realm which is access is being requested 
+     * @return string URL to bounce the user to
+     */
+    public static function login_url($returnaddr, $realm){
+        $uri = google_authsub::LOGINAUTH_URL.'?next='
+            .urlencode($returnaddr)
+            .'&scope='
+            .urlencode($realm)
+            .'&session=1&secure=0';
+
+        return $uri;
+    }
+
+    public static function get_auth_header_name(){
+        return 'AuthSub token=';
+    }
+}
+
+/**
+ * Class for manipulating google documents through the google data api
+ * Docs for this can be found here:
+ * http://code.google.com/apis/documents/docs/2.0/developers_guide_protocol.html
+ */
+class google_docs {
+    const REALM            = 'http://docs.google.com/feeds/documents';
+    const DOCUMENTFEED_URL = 'http://docs.google.com/feeds/documents/private/full';
+    const USER_PREF_NAME   = 'google_authsub_sesskey';
+
+    private $google_curl = null;
+
+    /**
+     * Constructor.
+     *
+     * @param object A google_auth_request object which can be used to do http requests
+     */
+    public function __construct($google_curl){
+        if(is_a($google_curl, 'google_auth_request')){
+            $this->google_curl = $google_curl;
+        }else{
+            throw new moodle_exception('Google Curl Request object not given');
+        }
+    }
+
+    public static function get_sesskey($userid){
+        return get_user_preferences(google_docs::USER_PREF_NAME, false, $userid);
+    }
+
+    public static function set_sesskey($value, $userid){
+        return set_user_preference(google_docs::USER_PREF_NAME, $value, $userid);
+    }
+
+    public static function delete_sesskey($userid){
+        return unset_user_preference(google_docs::USER_PREF_NAME, $userid);
+    }
+
+    /**
+     * Returns a list of files the user has formated for files api
+     *
+     * @param string $search A search string to do full text search on the documents
+     * @return mixed Array of files formated for fileapoi
+     */
+    #FIXME
+    public function get_file_list($search = ''){
+        $url = google_docs::DOCUMENTFEED_URL;
+
+        if($search){
+            $url.='?q='.urlencode($search);
+        }
+        $content = $this->google_curl->get($url);
+
+        $xml = new SimpleXMLElement($content);
+
+        $files = array();
+        foreach($xml->entry as $gdoc){
+
+            $files[] =  array( 'title' => "$gdoc->title",
+                'url'  =>  "{$gdoc->content['src']}",
+                'source' => "{$gdoc->content['src']}",
+                'date'   => usertime(strtotime($gdoc->updated)),
+            );
+        }
+
+        return $files;
+    }
+
+    /**
+     * Sends a file object to google documents
+     *
+     * @param object $file File object
+     * @return boolean True on success
+     */
+    public function send_file($file){
+        $this->google_curl->setHeader("Content-Length: ". $file->get_filesize());
+        $this->google_curl->setHeader("Content-Type: ". $file->get_mimetype());
+        $this->google_curl->setHeader("Slug: ". $file->get_filename());
+
+        $this->google_curl->post(google_docs::DOCUMENTFEED_URL, $file->get_content());
+
+        if($this->google_curl->info['http_code'] === 201){
+            return true;
+        }else{
+            return false;
+        }
+    }
+
+    public function download_file($url, $fp){
+        return $this->google_curl->download(array( array('url'=>$url, 'file' => $fp) ));
+    }
+}
+
+/**
+ * Class for manipulating picasa through the google data api
+ * Docs for this can be found here:
+ * http://code.google.com/apis/picasaweb/developers_guide_protocol.html
+ */
+class google_picasa {
+    const REALM             = 'http://picasaweb.google.com/data/';
+    const USER_PREF_NAME    = 'google_authsub_sesskey_picasa';
+    const UPLOAD_LOCATION   = 'http://picasaweb.google.com/data/feed/api/user/default/albumid/default';
+
+    private $google_curl = null;
+
+    /**
+     * Constructor.
+     *
+     * @param object A google_auth_request object which can be used to do http requests
+     */
+    public function __construct($google_curl){
+        if(is_a($google_curl, 'google_auth_request')){
+            $this->google_curl = $google_curl;
+        }else{
+            throw new moodle_exception('Google Curl Request object not given');
+        }
+    }
+
+    public static function get_sesskey($userid){
+        return get_user_preferences(google_picasa::USER_PREF_NAME, false, $userid);
+    }
+
+    public static function set_sesskey($value, $userid){
+        return set_user_preference(google_picasa::USER_PREF_NAME, $value, $userid);
+    }
+
+    public static function delete_sesskey($userid){
+        return unset_user_preference(google_picasa::USER_PREF_NAME, $userid);
+    }
+
+    /**
+     * Sends a file object to picasaweb
+     *
+     * @param object $file File object
+     * @return boolean True on success
+     */
+    public function send_file($file){
+        $this->google_curl->setHeader("Content-Length: ". $file->get_filesize());
+        $this->google_curl->setHeader("Content-Type: ". $file->get_mimetype());
+        $this->google_curl->setHeader("Slug: ". $file->get_filename());
+
+        $this->google_curl->post(google_picasa::UPLOAD_LOCATION, $file->get_content());
+
+        if($this->google_curl->info['http_code'] === 201){
+            return true;
+        }else{
+            return false;
+        }
+    }
+}
+
+/**
+ * Beginings of an implementation of Clientogin authenticaton for google 
+ * accounts as documented here:
+ * http://code.google.com/apis/accounts/docs/AuthForInstalledApps.html#ClientLogin
+ *
+ * With this authentication we have to accept a username and password and to post 
+ * it to google. Retrieving a token for use afterwards.
+ */
+class google_authclient extends google_auth_request {
+    const LOGIN_URL = 'https://www.google.com/accounts/ClientLogin';
+
+    public function __construct($sessiontoken = '', $username = '', $password = '', $options = array() ){
+        parent::__construct($options);
+
+        if($username and $password){
+            $param =  array(
+                'accountType'=>'GOOGLE',
+                'Email'=>$username,
+                'Passwd'=>$password,
+                'service'=>'writely'
+            );
+
+            $content = $this->post(google_authclient::LOGIN_URL, $param);
+
+            if( preg_match('/auth=(.*)/i', $content, $matches) ){
+                $sessiontoken = $matches[1];
+            }else{
+                throw new moodle_exception('could not upgrade authtoken');
+            }
+
+        }
+
+        if($sessiontoken){
+            $this->token = $sessiontoken;
+        }else{
+            throw new moodle_exception('no session token specified');
+        }
+    }
+
+    public static function get_auth_header_name(){
+        return 'GoogleLogin auth=';
+    }
+}
+
+?>
diff --git a/portfolio/type/googledocs/db/events.php b/portfolio/type/googledocs/db/events.php
new file mode 100644 (file)
index 0000000..181dc1d
--- /dev/null
@@ -0,0 +1,11 @@
+<?php // $Id$
+
+$handlers = array (
+    'user_deleted' => array (
+         'handlerfile'      => '/portfolio/type/googledocs/lib.php',
+         'handlerfunction'  => 'portfolio_googledocs_user_deleted', 
+         'schedule'         => 'cron'
+     ),
+);
+
+?>
diff --git a/portfolio/type/googledocs/lib.php b/portfolio/type/googledocs/lib.php
new file mode 100644 (file)
index 0000000..226c6ba
--- /dev/null
@@ -0,0 +1,114 @@
+<?php 
+/**
+ * Google Documents Portfolio Plugin
+ *
+ * @author Dan Poltawski <talktodan@gmail.com>
+ * @version $Id$
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+require_once($CFG->libdir.'/googleapi.php');
+
+class portfolio_plugin_googledocs extends portfolio_plugin_push_base {
+    private $sessiontoken;
+
+    public static function supported_formats() {
+        return array(PORTFOLIO_FORMAT_PLAINHTML, PORTFOLIO_FORMAT_IMAGE, PORTFOLIO_FORMAT_TEXT);
+    }
+
+    public static function get_name() {
+        return get_string('pluginname', 'portfolio_googledocs');
+    }
+
+    public function prepare_package() {
+        // we send the files as they are, no prep required
+        return true; 
+    }
+       
+    public function get_continue_url(){
+        return 'http://docs.google.com/';
+    }
+
+    public function expected_time($callertime) {
+        // we trust what the portfolio says
+        return $callertime;
+    }
+
+    public function send_package() {
+
+        if(!$this->sessiontoken){
+            throw new portfolio_plugin_exception('nosessiontoken', 'portfolio_googledocs');
+        }
+
+        $gdocs = new google_docs(new google_authsub($this->sessiontoken));
+
+        foreach ($this->exporter->get_tempfiles() as $file) {
+            if(!$gdocs->send_file($file)){
+                throw new portfolio_plugin_exception('sendfailed', 'portfolio_gdocs', $file->get_filename());
+            }
+        }
+    }
+
+    public function steal_control($stage) {
+        global $CFG;
+        if ($stage != PORTFOLIO_STAGE_CONFIG) {
+            return false;
+        }
+
+        $sesskey = google_docs::get_sesskey($this->get('user')->id);
+
+        if($sesskey){
+            try{
+                $gauth = new google_authsub($sesskey);
+                $this->sessiontoken = $sesskey;
+                return false;
+            }catch(Exception $e){
+                // sesskey is not valid, delete store and re-auth
+                google_docs::delete_sesskey($this->get('user')->id);
+            }
+        }
+
+        return google_authsub::login_url($CFG->wwwroot.'/portfolio/add.php?postcontrol=1', google_docs::REALM);
+    }
+
+    public function post_control($stage, $params) {
+        if ($stage != PORTFOLIO_STAGE_CONFIG) {
+            return;
+        }
+
+        if(!array_key_exists('token', $params)){
+            throw new portfolio_plugin_exception('noauthtoken', 'portfolio_googledocs');
+        }
+
+        // we now have our auth token, get a session token..
+        $gauth = new google_authsub(false, $params['token']);
+        $this->sessiontoken = $gauth->get_sessiontoken();
+
+        google_docs::set_sesskey($this->sessiontoken, $this->get('user')->id);
+    }
+
+}
+
+/**
+ * Registers to the user_deleted event to revoke any 
+ * subauth tokens we have from them
+ *
+ * @param $user user object
+ * @return boolean true in all cases as its only minor cleanup
+ */
+function portfolio_googledocs_user_deleted($user){
+    // it is only by luck that the user prefstill exists now?
+    // We probably need a pre-delete event?
+    if($sesskey = google_docs::get_sesskey($user->id)){
+        try{
+            $gauth = new google_authsub($sesskey);
+
+            $gauth->revoke_session_token();
+        }catch(Exception $e){
+            // we don't care that much about success- just being good
+            // google api citzens
+            return true;
+        }
+    }
+
+    return true; 
+}
diff --git a/portfolio/type/googledocs/version.php b/portfolio/type/googledocs/version.php
new file mode 100644 (file)
index 0000000..cd8312d
--- /dev/null
@@ -0,0 +1,6 @@
+<?php // $Id$
+
+$plugin->version  = 2008072505;
+$plugin->requires = 2008072500;
+$plugin->cron     = 0;
+?>
diff --git a/portfolio/type/picasa/db/events.php b/portfolio/type/picasa/db/events.php
new file mode 100644 (file)
index 0000000..dd5e280
--- /dev/null
@@ -0,0 +1,11 @@
+<?php // $Id$
+
+$handlers = array (
+    'user_deleted' => array (
+         'handlerfile'      => '/portfolio/type/picasa/lib.php',
+         'handlerfunction'  => 'portfolio_picasa_user_deleted', 
+         'schedule'         => 'cron'
+     ),
+);
+
+?>
diff --git a/portfolio/type/picasa/lib.php b/portfolio/type/picasa/lib.php
new file mode 100644 (file)
index 0000000..75e3c1c
--- /dev/null
@@ -0,0 +1,115 @@
+<?php 
+/**
+ * Picasa Portfolio Plugin
+ *
+ * @author Dan Poltawski <talktodan@gmail.com>
+ * @version $Id$
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+require_once($CFG->libdir.'/googleapi.php');
+
+class portfolio_plugin_picasa extends portfolio_plugin_push_base {
+    private $sessionkey;
+
+    public static function supported_formats() {
+        return array(PORTFOLIO_FORMAT_IMAGE, PORTFOLIO_FORMAT_VIDEO);
+    }
+
+    public static function get_name() {
+        return get_string('pluginname', 'portfolio_picasa');
+    }
+
+    public function prepare_package() {
+        // we send the files as they are, no prep required
+        return true; 
+    }
+       
+    public function get_continue_url(){
+        return 'http://picasaweb.google.com/';
+    }
+
+    public function expected_time($callertime) {
+        return $callertime;
+    }
+
+    public function send_package() {
+        if(!$this->sessionkey){
+            throw new portfolio_plugin_exception('noauthtoken', 'portfolio_picasa');
+        }
+
+        $picasa = new google_picasa(new google_authsub($this->sessionkey));
+
+        foreach ($this->exporter->get_tempfiles() as $file) {
+
+            if(!$picasa->send_file($file)){
+                throw new portfolio_plugin_exception('sendfailed', 'portfolio_picasa', $file->get_filename());
+            }
+        }
+    }
+
+    public function steal_control($stage) {
+        global $CFG;
+        if ($stage != PORTFOLIO_STAGE_CONFIG) {
+            return false;
+        }
+
+        $sesskey = google_picasa::get_sesskey($this->get('user')->id);
+
+        if($sesskey){
+            try{
+                $gauth = new google_authsub($sesskey);
+                $this->sessionkey = $sesskey;
+                return false;
+            }catch(Exception $e){
+                // sesskey is not valid, delete store and re-auth
+                google_picasa::delete_sesskey($this->get('user')->id);
+            }
+        }
+
+        return google_authsub::login_url($CFG->wwwroot.'/portfolio/add.php?postcontrol=1', google_picasa::REALM);
+    }
+
+    public function post_control($stage, $params) {
+        if ($stage != PORTFOLIO_STAGE_CONFIG) {
+            return;
+        }
+
+        if(!array_key_exists('token', $params)){
+            throw new portfolio_plugin_exception('noauthtoken', 'portfolio_picasa');
+        }
+
+        // we now have our auth token, get a session token..
+        $gauth = new google_authsub(false, $params['token']);
+
+        $this->sessionkey = $gauth->get_sessiontoken();
+
+        google_picasa::set_sesskey($this->sessionkey, $this->get('user')->id);
+    }
+
+}
+
+/**
+ * Registers to the user_deleted event to revoke any 
+ * subauth tokens we have from them
+ *
+ * @param $user user object
+ * @return boolean true in all cases as its only minor cleanup
+ */
+function portfolio_picasa_user_deleted($user){
+    // it is only by luck that the user prefstill exists now?
+    // We probably need a pre-delete event?
+    if($sesskey = google_picasa::get_sesskey($user->id)){
+        try{
+            $gauth = new google_authsub($sesskey);
+
+            $gauth->revoke_session_token();
+        }catch(Exception $e){
+            // we don't care that much about success- just being good
+            // google api citzens
+            return true;
+        }
+    }
+
+    return true; 
+}
diff --git a/portfolio/type/picasa/version.php b/portfolio/type/picasa/version.php
new file mode 100644 (file)
index 0000000..e210acd
--- /dev/null
@@ -0,0 +1,7 @@
+<?php // $Id$
+
+$plugin->version  = 2008072505;
+$plugin->requires = 2008072500;
+$plugin->cron     = 0;
+
+?>