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-SNAPSHOT application-googleapps-api - 3.0-SNAPSHOT jar Google 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-client 1.24.1 - - com.google.gdata - core - 1.47.1 - com.google.apis google-api-services-people @@ -57,33 +51,10 @@ google-api-services-drive v2-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 wikiUserList = findExistingUser(googleUserId, email); + + if (wikiUserList == null || wikiUserList.size() == 0) { + xwikiUser = createUser(googleUserId, email, firstName, lastName, avatarUrl, context); + } else { + // user found.. we should update it if needed + xwikiUser = (String) (wikiUserList.get(0)); + if (xwikiUser.startsWith(XWIKISPACE + '.')) { + xwikiUser = xwikiUser.substring(XWIKISPACE.length() + 1); + } + updateUser(xwikiUser, email, firstName, lastName, avatarUrl, googleUserId, context); + } + } finally { + // Restore database + context.setMainXWiki(currentWiki); + } + return xwikiUser; + } + + private List findExistingUser(String googleUserId, String email) + { + try { + List wikiUserList = queryManager.createQuery( + "from doc.object(GoogleApps.GoogleAppsAuthClass) as auth where auth.id=:id", + Query.XWQL).bindValue(ID, googleUserId).execute(); + if ((wikiUserList == null) || (wikiUserList.size() == 0)) { + wikiUserList = queryManager.createQuery( + "from doc.object(XWiki.XWikiUsers) as user where user.email=:email", + Query.XWQL) + .bindValue(EMAIL, email).execute(); + } + return wikiUserList; + } catch (Exception e) { + e.printStackTrace(); + throw new GoogleAppsException(e); + } + } + + private String createUser(String googleUserId, + String email, String firstName, String lastName, String avatarUrl, XWikiContext context) + { + try { + // user not found.. need to create new user + String xwikiUser = email.substring(0, email.indexOf("@")); + // make sure user is unique + xwikiUser = context.getWiki().getUniquePageName(XWIKISPACE, xwikiUser, context); + // create user + DocumentReference userDirRef = new DocumentReference( + context.getWikiId(), "Main", "UserDirectory"); + String randomPassword = + Integer.toString((int) (Math.pow(10, 8) + + Math.floor(Math.random() * Math.pow(10, 7))), 10); + Map userAttributes = new HashMap<>(); + + if (firstName != null) { + userAttributes.put(FIRSTNAME, firstName); + } + if (lastName != null) { + userAttributes.put(LASTNAME, lastName); + } + userAttributes.put(EMAIL, email); + userAttributes.put(PASSWORD, randomPassword); + int isCreated = context.getWiki().createUser(xwikiUser, userAttributes, + userDirRef, null, null, "edit", context); + // Add google apps id to the user + if (isCreated == 1) { + log.debug("Creating user " + xwikiUser); + XWikiDocument userDoc = context.getWiki() + .getDocument(createUserReference(xwikiUser), context); + BaseObject userObj = userDoc.getXObject(getXWikiUserClassRef()); + + userObj.set("active", 1, context); + fetchUserImage(userDoc, userObj, avatarUrl); + // TODO: check first and last names are written + + userDoc.createXObject(getGoogleAuthClassReference(), context); + BaseObject gAppsAuthClass = userDoc.getXObject(getGoogleAuthClassReference()); + gAppsAuthClass.set(ID, googleUserId, context); + context.getWiki().saveDocument(userDoc, "Google Apps login user creation", false, context); + } else { + log.debug("User creation failed"); + xwikiUser = null; + } + return xwikiUser; + } catch (Exception e) { + throw new GoogleAppsException(e); + } + } + + private void updateUser(String xwikiUser, + String email, String firstName, String lastName, String avatarUrl, String googleUserId, + XWikiContext context) + { + + try { + log.debug("Found user " + xwikiUser); + XWikiDocument userDoc = context.getWiki().getDocument(createUserReference(xwikiUser), context); + BaseObject userObj = userDoc.getXObject(getXWikiUserClassRef()); + if (userObj == null) { + log.debug("User found is not a user"); + } else { + boolean changed = updateField(userObj, email, EMAIL, context) + || updateField(userObj, firstName, FIRSTNAME, context) + || updateField(userObj, lastName, LASTNAME, context); + changed = changed || fetchUserImage(userDoc, userObj, avatarUrl); + + BaseObject googleAppsAuth = userDoc.getXObject(getGoogleAuthClassReference()); + if (googleAppsAuth == null) { + userDoc.createXObject(getGoogleAuthClassReference(), context); + googleAppsAuth = userDoc.getXObject(getGoogleAuthClassReference()); + changed = true; + } + + changed = changed || updateField(googleAppsAuth, googleUserId, ID, context); + + if (changed) { + log.info("User changed."); + context.getWiki().saveDocument(userDoc, "Google Apps login user updated.", context); + } else { + log.info("User unchanged."); + } + } + } catch (Exception e) { + e.printStackTrace(); + throw new GoogleAppsException(e); + } + } + + private boolean updateField(BaseObject userObj, String value, String fieldName, XWikiContext context) + { + if (!userObj.getStringValue(fieldName).equals(value)) { + userObj.set(fieldName, value, context); + return true; + } else { + return false; + } + } + + private boolean fetchUserImage(XWikiDocument userDoc, BaseObject userObj, + String imgUrl) + { + try { + if (configScopeUseAvatar && imgUrl != null) { + String imageUrl = imgUrl + + (imgUrl.indexOf('?') > -1 ? "&" : '?') + + "sz=512"; + XWikiAttachment attachment = + userObj.getStringValue(AVATAR) == null ? null + : userDoc.getAttachment(userObj.getStringValue(AVATAR)); + java.net.URL u = new URL(imageUrl); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + + if (attachment != null) { + conn.addRequestProperty("If-Modified-Since", + DateUtil.formatDate(attachment.getDate())); + } + + if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { + XWikiContext context = contextProvider.get(); + log.debug("Pulling avatar " + imageUrl); + + String fileName = imageUrl.substring(imageUrl.lastIndexOf('/') + 1); + if (fileName.contains("?")) { + fileName = fileName.substring(0, fileName.indexOf('?')); + } + log.debug("Avatar changed " + fileName); + userObj.set(AVATAR, fileName, context); + userDoc.addAttachment(fileName, conn.getInputStream(), context).setDate( + new Date(conn.getLastModified())); + return true; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + DocumentReference getXWikiUserClassRef() + { + return new DocumentReference(WIKINAME, XWIKISPACE, "XWikiUsers"); + } + + /** + * @param userName the name of the user + * @return A DocumentReference for the given username. + * @since 3.0 + */ + @Unstable + DocumentReference createUserReference(String userName) + { + return userResolver.resolve(userName); + } + + private DocumentReference getGoogleAuthClassReference() + { + if (gauthClassRef == null) { + gauthClassRef = new DocumentReference(WIKINAME, SPACENAME, "GoogleAppsAuthClass"); + } + return gauthClassRef; + } + + File getPermanentDir() + { + return permanentDir; + } + + String getUserEmail(String userId) + { + try { + XWikiContext context = contextProvider.get(); + XWikiDocument userDoc = context.getWiki().getDocument(createUserReference(userId), + context); + String userEmail = null; + BaseObject userObj = userDoc.getXObject(getXWikiUserClassRef(), false, + context); + // userclass "XWiki.XWikiUsers" + + if (userObj != null) { + userEmail = userDoc.getStringValue(EMAIL); + } + return userEmail; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} + diff --git a/api/src/main/java/com/xwiki/googleapps/internal/GoogleDriveAccess.java b/api/src/main/java/com/xwiki/googleapps/internal/GoogleDriveAccess.java new file mode 100644 index 0000000..a9488a7 --- /dev/null +++ b/api/src/main/java/com/xwiki/googleapps/internal/GoogleDriveAccess.java @@ -0,0 +1,250 @@ +/* + * 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.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.tika.Tika; +import org.slf4j.Logger; +import org.xwiki.component.annotation.Component; +import org.xwiki.component.phase.Initializable; +import org.xwiki.stability.Unstable; + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.InputStreamContent; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.drive.Drive; +import com.google.api.services.drive.model.File; +import com.google.api.services.drive.model.FileList; +import com.xwiki.googleapps.DriveDocMetadata; +import com.xwiki.googleapps.GoogleAppsException; + +/** + * Tools to access the web-services of the Google Apps. + * + * @version $Id$ + * @since 3.0 + */ +@Component(roles = GoogleDriveAccess.class) +@Singleton +public class GoogleDriveAccess implements GoogleAppsConstants, Initializable +{ + + // ----- communication tools + @Inject + private Provider gaIdentity; + + @Inject + private Provider gaXWikiObjects; + + @Inject + private Logger log; + + private JacksonFactory jacksonFactory; + + private NetHttpTransport httpTransport; + + /** Initializes the communication objects. + * */ + public void initialize() + { + try { + this.jacksonFactory = JacksonFactory.getDefaultInstance(); + this.httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + } catch (Exception e) { + e.printStackTrace(); + throw new GoogleAppsException("Trouble at building GoogleDriveAccess", e); + } + } + + /** + * Builds and returns an authorized Drive client service. + * + * @return an authorized Drive client service + */ + private Drive getDriveService() + { + Credential credential = gaIdentity.get().authorize(false); + return new Drive.Builder( + httpTransport, jacksonFactory, credential) + .setApplicationName(gaXWikiObjects.get().getConfigAppName()) + .build(); + } + + /** + * 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 objects documenting the Google Drive documents. + * @since 3.0 + */ + @Unstable + List listDriveDocuments(String query, int nbResults) + { + try { + Drive drive = getDriveService(); + Drive.Files.List req = drive.files().list() + .setFields("items(id,mimeType,title,exportLinks,selfLink,version,alternateLink)") + .setMaxResults(nbResults); + if (query != null && query.length() > 0) { + req.setQ(query); + } + FileList result = req.execute(); + List files = result.getItems(); + List r = new ArrayList<>(files.size()); + for (File file : files) { + r.add(createDriveDocMetadata(file, null)); + } + return r; + } catch (IOException e) { + throw new GoogleAppsException(e); + } + } + + /** + * 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 + void retrieveFileFromGoogle(String page, String name, String id, String mediaType) + { + Drive driveService = getDriveService(); + String mt = mediaType; + + try { + if ("unknown".equalsIgnoreCase(mediaType) || mediaType == null || !mediaType.contains("/")) { + mt = new Tika().detect(name); + } + log.debug("Retrieving " + name + " to page " + page + ": " + id + "(mediatype " + mt + ")."); + InputStream downloadStream = driveService.files().export(id, mt).executeMediaAsInputStream(); + String user = driveService.about().get().execute().getUser().getEmailAddress(); + File docData = driveService.files().get(id).execute(); + createDriveDocMetadata(docData, user); + gaXWikiObjects.get().saveFileToXWiki(page, id, name, downloadStream, createDriveDocMetadata(docData, user)); + } catch (Exception e) { + log.info(e.getMessage(), e); + throw new GoogleAppsException("Trouble at retrieving from Google.", e); + } + } + + DriveDocMetadata getEmbedData(String docId) + { + try { + File docData = getDriveService().files().get(docId).execute(); + DriveDocMetadata ddm = new DriveDocMetadata(); + ddm.setId(docId); + ddm.setEmbedLink(docData.getEmbedLink()); + if (ddm.getEmbedLink() == null) { + ddm.setEmbedLink(docData.getAlternateLink()); + } + ddm.setEditLink(docData.getAlternateLink()); + ddm.setFileName( + docData.getOriginalFilename() != null ? docData.getOriginalFilename() : docData.getTitle()); + ddm.setUser(getDriveService().about().get().execute().getUser().getEmailAddress()); + ddm.setVersion(docData.getVersion().toString()); + return ddm; + } catch (IOException e) { + throw new GoogleAppsException(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 + */ + @Unstable + public DriveDocMetadata saveAttachmentToGoogle(String page, String name) + { + try { + log.debug("Starting saving attachment ${name} from page ${page}"); + Pair attachPair = gaXWikiObjects.get().getAttachment(name, page); + + File file = new File(); + file.setTitle(name); + file.setOriginalFilename(name); + InputStreamContent content = new InputStreamContent(attachPair.getRight(), attachPair.getLeft()); + Drive drive = getDriveService(); + String user = drive.about().get().execute().getUser().getEmailAddress(); + Drive.Files.Insert insert = drive.files().insert(file, content); + insert.setConvert(true); + File docData = insert.execute(); + if (docData != null) { + log.debug("File inserted " + docData); + DriveDocMetadata ddm = createDriveDocMetadata(docData, user); + gaXWikiObjects.get().insertSyncDocObject(page, name, ddm); + return ddm; + } else { + log.warn("File insert failed"); + throw new GoogleAppsException("Google did not let us save attachment", new IOException()); + } + } catch (Exception e) { + throw new GoogleAppsException("Couldn't save attachment to Google.", e); + } + } + + private DriveDocMetadata createDriveDocMetadata(File googleFile, String userName) + { + DriveDocMetadata ddm = new DriveDocMetadata(); + ddm.setEmbedLink(googleFile.getEmbedLink() != null + ? googleFile.getEmbedLink() : googleFile.getAlternateLink()); + ddm.setEditLink(googleFile.getAlternateLink()); + ddm.setVersion(Long.toString(googleFile.getVersion())); + ddm.setFileName(googleFile.getOriginalFilename()); + if (ddm.getFileName() == null) { + ddm.setFileName(googleFile.getTitle()); + } + ddm.setId(googleFile.getId()); + ddm.setUser(userName); + if (googleFile.getExportLinks() != null) { + for (String elink : googleFile.getExportLinks().values()) { + int index = elink.indexOf(EXPORTFORMATEQ) + 13; + String extension = elink.substring(index); + String newFileName = ddm.getFileName() + .replaceAll("\\.(doc|docx|odt|xls|xlsx|ods|pptx|svg|png|jpeg|pdf|)$", ""); + newFileName += '.' + extension; + ddm.addExportAlternative(extension, newFileName, elink); + } + } + ddm.setExportLink(googleFile.getDownloadUrl()); + return ddm; + } +} diff --git a/api/src/main/java/org/xwiki/apps/googleapps/CookieAuthenticationPersistence.java b/api/src/main/java/org/xwiki/apps/googleapps/CookieAuthenticationPersistence.java deleted file mode 100644 index 754941b..0000000 --- a/api/src/main/java/org/xwiki/apps/googleapps/CookieAuthenticationPersistence.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 org.xwiki.apps.googleapps; - -import org.xwiki.component.annotation.Role; - -import com.xpn.xwiki.XWikiContext; -import com.xpn.xwiki.XWikiException; - -/** - * Set of methods for the management of the cookies. - * - * @since 3.0 - * @version $Id$ - */ -@Role -public interface CookieAuthenticationPersistence -{ - /** - * Stores the user-id in an encryted fashion in the cookie. - * @param userId the string to store - * @since 3.0 - */ - void store(String userId); - - /** - * Reads the user-id from the cookie. - * @return the decrypted user-id - * @since 3.0 - */ - String retrieve(); - - /** - * Removes stored information from the cookie. - * @since 3.0 - */ - void clear(); - - /** - * Initialize with the local parameters. - * - * @param context Context of the request (e.g. for cookies) - * @param cookieMaxAge Configure maximum age of the cookie. - * @throws XWikiException if anything goes wrong - * @since 3.0 - */ - void initialize(XWikiContext context, long cookieMaxAge) throws XWikiException; - -} diff --git a/api/src/main/java/org/xwiki/apps/googleapps/GoogleAppsAuthService.java b/api/src/main/java/org/xwiki/apps/googleapps/GoogleAppsAuthService.java deleted file mode 100644 index 8b78297..0000000 --- a/api/src/main/java/org/xwiki/apps/googleapps/GoogleAppsAuthService.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 org.xwiki.apps.googleapps; - -import org.xwiki.component.annotation.Role; -import com.xpn.xwiki.user.api.XWikiAuthService; - -/** - * A badge interface to denote the role of the component that will replace the - * default authentication-service. - * - * @since 3.0 - * @version $Id$ - */ -@Role -public interface GoogleAppsAuthService extends XWikiAuthService -{ -} diff --git a/api/src/main/java/org/xwiki/apps/googleapps/GoogleAppsManager.java b/api/src/main/java/org/xwiki/apps/googleapps/GoogleAppsManager.java deleted file mode 100644 index ed85369..0000000 --- a/api/src/main/java/org/xwiki/apps/googleapps/GoogleAppsManager.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * 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 org.xwiki.apps.googleapps; - -import java.io.IOException; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import org.xwiki.component.annotation.Role; -import org.xwiki.stability.Unstable; - -import com.google.api.client.auth.oauth2.Credential; -import com.google.api.services.drive.model.File; -import com.google.api.services.drive.model.FileList; -import com.xpn.xwiki.XWikiException; -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 XWikiException in case a context cannot be read from thread. - * @since 3.0 - */ - @Unstable - boolean isActive() throws XWikiException; - - /** - * - * @return if the app is configured to use the Google Drive integration (default: yes). - * @since 3.0 - */ - @Unstable - boolean useDrive(); - - - /** - * Reads the manifest to find when the JAR file was assembled by maven. - * @return the build date. - * @since 3.0 - */ - @Unstable - Date getBuildTime(); - - - /** Inspects the stored information to see if an authorization or a redirect needs to be pronounced. - * - * @return found credential - * @throws XWikiException if the interaction with xwiki failed - * @throws IOException if a communication problem to Google services occured - * @since 3.0 - */ - @Unstable - Credential authorize() throws XWikiException, IOException; - - - /** 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 - * @throws XWikiException if the interaction with xwiki failed - * @throws IOException if a communication problem to Google services occured - * @since 3.0 - */ - @Unstable - Credential authorize(boolean redirect) throws XWikiException, IOException; - - /** - * 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(); - - /** - * Get the list of all documents in the user's associated account. - * - * @return A list of max 10 documents. - * @throws XWikiException if an authorization process failed. - * @throws IOException if a communication process to Google services occurred. - * @since 3.0 - */ - @Unstable - List getDocumentList() throws XWikiException, IOException; - - /** 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 XWikiException if an XWiki issue occurs - * @throws IOException if an error interacting with Google services occurred - * @since 3.0 - */ - @Unstable - List listDriveDocumentsWithTypes(String query, int nbResults) throws XWikiException, IOException; - - /** Fetches a list of Google Drive document matching a given query. - * - * @param query the expected filename substring - * @param nbResults max number of results - * @return The list of files at Google Drive. - * @throws XWikiException if an XWiki issue occurs - * @throws IOException if an error interacting with Google services occurred - * @since 3.0 - */ - @Unstable - FileList listDocuments(String query, int nbResults) throws XWikiException, IOException; - - /** - * 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 url fetch from this URL - * @return true if successful - * @throws XWikiException if an issue occurred in XWiki - * @throws IOException if an issue occurred in the communication with teh Google services - * @since 3.0 - */ - @Unstable - boolean retrieveFileFromGoogle(String page, String name, String id, String url) throws XWikiException, IOException; - - /** - * 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 XWikiException if something happened at XWiki side - * @since 3.0 - */ - @Unstable - GoogleAppsManager.GoogleDocMetadata getGoogleDocument(String pageName, String fileName) throws XWikiException; - - /** - * Simple pojo for metadata about a google doc. - * @since 3.0 - */ - @Unstable - class GoogleDocMetadata - { - /** - * Google's internal id to find the document again. - */ - public String id; - - /** - * URL to direct the user to for editing. - */ - public String editLink; - - /** - * URL to pull from in order to fetch the document. - */ - public String exportLink; - } - - /** - * Reads the extension and document name. - * @param docName the raw docName - * @param elink the link where to read the extension name - * @return an array with extension and simplified document name - * @since 3.0 - */ - @Unstable - String[] getExportLink(String docName, String elink); - - - /** - * 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 IOException If the communication with Google went wrong - * @throws XWikiException If something at the XWiki side went wrong (e.g. saving) - */ - @Unstable - public BaseObject createOrUpdateEmbedObject(String docId, XWikiDocument doc, BaseObject obj, int nb) throws IOException, XWikiException; - - /** - * 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 - * @throws XWikiException if something went wrong at the XWiki side - * @throws IOException if something went wrong int he communication with Google drive. - * @since 3.0 - */ - @Unstable - Map saveAttachmentToGoogle(String page, String name) throws XWikiException, IOException; - - /** - * Reads the google user-info attached to the current user as stored in the request. - * - * @return the google user-info with keys displayName, emails (array of type,value pairs), - * etag, id, image (map with keys isDefault and url), kind, language, - * name (map with keys familyName and givenName). - * @since 3.0 - */ - @Unstable - Map getGoogleUser(); - -} diff --git a/api/src/main/java/org/xwiki/apps/googleapps/GoogleAppsScriptService.java b/api/src/main/java/org/xwiki/apps/googleapps/GoogleAppsScriptService.java deleted file mode 100644 index fc74bae..0000000 --- a/api/src/main/java/org/xwiki/apps/googleapps/GoogleAppsScriptService.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * 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 org.xwiki.apps.googleapps; - -import java.io.IOException; -import java.util.Date; -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.xwiki.component.annotation.Component; -import org.xwiki.script.service.ScriptService; -import org.xwiki.stability.Unstable; - -import com.google.api.client.auth.oauth2.Credential; -import com.google.api.services.drive.model.File; -import com.google.api.services.drive.model.FileList; -import com.xpn.xwiki.XWikiContext; -import com.xpn.xwiki.XWikiException; -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. - * - * @since 3.0 - * @version $Id$ - */ -@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 - * @throws XWikiException in case a context cannot be read from thread. - * @since 3.0 - */ - @Unstable - public boolean isActive() throws XWikiException { - return manager.isActive(); - } - - /** - * @return if the app is configured to use the Google Drive integration (default: yes). - * @since 3.0 - */ - @Unstable - public boolean useDrive() { - return manager.useDrive(); - } - - - /** - * Reads the manifest to find when the JAR file was assembled by maven. - * @return the build date. - * @since 3.0 - */ - @Unstable - public Date getBuildTime() { - return manager.getBuildTime(); - } - - /** Inspects the stored information to see if an authorization or a redirect needs to be pronounced. - * - * @return found credential - * @throws XWikiException if the interaction with xwiki failed - * @throws IOException if a communication problem to Google services occured - * @since 3.0 - */ - @Unstable - public Credential authorize() throws XWikiException, IOException { - return manager.authorize(); - } - - - /** 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 - * @throws XWikiException if the interaction with xwiki failed - * @throws IOException if a communication problem to Google services occured - * @since 3.0 - */ - @Unstable - public Credential authorize(boolean redirect) throws XWikiException, IOException { - 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(); - } - - /** - * Get the list of all documents in the user's associated account. - * - * @return A list of max 10 documents. - * @throws XWikiException if an authorization process failed. - * @throws IOException if a communication process to Google services occurred. - * @since 3.0 - */ - @Unstable - public List getDocumentList() throws XWikiException, IOException { - return manager.getDocumentList(); - } - - /** 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 XWikiException if an XWiki issue occurs - * @throws IOException if an error interacting with Google services occurred - * @since 3.0 - */ - @Unstable - public List listDriveDocumentsWithTypes(String query, int nbResults) throws XWikiException, IOException { - return manager.listDriveDocumentsWithTypes(query, nbResults); - } - - /** Fetches a list of Google Drive document matching a given query. - * - * @param query the expected filename substring - * @param nbResults max number of results - * @return The list of files at Google Drive. - * @throws XWikiException if an XWiki issue occurs - * @throws IOException if an error interacting with Google services occurred - * @since 3.0 - */ - @Unstable - public FileList listDocuments(String query, int nbResults) throws XWikiException, IOException { - return manager.listDocuments(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 - * @throws IOException If the communication with Google went wrong - * @throws XWikiException If something at the XWiki side went wrong (e.g. saving) - */ - @Unstable - public Object createOrUpdateEmbedObject(String docId, Document doc, Object obj, int nb) throws XWikiException, IOException { - 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 url fetch from this URL - * @return true if successful - * @throws XWikiException if an issue occurred in XWiki - * @throws IOException if an issue occurred in the communication with teh Google services - * @since 3.0 - */ - @Unstable - public boolean retrieveFileFromGoogle(String page, String name, String id, String url) - throws XWikiException, IOException { - return manager.retrieveFileFromGoogle(page, name, id, url); - } - - /** - * 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 XWikiException if something happened at XWiki side - * @since 3.0 - */ - @Unstable - public GoogleAppsManager.GoogleDocMetadata getGoogleDocument(String pageName, String fileName) - throws XWikiException { - return manager.getGoogleDocument(pageName, fileName); - } - - - /** - * Reads the extension and document name. - * @param docName the raw docName - * @param elink the link where to read the extension name - * @return an array with extension and simplified document name - * @since 3.0 - */ - @Unstable - public String[] getExportLink(String docName, String elink) { - return manager.getExportLink(docName, elink); - } - - /** - * 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 - * @throws XWikiException if something went wrong at the XWiki side - * @throws IOException if something went wrong int he communication with Google drive. - * @since 3.0 - */ - @Unstable - public Map saveAttachmentToGoogle(String page, String name) - throws XWikiException, IOException { - return manager.saveAttachmentToGoogle(page, name); - } - - /** - * Reads the google user-info attached to the current user as stored in the request. - * - * @return the google user-info with keys displayName, emails (array of type,value pairs), - * etag, id, image (map with keys isDefault and url), kind, language, - * name (map with keys familyName and givenName). - * @since 3.0 - */ - @Unstable - public Map getGoogleUser() { - return manager.getGoogleUser(); - } - - -} diff --git a/api/src/main/java/org/xwiki/apps/googleapps/internal/GoogleAppsManagerImpl.java b/api/src/main/java/org/xwiki/apps/googleapps/internal/GoogleAppsManagerImpl.java deleted file mode 100644 index 83b4afc..0000000 --- a/api/src/main/java/org/xwiki/apps/googleapps/internal/GoogleAppsManagerImpl.java +++ /dev/null @@ -1,1373 +0,0 @@ -/* - * 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 org.xwiki.apps.googleapps.internal; - -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.InputStreamContent; -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.Drive; -import com.google.api.services.drive.DriveScopes; -import com.google.api.services.drive.model.File; -import com.google.api.services.drive.model.FileList; -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.google.gdata.client.docs.DocsService; -import com.google.gdata.data.MediaContent; -import com.google.gdata.data.media.MediaSource; -import com.xpn.xwiki.XWiki; -import com.xpn.xwiki.XWikiContext; -import com.xpn.xwiki.XWikiException; -import com.xpn.xwiki.doc.XWikiAttachment; -import com.xpn.xwiki.doc.XWikiDocument; -import com.xpn.xwiki.objects.BaseObject; -import com.xpn.xwiki.web.XWikiRequest; -import com.xpn.xwiki.web.XWikiResponse; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.RandomStringUtils; -import org.apache.http.HttpEntity; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.xwiki.apps.googleapps.CookieAuthenticationPersistence; -import org.xwiki.apps.googleapps.GoogleAppsAuthService; -import org.xwiki.apps.googleapps.GoogleAppsManager; -import org.xwiki.bridge.event.ApplicationReadyEvent; -import org.xwiki.bridge.event.DocumentUpdatedEvent; -import org.xwiki.component.manager.ComponentLookupException; -import org.xwiki.component.manager.ComponentManager; -import org.xwiki.component.phase.Disposable; -import org.xwiki.component.phase.Initializable; -import org.xwiki.component.phase.InitializationException; -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.EventListener; -import org.xwiki.observation.event.Event; - -import javax.inject.Named; -import javax.inject.Provider; -import javax.inject.Singleton; -import javax.inject.Inject; -import javax.servlet.http.HttpSession; - -import org.slf4j.Logger; -import org.xwiki.component.annotation.Component; -import org.xwiki.query.Query; -import org.xwiki.query.QueryException; -import org.xwiki.query.QueryManager; -import org.xwiki.stability.Unstable; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.jar.Attributes; -import java.util.jar.Manifest; - -/** - * Set of methods accessible to the scripts using the GoogleApps functions. - * @version $Id$ - * @since 3.0 - */ -@Component -@Singleton -public class GoogleAppsManagerImpl - implements GoogleAppsManager, EventListener, Initializable, Disposable -{ - - // ----------------------------- Lifecycle --------------------------- - - @Inject - private Provider xwikiContextProvider; - - @Inject - private QueryManager queryManager; - - @Inject - private Environment environment; - - @Inject - @Named("current") - private DocumentReferenceResolver documentResolver; - - @Inject - @Named("user") - private DocumentReferenceResolver userResolver; - - @Inject - private Logger log; - - @Inject - private ComponentManager componentManager; - - @Override - public String getName() - { - return "googleapps.scriptservice"; - } - - - @Override - public void initialize() throws InitializationException - { - log.info("GoogleAppsScriptService initting."); - XWiki xwiki = getXWiki(); - XWikiContext context = xwikiContextProvider.get(); - - if (context != null) { - readConfigDoc(context); - } - - if (xwiki != null && context != 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 { - authService = componentManager.getInstance(GoogleAppsAuthService.class); - authService.setGoogleAppsManager(this); - xwiki.setAuthService(authService); - log.info("Succeeded initting authService,"); - } catch (ComponentLookupException e) { - log.info("Failed initting authService", e); - } - } - if (authService == null) { - log.info("Not yet initting authService."); - } - - try { - jacksonFactory = JacksonFactory.getDefaultInstance(); - httpTransport = GoogleNetHttpTransport.newTrustedTransport(); - } catch (Exception e) { - e.printStackTrace(); - throw new InitializationException("Trouble at initializing", e); - } - } - - @Override - public List getEvents() - { - return Arrays.asList(new ApplicationReadyEvent(), new DocumentUpdatedEvent()); - } - - @Override - public void onEvent(Event event, Object source, Object data) - { - log.info("Event triggered: " + event + " with source " + source); - boolean applicationStarted = false, configChanged = false; - if (event instanceof ApplicationReadyEvent) { - applicationStarted = true; - } - if (event instanceof DocumentUpdatedEvent) { - XWikiDocument document = (XWikiDocument) source; - configDocRef = getConfigDocRef(); - if (document != null && document.getDocumentReference().compareTo(configDocRef) == 0) { - configChanged = true; - } - } - - if(configChanged) { - log.info("Reloading config."); - readConfigDoc(xwikiContextProvider.get()); - } - - if(applicationStarted || configChanged) { - try { - initialize(); - } catch (InitializationException e) { - e.printStackTrace(); - } - } - } - - - // internals - - private GoogleAppsAuthServiceImpl authService; - - private DocumentReference configDocRef; - private ObjectReference configObjRef; - - /** A map of hash to full redirects. */ - private Map storedStates = new HashMap<>(); - - private FileDataStoreFactory dsFactory; - - private JacksonFactory jacksonFactory; - - private NetHttpTransport httpTransport; - - private CloseableHttpClient httpclient = HttpClients.createDefault(); - - - private BaseObject getConfigDoc(XWikiContext context) throws XWikiException - { - 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; - } - - 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 Long configCookiesTTL; - - // ---------------------------- constants --------------------------------------------- - - private static final String AVATAR = "avatar"; - private static final String SPACENAME = "GoogleApps"; - private static final String VIEWACTION = "view"; - private static final String WIKINAME = "xwiki"; - private static final String XWIKISPACE = "XWiki"; - private static final String XWIKIGUEST = "XWikiGuest"; - private static final String AUTOAPPROVAL = "auto"; - private static final String EMAIL = "email"; - private static final String PASSWORD = "password"; - private static final String FIRSTNAME = "first_name"; - private static final String LASTNAME = "last_name"; - private static final String OAUTH = "OAuth"; - private static final String FAILEDLOGIN = "failed login"; - private static final String NOUSER = "no user"; - private static final String USER = "user"; - private static final String GOOGLEUSERATT = "googleUser"; - private static final String ID = "id"; - private static final String FILENAME = "fileName"; - private static final String VERSION = "version"; - private static final String URL = "url"; - private static final String EXPORTLINK = "exportLink"; - private static final String EDITLINK = "editLink"; - private static final String EMBEDLINK = "embedLink"; - - - private static final String UPDATECOMMENT = "Updated Google Apps Document metadata"; - private static final String EXPORTFORMATEQ = "exportFormat="; - - - // ----------------------------- APIs ------------------------------------------------- - - /** 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 - * */ - @Unstable - 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 - * */ - @Unstable - public boolean isActive(XWikiContext context) - { - log.info("Is active " + this.toString() + " with configClient non-null? " + (configClientId!=null)); - if (configActiveFlag == null || configClientId == null || configClientId.length() == 0) { - readConfigDoc(context); - } - if (authService == null) { - try { - initialize(); - } catch (InitializationException e) { - e.printStackTrace(); - } - } - if (configActiveFlag != null) { - return configActiveFlag; - } - return false; - } - - /** - * @return true if the app is configured to use cookies to store the association to the Google user. - * @since 3.0 - */ - @Unstable - public boolean useCookies() { - return useCookies; - } - - /** - * @return true if the app is configured to simply use the cookie and thus recognize the user based on cookie - * @since 3.0 - */ - @Unstable - public boolean skipLoginPage() { - return skipLoginPage; - } - - /** - * @return true if the app is configured to use cookies to store the association to the Google user - * @since 3.0 - */ - @Unstable - public boolean authWithCookies() { - return authWithCookies; - } - - /** - * @return how long (in seconds) the cookies should be valid - * @since 3.0 - */ - @Unstable - long getConfigCookiesTTL() { - return configCookiesTTL; - } - - private void readConfigDoc(XWikiContext context) { - - try { - 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.getLongValue("cookiesTTL"); - } - } catch (XWikiException e) { - e.printStackTrace(); - if (log != null) { - log.warn("can't fetch Config doc"); - } - } - } - - /** - * Reads the manifest to find when the JAR file was assembled by maven. - * @return the build date. - * @since 3.0 - */ - @Unstable - public Date getBuildTime() { - try { - Class clazz = getClass(); - String className = clazz.getSimpleName() - + ".class"; - String classPath = clazz.getResource(className).toString(); - String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) - + "/META-INF/MANIFEST.MF"; - Manifest manifest = new Manifest(new URL(manifestPath).openStream()); - Attributes attr = manifest.getMainAttributes(); - return new Date(Long.parseLong(attr.getValue("Bnd-LastModified"))); - } catch (IOException e) { - String msg = "Can't read build time."; - log.warn(msg, e); - throw new RuntimeException(msg, e); - } - } - - - // from ActiveDirectorySetupListener - - /** - * 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() - { - 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); - } - } - - - // ----------------------------------------------------------------------------------------- - private 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; - } - - // ------------------------- public API --------------------------------------------- - - /** - * - * @return if the app is configured to use the Google Drive integration (default: yes). - * @since 3.0 - */ - @Unstable - public boolean useDrive() { - return configScopeUseDrive; - } - - - - - // ----------------------------- Google Apps Tool (mostly request specific) ----------------------------------- - - private String getOAuthUrl() throws XWikiException { - XWikiContext context = xwikiContextProvider.get(); - DocumentReference oauthReference = new DocumentReference(context.getWikiId(), - SPACENAME, OAUTH); - return getXWiki().getDocument(oauthReference, context).getExternalURL(VIEWACTION, context); - } - - private DocumentReference getXWikiUserClassRef() { - return new DocumentReference(WIKINAME, XWIKISPACE, "XWikiUsers"); - } - - private String getCurrentXWikiUserName() { - DocumentReference userDoc = xwikiContextProvider.get().getUserReference(); - String uName = userDoc == null ? XWIKIGUEST : userDoc.getName(); - if (XWIKIGUEST.equals(uName)) { - uName = uName + "-" + xwikiContextProvider.get().getRequest().getSession().hashCode(); - } - return uName; - } - - /** - * @param userName the name of the user - * @return A DocumentReference for the given username. - * @since 3.0 - */ - @Unstable - DocumentReference createUserReference(String userName) { - return userResolver.resolve(userName); - } - - private DocumentReference gauthClassRef; - private DocumentReference getGoogleAuthClassReference() { - if (gauthClassRef == null) { - gauthClassRef = new DocumentReference(WIKINAME, SPACENAME, "GoogleAppsAuthClass"); - } - return gauthClassRef; - } - - private DocumentReference getSyncDocClassReference() { - if (gauthClassRef == null) { - gauthClassRef = new DocumentReference(WIKINAME, SPACENAME, "SynchronizedDocumentClass"); - } - return gauthClassRef; - } - - private DocumentReference getConfigDocRef() { - if (configDocRef == null) { - configDocRef = new DocumentReference(WIKINAME, - SPACENAME, "GoogleAppsConfig"); - configObjRef = new ObjectReference("GoogleApps.GoogleAppsConfigClass", configDocRef); - } - return configDocRef; - } - - - /** - * Build flow and trigger user authorization request. - * @return the configured flow - * @throws IOException in case something can't be built - */ - private GoogleAuthorizationCodeFlow getFlow() throws IOException - { - try { - if (dsFactory == null) { - dsFactory = - new FileDataStoreFactory(new java.io.File(environment.getPermanentDirectory(), SPACENAME)); - } - - // create scopes from config - List gScopes = new ArrayList<>(); - gScopes.add(PeopleServiceScopes.USERINFO_EMAIL); - gScopes.add(PeopleServiceScopes.USERINFO_PROFILE); - if (configScopeUseDrive != null && configScopeUseDrive) { - gScopes.add(DriveScopes.DRIVE); - } - - // create flow - return new GoogleAuthorizationCodeFlow.Builder( - httpTransport, - jacksonFactory, configClientId, configClientSecret, gScopes) - .setDataStoreFactory(dsFactory) - .setAccessType("online").setApprovalPrompt(AUTOAPPROVAL) - .setClientId(configClientId) - .build(); - } catch (Exception e) { - e.printStackTrace(); - throw new IOException("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) { - try { - GoogleAuthorizationCodeFlow flow = getFlow(); - GoogleTokenResponse tokenResponse = flow - .newTokenRequest(authorizationCode) - .setRedirectUri(getOAuthUrl()) - .execute(); - log.info("Token: " + tokenResponse); - return flow.createAndStoreCredential(tokenResponse, getCurrentXWikiUserName()); - } catch (Exception ex) { - log.warn("An error occurred: ", ex); - ex.printStackTrace(); - return null; - } - } - - private Map getCredentialStore() { - final String key = "GoogleAppsCredentialStore"; - HttpSession session = xwikiContextProvider.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) throws XWikiException { - try { - if (userId.contains(XWIKIGUEST)) { - if (useCookies) { - // create a cookie - CookieAuthenticationPersistence cookieTools = - componentManager.getInstance(CookieAuthenticationPersistence.class); - cookieTools.initialize(xwikiContextProvider.get(), configCookiesTTL); - cookieTools.store(userId); - } - } - log.info("Storing credentials for user " + userId); - getCredentialStore().put(userId, credentials); - } catch (Exception e) { - e.printStackTrace(); - throw new XWikiException("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 IOException Unable to load client_secret.json. - */ - private Credential retrieveCredentials(String authorizationCode, boolean redirect) - throws XWikiException, IOException { - 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) { - String rtoken = credentials.getRefreshToken(); - if (rtoken != null) { - log.debug("Refresh token has been created: " + rtoken); - storeCredentials(user, credentials); - return credentials; - } 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."); - xwikiContextProvider.get().getResponse().sendRedirect(getAuthorizationURL()); - } - return null; - } - - private String getAuthorizationURL() throws XWikiException, IOException { - String state = ""; - XWikiContext context = xwikiContextProvider.get(); - XWikiRequest request = context.getRequest(); - DocumentReference ref = context.getDoc().getDocumentReference(); - if (!(OAUTH.equals(ref.getName()) && SPACENAME.equals(ref.getLastSpaceReference().getName()))) { - - String finalRedirect = new URL( - new URL(getXWiki().getExternalURL("GoogleApps.Login", VIEWACTION, context)), - context.getDoc().getURL(VIEWACTION, request.getQueryString(), context)).toExternalForm(); - state = Integer.toHexString(finalRedirect.hashCode()); - storedStates.put(state, finalRedirect); - } - - GoogleAuthorizationCodeRequestUrl urlBuilder = getFlow() - .newAuthorizationUrl() - .setRedirectUri(getOAuthUrl()) - .setState(state).setClientId(configClientId) - .setAccessType("offline").setApprovalPrompt(AUTOAPPROVAL); - // Add user email to filter account if the user is logged with multiple account - if (useCookies) { - try { - CookieAuthenticationPersistence cookieTools = - componentManager.getInstance(CookieAuthenticationPersistence.class); - cookieTools.initialize(context, configCookiesTTL); - String userId = cookieTools.retrieve(); - if (userId != null) { - XWikiDocument userDoc = getXWiki().getDocument(createUserReference(userId), - xwikiContextProvider.get()); - String userEmail = null; - BaseObject userObj = userDoc.getXObject(getXWikiUserClassRef(), false, - xwikiContextProvider.get()); - // userclass "XWiki.XWikiUsers" - - if (userObj != null) { - userEmail = userDoc.getStringValue(EMAIL); - } - if (userEmail != null) { - urlBuilder = urlBuilder.set("login_hint", userEmail); - } - } - } catch (ComponentLookupException e) { - e.printStackTrace(); - throw new XWikiException("Issue at accessing CookieAuthenticationPersistance", e); - } - } - String authurl = urlBuilder.build(); - log.debug("google authentication url : " + authurl); - return authurl; - } - - /** Inspects the stored information to see if an authorization or a redirect needs to be pronounced. - * - * @return found credential - * @throws XWikiException if the interaction with xwiki failed - * @throws IOException if a communication problem to Google services occured - * @since 3.0 - */ - @Unstable - public Credential authorize() throws XWikiException, IOException { - return authorize(true); - } - - /** 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 - * @throws XWikiException if the interaction with xwiki failed - * @throws IOException if a communication problem to Google services occured - * @since 3.0 - */ - @Unstable - public Credential authorize(boolean redirect) throws XWikiException, IOException { - log.info("In authorize"); - GoogleAuthorizationCodeFlow flow = getFlow(); // useless? - XWikiRequest request = xwikiContextProvider.get().getRequest(); - String state = request.getParameter("state"); - XWikiResponse response = xwikiContextProvider.get().getResponse(); - Credential creds = retrieveCredentials(request.getParameter("code"), redirect); - log.info("Got credentials: " + creds); - if (state != null && state.length() > 0) { - String url = storedStates.get(state); - if (url != null) { - log.info("Redirecting to final destination after authorization: " + url); - response.sendRedirect(new URL(new URL(request.getRequestURL().toString()), url).toExternalForm()); - } - } - return creds; - } - - - /** - * 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 - */ - @Unstable - public String updateUser() { - try { - if (!isActive()) { - return FAILEDLOGIN; - } - log.info("Updating user..."); - XWikiContext context = xwikiContextProvider.get(); - String xwikiUser; - Credential credential = authorize(); - - Person user = null; - if (credential != null) { - PeopleService pservice = new PeopleService.Builder(httpTransport, - jacksonFactory, credential).setApplicationName(configAppName) - .build(); - user = 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.info("user: " + user); - context.getRequest().setAttribute(GOOGLEUSERATT, user); - if (user == null) { - return NOUSER; - } else if (configDomain != null) { - boolean foundCompatibleDomain = false; - if (user.getEmailAddresses() != null) { - for (EmailAddress address: user.getEmailAddresses()) { - String email = address.getValue(); - if (email.endsWith(configDomain)) { - foundCompatibleDomain = true; - break; - } - } - } - if (!foundCompatibleDomain) { - String userId = getCurrentXWikiUserName(); - getCredentialStore().remove(userId); - log.debug("Wrong domain: Removed credentials for userid " + userId); - return FAILEDLOGIN; - } - } else { - // this seems undocumented but well working - String id = (String) user.get("resourceName"); - String email; - String currentWiki = context.getWikiId(); - try { - // Force main wiki database to create the user as global - context.setMainXWiki(WIKINAME); - email = (user.getEmailAddresses() != null && user.getEmailAddresses().size() > 0) - ? user.getEmailAddresses().get(0).getValue() : ""; - List wikiUserList = queryManager.createQuery( - "from doc.object(GoogleApps.GoogleAppsAuthClass) as auth where auth.id=:id", - Query.XWQL).bindValue(ID, id).execute(); - if ((wikiUserList == null) || (wikiUserList.size() == 0)) { - wikiUserList = queryManager.createQuery( - "from doc.object(XWiki.XWikiUsers) as user where user.email=:email", - Query.XWQL) - .bindValue(EMAIL, email).execute(); - } - - if ((wikiUserList == null) || (wikiUserList.size() == 0)) { - // user not found.. need to create new user - xwikiUser = email.substring(0, email.indexOf("@")); - // make sure user is unique - xwikiUser = getXWiki().getUniquePageName(XWIKISPACE, xwikiUser, context); - // create user - DocumentReference userDirRef = new DocumentReference( - context.getWikiId(), "Main", "UserDirectory"); - String randomPassword = RandomStringUtils.randomAlphanumeric(8); - Map userAttributes = new HashMap<>(); - - if (user.getNames() != null && user.getNames().size() > 0) { - userAttributes.put(FIRSTNAME, user.getNames().get(0).getGivenName()); - userAttributes.put(LASTNAME, user.getNames().get(0).getFamilyName()); - } - userAttributes.put(EMAIL, email); - userAttributes.put(PASSWORD, randomPassword); - int isCreated = getXWiki().createUser(xwikiUser, userAttributes, - userDirRef, null, null, "edit", context); - // Add google apps id to the user - if (isCreated == 1) { - log.debug("Creating user " + xwikiUser); - XWikiDocument userDoc = getXWiki() - .getDocument(createUserReference(xwikiUser), context); - BaseObject userObj = userDoc.getXObject(getXWikiUserClassRef()); - - // TODO: is this not redundant when having used createUser (map) ? - if (user.getNames() != null && user.getNames().size() > 0) { - userObj.set(FIRSTNAME, user.getNames().get(0).getGivenName(), context); - userObj.set(LASTNAME, user.getNames().get(0).getFamilyName(), context); - } - userObj.set("active", 1, context); - if (configScopeUseAvatar && user.getPhotos() != null - && user.getPhotos().size() > 0 - && user.getPhotos().get(0).getUrl() != null) { - String photoUrl = user.getPhotos().get(0).getUrl(); - log.debug("Adding avatar " + photoUrl); - URL u = new URL(photoUrl); - InputStream b = u.openStream(); - String fileName = u.getFile().substring(u.getFile().lastIndexOf('/') + 1); - userDoc.addAttachment(fileName, u.openStream(), context); - userObj.set(AVATAR, fileName, context); - b.close(); - } - - userDoc.createXObject(getGoogleAuthClassReference(), context); - BaseObject gAppsAuthClass = userDoc.getXObject(getGoogleAuthClassReference()); - gAppsAuthClass.set(ID, id, context); - getXWiki().saveDocument(userDoc, "Google Apps login user creation", false, context); - } else { - log.debug("User creation failed"); - return FAILEDLOGIN; - } - } else { - // user found.. we should update it if needed - xwikiUser = (String) (wikiUserList.get(0)); - log.debug("Found user " + xwikiUser); - boolean changed = false; - XWikiDocument userDoc = getXWiki().getDocument(createUserReference(xwikiUser), context); - BaseObject userObj = userDoc.getXObject(getXWikiUserClassRef()); - if (userObj == null) { - log.debug("User found is not a user"); - return FAILEDLOGIN; - } else { - if (!userObj.getStringValue(EMAIL).equals(email)) { - userObj.set(EMAIL, email, context); - changed = true; - } - if (user.getNames() != null && user.getNames().size() > 0) { - if (!userObj.getStringValue(FIRSTNAME).equals( - user.getNames().get(0).getGivenName())) { - userObj.set(FIRSTNAME, user.getNames().get(0).getGivenName(), context); - changed = true; - } - if (!userObj.getStringValue(LASTNAME).equals( - user.getNames().get(0).getFamilyName())) { - userObj.set(LASTNAME, user.getNames().get(0).getFamilyName(), context); - changed = true; - } - } - if (configScopeUseAvatar && user.getPhotos() != null && user.getPhotos().size() > 0 - && user.getPhotos().get(0).getUrl() != null) { - String imageUrl = user.getPhotos().get(0).getUrl(); - imageUrl = imageUrl - + (imageUrl.contains("?") ? "&" : "?") - + "sz=256"; - log.debug("Pulling avatar " + imageUrl); - HttpGet httpget = new HttpGet(imageUrl); - // TODO: add an if-modified-since - CloseableHttpResponse response = httpclient.execute(httpget); - HttpEntity entity = response.getEntity(); - if (entity != null) { - ByteArrayOutputStream bOut = - new ByteArrayOutputStream((int) entity.getContentLength()); - IOUtils.copy(entity.getContent(), bOut); - byte[] bytesFromGoogle = bOut.toByteArray(); - - XWikiAttachment attachment = - userObj.getStringValue(AVATAR) == null ? null - : userDoc.getAttachment(userObj.getStringValue(AVATAR)); - boolean fileChanged = attachment == null - || attachment.getFilesize() != bytesFromGoogle.length; - if (!fileChanged) { - byte[] b = attachment.getContent(context); - for (int i = 0; i < b.length; i++) { - if (b[i] != bytesFromGoogle[i]) { - fileChanged = true; - break; - } - } - } - if (fileChanged) { - String fileName = imageUrl.substring(imageUrl.lastIndexOf('/') + 1); - log.debug("Avatar changed " + fileName); - userObj.set(AVATAR, fileName, context); - userDoc.addAttachment(fileName, bytesFromGoogle, context); - changed = true; - } - } - - } - - BaseObject googleAppsAuth = userDoc.getXObject(getGoogleAuthClassReference()); - if (googleAppsAuth == null) { - userDoc.createXObject(getGoogleAuthClassReference(), context); - googleAppsAuth = userDoc.getXObject(getGoogleAuthClassReference()); - changed = true; - } - - if (!googleAppsAuth.getStringValue(ID).equals(id)) { - googleAppsAuth.set(ID, id, context); - changed = true; - } - - if (changed) { - log.info("User changed."); - getXWiki().saveDocument(userDoc, "Google Apps login user updated.", context); - } else { - log.info("User unchanged."); - } - } - } - } catch (QueryException qe) { - log.warn("Cannot query for users.", qe); - throw new XWikiException("Can't query for users.", qe); - } finally { - // Restore database - context.setMainXWiki(currentWiki); - } - - // 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 - context.getRequest().getSession().setAttribute("googleappslogin", xwikiUser); - } - return "ok"; - } catch (Exception e) { - log.warn("Problem at updateUser", e); - return NOUSER; - } - } - - - - - - /** - * Builds and returns an authorized Drive client service. - * - * @return an authorized Drive client service - * @throws IOException if a communication error occurs - */ - private Drive getDriveService() throws XWikiException, IOException { - Credential credential = authorize(); - return new Drive.Builder( - httpTransport, jacksonFactory, credential) - .setApplicationName(configAppName) - .build(); - } - - /** - * Build and return an authorized Drive client service. - * @return an authorized Drive client service - * @throws IOException if a communication error occurred - */ - private DocsService getDocsService() throws XWikiException, IOException { - Credential credential = authorize(); - DocsService service = new DocsService(configAppName); - service.setOAuth2Credentials(credential); - return service; - } - - /** - * Get the list of all documents in the user's associated account. - * - * @return A list of max 10 documents. - * @throws XWikiException if an authorization process failed. - * @throws IOException if a communication process to Google services occurred. - * @since 3.0 - */ - @Unstable - public List getDocumentList() throws XWikiException, IOException { - Drive drive = getDriveService(); - FileList result = drive.files().list().setMaxResults(10).execute(); - return result.getItems(); - } - - /** 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 XWikiException if an XWiki issue occurs - * @throws IOException if an error interacting with Google services occurred - * @since 3.0 - */ - @Unstable - public List listDriveDocumentsWithTypes(String query, int nbResults) throws XWikiException, IOException { - Drive drive = getDriveService(); - Drive.Files.List req = drive.files().list() - .setQ(query) - .setFields("items(id,mimeType,title,exportLinks,selfLink,version,alternateLink)") - .setMaxResults(nbResults); - FileList result = req.execute(); - return result.getItems(); - } - - /** Fetches a list of Google Drive document matching a given query. - * - * @param query the expected filename substring - * @param nbResults max number of results - * @return The list of files at Google Drive. - * @throws XWikiException if an XWiki issue occurs - * @throws IOException if an error interacting with Google services occurred - * @since 3.0 - */ - @Unstable - public FileList listDocuments(String query, int nbResults) throws XWikiException, IOException { - Drive drive = getDriveService(); - Drive.Files.List req = drive.files().list().setQ(query).setMaxResults(nbResults); - FileList result = req.execute(); - return result; - } - - /** - * 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 url fetch from this URL - * @return true if successful - * @throws XWikiException if an issue occurred in XWiki - * @throws IOException if an issue occurred in the communication with teh Google services - * @since 3.0 - */ - @Unstable - public boolean retrieveFileFromGoogle(String page, String name, String id, String url) - throws XWikiException, IOException { - return retrieveFileFromGoogle(getDocsService(), getDriveService(), page, name, id, url); - } - - private boolean retrieveFileFromGoogle(DocsService docsService, Drive driveService, - String page, String name, String id, String url) throws XWikiException { - log.info("Retrieving " + name + " to page " + page + ": " + id + url); - - XWikiDocument adoc = getXWiki().getDocument(documentResolver.resolve(page), xwikiContextProvider.get()); - try { - byte[] data = downloadFile(docsService, url); - saveFileToXWiki(driveService, adoc, id, name, new ByteArrayInputStream(data), true); - return true; - } catch (Exception e) { - log.info(e.getMessage(), e); - throw new XWikiException("Trouble at retrieving from Google.", e); - } - } - - - private byte[] downloadFile(DocsService docsService, String exportUrl) throws XWikiException { - try { - MediaContent mc = new MediaContent(); - mc.setUri(exportUrl); - MediaSource ms = docsService.getMedia(mc); - - InputStream inStream = null; - ByteArrayOutputStream outStream = new ByteArrayOutputStream(); - - try { - inStream = ms.getInputStream(); - - int c; - while ((c = inStream.read()) != -1) { - outStream.write(c); - } - } finally { - if (inStream != null) { - inStream.close(); - } - outStream.flush(); - outStream.close(); - } - return outStream.toByteArray(); - } catch (Exception e) { - e.printStackTrace(); - throw new XWikiException("trouble at downloading document", e); - } - } - - private void saveFileToXWiki(Drive driveService, XWikiDocument adoc, - String id, String name, InputStream data, boolean redirect) throws XWikiException, IOException { - XWikiContext context = xwikiContextProvider.get(); - XWikiAttachment attachment = adoc.addAttachment(name, data, context); - - // ready to save now - adoc.saveAttachmentContent(attachment, context); - - String user = driveService.about().get().execute().getUser().getEmailAddress(); - File docData = driveService.files().get(id).execute(); - String embedLink = docData.getEmbedLink(); - if (embedLink == null) { - embedLink = docData.getAlternateLink(); - } - - getXWiki().saveDocument(adoc, "Updated Attachment From Google Apps", context); - - BaseObject object = adoc.getXObject(getSyncDocClassReference(), FILENAME, name, false); - if (object == null) { - object = adoc.newXObject(getGoogleAuthClassReference(), context); - } - object.set(ID, id, context); - object.set(FILENAME, name, context); - if (context.getRequest().getParameter(URL) != null) { - object.set(EXPORTLINK, context.getRequest().getParameter(URL), context); - } - object.set(VERSION, docData.getVersion().toString(), context); - object.set(EDITLINK, docData.getAlternateLink(), context); - object.set(EMBEDLINK, embedLink, context); - if (object.getStringValue(USER) == null || object.getStringValue(USER).length() == 0) { - object.set(USER, user, context); - } - getXWiki().saveDocument(adoc, UPDATECOMMENT, context); - log.info("Document " + name + " has been saved to XWiki"); - - if (redirect) { - String rurl = adoc.getURL(VIEWACTION, "#Attachments", context); - context.getResponse().sendRedirect(rurl); - } - } - - /** - * 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 XWikiException if something happened at XWiki side - * @since 3.0 - */ - @Unstable - public GoogleAppsManager.GoogleDocMetadata getGoogleDocument(String pageName, String fileName) - throws XWikiException { - XWikiDocument adoc = getXWiki().getDocument(documentResolver.resolve(pageName), xwikiContextProvider.get()); - BaseObject object = adoc.getXObject(getSyncDocClassReference(), FILENAME, fileName, false); - if (object == null) { - return null; - } else { - GoogleAppsManager.GoogleDocMetadata gdm = new GoogleAppsManager.GoogleDocMetadata(); - gdm.id = object.getStringValue(ID); - gdm.editLink = object.getStringValue(EDITLINK); - gdm.exportLink = object.getStringValue(EXPORTLINK); - return gdm; - } - } - - /** - * 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 IOException If the communication with Google went wrong - * @throws XWikiException If something at the XWiki side went wrong (e.g. saving) - */ - @Unstable - public BaseObject createOrUpdateEmbedObject(String docId, XWikiDocument doc, BaseObject obj, int nb) throws IOException, XWikiException { - Drive drive = getDriveService(); - XWikiContext context = xwikiContextProvider.get(); - String user = drive.about().get().execute().getUser().getEmailAddress(); - File docData = drive.files().get(docId).execute(); - String embedLink = docData.getEmbedLink(); - if (embedLink==null) - embedLink = docData.getAlternateLink(); - - if (obj==null) { - obj = doc.newXObject(getSyncDocClassReference(), context); - obj.setNumber(nb); - } - obj.setStringValue("id", docId); - if (embedLink!=null) - obj.setStringValue("embedLink", embedLink); - obj.setStringValue("editLink", docData.getAlternateLink()); - obj.setStringValue("version", docData.getVersion().toString()); - obj.setStringValue("fileName", docData.getOriginalFilename() != null ? docData.getOriginalFilename() : docData.getTitle()); - obj.setStringValue("user", user); - getXWiki().saveDocument(doc, "Inserting Google Document", context); - return obj; - } - - - /** - * Reads the extension and document name. - * @param docName the raw docName - * @param elink the link where to read the extension name - * @return an array with extension and simplified document name - * @since 3.0 - */ - @Unstable - public String[] getExportLink(String docName, String elink) { - int index = elink.indexOf(EXPORTFORMATEQ) + 13; - String extension = elink.substring(index); - String newDocName = docName - .replaceAll("\\.(doc|docx|odt|xls|xlsx|ods|pptx|svg|png|jpeg|pdf|)$", ""); - newDocName += '.' + extension; - return new String[] {extension, newDocName}; - } - - private String findExportLink(String name, File entry) { - String exportLink; - String lastLink = ""; - for (Map.Entry elink: entry.getExportLinks().entrySet()) { - log.info("Checking link: " + elink); - lastLink = elink.getValue(); - int index = lastLink.indexOf(EXPORTFORMATEQ) + 13; - String extension = lastLink.substring(index); - if (name.endsWith('.' + extension)) { - return lastLink; - } - } - int index = lastLink.indexOf(EXPORTFORMATEQ) + 13; - exportLink = lastLink.substring(0, index); - if (name.endsWith(".xls")) { - exportLink += "xlsx"; - } else { - exportLink += name.substring(name.lastIndexOf('.') + 1); - } - return exportLink; - } - - - /** - * 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 - * @throws XWikiException if something went wrong at the XWiki side - * @throws IOException if something went wrong int he communication with Google drive. - * @since 3.0 - */ - @Unstable - public Map saveAttachmentToGoogle(String page, String name) throws XWikiException, IOException { - log.info("Starting saving attachment ${name} from page ${page}"); - XWikiContext context = xwikiContextProvider.get(); - XWikiDocument adoc = getXWiki().getDocument(documentResolver.resolve(page), context); - XWikiAttachment attach = adoc.getAttachment(name); - String ctype = attach.getMimeType(); - - File file = new com.google.api.services.drive.model.File(); - file.setTitle(name); - file.setOriginalFilename(name); - InputStreamContent content = new InputStreamContent(ctype, attach.getContentInputStream(context)); - Drive drive = getDriveService(); - String user = drive.about().get().execute().getUser().getEmailAddress(); - Drive.Files.Insert insert = drive.files().insert(file, content); - insert.setConvert(true); - File docData = insert.execute(); - if (docData != null) { - log.info("File inserted " + docData); - String embedLink = docData.getEmbedLink(); - if (embedLink == null) { - embedLink = docData.getAlternateLink(); - } - - BaseObject object = adoc.newXObject(getSyncDocClassReference(), context); - Map r = new HashMap<>(); - object.set(ID, docData.getId(), context); - r.put(ID, docData.getId()); - object.set(FILENAME, name, context); - object.set(EXPORTLINK, findExportLink(name, docData), context); - r.put(EXPORTLINK, findExportLink(name, docData)); - object.set(VERSION, Long.toString(docData.getVersion()), context); - object.set(EDITLINK, docData.getAlternateLink(), context); - r.put(EDITLINK, docData.getAlternateLink()); - object.set(EMBEDLINK, embedLink, context); - object.set(USER, user, context); - - getXWiki().saveDocument(adoc, UPDATECOMMENT, context); - return r; - - } else { - log.info("File insert failed"); - return null; - } - } - - /** - * Reads the google user-info attached to the current user as stored in the request. - * - * @return the google user-info with keys displayName, emails (array of type,value pairs), - * etag, id, image (map with keys isDefault and url), kind, language, - * name (map with keys familyName and givenName). - * @since 3.0 - */ - @Unstable - public Map getGoogleUser() { - // e.g.: User: [displayName: name name, - // emails:[[type:account, value:xxx@googlemail.com]], - // etag:"k-5ZH5-al;sdsdkl;-sdsadsd", - // id:948382, - // image:[isDefault:false, url:https://222.googleusercontent.com/-2323/s50/photo.jpg], - // kind:plus#person, language:uu, - // name:[familyName:XXX, givenName:xxx]] - return (Map) (xwikiContextProvider.get().getRequest().getAttribute(GOOGLEUSERATT)); - } - -} diff --git a/api/src/main/resources/META-INF/components.txt b/api/src/main/resources/META-INF/components.txt index 7c75ce7..4bdb925 100644 --- a/api/src/main/resources/META-INF/components.txt +++ b/api/src/main/resources/META-INF/components.txt @@ -1,4 +1,8 @@ -org.xwiki.apps.googleapps.internal.GoogleAppsManagerImpl -org.xwiki.apps.googleapps.internal.CookieAuthenticationPersistenceImpl -org.xwiki.apps.googleapps.GoogleAppsScriptService -org.xwiki.apps.googleapps.internal.GoogleAppsAuthServiceImpl +com.xwiki.googleapps.internal.GoogleAppsManagerImpl +com.xwiki.googleapps.GoogleAppsScriptService +com.xwiki.googleapps.internal.CookieAuthenticationPersistence +com.xwiki.googleapps.internal.GoogleAppsAuthService +com.xwiki.googleapps.internal.GoogleAppsEventListener +com.xwiki.googleapps.internal.GoogleAppsIdentity +com.xwiki.googleapps.internal.GoogleAppsXWikiObjects +com.xwiki.googleapps.internal.GoogleDriveAccess \ No newline at end of file diff --git a/ui/pom.xml b/ui/pom.xml index bd595fd..c607523 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -59,16 +59,6 @@ ${licensing.version} - - 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 - @@ -91,6 +81,20 @@ INSERT_TEXT src/main/resources/GoogleApps/GoogleAppsConfigClass.js + + GoogleApps/DriveMacro.xml + /xwikidoc/object[className[text()='XWiki.WikiMacroClass']]/property/code + INSERT_TEXT + src/main/resources/GoogleApps/DriveMacro.groovy + + + GoogleApps/LoginUIExtension.xml + /xwikidoc/object[className[text()='XWiki.UIExtensionClass']]/property/content + INSERT_TEXT + src/main/resources/GoogleApps/LoginUIExtension.vm + + + diff --git a/ui/src/main/resources/GoogleApps/DriveMacro.groovy b/ui/src/main/resources/GoogleApps/DriveMacro.groovy new file mode 100644 index 0000000..3a66148 --- /dev/null +++ b/ui/src/main/resources/GoogleApps/DriveMacro.groovy @@ -0,0 +1,96 @@ +{{groovy}} +def mainReference = services.model.createDocumentReference('', 'GoogleApps','OAuth') +if (!services.licensing.licensor.hasLicensureForEntity(mainReference)) { + println """ + {{error}}{{translation key='googleapps.error.noValidLicense' /}}{{/error}} + """ +} else { + def id = xcontext.macro.params.id; + def width = xcontext.macro.params.width; + def height = xcontext.macro.params.height; + def nb = Integer.parseInt(xcontext.macro.params.nb); + def obj = doc.getObject("GoogleApps.SynchronizedDocumentClass", nb) + def escapetool = new org.xwiki.velocity.tools.EscapeTool(); + def force = false; + def googleApps = services.googleApps + + if(!googleApps.active || !googleApps.driveEnabled) { + println services.localization.render('googleapps.error.driveDisabled') + } else { + // adding stylesheet + xwiki.ssx.use("GoogleApps.DriveMacro"); + + if (xcontext.macro.params.authentication) { + if (googleApps.authorize(false)) { + def url = googleApps.getAuthorizationURL() + println services.localization.render("googleapps.macro.maybeReqAuth", ["[[", ">>url:${url}]]"]); + } + } + + if (nb==null) + nb = 0; + def embednb = (!request.nb) ? 0 : Integer.parseInt(request.nb) + def query = request.getParameter("query${nb}") + + if (request.update=="1" && nb==embednb) + force = true; + + if (request.embed=="1") { + if (embednb==nb) { + obj = googleApps.createOrUpdateEmbedObject(request.id, doc, obj, nb); + doc.use(obj); + } + } + + if (id!=null) { + println id; + } else if (obj!=null && !force) { + doc.use(obj); + def embedLink = doc.getValue("embedLink") + def editLink = doc.getValue("editLink") + def swidth = (width.endsWith("%")) ? width : width + "px"; + print """(% class="drive-links" style="width: ${swidth};" %)(((""" + print """[[Change>>||queryString="update=1&nb=${nb}"]]""" + if (editLink && editLink.startsWith("http")) + print """ - [[Edit>>url:${editLink}]]""" + println ")))" + println """{{html clean=false}}{{/html}}""" + } else { + def tquery = "" + if (query) + tquery = escapetool.xml(query) + println """ + +{{translation key='googleapps.macro.mainHint'/}} + + {{html clean=false wiki=true}} +
+ {{translation key='googleapps.macro.insert'/}} + + + + +
+ {{/html}} + """ + if (query && embednb==nb) { + def squery = "'" + query + "'" + def results = googleApps.listDriveDocuments("fullText contains ${squery}", 10) + def nbres = results.size(); + + println "${nbres} documents found: " + for (entry in results) { + def docName = entry.title; + def embedLink = entry.embedLink; + if (embedLink==null) + embedLink = entry.alternateLink; + if (embedLink==null) + println """* ${docName}: """ + services.localization.render('googleapps.macro.canBeEmbedded') + else + println """* ${docName}: [[{{translation key='googleapps.macro.nowEmbed'/}}>>||queryString="embed=1&nb=${nb}&id=${entry.id}"]]""" + } + } + } + } +} +{{/groovy}} \ No newline at end of file diff --git a/ui/src/main/resources/GoogleApps/DriveMacro.xml b/ui/src/main/resources/GoogleApps/DriveMacro.xml index d4471f6..596c1ef 100644 --- a/ui/src/main/resources/GoogleApps/DriveMacro.xml +++ b/ui/src/main/resources/GoogleApps/DriveMacro.xml @@ -145,7 +145,7 @@ margin-bottom: 0; padding-bottom: 0; } - + CSS @@ -275,102 +275,7 @@ XWiki.WikiMacroClass 41e5d116-4e82-408a-a49e-eac3af94af90 - {{groovy}} -def mainReference = services.model.createDocumentReference('', 'GoogleApps','OAuth') -if (!services.licensing.licensor.hasLicensureForEntity(mainReference)) { - println """ - {{error}}{{translation key='googleapps.error.noValidLicense' /}}{{/error}} - """ -} else { -def id = xcontext.macro.params.id; -def width = xcontext.macro.params.width; -def height = xcontext.macro.params.height; -def nb = Integer.parseInt(xcontext.macro.params.nb); -def obj = doc.getObject("GoogleApps.SynchronizedDocumentClass", nb) -def escapetool = new org.xwiki.velocity.tools.EscapeTool(); -def force = false; -def googleApps = services.googleApps - -if(!googleApps.active || !googleApps.useDrive) { - println services.localization.render('googleapps.error.driveDisabled') -} else { - // adding stylesheet - xwiki.ssx.use("GoogleApps.DriveMacro"); - - if (xcontext.macro.params.authentication) { - if (googleApps.authorize(false)==null) { - def url = googleApps.getAuthorizationURL() - println services.localization.render("googleapps.macro.maybeReqAuth", ["[[", ">>url:${url}]]"]); - } - } - - if (nb==null) - nb = 0; - def embednb = (!request.nb) ? 0 : Integer.parseInt(request.nb) - def query = request.getParameter("query${nb}") - - if (request.update=="1" && nb==embednb) - force = true; - - if (request.embed=="1") { - if (embednb==nb) { - obj = googleApps.createOrUpdateEmbedObject(request.id, doc, obj, nb); - doc.use(obj); - } - } - - if (id!=null) { - println id; - } else if (obj!=null && !force) { - doc.use(obj); - def embedLink = doc.getValue("embedLink") - def editLink = doc.getValue("editLink") - def swidth = (width.endsWith("%")) ? width : width + "px"; - print """(% class="drive-links" style="width: ${swidth};" %)(((""" - print """[[Change>>||queryString="update=1&nb=${nb}"]]""" - if (editLink && editLink.startsWith("http")) - print """ - [[Edit>>url:${editLink}]]""" - println ")))" - println """{{html clean=false}}<iframe src="${embedLink}" width="${width}" height="${height}"></iframe>{{/html}}""" - } else { - def tquery = "" - if (query) - tquery = escapetool.xml(query) - println """ - -{{translation key='googleapps.macro.mainHint'/}} - - {{html clean=false wiki=true}} - <form action="" method="get"> - {{translation key='googleapps.macro.insert'/}} - <input type="hidden" name="update" value="1" /> - <input type="hidden" name="nb" value="${nb}" /> - <input type="text" name="query${nb}" value="${tquery}" /> - <input type="submit" value="Search" /> - </form> - {{/html}} - """ - if (query && embednb==nb) { - def squery = "'" + query + "'" - def results = googleApps.listDocuments("fullText contains ${squery}", 10) - def nbres = results.items.size(); - - println "${nbres} documents found: " - for (entry in results.items) { - def docName = entry.title; - def embedLink = entry.embedLink; - if (embedLink==null) - embedLink = entry.alternateLink; - if (embedLink==null) - println """* ${docName}: """ + services.localization.render('googleapps.macro.canBeEmbedded') - else - println """* ${docName}: [[{{translation key='googleapps.macro.nowEmbed'/}}>>||queryString="embed=1&nb=${nb}&id=${entry.id}"]]""" - } - } - } - } -} -{{/groovy}} + diff --git a/ui/src/main/resources/GoogleApps/DriveMacroTest.xml b/ui/src/main/resources/GoogleApps/DriveMacroTest.xml index 88c674d..0e8d2b6 100644 --- a/ui/src/main/resources/GoogleApps/DriveMacroTest.xml +++ b/ui/src/main/resources/GoogleApps/DriveMacroTest.xml @@ -39,15 +39,15 @@ false xwiki/2.1 true - {{velocity}}## + {{velocity}} {{translation key='googleapps.macro.testIntro'/}} - #if($doc.hasAccessLevel("edit"))## + #if($doc.hasAccessLevel("edit")) {{drive width="600" height="400" authentication="1" /}} #else - #if($doc.getObject(""))## + #if($doc.getObject("")) {{drive width="600" height="400" authentication="1" /}} - #else## + #else {{translation key='googleapps.macro.cantTest'/}} #end #end diff --git a/ui/src/main/resources/GoogleApps/EditInGoogleApps.xml b/ui/src/main/resources/GoogleApps/EditInGoogleApps.xml index 1d54116..1192bcf 100644 --- a/ui/src/main/resources/GoogleApps/EditInGoogleApps.xml +++ b/ui/src/main/resources/GoogleApps/EditInGoogleApps.xml @@ -41,26 +41,26 @@ true {{velocity}} #set($googleApps = $services.googleApps) -#if($googleApps.active && $googleApps.useDrive) - #set($gdoc = $googleApps.getGoogleDocument($request.page, $request.name)) +#if($googleApps.active && $googleApps.driveEnabled) + #set($gdoc = $googleApps.getSyncDocMetadata($request.page, $request.name)) #if($request.confirm) - {{translation key='googleapps.editInGA.uploading'/}} - #set($gdoc = $googleApps.saveAttachmentToGoogle($request.page, $request.name)) + {{translation key='googleapps.editInGA.uploading'/}} + #set($gdoc = $googleApps.saveAttachmentToGoogle($request.page, $request.name)) #end #if($gdoc) - {{translation key='googleapps.editInGA.isInGDrive'/}} + {{translation key='googleapps.editInGA.isInGDrive'/}} - (% class="buttonwrapper" %) - {{html clean=false}}<a href="${gdoc.editLink}" target="_blank">$services.localization.render("googleapps.edit.editingoogle.button")</a>{{/html}} + (% class="buttonwrapper" %) + {{html clean=false}}<a href="${gdoc.editLink}" target="_blank">$services.localization.render("googleapps.edit.editingoogle.button")</a>{{/html}} - $services.localization.render("googleapps.edit.desc") + $services.localization.render("googleapps.edit.desc") - (% class="buttonwrapper" %)[[$services.localization.render("googleapps.edit.retrieve.button")>>RetrieveFromGoogleApps||queryString="page=${escapetool.url($request.page)}&name=${escapetool.url($request.name)}&url=${escapetool.url($gdoc.exportLink)}&id=${gdoc.id}"]] + (% class="buttonwrapper" %)[[$services.localization.render("googleapps.edit.retrieve.button")>>RetrieveFromGoogleApps||queryString="page=${escapetool.url($request.page)}&name=${escapetool.url($request.name)}&url=${escapetool.url($gdoc.exportLink)}&id=${gdoc.id}"]] #else - {{translation key='googleapps.editInGA.notYetInGDrive'/}} + {{translation key='googleapps.editInGA.notYetInGDrive'/}} - (% class="buttonwrapper" %)[[$services.localization.render("googleapps.edit.confirm")>>||queryString="page=${escapetool.url($request.page)}&name=${escapetool.url($request.name)}&confirm=1"]] + (% class="buttonwrapper" %)[[$services.localization.render("googleapps.edit.confirm")>>||queryString="page=${escapetool.url($request.page)}&name=${escapetool.url($request.name)}&confirm=1"]] #end #else {{translation key='googleapps.error.driveDisabled'/}} diff --git a/ui/src/main/resources/GoogleApps/GoogleAppsConfigClass.js b/ui/src/main/resources/GoogleApps/GoogleAppsConfigClass.js index 245e71a..5bd1ea7 100644 --- a/ui/src/main/resources/GoogleApps/GoogleAppsConfigClass.js +++ b/ui/src/main/resources/GoogleApps/GoogleAppsConfigClass.js @@ -1,115 +1,119 @@ require(['jquery'], function (jQuery) { - var prefix = '#GoogleApps\\.GoogleAppsConfigClass_0_'; - - //deactivating - function deactivate(elts) { - elts.each(function () { - jQuery(jQuery(this).closest('dl')).find('label').css('color','darkgrey'); - this.wasDisabled = jQuery(this).prop('disabled'); - jQuery(this).prop('disabled', true); - }); + var prefix = '#GoogleApps\\.GoogleAppsConfigClass_0_'; + + //deactivating + function deactivate(elts) { + elts.each(function () { + jQuery(jQuery(this).closest('dl')).find('label').css('color','darkgrey'); + this.wasDisabled = jQuery(this).prop('disabled'); + jQuery(this).prop('disabled', true); + }); + } + + + // reactivating + function reactivate(elements) { + jQuery(elements).each(function () { + if (typeof (this.wasDisabled)) { + jQuery(jQuery(this).closest('dl')).find('label').css('color','black'); + jQuery(this).prop('disabled', this.wasDisabled); + } + }); + } + + + // updaters + function updateAllInputs() { + if (this.checked) { + reactivate(allInputs); + } else { + deactivate(allInputs); } + } - - // reactivating - function reactivate(elts) { - jQuery(elts).each(function () { - if (typeof (this.wasDisabled)) { - jQuery(jQuery(this).closest('dl')).find('label').css('color','black'); - jQuery(this).prop('disabled', this.wasDisabled); - } - }); - } - - - // updaters - function updateAllInputs() { - if (this.checked) { - reactivate(allInputs); - } else { - deactivate(allInputs); - } + function updateCookieFields() { + if (this.checked) { + reactivate(cookieInputs); + } else { + deactivate(cookieInputs); } - - function updateCookieFields() { - if (this.checked) { - reactivate(cookieInputs); - } else { - deactivate(cookieInputs); - } + } + + function updateDomainHint() { + if (typeof(this.value)=='undefined') return; + var domainHint = jQuery('#googleapps-domain-livehint'); + var valid = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/.test(this.value); + if (this.value.length > 0) { + if (valid) { + domainHint.text(hintTextOn.replace('\{0\}', this.value)); + domainHint.removeClass('warningmessage'); + } else { + domainHint.text(hintTextInvaliddomain.replace('\{0\}', this.value)); + domainHint.addClass('warningmessage'); + } + } else { + domainHint.text(hintTextOff); + domainHint.removeClass('warningmessage'); } - - function updateDomainHint() { - if (typeof(this.value)=='undefined') return; - var domainHint = jQuery('#googleapps-domain-livehint'); - var valid = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/.test(this.value); - if (this.value.length > 0) { - if (valid) { - domainHint.text(hintTextOn.replace('\{0\}', this.value)); - domainHint.removeClass('warningmessage'); - } else { - domainHint.text(hintTextInvaliddomain.replace('\{0\}', this.value)); - domainHint.addClass('warningmessage'); - } + } + + + var allInput, cookieInputs; + var hintTextOn = "${escapetool.javascript($services.localization.render('GoogleApps.GoogleAppsConfigClass_domain.hintTextOn'))}", + hintTextOff = "${escapetool.javascript($services.localization.render('GoogleApps.GoogleAppsConfigClass_domain.hintTextOff'))}", + hintTextInvaliddomain = "${escapetool.javascript($services.localization.render('GoogleApps.GoogleAppsConfigClass_domain.hintTextInvaliddomain'))}"; + + function updateScopes() { + var scopes = ""; + jQuery(("input[name^='scope_']")).each(function() { + if(jQuery.attr(this, "disabled")!=="disabled" && this.checked) + scopes = scopes + " " + this.name.substring('scope_'.length); + }); + jQuery("input[name$='_scope']").val(scopes); + updateDriveNowWhat(); + } + + function readScopes() { + var scope = jQuery("input[name$='_scope']").val(); + jQuery(("input[name^='scope_']")).each(function() { + if(jQuery.attr(this, "disabled")!=="disabled") { + var n = this.name.substring('scope_'.length); + if (scope.indexOf(n) > -1) { + this.checked = true; } else { - domainHint.text(hintTextOff); - domainHint.removeClass('warningmessage'); + this.checked = false; } - } - - - var allInput, cookieInputs; - var hintTextOn = "${escapetool.javascript($services.localization.render('GoogleApps.GoogleAppsConfigClass_domain.hintTextOn'))}", - hintTextOff = "${escapetool.javascript($services.localization.render('GoogleApps.GoogleAppsConfigClass_domain.hintTextOff'))}", - hintTextInvaliddomain = "${escapetool.javascript($services.localization.render('GoogleApps.GoogleAppsConfigClass_domain.hintTextInvaliddomain'))}"; - - function updateScopes() { - var scopes = ""; - jQuery(("input[name^='scope_']")).each(function() { - if(jQuery.attr(this, "disabled")!=="disabled" && this.checked) - scopes = scopes + " " + this.name.substring('scope_'.length); - }); - jQuery("input[name$='_scope']").val(scopes); - updateDriveNowWhat(); - } - - function readScopes() { - var scope = jQuery("input[name$='_scope']").val(); - jQuery(("input[name^='scope_']")).each(function() { - if(jQuery.attr(this, "disabled")!=="disabled") { - var n = this.name.substring('scope_'.length); - if (scope.indexOf(n) > -1) { - this.checked = true; - } else { - this.checked = false; - } - } - }); - updateDriveNowWhat(); - } - - function updateDriveNowWhat() { - var driveInput = jQuery("input[name='scope_drive']")[0]; - if(driveInput.checked) jQuery("#driveOnNowWhat").show(); - else jQuery("#driveOnNowWhat").hide(); - } - - (function () { - // register listeners - cookieInputs = jQuery(prefix + 'skipLoginPage, ' + prefix + 'authWithCookies, ' + prefix + 'cookiesTTL'); - jQuery(prefix + 'useCookies').each(updateCookieFields).change(updateCookieFields); - - allInputs = jQuery('#googleapps_GoogleApps\\.GoogleAppsConfig input:not([name$=\'_activate\'])'); - jQuery(prefix + 'activate').each(updateAllInputs).change(updateAllInputs); - - jQuery(prefix + 'domain').each(updateDomainHint).on('change keyup', updateDomainHint); - - jQuery(("input[name^='scope_']")).each(function() { - jQuery(this).change(function() { updateScopes(); }); - }); - readScopes(); - - jQuery('#googleapps_GoogleApps\.GoogleAppsConfig').submit(cancelCancelEdit); - }).defer(); + } + }); + updateDriveNowWhat(); + } + + function updateDriveNowWhat() { + var driveInput = jQuery("input[name='scope_drive']")[0]; + if(driveInput.checked) jQuery("#driveOnNowWhat").show(); + else jQuery("#driveOnNowWhat").hide(); + } + + (function () { + // register listeners + cookieInputs = jQuery(prefix + 'skipLoginPage, ' + prefix + 'authWithCookies, ' + prefix + 'cookiesTTL'); + jQuery(prefix + 'useCookies').each(updateCookieFields).change(updateCookieFields); + + var keepThem = "name$=\'_activate\'],[name=\'formactionsac\'],[name=\'form_token\']," + + "[name=\'xcontinue\'],[name=\'xredirect\'"; + + allInputs = jQuery( + '#googleapps_GoogleApps\\.GoogleAppsConfig input:not(['+keepThem+'])'); + jQuery(prefix + 'activate').each(updateAllInputs).change(updateAllInputs); + + jQuery(prefix + 'domain').each(updateDomainHint).on('change keyup', updateDomainHint); + + jQuery(("input[name^='scope_']")).each(function() { + jQuery(this).change(function() { updateScopes(); }); + }); + readScopes(); + + jQuery('#googleapps_GoogleApps\.GoogleAppsConfig').submit(cancelCancelEdit); + }).defer(); }); diff --git a/ui/src/main/resources/GoogleApps/GoogleAppsConfigSheet.vm b/ui/src/main/resources/GoogleApps/GoogleAppsConfigSheet.vm index abc25f2..d055a98 100644 --- a/ui/src/main/resources/GoogleApps/GoogleAppsConfigSheet.vm +++ b/ui/src/main/resources/GoogleApps/GoogleAppsConfigSheet.vm @@ -6,108 +6,130 @@ $xwiki.ssx.use('GoogleApps.GoogleAppsConfigClass') #set($formId = "${section.toLowerCase()}_${configClassName}") #set($configDoc = $xwiki.getDocument($configClassName)) #set($className="GoogleApps.GoogleAppsConfigClass") -#set($prefix="${configDoc.fullName}_${className}_0") +#set($propNamePrefix="${configDoc.fullName}_${className}_0") #set($obj = $configDoc.getObject($className)) + ## shorthand for t(ranslation) and tp(translation with parameters). -#macro(t $n)## - $services.localization.render("GoogleApps.GoogleAppsConfigClass_${n}")## +#macro (t $name) + $services.localization.render("GoogleApps.GoogleAppsConfigClass_${name}") #end -#macro(tp $n $p)## - $services.localization.render("GoogleApps.GoogleAppsConfigClass_${n}",$p)## + +#macro (tp $name $params) + #set ($transNamePrefix = 'GoogleApps.GoogleAppsConfigClass_') + #if ($name.startsWith("googleapps")) + #set($transNamePrefix='') + #end + #set ($msg = $services.localization.render("GoogleApps.GoogleAppsConfigClass_${name}",$params)) + ## convert to link-text for the simple wiki syntax of links + $stringtool.replacePattern($msg, '\[\[([^>]*)>>([^\]]*)\]\]', '$1') #end -$services.localization.render('googleapps.config.explanation') +#macro (displayInput $property) + #set ($output = $doc.display($property, 'edit', $obj)) + #set ($output = $stringtool.removeStart($output, '{{html clean="false" wiki="false"}}')) + #set ($output = $stringtool.removeEnd($output, '{{/html}}')) + $output +#end + +$services.localization.render('googleapps.config.explanation') -{{html wiki=true}} +{{html clean="false"}}
-; $configDoc.display("activate", 'edit', $obj) - -
-#t("communicate") - -; ## -#tp("communicate.hint", ['[[', '>>https://accounts.google.com/ManageDomains]]'])
-#tp("communicate.hint2", ['[[', '>>https://store.xwiki.com/xwiki/bin/view/Extension/GoogleAppsIntegration#installation]]'])
- - -; ## -#t("clientid.hint") -: $configDoc.display("clientid", 'edit', $obj) - -; ## -#t("secret.hint") -: $configDoc.display("secret", 'edit', $obj) - -## checkboxes for scope, needs JS -; ## -#t("scope.hint") -: ## - ## - ## - ## -$configDoc.display("scope", "hidden") - -; ## -#t("appname.hint") -: $configDoc.display("appname", 'edit', $obj) - -
- -
-#t("loginbehaviour") - -; ## -#t("domain.hint") -: $configDoc.display("domain", 'edit', $obj)
## -  - -; $configDoc.display("useCookies", 'edit', $obj) ## -#t("useCookies.hint") - -:; $configDoc.display("skipLoginPage", 'edit', $obj) ## - ## -#t("skipLoginPage.hint") - -:; $configDoc.display("authWithCookies", 'edit', $obj) ## - ## -#t("authWithCookies.hint") - -:; ## -#t("cookiesTTL.hint") -:: $configDoc.display("cookiesTTL", 'edit', $obj) -
- - -## Hidden form elements -#set($params="editor=${escapetool.url(${editor})}&section=${escapetool.url(${section})}") -#set($params="${params}&space=${escapetool.url(${currentSpace})}") -#set($continueURL=$xwiki.getURL($currentDoc, 'admin', $params)) - - - - -## submit -
-

- -

-
+
+
#displayInput ('activate')
+
+
+ +
+ #t ('communicate') + +
+
+ #tp ('communicate.hint', ['[[', '>>https://accounts.google.com/ManageDomains]]'])
+ #tp ('communicate.hint2', ['[[', '>>https://store.xwiki.com/xwiki/bin/view/Extension/GoogleAppsIntegration#installation]]'])
+
+
+
+ #t ("clientid.hint")
+
#displayInput ('clientid')
+ +
+ #t("secret.hint")
+
#displayInput ("secret")
+ + ## checkboxes for scope, needs JS +
+ #t ('scope.hint')
+
+ + + + + +
+ +
+ #t ('appname.hint')
+
#displayInput ('appname')
+
+
+ +
+ #t ('loginbehaviour') + +
+
+ #t("domain.hint")
+
#displayInput ("domain")
+  
+ +
#displayInput ('useCookies') + #t ('useCookies.hint')
+
+
+
#displayInput ('skipLoginPage') + + #t ('skipLoginPage.hint')
+
+
#displayInput ('authWithCookies') + + #t ('authWithCookies.hint')
+
+
+ #t ('cookiesTTL.hint')
+
#displayInput ('cookiesTTL')
+
+
+
+
+ + + ## Hidden form elements + #set ($params = "editor=${escapetool.url(${editor})}&section=${escapetool.url(${section})}") + #set ($params = "${params}&space=${escapetool.url(${currentSpace})}") + #set ($continueURL = $xwiki.getURL($currentDoc, 'admin', $params)) + + + + + ## submit +
+

+ +

+
-
- -#tp("nowWhat1", ['[[','>>GoogleApps.TestDocumentList]]']) - -#tp("nowWhat2", ['##~{~{drive/~}~}##']) - +

#tp ('nowWhat1', ['[[',">>${xwiki.getURL('GoogleApps.TestDocumentList')}]]"])

+

#tp ('nowWhat2', ['{{drive/}}'])

{{/html}} -{{/velocity}} +{{/velocity}} \ No newline at end of file diff --git a/ui/src/main/resources/GoogleApps/ImportFromGoogleApps.xml b/ui/src/main/resources/GoogleApps/ImportFromGoogleApps.xml index 521db3d..46573ae 100644 --- a/ui/src/main/resources/GoogleApps/ImportFromGoogleApps.xml +++ b/ui/src/main/resources/GoogleApps/ImportFromGoogleApps.xml @@ -45,29 +45,30 @@ {{translation key='googleapps.importGA.explain' parameters='${pagetitle},${pagedoc.fullName}'/}} {{html clean==false wiki=true}} -<form action="" method="get"> +<form action="" method="get"> {{translation key='googleapps.importGA.search'/}} -<input type="text" name="query" value="$!{request.query}" /> -<input type="hidden" name="page" value="$!{request.page}" /> -<input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" /> -<input type="submit" value="Search" /> -</form> +<input type="text" name="query" value="$!{request.query}" /> +<input type="hidden" name="page" value="$!{request.page}" /> +<input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" /> +<input type="submit" value="Search" /> +</form> {{/html}} #if($request.query) #set($googleApps = $services.googleApps) #set($squery = "'" + $request.query + "'") -#set($result = $googleApps.listDriveDocumentsWithTypes("fullText contains ${squery}", 20)) +#set($result = $googleApps.listDriveDocuments("fullText contains ${squery}", 20)) #if(!$result.empty) #foreach($entry in $result) - #set($docName = $entry.title) -* $docName #foreach($elink in $entry.exportLinks) - #set($exportData = $googleApps.getExportLink($docName, $elink)) - #set($stype = $exportData[0]) - #set($newDocName = $exportData[1]) - [[${stype}>>RetrieveFromGoogleApps||queryString="page=${escapetool.url($request.page)}&name=${escapetool.url($newDocName)}&url=${escapetool.url($elink)}&editLink=${escapetool.url($entry.alternateLink)}&version=${entry.version}&id=${entry.id}"]] #end + #set($docName = $entry.fileName) +* $docName: #foreach($alternative in $entry.exportLinksAlternatives) + #set($elink = $alternative.exportUrl) + #set($stype = $alternative.extension) + #set($newFileName = $alternative.newFileName) + [[${stype}>>RetrieveFromGoogleApps||queryString="page=${escapetool.url($request.page)}&name=${escapetool.url($newFileName)}&url=${escapetool.url($elink)}&editLink=${escapetool.url($entry.editLink)}&version=${entry.version}&id=${entry.id}"]] #end + #if($entry.downloadUrl) [[${stype}>>RetrieveFromGoogleApps||queryString="page=${escapetool.url($request.page)}&name=${escapetool.url($newFileName)}&url=${escapetool.url($downloadUrl)}&editLink=${escapetool.url($entry.editLink)}&version=${entry.version}&id=${entry.id}"]]#end - #end +#end #else {{translation key='googleapps.importGA.empty'/}} #end diff --git a/ui/src/main/resources/GoogleApps/Install.xml b/ui/src/main/resources/GoogleApps/Install.xml index e1e4155..335eddd 100644 --- a/ui/src/main/resources/GoogleApps/Install.xml +++ b/ui/src/main/resources/GoogleApps/Install.xml @@ -41,14 +41,14 @@ true {{velocity}} #if(!$xwiki.hasAdminRights()) -You are running this script as a non admin. It will have no effect. Login as admin. + You are running this script as a non admin. It will have no effect. Login as admin. #else -This script automatically set the owner of the pages in the Google Apps Application. This will allow the priviledged scripts included in them to work. + This script automatically set the owner of the pages in the Google Apps Application. This will allow the priviledged scripts included in them to work. #end #if($request.confirm=="1") -Assigning programming rights to the following pages: + Assigning programming rights to the following pages: #else -[[Confirm assigning programming rights to the following pages:>>$doc.fullName?confirm=1]] + [[Confirm assigning programming rights to the following pages:>>$doc.fullName?confirm=1]] #end #foreach($item in $xwiki.searchDocuments("where doc.web='GoogleApps'")) @@ -61,11 +61,11 @@ Assigning programming rights to the following pages: #set($ok = $transdoc.use("XWiki.XWikiPreferences")) #set($transprefs = $transdoc.getValue("documentBundles")) #if($transprefs.indexOf("GoogleApps.Translations")==-1) -#if($request.confirm=="1") -#set($transprefs = "${transprefs},GoogleApps.Translations") -#set($ok = $transdoc.set("documentBundles", $transprefs)) -#set($ok = $transdoc.save()) -#end + #if($request.confirm=="1") + #set($transprefs = "${transprefs},GoogleApps.Translations") + #set($ok = $transdoc.set("documentBundles", $transprefs)) + #set($ok = $transdoc.save()) + #end * Added translation bundle to XWiki Preferences #end {{/velocity}} diff --git a/ui/src/main/resources/GoogleApps/JSExtension.xml b/ui/src/main/resources/GoogleApps/JSExtension.xml index 85fc57e..59a3d77 100644 --- a/ui/src/main/resources/GoogleApps/JSExtension.xml +++ b/ui/src/main/resources/GoogleApps/JSExtension.xml @@ -120,24 +120,26 @@
#set($googleApps = $services.googleApps) - #if($googleApps.active && $googleApps.useDrive) -var listener = function(event) { + #if($googleApps.active && $googleApps.driveEnabled) + var listener = function(event) { if (event.memo.id == 'Attachments') { - var buttons = $$(".xwikibuttonlinks") - for (i=0;i<=buttons.length;i++) - { - var dlink = (buttons[i]) ? buttons[i].getElementsByClassName("deletelink") : null; - if (dlink && dlink[0]) { + var buttons = $$(".xwikibuttonlinks") + for (i=0;i<=buttons.length;i++) + { + var dlink = (buttons[i]) ? buttons[i].getElementsByClassName("deletelink") : null; + if (dlink && dlink[0]) { var dlinkurl = dlink[0].href; var dlinkpos = dlinkurl.indexOf("?"); var filename = dlinkurl.substring(dlinkurl.lastIndexOf("/", dlinkpos) + 1, dlinkpos) buttons[i].insert('<a class="editlink" href="$xwiki.getURL("GoogleApps.EditInGoogleApps")?page=' + encodeURIComponent(XWiki.currentSpace) + '.' + encodeURIComponent(XWiki.currentPage) + '&name=' + filename + '" title="$services.localization.render("googleapps.edit.editingoogleapps.link")">$services.localization.render("googleapps.edit.editingoogleapps.link")</a>') - } - } - var attachaddform = $("AddAttachment"); - attachaddform.insert('<span class="buttonwrapper" style="float: right; position: relative; top: -25px;"><a href="$xwiki.getURL("GoogleApps.ImportFromGoogleApps")?page=' + encodeURIComponent(XWiki.currentSpace) + '.' + encodeURIComponent(XWiki.currentPage) + '">$services.localization.render("googleapps.import.importfromgoogleapps")</a></span>') - document.stopObserving("xwiki:docextra:loaded", listener); - delete listener; + } + } + var attachaddform = $("AddAttachment"); + if (attachaddform) { + attachaddform.insert('<span class="buttonwrapper" style="float: right; position: relative; top: -25px;"><a href="$xwiki.getURL("GoogleApps.ImportFromGoogleApps")?page=' + encodeURIComponent(XWiki.currentSpace) + '.' + encodeURIComponent(XWiki.currentPage) + '">$services.localization.render("googleapps.import.importfromgoogleapps")</a></span>') + } + document.stopObserving("xwiki:docextra:loaded", listener); + delete listener; } }.bindAsEventListener(this); diff --git a/ui/src/main/resources/GoogleApps/Login.xml b/ui/src/main/resources/GoogleApps/Login.xml deleted file mode 100644 index d9e1e44..0000000 --- a/ui/src/main/resources/GoogleApps/Login.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - GoogleApps - Login - - - 0 - GoogleApps.WebHome - xwiki:XWiki.Admin - xwiki:XWiki.Admin - - xwiki:XWiki.Admin - 1.1 - $services.localization.render("googleapps.login.title") - - - - false - xwiki/2.1 - true - {{velocity}} -#set($googleApps = $services.googleApps) -#if($googleApps.active) - #set($result = $googleApps.updateUser()) - #if($result=="failed login") - #set($user = $googleApps.getGoogleUser()) - #set($email = $user.emails[0].value) - ${services.localization.render("googleapps.login.domainerror1")} - - ${services.localization.render("googleapps.login.domainerror2")} $!{email}. - ${services.localization.render("googleapps.login.domainerror3")} - - #elseif($result=="ok") - ${services.localization.render("googleapps.login.redirectmessage")} - - #if($request.xredirect && $request.xredirect!="") - $response.sendRedirect($request.xredirect) - #else - $response.sendRedirect($xwiki.getURL("Main.WebHome")) - #end - #else ## $result is "no user" - ${services.localization.render("googleapps.login.message")} - - ${services.localization.render("googleapps.login.error")} - #end -#else - ${services.localization.render("googleapps.error.extensionDisabled")} -#end -{{/velocity}} - diff --git a/ui/src/main/resources/GoogleApps/LoginExtension.xml b/ui/src/main/resources/GoogleApps/LoginExtension.xml deleted file mode 100644 index 3ad779c..0000000 --- a/ui/src/main/resources/GoogleApps/LoginExtension.xml +++ /dev/null @@ -1,248 +0,0 @@ - - - - - - GoogleApps - LoginExtension - - - 0 - GoogleApps.WebHome - xwiki:XWiki.Admin - xwiki:XWiki.Admin - - xwiki:XWiki.Admin - 1.1 - Login Javascript Extension - - - - false - xwiki/2.1 - true - - - XWiki.JavaScriptExtension - - - - - - - - - 0 - 0 - select - 0 - cache - 5 - Caching policy - 0 - - ,| - 1 - 0 - long|short|default|forbid - com.xpn.xwiki.objects.classes.StaticListClass - - - 0 - code - 2 - Code - 20 - 50 - 0 - com.xpn.xwiki.objects.classes.TextAreaClass - - - 0 - name - 1 - Name - 30 - 0 - com.xpn.xwiki.objects.classes.StringClass - - - 0 - select - yesno - parse - 4 - Parse content - 0 - com.xpn.xwiki.objects.classes.BooleanClass - - - 0 - 0 - select - 0 - use - 3 - Use this extension - 0 - - ,| - 1 - 0 - currentPage|onDemand|always - com.xpn.xwiki.objects.classes.StaticListClass - - - GoogleApps.LoginExtension - 0 - XWiki.JavaScriptExtension - 03df4bd2-ff8e-4084-8694-f7cf390352e5 - - long - - - #set($url = $xwiki.getURL("GoogleApps.Login", "view")) - -function loginWithXWiki() { - jQuery(".panel .panel-body dl").show() - return false; -} - -require(['jquery', 'xwiki-events-bridge', 'xwiki-meta'], function($, xm) { - $(document).ready(function(event, data) { - if (XWiki.contextaction == "login" || XWiki.contextaction == "loginsubmit" ) { - jQuery('<div id="googleapps-login-choice">' - + '<div class="col-xs-12" style="margin-bottom: 20px; padding: 20px;">' - + '<a href="${url}?' + location.href.substring(location.href.indexOf("?")) - + '" class="btn btn-primary col-xs-5" href="">${escapetool.javascript($services.localization.render("googleapps.login.withgoogle"))}</a>' - + '<div class="col-xs-2"></div>' - + '<a href="javascript:void(0)" onclick="return loginWithXWiki()" class="btn btn-primary col-xs-5" href="">${escapetool.javascript($services.localization.render("googleapps.login.withxwiki"))}</a></div>' - + '<div style="clear: both;"></div></div>').insertBefore(jQuery(".panel .panel-body dl")) - } - if (XWiki.contextaction != "loginsubmit") { - jQuery(".panel .panel-body dl").hide(); - } - }); // end document ready - -}); // end requirejs - - - - LoginExtension - - - 1 - - - onDemand - - - - - XWiki.UIExtensionClass - - - - - - - - - 0 - content - 3 - Extension Content - 10 - 40 - 0 - com.xpn.xwiki.objects.classes.TextAreaClass - - - 0 - extensionPointId - 1 - Extension Point ID - 30 - 0 - com.xpn.xwiki.objects.classes.StringClass - - - 0 - name - 2 - Extension ID - 30 - 0 - com.xpn.xwiki.objects.classes.StringClass - - - 0 - parameters - 4 - Extension Parameters - 10 - 40 - 0 - com.xpn.xwiki.objects.classes.TextAreaClass - - - 0 - 0 - select - 0 - scope - 5 - Extension Scope - 0 - - ,| - 1 - 0 - wiki=Current Wiki|user=Current User|global=Global - com.xpn.xwiki.objects.classes.StaticListClass - - - GoogleApps.LoginExtension - 0 - XWiki.UIExtensionClass - d929f4a6-d7a7-412c-acb9-e77c9bc3038a - - {{velocity}} -#if("${doc}" == "XWiki.XWikiLogin") - $xwiki.jsx.use('xwiki:GoogleApps.LoginExtension') -#end -{{/velocity}} - - - org.xwiki.platform.template.header.after - - - xwiki:GoogleApps.Login - - - - - - global - - - - diff --git a/ui/src/main/resources/GoogleApps/LoginUIExtension.vm b/ui/src/main/resources/GoogleApps/LoginUIExtension.vm new file mode 100644 index 0000000..825838e --- /dev/null +++ b/ui/src/main/resources/GoogleApps/LoginUIExtension.vm @@ -0,0 +1,99 @@ +{{velocity}}{{html clean="false" wiki="false"}} + #if ($context.action=="login" && $doc.fullName=="XWiki.XWikiLogin") + #set($googleApps = $services.googleApps) + #if($googleApps.active) + #if ($request.googleLogin == "oauthReturn") + + #set($result = $googleApps.updateUser()) + #set($prfx = "failed login:") + #if ($result.startsWith($prfx)) + #set($email = $result.substring($prfx.length())) + #set($errorMsg = " ${services.localization.render('googleapps.login.domainerror1')} ") + #set($errorMsg = " ${errorMsg} ${services.localization.render('googleapps.login.domainerror2')} $!{email}.") + #set($errorMsg = " ${errorMsg} ${services.localization.render('googleapps.login.domainerror3')}") + #elseif ($result=="ok") + #set($successM = ${services.localization.render("googleapps.login.redirectmessage")}) + #if ($request.xredirect && $request.xredirect!="") + $response.sendRedirect($request.xredirect) + #else + $response.sendRedirect($xwiki.getURL("Main.WebHome")) + #end + #else ## $result is "no user" + #set($errorMsg = ${services.localization.render('googleapps.login.message')}) + #set($errorMsg = " ${errorMsg} ${services.localization.render('googleapps.login.error')}") + #end + #elseif ($request.googleLogin == "start") + + #set($succesMsg = $services.localization.render("googleapps.login.oauth.message")) + #set($success = $googleApps.authorize(true)) + #if($success) + #if($request.state) + #set($successMsg = $services.localization.render("googleapps.login.oauth.successwithredirect")) + #else + #set($successMsg = $services.localization.render("googleapps.login.oauth.success")) + #end + #else + #set($errorMsg = $services.localization.render("googleapps.login.oauth.failedGoogleRequest")) + #end + #else + + #end + + #if ($successMsg || $errorMsg) + + + #end + + + + + #else + + #end ## googleApps is active + #end ## on login page +{{/html}}{{/velocity}} \ No newline at end of file diff --git a/ui/src/main/resources/GoogleApps/LoginUIExtension.xml b/ui/src/main/resources/GoogleApps/LoginUIExtension.xml new file mode 100644 index 0000000..c280eca --- /dev/null +++ b/ui/src/main/resources/GoogleApps/LoginUIExtension.xml @@ -0,0 +1,127 @@ + + + + + GoogleApps + LoginUIExtension + + + 0 + GoogleApps.WebHome + xwiki:XWiki.Admin + xwiki:XWiki.Admin + xwiki:XWiki.Admin + 1.1 + + <comment/> + <minorEdit>false</minorEdit> + <syntaxId>xwiki/2.1</syntaxId> + <hidden>true</hidden> + <content/> + <object><!-- TODO: replace with one such class from XWiki 8 or 9 --> + <name>GoogleApps.LoginUIExtension</name> + <number>0</number> + <className>XWiki.UIExtensionClass</className> + <guid>31588dab-0888-42ee-be22-fb6747740768</guid> + <class> + <name>XWiki.UIExtensionClass</name> + <customClass/> + <customMapping/> + <defaultViewSheet/> + <defaultEditSheet/> + <defaultWeb/> + <nameField/> + <validationScript/> + <content> + <disabled>0</disabled> + <editor>Text</editor> + <name>content</name> + <number>3</number> + <prettyName>Extension Content</prettyName> + <rows>10</rows> + <size>40</size> + <unmodifiable>0</unmodifiable> + <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType> + </content> + <extensionPointId> + <disabled>0</disabled> + <name>extensionPointId</name> + <number>1</number> + <prettyName>Extension Point ID</prettyName> + <size>30</size> + <unmodifiable>0</unmodifiable> + <classType>com.xpn.xwiki.objects.classes.StringClass</classType> + </extensionPointId> + <name> + <disabled>0</disabled> + <name>name</name> + <number>2</number> + <prettyName>Extension ID</prettyName> + <size>30</size> + <unmodifiable>0</unmodifiable> + <classType>com.xpn.xwiki.objects.classes.StringClass</classType> + </name> + <parameters> + <contenttype>PureText</contenttype> + <disabled>0</disabled> + <editor>PureText</editor> + <name>parameters</name> + <number>4</number> + <prettyName>Extension Parameters</prettyName> + <rows>10</rows> + <size>40</size> + <unmodifiable>0</unmodifiable> + <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType> + </parameters> + <scope> + <cache>0</cache> + <disabled>0</disabled> + <displayType>select</displayType> + <multiSelect>0</multiSelect> + <name>scope</name> + <number>5</number> + <prettyName>Extension Scope</prettyName> + <relationalStorage>0</relationalStorage> + <separator> </separator> + <separators>|, </separators> + <size>1</size> + <unmodifiable>0</unmodifiable> + <values>wiki=Current Wiki|user=Current User|global=Global</values> + <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType> + </scope> + </class> + <property> + <content></content> + </property> + <property> + <extensionPointId>org.xwiki.platform.topmenu.left</extensionPointId> + </property> + <property> + <name>com.xwiki.googleapps.application-googleapps.loginUIX</name> + </property> + <property> + <parameters></parameters> + </property> + <property> + <scope></scope> + </property> + </object> +</xwikidoc> \ No newline at end of file diff --git a/ui/src/main/resources/GoogleApps/OAuth.xml b/ui/src/main/resources/GoogleApps/OAuth.xml index 52b4b7b..ab2ca4d 100644 --- a/ui/src/main/resources/GoogleApps/OAuth.xml +++ b/ui/src/main/resources/GoogleApps/OAuth.xml @@ -41,19 +41,8 @@ <hidden>true</hidden> <content> {{velocity}} -$services.localization.render("googleapps.login.oauth.message") - -#set($googleApps = $services.googleApps) -#set($creds = $googleApps.authorize()) -#if($creds) -#if($request.state) -$services.localization.render("googleapps.login.oauth.success") -#else -$services.localization.render("googleapps.login.oauth.successwithredirect") -#end -#else -$services.localization.render("googleapps.login.oauth.notauthenticated") -#end + This page is not used anymore in Google Apps. Please remove any link to it. + Please replace the redirect URLs at the Google Console to be /login/XWiki/XWikiLogin . {{/velocity}}</content> </xwikidoc> diff --git a/ui/src/main/resources/GoogleApps/RetrieveFromGoogleApps.xml b/ui/src/main/resources/GoogleApps/RetrieveFromGoogleApps.xml index fc2c9e4..b9b52cb 100644 --- a/ui/src/main/resources/GoogleApps/RetrieveFromGoogleApps.xml +++ b/ui/src/main/resources/GoogleApps/RetrieveFromGoogleApps.xml @@ -41,13 +41,20 @@ <hidden>true</hidden> <content>{{velocity}} #set($googleApps = $services.googleApps) -#if($request.url) - #set($ok = $googleApps.retrieveFileFromGoogle($request.page, $request.name, $request.id, $request.url)) - #if(!$ok) -$services.localization.render("googleapps.retrieve.fail") $request.id - #end -#else -$services.localization.render("googleapps.retrieve.nodocument") +#if($request.url || $request.mediaType) + #set($mediaType = $request.mediaType) + #if("$!{mediaType}"=="")#set($mediaType="unknown")#end + #try() + #set($ok = $googleApps.retrieveFileFromGoogle($request.page, $request.name, $request.id, $mediaType)) + #set($url = $xwiki.getDocument($request.page).getURL("view", "#Attachments")) + $response.sendRedirect($url) + #end + #if ("$!exception" != '') + $services.localization.render("googleapps.retrieve.fail") $request.id + #displayException($exception) + #else + redirecting... + #end #end {{/velocity}}</content> </xwikidoc> diff --git a/ui/src/main/resources/GoogleApps/TestDocumentList.xml b/ui/src/main/resources/GoogleApps/TestDocumentList.xml index 8c30515..f0b4ceb 100644 --- a/ui/src/main/resources/GoogleApps/TestDocumentList.xml +++ b/ui/src/main/resources/GoogleApps/TestDocumentList.xml @@ -52,7 +52,7 @@ {{translation key='googleapps.testDocList.about'/}} #set($googleApps = $services.googleApps) -#set($doclist = $googleApps.getDocumentList()) +#set($doclist = $googleApps.listDriveDocuments("",10)) #foreach($item in $doclist) * [[$item.title $item.id>>$item.editLink]] #end diff --git a/ui/src/main/resources/GoogleApps/Translations.de.properties b/ui/src/main/resources/GoogleApps/Translations.de.properties index e885ca2..83e13b4 100644 --- a/ui/src/main/resources/GoogleApps/Translations.de.properties +++ b/ui/src/main/resources/GoogleApps/Translations.de.properties @@ -64,7 +64,6 @@ googleapps.import.nextresults.button=Weitere Ergebnisse googleapps.retrieve.title=Aus Google Apps abrufen googleapps.retrieve.fail=Fehler beim Abrufen aus Google Apps -googleapps.retrieve.nodocument=Kein Dokument kann abgerufen werden. googleapps.login.title=Google Apps Anmeldung googleapps.login.withgoogle=Mit Google anmelden @@ -118,6 +117,7 @@ googleapps.webHome.5=Siehe das {0}Drive Macro Test{1}, um es zu testen. ################################################## googleapps.error.authConfigNotFound=Die Authentifizierungskonfiguration, die die Anemdlung durhc Google erlauben sollte, ist nicht gefunden worden. Es ist wahrscheinlich, dass die Benutzer, die versuchen sich durch Google anuzmelden, nicht angemelddet werden. googleapps.error.authConfigShould=Die {0}Installationsanweisungen{1} schlagen vor, die folgende Zeilen in xwiki.cfg einzufügen +googleapps.retrieve.nodocument=Kein Dokument kann abgerufen werden. ################################################## ## until 2.4-rc-1 diff --git a/ui/src/main/resources/GoogleApps/Translations.de.xml b/ui/src/main/resources/GoogleApps/Translations.de.xml index 08a0b4f..46a69d0 100644 --- a/ui/src/main/resources/GoogleApps/Translations.de.xml +++ b/ui/src/main/resources/GoogleApps/Translations.de.xml @@ -35,6 +35,6 @@ <comment/> <minorEdit>false</minorEdit> <syntaxId>plain/1.0</syntaxId> - <hidden>false</hidden> + <hidden>true</hidden> <content/> </xwikidoc> diff --git a/ui/src/main/resources/GoogleApps/Translations.fr.properties b/ui/src/main/resources/GoogleApps/Translations.fr.properties index df5a767..2891c20 100644 --- a/ui/src/main/resources/GoogleApps/Translations.fr.properties +++ b/ui/src/main/resources/GoogleApps/Translations.fr.properties @@ -63,7 +63,6 @@ googleapps.import.nextresults.button=Résultats suivants googleapps.retrieve.title=Récuperer document de Google Apps googleapps.retrieve.fail=Échec de la récupération du document de Google Apps -googleapps.retrieve.nodocument=Aucun document donné pour la récupération. googleapps.login.title=Authentification Google Apps googleapps.login.withgoogle=Connexion avec Google @@ -74,7 +73,6 @@ googleapps.login.redirectmessage=Vous allez être redirigé vers la page demand googleapps.login.oauth.message=Authentication Google en cours googleapps.login.oauth.success=Vous avez été authentifié avec succès. googleapps.login.oauth.successwithredirect=Vous avez été authentifié avec succès. Vous allez être redirigé vers votre page. -googleapps.login.oauth.notauthenticated=Vous allez être redirigé vers Google pour l'authentification. googleapps.login.domainerror1=Vous n'êtes pas autorisé à vous connecter à cette instance XWiki. googleapps.login.domainerror2=Votre compte Google actuel est googleapps.login.domainerror3=Merci de vous déconnecter de ce compte et réessayer l'authentification. @@ -117,6 +115,8 @@ googleapps.webHome.5=Voyez la apge {0}Drive Macro Test{1} pour tester la macro. ################################################## googleapps.error.authConfigNotFound=La configuration de "l'authentificateur" permettant l'idenfitication par Google n'as pas été trouvée; il est probable que les utilisateurs qui tentent de s'inscrire par le biais de Google ne le feront pas. googleapps.error.authConfigShould=Les {0}instructions d''installation{1} suggèrent d''ajouter les lignes suivantes à xwiki.cfg +googleapps.retrieve.nodocument=Aucun document donné pour la récupération. +googleapps.login.oauth.notauthenticated=Vous allez être redirigé vers Google pour l'authentification. ################################################## ## until 2.4-rc-1 diff --git a/ui/src/main/resources/GoogleApps/Translations.fr.xml b/ui/src/main/resources/GoogleApps/Translations.fr.xml index c25bc53..11d66df 100644 --- a/ui/src/main/resources/GoogleApps/Translations.fr.xml +++ b/ui/src/main/resources/GoogleApps/Translations.fr.xml @@ -35,6 +35,6 @@ <comment/> <minorEdit>false</minorEdit> <syntaxId>plain/1.0</syntaxId> - <hidden>false</hidden> + <hidden>true</hidden> <content/> </xwikidoc> diff --git a/ui/src/main/resources/GoogleApps/Translations.properties b/ui/src/main/resources/GoogleApps/Translations.properties index 38823eb..8d012e2 100644 --- a/ui/src/main/resources/GoogleApps/Translations.properties +++ b/ui/src/main/resources/GoogleApps/Translations.properties @@ -64,7 +64,6 @@ googleapps.import.nextresults.button=Next results googleapps.retrieve.title=Retrieve document from Google Apps googleapps.retrieve.fail=Failed to retrieve document from Google Apps -googleapps.retrieve.nodocument=No document has been given for retrieval. googleapps.login.title=Google Apps Login googleapps.login.withgoogle=Login with Google @@ -75,7 +74,7 @@ googleapps.login.redirectmessage=You are being redirected to your requested page googleapps.login.oauth.message=Google Authentication in progress googleapps.login.oauth.success=Successful authentication. googleapps.login.oauth.successwithredirect=Successful authentication. You are being redirected to your page. -googleapps.login.oauth.notauthenticated=You will be redirected to Google for Authentication. +googleapps.login.oauth.failedGoogleRequest=Cannot recognize your Google token, please try again. googleapps.login.domainerror1=This Google account does not allow you to login to this XWiki, because it does not match the expected domain name. googleapps.login.domainerror2=Your current Google account is googleapps.login.domainerror3=Please disconnect from this account, and try to login again. @@ -118,6 +117,8 @@ googleapps.webHome.5=See {0}Drive Macro Test{1} to try it out. ################################################## googleapps.error.authConfigNotFound=The authenticator configuration to enable Google identification has not been found; it is likely that users that attempt to login through Google will not do so. googleapps.error.authConfigShould=The {0}installation instructions{1} advise to add the following line to the file xwiki.cfg +googleapps.retrieve.nodocument=No document has been given for retrieval. +googleapps.login.oauth.notauthenticated=You will be redirected to Google for Authentication. ################################################## ## until 2.4-rc-1 diff --git a/ui/src/main/resources/GoogleApps/WebHome.xml b/ui/src/main/resources/GoogleApps/WebHome.xml index ed0d3f7..f21a398 100644 --- a/ui/src/main/resources/GoogleApps/WebHome.xml +++ b/ui/src/main/resources/GoogleApps/WebHome.xml @@ -46,19 +46,21 @@ #if (!$services.licensing.licensor.hasLicensureForEntity($mainReference)) {{error}}#getMissingLicenseMessage('googleapps.extension.name'){{/error}} #else -$services.localization.render('googleapps.webHome.1') -$services.localization.render('googleapps.webHome.2') -## You can test the integration in test-document-list -$services.localization.render('googleapps.webHome.3', ["[[",">>TestDocumentList]]"]) + $services.localization.render('googleapps.webHome.1') + $services.localization.render('googleapps.webHome.2') + ## You can test the integration in test-document-list + $services.localization.render('googleapps.webHome.3', ["[[",">>TestDocumentList]]"]) -#if($hasGlobalAdmin) -#set($url=$xwiki.getURL('XWiki.XWikiPreferences', 'admin', 'editor=globaladmin&section=googleapps')) -## you can modify the config here -$services.localization.render('googleapps.webHome.4', ["[[",">>path:$url]]"]) -## you can try it in driveMacroTest -$services.localization.render('googleapps.webHome.5', ["[[",">>DriveMacroTest]]"]) -#end + #if($hasGlobalAdmin) + #set($url=$xwiki.getURL('XWiki.XWikiPreferences', 'admin', 'editor=globaladmin&section=googleapps')) + ## you can modify the config here + $services.localization.render('googleapps.webHome.4', ["[[",">>path:$url]]"]) + ## you can try it in driveMacroTest + $services.localization.render('googleapps.webHome.5', ["[[",">>DriveMacroTest]]"]) + #end #end + +[[=> More about the GoogleApps Application>>https://store.xwiki.com/xwiki/bin/view/Extension/GoogleAppsIntegration]]. {{/velocity}} </content> </xwikidoc>