├── GoogleCalendar.php ├── GoogleClientAPI.module ├── GoogleClientClass.php ├── GoogleClientConfig.php ├── GoogleMail.php ├── GoogleSheets.php ├── MarkupGoogleCalendar.module ├── README.md ├── _mgc-event.php └── composer.json /GoogleCalendar.php: -------------------------------------------------------------------------------- 1 | getClient($options)); 29 | } 30 | 31 | /** 32 | * Set the calendar ID via a shareable calendar URL 33 | * 34 | * Accepts URLs in any of the following formats: 35 | * 36 | * - https://calendar.google.com/calendar?cid=cxlhbkByYy1kLn4lcA 37 | * - https://calendar.google.com/calendar/embed?src=ryan%40processwire.com&ctz=America%2FNew_York 38 | * - https://calendar.google.com/calendar/ical/ryan%40processwire.com/public/basic.ics 39 | * 40 | * @param string $calendarUrl 41 | * @return self 42 | * @throws WireException 43 | * 44 | */ 45 | public function setCalendarUrl($calendarUrl) { 46 | $calendarUrl = str_replace('%40', '@', $calendarUrl); 47 | if(preg_match('![\?&;/](cid=|src=|ical/)([-_.@a-zA-Z0-9]+)!', $calendarUrl, $matches)) { 48 | $this->calendarId = $matches[2]; 49 | } else { 50 | throw new WireException( 51 | "Unrecognized calendar URL format. " . 52 | "Please use shareable calendar URL that contains a calendar ID (cid) in the query string" 53 | ); 54 | } 55 | return $this; 56 | } 57 | 58 | /** 59 | * Set the calendar ID by URL or ID 60 | * 61 | * @param string $calendar 62 | * @return self 63 | * @throws WireException 64 | * 65 | */ 66 | public function setCalendar($calendar) { 67 | if(strpos($calendar, '://') !== false) { 68 | $this->setCalendarUrl($calendar); 69 | } else { 70 | $this->setCalendarId($calendar); 71 | } 72 | return $this; 73 | } 74 | 75 | /** 76 | * Set the current calendar ID 77 | * 78 | * @param string $calendarId 79 | * @return self 80 | * 81 | */ 82 | public function setCalendarId($calendarId) { 83 | $this->calendarId = $calendarId; 84 | return $this; 85 | } 86 | 87 | /** 88 | * Get the current calendar ID 89 | * 90 | * @return string 91 | * 92 | */ 93 | public function getCalendarId() { 94 | return $this->calendarId; 95 | } 96 | 97 | /** 98 | * Get events for given calendar ID (example usage of Google Client) 99 | * 100 | * Default behavior is to return the next 10 upcoming events. Use the 101 | * $options argument to adjust this behavior. 102 | * 103 | * USAGE EXAMPLE 104 | * ============= 105 | * $google = $modules->get('GoogleClientAPI'); 106 | * $calendar = $google->calendar(); 107 | * $calendar->setCalendarId('primary'); // optional 108 | * $events = $calendar->getEvents(); 109 | * foreach($events->getItems() as $event) { 110 | * $start = $event->getStart()->dateTime; 111 | * if(empty($start)) $start = $event->getStart()->date; 112 | * echo sprintf("
  • %s (%s)
  • ", $event->getSummary(), $start); 113 | * } 114 | * 115 | * @param array|string $options One or more of the following options: 116 | * - `calendarId` (string): Calendar ID to pull events from. If not specified 117 | * it will use whatever calendar specified in previous setCalendarId() call, 118 | * which has a default value of 'primary'. 119 | * - `maxResults` (int): Max number of results to return (default=10) 120 | * - `orderBy` (string): Field to order events by (default=startTime) 121 | * - `timeMin` (string|int): Events starting after this date/time (default=now) 122 | * - `timeMax` (string|int): Events up to this date/time (default=null) 123 | * - `q` (string): Text string to search for 124 | * @param array $o Additional options for legacy support (deprecated). 125 | * This argument used as $options if calendar ID was specified in first argument. 126 | * It is here to be backwards compatible with previous argumnet layout, 127 | * but should otherwise be skipped. 128 | * @return \Google_Service_Calendar_Events|bool 129 | * 130 | */ 131 | public function getEvents($options = array(), array $o = array()) { 132 | 133 | $defaults = array( 134 | 'calendarId' => '', 135 | 'maxResults' => 10, 136 | 'orderBy' => 'startTime', 137 | 'singleEvents' => true, 138 | 'timeMin' => date('c'), 139 | 'timeMax' => null, 140 | 'q' => '', 141 | ); 142 | 143 | if(!is_array($options)) { 144 | // legacy support for calendar ID as first argument 145 | $calendarId = $options; 146 | $options = $o; 147 | $options['calendarId'] = $calendarId; 148 | } 149 | 150 | $options = array_merge($defaults, $options); 151 | $calendarId = empty($options['calendarId']) ? $this->calendarId : $options['calendarId']; 152 | 153 | try { 154 | $service = $this->getService(); 155 | } catch(\Exception $e) { 156 | $this->error($e->getMessage(), Notice::log); 157 | return false; 158 | } 159 | 160 | // make sure times are in format google expects 161 | foreach(array('timeMin', 'timeMax') as $t) { 162 | if(!isset($options[$t]) || $options[$t] === null) continue; 163 | $v = $options[$t]; 164 | if(is_string($v)) $v = ctype_digit("$v") ? (int) $v : strtotime($v); 165 | if(is_int($v)) $options[$t] = date('c', $v); 166 | } 167 | 168 | // remove options that are not applicable or not in use 169 | unset($options['calendarId']); 170 | if(empty($options['q'])) unset($options['q']); 171 | if(empty($options['timeMax'])) unset($options['timeMax']); 172 | 173 | // return the events 174 | return $service->events->listEvents($calendarId, $options); 175 | } 176 | 177 | /** 178 | * Test Google Calendar API 179 | * 180 | * @return string 181 | * 182 | */ 183 | public function test() { 184 | $out = []; 185 | $sanitizer = $this->wire('sanitizer'); 186 | try { 187 | $events = $this->getEvents([ 'timeMin' => time() ]); 188 | foreach($events->getItems() as $event) { 189 | $start = $event->getStart()->dateTime; 190 | if(empty($start)) $start = $event->getStart()->date; 191 | $out[] = "" . $sanitizer->entities($event->getSummary()) . "$start"; 192 | } 193 | } catch(\Exception $e) { 194 | $out[] = "Google Calendar test failed: " . 195 | get_class($e) . ' ' . 196 | $e->getCode() . ' — ' . 197 | $sanitizer->entities($e->getMessage()); 198 | } 199 | if(count($out)) { 200 | $out = "" . implode("\n", $out) . "
    "; 201 | } else { 202 | $out = "No upcoming events found."; 203 | } 204 | $out = "
    Google Calendar — Upcoming Events Test:
    $out"; 205 | return $out; 206 | } 207 | } -------------------------------------------------------------------------------- /GoogleClientAPI.module: -------------------------------------------------------------------------------- 1 | get('GoogleClientAPI'); 25 | * 26 | * // use ProcessWire GoogleSheets API 27 | * $sheets = $google->sheets(); 28 | * 29 | * // use ProcessWire GoogleCalendar API 30 | * $calendar = $google->calendar(); 31 | * 32 | * // use any other google services via the \Google_Client class 33 | * $client = $google->getClient(); 34 | * ~~~~~ 35 | * 36 | * CONFIG SETTINGS 37 | * @property string $accessToken JSON access token data 38 | * @property string $refreshToken refresh token 39 | * @property string $authConfig JSON client secret data 40 | * @property string $authConfigHash Hash of authConfig for detecting changes 41 | * @property int $configUserID ProccessWire user ID of user that $authConfig belongs to 42 | * @property string $redirectURL 43 | * @property string $applicationName 44 | * @property array $scopes 45 | * @property string $scopesHash 46 | * @property string $libVersion Google Client PHP API library version 47 | * 48 | * API PROPERTIES 49 | * @property GoogleCalendar $calender 50 | * @property GoogleSheets $sheets 51 | * @property \Google_Client $client 52 | * 53 | * 54 | */ 55 | class GoogleClientAPI extends WireData implements Module, ConfigurableModule { 56 | 57 | public static function getModuleInfo() { 58 | return array( 59 | 'title' => 'Google Client API', 60 | 'summary' => 'Connects ProcessWire with the Google Client Library and manages authentication.', 61 | 'version' => 4, 62 | 'license' => 'MPL 2.0', 63 | 'author' => 'Ryan Cramer', 64 | 'icon' => 'google', 65 | ); 66 | } 67 | 68 | const debug = false; 69 | 70 | /** 71 | * Google PHP API client default download version 72 | * 73 | */ 74 | const defaultLibVersion = '2.2.3'; 75 | 76 | /** 77 | * Construct by setup of default config values 78 | * 79 | */ 80 | public function __construct() { 81 | parent::__construct(); 82 | $this->set('applicationName', ''); 83 | $this->set('accessToken', ''); 84 | $this->set('refreshToken', ''); 85 | $this->set('authConfig', ''); 86 | $this->set('authConfigHash', ''); 87 | $this->set('configUserID', 0); 88 | $this->set('redirectURL', ''); 89 | $this->set('scopes', array()); 90 | $this->set('scopesHash', ''); 91 | $this->set('libVersion', ''); 92 | } 93 | 94 | /** 95 | * Initialize the module 96 | * 97 | */ 98 | public function init() { 99 | $this->loadGoogleLibrary(); 100 | if(!count($this->scopes)) { 101 | $this->set('scopes', array('https://www.googleapis.com/auth/calendar.readonly')); 102 | } 103 | require_once(__DIR__ . '/GoogleClientClass.php'); 104 | } 105 | 106 | /** 107 | * Get setting 108 | * 109 | * @param string $key 110 | * @return mixed|null|GoogleClientClass 111 | * 112 | */ 113 | public function get($key) { 114 | if($key === 'calendar') return $this->calendar(); 115 | if($key === 'sheets') return $this->sheets(); 116 | if($key === 'client') return $this->getClient(); 117 | return parent::get($key); 118 | } 119 | 120 | /** 121 | * Set config setting 122 | * 123 | * @param string $key 124 | * @param mixed $value 125 | * @return WireData|GoogleClientAPI 126 | * 127 | */ 128 | public function set($key, $value) { 129 | if($key === 'scopes') { 130 | if(is_string($value)) { 131 | $value = empty($value) ? array() : explode("\n", $value); 132 | foreach($value as $k => $v) $value[$k] = trim($v); 133 | } 134 | } 135 | return parent::set($key, $value); 136 | } 137 | 138 | /** 139 | * Return a new instance of the GoogleCalendar class 140 | * 141 | * (currently just a method for finding events, will expand upon it later) 142 | * 143 | * @param string $calendarId Optional calendar ID or shareable URL to use 144 | * @return GoogleCalendar 145 | * 146 | */ 147 | public function calendar($calendarId = '') { 148 | require_once(__DIR__ . '/GoogleCalendar.php'); 149 | $calendar = new GoogleCalendar($this); 150 | $this->wire($calendar); 151 | if(!empty($calendarId)) $calendar->setCalendar($calendarId); 152 | return $calendar; 153 | } 154 | 155 | /** 156 | * Return a new instance of the GoogleSheets class 157 | * 158 | * @param string $spreadsheetId Optional spreadsheet ID or spreadsheet URL to use 159 | * @return GoogleSheets 160 | * 161 | */ 162 | public function sheets($spreadsheetId = '') { 163 | require_once(__DIR__ . '/GoogleSheets.php'); 164 | $sheets = new GoogleSheets($this); 165 | $this->wire($sheets); 166 | if(!empty($spreadsheetId)) $sheets->setSpreadsheet($spreadsheetId); 167 | return $sheets; 168 | } 169 | 170 | /** 171 | * Get the Google Client 172 | * 173 | * @param array $options 174 | * @return bool|\Google_Client 175 | * @throws \Google_Exception 176 | * 177 | */ 178 | public function getClient($options = array()) { 179 | 180 | if(!$this->authConfig) return false; 181 | 182 | $defaults = array( 183 | 'applicationName' => $this->applicationName, 184 | 'scopes' => $this->scopes, 185 | 'accessType' => 'offline', 186 | 'redirectUri' => $this->redirectURL, 187 | ); 188 | 189 | $options = array_merge($defaults, $options); 190 | 191 | $client = new \Google_Client(); 192 | $client->setApplicationName($options['applicationName']); 193 | $client->setScopes($options['scopes']); 194 | $client->setAuthConfig(json_decode($this->authConfig, true)); 195 | $client->setAccessType($options['accessType']); 196 | $client->setRedirectUri($options['redirectUri']); 197 | 198 | $this->setAccessToken($client); 199 | 200 | return $client; 201 | } 202 | 203 | /** 204 | * Setup the access token and refresh when needed (internal use) 205 | * 206 | * #pw-internal 207 | * 208 | * @param \Google_Client $client 209 | * @return bool 210 | * 211 | */ 212 | public function setAccessToken(\Google_Client $client) { 213 | 214 | if(!$this->accessToken && $this->wire('process')->className() == 'ProcessModule') { 215 | // module config, request authorization 216 | $session = $this->wire('session'); 217 | $input = $this->wire('input'); 218 | $user = $this->wire('user'); 219 | if(!$user->isSuperuser()) return false; 220 | $code = $input->get('code'); 221 | if(empty($code)) { 222 | // Request authorization from the user 223 | $authURL = str_replace('approval_prompt=auto', 'approval_prompt=force', $client->createAuthUrl()); 224 | if($authURL) $session->redirect($authURL); 225 | return false; 226 | } else { 227 | // Exchange auth code for an access token 228 | $this->accessToken = $client->fetchAccessTokenWithAuthCode($code); 229 | if(self::debug) $this->message("client->authenticate($code) == $this->accessToken"); 230 | if($this->accessToken) { 231 | $this->saveAccessToken(); 232 | $session->message($this->_('Saved Google authentication credentials')); 233 | $session->redirect($this->redirectURL); 234 | return false; 235 | } 236 | } 237 | } 238 | 239 | $client->setAccessToken($this->accessToken); 240 | if(!$this->refreshToken) $this->saveAccessToken(); 241 | 242 | if($client->isAccessTokenExpired()) { 243 | $refreshToken = $this->getRefreshToken(); 244 | if($refreshToken) { 245 | $client->refreshToken($refreshToken); 246 | $this->accessToken = $client->getAccessToken(); 247 | if($this->accessToken) $this->saveAccessToken(); 248 | } else { 249 | $this->error('Unable to get refresh token'); 250 | return false; 251 | } 252 | } 253 | 254 | return true; 255 | } 256 | 257 | /** 258 | * Get the refresh token (internal use) 259 | * 260 | * #pw-internal 261 | * 262 | * @return string 263 | * 264 | */ 265 | public function getRefreshToken() { 266 | 267 | $refreshToken = ''; 268 | 269 | if($this->refreshToken) { 270 | if(strpos($this->refreshToken, '{') === 0) { 271 | // json encoded (legacy, can eventually be removed) 272 | $token = json_decode($this->refreshToken, true); 273 | if(isset($token['refresh_token'])) $refreshToken = $token['refresh_token']; 274 | } else { 275 | // not encoded 276 | $refreshToken = $this->refreshToken; 277 | } 278 | 279 | } else if($this->accessToken) { 280 | // attempt to get from accessToken 281 | $token = is_array($this->accessToken) ? $this->accessToken : json_decode($this->accessToken, true); 282 | if($token && isset($token['refresh_token'])) { 283 | $refreshToken = $token['refresh_token']; 284 | } 285 | 286 | } else { 287 | // unable to get it 288 | } 289 | 290 | return $refreshToken; 291 | } 292 | 293 | /** 294 | * Save the access token to module config data (internal use) 295 | * 296 | * #pw-internal 297 | * 298 | */ 299 | public function saveAccessToken() { 300 | $configData = $this->wire('modules')->getModuleConfigData($this); 301 | $configData['accessToken'] = $this->accessToken; 302 | $configData['authConfigHash'] = md5($this->authConfig); 303 | $configData['scopesHash'] = $this->scopesHash(); 304 | if(empty($configData['refreshToken'])) { 305 | $configData['refreshToken'] = $this->getRefreshToken(); 306 | } 307 | $this->wire('modules')->saveModuleConfigData($this, $configData); 308 | if(self::debug) { 309 | $this->message('saveModuleConfigData'); 310 | $this->message($configData); 311 | } 312 | } 313 | 314 | /** 315 | * Generate the current hash from $this->>scopes, which may be different from $this->scopesHash (internal use) 316 | * 317 | * #pw-internal 318 | * 319 | * @return string 320 | * 321 | */ 322 | public function scopesHash() { 323 | return md5(implode(' ', $this->scopes)); 324 | } 325 | 326 | /** 327 | * Get Google library path 328 | * 329 | * #pw-internal 330 | * 331 | * @param bool $getParentPath 332 | * @param bool $getUrl 333 | * @return string 334 | * @throws WireException 335 | * 336 | */ 337 | public function getGoogleLibPath($getParentPath = false, $getUrl = false) { 338 | $config = $this->wire('config'); 339 | $path = ($getUrl ? $config->urls->assets : $config->paths->assets) . $this->className() . '/'; 340 | if($getParentPath) return $path; 341 | return $path . 'google-api-php-client/'; 342 | } 343 | 344 | /** 345 | * Get Google library version 346 | * 347 | * @return string 348 | * 349 | */ 350 | public function getGoogleLibVersion() { 351 | if(!class_exists("\\Google_Client")) return ''; 352 | return \Google_Client::LIBVER; 353 | } 354 | 355 | /** 356 | * Get autoload file for Google library 357 | * 358 | * #pw-internal 359 | * 360 | * @param bool $getUrl 361 | * @return string 362 | * 363 | */ 364 | public function getGoogleAutoloadFile($getUrl = false) { 365 | return $this->getGoogleLibPath(false, $getUrl) . 'vendor/autoload.php'; 366 | } 367 | 368 | /** 369 | * Load the Google Library 370 | * 371 | * #pw-internal 372 | * 373 | * @return bool 374 | * 375 | */ 376 | protected function loadGoogleLibrary() { 377 | if(class_exists("\\Google_Client")) return true; 378 | $file = $this->getGoogleAutoloadFile(); 379 | if(file_exists($file)) { 380 | require_once($file); 381 | return true; 382 | } else { 383 | /* 384 | $this->warning( 385 | "ProcessWire $this module requires that the " . 386 | "Google API PHP client library " . 387 | "be installed. See module configuration for further instructions.", 388 | Notice::allowMarkup 389 | ); 390 | */ 391 | return false; 392 | } 393 | } 394 | 395 | /** 396 | * Module configuration 397 | * 398 | * #pw-internal 399 | * 400 | * @param InputfieldWrapper $form 401 | * 402 | */ 403 | public function getModuleConfigInputfields(InputfieldWrapper $form) { 404 | require_once(__DIR__ . '/GoogleClientConfig.php'); 405 | $moduleConfig = new GoogleClientConfig($this); 406 | $moduleConfig->getModuleConfigInputfields($form); 407 | } 408 | 409 | /** 410 | * Uninstall module 411 | * 412 | * #pw-internal 413 | * 414 | */ 415 | public function ___uninstall() { 416 | $assetPath = $this->wire('config')->paths->assets . $this->className() . '/'; 417 | if(is_dir($assetPath)) { 418 | if($this->wire('files')->rmdir($assetPath, true)) { 419 | $this->message("Removed: $assetPath"); 420 | } else { 421 | $this->error("Error removing: $assetPath"); 422 | } 423 | } 424 | } 425 | 426 | /*** DEPRECATED METHODS ***************************************************************/ 427 | 428 | /** 429 | * Get calendar events (deprecated) 430 | * 431 | * @deprecated please use $modules->GoogleClientAPI->calendar($calendarId)->getEvents(...) instead 432 | * @param string $calendarId 433 | * @param array $options 434 | * @return \Google_Service_Calendar_Events|bool 435 | * 436 | */ 437 | public function getCalendarEvents($calendarId = '', array $options = array()) { 438 | return $this->calendar($calendarId)->getEvents($options); 439 | } 440 | } 441 | 442 | 443 | -------------------------------------------------------------------------------- /GoogleClientClass.php: -------------------------------------------------------------------------------- 1 | module = $module; 25 | } 26 | 27 | /** 28 | * Get the Google_Client 29 | * 30 | * @param array $options 31 | * @return \Google_Client 32 | * @throws \Google_Exception|WireException 33 | * 34 | */ 35 | protected function getClient($options = array()) { 36 | $client = $this->module->getClient($options); 37 | if(!$client) throw new WireException("The GoogleClientAPI module is not yet configured"); 38 | return $client; 39 | } 40 | 41 | /** 42 | * Get the Google_Service 43 | * 44 | * @param array $options 45 | * @return \Google_Service 46 | * @throws \Google_Exception 47 | * 48 | */ 49 | abstract public function getService(array $options = array()); 50 | 51 | /** 52 | * Get the Google_Service with default options 53 | * 54 | * @return \Google_Service 55 | * @throws \Google_Exception 56 | * 57 | */ 58 | public function service() { 59 | if(!$this->service) $this->service = $this->getService(); 60 | return $this->service; 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /GoogleClientConfig.php: -------------------------------------------------------------------------------- 1 | wire($this); 25 | $this->module = $module; 26 | } 27 | 28 | /** 29 | * Install the Google PHP API Client library 30 | * 31 | * @param string $version i.e. "2.2.3" 32 | * @return bool 33 | * 34 | */ 35 | protected function installGoogleLibrary($version) { 36 | 37 | /** @var WireFileTools $files */ 38 | $files = $this->wire('files'); 39 | $version = trim($version, '. '); 40 | 41 | if(strlen($version) < 4 || !preg_match('/^[\.0-9]+$/', $version)) { 42 | $this->error( 43 | "Please use version number format “0.0.0” where each “0” is any number " . 44 | "(i.e. " . GoogleClientAPI::defaultLibVersion . ")" 45 | ); 46 | return false; 47 | } 48 | 49 | set_time_limit(3600); 50 | 51 | if(empty($version)) $version = GoogleClientAPI::defaultLibVersion; 52 | 53 | $downloadUrl = 54 | "https://github.com/googleapis/google-api-php-client/releases/" . 55 | "download/v$version/google-api-php-client-$version.zip"; 56 | 57 | $libPath = $this->module->getGoogleLibPath(); // site/assets/GoogleClientAPI/google-api-php-client/ 58 | $apiPath = $this->module->getGoogleLibPath(true); // site/assets/GoogleClientAPI/ 59 | $zipName = 'download.zip'; 60 | $zipFile = $apiPath . $zipName; 61 | $htaFile = $apiPath . '.htaccess'; 62 | $completed = false; 63 | $n = 0; 64 | 65 | if(!is_dir($apiPath)) $files->mkdir($apiPath, true); 66 | if(is_file($zipFile)) $files->unlink($zipFile); 67 | if(is_dir($libPath)) $files->rmdir($libPath, true); 68 | 69 | if(!is_file($htaFile)) { 70 | // block web access to this path, not really necessary, but just in case 71 | file_put_contents($htaFile, "RewriteEngine On\nRewriteRule ^.*$ - [F,L]"); 72 | $files->chmod($htaFile); 73 | } 74 | 75 | while(is_file($zipFile)) { 76 | // ensure we do not unzip something that was already present (not likely) 77 | $zipFile = $apiPath . (++$n) . $zipName; 78 | } 79 | 80 | try { 81 | // download ZIP 82 | $http = new WireHttp(); 83 | $http->download($downloadUrl, $zipFile); 84 | if(!is_file($zipFile)) throw new WireException("Error downloading to: $zipFile"); 85 | // $this->message("Downloaded $downloadUrl => $zipFile"); 86 | } catch(\Exception $e) { 87 | $this->error($e->getMessage()); 88 | } 89 | 90 | if(is_file($zipFile)) try { 91 | $unzipped = $files->unzip($zipFile, $apiPath); 92 | $this->message("Unzipped $zipFile (" . count($unzipped) . " files)"); 93 | clearstatcache(); 94 | foreach(new \DirectoryIterator($apiPath) as $file) { 95 | if(!$file->isDir() || $file->isDot()) continue; 96 | $files->rename($file->getPathname(), $libPath); // /google-api-php-client/ 97 | break; 98 | } 99 | $autoloadFile = $this->module->getGoogleAutoloadFile(); 100 | $completed = is_file($autoloadFile); 101 | if(!$completed) $this->error("Unable to find: $autoloadFile"); 102 | } catch(\Exception $e) { 103 | $this->error($e->getMessage()); 104 | } 105 | 106 | if($completed) { 107 | $this->message("Installed: $libPath"); 108 | } else { 109 | $this->error("Unable to install: $libPath"); 110 | $files->rmdir($libPath, true); 111 | } 112 | 113 | if(is_file($zipFile)) { 114 | $files->unlink($zipFile); 115 | } 116 | 117 | return $completed; 118 | } 119 | 120 | /** 121 | * Module configuration 122 | * 123 | * @param InputfieldWrapper $form 124 | * 125 | */ 126 | public function getModuleConfigInputfields(InputfieldWrapper $form) { 127 | 128 | $modules = $this->wire('modules'); 129 | $session = $this->wire('session'); 130 | $input = $this->wire('input'); 131 | $redirectURL = $this->module->redirectURL ? $this->module->redirectURL : $input->httpUrl(true); 132 | $user = $this->wire('user'); 133 | $module = $this->module; 134 | 135 | if($module->configUserID && $module->configUserID != $user->id) { 136 | $configUser = $this->wire('users')->get((int) $module->configUserID); 137 | $userName = $configUser && $configUser->id ? $configUser->name : "id=$this->module->configUserID"; 138 | $this->error(sprintf($this->_('Configuration of this module is limited to user: %s'), $userName)); 139 | return; 140 | } 141 | 142 | // ------------- 143 | 144 | /** @var InputfieldFieldset $fs */ 145 | $fs = $modules->get('InputfieldFieldset'); 146 | $fs->label = $this->_('Google API PHP client library'); 147 | $fs->icon = 'google'; 148 | $fs->set('themeOffset', 1); 149 | $form->add($fs); 150 | 151 | /** @var InputfieldCheckbox $f */ 152 | $f = $modules->get('InputfieldCheckbox'); 153 | $f->attr('name', '_install_lib'); 154 | $libVersion = $module->getGoogleLibVersion(); 155 | if($libVersion) { 156 | $fs->collapsed = Inputfield::collapsedYes; 157 | $fs->label .= ' ' . sprintf($this->_('(version %s installed)'), $libVersion); 158 | $f->label = $this->_('Change version?'); 159 | $f->description = $this->_('After successfully changing the version, please use the “force re-authenticate” option that appears further down on this screen.'); 160 | $required = true; 161 | } else { 162 | $f->label = $this->_('Install now?'); 163 | $f->description = 164 | $this->_('The required Google API PHP client library is not yet installed.') . ' ' . 165 | $this->_('You may install it automatically by clicking this checkbox.'); 166 | $required = false; 167 | } 168 | $f->description .= ' ' . sprintf( 169 | $this->_('If you prefer, you may clone/download/unzip and install it yourself into %s.'), 170 | $module->getGoogleLibPath(false, true) 171 | ); 172 | $f->notes = 173 | sprintf($this->_('Checking the box installs the library into %s.'), $module->getGoogleLibPath(false, true)) . " \n" . 174 | $this->_('Please note the library is quite large and may take several minutes to download and install.'); 175 | $fs->add($f); 176 | 177 | /** @var InputfieldText $f */ 178 | $f = $modules->get('InputfieldText'); 179 | $f->attr('name', '_install_lib_ver'); 180 | $f->label = $this->_('Google API PHP client library version'); 181 | $f->description = sprintf( 182 | $this->_('Enter the library version from the [releases](%s) page that you want to install, or accept the default, then click submit.'), 183 | 'https://github.com/googleapis/google-api-php-client/releases' 184 | ); 185 | $f->attr('placeholder', GoogleClientAPI::defaultLibVersion); 186 | $f->attr('value', GoogleClientAPI::defaultLibVersion); 187 | $f->showIf = '_install_lib>0'; 188 | $fs->add($f); 189 | 190 | if($input->post('_install_lib') && $input->post('_install_lib_ver')) { 191 | $session->setFor($this, 'install_lib', $input->post->name('_install_lib_ver')); 192 | } else if($session->getFor($this, 'install_lib')) { 193 | $version = $session->getFor($this, 'install_lib'); 194 | $session->removeFor($this, 'install_lib'); 195 | $this->installGoogleLibrary($version); 196 | $session->redirect($input->url(true)); 197 | } 198 | 199 | // ----------------- 200 | 201 | $fs = $modules->get('InputfieldFieldset'); 202 | $fs->label = $this->_('Google API services authentication'); 203 | $fs->icon = 'google'; 204 | $fs->set('themeOffset', 1); 205 | if(!$required) { 206 | $fs->collapsed = Inputfield::collapsedYes; 207 | $fs->description = $this->_('Please install the Google API PHP client library before configuring this section.'); 208 | } else { 209 | $fs->description = sprintf( 210 | $this->_('Please follow [these instructions](%s) to complete this section.'), 211 | 'https://github.com/ryancramerdesign/GoogleClientAPI/blob/master/README.md' 212 | ); 213 | } 214 | $form->add($fs); 215 | 216 | /** @var InputfieldText $f */ 217 | $f = $modules->get('InputfieldText'); 218 | $f->attr('name', 'applicationName'); 219 | $f->label = $this->_('Application name'); 220 | $f->attr('value', $module->applicationName); 221 | $f->required = $required; 222 | $fs->add($f); 223 | 224 | /** @var InputfieldTextarea $f */ 225 | $f = $modules->get('InputfieldTextarea'); 226 | $f->attr('name', 'scopes'); 227 | $f->label = $this->_('Scopes (one per line)'); 228 | $f->attr('value', implode("\n", $module->scopes)); 229 | $f->description = 230 | sprintf($this->_('A list of available scopes can be found [here](%s).'), 'https://developers.google.com/identity/protocols/googlescopes') . ' ' . 231 | $this->_('Note that any changes to scopes will redirect you to Google to confirm the change.'); 232 | $f->notes = '**' . $this->_('Example:') . "**\nhttps://www.googleapis.com/auth/spreadsheets\nhttps://www.googleapis.com/auth/calendar.readonly"; 233 | $f->required = $required; 234 | 235 | if(!strlen($module->scopesHash) && count($module->scopes)) $module->scopesHash = $module->scopesHash(); 236 | $fs->add($f); 237 | 238 | $f = $modules->get('InputfieldTextarea'); 239 | $f->attr('name', 'authConfig'); 240 | $f->label = $this->_('Authentication config / client secret JSON'); 241 | $f->description = $this->_('Paste in the client secret JSON provided to you by Google.'); 242 | $f->attr('value', $module->authConfig); 243 | $f->required = $required; 244 | $f->collapsed = Inputfield::collapsedPopulated; 245 | $fs->add($f); 246 | 247 | /** @var InputfieldCheckbox $f */ 248 | if($module->authConfig) { 249 | $f = $modules->get('InputfieldCheckbox'); 250 | $f->attr('name', '_reauth'); 251 | $f->label = $this->_('Force re-authenticate with Google now?'); 252 | $f->description = $this->_('If you get any permission errors during API calls you may need to force re-authenticate with Google.'); 253 | $f->collapsed = Inputfield::collapsedYes; 254 | $fs->add($f); 255 | } 256 | 257 | if(GoogleClientAPI::debug) { 258 | $f = $modules->get('InputfieldTextarea'); 259 | $f->attr('name', '_accessToken'); 260 | $f->label = 'Access Token (populated automatically)'; 261 | $f->attr('value', is_array($module->accessToken) ? json_encode($module->accessToken) : $module->accessToken); 262 | $f->collapsed = Inputfield::collapsedYes; 263 | $fs->add($f); 264 | 265 | $f = $modules->get('InputfieldTextarea'); 266 | $f->attr('name', '_refreshToken'); 267 | $f->label = 'Refresh Token (populated automatically)'; 268 | $f->attr('value', $module->getRefreshToken()); 269 | $f->collapsed = Inputfield::collapsedYes; 270 | $fs->add($f); 271 | } 272 | 273 | $module->saveAccessToken(); 274 | 275 | $reAuth = $module->authConfig && md5($module->authConfig) != $module->authConfigHash; 276 | if(!$reAuth) $reAuth = $module->scopesHash && $module->scopesHash != $module->scopesHash(); 277 | if(!$reAuth) $reAuth = $input->post('_reauth') ? true : false; 278 | if($reAuth) $session->setFor($this, 'authConfigTest', 1); 279 | 280 | if(!$input->requestMethod('POST') && ($input->get('code') || $session->getFor($this, 'authConfigTest'))) { 281 | $session->setFor($this, 'authConfigTest', null); 282 | $test = json_decode($module->authConfig, true); 283 | if(is_array($test) && count($test)) { 284 | $module->accessToken = ''; 285 | $module->getClient(); 286 | // $this->message("Setup new access token"); 287 | } else { 288 | $this->error('Authentication config did not validate as JSON, please check it'); 289 | $this->warning($module->authConfig); 290 | } 291 | } 292 | 293 | /** @var InputfieldText $f */ 294 | $f = $modules->get('InputfieldText'); 295 | $f->attr('name', 'redirectURL'); 296 | $f->label = $this->_('Redirect URL (auto-generated)'); 297 | $f->description = $this->_('Please provide this URL to Google as part of your API configuration.'); 298 | $f->attr('value', $redirectURL); 299 | $f->notes = $this->_('Note: this is generated automatically and you should not change it.'); 300 | if($module->authConfig) { 301 | $f->collapsed = Inputfield::collapsedYes; 302 | } else { 303 | $this->warning(sprintf($this->_('FYI: Your “Authorized redirect URI” (for Google) is: %s'), "\n$redirectURL")); 304 | } 305 | $fs->add($f); 306 | 307 | /** @var InputfieldRadios $f */ 308 | $f = $modules->get('InputfieldRadios'); 309 | $f->attr('name', 'configUserID'); 310 | $f->label = sprintf($this->_('Only superuser “%s” may view and configure this module?'), $user->name); 311 | $f->description = $this->_('Answering “Yes” here ensures that other superusers in the system cannot view your client secret JSON or modify module settings.'); 312 | $f->addOption($this->wire('user')->id, sprintf($this->_('Yes (%s)'), $user->name)); 313 | $f->addOption(0, $this->_('No')); 314 | $f->attr('value', $module->configUserID); 315 | $f->icon = 'user-circle-o'; 316 | $f->set('themeOffset', 1); 317 | if(!$module->configUserID) $f->collapsed = Inputfield::collapsedYes; 318 | $form->add($f); 319 | 320 | $form->add($this->configTests()); 321 | } 322 | 323 | /** 324 | * Google client API tests 325 | * 326 | * @throws WireException 327 | * @throws WirePermissionException 328 | * @return InputfieldFieldset 329 | * 330 | */ 331 | protected function configTests() { 332 | 333 | $modules = $this->wire('modules'); 334 | $input = $this->wire('input'); 335 | $session = $this->wire('session'); 336 | $requiresLabel = $this->_('Requires that at least one of the following URLs is in your “scopes” field above:'); 337 | 338 | /** @var InputfieldFieldset $fs */ 339 | $fs = $modules->get('InputfieldFieldset'); 340 | $fs->attr('name', '_configTests'); 341 | $fs->label = $this->_('API tests'); 342 | $fs->collapsed = Inputfield::collapsedYes; 343 | $fs->description = $this->_('Once you have everything configured, it’s worthwhile to test APIs here to make sure everything is working with your Google credentials.'); 344 | $fs->icon = 'certificate'; 345 | $fs->set('themeOffset', 1); 346 | 347 | /** @var InputfieldText $f */ 348 | $f = $modules->get('InputfieldText'); 349 | $f->attr('name', '_testCalendar'); 350 | $f->label = $this->_('Test Google Calendar API'); 351 | $f->description = 352 | $this->_('Open a Google Calendar, go to the settings and get the “Calendar ID” or “Shareable link” URL to the calendar, and paste it below.') . ' ' . 353 | $this->_('This test will show you the next 10 upcoming events in the calendar.'); 354 | $f->notes = $requiresLabel . 355 | "\nhttps://www.googleapis.com/auth/calendar" . 356 | "\nhttps://www.googleapis.com/auth/calendar.readonly"; 357 | $f->collapsed = Inputfield::collapsedYes; 358 | $fs->add($f); 359 | 360 | /** @var InputfieldText $f */ 361 | $f = $modules->get('InputfieldText'); 362 | $f->attr('name', '_testSheets'); 363 | $f->label = $this->_('Test Google Sheets API'); 364 | $f->description = 365 | $this->_('Open a Google Sheets spreadsheet and copy/paste the URL from your browser address bar into here.') . ' ' . 366 | $this->_('This test will show you some stats about the spreadsheet.'); 367 | $f->notes = $requiresLabel . 368 | "\nhttps://www.googleapis.com/auth/spreadsheets" . 369 | "\nhttps://www.googleapis.com/auth/spreadsheets.readonly"; 370 | $f->collapsed = Inputfield::collapsedYes; 371 | $fs->add($f); 372 | 373 | if($input->post('_testCalendar')) { 374 | $session->setFor($this, 'testCalendar', $input->post('_testCalendar')); 375 | } else if($session->getFor($this, 'testCalendar')) { 376 | $calendarUrl = $session->getFor($this, 'testCalendar'); 377 | $session->removeFor($this, 'testCalendar'); 378 | $calendar = $this->module->calendar(); 379 | $calendar->setCalendar($calendarUrl); 380 | $this->warning($calendar->test(), Notice::allowMarkup); 381 | } 382 | 383 | if($input->post('_testSheets')) { 384 | $session->setFor($this, 'testSheets', $input->post->url('_testSheets')); 385 | } else if($session->getFor($this, 'testSheets')) { 386 | $spreadsheetUrl = $session->getFor($this, 'testSheets'); 387 | $session->removeFor($this, 'testSheets'); 388 | $sheets = $this->module->sheets($spreadsheetUrl); 389 | $this->warning($sheets->test(), Notice::allowMarkup); 390 | } 391 | 392 | return $fs; 393 | } 394 | 395 | 396 | } -------------------------------------------------------------------------------- /GoogleMail.php: -------------------------------------------------------------------------------- 1 | getClient($options)); 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /GoogleSheets.php: -------------------------------------------------------------------------------- 1 | get('GoogleClientAPI'); 13 | * $sheets = $google->sheets(); 14 | * 15 | * // Print out rows 1-5 from a spreadsheet 16 | * $sheets->setSpreadsheetUrl('https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit#gid=SHEET_ID'); 17 | * $rows = $sheets->getRows(1, 5); 18 | * print_r($rows); 19 | * 20 | * // Create a new spreadsheet, add a header row and another row 21 | * $sheets->addSpreadsheet('Hello World'); 22 | * $sheets->setRows(1, [ // set rows starting from row 1 23 | * [ 'First name', 'Last name', 'Email', 'Age' ], // row 1: our header row 24 | * [ 'Ryan', 'Cramer', 'ryan@processwire.com', 44 ] // row 2: example data 25 | * ]); 26 | * 27 | * // Append one new row to a spreadsheet 28 | * $sheets->appendRow([ 'Ryan', 'Cramer', 'ryan@processwire.com', 44 ]); 29 | * ~~~~~~ 30 | * 31 | * Note the term “Spreadsheet” refers to an entire spreadsheet, while the term 32 | * “Sheet” refers to a tab within a spreadsheet. 33 | * 34 | * @method \Google_Service_Sheets service() 35 | * 36 | * --- 37 | * Copyright 2019 by Ryan Cramer Design, LLC 38 | * 39 | */ 40 | class GoogleSheets extends GoogleClientClass { 41 | 42 | /** 43 | * Current working spreadsheet ID 44 | * 45 | * @var string 46 | * 47 | */ 48 | protected $spreadsheetId = ''; 49 | 50 | /** 51 | * ID of the current tab/sheet in the spreadsheet, i.e. 0 or 123 or null if not set by a setSheet() call 52 | * 53 | * @var null 54 | * 55 | */ 56 | protected $sheetId = null; 57 | 58 | /** 59 | * Title of the current tab/sheet in the spreadsheet, i.e. "Sheet1" or null if not specified by a setSheet() call 60 | * 61 | * @var null 62 | * 63 | */ 64 | protected $sheetTitle = null; 65 | 66 | /** 67 | * Set current spreadsheet by ID or URL (auto-detect) and optionally set sheet 68 | * 69 | * @param string $spreadsheet Spreadsheet ID or URL 70 | * @return self 71 | * 72 | */ 73 | public function setSpreadsheet($spreadsheet) { 74 | if(strpos($spreadsheet, '://') !== false) { 75 | $this->setSpreadsheetUrl($spreadsheet); 76 | } else { 77 | $this->setSpreadsheetId($spreadsheet); 78 | } 79 | return $this; 80 | } 81 | 82 | /** 83 | * Set the current working spreadsheet by URL 84 | * 85 | * This automatically extracts the spreadsheet ID and sheet ID. 86 | * If you call this, it is NOT necessary to call setSpreadsheetId() or setSheetId() 87 | * 88 | * @param string $url 89 | * @throws WireException if given URL that isn’t recognized as Google Sheets URL 90 | * @return self 91 | * 92 | */ 93 | public function setSpreadsheetUrl($url) { 94 | if(strpos($url, '/d/') === false) { 95 | throw new WireException( 96 | 'Unrecognized Google Sheets URL. Must be in this format: ' . 97 | 'https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit#gid=SHEET_ID' 98 | ); 99 | } 100 | $sheetId = ''; 101 | list(, $spreadsheetId) = explode('/d/', $url, 2); // SPREADSHEET_ID/edit#gid=SHEET_ID 102 | list($spreadsheetId, $s) = explode('/', $spreadsheetId, 2); // 'SPREADSHEET_ID', 'edit#gid=SHEET_ID' 103 | if(strpos($s, 'gid=') !== false && preg_match('/gid=(\d+)/', $s, $matches)) $sheetId = $matches[1]; 104 | $this->setSpreadsheetId($spreadsheetId); 105 | if($sheetId !== '') $this->setSheetId($sheetId); 106 | return $this; 107 | } 108 | 109 | /** 110 | * Set the current working spreadsheet by ID 111 | * 112 | * If you will also be setting a sheet, make sure you call the setSheet* method after this rather than before. 113 | * 114 | * @param string $spreadsheetId 115 | * @param string|int $sheet Optional sheet title or sheetId, within the spreadsheet 116 | * @return self 117 | * 118 | */ 119 | public function setSpreadsheetId($spreadsheetId, $sheet = '') { 120 | if($spreadsheetId !== $this->spreadsheetId) { 121 | $this->sheetTitle = null; 122 | $this->sheetId = null; 123 | $this->spreadsheetId = $spreadsheetId; 124 | } 125 | if($sheet !== '') $this->setSheet($sheet); 126 | return $this; 127 | } 128 | 129 | /** 130 | * Get the current spreadsheet ID 131 | * 132 | * @return string 133 | * 134 | */ 135 | public function getSpreadsheetId() { 136 | if(empty($this->spreadsheetId)) { 137 | throw new WireException("Please call setSpreadsheetId() before calling any methods on GoogleSheets"); 138 | } 139 | return $this->spreadsheetId; 140 | } 141 | 142 | /** 143 | * Set the current sheet/tab 144 | * 145 | * @param string|int $sheet Sheet title or id 146 | * @return self 147 | * 148 | */ 149 | public function setSheet($sheet) { 150 | if(is_int($sheet) || ctype_digit("$sheet")) { 151 | $this->setSheetId($sheet); 152 | } else if(is_string($sheet) && strlen($sheet)) { 153 | $this->setSheetTitle($sheet); 154 | } 155 | return $this; 156 | } 157 | 158 | /** 159 | * Get the current sheet/tab by ID (#gid in spreadsheet URL) 160 | * 161 | * @param string|int $sheetId 162 | * @return self 163 | * 164 | */ 165 | public function setSheetId($sheetId) { 166 | $this->sheetId = (int) $sheetId; 167 | $this->sheetTitle = null; 168 | return $this; 169 | } 170 | 171 | /** 172 | * Set the current sheet/tab in the spreadsheet by title 173 | * 174 | * @param string $title 175 | * @return self 176 | * 177 | */ 178 | public function setSheetTitle($title) { 179 | $this->sheetTitle = $title; 180 | $this->sheetId = null; 181 | return $this; 182 | } 183 | 184 | /** 185 | * Get the current sheet/tab ID (aka gid) 186 | * 187 | * @return int||null 188 | * 189 | */ 190 | public function getSheetId() { 191 | if($this->sheetId === null && $this->sheetTitle) return $this->sheetId(); 192 | return (int) $this->sheetId; 193 | } 194 | 195 | /** 196 | * Get the current sheet/tab title (label that user clicks on for tab in spreadsheet) 197 | * 198 | * @return int||null 199 | * 200 | */ 201 | public function getSheetTitle() { 202 | if($this->sheetTitle === null && $this->sheetId !== null) return $this->sheetTitle(); 203 | return $this->sheetTitle === null ? '' : $this->sheetTitle; 204 | } 205 | 206 | /** 207 | * Get the Google Sheets service 208 | * 209 | * #pw-internal 210 | * 211 | * @param array $options 212 | * @return \Google_Service|\Google_Service_Sheets 213 | * @throws WireException 214 | * @throws \Google_Exception 215 | * 216 | */ 217 | public function getService(array $options = array()) { 218 | return new \Google_Service_Sheets($this->getClient($options)); 219 | } 220 | 221 | /** 222 | * Add/create a new spreadsheet and set it as the current spreadsheet 223 | * 224 | * ~~~~~ 225 | * $spreadsheet = $google->sheets()->addSpreadsheet('My test spreadsheet'); 226 | * $spreadsheetId = $spreadsheet->spreadsheetId; 227 | * ~~~~~ 228 | * 229 | * @param string $title Title for spreadsheet 230 | * @param bool $setAsCurrent Set as the current working spreadsheet? (default=true) 231 | * @return \Google_Service_Sheets_Spreadsheet 232 | * @throws \Google_Exception 233 | * 234 | */ 235 | public function addSpreadsheet($title, $setAsCurrent = true) { 236 | $spreadsheet = new \Google_Service_Sheets_Spreadsheet([ 237 | 'properties' => [ 238 | 'title' => $title 239 | ] 240 | ]); 241 | $spreadsheet = $this->service()->spreadsheets->create($spreadsheet, [ 242 | 'fields' => 'spreadsheetId,spreadsheetUrl' 243 | ]); 244 | if($setAsCurrent) $this->setSpreadsheetId($spreadsheet->spreadsheetId); 245 | return $spreadsheet; 246 | } 247 | 248 | /** 249 | * Get cells from the spreadsheet 250 | * 251 | * @param string $range Specify one of the following: 252 | * - Range of rows to retrieve in format "1:3" where 1 is first row number and 3 is last row number 253 | * - Range of cells to retrieve in A1 format, i.e. "A1:C3" where A1 is starting col-row, and C3 is ending col-row. 254 | * @param array $options 255 | * @return array 256 | * @throws \Google_Exception 257 | * 258 | */ 259 | public function getCells($range = '', array $options = array()) { 260 | $params = array( 261 | 'majorDimension' => 'ROWS', // or COLUMNS 262 | 'valueRenderOption' => 'FORMATTED_VALUE', // or UNFORMATTED_VALUE or FORMULA: https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption 263 | 'dateTimeRenderOption' => 'FORMATTED_STRING', // or SERIAL_NUMBER: https://developers.google.com/sheets/api/reference/rest/v4/DateTimeRenderOption 264 | ); 265 | foreach(array_keys($params) as $key) { 266 | if(isset($options[$key])) $params[$key] = $options[$key]; 267 | } 268 | $result = $this->service()->spreadsheets_values->get($this->getSpreadsheetId(), $this->rangeStr($range), $options); 269 | return $result->getValues(); 270 | } 271 | 272 | /** 273 | * Get requested rows (multiple) 274 | * 275 | * @param int $fromRowNum 276 | * @param int $toRowNum 277 | * @param array $options 278 | * @return array 279 | * @throws \Google_Exception 280 | * 281 | */ 282 | public function getRows($fromRowNum, $toRowNum, array $options = array()) { 283 | return $this->getCells($this->rangeStr("$fromRowNum:$toRowNum"), $options); 284 | } 285 | 286 | /** 287 | * Get requested row (single) 288 | * 289 | * @param int $rowNum 290 | * @param array $options 291 | * @return array 292 | * @throws \Google_Exception 293 | * 294 | */ 295 | public function getRow($rowNum, array $options = array()) { 296 | $rows = $this->getRows($rowNum, $rowNum, $options); 297 | return count($rows) ? $rows[0] : array(); 298 | } 299 | 300 | /** 301 | * Update/modify cells in a spreadsheet 302 | * 303 | * The $range argument can specify multiple cells (for example, A1:D5) or a single cell (for example, A1). 304 | * If it specifies multiple cells, the $rows argument must be within that range. If it specifies a single cell, 305 | * the input data starts at that coordinate can extend any number of rows or columns. 306 | * 307 | * The $values argument should be a PHP array in this format: 308 | * ~~~~~ 309 | * $data = [ 310 | * [ 311 | * 'row 1 col A value', 312 | * 'row 1 col B value' 313 | * ], 314 | * [ 315 | * 'row 2 col A value', 316 | * 'row 2 col B value' 317 | * ], 318 | * // and so on for each row 319 | * ]; 320 | * ~~~~~ 321 | * 322 | * @param string $range Range in A1 notation, see notes above. 323 | * @param array $values Rows/cells you want to update in format shown above 324 | * @param array $options Options to modify default behavior: 325 | * - `raw` (bool): Add as raw data? Raw data unlike user-entered data is not converted to dates, formulas, etc. (default=false) 326 | * - `overwrite` (bool): Allow overwrite existing rows rather than inserting new rows after range? (default=false) 327 | * - `params` (array): Additional params ($optParams argument) for GoogleSheets call (internal). 328 | * @return \Google_Service_Sheets_AppendValuesResponse|\Google_Service_Sheets_UpdateValuesResponse 329 | * @throws \Google_Exception 330 | * 331 | */ 332 | public function setCells($range, array $values, array $options = array()) { 333 | 334 | $defaults = array( 335 | 'raw' => false, 336 | 'action' => 'update', // method to call from spreadsheet_values 337 | 'replace' => true, 338 | 'params' => [], 339 | ); 340 | 341 | $options = array_merge($defaults, $options); 342 | $action = $options['action']; 343 | $body = new \Google_Service_Sheets_ValueRange([ 'values' => $values ]); 344 | $params = [ 'valueInputOption' => ($options['raw'] ? 'RAW' : 'USER_ENTERED') ]; 345 | 346 | if($options['action'] === 'append') { 347 | $params['insertDataOption'] = $options['replace'] ? 'OVERWRITE' : 'INSERT_ROWS'; 348 | } 349 | 350 | if(!empty($options['params'])) { 351 | $params = array_merge($params, $options['params']); 352 | } 353 | 354 | $range = $this->rangeStr($range); 355 | 356 | /* 357 | * spreasheet_values->update( 358 | * $spreadsheetId, 359 | * $range, 360 | * Google_Service_Sheets_ValueRange $postBody, 361 | * $optParams = array() 362 | * ); 363 | * 364 | * $optParams for update() method: 365 | * 366 | * - string `responseValueRenderOption` Determines how values in the response should 367 | * be rendered. The default render option is ValueRenderOption.FORMATTED_VALUE. 368 | * 369 | * - string `valueInputOption` How the input data should be interpreted. 370 | * 371 | * - string `responseDateTimeRenderOption` Determines how dates, times, and durations 372 | * in the response should be rendered. This is ignored if response_value_render_option 373 | * is FORMATTED_VALUE. The default dateTime render option is DateTimeRenderOption.SERIAL_NUMBER. 374 | * 375 | * - bool `includeValuesInResponse` Determines if the update response should include the 376 | * values of the cells that were updated. By default, responses do not include the updated 377 | * values. If the range to write was larger than than the range actually written, the 378 | * response will include all values in the requested range (excluding trailing empty rows 379 | * and columns). 380 | * 381 | */ 382 | 383 | $result = $this->service()->spreadsheets_values->$action($this->getSpreadsheetId(), $range, $body, $params); 384 | 385 | if($action === 'append') { 386 | /** @var \Google_Service_Sheets_AppendValuesResponse $result */ 387 | // $numCells = $result->getUpdates()->getUpdatedCells(); 388 | } else { 389 | /** @var \Google_Service_Sheets_UpdateValuesResponse $result */ 390 | // $numCells = $result->getUpdatedCells(); 391 | } 392 | // $this->message("
    " . print_r($result, true) . "
    ", Notice::allowMarkup); 393 | 394 | return $result; 395 | } 396 | 397 | /** 398 | * Update values for multiple rows 399 | * 400 | * @param int $fromRowNum Row number to start update from 401 | * @param array $rows Array of rows, each containing an array of column data 402 | * @param array $options See options for updateCells() method 403 | * @return \Google_Service_Sheets_AppendValuesResponse|\Google_Service_Sheets_UpdateValuesResponse|bool 404 | * @throws \Google_Exception 405 | * 406 | */ 407 | public function setRows($fromRowNum, array $rows, array $options = array()) { 408 | $fromRowNum = (int) $fromRowNum; 409 | $numRows = count($rows); 410 | if(!$numRows) return false; 411 | $toRowNum = $fromRowNum + ($numRows - 1); 412 | $range = $this->rangeStr("A$fromRowNum:A$toRowNum"); 413 | return $this->setCells($range, $rows, $options); 414 | } 415 | 416 | /** 417 | * Add/append rows to a spreadsheet 418 | * 419 | * The rows argument should be a PHP array in this format: 420 | * ~~~~~ 421 | * $rows = [ 422 | * [ 423 | * 'row 1 col A value', 424 | * 'row 1 col B value' 425 | * ], 426 | * [ 427 | * 'row 2 col A value', 428 | * 'row 2 col B value' 429 | * ], 430 | * // and so on for each row 431 | * ]; 432 | * ~~~~~ 433 | * 434 | * @param array $rows Rows you want to add in format shown above 435 | * @param int $fromRowNum Append rows after block of rows that $fromRowNum is within (default=1) 436 | * @param array $options Options to modify default behavior: 437 | * - `raw` (bool): Add as raw data? Raw data unlike user-entered data is not converted to dates, formulas, etc. (default=false) 438 | * @return \Google_Service_Sheets_AppendValuesResponse 439 | * @throws \Google_Exception 440 | * 441 | */ 442 | public function appendRows(array $rows, $fromRowNum = 1, array $options = array()) { 443 | $options['action'] = 'append'; 444 | $options['replace'] = false; 445 | return $this->setRows($fromRowNum, $rows, $options); 446 | } 447 | 448 | /** 449 | * Add/append a single row to a spreadsheet 450 | * 451 | * The row argument should be a PHP array in this format: 452 | * ~~~~~ 453 | * $row = [ 454 | * 'column A value', 455 | * 'column B value', 456 | * 'column C value', 457 | * // and so on for each column 458 | * ]; 459 | * ~~~~~ 460 | * 461 | * @param array $row Row you want to add in format shown above 462 | * @param int $fromRowNum Append rows after block of rows that $fromRowNum is within (default=1) 463 | * @param array $options Options to modify default behavior: 464 | * - `raw` (bool): Add as raw data? Raw data unlike user-entered data is not converted to dates, formulas, etc. (default=false) 465 | * @return \Google_Service_Sheets_AppendValuesResponse 466 | * @throws \Google_Exception 467 | * 468 | */ 469 | public function appendRow(array $row, $fromRowNum = 1, array $options = array()) { 470 | return $this->appendRows([ $row ], $fromRowNum, $options); 471 | } 472 | 473 | /** 474 | * Insert blank cells in the spreadsheet 475 | * 476 | * @param int $fromNum Row or column number to start inserting after or specify negative row/col number to insert before 477 | * @param int $qty Quantity of rows/columns to insert 478 | * @param bool $insertRows Insert rows? If false, it will insert columns rather than rows. 479 | * @param array $options 480 | * @return \Google_Service_Sheets_BatchUpdateSpreadsheetResponse|bool 481 | * @throws \Google_Exception 482 | * 483 | */ 484 | protected function insertBlanks($fromNum, $qty, $insertRows = true, array $options = array()) { 485 | if($qty < 1) return false; 486 | 487 | $insertBefore = $fromNum < 0; 488 | $fromNum = abs($fromNum); 489 | $startIndex = $insertBefore ? $fromNum - 1 : $fromNum; 490 | $endIndex = ($startIndex + $qty) - 1; 491 | $inheritFromBefore = $startIndex > 0 ? true : false; 492 | 493 | $request = new \Google_Service_Sheets_Request([ 494 | 'insertDimension' => [ 495 | 'range' => [ 496 | 'sheetId' => isset($options['sheetId']) ? $options['sheetId'] : 0, 497 | 'dimension' => $insertRows ? 'ROWS' : 'COLUMNS', 498 | 'startIndex' => $startIndex, 499 | 'endIndex' => $endIndex, 500 | ], 501 | // inherit properties from rows before newly inserted rows? (false=inherit after) 502 | 'inheritFromBefore' => $inheritFromBefore, 503 | ] 504 | ]); 505 | 506 | $batchUpdateRequest = new \Google_Service_Sheets_BatchUpdateSpreadsheetRequest([ 'requests' => [ $request ] ]); 507 | $response = $this->service()->spreadsheets->batchUpdate($this->getSpreadsheetId(), $batchUpdateRequest); 508 | 509 | return $response; 510 | } 511 | 512 | /** 513 | * Insert new rows after a specific row 514 | * 515 | * @param array $rows 516 | * @param int $rowNum Insert rows before this row number 517 | * @param array $options 518 | * @return \Google_Service_Sheets_UpdateValuesResponse 519 | * 520 | */ 521 | public function insertRowsAfter(array $rows, $rowNum, array $options = array()) { 522 | $this->insertBlanks($rowNum, count($rows), $options); 523 | $options['replace'] = false; 524 | $options['action'] = 'update'; 525 | return $this->setRows($rowNum, $rows, $options); 526 | } 527 | 528 | /** 529 | * Insert new rows before a specific row 530 | * 531 | * @param array $rows 532 | * @param int $rowNum Insert rows before this row number 533 | * @param array $options 534 | * @return \Google_Service_Sheets_UpdateValuesResponse 535 | * 536 | */ 537 | public function insertRowsBefore(array $rows, $rowNum, array $options = array()) { 538 | $this->insertBlanks($rowNum * -1, count($rows), $options); 539 | $options['replace'] = false; 540 | $options['action'] = 'update'; 541 | return $this->setRows($rowNum, $rows, $options); 542 | } 543 | 544 | /** 545 | * Update/set property for existing spreadsheet 546 | * 547 | * @param string $propertyName Property name, like "title" 548 | * @param string $propertyValue Property value 549 | * @return \Google_Service_Sheets_BatchUpdateSpreadsheetResponse 550 | * @throws \Google_Exception 551 | * 552 | */ 553 | public function setProperty($propertyName, $propertyValue) { 554 | $request = new \Google_Service_Sheets_Request([ 555 | 'updateSpreadsheetProperties' => [ 556 | 'properties' => [ $propertyName => $propertyValue ], 557 | 'fields' => $propertyName 558 | ] 559 | ]); 560 | $batchUpdateRequest = new \Google_Service_Sheets_BatchUpdateSpreadsheetRequest([ 'requests' => [ $request ] ]); 561 | $response = $this->service()->spreadsheets->batchUpdate($this->getSpreadsheetId(), $batchUpdateRequest); 562 | return $response; 563 | } 564 | 565 | /** 566 | * Get all properties for the spreadsheet (or optionally a specific property) 567 | * 568 | * @param string|bool $property Specify property to retrieve, omit to return entire spreasheet, or boolean true for all properties. 569 | * @return \Google_Service_Sheets_Spreadsheet|mixed 570 | * @throws \Google_Exception 571 | * 572 | */ 573 | public function getProperties($property = '') { 574 | 575 | $spreadsheet = $this->service()->spreadsheets->get($this->getSpreadsheetId()); 576 | 577 | if($property === true) { 578 | return $spreadsheet->getProperties(); 579 | } 580 | 581 | switch($property) { 582 | case 'title': 583 | case 'timeZone': 584 | case 'locale': 585 | $value = $spreadsheet->getProperties()->$property; 586 | break; 587 | case 'url': 588 | $value = $spreadsheet->spreadsheetUrl; 589 | break; 590 | default: 591 | $value = $spreadsheet; 592 | } 593 | 594 | return $value; 595 | } 596 | 597 | /** 598 | * Get all sheets/tabs in the Spreadsheet 599 | * 600 | * Returns array of arrays, each containing basic info for each sheet. If the $verbose option is true, then it 601 | * returns array of \Google_Service_Sheets_Sheet objects rather than array of basic info for each sheet. 602 | * 603 | * @param bool $verbose Returns verbose objects? (default=false) 604 | * @param string $indexBy Specify "title" or "sheetId" to return array indexed by those properties, or omit for no index (regular PHP array) 605 | * @return array|\Google_Service_Sheets_Sheet[] 606 | * 607 | */ 608 | public function getSheets($verbose = false, $indexBy = '') { 609 | $sheets = array(); 610 | foreach($this->getProperties()->getSheets() as $sheet) { 611 | /** @var \Google_Service_Sheets_Sheet $sheet */ 612 | if($verbose) { 613 | $sheets[] = $sheet; 614 | continue; 615 | } 616 | $properties = $sheet->getProperties(); 617 | $gridProperties = $properties->getGridProperties(); 618 | $sheetArray = [ 619 | 'title' => $properties->getTitle(), 620 | 'sheetId' => $properties->getSheetId(), 621 | 'sheetType' => $properties->getSheetType(), 622 | 'index' => $properties->getIndex(), 623 | 'hidden' => $properties->getHidden(), 624 | 'numRows' => $gridProperties->getRowCount(), 625 | 'numCols' => $gridProperties->getColumnCount(), 626 | ]; 627 | if($indexBy) { 628 | $key = $sheetArray[$indexBy]; 629 | $sheets[$key] = $sheetArray; 630 | } else { 631 | $sheets[] = $sheetArray; 632 | } 633 | } 634 | return $sheets; 635 | } 636 | 637 | /** 638 | * Get current sheet ID, auto-detecting from sheet title if necessary 639 | * 640 | * @return int|mixed|null 641 | * 642 | */ 643 | protected function sheetId() { 644 | if($this->sheetId !== null) return $this->sheetId; 645 | if(!empty($this->sheetTitle)) { 646 | $sheets = $this->getSheets(false, 'title'); 647 | if(isset($sheets[$this->sheetTitle])) { 648 | $this->sheetId = $sheets[$this->sheetTitle]['sheetId']; 649 | return $this->sheetId; 650 | } 651 | } 652 | return 0; 653 | } 654 | 655 | /** 656 | * Get current sheet title, auto-detecting from sheet ID if necessary 657 | * 658 | * @return null|string 659 | * 660 | */ 661 | protected function sheetTitle() { 662 | if(!empty($this->sheetTitle)) return $this->sheetTitle; 663 | if($this->sheetId !== null) { 664 | $sheets = $this->getSheets(false, 'sheetId'); 665 | $this->sheetTitle = isset($sheets[$this->sheetId]) ? $sheets[$this->sheetId]['title'] : null; 666 | } 667 | return ''; 668 | } 669 | 670 | /** 671 | * Given a range string, prepare it for API call, plus update it to include sheet title when applicable 672 | * 673 | * @param string $range 674 | * @return string 675 | * 676 | */ 677 | protected function rangeStr($range) { 678 | 679 | if(strlen($range) && strpos($range, ':') === false) { 680 | $range = "$range:$range"; 681 | } 682 | 683 | if(strpos($range, '!') === false) { 684 | // no sheet present 685 | $sheetTitle = $this->sheetTitle(); 686 | if(!empty($sheetTitle)) { 687 | $sheetTitle = "'" . trim($sheetTitle, "'") . "'"; 688 | if(strlen($range)) { 689 | // range of cells in sheet 690 | $range = $sheetTitle . "!$range"; 691 | } else { 692 | // all cells in sheet 693 | $range = $sheetTitle; 694 | } 695 | } 696 | } 697 | 698 | return $range; 699 | } 700 | 701 | 702 | /** 703 | * Test the Google Sheets API 704 | * 705 | * @return string 706 | * 707 | */ 708 | public function test() { 709 | $sanitizer = $this->wire('sanitizer'); 710 | $out = []; 711 | try { 712 | $title = $this->getProperties('title'); 713 | $sheets = $this->getSheets(); 714 | $out[] = "Google Sheets Spreadsheet: " . $sanitizer->entities($title) . ""; 715 | foreach($sheets as $sheet) { 716 | $out[] = "Sheet: " . $sanitizer->entities($sheet['title']) . " ($sheet[numRows] rows, $sheet[numCols] columns)"; 717 | } 718 | } catch(\Exception $e) { 719 | $out[] = "GoogleSheets test failed: " . 720 | get_class($e) . ' ' . 721 | $e->getCode() . ' ' . 722 | $sanitizer->entities($e->getMessage()); 723 | } 724 | return implode('
    ', $out); 725 | } 726 | } 727 | 728 | -------------------------------------------------------------------------------- /MarkupGoogleCalendar.module: -------------------------------------------------------------------------------- 1 | get('MarkupGoogleCalendar'); 21 | * $cal->calendarID = 'your-calendar-id'; // Your Google Calendar ID (default=primary) 22 | * $cal->cacheExpire = 3600; // how many seconds to cache output (default=3600) 23 | * $cal->maxResults = 100; // maximum number of results to render (default=100) 24 | * 25 | * echo $cal->renderMonth(); // render events for this month 26 | * echo $cal->renderMonth($month, $year); // render events for given month 27 | * echo $cal->renderDay(); // render events for today 28 | * echo $cal->renderDay($day, $month, $year); // render events for given day 29 | * echo $cal->renderUpcoming(10); // render next 10 upcoming events 30 | * echo $cal->renderRange($timeMin, $timeMax); // render events between given min/max dates/times 31 | * 32 | * SETTINGS 33 | * ======== 34 | * Any of the following settings can be set directly to the module, or passed 35 | * in via the $options array that any of the render() methods accepts. 36 | * 37 | * @property string $calendarID The Google calendar ID (default=primary) 38 | * @property string $dateFormat The date() or strftime() date format (default='F j, Y') 39 | * @property string $timeFormat The date() or strftime() time format (default='g:i a'); 40 | * @property int $cacheExpire How many seconds to cache rendered markup (default=3600) 41 | * @property string $orderBy Event property to order results by (default=startTime) 42 | * @property int $maxResults Maximum number of events to find/render (default=100) 43 | * @property string $eventTemplate PHP template file to use for render (default=/path/to/site/templates/_mgc-event.php) 44 | * 45 | */ 46 | 47 | class MarkupGoogleCalendar extends WireData implements Module { 48 | 49 | public static function getModuleInfo() { 50 | return array( 51 | 'title' => 'Google Calendar Markup', 52 | 'summary' => 'Renders a calendar with data from Google', 53 | 'version' => 3, 54 | 'license' => 'MPL 2.0', 55 | 'author' => 'Ryan Cramer', 56 | 'icon' => 'google', 57 | 'requires' => 'GoogleClientAPI, PHP>=5.4.0, ProcessWire>=3.0.10', 58 | ); 59 | } 60 | 61 | /** 62 | * Construct and set default config values 63 | * 64 | */ 65 | public function __construct() { 66 | 67 | $this->set('calendarID', 'primary'); 68 | $this->set('dateFormat', $this->_('F j, Y')); // default date format 69 | $this->set('timeFormat', $this->_('g:i a')); // default time format 70 | $this->set('cacheExpire', 3600); 71 | $this->set('orderBy', 'startTime'); 72 | $this->set('maxResults', 100); 73 | 74 | $name = '_mgc-event.php'; 75 | $file = $this->wire('config')->paths->templates . $name; 76 | if(!is_file($file)) $file = $this->wire('config')->paths->MarkupGoogleCalendar . $name; 77 | $this->set('eventTemplate', $file); 78 | 79 | parent::__construct(); 80 | } 81 | 82 | /** 83 | * Render a single event 84 | * 85 | * @param \Google_Service_Calendar_Event $event 86 | * @param array $options 87 | * @return string 88 | * @throws WireException 89 | * 90 | */ 91 | public function renderEvent(\Google_Service_Calendar_Event $event, array $options = array()) { 92 | 93 | $sanitizer = $this->wire('sanitizer'); 94 | 95 | if($event->getStart()->dateTime) { 96 | // date and time 97 | $startDate = wireDate($this->dateFormat, $event->getStart()->dateTime); 98 | $startTime = wireDate($this->timeFormat, $event->getStart()->dateTime); 99 | $startTS = strtotime($event->getStart()->dateTime); 100 | } else { 101 | // date only 102 | $startDate = wireDate($this->dateFormat, $event->getStart()->date); 103 | $startTS = strtotime($event->getStart()->date); 104 | $startTime = ''; 105 | } 106 | 107 | if($event->getEnd()->dateTime) { 108 | // date and time 109 | $endDate = wireDate($this->dateFormat, $event->getEnd()->dateTime); 110 | $endTime = wireDate($this->timeFormat, $event->getEnd()->dateTime); 111 | $endTS = strtotime($event->getEnd()->dateTime); 112 | } else if($event->getEnd()->date) { 113 | // date only 114 | $endDate = wireDate($this->dateFormat, $event->getEnd()->date); 115 | $endTS = strtotime($event->getEnd()->date); 116 | $endTime = ''; 117 | } else { 118 | // no end date/time 119 | $endDate = ''; 120 | $endTime = ''; 121 | $endTS = 0; 122 | } 123 | 124 | // if startDate and endDate are the same, don't show endDate 125 | if($endDate == $startDate) { 126 | $endDate = ''; 127 | if($endTime == $startTime) $endTime = ''; 128 | } 129 | 130 | $startDateTime = trim("$startDate $startTime"); 131 | $endDateTime = trim("$endDate $endTime"); 132 | $dateRange = ($startDateTime && $endDateTime ? "$startDateTime – $endDateTime" : $startDateTime); 133 | 134 | // prepare variables we will use for output 135 | $vars = array( 136 | 'event' => $event, 137 | 'startTS' => $startTS, 138 | 'startDate' => $startDate, 139 | 'startTime' => $startTime, 140 | 'startDateTime' => $startDateTime, 141 | 'endTS' => $endTS, 142 | 'endDate' => $endDate, 143 | 'endTime' => $endTime, 144 | 'endDateTime' => $endDateTime, 145 | 'dateRange' => $dateRange, 146 | 'summary' => $sanitizer->entities($event->getSummary()), 147 | 'description' => $sanitizer->entities($event->getDescription()), 148 | 'location' => $sanitizer->entities($event->getLocation()), 149 | 'htmlLink' => $sanitizer->entities($event->getHtmlLink()), 150 | ); 151 | 152 | $eventTemplate = isset($options['eventTemplate']) ? $options['eventTemplate'] : $this->eventTemplate; 153 | 154 | return $this->wire('files')->render($eventTemplate, $vars); 155 | } 156 | 157 | /** 158 | * Render the given events 159 | * 160 | * @param \Google_Service_Calendar_Events $events 161 | * @param array $options 162 | * @return string Returns markup, or blank string if no events to render 163 | * 164 | */ 165 | public function renderEvents(\Google_Service_Calendar_Events $events, array $options = array()) { 166 | $out = ''; 167 | foreach($events->getItems() as $event) { 168 | $out .= $this->renderEvent($event, $options); 169 | } 170 | return $out; 171 | } 172 | 173 | /** 174 | * Render events for the given month and year 175 | * 176 | * If month or year are omitted or 0, it renders events for the current month. 177 | * 178 | * @param int $month 179 | * @param int $year 180 | * @param array $options 181 | * @return string 182 | * 183 | */ 184 | public function renderMonth($month = 0, $year = 0, array $options = array()) { 185 | 186 | if(!$month) { 187 | $timeMin = strtotime(date('Y-m') . '-01 00:00'); 188 | } else { 189 | $timeMin = strtotime("$year-$month-01 00:00"); 190 | } 191 | 192 | $timeMax = strtotime("+1 MONTH", $timeMin) - 1; 193 | 194 | return $this->renderRange($timeMin, $timeMax, $options); 195 | } 196 | 197 | /** 198 | * Render events for the given day (in month and year) 199 | * 200 | * If day, month or year are omitted or 0, it renders events for today. 201 | * 202 | * @param int $day 203 | * @param int $month 204 | * @param int $year 205 | * @param array $options 206 | * @return string 207 | * 208 | */ 209 | public function renderDay($day = 0, $month = 0, $year = 0, array $options = array()) { 210 | 211 | if(!$day || !$month || !$year) { 212 | $timeMin = strtotime(date('Y-m-d') . ' 00:00'); 213 | } else { 214 | $timeMin = strtotime("$year-$month-$day 00:00"); 215 | } 216 | 217 | $timeMax = strtotime("+1 DAY", $timeMin) - 1; 218 | 219 | return $this->renderRange($timeMin, $timeMax, $options); 220 | } 221 | 222 | /** 223 | * Render events for the given range of times 224 | * 225 | * @param int|string $timeMin 226 | * @param int|string $timeMax 227 | * @param array $options 228 | * @return string 229 | * 230 | */ 231 | public function renderRange($timeMin, $timeMax, array $options = array()) { 232 | 233 | /** @var WireCache $cache */ 234 | $cache = $this->wire('cache'); 235 | 236 | if(!ctype_digit("$timeMin")) $timeMin = strtotime($timeMin); 237 | if(!ctype_digit("$timeMax")) $timeMax = strtotime($timeMax); 238 | 239 | $defaults = array( 240 | 'timeMin' => (int) $timeMin, 241 | 'timeMax' => (int) $timeMax, 242 | ); 243 | 244 | $options = array_merge($defaults, $options); 245 | $cacheExpire = isset($options['cacheExpire']) ? $options['cacheExpire'] : $this->cacheExpire; 246 | $cacheKey = $this->makeCacheKey('range', $options); 247 | $out = $cacheExpire ? $this->cache->getFor($this, $cacheKey, $cacheExpire) : null; 248 | 249 | if(is_null($out)) { 250 | $out = $this->renderEvents($this->findEvents($options), $options); 251 | if($cacheExpire) $cache->saveFor($this, $cacheKey, $out, $cacheExpire); 252 | } 253 | 254 | return $out; 255 | } 256 | 257 | /** 258 | * Render upcoming events 259 | * 260 | * @param int $maxResults Maximum number of events to include 261 | * @param array $options 262 | * @return string 263 | * 264 | */ 265 | public function renderUpcoming($maxResults = 10, array $options = array()) { 266 | 267 | /** @var WireCache $cache */ 268 | $cache = $this->wire('cache'); 269 | $cacheExpire = isset($options['cacheExpire']) ? $options['cacheExpire'] : $this->cacheExpire; 270 | $cacheKey = $this->makeCacheKey("upcoming$maxResults", $options); 271 | $out = $cacheExpire ? $cache->getFor($this, $cacheKey, $cacheExpire) : null; 272 | 273 | if(is_null($out)) { 274 | $options['maxResults'] = $maxResults; 275 | if(empty($options['timeMin'])) $options['timeMin'] = time(); 276 | $out = $this->renderEvents($this->findEvents($options), $options); 277 | if($cacheExpire) $cache->saveFor($this, $cacheKey, $out, $cacheExpire); 278 | } 279 | 280 | return $out; 281 | } 282 | 283 | /** 284 | * Find calendar events 285 | * 286 | * @param array $options 287 | * @return \Google_Service_Calendar_Events 288 | * 289 | */ 290 | public function findEvents(array $options = array()) { 291 | 292 | $google = $this->wire('modules')->get('GoogleClientAPI'); 293 | 294 | $defaults = array( 295 | 'maxResults' => $this->maxResults, 296 | 'orderBy' => $this->orderBy, 297 | ); 298 | 299 | $options = array_merge($defaults, $options); 300 | $calendarID = isset($options['calendarID']) ? $options['calendarID'] : $this->calendarID; 301 | $events = $google->calendar($calendarID)->getEvents($options); 302 | 303 | return $events; 304 | } 305 | 306 | /** 307 | * Make a unique key to use for cache 308 | * 309 | * @param $name 310 | * @param array $options 311 | * @return string 312 | * 313 | */ 314 | protected function makeCacheKey($name, array $options = array()) { 315 | return md5( 316 | $name . '-' . 317 | print_r($options, true) . 318 | print_r($this->getArray(), true) 319 | ); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Client API for ProcessWire 2 | 3 | This module manages and simplifies the authentication and setup between 4 | Google service APIs and ProcessWire, and it is handled in the module settings. 5 | The module also currently provides common API calls for Google Sheets and 6 | Google Calendar via dedicated classes, enabling a simpler interface to these 7 | services than is available through Google’s client libraries. 8 | 9 | This package also includes the MarkupGoogleCalendar module, which is useful 10 | for rendering calendars in your site, but also serves as a good demonstration 11 | of using the GoogleClientAPI module. 12 | 13 | ### Requirements 14 | 15 | - ProcessWire 3.0.123 or newer 16 | - PHP 5.4 or newer (PHP 7+ recommended) 17 | - A Google account that you want to enable APIs for 18 | 19 | ---------------------- 20 | 21 | ## Installation 22 | 23 | Google sometimes changes things around in their APIs interface, though the essence remains the same. 24 | These instructions have gone through 3 iterations since 2015 to keep them up to date with Google's 25 | changes, and the current iteration was last updated July 22, 2019. If you encounter significant 26 | differences on the Google side, please let us know. 27 | 28 | ### Step A: Install module files 29 | 30 | - Download the module’s [ZIP file](https://github.com/ryancramerdesign/GoogleClientAPI/archive/master.zip), 31 | unzip and place the files in a new directory named: 32 | `/site/modules/GoogleClientAPI/` 33 | 34 | - Login to your ProcessWire admin and go to: Modules > Refresh. 35 | 36 | - Click “Install” next to the *GoogleClientAPI* module (which should appear on 37 | the “Site” tab). 38 | 39 | - You should now be at the module’s configuration screen, remain here for the next step. 40 | 41 | ### Step B: Install Google Client library 42 | 43 | - On the module configuration screen, you should now see a checkbox giving you the option to 44 | install the Google PHP API Client library. 45 | 46 | - Proceed with installing the library, either by checking the box and clicking Submit, or 47 | downloading and installing yourself to the location it provides. 48 | 49 | - Note that the library is quite large, so it may take a few minutes to complete installation. 50 | 51 | ### Step C: Enable APIs in Google Console 52 | 53 | 1. Open a new/window tab in your browser, go to [Google Console](https://console.developers.google.com) 54 | and login (if not already). It may ask you to agree to terms of service—check the box and continue. 55 | 56 | 2. Now you will be in the “Google API and Services” Dashboard. It will ask you to select a project. 57 | Chances are you don't yet have one, so click **“Create”**. 58 | 59 | 3. You should now be at the “New Project” screen. Optionally modify the project name or location, or 60 | just accept the defaults. Then click the **“Create”** button to continue. 61 | 62 | *In my case, I entered "ProcessWire website services" for the project name and left the 63 | location as-is.* 64 | 65 | 4. Now you'll be back at the Dashboard and there will be a link that says 66 | **“Enable APIs and Services”**, go ahead and click that link to continue. 67 | 68 | 5. The next screen will list all the Google APIs and services. Click on the API/service that you’d 69 | like to enable. 70 | 71 | 6. On the screen that follows the service you clicked, click the **“Enable”** button to enable the 72 | API for that service. 73 | 74 | ### Step D: Creating credentials in Google Console 75 | 76 | 1. After enabling your chosen API service(s), the next screen will show a notification that says: 77 | 78 | > To use this API, you may need credentials. Click 'Create credentials' to get started. 79 | 80 | Go ahead and click the **“Create credentials”** button as it suggests and fill in the following 81 | inputs that it displays to you: 82 | 83 | - **Which API are you using?** — Select the API you want to use. 84 | - **Where will you be calling the API from?** — Select “Web server”. 85 | - **What data will you be accessing?** — Select “User data”. 86 | 87 | Then click the **“What credentials do I need?”** button. 88 | 89 | 2. After clicking the button above, it may pop up a box that says “Set up OAuth consent screen”, 90 | in which case you should click the **“Set up consent screen”** button. The “OAuth consent 91 | screen” has several inputs, but you don't need to fill them all in if you don't want to. 92 | I do recommend completing the following though: 93 | 94 | - **Application name:** You can enter whatever you'd like here, but in my case I entered: 95 | “ProcessWire Google Client API”. 96 | 97 | - **Application logo:** you can leave this blank. 98 | 99 | - **Support email:** you can accept the default value. 100 | 101 | - **Scopes for Google APIs:** leave as-is, you'll be completing this part in ProcessWire. 102 | 103 | - **Authorized domains:** Enter the domain name where your website is running and hit enter. 104 | If it will be running at other domains, enter them as well and hit enter for each. 105 | 106 | *The next 3 inputs are only formalities. Only you will be seeing them, so they aren't really 107 | applicable to our project, but we have to fill them in anyway. You can put in any URLs on 108 | your website that you want to.* 109 | 110 | - **Application homepage:** Enter your website’s homepage URL. This will probably the the same 111 | as your first “authorized domain” but with an `http://` or `https://` in front of it. 112 | 113 | - **Application privacy policy link:** Enter a link to your website privacy policy, or some 114 | other URL in your website, if you don't have a privacy policy. Only you will see it. 115 | 116 | - **Application terms of service:** Enter a URL or leave it blank. 117 | 118 | After completing the above inputs, click the **“Save”** button. 119 | 120 | 3. The next screen will present you with a new **“Create Credentials”** button. 121 | Click that button and it will reveal a drop down menu, select **“OAuth client ID”**. 122 | Complete these inputs on the screen that follows: 123 | 124 | - **Application type:** Select “Web application” 125 | 126 | - **Name:** Accept the default “Web client 1”, or enter whatever you’d like. 127 | 128 | - **Authorized JavaScript origins:** You can leave this blank. 129 | 130 | - **Authorized redirect URIs:** To get this value, you'll need switch windows/tabs to go back 131 | to your ProcessWire Admin in: Modules > Configure > GoogleClientAPI. There will be a 132 | notification at the top that says this: 133 | ~~~~~ 134 | Your “Authorized redirect URI” (for Google) is: 135 | https://domain.com/processwire/module/edit?name=GoogleClientAPI 136 | ~~~~~ 137 | 138 | Copy the URL out of the notification that appears on your screen and paste it into the 139 | “Authorized redirect URIs” input on Google’s screen, and hit ENTER. 140 | 141 | - If you see a **“Refresh”** button, go ahead and click it. 142 | 143 | 4. When you've filled in all of the above inputs, you should see a **“Create OAuth Client ID”** 144 | button, please go ahead and click it to continue, and move on to step E below. 145 | 146 | 147 | ### Step E: Download credentials JSON file from Google 148 | 149 | 1. If the next screen says “Download Credentials”, go ahead and click the **“Download”** 150 | button now. It will download a `client_id.json` file (or some other named JSON file) to 151 | your computer. 152 | 153 | *If you don't see a download option here, it’s okay to proceed, you'll see it on the next step.* 154 | 155 | 2. Click the **“Done”** button. You will now be at the main “Credentials” screen which lists 156 | your OAuth clients. 157 | 158 | 3. If you haven't yet downloaded the JSON file, click the little download icon that appears on 159 | the right side of the “OAuth 2.0 Client IDs” table on this screen to download the file. Note 160 | the location of the file, or go ahead and load it into a text editor now. We'll be returning 161 | to it shortly in step F below. 162 | 163 | 4. You are done with Google Console though please stay logged in to it. For the next step we'll 164 | be going back into the ProcessWire admin. 165 | 166 | ### Step F: Authenticating ProcessWire with Google 167 | 168 | *Please note: In this step, even though you'll be in ProcessWire, you'll want to be sure you are still logged 169 | in with the Google account that you were using in step 3.* 170 | 171 | 1. Now we will fill in settings on the ProcessWire side. You'll want to be in the GoogleClientAPI 172 | module settings in your ProcessWire admin at: Modules > Configure > GoogleClientAPI. 173 | Complete the following inputs in the module settings: 174 | 175 | - **Application name:** Enter an application name. Likely you want the same one you entered 176 | into Google, i.e. “ProcessWire Google Client API”, or whatever you decided. 177 | 178 | - **Scopes (one per line):** for this field you are going to want to paste in one or more 179 | scopes (which look like URLs). The scopes are what specifies the permissions you want for 180 | the APIs you have enabled. Determine what scopes you will be using and paste them into 181 | this field. There's a good chance you'll only have one. 182 | [View all available scopes](https://developers.google.com/identity/protocols/googlescopes). 183 | Examples of scopes include: 184 | 185 | `https://www.googleapis.com/auth/calendar.readonly` for read-only access to calendars. 186 | `https://www.googleapis.com/auth/spreadsheets` for full access to Google Sheets spreadsheets. 187 | `https://www.googleapis.com/auth/gmail.send` for access to send email on your behalf. 188 | 189 | - **Authentication config / client secret JSON:** Open/load the JSON file that you downloaded 190 | earlier into a text editor. Select all, copy, and paste that JSON into this field. 191 | 192 | Click the **“Submit”** button to save the module settings. 193 | 194 | 2. After clicking the Submit button in the previous step, you should now find yourself at a 195 | Google screen asking you for permission to access the requested services. **Confirm all access.** 196 | 197 | - Depending on the scope(s) you requested, it may tell you that your app is from an unverified 198 | developer and encourage you to back out. It might even look like a Google error screen, but 199 | don't worry, all is well — find the link to proceed, hidden at the bottom. Unless you aren't 200 | sure if you trust yourself, keep moving forward with whatever prompts it asks to enable access. 201 | 202 | - Once you have confirmed the access, it will return you to the GoogleClientAPI module configuration 203 | screen in ProcessWire. 204 | 205 | 3. Your GoogleClientAPI module is now configured and ready to test! 206 | 207 | ---------------------- 208 | 209 | # Markup Google Calendar module 210 | 211 | This add-on helper module renders a calendar with data from Google. This module demonstrates 212 | use of and requires the *GoogleClientAPI* module, which must be installed and configured 213 | prior to using this module. It requires the following scope in GoogleClientAPI: 214 | `https://www.googleapis.com/auth/calendar.readonly` 215 | 216 | See the `_mgc-event.php` file which is what handles the output markup. You should 217 | copy this file to `/site/templates/_mgc-event.php` and modify it as you see fit. 218 | If you do not copy to your /site/templates/ directory then it will use the 219 | default one in the module directory. 220 | 221 | Please note that all render methods cache output by default for 1 hour. You can 222 | change this by adjusting the $cacheExpire property of the module. 223 | 224 | ## Usage 225 | 226 | ~~~~~ 227 | get('MarkupGoogleCalendar'); 229 | $cal->calendarID = 'your-calendar-id'; 230 | $cal->cacheExpire = 3600; // how many seconds to cache output (default=3600) 231 | $cal->maxResults = 100; // maximum number of results to render (default=100) 232 | 233 | // use any one of the following 234 | echo $cal->renderMonth(); // render events for this month 235 | echo $cal->renderMonth($month, $year); // render events for given month 236 | echo $cal->renderDay(); // render events for today 237 | echo $cal->renderDay($day, $month, $year); // render events for given day 238 | echo $cal->renderUpcoming(10); // render next 10 upcoming events 239 | echo $cal->renderRange($timeMin, $timeMax); // render events between given min/max dates/times 240 | ~~~~~ 241 | 242 | More details and options can be found in the phpdoc comments for each 243 | of the above mentioned methods. -------------------------------------------------------------------------------- /_mgc-event.php: -------------------------------------------------------------------------------- 1 | 37 | 38 |
    39 |

    40 |

    41 | 42 | 43 | 44 | 45 | 46 | 47 | – 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |

    58 | 59 |

    60 | 61 |

    62 | 63 | 64 |

    65 | 66 |

    67 | 68 |
    69 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "processwire/google-client-api", 3 | "type": "pw-module", 4 | "description": "Google Client library for use with ProcessWire CMS/CMF.", 5 | "keywords": [ "google", "client", "calendar", "sheets", "processwire" ], 6 | "homepage": "https://processwire.com", 7 | "license": "MPL-2.0", 8 | "authors": [ 9 | { 10 | "name": "Ryan Cramer", 11 | "email": "ryan@processwire.com", 12 | "homepage": "https://processwire.com", 13 | "role": "Developer" 14 | } 15 | ], 16 | "require": { 17 | "hari/pw-module": "~1.0", 18 | "google/apiclient": ">=2.2.2", 19 | "ext-ctype": "*", 20 | "ext-json": "*" 21 | } 22 | } 23 | --------------------------------------------------------------------------------