From 800609161cae04330b192f6856b40c1cf1f03720 Mon Sep 17 00:00:00 2001 From: Massimiliano Assante Date: Thu, 4 Sep 2014 16:08:31 +0000 Subject: [PATCH] ready to release git-svn-id: http://svn.research-infrastructures.eu/public/d4science/gcube/trunk/portlets/user/social-profile@99511 82a268e6-3cf1-43bd-a215-b396298e98cf --- pom.xml | 10 + .../socialprofile/client/SocialProfile.java | 10 +- .../client/ui/DisplayProfile.java | 2 +- .../socialprofile/client/ui/ErrorAlert.java | 15 +- .../socialprofile/client/ui/ErrorAlert.ui.xml | 31 ++- .../server/SocialServiceImpl.java | 200 ++++++++++++++---- 6 files changed, 207 insertions(+), 61 deletions(-) diff --git a/pom.xml b/pom.xml index dec180c..c25ee04 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,16 @@ custom-portal-handler provided + + org.gcube.core + common-encryption + provided + + + org.gcube.core + common-scope-maps + compile + com.googlecode.json-simple json-simple diff --git a/src/main/java/org/gcube/portlets/user/socialprofile/client/SocialProfile.java b/src/main/java/org/gcube/portlets/user/socialprofile/client/SocialProfile.java index 1db83f0..edd6592 100644 --- a/src/main/java/org/gcube/portlets/user/socialprofile/client/SocialProfile.java +++ b/src/main/java/org/gcube/portlets/user/socialprofile/client/SocialProfile.java @@ -37,7 +37,8 @@ public class SocialProfile implements EntryPoint { public void onSuccess(Boolean result) { if (!result) { mainPanel.clear(); - mainPanel.add(new ErrorAlert("Something went wrong while parsing your professional summary from LinkedIn, please report the issue.")); + mainPanel.add(new ErrorAlert("Something went wrong while parsing your professional summary from LinkedIn, please report the issue.", true)); + displayProfile(); } else { mainPanel.clear(); @@ -48,7 +49,8 @@ public class SocialProfile implements EntryPoint { @Override public void onFailure(Throwable caught) { mainPanel.clear(); - mainPanel.add(new ErrorAlert("Something went wrong while communicating with LinkedIn service, please report us the issue.")); + mainPanel.add(new ErrorAlert("Something went wrong while communicating with LinkedIn service, please report us the issue.", true)); + displayProfile(); } }); @@ -85,14 +87,14 @@ public class SocialProfile implements EntryPoint { */ private String checkLinkedInAuthZ() { if (Window.Location.getParameter("error") != null) { - mainPanel.add(new ErrorAlert("it seems you denied our request to import your professional summary from LinkedIn.")); + mainPanel.add(new ErrorAlert("it seems you denied our request to import your professional summary from LinkedIn.", true)); return null; } String code = Window.Location.getParameter("code"); String controlSequence = Window.Location.getParameter("state"); String cSeq2Compare = Cookies.getCookie(DisplayProfile.CONTROL_SEQUENCE_COOKIE); if (controlSequence.compareTo(cSeq2Compare) != 0) { - mainPanel.add(new ErrorAlert("Something went wrong when importing your professional summary from LinkedIn, please try again.")); + mainPanel.add(new ErrorAlert("Something went wrong when importing your professional summary from LinkedIn, please try again.", true)); return null; } diff --git a/src/main/java/org/gcube/portlets/user/socialprofile/client/ui/DisplayProfile.java b/src/main/java/org/gcube/portlets/user/socialprofile/client/ui/DisplayProfile.java index b25331c..8de2f75 100644 --- a/src/main/java/org/gcube/portlets/user/socialprofile/client/ui/DisplayProfile.java +++ b/src/main/java/org/gcube/portlets/user/socialprofile/client/ui/DisplayProfile.java @@ -35,6 +35,7 @@ public class DisplayProfile extends Composite { protected final static String HEADLINE_ERROR = "Your Headline please"; protected final static String ISTI_TEXT = "Company"; protected final static String ISTI_ERROR = "Your Company please"; + private final static String D4S_APP_ID = "77n7r4c9nwuwk2"; private static DisplayProfileUiBinder uiBinder = GWT .create(DisplayProfileUiBinder.class); @@ -135,7 +136,6 @@ public class DisplayProfile extends Composite { @Override public void onClick(ClickEvent event) { String OAUTH2_SERVICE = "https://www.linkedin.com/uas/oauth2/authorization?response_type=code"; - String D4S_APP_ID = "77n7r4c9nwuwk2"; String controlSequence = getRandomString(); //needed to prevent Cross Site Request Forgery attacks diff --git a/src/main/java/org/gcube/portlets/user/socialprofile/client/ui/ErrorAlert.java b/src/main/java/org/gcube/portlets/user/socialprofile/client/ui/ErrorAlert.java index 3855abe..37331ed 100644 --- a/src/main/java/org/gcube/portlets/user/socialprofile/client/ui/ErrorAlert.java +++ b/src/main/java/org/gcube/portlets/user/socialprofile/client/ui/ErrorAlert.java @@ -1,9 +1,13 @@ package org.gcube.portlets.user.socialprofile.client.ui; +import org.gcube.portlets.user.gcubewidgets.client.elements.Span; + import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Element; +import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; +import com.google.gwt.uibinder.client.UiHandler; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.Widget; @@ -16,9 +20,18 @@ public class ErrorAlert extends Composite { } @UiField Element errorMessage; + @UiField Span handler; - public ErrorAlert(String message) { + public ErrorAlert(String message, boolean removable) { initWidget(uiBinder.createAndBindUi(this)); errorMessage.setInnerText(message); + if (removable) + handler.setHTML(" Close"); + } + + + @UiHandler("handler") + void onCloseClick(ClickEvent e) { + this.removeFromParent(); } } diff --git a/src/main/java/org/gcube/portlets/user/socialprofile/client/ui/ErrorAlert.ui.xml b/src/main/java/org/gcube/portlets/user/socialprofile/client/ui/ErrorAlert.ui.xml index 3e42dd9..2654bdd 100644 --- a/src/main/java/org/gcube/portlets/user/socialprofile/client/ui/ErrorAlert.ui.xml +++ b/src/main/java/org/gcube/portlets/user/socialprofile/client/ui/ErrorAlert.ui.xml @@ -1,19 +1,32 @@ + xmlns:g="urn:import:com.google.gwt.user.client.ui" + xmlns:m="urn:import:org.gcube.portlets.user.gcubewidgets.client.elements"> .alert { border: 1px solid #EBCCD1; - border-radius: 4px; + border-radius: 4px; margin: 20px; - padding: 15px; - background-color: #F2DEDE; - color: #A94440; - font-family: 'Helvetica Neue', Arial, sans-serif; - font-size: 14px; + padding: 15px; + background-color: #F2DEDE; + color: #A94440; + font-family: 'Helvetica Neue', Arial, sans-serif; + font-size: 14px; + } + + .profile-link { + font-weight: bold; + } + + .profile-link:hover { + cursor: pointer; + cursor: hand; + text-decoration: underline; } - - Import Error: + + Import Error: + + \ No newline at end of file diff --git a/src/main/java/org/gcube/portlets/user/socialprofile/server/SocialServiceImpl.java b/src/main/java/org/gcube/portlets/user/socialprofile/server/SocialServiceImpl.java index 5b2c77f..e000850 100644 --- a/src/main/java/org/gcube/portlets/user/socialprofile/server/SocialServiceImpl.java +++ b/src/main/java/org/gcube/portlets/user/socialprofile/server/SocialServiceImpl.java @@ -1,5 +1,8 @@ package org.gcube.portlets.user.socialprofile.server; +import static org.gcube.resources.discovery.icclient.ICFactory.clientFor; +import static org.gcube.resources.discovery.icclient.ICFactory.queryFor; + import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; @@ -23,11 +26,19 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.gcube.application.framework.core.session.ASLSession; import org.gcube.application.framework.core.session.SessionManager; +import org.gcube.common.encryption.StringEncrypter; +import org.gcube.common.resources.gcore.ServiceEndpoint; +import org.gcube.common.resources.gcore.ServiceEndpoint.AccessPoint; +import org.gcube.common.resources.gcore.ServiceEndpoint.Property; +import org.gcube.common.resources.gcore.utils.XPathHelper; +import org.gcube.common.scope.api.ScopeProvider; import org.gcube.portal.custom.communitymanager.OrganizationsUtil; import org.gcube.portal.custom.scopemanager.scopehelper.ScopeHelper; import org.gcube.portal.databook.shared.UserInfo; import org.gcube.portlets.user.socialprofile.client.SocialService; import org.gcube.portlets.user.socialprofile.shared.UserContext; +import org.gcube.resources.discovery.client.api.DiscoveryClient; +import org.gcube.resources.discovery.client.queries.api.SimpleQuery; import org.json.simple.parser.ContainerFactory; import org.json.simple.parser.JSONParser; import org.slf4j.Logger; @@ -48,6 +59,12 @@ public class SocialServiceImpl extends RemoteServiceServlet implements SocialSer private static final Logger _log = LoggerFactory.getLogger(SocialServiceImpl.class); + private static final String LINKEDIN_HOST_SERVICE_NAME = "host"; + private static final String LINKEDIN_CLIEND_ID_PROPNAME = "client_id"; + private static final String LINKEDIN_CLIEND_SECRET_PROPNAME = "client_secret"; + + private static final String LINKEDIN_API_REQUEST = "https://api.linkedin.com/v1/people/~:(id,headline,summary,location:(name),industry,positions)"; + /** * the current ASLSession * @return the session @@ -58,6 +75,7 @@ public class SocialServiceImpl extends RemoteServiceServlet implements SocialSer if (user == null) { _log.warn("USER IS NULL setting test.user and Running OUTSIDE PORTAL"); user = getDevelopmentUser(); + SessionManager.getInstance().getASLSession(sessionID, user).setScope("/gcube"); } return SessionManager.getInstance().getASLSession(sessionID, user); } @@ -216,32 +234,24 @@ public class SocialServiceImpl extends RemoteServiceServlet implements SocialSer } /** - * Escape an html string. Escaping data received from the client helps to - * prevent cross-site script vulnerabilities. - * - * @param html the html string to escape - * @return the escaped string + * This method fetches the user profile by authenticating with LinkedIn through OAuth2 protocol + * once authenitcated it call another methos to get the profile and parse it. + * @return true if everything goes ok, false otherwise. */ - private String escapeHtml(String html) { - if (html == null) { - return null; - } - return html.replaceAll("&", "&").replaceAll("<", "<") - .replaceAll(">", ">"); - } @Override public Boolean fetchUserProfile(String authCode, String redirectURI) { try { + HashMap infoMap = getLinkedInUASInfo(); HttpClient httpClient = HttpClients.createDefault(); - HttpPost httpPost = new HttpPost("https://www.linkedin.com/uas/oauth2/accessToken"); + HttpPost httpPost = new HttpPost(infoMap.get(LINKEDIN_HOST_SERVICE_NAME)); // Request parameters and other properties. ArrayList params = new ArrayList(5); params.add(new BasicNameValuePair("grant_type", "authorization_code")); params.add(new BasicNameValuePair("code", authCode)); params.add(new BasicNameValuePair("redirect_uri", redirectURI)); - params.add(new BasicNameValuePair("client_id", "xxx")); - params.add(new BasicNameValuePair("client_secret", "xxx")); + params.add(new BasicNameValuePair("client_id", infoMap.get(LINKEDIN_CLIEND_ID_PROPNAME))); + params.add(new BasicNameValuePair("client_secret", infoMap.get(LINKEDIN_CLIEND_SECRET_PROPNAME))); httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")); @@ -263,11 +273,9 @@ public class SocialServiceImpl extends RemoteServiceServlet implements SocialSer public List creatArrayContainer() { return new LinkedList(); } - public Map createObjectContainer() { return new LinkedHashMap(); } - }; @SuppressWarnings("unchecked") Map json = (Map) parser.parse(jsonText, containerFactory); @@ -278,7 +286,7 @@ public class SocialServiceImpl extends RemoteServiceServlet implements SocialSer String token = json.get("access_token"); if (token == null) return false; - + //here we got authorized by the user return parseProfile(httpClient, token); } finally { @@ -293,16 +301,18 @@ public class SocialServiceImpl extends RemoteServiceServlet implements SocialSer return true; } - - + /** + * Ask the basic profile to LinkedIn API for the authenticated user, parse rge profile and write info into local DB. + * @param httpClient + * @param token + * @return + */ private boolean parseProfile(HttpClient httpClient, String token) { - // HttpGet request = new HttpGet("https://api.linkedin.com/v1/people/~"); - - HttpGet request = new HttpGet("https://api.linkedin.com/v1/people/~:(id,headline,summary,location:(name),industry)"); - - // add request header + HttpGet request = new HttpGet(LINKEDIN_API_REQUEST); + // add request header as in the documentation, @see https://developer.linkedin.com/documents/authentication request.addHeader("Authorization", "Bearer " + token); try { + _log.debug("Asking LinkedIn profile via http GET for " + getASLSession().getUsername()); HttpResponse httpResponse = httpClient.execute(request); HttpEntity entity = httpResponse.getEntity(); @@ -316,30 +326,67 @@ public class SocialServiceImpl extends RemoteServiceServlet implements SocialSer DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); Document doc = dBuilder.parse(IOUtils.toInputStream(xmlResponse)); - _log.debug("Parsing xmlResponse ... "); - doc.getDocumentElement().normalize(); - + _log.debug("Parsing LinkedIn profile xmlResponse for " + getASLSession().getUsername()); String headline = ""; - if (doc.getDocumentElement().getElementsByTagName("headline") != null && doc.getDocumentElement().getElementsByTagName("headline").getLength() > 0) - headline = doc.getDocumentElement().getElementsByTagName("headline").item(0).getTextContent(); - String summary = ""; - if (doc.getDocumentElement().getElementsByTagName("summary") != null && doc.getDocumentElement().getElementsByTagName("summary").getLength() > 0) - summary = doc.getDocumentElement().getElementsByTagName("summary").item(0).getTextContent(); - String location = ""; - if (doc.getDocumentElement().getElementsByTagName("name") != null && doc.getDocumentElement().getElementsByTagName("name").getLength() > 0) - location = doc.getDocumentElement().getElementsByTagName("name").item(0).getTextContent(); - String industry = ""; - if (doc.getDocumentElement().getElementsByTagName("industry") != null && doc.getDocumentElement().getElementsByTagName("industry").getLength() > 0) - industry = doc.getDocumentElement().getElementsByTagName("industry").item(0).getTextContent(); - System.out.println("headline: " + headline); - System.out.println("summary: " + summary); - System.out.println("location: " + location); - System.out.println("industry: " + industry); - + List currValue = null; + XPathHelper helper = new XPathHelper(doc.getDocumentElement()); + currValue = helper.evaluate("/person/headline/text()"); + if (currValue != null && currValue.size() > 0) { + headline = currValue.get(0); + } + currValue = helper.evaluate("/person/summary/text()"); + if (currValue != null && currValue.size() > 0) { + summary = currValue.get(0); + } + currValue = helper.evaluate("/person/location/name/text()"); + if (currValue != null && currValue.size() > 0) { + location = currValue.get(0); + } + currValue = helper.evaluate("/person/industry/text()"); + if (currValue != null && currValue.size() > 0) { + industry = currValue.get(0); + } + //positions + String positions = ""; + currValue = helper.evaluate("/person/positions/position"); + int positionsNo = currValue.size(); + if (positionsNo > 0) { + positions = (positionsNo > 1) ? "\n\nCurrent Positions:" : "\n\nCurrent Position:"; + for (int i = 0; i < positionsNo; i++) { + currValue = helper.evaluate("/person/positions/position/title/text()"); + if (currValue != null && currValue.size() > 0) { + positions += "\n\n"+currValue.get(i); + } + currValue = helper.evaluate("/person/positions/position/company/name/text()"); + if (currValue != null && currValue.size() > 0) { + positions += "\n"+currValue.get(i); + } + currValue = helper.evaluate("/person/positions/position/company/type/text()"); + if (currValue != null && currValue.size() > 0) { + positions += "\n" + currValue.get(i)+"; "; + } + currValue = helper.evaluate("/person/positions/position/company/size/text()"); + if (currValue != null && currValue.size() > 0) { + positions += currValue.get(i)+"; "; + } + currValue = helper.evaluate("/person/positions/position/company/industry/text()"); + if (currValue != null && currValue.size() > 0) { + positions += currValue.get(i) + " industry."; + } + currValue = helper.evaluate("/person/positions/position/summary/text()"); + if (currValue != null && currValue.size() > 0) { + positions += "\n\n" + currValue.get(i); + } + } + } + //add the positions to the summary + summary += positions; + _log.debug("LinkedIn Profile gotten correctly for " + getASLSession().getUsername() + " attempting to write into DB ..."); + if (isWithinPortal()) { com.liferay.portal.model.User user; user = UserLocalServiceUtil.getUserByScreenName(OrganizationsUtil.getCompany().getCompanyId(), getASLSession().getUsername()); @@ -354,12 +401,11 @@ public class SocialServiceImpl extends RemoteServiceServlet implements SocialSer //summary if (summary.compareTo("") != 0) user.setComments(escapeHtml(summary)); - + return (UserLocalServiceUtil.updateUser(user) != null); } else { _log.warn("Development Mode ON, not attempting to write into DB"); } - } finally { myInputStream.close(); @@ -372,4 +418,66 @@ public class SocialServiceImpl extends RemoteServiceServlet implements SocialSer } } + + /** + * this method return the URL and the keys to access the LinkedIn User AuthN Service + * @return an hashmap containing the 3 info needed + */ + private HashMap getLinkedInUASInfo(){ + String scope = getASLSession().getScope(); + _log.info("Looking for a LinkedIn UAS in " + scope); + String previousScope = ScopeProvider.instance.get(); + ScopeProvider.instance.set(scope); + SimpleQuery query = queryFor(ServiceEndpoint.class); + query.addCondition("$resource/Profile/Category/string() eq 'Service'"); + query.addCondition("$resource/Profile/Name/string() eq 'LinkedIn-user-authorization'"); + + DiscoveryClient client = clientFor(ServiceEndpoint.class); + List list = client.submit(query); + ScopeProvider.instance.set(previousScope); + + if (list.size() > 1) { + _log.warn("Multiple LinkedIn-user-authorization Service Endpoints available in the scope, should be only one."); + return null; + } + else if (list.size() == 1) { + ServiceEndpoint se = list.get(0); + AccessPoint ap = se.profile().accessPoints().iterator().next(); + String authServiceURL = ap.address(); + String clientId = ""; + String clientSecret = ""; + try { + for (Property property : ap.properties()) { + if (property.name().compareTo(LINKEDIN_CLIEND_ID_PROPNAME) == 0) + clientId = StringEncrypter.getEncrypter().decrypt(property.value()); + if (property.name().compareTo(LINKEDIN_CLIEND_SECRET_PROPNAME) == 0) + clientSecret = StringEncrypter.getEncrypter().decrypt(property.value()); + } + } catch (Exception e) { + e.printStackTrace(); + } + + HashMap map = new HashMap(); + map.put(LINKEDIN_HOST_SERVICE_NAME, authServiceURL); + map.put(LINKEDIN_CLIEND_ID_PROPNAME, clientId); + map.put(LINKEDIN_CLIEND_SECRET_PROPNAME, clientSecret); + return map; + } + else return null; + } + + /** + * Escape an html string. Escaping data received from the client helps to + * prevent cross-site script vulnerabilities. + * + * @param html the html string to escape + * @return the escaped string + */ + private String escapeHtml(String html) { + if (html == null) { + return null; + } + return html.replaceAll("&", "&").replaceAll("<", "<") + .replaceAll(">", ">"); + } }