added stepic.org authorization

This commit is contained in:
Ekaterina Tuzova
2015-03-25 16:57:45 +03:00
parent 8aa8c822b7
commit 2e9528203a
6 changed files with 410 additions and 83 deletions
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.jetbrains.edu.learning.ui.StudyNewProjectPanel">
<grid id="27dc6" binding="myContentPanel" layout-manager="GridLayoutManager" row-count="2" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<grid id="27dc6" binding="myContentPanel" layout-manager="GridLayoutManager" row-count="4" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
@@ -67,6 +67,72 @@
<verticalTextPosition value="1"/>
</properties>
</component>
<grid id="f583f" binding="myHideablePanel" layout-manager="BorderLayout" hgap="0" vgap="0">
<constraints>
<grid row="3" column="1" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<grid id="b4a52" binding="myLoginPanel" layout-manager="GridBagLayout">
<constraints border-constraint="Center"/>
<properties/>
<border type="none"/>
<children>
<component id="f0dd0" class="javax.swing.JPasswordField" binding="myPasswordField">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
<gridbag weightx="1.0" weighty="1.0"/>
</constraints>
<properties/>
</component>
<component id="8d976" class="javax.swing.JLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
<gridbag weightx="0.0" weighty="1.0"/>
</constraints>
<properties>
<text value="Password:"/>
</properties>
</component>
<component id="56bf1" class="javax.swing.JTextField" binding="myLoginField">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
<gridbag weightx="1.0" weighty="1.0"/>
</constraints>
<properties/>
</component>
<component id="fb0e2" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
<gridbag weightx="0.0" weighty="1.0"/>
</constraints>
<properties>
<text value="E-mail:"/>
</properties>
</component>
<component id="5f574" class="javax.swing.JButton" binding="myLogInButton" default-binding="true">
<constraints>
<grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
<gridbag weightx="0.0" weighty="1.0"/>
</constraints>
<properties>
<text value="Log in"/>
</properties>
</component>
</children>
</grid>
</children>
</grid>
<vspacer id="9a0a">
<constraints>
<grid row="2" column="1" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
</children>
</grid>
</form>
@@ -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<CourseInfo> myAvailableCourses = new ArrayList<CourseInfo>();
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 = "<html>Failed to download courses.<br>Check your Internet connection.</html>";
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<CourseInfo> 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<CourseInfo> 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
@@ -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> T getFromStepic(String link, final Class<T> container) throws IOException {
return HttpRequests.request(stepicApiUrl + link).connect(new HttpRequests.RequestProcessor<T>() {
@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<Cookie> 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 <NameValuePair> nvps = new ArrayList <NameValuePair>();
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> T getFromStepic(String link, final Class<T> 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<CourseInfo> getCourses() {
try {
List<CourseInfo> result = new ArrayList<CourseInfo>();
final List<CourseInfo> courseInfos =
HttpRequests.request(stepicApiUrl + "courses").connect(new HttpRequests.RequestProcessor<List<CourseInfo>>() {
@Override
public List<CourseInfo> 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<CourseInfo> 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<Header> 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<Header> 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<Header> getHeaders(HttpPost request) {
final ArrayList<Header> headers = new ArrayList<Header>();
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);
}
});
@@ -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;
}
}
}
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.jetbrains.edu.stepic.LoginPanel">
<grid id="27dc6" binding="myPane" layout-manager="GridLayoutManager" row-count="3" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="174"/>
</constraints>
<properties>
<focusable value="false"/>
</properties>
<border type="none"/>
<children>
<vspacer id="9b306">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<component id="9edbd" class="javax.swing.JPasswordField" binding="myPasswordField">
<constraints>
<grid row="1" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="300" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="fe9db" class="javax.swing.JLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Password:"/>
</properties>
</component>
<component id="e76ec" class="javax.swing.JTextField" binding="myLoginTextField">
<constraints>
<grid row="0" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="c0234" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="E-mail:"/>
</properties>
</component>
</children>
</grid>
</form>
@@ -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());
}
}