├── 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('([-_.@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 |
=$summary?>
40 |
41 |
42 | =$startDate?>
43 | if($startTime): ?>
44 | =$startTime?>
45 | endif; ?>
46 |
47 | if($endDate || $endTime): ?> –
48 |
49 | if($endDate): ?>
50 | =$endDate?>
51 | endif; ?>
52 | if($endTime): ?>
53 | =$endTime?>
54 | endif; ?>
55 |
56 | endif; ?>
57 |
58 | if($location): ?>
59 |
60 | =$location?>
61 |
62 | endif; ?>
63 | if($description): ?>
64 |
65 | =nl2br($description)?>
66 |
67 | endif; ?>
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 |
--------------------------------------------------------------------------------