diff --git a/.gitignore b/.gitignore
index b49c0bd..aa7ab62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,4 +38,6 @@ nbproject
*.log.*
*.log
.sonar-ide.properties
-.clover
\ No newline at end of file
+.clover
+.DS_Store
+
diff --git a/api/pom.xml b/api/pom.xml
index d4eb0b9..9ff0520 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -25,7 +25,6 @@
3.0-SNAPSHOTapplication-googleapps-api
- 3.0-SNAPSHOTjarGoogle Apps Integration (API)This is the XWiki-API part of the Google Apps which allows to connect Google Apps to XWiki.
@@ -42,11 +41,6 @@
google-api-client1.24.1
-
- com.google.gdata
- core
- 1.47.1
- com.google.apisgoogle-api-services-people
@@ -57,33 +51,10 @@
google-api-services-drivev2-rev358-1.24.1
+
+ javax.servlet
+ javax.servlet-api
+ provided
+
-
- GitHub
- https://github.com/xwikisas/application-googleapps/issues
-
-
- scm:git:git://github.com/xwikisas/application-googleapps.git
- scm:git:git@github.com:xwikisas/application-googleapps.git
- https://github.com/xwikisas/application-googleapps
- HEAD
-
-
-
-
- org.apache.maven.plugins
- maven-checkstyle-plugin
-
- false
-
-
-
- checkstyle-validation
- none
-
-
-
-
-
-
diff --git a/api/src/main/java/com/xwiki/googleapps/DriveDocMetadata.java b/api/src/main/java/com/xwiki/googleapps/DriveDocMetadata.java
new file mode 100644
index 0000000..c5c056b
--- /dev/null
+++ b/api/src/main/java/com/xwiki/googleapps/DriveDocMetadata.java
@@ -0,0 +1,284 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xwiki.googleapps;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.xwiki.stability.Unstable;
+
+/**
+ * Simple pojo for metadata about a doc in Google Drive.
+ *
+ * @version $Id$
+ * @since 3.0
+ */
+@Unstable
+public class DriveDocMetadata
+{
+ /**
+ * Google's internal id to find the document again.
+ */
+ private String id;
+
+ /**
+ * URL to direct the user to for editing.
+ */
+ private String editLink;
+
+ /**
+ * URL to pull from in order to fetch the document.
+ */
+ private String exportLink;
+
+ /**
+ * URL to use to show an embedded view.
+ */
+ private String embedLink;
+
+ /**
+ * A stringified version number.
+ */
+ private String version;
+
+ /**
+ * The name of the file in case it is an uploaded file.
+ */
+ private String fileName;
+
+ /**
+ * The email-address of the user with which this document's connection was created.
+ */
+ private String user;
+
+ /**
+ * A list of export possibilities.
+ */
+ private List exportLinksAlternatives = new LinkedList<>();
+
+ /**
+ * @return the internal Google Id of the document.
+ */
+ public String getId()
+ {
+ return id;
+ }
+
+ /**
+ * Sets the Google ID of the document having requested this document.
+ * @param id The id of the document.
+ * */
+ public void setId(String id)
+ {
+ this.id = id;
+ }
+
+ /**
+ * @return the version number
+ */
+ public String getVersion()
+ {
+ return version;
+ }
+
+ /**
+ * Set a designation of the version of the document.
+ * @param version The version (a number, expectedly)
+ */
+ public void setVersion(String version)
+ {
+ this.version = version;
+ }
+
+ /**
+ * @return the URL to direct the user to for editing.
+ */
+ public String getEditLink()
+ {
+ return editLink;
+ }
+
+ /**
+ * Sets the link where this document can be edited.
+ * @param link The link where it can be edited.
+ */
+ public void setEditLink(String link)
+ {
+ this.editLink = link;
+ }
+
+ /**
+ * @return the URL to pull from in order to fetch the document.
+ */
+ public String getExportLink()
+ {
+ return exportLink;
+ }
+
+ /**
+ * Sets the link from which this document can be exported (and thus saved to XWiki).
+ * @param link The link where it can be exported.
+ */
+ public void setExportLink(String link)
+ {
+ this.exportLink = link;
+ }
+
+ /**
+ * @return a list of export alternatives.
+ */
+ public List getExportLinksAlternatives()
+ {
+ return exportLinksAlternatives;
+ }
+
+ /**
+ * Inserts one of the information about one of the export-alternatives.
+ *
+ * @param extension the filename extension (understood as a name of the file-type)
+ * @param newFileName the filename when this file is stored on a desktop with this type
+ * @param exportUrl the url to pull from.
+ */
+ public void addExportAlternative(String extension, String newFileName, String exportUrl)
+ {
+ ExportAlternative ea = new ExportAlternative();
+ ea.extension = extension;
+ ea.exportUrl = exportUrl;
+ ea.newFileName = newFileName;
+ if (ea.newFileName == null) {
+ ea.newFileName = "unnamed";
+ }
+ exportLinksAlternatives.add(ea);
+ }
+
+ /**
+ * @return the name of the file in case it is an uploaded file.
+ */
+ public String getFileName()
+ {
+ return fileName;
+ }
+
+ /**
+ * Sets the name of the file when it was uploaded.
+ * @param fileName The name of the file.
+ */
+ public void setFileName(String fileName)
+ {
+ this.fileName = fileName;
+ }
+
+ /**
+ * @return the same as {#getFileName}.
+ */
+ public String getTitle()
+ {
+ return fileName;
+ }
+
+ /**
+ * @return a useful string representation
+ */
+ public String toString()
+ {
+ return "id " + id + " edit: " + editLink + " export " + exportLink;
+ }
+
+ /**
+ * The link of the iframe where it can be embedded.
+ * @return The link.
+ */
+ public String getEmbedLink()
+ {
+ return embedLink;
+ }
+
+ /**
+ * Sets the link with which the document can be embedded.
+ * @param link The link.
+ */
+ public void setEmbedLink(String link)
+ {
+ this.embedLink = link;
+ }
+
+ /**
+ * The Google user that was used to include this document.
+ * @return The google user email.
+ */
+ public String getUser()
+ {
+ return user;
+ }
+
+ /**
+ * The Google user that was used to include this document.
+ * @param emailAddress The user email.
+ */
+ public void setUser(String emailAddress)
+ {
+ this.user = emailAddress;
+ }
+
+ /**
+ * A class to denote export possibilities of a drive file.
+ */
+ public static class ExportAlternative
+ {
+ /**
+ * a short nickname of the file type, typically the file-ending.
+ */
+ private String extension;
+
+ /**
+ * the revised filename if exported to this extension.
+ */
+ private String newFileName;
+
+ /**
+ * the URL to pull from.
+ */
+ private String exportUrl;
+
+ /**
+ * @return a short nickname of the file type, typically the file-ending.
+ */
+ public String getExtension()
+ {
+ return extension;
+ }
+
+ /**
+ * @return the revised filename if exported to this extension.
+ */
+ public String getNewFileName()
+ {
+ return newFileName;
+ }
+
+ /**
+ * @return the URL to pull from.
+ */
+ public String getExportUrl()
+ {
+ return exportUrl;
+ }
+ }
+}
diff --git a/api/src/main/java/com/xwiki/googleapps/GoogleAppsException.java b/api/src/main/java/com/xwiki/googleapps/GoogleAppsException.java
new file mode 100644
index 0000000..2178ebf
--- /dev/null
+++ b/api/src/main/java/com/xwiki/googleapps/GoogleAppsException.java
@@ -0,0 +1,84 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xwiki.googleapps;
+
+import java.io.IOException;
+
+import org.xwiki.stability.Unstable;
+
+import com.xpn.xwiki.XWikiException;
+
+/**
+ * Generic class to denote an exception condition within the GoogleApps code. Wraps XWikiException and IOException.
+ *
+ * @version $Id$
+ * @since 3.0
+ */
+@Unstable
+public class GoogleAppsException extends RuntimeException
+{
+ private static final long serialVersionUID = 3000;
+
+ /**
+ * @param msg Message to denote the error for programmers.
+ * @param wrapped Exception that has caused this one.
+ * @since 3.0
+ */
+ public GoogleAppsException(String msg, Exception wrapped)
+ {
+ super(msg, wrapped);
+ }
+
+ /**
+ * @param msg Message to denote the error for programmers.
+ * @since 3.0
+ */
+ public GoogleAppsException(String msg)
+ {
+ super(msg);
+ }
+
+ /**
+ * @param wrapped Exception that has caused this one.
+ * @since 3.0
+ */
+ public GoogleAppsException(Exception wrapped)
+ {
+ super(wrapped);
+ }
+
+ /**
+ * @return true if the wrapped exception of XWiki origin.
+ * @since 3.0
+ */
+ public boolean isWikiException()
+ {
+ return getCause() instanceof XWikiException;
+ }
+
+ /**
+ * @return true if the wrapped exception of Google origin (for now: any IO-related exception).
+ * @since 3.0
+ */
+ public boolean isGoogleException()
+ {
+ return getCause() instanceof IOException;
+ }
+}
diff --git a/api/src/main/java/com/xwiki/googleapps/GoogleAppsManager.java b/api/src/main/java/com/xwiki/googleapps/GoogleAppsManager.java
new file mode 100644
index 0000000..953b666
--- /dev/null
+++ b/api/src/main/java/com/xwiki/googleapps/GoogleAppsManager.java
@@ -0,0 +1,141 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xwiki.googleapps;
+
+import java.util.List;
+
+import org.xwiki.component.annotation.Role;
+import org.xwiki.stability.Unstable;
+
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.BaseObject;
+
+/**
+ * The specification of the methods that the manager of the GoogleApps application is doing. Methods of this interface
+ * are mostly called by the script-service (itself called by the views).
+ *
+ * @version $Id$
+ * @since 3.0
+ */
+@Role
+public interface GoogleAppsManager
+{
+ /**
+ * @return if the application is licensed and activated
+ * @throws GoogleAppsException in case a context cannot be read from thread.
+ * @since 3.0
+ */
+ @Unstable
+ boolean isActive() throws GoogleAppsException;
+
+ /**
+ * @return if the app is configured to use the Google Drive integration (default: yes).
+ * @since 3.0
+ */
+ @Unstable
+ boolean isDriveEnabled();
+
+ /**
+ * Inspects the stored information to see if an authorization or a redirect needs to be pronounced.
+ *
+ * @param redirect If a redirect can be done
+ * @return if found a credential
+ * @throws GoogleAppsException if a communication problem with the other components occured
+ * @since 3.0
+ */
+ @Unstable
+ boolean authorize(boolean redirect) throws GoogleAppsException;
+
+ /**
+ * Performs the necessary communication with Google-Services to fetch identity and update the XWiki-user object or
+ * possibly sends a redirect to a Google login screen.
+ *
+ * @return "failed login" if failed, "no user" (can be attempted to Google-OAuth), or "ok" if successful
+ * @since 3.0
+ */
+ @Unstable
+ String updateUser();
+
+ /**
+ * Fetches a list of Google Drive document matching a substring query in the filename.
+ *
+ * @param query the expected query (e.g. fullText contains winter ski)
+ * @param nbResults max number of results
+ * @return The list of files at Google Drive.
+ * @throws GoogleAppsException if a communication problem with the other components occured
+ * @since 3.0
+ */
+ @Unstable
+ List listDriveDocuments(String query, int nbResults) throws GoogleAppsException;
+
+ /**
+ * Fetches the google-drive document's representation and stores it as attachment.
+ *
+ * @param page attach to this page
+ * @param name attach using this file name
+ * @param id store object attached to this attachment using this id (for later sync)
+ * @param mediaType content-type of the file to be fetched (or "unknown"; in this case the mediaType is read from
+ * Tika.
+ * @throws GoogleAppsException if a communication problem with the other components occured
+ * @since 3.0
+ */
+ @Unstable
+ void retrieveFileFromGoogle(String page, String name, String id, String mediaType) throws GoogleAppsException;
+
+ /**
+ * Extracts metadata about the Google Drive document corresponding to the named attachment.
+ *
+ * @param pageName The XWiki page where the attachment is
+ * @param fileName The filename of the attachment
+ * @return information about the corresponding Google Drive document
+ * @throws GoogleAppsException if a communication problem with the other components occured
+ * @since 3.0
+ */
+ @Unstable
+ DriveDocMetadata getSyncDocMetadata(String pageName, String fileName) throws GoogleAppsException;
+
+
+ /**
+ * Inserts the current information on the document to be embedded.
+ *
+ * @param docId the identifier of the Google Docs document to be embedded
+ * @param doc the XWiki document where to attach the embedding
+ * @param obj the XWiki object where this embedding is to be updated (or null if it is to be created)
+ * @param nb the number of the embedding across all the page's embeddings
+ * @return the created or actualized document
+ * @throws GoogleAppsException if a communication problem with the other components occured
+ * @since 3.0
+ */
+ @Unstable
+ BaseObject createOrUpdateEmbedObject(String docId, XWikiDocument doc, BaseObject obj, int nb)
+ throws GoogleAppsException;
+
+ /**
+ * Saves the attachment stored in XWiki to the Google drive of the user attached to the current logged-in user.
+ *
+ * @param page the XWiki page name
+ * @param name the attachment name
+ * @return a metadata about the file
+ * @throws GoogleAppsException if a communication problem with the other components occured
+ * @since 3.0
+ */
+ @Unstable
+ DriveDocMetadata saveAttachmentToGoogle(String page, String name) throws GoogleAppsException;
+}
diff --git a/api/src/main/java/com/xwiki/googleapps/GoogleAppsScriptService.java b/api/src/main/java/com/xwiki/googleapps/GoogleAppsScriptService.java
new file mode 100644
index 0000000..e81e540
--- /dev/null
+++ b/api/src/main/java/com/xwiki/googleapps/GoogleAppsScriptService.java
@@ -0,0 +1,177 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xwiki.googleapps;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+import org.xwiki.script.service.ScriptService;
+import org.xwiki.stability.Unstable;
+
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.api.Document;
+import com.xpn.xwiki.api.Object;
+
+/**
+ * Script service containing the methods used by the view files contained in the ui module.
+ *
+ * @version $Id$
+ * @since 3.0
+ */
+@Component
+@Named("googleApps")
+@Singleton
+public class GoogleAppsScriptService implements ScriptService
+{
+ @Inject
+ private GoogleAppsManager manager;
+
+ @Inject
+ private Provider contextProvider;
+
+ /**
+ * @return if the application is licensed and activated
+ * @since 3.0
+ */
+ @Unstable
+ public boolean isActive()
+ {
+ return manager.isActive();
+ }
+
+ /**
+ * @return if the app is configured to use the Google Drive integration (default: yes).
+ * @since 3.0
+ */
+ @Unstable
+ public boolean isDriveEnabled()
+ {
+ return manager.isDriveEnabled();
+ }
+
+ /**
+ * Inspects the stored information to see if an authorization or a redirect needs to be pronounced.
+ *
+ * @param redirect If a redirect can be done
+ * @return if found a credential
+ * @since 3.0
+ */
+ @Unstable
+ public boolean authorize(boolean redirect)
+ {
+ return manager.authorize(redirect);
+ }
+
+ /**
+ * Performs the necessary communication with Google-Services to fetch identity and update the XWiki-user object or
+ * possibly sends a redirect to a Google login screen.
+ *
+ * @return "failed login" if failed, "no user" (can be attempted to Google-OAuth), or "ok" if successful
+ * @since 3.0
+ */
+ @Unstable
+ public String updateUser()
+ {
+ return manager.updateUser();
+ }
+
+ /**
+ * Fetches a list of Google Drive document matching a substring query in the filename. (used in the import
+ * function)
+ *
+ * @param query the expected query (e.g. fullText contains winter ski)
+ * @param nbResults max number of results
+ * @return The list of DriveDocMetadata
+ * @since 3.0
+ */
+ @Unstable
+ public List listDriveDocuments(String query, int nbResults)
+ {
+ return manager.listDriveDocuments(query, nbResults);
+ }
+
+ /**
+ * Inserts the current information on the document to be embedded.
+ *
+ * @param docId the identifier of the Google Docs document to be embedded
+ * @param doc the XWiki document where to attach the embedding
+ * @param obj the XWiki object where this embedding is to be updated (or null if it is to be created)
+ * @param nb the number of the embedding across all the page's embeddings
+ * @return the created or actualized document
+ * @since 3.0
+ */
+ @Unstable
+ public Object createOrUpdateEmbedObject(String docId, Document doc, Object obj, int nb)
+ {
+ return new Object(manager.createOrUpdateEmbedObject(docId, doc.getDocument(),
+ obj == null ? null : obj.getXWikiObject(), nb),
+ contextProvider.get());
+ }
+
+ /**
+ * Fetches the google-drive document's representation and stores it as attachment.
+ *
+ * @param page attach to this page
+ * @param name attach using this file name
+ * @param id store object attached to this attachment using this id (for later sync)
+ * @param mediaType content-type of the file to be fetched (or "unknown"; in this case the mediaType is read from
+ * Tika.
+ * @since 3.0
+ */
+ @Unstable
+ public void retrieveFileFromGoogle(String page, String name, String id, String mediaType)
+ {
+ manager.retrieveFileFromGoogle(page, name, id, mediaType);
+ }
+
+ /**
+ * Extracts metadata about the Google Drive document corresponding to the named attachment.
+ *
+ * @param pageName The XWiki page where the attachment is
+ * @param fileName The filename of the attachment
+ * @return information about the corresponding Google Drive document
+ * @since 3.0
+ */
+ @Unstable
+ public DriveDocMetadata getSyncDocMetadata(String pageName, String fileName)
+ {
+ return manager.getSyncDocMetadata(pageName, fileName);
+ }
+
+ /**
+ * Saves the attachment stored in XWiki to the Google drive of the user attached to the current logged-in user.
+ *
+ * @param page the XWiki page name
+ * @param name the attachment name
+ * @return a record with the keys fileName, exportLink, version, editLink, embedLink, and google-user's
+ * email-address
+ * @since 3.0
+ */
+ @Unstable
+ public DriveDocMetadata saveAttachmentToGoogle(String page, String name)
+ {
+ return manager.saveAttachmentToGoogle(page, name);
+ }
+}
diff --git a/api/src/main/java/org/xwiki/apps/googleapps/internal/CookieAuthenticationPersistenceImpl.java b/api/src/main/java/com/xwiki/googleapps/internal/CookieAuthenticationPersistence.java
similarity index 71%
rename from api/src/main/java/org/xwiki/apps/googleapps/internal/CookieAuthenticationPersistenceImpl.java
rename to api/src/main/java/com/xwiki/googleapps/internal/CookieAuthenticationPersistence.java
index 56b3603..3c66622 100644
--- a/api/src/main/java/org/xwiki/apps/googleapps/internal/CookieAuthenticationPersistenceImpl.java
+++ b/api/src/main/java/com/xwiki/googleapps/internal/CookieAuthenticationPersistence.java
@@ -17,7 +17,7 @@
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
-package org.xwiki.apps.googleapps.internal;
+package com.xwiki.googleapps.internal;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
@@ -27,42 +27,43 @@
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Provider;
+import javax.inject.Singleton;
import javax.servlet.http.Cookie;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
-
import org.slf4j.Logger;
-import org.xwiki.apps.googleapps.CookieAuthenticationPersistence;
import org.xwiki.component.annotation.Component;
-import org.xwiki.component.annotation.InstantiationStrategy;
-import org.xwiki.component.descriptor.ComponentInstantiationStrategy;
-import org.xwiki.component.manager.ComponentManager;
-import org.xwiki.stability.Unstable;
+import org.xwiki.component.phase.Initializable;
+import org.xwiki.configuration.ConfigurationSource;
import com.xpn.xwiki.XWikiContext;
-import com.xpn.xwiki.XWikiException;
+import com.xwiki.googleapps.GoogleAppsException;
/**
- * Tools to help storing and retrieving enriched information within cookies such as the
- * linked Google user profile.
+ * Tools to help storing and retrieving enriched information within cookies such as the linked Google user profile.
+ *
+ * Inspiration: xwiki-authenticator-trusted https://github.com/xwiki-contrib/xwiki-authenticator-trusted/edit/master\
+ * /xwiki-authenticator-trusted-api/src/main/java/org/xwiki/contrib/authentication\
+ * /internal/CookieAuthenticationPersistenceStore.java.
*
- * Copied code from xwiki-authenticator-trusted
- * https://github.com/xwiki-contrib/xwiki-authenticator-trusted/edit/master\
- * /xwiki-authenticator-trusted-api/src/main/java/org/xwiki/contrib/authentication\
- * /internal/CookieAuthenticationPersistenceStore.java.
* @version $Id$
* @since 3.0
*/
-@Component
-@InstantiationStrategy(ComponentInstantiationStrategy.PER_LOOKUP)
-public class CookieAuthenticationPersistenceImpl implements CookieAuthenticationPersistence
+@Component(roles = CookieAuthenticationPersistence.class)
+@Singleton
+public class CookieAuthenticationPersistence implements Initializable
{
private static final String AUTHENTICATION_CONFIG_PREFIX = "xwiki.authentication";
private static final String COOKIE_PREFIX_PROPERTY = AUTHENTICATION_CONFIG_PREFIX + ".cookieprefix";
- private static final String COOKIE_PATH_PROPERTY = AUTHENTICATION_CONFIG_PREFIX + ".cookiepath";
+
+ private static final String COOKIE_PATH_PROPERTY = AUTHENTICATION_CONFIG_PREFIX + ".cookiepath";
+
private static final String COOKIE_DOMAINS_PROPERTY = AUTHENTICATION_CONFIG_PREFIX + ".cookiedomains";
+
private static final String ENCRYPTION_KEY_PROPERTY = AUTHENTICATION_CONFIG_PREFIX + ".encryptionKey";
private static final String CIPHER_ALGORITHM = "TripleDES";
@@ -75,112 +76,114 @@ public class CookieAuthenticationPersistenceImpl implements CookieAuthentication
private static final String COOKIE_DOT_PFX = ".";
private static final String EQUAL_SIGN = "=";
+
private static final String UNDERSCORE = "_";
@Inject
private Logger logger;
- private XWikiContext context;
-
@Inject
- private ComponentManager componentManager;
+ private Provider gaXwikiObjects;
+
+ private String cookiePrefix;
- private String cookiePfx;
private String cookiePath;
+
private String[] cookieDomains;
- private long cookieMaxAge;
+
private Cipher encryptionCipher;
+
private Cipher decryptionCipher;
+ private String encryptionKey;
+
+ @Inject
+ private Provider contextProvider;
+
+ @Inject
+ @Named("xwikicfg")
+ private ConfigurationSource xwikiCfg;
/**
- * Initialize the tool.
- * @param context XWiki Context
- * @param cookieMaxAge Time To Live of the created cookies in scd
- * @throws XWikiException in case of trouble
- * @since 3.0
+ * Builds a configured object.
*/
- @Unstable
- public void initialize(XWikiContext context, long cookieMaxAge) throws XWikiException
+ public void initialize()
{
- this.context = context;
- cookiePfx = this.context.getWiki().Param(COOKIE_PREFIX_PROPERTY, "");
- cookiePath = this.context.getWiki().Param(COOKIE_PATH_PROPERTY, "/");
+ this.cookiePrefix = xwikiCfg.getProperty(COOKIE_PREFIX_PROPERTY, "");
+ this.cookiePath = xwikiCfg.getProperty(COOKIE_PATH_PROPERTY, "/");
+ this.encryptionKey = xwikiCfg.getProperty(ENCRYPTION_KEY_PROPERTY);
+
+ String[] cdlist = StringUtils.split(xwikiCfg.getProperty(COOKIE_DOMAINS_PROPERTY), ',');
- String[] cdlist = StringUtils.split(this.context.getWiki().Param(COOKIE_DOMAINS_PROPERTY), ',');
if (cdlist != null && cdlist.length > 0) {
this.cookieDomains = new String[cdlist.length];
for (int i = 0; i < cdlist.length; ++i) {
- cookieDomains[i] = conformCookieDomain(cdlist[i]);
+ this.cookieDomains[i] = conformCookieDomain(cdlist[i]);
}
} else {
- cookieDomains = null;
+ this.cookieDomains = null;
}
- this.cookieMaxAge = cookieMaxAge;
-
try {
encryptionCipher = getCipher(true);
decryptionCipher = getCipher(false);
} catch (Exception e) {
- throw new XWikiException("Unable to initialize ciphers", e);
+ throw new GoogleAppsException("Unable to initialize ciphers", e);
}
}
/**
* Erases the information stored.
+ *
* @since 3.0
*/
- @Unstable
- public void clear()
+ void clear()
{
- cookieMaxAge = 0;
- this.store(this.retrieve());
+ this.setUserId("XWikiGuest");
}
/**
- * Store the user-information within the cookie.
- * @param userUid the user-name (without xwiki. prefix)
+ * Retrieving the login read from the cookie.
+ *
+ * @return the login name found, or null.
* @since 3.0
*/
- @Unstable
- public void store(String userUid)
+ String getUserId()
{
- Cookie cookie = new Cookie(cookiePfx + AUTHENTICATION_COOKIE, encryptText(userUid));
- cookie.setMaxAge((int) cookieMaxAge);
- cookie.setPath(cookiePath);
- String cookieDomain = getCookieDomain();
- if (cookieDomain != null) {
- cookie.setDomain(cookieDomain);
- }
- if (context.getRequest().isSecure()) {
- cookie.setSecure(true);
+ logger.info("retrieve cookie " + cookiePrefix + AUTHENTICATION_COOKIE);
+ String cookie = getCookieValue(cookiePrefix + AUTHENTICATION_COOKIE);
+ if (cookie != null) {
+ return decryptText(cookie);
}
- context.getResponse().addCookie(cookie);
+ return null;
}
/**
- * Retrieving the login read from the cookie.
+ * Store the user-information within the cookie.
*
- * @return the login name found, or null.
+ * @param userUid the user-name (without xwiki. prefix)
* @since 3.0
*/
- @Unstable
- public String retrieve()
+ void setUserId(String userUid)
{
- logger.info("retrieve cookie " + cookiePfx + AUTHENTICATION_COOKIE);
- String cookie = getCookieValue(cookiePfx + AUTHENTICATION_COOKIE);
- if (cookie != null) {
- return decryptText(cookie);
+ Cookie cookie = new Cookie(cookiePrefix + AUTHENTICATION_COOKIE, encryptText(userUid));
+ cookie.setMaxAge(gaXwikiObjects.get().getConfigCookiesTTL());
+ cookie.setPath(cookiePath);
+ String cookieDomain = getCookieDomain();
+ if (cookieDomain != null) {
+ cookie.setDomain(cookieDomain);
}
- return null;
+ if (contextProvider.get().getRequest().isSecure()) {
+ cookie.setSecure(true);
+ }
+ contextProvider.get().getResponse().addCookie(cookie);
}
private Cipher getCipher(boolean encrypt)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException
{
Cipher cipher = null;
- String secretKey = context.getWiki().Param(ENCRYPTION_KEY_PROPERTY);
+ String secretKey = encryptionKey;
if (secretKey != null) {
secretKey = secretKey.substring(0, 24);
SecretKeySpec key = new SecretKeySpec(secretKey.getBytes(), CIPHER_ALGORITHM);
@@ -200,8 +203,8 @@ private String encryptText(String text)
return encryptedText;
} catch (Exception e) {
logger.error("Failed to encrypt text", e);
+ return null;
}
- return null;
}
private String decryptText(String text)
@@ -210,25 +213,26 @@ private String decryptText(String text)
logger.info("text to decrypt : " + text);
String decryptedText = new String(decryptionCipher.doFinal(
Base64.decodeBase64(text.replaceAll(UNDERSCORE, EQUAL_SIGN).getBytes(
- StandardCharsets.ISO_8859_1))));
+ StandardCharsets.ISO_8859_1))));
logger.info("decrypted text : " + decryptedText);
return decryptedText;
} catch (Exception e) {
logger.error("Failed to decrypt text", e);
+ return null;
}
- return null;
}
/**
* Retrieve given cookie null-safe.
+ *
* @param cookieName name of the cookie
* @return the cookie
* @since 3.0
*/
private String getCookieValue(String cookieName)
{
- if (context.getRequest() != null) {
- Cookie cookie = context.getRequest().getCookie(cookieName);
+ if (contextProvider.get().getRequest() != null) {
+ Cookie cookie = contextProvider.get().getRequest().getCookie(cookieName);
if (cookie != null) {
logger.info("cookie : " + cookie);
return cookie.getValue();
@@ -252,7 +256,7 @@ private String getCookieDomain()
// Conform the server name like we conform cookie domain by prefixing with a dot.
// This will ensure both localhost.localdomain and any.localhost.localdomain will match
// the same cookie domain.
- String servername = conformCookieDomain(context.getRequest().getServerName());
+ String servername = conformCookieDomain(contextProvider.get().getRequest().getServerName());
for (String domain : this.cookieDomains) {
if (servername.endsWith(domain)) {
cookieDomain = domain;
diff --git a/api/src/main/java/org/xwiki/apps/googleapps/internal/GoogleAppsAuthServiceImpl.java b/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsAuthService.java
similarity index 71%
rename from api/src/main/java/org/xwiki/apps/googleapps/internal/GoogleAppsAuthServiceImpl.java
rename to api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsAuthService.java
index 5de3410..39dc347 100644
--- a/api/src/main/java/org/xwiki/apps/googleapps/internal/GoogleAppsAuthServiceImpl.java
+++ b/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsAuthService.java
@@ -17,15 +17,7 @@
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
-package org.xwiki.apps.googleapps.internal;
-
-
-import com.xpn.xwiki.XWikiContext;
-import com.xpn.xwiki.XWikiException;
-import com.xpn.xwiki.doc.XWikiDocument;
-import com.xpn.xwiki.user.api.XWikiUser;
-import com.xpn.xwiki.user.impl.xwiki.XWikiAuthServiceImpl;
-import com.xpn.xwiki.web.XWikiRequest;
+package com.xwiki.googleapps.internal;
import java.net.URLEncoder;
import java.security.Principal;
@@ -34,45 +26,60 @@
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
+import javax.inject.Singleton;
import javax.servlet.http.HttpSession;
import org.securityfilter.realm.SimplePrincipal;
import org.slf4j.Logger;
-import org.xwiki.apps.googleapps.CookieAuthenticationPersistence;
-import org.xwiki.apps.googleapps.GoogleAppsAuthService;
import org.xwiki.component.annotation.Component;
-import org.xwiki.component.annotation.InstantiationStrategy;
-import org.xwiki.component.descriptor.ComponentInstantiationStrategy;
-import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.component.phase.Initializable;
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.container.servlet.filters.SavedRequestManager;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.text.StringUtils;
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.XWikiException;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.user.api.XWikiUser;
+import com.xpn.xwiki.user.impl.xwiki.XWikiAuthServiceImpl;
+import com.xpn.xwiki.web.XWikiRequest;
+
/**
- * An authenticator that can include a negotiation with the Google Cloud (e.g. Google Drive) services.
- * This authenticator is created, configured and maintained by the GoogleAppsScriptService.
- * @since 3.0
+ * An authenticator that can include a negotiation with the Google Cloud (e.g. Google Drive) services. This
+ * authenticator is created, configured and maintained by the GoogleAppsScriptService.
+ *
* @version $Id$
+ * @since 3.0
*/
-@Component
-@InstantiationStrategy(ComponentInstantiationStrategy.PER_LOOKUP)
-public class GoogleAppsAuthServiceImpl extends XWikiAuthServiceImpl
- implements GoogleAppsAuthService
+@Component(roles = GoogleAppsAuthService.class)
+@Singleton
+public class GoogleAppsAuthService extends XWikiAuthServiceImpl implements Initializable
{
-
private static final String XWIKISPACE = "XWiki.";
@Inject
private Logger log;
@Inject
- private ComponentManager componentManager;
+ private GoogleAppsXWikiObjects gaXwikiObjects;
- private GoogleAppsManagerImpl googleAppsManager;
+ @Inject
+ private CookieAuthenticationPersistence cookiePersistance;
+
+ @Inject
+ @Named("xwikicfg")
+ private Provider xwikiCfg;
+
+ private Pattern logoutRequestMatcher;
- void setGoogleAppsManager(GoogleAppsManagerImpl m) {
- this.googleAppsManager = m;
+ /**
+ * Reads the configuration.
+ */
+ public void initialize()
+ {
+ this.logoutRequestMatcher = Pattern.compile(
+ xwikiCfg.get().getProperty("xwiki.authentication.logoutpage", ""));
}
/**
@@ -82,14 +89,13 @@ void setGoogleAppsManager(GoogleAppsManagerImpl m) {
* @return a valid user, if found.
* @throws XWikiException if anything went wrong
*/
- public XWikiUser checkAuth(XWikiContext context) throws XWikiException {
+ public XWikiUser checkAuth(XWikiContext context) throws XWikiException
+ {
try {
log.info("GoogleApps authentificator - checkAuth");
if (isLogoutRequest(context)) {
log.info("caught a logout request");
- CookieAuthenticationPersistence cookieTools =
- componentManager.getInstance(CookieAuthenticationPersistence.class);
- cookieTools.clear();
+ cookiePersistance.clear();
log.info("cleared cookie");
}
return super.checkAuth(context);
@@ -101,42 +107,39 @@ public XWikiUser checkAuth(XWikiContext context) throws XWikiException {
/**
* Checks authentication.
- * i
- * @param username the name of the user to verify against
- * @param password the password of the user to verify against
+ *
+ * @param username the name of the user to verify against
+ * @param password the password of the user to verify against
* @param rememberme insert-cookies to remember the login
- * @param context the context containing the request
+ * @param context the context containing the request
* @return an XWikiUser is it succeded.
* @throws XWikiException in case something goes wrong
*/
public XWikiUser checkAuth(String username, String password,
- String rememberme, XWikiContext context) throws XWikiException {
+ String rememberme, XWikiContext context) throws XWikiException
+ {
return super.checkAuth(username, password, rememberme, context);
}
-
-
/**
* Redirect user to the login.
*
* @param context the xwiki-context of the request
* @throws XWikiException a wrapped exception
*/
- public void showLogin(XWikiContext context) throws XWikiException {
- log.info("GoogleApps authentificator - showLogin");
- if (!googleAppsManager.isActive(context)) {
+ public void showLogin(XWikiContext context) throws XWikiException
+ {
+ log.debug("GoogleApps authentificator - showLogin");
+ if (!gaXwikiObjects.isActive()) {
return;
}
boolean redirected = false;
try {
String url = context.getWiki().getExternalURL("GoogleApps.Login", "view", context);
- if (googleAppsManager.useCookies() && googleAppsManager.skipLoginPage()) {
+ if (gaXwikiObjects.doesUseCookies() && gaXwikiObjects.doesSkipLoginPage()) {
log.info("skip the login page ");
XWikiRequest request = context.getRequest();
- CookieAuthenticationPersistence cookieTools =
- componentManager.getInstance(CookieAuthenticationPersistence.class);
- cookieTools.initialize(context, googleAppsManager.getConfigCookiesTTL());
- String userCookie = cookieTools.retrieve();
+ String userCookie = cookiePersistance.getUserId();
log.info("retrieved user from cookie : " + userCookie);
String savedRequestId = request.getParameter(
SavedRequestManager.getSavedRequestIdentifier());
@@ -160,7 +163,7 @@ public void showLogin(XWikiContext context) throws XWikiException {
String finalURL = url + "?" + sridParameter + "&xredirect="
+ URLEncoder.encode(redirectBack.toString(), "UTF-8");
- log.info("Redirecting to " + finalURL);
+ log.info("Redirecting to " + finalURL);
redirected = true;
context.getResponse().sendRedirect(finalURL);
}
@@ -176,19 +179,21 @@ public void showLogin(XWikiContext context) throws XWikiException {
/**
* Processes a password entry and creates the appropriate principal.
+ *
* @param username the provided user-name
* @param password the provided password
- * @param context the context describing the request
+ * @param context the context describing the request
* @return a null Principal Object if the user hasn't been authenticated or a valid Principal Object if the user is
- * correctly authenticated
+ * correctly authenticated
* @throws XWikiException if something goes wrong.
*/
- public Principal authenticate(String username, String password, XWikiContext context) throws XWikiException {
+ public Principal authenticate(String username, String password, XWikiContext context) throws XWikiException
+ {
try {
log.info("GoogleApps authentificator - authenticate");
// case of a too early call or deactivated... can only count on local users
- if (googleAppsManager == null || !googleAppsManager.isActive(context)) {
+ if (!gaXwikiObjects.isActive()) {
return super.authenticate(username, password, context);
}
@@ -198,17 +203,16 @@ public Principal authenticate(String username, String password, XWikiContext con
// get configuration for authentification with cookies
// authenticate user from cookie value
- if (xwikiUser == null && googleAppsManager.useCookies() && googleAppsManager.authWithCookies()) {
+ if (xwikiUser == null && gaXwikiObjects.doesUseCookies()
+ && gaXwikiObjects.doesAuthWithCookies())
+ {
log.info("Authenticate with cookie");
- CookieAuthenticationPersistence cookieTools =
- componentManager.getInstance(CookieAuthenticationPersistence.class);
- cookieTools.initialize(context, googleAppsManager.getConfigCookiesTTL());
- String userCookie = cookieTools.retrieve();
+ String userCookie = cookiePersistance.getUserId();
if (userCookie != null) {
log.info("Found user from cookie : " + userCookie);
- DocumentReference userDocRef = googleAppsManager.createUserReference(username);
+ DocumentReference userDocRef = gaXwikiObjects.createUserReference(username);
XWikiDocument userDoc = context.getWiki().getDocument(userDocRef, context);
- if (!userDoc.isNew()) {
+ if (!userDoc.isNew()) {
xwikiUser = userDocRef.getName();
}
log.info("xwikiUser from cookie : " + xwikiUser);
@@ -230,23 +234,11 @@ public Principal authenticate(String username, String password, XWikiContext con
}
}
- private Pattern logoutRequestMatcher;
-
- @Inject
- @Named("xwikicfg")
- private Provider xwikicfgProvider;
-
/**
* @return true if the current request match the configured logout page pattern.
*/
- private boolean isLogoutRequest(XWikiContext context) {
- if (logoutRequestMatcher == null) {
- if (xwikicfgProvider == null) {
- return false;
- }
- String patt = xwikicfgProvider.get().getProperty("xwiki.authentication.logoutpage");
- logoutRequestMatcher = Pattern.compile(patt);
- }
+ private boolean isLogoutRequest(XWikiContext context)
+ {
return logoutRequestMatcher.matcher(context.getRequest().getPathInfo()).matches();
}
}
diff --git a/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsConstants.java b/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsConstants.java
new file mode 100644
index 0000000..a175fb3
--- /dev/null
+++ b/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsConstants.java
@@ -0,0 +1,131 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xwiki.googleapps.internal;
+
+/**
+ * A set of string constants used across the classes of the app.
+ *
+ * @version $Id$
+ * @since 3.0
+ */
+public interface GoogleAppsConstants
+{
+ /**
+ * avatar.
+ */
+ String AVATAR = "avatar";
+ /**
+ * user.
+ */
+ String USER = "user";
+ /**
+ * GoogleApps.
+ */
+ String SPACENAME = "GoogleApps";
+ /**
+ * view.
+ */
+ String VIEWACTION = "view";
+ /**
+ * xwiki.
+ */
+ String WIKINAME = "xwiki";
+ /**
+ * id.
+ */
+ String ID = "id";
+ /**
+ * fileName.
+ */
+ String FILENAME = "fileName";
+ /**
+ * version.
+ */
+ String VERSION = "version";
+ /**
+ * url.
+ */
+ String URL = "url";
+ /**
+ * exportLink.
+ */
+ String EXPORTLINK = "exportLink";
+ /**
+ * editLink.
+ */
+ String EDITLINK = "editLink";
+ /**
+ * embedLink.
+ */
+ String EMBEDLINK = "embedLink";
+ /**
+ * Comment used when saving a google-document's information.
+ */
+ String UPDATECOMMENT = "Updated Google Apps Document metadata";
+ /**
+ * exportFormat=.
+ */
+ String EXPORTFORMATEQ = "exportFormat=";
+ /**
+ * The name of the XWiki space.
+ */
+ String XWIKISPACE = "XWiki";
+ /**
+ * The name of the XWiki login page.
+ */
+ String XWIKILOGIN = "XWikiLogin";
+ /**
+ * The name of the guest user in XWiki.
+ */
+ String XWIKIGUEST = "XWikiGuest";
+ /**
+ * auto.
+ */
+ String AUTOAPPROVAL = "auto";
+ /**
+ * email.
+ */
+ String EMAIL = "email";
+ /**
+ * password.
+ */
+ String PASSWORD = "password";
+ /**
+ * first_name.
+ */
+ String FIRSTNAME = "first_name";
+ /**
+ * last_name.
+ */
+ String LASTNAME = "last_name";
+ /**
+ * OAuth.
+ */
+ String OAUTH = "OAuth";
+ /**
+ * "failed login": a constant sent to the UI to indicate a failed login.
+ */
+ String FAILEDLOGIN = "failed login";
+ /**
+ * "no user": a constant sent to the UI to indicate that Google could not give us a user, e.g. because the user
+ * rejected the authorization request.
+ */
+ String NOUSER = "no user";
+}
diff --git a/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsEventListener.java b/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsEventListener.java
new file mode 100644
index 0000000..10e4ee7
--- /dev/null
+++ b/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsEventListener.java
@@ -0,0 +1,100 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xwiki.googleapps.internal;
+
+import java.util.Arrays;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.xwiki.bridge.event.ApplicationReadyEvent;
+import org.xwiki.bridge.event.DocumentUpdatedEvent;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.observation.EventListener;
+import org.xwiki.observation.event.Event;
+
+import com.xpn.xwiki.doc.XWikiDocument;
+
+/**
+ * Registered object to listen to document changes.
+ *
+ * @version $Id$
+ * @since 3.0
+ */
+@Component(roles = GoogleAppsEventListener.class)
+@Singleton
+public class GoogleAppsEventListener implements EventListener
+{
+ @Inject
+ private GoogleAppsXWikiObjects gaXWikiObjects;
+
+ /**
+ * The name of the event listener.
+ *
+ * @return googleapps.scriptservice.
+ */
+ @Override
+ public String getName()
+ {
+ return "googleapps.scriptservice";
+ }
+
+ /**
+ * The event-types listened to.
+ *
+ * @return ApplicationReadyEvent and DocumentUpdatedEvent
+ */
+ @Override
+ public List getEvents()
+ {
+ return Arrays.asList(new ApplicationReadyEvent(), new DocumentUpdatedEvent());
+ }
+
+ /**
+ * Triggers a configuration reload (if the configuration is changed or the app is started) or an initialization (if
+ * the app is started).
+ *
+ * @param event The event listened to.
+ * @param source The object sending the event.
+ * @param data Data about the event.
+ */
+ @Override
+ public void onEvent(Event event, Object source, Object data)
+ {
+ boolean applicationStarted = false;
+ boolean configChanged = false;
+ if (event instanceof ApplicationReadyEvent) {
+ applicationStarted = true;
+ }
+ if (event instanceof DocumentUpdatedEvent) {
+ XWikiDocument document = (XWikiDocument) source;
+ DocumentReference configDocRef = gaXWikiObjects.getConfigDocRef();
+ if (document != null && document.getDocumentReference().equals(configDocRef)) {
+ configChanged = true;
+ }
+ }
+
+ if (configChanged || applicationStarted) {
+ gaXWikiObjects.restart();
+ }
+ }
+}
diff --git a/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsIdentity.java b/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsIdentity.java
new file mode 100644
index 0000000..a7e77a2
--- /dev/null
+++ b/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsIdentity.java
@@ -0,0 +1,444 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xwiki.googleapps.internal;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+import javax.servlet.http.HttpSession;
+
+import org.slf4j.Logger;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.model.reference.DocumentReference;
+
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
+import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeRequestUrl;
+import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
+import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.client.util.store.FileDataStoreFactory;
+import com.google.api.services.drive.DriveScopes;
+import com.google.api.services.people.v1.PeopleService;
+import com.google.api.services.people.v1.PeopleServiceScopes;
+import com.google.api.services.people.v1.model.EmailAddress;
+import com.google.api.services.people.v1.model.Person;
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.web.XWikiRequest;
+import com.xwiki.googleapps.GoogleAppsException;
+
+/**
+ * Set of objects to recognize and read the identity of the user.
+ * @since 3.0
+ * @version $Id$
+ */
+@Component(roles = GoogleAppsIdentity.class)
+@Singleton
+public class GoogleAppsIdentity implements GoogleAppsConstants
+{
+ /**
+ * A map of hash to full redirects.
+ */
+ private final Map storedStates = new HashMap<>();
+
+ @Inject
+ private Logger log;
+
+ @Inject
+ private Provider contextProvider;
+
+ @Inject
+ private Provider gaXwikiObjects;
+
+ @Inject
+ private Provider cookiePersistence;
+
+
+ private JacksonFactory jacksonFactory;
+
+ private NetHttpTransport httpTransport;
+
+ private FileDataStoreFactory dsFactory;
+
+
+
+ private void initIfNeedBe() {
+ if (this.jacksonFactory==null) {
+ try {
+ this.jacksonFactory = JacksonFactory.getDefaultInstance();
+ this.httpTransport = GoogleNetHttpTransport.newTrustedTransport();
+ this.dsFactory = new FileDataStoreFactory(gaXwikiObjects.get().getPermanentDir());
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new GoogleAppsException("Trouble at constructing GoogleAppsIdentity", e);
+ }
+ }
+ }
+
+
+ String updateUser()
+ {
+ initIfNeedBe();
+ try {
+ if (!gaXwikiObjects.get().isActive()) {
+ return FAILEDLOGIN;
+ }
+ log.debug("Updating user...");
+ Credential credential = authorize(true);
+
+ Person gUser = null;
+ if (credential != null) {
+ PeopleService pservice = new PeopleService.Builder(httpTransport,
+ jacksonFactory, credential)
+ .setApplicationName(gaXwikiObjects.get().getConfigAppName())
+ .build();
+ gUser = pservice.people().get("people/me").setPersonFields("emailAddresses,names,photos").execute();
+ // GOOGLEAPPS: User: [displayName:..., emails:[[type:account, value:...]], etag:"...",
+ // id:...., image:[isDefault:false, url:https://...], kind:plus#person, language:en,
+ // name:[familyName:..., givenName:...]]
+ }
+ log.debug("user on google: " + gUser);
+ if (gUser == null) {
+ return NOUSER;
+ }
+
+ String xwikiUser;
+ List emails = new ArrayList<>();
+ // grab emailaddresses
+ if (gUser.getEmailAddresses() != null) {
+ for (EmailAddress address : gUser.getEmailAddresses()) {
+ emails.add(address.getValue());
+ }
+ }
+ String email = checkDomain(emails);
+ if (email == null) {
+ return FAILEDLOGIN;
+ }
+
+ String firstName = null;
+ String lastName = null;
+ if (gUser.getNames() != null) {
+ firstName = gUser.getNames().get(0).getGivenName();
+ lastName = gUser.getNames().get(0).getFamilyName();
+ }
+
+ String googleUserId = (String) gUser.get("resourceName");
+
+ String photoUrl = extractPhotoUrl(gUser);
+ xwikiUser = gaXwikiObjects.get().updateXWikiUser(googleUserId, emails, email,
+ firstName, lastName, photoUrl);
+
+ // we need to restore the credentials as the user will now be logged-in
+ storeCredentials(xwikiUser, credential);
+
+ // store the validated xwiki user for the authentication module
+ // TODO: discuss: Is this not a security risk?
+ contextProvider.get().getRequest().getSession().setAttribute("googleappslogin", xwikiUser);
+ return "ok";
+ } catch (Exception e) {
+ log.warn("Problem at updateUser", e);
+ return NOUSER;
+ }
+ }
+
+ private String checkDomain(List emails)
+ {
+ String email = null;
+ String domain = gaXwikiObjects.get().getConfigDomain();
+ if (domain != null && domain.length() > 0) {
+ domain = domain.trim();
+ for (String address : emails) {
+ if (address.endsWith(domain)) {
+ email = address;
+ break;
+ }
+ }
+ if (email == null) {
+ String userId = getCurrentXWikiUserName();
+ getCredentialStore().remove(userId);
+ log.debug("Wrong domain: Removed credentials for userid " + userId);
+ return null;
+ }
+ }
+ return emails.isEmpty() ? null : emails.get(0);
+ }
+
+ private String extractPhotoUrl(Person gUser)
+ {
+ if (gaXwikiObjects.get().getConfigScopeUseAvatar()
+ && gUser.getPhotos() != null
+ && gUser.getPhotos().size() > 0
+ && gUser.getPhotos().get(0).getUrl() != null)
+ {
+ String photoUrl = gUser.getPhotos().get(0).getUrl();
+ log.debug("Avatar " + photoUrl);
+ return photoUrl;
+ }
+ return null;
+ }
+
+ private String getOAuthUrl()
+ {
+ try {
+ XWikiContext context = contextProvider.get();
+
+ DocumentReference loginPage = new DocumentReference(context.getWikiId(),
+ XWIKISPACE, XWIKILOGIN);
+ String u = context.getWiki().getDocument(loginPage, context).getExternalURL("login",
+ "googleLogin=oauthReturn", context);
+ return u;
+ } catch (Exception e) {
+ throw new GoogleAppsException("Trouble at getting OAuth URL", e);
+ }
+ }
+
+ private String getCurrentXWikiUserName()
+ {
+ initIfNeedBe();
+ DocumentReference userDoc = contextProvider.get().getUserReference();
+ String uName = userDoc == null ? XWIKIGUEST : userDoc.getName();
+ if (XWIKIGUEST.equals(uName)) {
+ uName = uName + "-" + contextProvider.get().getRequest().getSession().hashCode();
+ }
+ return uName;
+ }
+
+ /**
+ * Build flow and trigger user authorization request.
+ *
+ * @return the configured flow
+ * @throws GoogleAppsException in case something can't be built
+ */
+ private GoogleAuthorizationCodeFlow getFlow()
+ {
+ initIfNeedBe();
+ try {
+ // create scopes from config
+ List gScopes = new ArrayList<>();
+ gScopes.add(PeopleServiceScopes.USERINFO_EMAIL);
+ gScopes.add(PeopleServiceScopes.USERINFO_PROFILE);
+ if (gaXwikiObjects.get().doesConfigScopeUseDrive()) {
+ gScopes.add(DriveScopes.DRIVE);
+ }
+
+ // create flow
+ return new GoogleAuthorizationCodeFlow.Builder(
+ httpTransport,
+ jacksonFactory, gaXwikiObjects.get().getConfigClientId(),
+ gaXwikiObjects.get().getConfigClientSecret(), gScopes)
+ .setDataStoreFactory(dsFactory)
+ .setAccessType("online").setApprovalPrompt(AUTOAPPROVAL)
+ .setClientId(gaXwikiObjects.get().getConfigClientId())
+ .build();
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new GoogleAppsException("Issue at building Google Authorization Flow.", e);
+ }
+ }
+
+ /**
+ * Exchange an authorization code for OAuth 2.0 credentials.
+ *
+ * @param authorizationCode Authorization code to exchange for OAuth 2.0 credentials.
+ * @return OAuth 2.0 credentials.
+ */
+ private Credential exchangeCode(String authorizationCode)
+ {
+ initIfNeedBe();
+ try {
+ GoogleAuthorizationCodeFlow flow = getFlow();
+ GoogleTokenResponse tokenResponse = flow
+ .newTokenRequest(authorizationCode)
+ .setRedirectUri(getOAuthUrl())
+ .execute();
+ log.debug("Token: " + tokenResponse);
+ return flow.createAndStoreCredential(tokenResponse, getCurrentXWikiUserName());
+ } catch (Exception ex) {
+ throw new GoogleAppsException("Trouble at exchanging authorization code", ex);
+ }
+ }
+
+ private Map getCredentialStore()
+ {
+ initIfNeedBe();
+ final String key = "GoogleAppsCredentialStore";
+ HttpSession session = contextProvider.get().getRequest().getSession(true);
+ Map store = (Map) (session.getAttribute(key));
+ if (store == null) {
+ store = new HashMap<>();
+ session.setAttribute(key, store);
+ }
+ return store;
+ }
+
+ private void storeCredentials(String userId, Credential credentials)
+ {
+ try {
+ if (userId.contains(XWIKIGUEST)) {
+ if (gaXwikiObjects.get().doesUseCookies()) {
+ cookiePersistence.get().setUserId(userId);
+ }
+ }
+ log.debug("Storing credentials for user " + userId + " (" + credentials + ").");
+ getCredentialStore().put(userId, credentials);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new GoogleAppsException("Issue at storing credential.", e);
+ }
+ }
+
+ private Credential getStoredCredentials(String userId)
+ {
+ if (userId == null) {
+ return null;
+ }
+ log.debug("Getting credentials for user " + userId);
+ return getCredentialStore().get(userId);
+ }
+
+ /**
+ * Retrieve credentials using the provided authorization code.
+ *
+ * This function exchanges the authorization code for an access token and queries the UserInfo API to retrieve the
+ * user's e-mail address. If a refresh token has been retrieved along with an access token, it is stored in the
+ * application database using the user's e-mail address as key. If no refresh token has been retrieved, the function
+ * checks in the application database for one and returns it if found or throws a NoRefreshTokenException with the
+ * authorization URL to redirect the user to.
+ *
+ * @param authorizationCode Authorization code to use to retrieve an access token.
+ * @return OAuth 2.0 credentials instance containing an access and refresh token.
+ * @throws GoogleAppsException Unable to load client_secret.json.
+ */
+ private Credential retrieveCredentials(String authorizationCode, boolean redirect)
+ {
+ try {
+ Credential credentials;
+ String user = getCurrentXWikiUserName();
+
+ if (authorizationCode != null && authorizationCode.length() > 0) {
+ log.debug("Trying to get credentials from authorization code: " + authorizationCode);
+ credentials = exchangeCode(authorizationCode);
+ if (credentials != null) {
+ if (credentials.getRefreshToken() != null) {
+ log.debug("Refresh token has been created.");
+ } else {
+ log.debug("Failure to create refresh token");
+ }
+ storeCredentials(user, credentials);
+ return credentials;
+ }
+ }
+
+ log.debug("No credentials found. Checking stored credentials for user " + user);
+ credentials = getStoredCredentials(user);
+ if (credentials != null) {
+ log.debug("Retrieved stored credentials");
+ return credentials;
+ }
+ log.debug("Could not find stored credentials");
+
+ log.debug("No credentials retrieved.");
+ // No refresh token has been retrieved.
+ if (redirect) {
+ log.debug("Redirecting to authorization URL.");
+ contextProvider.get().getResponse().sendRedirect(getAuthorizationURL());
+ }
+ return null;
+ } catch (Exception e) {
+ throw new GoogleAppsException("Trouble at retrieving credentials", e);
+ }
+ }
+
+ private String getAuthorizationURL()
+ {
+ try {
+ String state = "";
+ XWikiContext context = contextProvider.get();
+ XWikiRequest request = context.getRequest();
+ DocumentReference ref = context.getDoc().getDocumentReference();
+ if (!(XWIKILOGIN.equals(ref.getName()) && XWIKISPACE.equals(ref.getLastSpaceReference().getName()))) {
+
+ String finalRedirect = context.getDoc().getURL(VIEWACTION, request.getQueryString(), context);
+ state = Integer.toHexString(finalRedirect.hashCode());
+ storedStates.put(state, finalRedirect);
+ }
+
+ GoogleAuthorizationCodeRequestUrl urlBuilder = getFlow()
+ .newAuthorizationUrl()
+ .setRedirectUri(getOAuthUrl())
+ .setState(state).setClientId(gaXwikiObjects.get().getConfigClientId())
+ .setAccessType("offline").setApprovalPrompt(AUTOAPPROVAL);
+ // Add user email to filter account if the user is logged with multiple account
+ if (gaXwikiObjects.get().doesUseCookies()) {
+ String userId = cookiePersistence.get().getUserId();
+ if (userId != null) {
+ String userEmail = gaXwikiObjects.get().getUserEmail(userId);
+ if (userEmail != null) {
+ urlBuilder = urlBuilder.set("login_hint", userEmail);
+ }
+ }
+ }
+ String authurl = urlBuilder.build();
+ log.debug("google authentication url : " + authurl);
+ return authurl;
+ } catch (Exception ex) {
+ throw new GoogleAppsException("trouble at getAuthorizationURL", ex);
+ }
+ }
+
+ /**
+ * Inspects the stored information to see if an authorization or a redirect needs to be pronounced.
+ *
+ * @param redirect If a redirect can be done
+ * @return found credential
+ * @since 3.0
+ */
+ public Credential authorize(boolean redirect)
+ {
+ initIfNeedBe();
+ try {
+ log.debug("In authorize");
+ // TODO: useless? GoogleAuthorizationCodeFlow flow =
+ getFlow();
+ XWikiContext context = contextProvider.get();
+ XWikiRequest request = context.getRequest();
+ String state = request.getParameter("code");
+ Credential creds = retrieveCredentials(state, redirect);
+ log.debug("Got credentials: " + creds);
+ if (state != null && state.length() > 0) {
+ String url = storedStates.get(state);
+ if (url != null) {
+ log.debug("Redirecting to final destination after authorization: " + url);
+ context.getResponse().sendRedirect(url);
+ }
+ }
+ return creds;
+ } catch (Exception e) {
+ log.warn("Trouble in authorize", e);
+ throw new GoogleAppsException(e);
+ }
+ }
+}
diff --git a/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsManagerImpl.java b/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsManagerImpl.java
new file mode 100644
index 0000000..cb49142
--- /dev/null
+++ b/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsManagerImpl.java
@@ -0,0 +1,354 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xwiki.googleapps.internal;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.component.phase.Disposable;
+import org.xwiki.component.phase.Initializable;
+
+import com.xpn.xwiki.XWiki;
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.BaseObject;
+import com.xwiki.googleapps.DriveDocMetadata;
+import com.xwiki.googleapps.GoogleAppsException;
+import com.xwiki.googleapps.GoogleAppsManager;
+
+/**
+ * Set of methods accessible to the scripts using the GoogleApps functions. The manager is the entry point for the set
+ * of classes of the GoogleApps functions.
+ *
+ * Just as other classes of this package, it is initialized as a component and thus injected with environment objects
+ * such as the logger or context-provider. It is then started at its first invocation and starts the connected
+ * components. This class, exposed through its interface {@GoogleAppsManager} and the authenticator functions in
+ * {@GoogleAppsAuthServiceImpl} is the only one exposing public APIs.
+ *
+ * @version $Id$
+ * @since 3.0
+ */
+@Component
+@Singleton
+public class GoogleAppsManagerImpl
+ implements GoogleAppsManager, Initializable, Disposable, GoogleAppsConstants
+{
+ // initialisation state
+
+ private LifeCycle lifeCycleState = LifeCycle.CONSTRUCTED;
+
+ // own components
+ @Inject
+ private Provider gaIdentity;
+
+ @Inject
+ private Provider gaXWikiObjects;
+
+ @Inject
+ private Provider gaDriveAccess;
+
+ @Inject
+ private Provider authService;
+
+ // ------ services from the environment
+ @Inject
+ private Provider xwikiContextProvider;
+
+ @Inject
+ private Logger log;
+
+ @Override
+ public void initialize()
+ {
+ log.info("GoogleAppsScriptService initializing (but not yet starting).");
+ updateLifeCycle(LifeCycle.INITIALIZED);
+ }
+
+ // ------ own objects
+
+ private void updateLifeCycle(LifeCycle lf)
+ {
+ log.info("Lifecycle to " + lf);
+ lifeCycleState = lf;
+ }
+
+ private void startIfNeedBe()
+ {
+ if (lifeCycleState == LifeCycle.RUNNING) {
+ return;
+ }
+ if (lifeCycleState != LifeCycle.INITIALIZED) {
+ throw new IllegalStateException("Can't start when in state " + lifeCycleState + "!");
+ }
+ updateLifeCycle(LifeCycle.STARTING);
+ boolean failed = false;
+
+ try {
+ gaXWikiObjects.get().startIfNeedBe();
+
+ tryInittingAuthService();
+ } catch (Exception e) {
+ e.printStackTrace();
+ failed = true;
+ }
+
+ if (!failed) {
+ log.info("GoogleAppsManagerImpl is now running.");
+ updateLifeCycle(LifeCycle.RUNNING);
+ } else {
+ updateLifeCycle(LifeCycle.INITIALIZED);
+ }
+ }
+
+ void tryInittingAuthService()
+ {
+ XWiki xwiki = getXWiki();
+ if (xwiki != null) {
+ log.info("Initting authService.");
+ // We do not verify with the context if the plugin is active and if the license is active
+ // this will be done by the GoogleAppsAuthService and UI pages later on, when it is called within a request
+ try {
+ xwiki.setAuthService(authService.get());
+ log.info("Succeeded initting authService,");
+ } catch (Exception e) {
+ log.info("Failed initting authService", e);
+ }
+ }
+ if (authService == null) {
+ log.info("Not yet initting authService.");
+ }
+ }
+
+ /**
+ * Evaluates weather the application is active and licensed by looking at the stored documents. Within a request,
+ * this method should always be the first to be called so that the config-object is read and other properties are
+ * cached if need be. The context is extracted from the thead-local.
+ *
+ * @return True if documents were readable, and the is licensed and active; false otherwise.
+ * @since 3.0
+ */
+ public boolean isActive()
+ {
+ return isActive(xwikiContextProvider.get());
+ }
+
+ /**
+ * Evaluates weather the application is active and licensed by looking at the stored documents. Within a request,
+ * this method should always be the first to be called so that the config-object is read and other properties are
+ * cached if need be.
+ *
+ * @param context The context (a page request).
+ * @return True if documents were readable, and the is licensed and active; false otherwise.
+ * @since 3.0
+ */
+ boolean isActive(XWikiContext context)
+ {
+ startIfNeedBe();
+ // move this into startIfNeedBe (and itself in xwiO?)
+ if (gaIdentity == null) {
+ initialize();
+ }
+ if (gaXWikiObjects.get().isActive() != null) {
+ return gaXWikiObjects.get().isActive();
+ }
+ return false;
+ }
+
+ /**
+ * @return true if the app is configured to use cookies to store the association to the Google user.
+ * @since 3.0
+ */
+ public boolean useCookies()
+ {
+ return gaXWikiObjects.get().doesUseCookies();
+ }
+
+ /**
+ * @return true if the app is configured to simply use the cookie and thus recognize the user based on cookie
+ * @since 3.0
+ */
+ public boolean skipLoginPage()
+ {
+ return gaXWikiObjects.get().doesSkipLoginPage();
+ }
+
+ /**
+ * @return true if the app is configured to use cookies to store the association to the Google user
+ * @since 3.0
+ */
+ public boolean authWithCookies()
+ {
+ return gaXWikiObjects.get().doesAuthWithCookies();
+ }
+
+ /**
+ * @return if the app is configured to use the Google Drive integration (default: yes).
+ * @since 3.0
+ */
+ public boolean isDriveEnabled()
+ {
+ return gaXWikiObjects.get().doesConfigScopeUseDrive();
+ }
+
+ /**
+ * Note that this dispose() will get called when this Extension is uninstalled which is the use case we want to
+ * serve. The fact that it'll also be called when XWiki stops is a side effect that is ok.
+ */
+ @Override
+ public void dispose()
+ {
+ updateLifeCycle(LifeCycle.STOPPING);
+ XWiki xwiki = getXWiki();
+ // XWiki can be null in the case when XWiki has been started and not accessed (no first request done and thus
+ // no XWiki object initialized) and then stopped.
+ if (xwiki != null) {
+ // Unset the Authentication Service (next time XWiki.getAuthService() is called it'll be re-initialized)
+ xwiki.setAuthService(null);
+ }
+ updateLifeCycle(LifeCycle.STOPPED);
+ }
+
+ XWiki getXWiki()
+ {
+ XWiki result = null;
+ XWikiContext xc = this.xwikiContextProvider.get();
+ // XWikiContext could be null at startup when the Context Provider has not been initialized yet (it's
+ // initialized after the first request).
+ if (xc != null) {
+ result = xc.getWiki();
+ }
+ return result;
+ }
+
+ /**
+ * Performs the necessary communication with Google-Services to fetch identity and update the XWiki-user object or
+ * possibly sends a redirect to a Google login screen.
+ *
+ * @return "failed login" if failed, {@NOUSER} (can be attempted to Google-OAuth), or "ok" if successful
+ * @since 3.0
+ */
+ public String updateUser()
+ {
+ return gaIdentity.get().updateUser();
+ }
+
+ /**
+ * Inspects the stored information to see if an authorization or a redirect needs to be pronounced.
+ *
+ * @param redirect If a redirect can be done
+ * @return if found a credential
+ * @since 3.0
+ */
+ public boolean authorize(boolean redirect)
+ {
+ try {
+ return null != gaIdentity.get().authorize(redirect);
+ } catch (Exception ex) {
+ log.warn("Trouble at authorizing", ex);
+ return false;
+ }
+ }
+
+ /**
+ * Fetches the google-drive document's representation and stores it as attachment.
+ *
+ * @param page attach to this page
+ * @param name attach using this file name
+ * @param id store object attached to this attachment using this id (for later sync)
+ * @param mediaType content-type of the file to be fetched (or "unknown"; in this case the mediaType is read from
+ * Tika.
+ * @since 3.0
+ */
+ public void retrieveFileFromGoogle(String page, String name, String id, String mediaType)
+ {
+ gaDriveAccess.get().retrieveFileFromGoogle(page, name, id, mediaType);
+ }
+
+ /**
+ * Extracts metadata about the Google Drive document corresponding to the named attachment.
+ *
+ * @param pageName The XWiki page where the attachment is
+ * @param fileName The filename of the attachment
+ * @return information about the corresponding Google Drive document
+ * @since 3.0
+ */
+ public DriveDocMetadata getSyncDocMetadata(String pageName, String fileName)
+ {
+ return gaXWikiObjects.get().getGoogleDocumentMetadata(pageName, fileName);
+ }
+
+ /**
+ * Inserts the current information on the document to be embedded.
+ *
+ * @param docId the identifier of the Google Docs document to be embedded
+ * @param doc the XWiki document where to attach the embedding
+ * @param objp the XWiki object where this embedding is to be updated (or null if it is to be created)
+ * @param nb the number of the embedding across all the page's embeddings
+ * @return the created or actualized document
+ * @since 3.0
+ */
+ public BaseObject createOrUpdateEmbedObject(String docId, XWikiDocument doc, BaseObject objp, int nb)
+ {
+ try {
+
+ DriveDocMetadata ddm = gaDriveAccess.get().getEmbedData(docId);
+ // use here and at retrieveFromGoogle
+ return gaXWikiObjects.get().createOrUpdateEmbedObject(docId, doc, objp, nb, ddm);
+ } catch (Exception e) {
+ throw new GoogleAppsException("Can't create or update embedded document.", e);
+ }
+ }
+
+ /**
+ * Saves the attachment stored in XWiki to the Google drive of the user attached to the current logged-in user.
+ *
+ * @param page the XWiki page name
+ * @param name the attachment name
+ * @return a record with the keys fileName, exportLink, version, editLink, embedLink, and google-user's
+ * email-address
+ * @since 3.0
+ */
+ public DriveDocMetadata saveAttachmentToGoogle(String page, String name)
+ {
+ return gaDriveAccess.get().saveAttachmentToGoogle(page, name);
+ }
+
+ /**
+ * Fetches a list of Google Drive document matching a substring query in the filename.
+ *
+ * @param query the exfpected query (e.g. fullText contains winter ski)
+ * @param nbResults max number of results
+ * @return The list of objects of Google Drive.
+ * @since 3.0
+ */
+ public List listDriveDocuments(String query, int nbResults)
+ {
+ startIfNeedBe();
+ return gaDriveAccess.get().listDriveDocuments(query, nbResults);
+ }
+
+ enum LifeCycle
+ { CONSTRUCTED, INITIALIZED, STARTING, RUNNING, STOPPING, STOPPED }
+}
diff --git a/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsXWikiObjects.java b/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsXWikiObjects.java
new file mode 100644
index 0000000..4217a57
--- /dev/null
+++ b/api/src/main/java/com/xwiki/googleapps/internal/GoogleAppsXWikiObjects.java
@@ -0,0 +1,634 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package com.xwiki.googleapps.internal;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.apache.commons.httpclient.util.DateUtil;
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.slf4j.Logger;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.component.phase.Initializable;
+import org.xwiki.environment.Environment;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.reference.DocumentReferenceResolver;
+import org.xwiki.model.reference.ObjectReference;
+import org.xwiki.observation.ObservationManager;
+import org.xwiki.query.Query;
+import org.xwiki.query.QueryManager;
+import org.xwiki.stability.Unstable;
+
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.doc.XWikiAttachment;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.BaseObject;
+import com.xwiki.googleapps.DriveDocMetadata;
+import com.xwiki.googleapps.GoogleAppsException;
+
+/**
+ * The objects representing the configuration of the application as well as methods to
+ * connect to XWiki for the manipulation of users and attachments.
+ *
+ * @version $Id$
+ * @since 3.0
+ */
+@Component(roles = GoogleAppsXWikiObjects.class)
+@Singleton
+public class GoogleAppsXWikiObjects implements GoogleAppsConstants, Initializable
+{
+ // environment
+ @Inject
+ private Logger log;
+
+ @Inject
+ private Provider contextProvider;
+
+ @Inject
+ private QueryManager queryManager;
+
+ @Inject
+ @Named("current")
+ private DocumentReferenceResolver documentResolver;
+
+ @Inject
+ @Named("user")
+ private DocumentReferenceResolver userResolver;
+
+ @Inject
+ private Provider eventListener;
+
+ @Inject
+ private Environment environment;
+
+ @Inject
+ private Provider observationManager;
+
+
+ private File permanentDir;
+
+ private boolean started;
+
+ // configuration properties
+ private Boolean configActiveFlag;
+
+ private boolean useCookies;
+
+ private boolean skipLoginPage;
+
+ private boolean authWithCookies;
+
+ private String configAppName;
+
+ private String configClientId;
+
+ private String configClientSecret;
+
+ private String configDomain;
+
+ private boolean configScopeUseAvatar;
+
+ private boolean configScopeUseDrive;
+
+ private int configCookiesTTL;
+
+ // internal objects
+ private DocumentReference configDocRef;
+
+ private ObjectReference configObjRef;
+
+ private DocumentReference syncdocClassRef;
+
+ private DocumentReference gauthClassRef;
+
+ /**
+ * Initializes the internal objects.
+ */
+ public void initialize()
+ {
+ this.permanentDir = new File(environment.getPermanentDirectory(), SPACENAME);
+ }
+
+ Boolean isActive()
+ {
+ return configActiveFlag;
+ }
+
+ boolean doesUseCookies()
+ {
+ return useCookies;
+ }
+
+ boolean doesSkipLoginPage()
+ {
+ return skipLoginPage;
+ }
+
+ boolean doesAuthWithCookies()
+ {
+ return authWithCookies;
+ }
+
+ String getConfigAppName()
+ {
+ return configAppName;
+ }
+
+ String getConfigClientId()
+ {
+ return configClientId;
+ }
+
+ String getConfigClientSecret()
+ {
+ return configClientSecret;
+ }
+
+ String getConfigDomain()
+ {
+ return configDomain;
+ }
+
+ boolean getConfigScopeUseAvatar()
+ {
+ return configScopeUseAvatar;
+ }
+
+ boolean doesConfigScopeUseDrive()
+ {
+ return configScopeUseDrive;
+ }
+
+ int getConfigCookiesTTL()
+ {
+ return configCookiesTTL;
+ }
+
+ private BaseObject getConfigDoc(XWikiContext context)
+ {
+ try {
+ configDocRef = getConfigDocRef();
+ XWikiDocument doc = context.getWiki().getDocument(configObjRef, context);
+ BaseObject result = doc.getXObject(configObjRef, false, context);
+ if (result == null) {
+ log.warn("Can't access Config document.");
+ }
+ return result;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private DocumentReference getSyncDocClassReference()
+ {
+ if (syncdocClassRef == null) {
+ syncdocClassRef = new DocumentReference(WIKINAME, SPACENAME, "SynchronizedDocumentClass");
+ }
+ return syncdocClassRef;
+ }
+
+ DocumentReference getConfigDocRef()
+ {
+ if (configDocRef == null) {
+ configDocRef = new DocumentReference(WIKINAME,
+ SPACENAME, "GoogleAppsConfig");
+ configObjRef = new ObjectReference("GoogleApps.GoogleAppsConfigClass", configDocRef);
+ }
+ return configDocRef;
+ }
+
+ private void readConfigDoc()
+ {
+ XWikiContext context = contextProvider.get();
+
+ log.warn("Attempting to fetch Config doc");
+ BaseObject config = getConfigDoc(context);
+ if (config != null) {
+ configActiveFlag = 0 != config.getIntValue("activate");
+ useCookies = 0 != config.getIntValue("useCookies");
+ skipLoginPage = 0 != config.getIntValue("skipLoginPage");
+ authWithCookies = 0 != config.getIntValue("authWithCookies");
+ configAppName = config.getStringValue("appname").trim();
+ configClientId = config.getStringValue("clientid").trim();
+ configClientSecret = config.getStringValue("secret").trim();
+ configDomain = config.getStringValue("domain").trim();
+ if (configDomain.length() == 0) {
+ configDomain = null;
+ }
+ List configScopes = Arrays.asList(config.getStringValue("scope").split("\\s"));
+ configScopeUseAvatar = configScopes.contains(AVATAR);
+ configScopeUseDrive = configScopes.contains("drive");
+ configCookiesTTL = config.getIntValue("cookiesTTL");
+ }
+ }
+
+ void startIfNeedBe()
+ {
+ if (started) {
+ return;
+ }
+ if (eventListener == null) {
+ observationManager.get().addListener(eventListener.get());
+ }
+ if (configActiveFlag == null || configClientId == null) {
+ readConfigDoc();
+ }
+ started = true;
+ }
+
+ void restart()
+ {
+ readConfigDoc();
+ }
+
+ void saveFileToXWiki(String page,
+ String driveFileId, String name, InputStream data, DriveDocMetadata ddm)
+ {
+ try {
+ XWikiContext context = contextProvider.get();
+ XWikiDocument adoc = context.getWiki().getDocument(documentResolver.resolve(page), context);
+ XWikiAttachment attachment = adoc.addAttachment(name, data, context);
+
+ // ready to save now
+ adoc.saveAttachmentContent(attachment, context);
+
+ context.getWiki().saveDocument(adoc, "Updated Attachment From Google Apps", context);
+
+ BaseObject object = adoc.getXObject(getSyncDocClassReference(), FILENAME, name, false);
+ if (object == null) {
+ object = adoc.newXObject(getSyncDocClassReference(), context);
+ }
+ object.set(ID, driveFileId, context);
+ object.set(FILENAME, name, context);
+ if (context.getRequest().getParameter(URL) != null) {
+ object.set(EXPORTLINK, context.getRequest().getParameter(URL), context);
+ }
+ object.set(VERSION, ddm.getVersion(), context);
+ object.set(EDITLINK, ddm.getEditLink(), context);
+ object.set(EMBEDLINK, ddm.getEmbedLink(), context);
+ if (object.getStringValue(USER) == null || object.getStringValue(USER).length() == 0) {
+ object.set(USER, ddm.getUser(), context);
+ }
+ context.getWiki().saveDocument(adoc, UPDATECOMMENT, context);
+ log.info("Document " + name + " has been saved to XWiki");
+ } catch (Exception e) {
+ throw new GoogleAppsException("Trouble at saving GoogleDrive file to XWiki.", e);
+ }
+ }
+
+ DriveDocMetadata getGoogleDocumentMetadata(String pageName, String fileName)
+ {
+ try {
+ XWikiDocument adoc = contextProvider.get().getWiki().getDocument(documentResolver.resolve(pageName),
+ contextProvider.get());
+ BaseObject object = adoc.getXObject(getSyncDocClassReference(), FILENAME, fileName, false);
+ if (object == null) {
+ return null;
+ } else {
+ DriveDocMetadata gdm = new DriveDocMetadata();
+ gdm.setId(object.getStringValue(ID));
+ gdm.setEditLink(object.getStringValue(EDITLINK));
+ gdm.setExportLink(object.getStringValue(EXPORTLINK));
+ return gdm;
+ }
+ } catch (Exception e) {
+ throw new GoogleAppsException("Can't get Google-Document inside XWiki.", e);
+ }
+ }
+
+ BaseObject createOrUpdateEmbedObject(String docId, XWikiDocument doc, BaseObject o,
+ int nb, DriveDocMetadata ddm)
+ {
+ try {
+ XWikiContext context = contextProvider.get();
+ BaseObject obj = o;
+ if (obj == null) {
+ obj = doc.newXObject(getSyncDocClassReference(), context);
+ obj.setNumber(nb);
+ }
+ obj.setStringValue(ID, docId);
+ if (ddm.getEmbedLink() != null) {
+ obj.setStringValue(EMBEDLINK, ddm.getEmbedLink());
+ }
+
+ obj.setStringValue(EDITLINK, ddm.getEditLink());
+ obj.setStringValue(VERSION, ddm.getVersion());
+ obj.setStringValue(FILENAME, ddm.getFileName());
+ obj.setStringValue(USER, ddm.getUser());
+ context.getWiki().saveDocument(doc, "Inserting Google Document", context);
+ return obj;
+ } catch (Exception e) {
+ throw new GoogleAppsException("Couldn't update object", e);
+ }
+ }
+
+ ImmutablePair getAttachment(String name, String page)
+ {
+ try {
+ XWikiContext context = contextProvider.get();
+ XWikiDocument adoc = context.getWiki().getDocument(documentResolver.resolve(page), context);
+ XWikiAttachment attach = adoc.getAttachment(name);
+ return new ImmutablePair<>(
+ attach.getContentInputStream(context), attach.getMimeType(context));
+ } catch (Exception e) {
+ throw new GoogleAppsException("Couldn't getAttachment", e);
+ }
+ }
+
+ void insertSyncDocObject(String page, String name, DriveDocMetadata ddm)
+ {
+ try {
+ XWikiContext context = contextProvider.get();
+ XWikiDocument adoc = context.getWiki().getDocument(documentResolver.resolve(page), context);
+ BaseObject object = adoc
+ .newXObject(getSyncDocClassReference(), context);
+ object.set(ID, ddm.getId(), context);
+ object.set(FILENAME, name, context);
+ object.set(EXPORTLINK, ddm.getExportLink(), context);
+ object.set(VERSION, ddm.getVersion(), context);
+ object.set(EDITLINK, ddm.getEditLink(), context);
+ object.set(EMBEDLINK, ddm.getEmbedLink(), context);
+ object.set(USER, ddm.getUser(), context);
+
+ context.getWiki().saveDocument(adoc, UPDATECOMMENT, context);
+ } catch (Exception e) {
+ throw new GoogleAppsException("Couldn't createSyncDocObject", e);
+ }
+ }
+
+ String updateXWikiUser(String googleUserId, List emails, String emailP,
+ String firstName, String lastName, String avatarUrl)
+ {
+ XWikiContext context = contextProvider.get();
+ String xwikiUser;
+ String email = emailP;
+ String currentWiki = context.getWikiId();
+ try {
+ // Force main wiki database to create the user as global
+ context.setMainXWiki(WIKINAME);
+ if (email == null) {
+ if (emails != null && emails.size() > 0) {
+ email = emails.get(0);
+ } else {
+ email = "";
+ }
+ }
+ List