--- /dev/null
+<?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=';
+ }
+}
+
+?>
--- /dev/null
+<?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;
+}
--- /dev/null
+<?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;
+}