From ee91cf95917f3920b3c2fa48c67a664191ef7bdf Mon Sep 17 00:00:00 2001 From: poltawski Date: Sat, 29 Nov 2008 20:24:25 +0000 Subject: [PATCH] portfolio: MDL-16417 - Adding googledocs/picasa plugins * Added simple library for googleapi things * Respository plugins to follow --- lang/en_utf8/portfolio_googledocs.php | 6 + lang/en_utf8/portfolio_picasa.php | 5 + lib/googleapi.php | 381 ++++++++++++++++++++++++ portfolio/type/googledocs/db/events.php | 11 + portfolio/type/googledocs/lib.php | 114 +++++++ portfolio/type/googledocs/version.php | 6 + portfolio/type/picasa/db/events.php | 11 + portfolio/type/picasa/lib.php | 115 +++++++ portfolio/type/picasa/version.php | 7 + 9 files changed, 656 insertions(+) create mode 100644 lang/en_utf8/portfolio_googledocs.php create mode 100644 lang/en_utf8/portfolio_picasa.php create mode 100644 lib/googleapi.php create mode 100644 portfolio/type/googledocs/db/events.php create mode 100644 portfolio/type/googledocs/lib.php create mode 100644 portfolio/type/googledocs/version.php create mode 100644 portfolio/type/picasa/db/events.php create mode 100644 portfolio/type/picasa/lib.php create mode 100644 portfolio/type/picasa/version.php diff --git a/lang/en_utf8/portfolio_googledocs.php b/lang/en_utf8/portfolio_googledocs.php new file mode 100644 index 0000000000..488e8268d4 --- /dev/null +++ b/lang/en_utf8/portfolio_googledocs.php @@ -0,0 +1,6 @@ + diff --git a/lang/en_utf8/portfolio_picasa.php b/lang/en_utf8/portfolio_picasa.php new file mode 100644 index 0000000000..df72d5cffb --- /dev/null +++ b/lang/en_utf8/portfolio_picasa.php @@ -0,0 +1,5 @@ + diff --git a/lib/googleapi.php b/lib/googleapi.php new file mode 100644 index 0000000000..69ba61a6d6 --- /dev/null +++ b/lib/googleapi.php @@ -0,0 +1,381 @@ +. + * + * @package moodle + * @subpackage lib + * @author Dan Poltawski + * @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 index 0000000000..181dc1de3c --- /dev/null +++ b/portfolio/type/googledocs/db/events.php @@ -0,0 +1,11 @@ + 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 index 0000000000..226c6bafa8 --- /dev/null +++ b/portfolio/type/googledocs/lib.php @@ -0,0 +1,114 @@ + + * @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 index 0000000000..cd8312d2d8 --- /dev/null +++ b/portfolio/type/googledocs/version.php @@ -0,0 +1,6 @@ +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 index 0000000000..dd5e280761 --- /dev/null +++ b/portfolio/type/picasa/db/events.php @@ -0,0 +1,11 @@ + 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 index 0000000000..75e3c1cad2 --- /dev/null +++ b/portfolio/type/picasa/lib.php @@ -0,0 +1,115 @@ + + * @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 index 0000000000..e210acd073 --- /dev/null +++ b/portfolio/type/picasa/version.php @@ -0,0 +1,7 @@ +version = 2008072505; +$plugin->requires = 2008072500; +$plugin->cron = 0; + +?> -- 2.39.5