From 2e9528203a5e8459a8cb6a7d60f3b186db1e272a Mon Sep 17 00:00:00 2001 From: Ekaterina Tuzova Date: Wed, 25 Mar 2015 16:41:54 +0300 Subject: [PATCH] added stepic.org authorization --- .../edu/learning/ui/StudyNewProjectPanel.form | 68 ++++++- .../edu/learning/ui/StudyNewProjectPanel.java | 58 ++++-- .../edu/stepic/EduStepicConnector.java | 184 +++++++++++------- .../com/jetbrains/edu/stepic/LoginDialog.java | 62 ++++++ .../com/jetbrains/edu/stepic/LoginPanel.form | 52 +++++ .../com/jetbrains/edu/stepic/LoginPanel.java | 69 +++++++ 6 files changed, 410 insertions(+), 83 deletions(-) create mode 100644 python/educational/src/com/jetbrains/edu/stepic/LoginDialog.java create mode 100644 python/educational/src/com/jetbrains/edu/stepic/LoginPanel.form create mode 100644 python/educational/src/com/jetbrains/edu/stepic/LoginPanel.java diff --git a/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.form b/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.form index 3c5369f403de..aeff7d7125ba 100644 --- a/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.form +++ b/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.form @@ -1,6 +1,6 @@
- + @@ -67,6 +67,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java b/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java index afa2484d96a6..b84c7f526a9f 100644 --- a/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java +++ b/python/educational/interactive-learning/src/com/jetbrains/edu/learning/ui/StudyNewProjectPanel.java @@ -3,10 +3,12 @@ package com.jetbrains.edu.learning.ui; import com.intellij.facet.ui.FacetValidatorsManager; import com.intellij.facet.ui.ValidationResult; import com.intellij.icons.AllIcons; +import com.intellij.ui.HideableDecorator; import com.jetbrains.edu.courseFormat.Course; import com.jetbrains.edu.learning.StudyUtils; import com.jetbrains.edu.learning.courseGeneration.StudyProjectGenerator; import com.jetbrains.edu.stepic.CourseInfo; +import com.jetbrains.edu.stepic.EduStepicConnector; import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -20,6 +22,7 @@ import java.util.List; * data: 7/31/14. */ public class StudyNewProjectPanel{ + private final HideableDecorator myDecorator; private List myAvailableCourses = new ArrayList(); private JComboBox myCoursesComboBox; private JButton myRefreshButton; @@ -28,6 +31,11 @@ public class StudyNewProjectPanel{ private JLabel myDescriptionLabel; private JLabel myLabel; private JPanel myInfoPanel; + private JPanel myHideablePanel; + private JPanel myLoginPanel; + private JPasswordField myPasswordField; + private JTextField myLoginField; + private JButton myLogInButton; private final StudyProjectGenerator myGenerator; private static final String CONNECTION_ERROR = "Failed to download courses.
Check your Internet connection."; private static final String INVALID_COURSE = "Selected course is invalid"; @@ -54,12 +62,26 @@ public class StudyNewProjectPanel{ myRefreshButton.setIcon(AllIcons.Actions.Refresh); myLabel.setPreferredSize(new JLabel("Project name").getPreferredSize()); + myDecorator = new HideableDecorator(myHideablePanel, "Credentials", false); + myDecorator.setOn(false); + myDecorator.setContentComponent(myLoginPanel); + } private void initListeners() { - myRefreshButton.addActionListener(new RefreshActionListener()); myCoursesComboBox.addActionListener(new CourseSelectedListener()); + myLogInButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final boolean isSuccess = EduStepicConnector.login(myLoginField.getText(), String.valueOf(myPasswordField.getPassword())); + if (!isSuccess) { + setError("Failed to log in"); + return; + } + refreshCoursesList(); + } + }); } private void setError(@NotNull final String errorMessage) { @@ -93,24 +115,28 @@ public class StudyNewProjectPanel{ private class RefreshActionListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { - final List courses = myGenerator.getCourses(true); - if (courses.isEmpty()) { - setError(CONNECTION_ERROR); - return; - } - myCoursesComboBox.removeAllItems(); - - for (CourseInfo courseInfo : courses) { - myCoursesComboBox.addItem(courseInfo); - } - myGenerator.setSelectedCourse(StudyUtils.getFirst(courses)); - - myGenerator.setCourses(courses); - myAvailableCourses = courses; - myGenerator.flushCache(); + refreshCoursesList(); } } + private void refreshCoursesList() { + final List courses = myGenerator.getCourses(true); + if (courses.isEmpty()) { + setError(CONNECTION_ERROR); + return; + } + myCoursesComboBox.removeAllItems(); + + for (CourseInfo courseInfo : courses) { + myCoursesComboBox.addItem(courseInfo); + } + myGenerator.setSelectedCourse(StudyUtils.getFirst(courses)); + + myGenerator.setCourses(courses); + myAvailableCourses = courses; + myGenerator.flushCache(); + } + /** * Handles selecting course in combo box diff --git a/python/educational/src/com/jetbrains/edu/stepic/EduStepicConnector.java b/python/educational/src/com/jetbrains/edu/stepic/EduStepicConnector.java index f4314caca004..a37c4680cf34 100644 --- a/python/educational/src/com/jetbrains/edu/stepic/EduStepicConnector.java +++ b/python/educational/src/com/jetbrains/edu/stepic/EduStepicConnector.java @@ -10,69 +10,139 @@ import com.intellij.openapi.editor.Document; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.util.io.HttpRequests; import com.intellij.util.net.ssl.CertificateManager; import com.jetbrains.edu.EduUtils; import com.jetbrains.edu.courseFormat.Course; import com.jetbrains.edu.courseFormat.Lesson; import com.jetbrains.edu.courseFormat.Task; import com.jetbrains.edu.courseFormat.TaskFile; -import org.apache.http.Header; +import org.apache.http.NameValuePair; import org.apache.http.StatusLine; +import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.cookie.Cookie; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; -import org.apache.http.impl.cookie.BasicClientCookie; import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.protocol.HTTP; import org.apache.http.util.EntityUtils; import org.jetbrains.annotations.NotNull; -import java.io.BufferedReader; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.util.*; public class EduStepicConnector { - private static final String stepicApiUrl = "https://stepic.org/api/"; + private static final String stepicUrl = "https://stepic.org/"; + private static final String stepicApiUrl = stepicUrl + "api/"; private static final Logger LOG = Logger.getInstance(EduStepicConnector.class.getName()); private static final String ourDomain = "stepic.org"; - private static String ourSessionId = "524iethiwju2tjywaqmf7tbwx0p0jk1b"; - private static String ourCSRFToken = "LJ9n6OyLVA7hxU94dlYWUu65MF51Nx37"; + private static String ourCSRFToken = ""; + private static CloseableHttpClient ourClient; + //this prefix indicates that course can be opened by educational plugin public static final String PYCHARM_PREFIX = "pycharm"; + private static BasicCookieStore ourCookieStore; private EduStepicConnector() { } - private static T getFromStepic(String link, final Class container) throws IOException { - return HttpRequests.request(stepicApiUrl + link).connect(new HttpRequests.RequestProcessor() { - @Override - public T process(@NotNull HttpRequests.Request request) throws IOException { - final BufferedReader reader = request.getReader(); - Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); - return gson.fromJson(reader, container); + public static boolean login(@NotNull final String user, @NotNull final String password) { + if (ourClient == null || ourCookieStore == null) + initializeClient(); + return postCredentials(user, password); + } + + private static void initializeClient() { + final HttpGet request = new HttpGet(stepicUrl); + request.addHeader(new BasicHeader("referer", "https://stepic.org")); + request.addHeader(new BasicHeader("content-type", "application/json")); + + HttpClientBuilder builder = HttpClients.custom().setSslcontext(CertificateManager.getInstance().getSslContext()).setMaxConnPerRoute(100); + ourCookieStore = new BasicCookieStore(); + ourClient = builder.setDefaultCookieStore(ourCookieStore).build(); + + try { + ourClient.execute(request); + saveCSRFToken(); + } + catch (IOException e) { + LOG.error(e.getMessage()); + } + } + + private static void saveCSRFToken() { + if (ourCookieStore == null) return; + final List cookies = ourCookieStore.getCookies(); + for (Cookie cookie : cookies) { + if (cookie.getName().equals("csrftoken")) { + ourCSRFToken = cookie.getValue(); } - }); + } + } + + private static boolean postCredentials(String user, String password) { + String url = stepicUrl + "accounts/login/"; + final HttpPost request = new HttpPost(url); + List nvps = new ArrayList (); + nvps.add(new BasicNameValuePair("csrfmiddlewaretoken", ourCSRFToken)); + nvps.add(new BasicNameValuePair("login", user)); + nvps.add(new BasicNameValuePair("next", "/")); + nvps.add(new BasicNameValuePair("password", password)); + nvps.add(new BasicNameValuePair("remember", "on")); + + try { + request.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); + } + catch (UnsupportedEncodingException e) { + LOG.error(e.getMessage()); + return false; + } + + setHeaders(request, "application/x-www-form-urlencoded"); + + try { + final CloseableHttpResponse response = ourClient.execute(request); + saveCSRFToken(); + final StatusLine line = response.getStatusLine(); + if (line.getStatusCode() != 302) { + LOG.error("Failed to login " + EntityUtils.toString(response.getEntity())); + return false; + } + } + catch (IOException e) { + LOG.error(e.getMessage()); + return false; + } + return true; + } + + private static T getFromStepic(String link, final Class container) throws IOException { + final HttpGet request = new HttpGet(stepicApiUrl + link); + if (ourClient == null) { + initializeClient(); + } + setHeaders(request, "application/json"); + + final CloseableHttpResponse response = ourClient.execute(request); + final String responseString = EntityUtils.toString(response.getEntity()); + Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + return gson.fromJson(responseString, container); } @NotNull public static List getCourses() { try { List result = new ArrayList(); - final List courseInfos = - HttpRequests.request(stepicApiUrl + "courses").connect(new HttpRequests.RequestProcessor>() { - - @Override - public List process(@NotNull HttpRequests.Request request) throws IOException { - final BufferedReader reader = request.getReader(); - Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); - return gson.fromJson(reader, CoursesContainer.class).courses; - } - }); + final List courseInfos = getFromStepic("courses", CoursesContainer.class).courses; for (CourseInfo info : courseInfos) { final String courseType = info.getType(); if (StringUtil.isEmptyOrSpaces(courseType)) continue; @@ -153,23 +223,29 @@ public class EduStepicConnector { } - public static boolean postLesson(Project project, @NotNull final Lesson lesson) { - final HttpPost request = new HttpPost(stepicApiUrl + "lessons"); - final ArrayList
headers = getHeaders(request); - HttpClientBuilder builder = HttpClients.custom().setSslcontext(CertificateManager.getInstance().getSslContext()); - final BasicCookieStore cookieStore = getCookies(); - final CloseableHttpClient client = builder.setDefaultHeaders(headers).setDefaultCookieStore(cookieStore).build(); + public static void showLoginDialog() { + final LoginDialog dialog = new LoginDialog(); + dialog.show(); + } + + public static void postLesson(Project project, @NotNull final Lesson lesson) { + final HttpPost request = new HttpPost(stepicApiUrl + "lessons"); + if (ourClient == null) { + showLoginDialog(); + } + + setHeaders(request, "application/json"); String requestBody = new Gson().toJson(new LessonWrapper(lesson)); request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); try { - final CloseableHttpResponse response = client.execute(request); + final CloseableHttpResponse response = ourClient.execute(request); final String responseString = EntityUtils.toString(response.getEntity()); final StatusLine line = response.getStatusLine(); if (line.getStatusCode() != 201) { - LOG.error("Failed to push " + EntityUtils.toString(response.getEntity())); - return false; + LOG.error("Failed to push " + responseString); + return; } final Lesson postedLesson = new Gson().fromJson(responseString, Course.class).getLessons().get(0); for (Task task : lesson.getTaskList()) { @@ -179,56 +255,31 @@ public class EduStepicConnector { catch (IOException e) { LOG.error(e.getMessage()); } - return false; } - public static boolean postTask(Project project, @NotNull final Task task, int id) { + public static void postTask(Project project, @NotNull final Task task, int id) { final HttpPost request = new HttpPost(stepicApiUrl + "step-sources"); - final ArrayList
headers = getHeaders(request); - HttpClientBuilder builder = HttpClients.custom().setSslcontext(CertificateManager.getInstance().getSslContext()); - final BasicCookieStore cookieStore = getCookies(); - final CloseableHttpClient client = builder.setDefaultHeaders(headers).setDefaultCookieStore(cookieStore).build(); + setHeaders(request, "application/json"); final Gson gson = new GsonBuilder().setPrettyPrinting().excludeFieldsWithoutExposeAnnotation().create(); String requestBody = gson.toJson(new StepSourceWrapper(project, task, id)); request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); try { - final CloseableHttpResponse response = client.execute(request); + final CloseableHttpResponse response = ourClient.execute(request); final StatusLine line = response.getStatusLine(); if (line.getStatusCode() != 201) { LOG.error("Failed to push " + EntityUtils.toString(response.getEntity())); } - return line.getStatusCode() == 201; } catch (IOException e) { LOG.error(e.getMessage()); } - - return false; } - private static BasicCookieStore getCookies() { - final BasicCookieStore cookieStore = new BasicCookieStore(); - final BasicClientCookie sessionid = new BasicClientCookie("sessionid", ourSessionId); - sessionid.setDomain(ourDomain); - sessionid.setPath("/"); - - cookieStore.addCookie(sessionid); - final BasicClientCookie csrfToken = new BasicClientCookie("csrftoken", ourCSRFToken); - csrfToken.setDomain(ourDomain); - csrfToken.setPath("/"); - - cookieStore.addCookie(csrfToken); - return cookieStore; - } - - private static ArrayList
getHeaders(HttpPost request) { - final ArrayList
headers = new ArrayList
(); - headers.add(new BasicHeader("referer", "https://stepic.org")); - headers.add(new BasicHeader("X-CSRFToken", ourCSRFToken)); - headers.add(new BasicHeader("content-type", "application/json")); - request.setHeaders(headers.toArray(new Header[headers.size()])); - return headers; + private static void setHeaders(@NotNull final HttpRequestBase request, String contentType) { + request.addHeader(new BasicHeader("referer", stepicUrl)); + request.addHeader(new BasicHeader("X-CSRFToken", ourCSRFToken)); + request.addHeader(new BasicHeader("content-type", contentType)); } private static class StepContainer { @@ -267,6 +318,7 @@ public class EduStepicConnector { @Override public void run() { final VirtualFile taskDir = task.getTaskDir(project); + assert taskDir != null; EduUtils.createStudentFileFromAnswer(project, taskDir, taskDir, entry); } }); diff --git a/python/educational/src/com/jetbrains/edu/stepic/LoginDialog.java b/python/educational/src/com/jetbrains/edu/stepic/LoginDialog.java new file mode 100644 index 000000000000..a670e83bc1b0 --- /dev/null +++ b/python/educational/src/com/jetbrains/edu/stepic/LoginDialog.java @@ -0,0 +1,62 @@ +package com.jetbrains.edu.stepic; + +import com.intellij.openapi.ui.DialogWrapper; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +public class LoginDialog extends DialogWrapper { + private final LoginPanel myLoginPanel; + + public LoginDialog() { + super(false); + myLoginPanel = new LoginPanel(this); + setTitle("Login to Stepic"); + setOKButtonText("Login"); + init(); + } + + @NotNull + protected Action[] createActions() { + return new Action[]{getOKAction(), getCancelAction()}; + } + + @Override + protected JComponent createCenterPanel() { + return myLoginPanel.getPanel(); + } + + @Override + protected String getHelpId() { + return "login_to_stepic"; + } + + @Override + public JComponent getPreferredFocusedComponent() { + return myLoginPanel.getPreferableFocusComponent(); + } + + @Override + protected void doOKAction() { + AuthDataHolder authData = myLoginPanel.getAuthData(); + final boolean success = EduStepicConnector.login(authData.email, authData.password); + if (!success) { + setErrorText("Log in failed"); + } + super.doOKAction(); + } + + public void clearErrors() { + setErrorText(null); + } + + public static class AuthDataHolder { + String email; + String password; + + public AuthDataHolder(String login, String password) { + email = login; + this.password = password; + } + } +} \ No newline at end of file diff --git a/python/educational/src/com/jetbrains/edu/stepic/LoginPanel.form b/python/educational/src/com/jetbrains/edu/stepic/LoginPanel.form new file mode 100644 index 000000000000..57570c149e93 --- /dev/null +++ b/python/educational/src/com/jetbrains/edu/stepic/LoginPanel.form @@ -0,0 +1,52 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/python/educational/src/com/jetbrains/edu/stepic/LoginPanel.java b/python/educational/src/com/jetbrains/edu/stepic/LoginPanel.java new file mode 100644 index 000000000000..3d883a774b24 --- /dev/null +++ b/python/educational/src/com/jetbrains/edu/stepic/LoginPanel.java @@ -0,0 +1,69 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.edu.stepic; + +import com.intellij.ui.DocumentAdapter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +public class LoginPanel { + private JPanel myPane; + private JTextField myLoginTextField; + private JPasswordField myPasswordField; + + public LoginPanel(final LoginDialog dialog) { + DocumentListener listener = new DocumentAdapter() { + @Override + protected void textChanged(DocumentEvent e) { + dialog.clearErrors(); + } + }; + myLoginTextField.getDocument().addDocumentListener(listener); + myPasswordField.getDocument().addDocumentListener(listener); + } + + public JComponent getPanel() { + return myPane; + } + + public void setLogin(@Nullable String login) { + myLoginTextField.setText(login); + } + + @NotNull + public String getLogin() { + return myLoginTextField.getText().trim(); + } + + @NotNull + private String getPassword() { + return String.valueOf(myPasswordField.getPassword()); + } + + public JComponent getPreferableFocusComponent() { + return myLoginTextField.isVisible() ? myLoginTextField : myPasswordField; + } + + @NotNull + public LoginDialog.AuthDataHolder getAuthData() { + return new LoginDialog.AuthDataHolder(getLogin(), getPassword()); + } +} +