mirror of
https://github.com/sismics/docs.git
synced 2024-11-22 05:57:57 +01:00
commit
05bfaa0035
@ -3,7 +3,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.1.0'
|
||||
classpath 'com.android.tools.build:gradle:2.1.2'
|
||||
}
|
||||
}
|
||||
apply plugin: 'com.android.application'
|
||||
@ -14,7 +14,7 @@ repositories {
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion '23.0.3'
|
||||
buildToolsVersion '24'
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
@ -50,13 +50,13 @@ android {
|
||||
|
||||
dependencies {
|
||||
compile fileTree(dir: 'libs', include: '*.jar')
|
||||
compile 'com.android.support:appcompat-v7:23.3.0'
|
||||
compile 'com.android.support:recyclerview-v7:23.3.0'
|
||||
compile 'com.android.support:design:23.3.0'
|
||||
compile 'com.android.support:appcompat-v7:23.4.0'
|
||||
compile 'com.android.support:recyclerview-v7:23.4.0'
|
||||
compile 'com.android.support:design:23.4.0'
|
||||
compile 'it.sephiroth.android.library.imagezoom:imagezoom:1.0.5'
|
||||
compile 'org.greenrobot:eventbus:3.0.0'
|
||||
compile 'com.squareup.picasso:picasso:2.5.2'
|
||||
compile 'com.squareup.okhttp3:okhttp:3.1.1'
|
||||
compile "com.squareup.okhttp3:okhttp-urlconnection:3.1.1"
|
||||
compile 'com.squareup.okhttp3:okhttp:3.3.1'
|
||||
compile "com.squareup.okhttp3:okhttp-urlconnection:3.3.1"
|
||||
compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.0.2'
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ public class DocumentEditActivity extends AppCompatActivity {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
JSONArray tagArray = tags.optJSONArray("stats");
|
||||
JSONArray tagArray = tags.optJSONArray("tags");
|
||||
|
||||
List<JSONObject> tagList = new ArrayList<>();
|
||||
for (int i = 0; i < tagArray.length(); i++) {
|
||||
|
@ -16,6 +16,7 @@ import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@ -51,7 +52,7 @@ import com.sismics.docs.resource.FileResource;
|
||||
import com.sismics.docs.service.FileUploadService;
|
||||
import com.sismics.docs.util.NetworkUtil;
|
||||
import com.sismics.docs.util.PreferenceUtil;
|
||||
import com.sismics.docs.util.TagUtil;
|
||||
import com.sismics.docs.util.SpannableUtil;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
@ -176,9 +177,11 @@ public class DocumentViewActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
// Fill the layout
|
||||
// Create date
|
||||
TextView createdDateTextView = (TextView) findViewById(R.id.createdDateTextView);
|
||||
createdDateTextView.setText(date);
|
||||
|
||||
// Description
|
||||
TextView descriptionTextView = (TextView) findViewById(R.id.descriptionTextView);
|
||||
if (description.isEmpty() || document.isNull("description")) {
|
||||
descriptionTextView.setVisibility(View.GONE);
|
||||
@ -187,17 +190,20 @@ public class DocumentViewActivity extends AppCompatActivity {
|
||||
descriptionTextView.setText(description);
|
||||
}
|
||||
|
||||
// Tags
|
||||
TextView tagTextView = (TextView) findViewById(R.id.tagTextView);
|
||||
if (tags.length() == 0) {
|
||||
tagTextView.setVisibility(View.GONE);
|
||||
} else {
|
||||
tagTextView.setVisibility(View.VISIBLE);
|
||||
tagTextView.setText(TagUtil.buildSpannable(tags));
|
||||
tagTextView.setText(SpannableUtil.buildSpannableTags(tags));
|
||||
}
|
||||
|
||||
// Language
|
||||
ImageView languageImageView = (ImageView) findViewById(R.id.languageImageView);
|
||||
languageImageView.setImageResource(getResources().getIdentifier(language, "drawable", getPackageName()));
|
||||
|
||||
// Shared status
|
||||
ImageView sharedImageView = (ImageView) findViewById(R.id.sharedImageView);
|
||||
sharedImageView.setVisibility(shared ? View.VISIBLE : View.GONE);
|
||||
|
||||
@ -642,10 +648,10 @@ public class DocumentViewActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
// Action only available if the document is writable
|
||||
findViewById(R.id.actionEditDocument).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
|
||||
findViewById(R.id.actionUploadFile).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
|
||||
findViewById(R.id.actionSharing).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
|
||||
findViewById(R.id.actionDelete).setVisibility(writable ? View.VISIBLE : View.INVISIBLE);
|
||||
findViewById(R.id.actionEditDocument).setVisibility(writable ? View.VISIBLE : View.GONE);
|
||||
findViewById(R.id.actionUploadFile).setVisibility(writable ? View.VISIBLE : View.GONE);
|
||||
findViewById(R.id.actionSharing).setVisibility(writable ? View.VISIBLE : View.GONE);
|
||||
findViewById(R.id.actionDelete).setVisibility(writable ? View.VISIBLE : View.GONE);
|
||||
|
||||
// ACLs
|
||||
ListView aclListView = (ListView) findViewById(R.id.aclListView);
|
||||
@ -679,10 +685,54 @@ public class DocumentViewActivity extends AppCompatActivity {
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
// Contributors
|
||||
TextView contributorsTextView = (TextView) findViewById(R.id.contributorsTextView);
|
||||
contributorsTextView.setText(SpannableUtil.buildSpannableContributors(document.optJSONArray("contributors")));
|
||||
|
||||
// Relations
|
||||
JSONArray relations = document.optJSONArray("relations");
|
||||
if (relations.length() > 0) {
|
||||
TextView relationsTextView = (TextView) findViewById(R.id.relationsTextView);
|
||||
relationsTextView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
relationsTextView.setText(SpannableUtil.buildSpannableRelations(relations));
|
||||
} else {
|
||||
findViewById(R.id.relationsLayout).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Additional dublincore metadata
|
||||
displayDublincoreMetadata(R.id.subjectTextView, R.id.subjectLayout, "subject");
|
||||
displayDublincoreMetadata(R.id.identifierTextView, R.id.identifierLayout, "identifier");
|
||||
displayDublincoreMetadata(R.id.publisherTextView, R.id.publisherLayout, "publisher");
|
||||
displayDublincoreMetadata(R.id.formatTextView, R.id.formatLayout, "format");
|
||||
displayDublincoreMetadata(R.id.sourceTextView, R.id.sourceLayout, "source");
|
||||
displayDublincoreMetadata(R.id.typeTextView, R.id.typeLayout, "type");
|
||||
displayDublincoreMetadata(R.id.coverageTextView, R.id.coverageLayout, "coverage");
|
||||
displayDublincoreMetadata(R.id.rightsTextView, R.id.rightsLayout, "rights");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a dublincore metadata.
|
||||
*
|
||||
* @param textViewId TextView ID
|
||||
* @param blockViewId View ID
|
||||
* @param name Name
|
||||
*/
|
||||
private void displayDublincoreMetadata(int textViewId, int blockViewId, String name) {
|
||||
if (document == null) return;
|
||||
String value = document.optString(name);
|
||||
if (document.isNull(name) || value.isEmpty()) {
|
||||
findViewById(blockViewId).setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
findViewById(blockViewId).setVisibility(View.VISIBLE);
|
||||
TextView textView = (TextView) findViewById(textViewId);
|
||||
textView.setText(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
|
||||
switch (view.getId()) {
|
||||
|
@ -9,7 +9,7 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.sismics.docs.R;
|
||||
import com.sismics.docs.util.TagUtil;
|
||||
import com.sismics.docs.util.SpannableUtil;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
@ -69,7 +69,7 @@ public class DocListAdapter extends RecyclerView.Adapter<DocListAdapter.ViewHold
|
||||
holder.titleTextView.setText(document.optString("title"));
|
||||
|
||||
JSONArray tags = document.optJSONArray("tags");
|
||||
holder.subtitleTextView.setText(TagUtil.buildSpannable(tags));
|
||||
holder.subtitleTextView.setText(SpannableUtil.buildSpannableTags(tags));
|
||||
|
||||
String date = DateFormat.getDateFormat(holder.dateTextView.getContext()).format(new Date(document.optLong("create_date")));
|
||||
holder.dateTextView.setText(date);
|
||||
|
@ -73,7 +73,7 @@ public class SearchFragment extends DialogFragment {
|
||||
dialog.cancel();
|
||||
return dialog;
|
||||
}
|
||||
JSONArray tagArray = tags.optJSONArray("stats");
|
||||
JSONArray tagArray = tags.optJSONArray("tags");
|
||||
|
||||
List<JSONObject> tagList = new ArrayList<>();
|
||||
for (int i = 0; i < tagArray.length(); i++) {
|
||||
|
@ -0,0 +1,33 @@
|
||||
package com.sismics.docs.ui.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListView;
|
||||
|
||||
/**
|
||||
* Non-scrollable ListView.
|
||||
* All items are visible from the start.
|
||||
*
|
||||
* @author http://stackoverflow.com/questions/18813296/non-scrollable-listview-inside-scrollview/24629341#24629341
|
||||
*/
|
||||
public class NonScrollListView extends ListView {
|
||||
|
||||
public NonScrollListView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
public NonScrollListView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
public NonScrollListView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
@Override
|
||||
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int heightMeasureSpec_custom = MeasureSpec.makeMeasureSpec(
|
||||
Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec_custom);
|
||||
ViewGroup.LayoutParams params = getLayoutParams();
|
||||
params.height = getMeasuredHeight();
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package com.sismics.docs.util;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Utility class for spannable.
|
||||
*
|
||||
* @author bgamard.
|
||||
*/
|
||||
public class SpannableUtil {
|
||||
/**
|
||||
* Create a colored spannable from tags.
|
||||
*
|
||||
* @param tags Tags
|
||||
* @return Colored spannable
|
||||
*/
|
||||
public static Spannable buildSpannableTags(JSONArray tags) {
|
||||
return buildSpannable(tags, "name", "color");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a spannable for contributors.
|
||||
*
|
||||
* @param contributors Contributors
|
||||
* @return Spannable
|
||||
*/
|
||||
public static Spannable buildSpannableContributors(JSONArray contributors) {
|
||||
return buildSpannable(contributors, "username", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a spannable for relations.
|
||||
*
|
||||
* @param relations Relations
|
||||
* @return Spannable
|
||||
*/
|
||||
public static Spannable buildSpannableRelations(JSONArray relations) {
|
||||
return buildSpannable(relations, "title", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a spannable from a JSONArray.
|
||||
*
|
||||
* @param array JSONArray
|
||||
* @param valueName Name of the value part
|
||||
* @param colorName Name of the color part (optional)
|
||||
* @return Spannable
|
||||
*/
|
||||
private static Spannable buildSpannable(JSONArray array, String valueName, String colorName) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
final JSONObject tag = array.optJSONObject(i);
|
||||
int start = builder.length();
|
||||
builder.append(" ").append(tag.optString(valueName)).append(" ");
|
||||
builder.setSpan(new ForegroundColorSpan(Color.WHITE), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
builder.setSpan(new BackgroundColorSpan(Color.parseColor(tag.optString(colorName, "#5bc0de"))), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
/*
|
||||
TODO : Make tags, relations and contributors clickable
|
||||
builder.setSpan(new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint ds) {
|
||||
super.updateDrawState(ds);
|
||||
ds.setColor(Color.WHITE);
|
||||
ds.setUnderlineText(false);
|
||||
}
|
||||
}, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);*/
|
||||
builder.append(" ");
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package com.sismics.docs.util;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Utility class for tags.
|
||||
*
|
||||
* @author bgamard.
|
||||
*/
|
||||
public class TagUtil {
|
||||
/**
|
||||
* Create a colored spannable from tags.
|
||||
*
|
||||
* @param tags Tags
|
||||
* @return Colored spannable
|
||||
*/
|
||||
public static Spannable buildSpannable(JSONArray tags) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
|
||||
for (int i = 0; i < tags.length(); i++) {
|
||||
JSONObject tag = tags.optJSONObject(i);
|
||||
int start = builder.length();
|
||||
builder.append(" ").append(tag.optString("name")).append(" ");
|
||||
builder.setSpan(new ForegroundColorSpan(Color.WHITE), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
builder.setSpan(new BackgroundColorSpan(Color.parseColor(tag.optString("color"))), start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
builder.append(" ");
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
@ -142,241 +142,523 @@
|
||||
|
||||
<!-- Right drawer -->
|
||||
|
||||
<LinearLayout
|
||||
<ScrollView
|
||||
android:id="@+id/right_drawer"
|
||||
android:layout_width="300dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:orientation="vertical"
|
||||
android:clickable="true"
|
||||
android:background="#fff"
|
||||
android:elevation="5dp">
|
||||
|
||||
<!-- Actions -->
|
||||
android:elevation="5dp"
|
||||
android:layout_gravity="end">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Actions -->
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
style="?android:buttonBarStyle">
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionEditDocument"
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_create_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/edit_document"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="8dp"/>
|
||||
android:orientation="horizontal"
|
||||
style="?android:buttonBarStyle">
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionUploadFile"
|
||||
<Button
|
||||
android:id="@+id/actionDownload"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_file_download_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/download_document"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="8dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionExportPdf"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_description_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/export_pdf"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="8dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionAuditLog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_assignment_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/activity"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="8dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_file_upload_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/upload_file"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="8dp"/>
|
||||
android:orientation="horizontal"
|
||||
style="?android:buttonBarStyle">
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionDownload"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_file_download_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/download_document"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="8dp"/>
|
||||
<Button
|
||||
android:id="@+id/actionEditDocument"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_create_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/edit_document"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="0dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionUploadFile"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_file_upload_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/upload_file"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="0dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionSharing"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_share_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/share"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="0dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionDelete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_delete_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/delete_document"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="0dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:background="#eee"/>
|
||||
|
||||
<!-- Document metadata -->
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/detailLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
style="?android:buttonBarStyle">
|
||||
android:padding="12dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionExportPdf"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_description_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/export_pdf"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="0dp"/>
|
||||
<TextView
|
||||
android:id="@+id/createdDateLabel"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/created_date"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionSharing"
|
||||
<TextView
|
||||
android:id="@+id/createdDateTextView"
|
||||
android:layout_toRightOf="@id/createdDateLabel"
|
||||
android:layout_toEndOf="@id/createdDateLabel"
|
||||
android:layout_toLeftOf="@id/sharedImageView"
|
||||
android:layout_toStartOf="@id/sharedImageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_share_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/share"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="0dp"/>
|
||||
android:layout_height="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentTop="true"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionAuditLog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_assignment_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/activity"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="0dp"/>
|
||||
<TextView
|
||||
android:id="@+id/creatorLabel"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_below="@+id/createdDateLabel"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/creator"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/actionDelete"
|
||||
<TextView
|
||||
android:id="@+id/creatorTextView"
|
||||
android:layout_toRightOf="@id/creatorLabel"
|
||||
android:layout_toEndOf="@id/creatorLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_below="@+id/createdDateTextView"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tagTextView"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableTop="@drawable/ic_delete_grey600_24dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:text="@string/delete_document"
|
||||
android:textColor="#ff5a595b"
|
||||
android:textAllCaps="false"
|
||||
android:layout_margin="0dp"/>
|
||||
android:layout_below="@id/creatorLabel"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/descriptionTextView"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_below="@id/tagTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
<ImageView
|
||||
android:contentDescription="@string/shared"
|
||||
android:id="@+id/sharedImageView"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_folder_shared_grey600_24dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_toLeftOf="@+id/languageImageView"
|
||||
android:layout_toStartOf="@+id/languageImageView"/>
|
||||
|
||||
<ImageView
|
||||
android:contentDescription="@string/language"
|
||||
android:id="@+id/languageImageView"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentTop="true"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- Additional dublincore metadata -->
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingRight="12dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<!-- Subject -->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/subjectLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_weight="0.33"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/subject"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subjectTextView"
|
||||
android:layout_weight="0.67"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Identifier -->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/identifierLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_weight="0.33"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/identifier"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/identifierTextView"
|
||||
android:layout_weight="0.67"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Publisher -->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/publisherLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_weight="0.33"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/publisher"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/publisherTextView"
|
||||
android:layout_weight="0.67"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Format -->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/formatLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_weight="0.33"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/format"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/formatTextView"
|
||||
android:layout_weight="0.67"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Source -->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sourceLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_weight="0.33"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/source"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sourceTextView"
|
||||
android:layout_weight="0.67"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Type -->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/typeLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_weight="0.33"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/type"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/typeTextView"
|
||||
android:layout_weight="0.67"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Coverage -->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/coverageLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_weight="0.33"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/coverage"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/coverageTextView"
|
||||
android:layout_weight="0.67"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Rights -->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/rightsLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_weight="0.33"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/rights"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rightsTextView"
|
||||
android:layout_weight="0.67"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Contributors -->
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_weight="0.33"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/contributors"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contributorsTextView"
|
||||
android:layout_weight="0.67"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Relations -->
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/relationsLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="2dp">
|
||||
|
||||
<TextView
|
||||
android:layout_weight="0.33"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/relations"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/relationsTextView"
|
||||
android:layout_weight="0.67"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:background="#eee"/>
|
||||
|
||||
<!-- ACLs -->
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textColor="#de000000"
|
||||
android:text="@string/who_can_access"
|
||||
android:layout_margin="12dp"/>
|
||||
|
||||
<com.sismics.docs.ui.view.NonScrollListView
|
||||
android:id="@+id/aclListView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:choiceMode="singleChoice"
|
||||
android:divider="@android:color/transparent"
|
||||
android:dividerHeight="0dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:background="#eee"/>
|
||||
|
||||
<!-- Document metadata -->
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/detailLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/createdDateLabel"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/created_date"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/createdDateTextView"
|
||||
android:layout_toRightOf="@id/createdDateLabel"
|
||||
android:layout_toEndOf="@id/createdDateLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentTop="true"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/creatorLabel"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_below="@+id/createdDateLabel"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:text="@string/creator"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/creatorTextView"
|
||||
android:layout_toRightOf="@id/creatorLabel"
|
||||
android:layout_toEndOf="@id/creatorLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_below="@+id/createdDateTextView"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tagTextView"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/creatorLabel"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:maxLines="1"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/descriptionTextView"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_below="@id/tagTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/sharedImageView"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_folder_shared_grey600_24dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_toLeftOf="@+id/languageImageView"
|
||||
android:layout_toStartOf="@+id/languageImageView"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/languageImageView"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentTop="true"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginRight="12dp"
|
||||
android:layout_marginLeft="12dp"
|
||||
android:background="#eee"/>
|
||||
|
||||
<!-- ACLs -->
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textColor="#de000000"
|
||||
android:text="@string/who_can_access"
|
||||
android:layout_margin="12dp"/>
|
||||
|
||||
<ListView
|
||||
android:id="@+id/aclListView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:choiceMode="singleChoice"
|
||||
android:divider="@android:color/transparent"
|
||||
android:dividerHeight="0dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</android.support.v4.widget.DrawerLayout>
|
@ -132,5 +132,17 @@
|
||||
<string name="storage_quota">Storage quota</string>
|
||||
<string name="storage_display">%1$d/%2$d MB</string>
|
||||
<string name="validation_code">Validation code</string>
|
||||
<string name="shared">Shared</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="coverage">Coverage</string>
|
||||
<string name="type">Type</string>
|
||||
<string name="source">Source</string>
|
||||
<string name="format">Format</string>
|
||||
<string name="publisher">Publisher</string>
|
||||
<string name="identifier">Identifier</string>
|
||||
<string name="subject">Subject</string>
|
||||
<string name="rights">Rights</string>
|
||||
<string name="contributors">Contributors</string>
|
||||
<string name="relations">Relations</string>
|
||||
|
||||
</resources>
|
||||
|
@ -12,6 +12,8 @@
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
|
||||
org.gradle.jvmargs=-Xmx3072m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
|
@ -13,5 +13,10 @@ public enum ConfigType {
|
||||
/**
|
||||
* Theme configuration.
|
||||
*/
|
||||
THEME
|
||||
THEME,
|
||||
|
||||
/**
|
||||
* Guest login.
|
||||
*/
|
||||
GUEST_LOGIN
|
||||
}
|
||||
|
@ -29,7 +29,12 @@ public class Constants {
|
||||
* File Lucene directory storage.
|
||||
*/
|
||||
public static final String LUCENE_DIRECTORY_STORAGE_FILE = "FILE";
|
||||
|
||||
|
||||
/**
|
||||
* Guest user ID.
|
||||
*/
|
||||
public static final String GUEST_USER_ID = "guest";
|
||||
|
||||
/**
|
||||
* Default generic user role.
|
||||
*/
|
||||
|
@ -1 +1 @@
|
||||
db.version=9
|
||||
db.version=10
|
@ -0,0 +1,3 @@
|
||||
insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('GUEST_LOGIN', 'false');
|
||||
insert into T_USER(USE_ID_C, USE_IDROLE_C, USE_USERNAME_C, USE_PASSWORD_C, USE_EMAIL_C, USE_CREATEDATE_D, USE_PRIVATEKEY_C) values('guest', 'user', 'guest', '', 'guest@localhost', NOW(), 'GuestPk');
|
||||
update T_CONFIG set CFG_VALUE_C = '10' where CFG_ID_C = 'DB_VERSION';
|
@ -59,4 +59,9 @@ public class AnonymousPrincipal implements IPrincipal {
|
||||
public Set<String> getGroupIdSet() {
|
||||
return Sets.newHashSet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGuest() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,13 @@ public interface IPrincipal extends Principal {
|
||||
*/
|
||||
boolean isAnonymous();
|
||||
|
||||
/**
|
||||
* Checks if the principal is a guest.
|
||||
*
|
||||
* @return True if the principal is a guest
|
||||
*/
|
||||
boolean isGuest();
|
||||
|
||||
/**
|
||||
* Returns the ID of the connected user, or null if the user is anonymous
|
||||
*
|
||||
|
@ -2,6 +2,7 @@ package com.sismics.security;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import com.sismics.docs.core.constant.Constants;
|
||||
import org.joda.time.DateTimeZone;
|
||||
|
||||
/**
|
||||
@ -108,4 +109,9 @@ public class UserPrincipal implements IPrincipal {
|
||||
public void setGroupIdSet(Set<String> groupIdSet) {
|
||||
this.groupIdSet = groupIdSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGuest() {
|
||||
return Constants.GUEST_USER_ID.equals(id);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,47 @@
|
||||
package com.sismics.util.filter;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.sismics.docs.core.dao.jpa.UserDao;
|
||||
import com.sismics.docs.core.model.jpa.User;
|
||||
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* A header-based security filter that authenticates an user using the "X-Authenticated-User" request header as the user ID.
|
||||
* This filter is intended to be used in conjunction with an external authenticating proxy.
|
||||
*
|
||||
* @author pacien
|
||||
*/
|
||||
public class HeaderBasedSecurityFilter extends SecurityFilter {
|
||||
/**
|
||||
* Authentication header.
|
||||
*/
|
||||
public static final String AUTHENTICATED_USER_HEADER = "X-Authenticated-User";
|
||||
|
||||
/**
|
||||
* True if this authentication method is enabled.
|
||||
*/
|
||||
private boolean enabled;
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
enabled = Boolean.parseBoolean(filterConfig.getInitParameter("enabled"))
|
||||
|| Boolean.parseBoolean(System.getProperty("docs.header_authentication"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected User authenticate(HttpServletRequest request) {
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String username = request.getHeader(AUTHENTICATED_USER_HEADER);
|
||||
if (Strings.isNullOrEmpty(username)) {
|
||||
return null;
|
||||
}
|
||||
return new UserDao().getActiveByUsername(username);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package com.sismics.util.filter;
|
||||
|
||||
import com.sismics.docs.core.constant.Constants;
|
||||
import com.sismics.docs.core.dao.jpa.GroupDao;
|
||||
import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao;
|
||||
import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria;
|
||||
import com.sismics.docs.core.dao.jpa.dto.GroupDto;
|
||||
import com.sismics.docs.core.model.jpa.User;
|
||||
import com.sismics.security.AnonymousPrincipal;
|
||||
import com.sismics.security.UserPrincipal;
|
||||
import jersey.repackaged.com.google.common.collect.Sets;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* An abstract security filter for user authentication, that injects corresponding users into the request.
|
||||
* Successfully authenticated users are injected as UserPrincipal, or as AnonymousPrincipal otherwise.
|
||||
* If an user has already been authenticated for the request, no further authentication attempt is made.
|
||||
*
|
||||
* @author pacien
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public abstract class SecurityFilter implements Filter {
|
||||
/**
|
||||
* Name of the attribute containing the principal.
|
||||
*/
|
||||
public static final String PRINCIPAL_ATTRIBUTE = "principal";
|
||||
|
||||
/**
|
||||
* Logger.
|
||||
*/
|
||||
static final Logger LOG = LoggerFactory.getLogger(SecurityFilter.class);
|
||||
|
||||
/**
|
||||
* Returns true if the supplied request has an UserPrincipal.
|
||||
*
|
||||
* @param request HTTP request
|
||||
* @return True if the supplied request has an UserPrincipal
|
||||
*/
|
||||
private boolean hasIdentifiedUser(HttpServletRequest request) {
|
||||
return request.getAttribute(PRINCIPAL_ATTRIBUTE) instanceof UserPrincipal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the given user into the request, with the appropriate authentication state.
|
||||
*
|
||||
* @param request HTTP request
|
||||
* @param user nullable User to inject
|
||||
*/
|
||||
private void injectUser(HttpServletRequest request, User user) {
|
||||
// Check if the user is still valid
|
||||
if (user != null && user.getDeleteDate() == null) {
|
||||
injectAuthenticatedUser(request, user);
|
||||
} else {
|
||||
injectAnonymousUser(request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject an authenticated user into the request attributes.
|
||||
*
|
||||
* @param request HTTP request
|
||||
* @param user User to inject
|
||||
*/
|
||||
private void injectAuthenticatedUser(HttpServletRequest request, User user) {
|
||||
UserPrincipal userPrincipal = new UserPrincipal(user.getId(), user.getUsername());
|
||||
|
||||
// Add groups
|
||||
GroupDao groupDao = new GroupDao();
|
||||
Set<String> groupRoleIdSet = new HashSet<>();
|
||||
List<GroupDto> groupDtoList = groupDao.findByCriteria(new GroupCriteria()
|
||||
.setUserId(user.getId())
|
||||
.setRecursive(true), null);
|
||||
Set<String> groupIdSet = Sets.newHashSet();
|
||||
for (GroupDto groupDto : groupDtoList) {
|
||||
groupIdSet.add(groupDto.getId());
|
||||
if (groupDto.getRoleId() != null) {
|
||||
groupRoleIdSet.add(groupDto.getRoleId());
|
||||
}
|
||||
}
|
||||
userPrincipal.setGroupIdSet(groupIdSet);
|
||||
|
||||
// Add base functions
|
||||
groupRoleIdSet.add(user.getRoleId());
|
||||
RoleBaseFunctionDao userBaseFuction = new RoleBaseFunctionDao();
|
||||
Set<String> baseFunctionSet = userBaseFuction.findByRoleId(groupRoleIdSet);
|
||||
userPrincipal.setBaseFunctionSet(baseFunctionSet);
|
||||
|
||||
// Add email
|
||||
userPrincipal.setEmail(user.getEmail());
|
||||
|
||||
request.setAttribute(PRINCIPAL_ATTRIBUTE, userPrincipal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject an anonymous user into the request attributes.
|
||||
*
|
||||
* @param request HTTP request
|
||||
*/
|
||||
private void injectAnonymousUser(HttpServletRequest request) {
|
||||
AnonymousPrincipal anonymousPrincipal = new AnonymousPrincipal();
|
||||
anonymousPrincipal.setDateTimeZone(DateTimeZone.forID(Constants.DEFAULT_TIMEZONE_ID));
|
||||
|
||||
request.setAttribute(PRINCIPAL_ATTRIBUTE, anonymousPrincipal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
// NOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// NOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest req, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
|
||||
if (!hasIdentifiedUser(request)) {
|
||||
User user = authenticate(request);
|
||||
injectUser(request, user);
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates an user from the given request parameters.
|
||||
*
|
||||
* @param request HTTP request
|
||||
* @return nullable User
|
||||
*/
|
||||
protected abstract User authenticate(HttpServletRequest request);
|
||||
|
||||
}
|
@ -1,38 +1,14 @@
|
||||
package com.sismics.util.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.sismics.docs.core.constant.Constants;
|
||||
import com.sismics.docs.core.dao.jpa.AuthenticationTokenDao;
|
||||
import com.sismics.docs.core.dao.jpa.GroupDao;
|
||||
import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao;
|
||||
import com.sismics.docs.core.dao.jpa.UserDao;
|
||||
import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria;
|
||||
import com.sismics.docs.core.dao.jpa.dto.GroupDto;
|
||||
import com.sismics.docs.core.model.jpa.AuthenticationToken;
|
||||
import com.sismics.docs.core.model.jpa.User;
|
||||
import com.sismics.security.AnonymousPrincipal;
|
||||
import com.sismics.security.UserPrincipal;
|
||||
|
||||
import jersey.repackaged.com.google.common.collect.Sets;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* This filter is used to authenticate the user having an active session via an authentication token stored in database.
|
||||
@ -42,22 +18,12 @@ import jersey.repackaged.com.google.common.collect.Sets;
|
||||
*
|
||||
* @author jtremeaux
|
||||
*/
|
||||
public class TokenBasedSecurityFilter implements Filter {
|
||||
/**
|
||||
* Logger.
|
||||
*/
|
||||
private static final Logger log = LoggerFactory.getLogger(TokenBasedSecurityFilter.class);
|
||||
|
||||
public class TokenBasedSecurityFilter extends SecurityFilter {
|
||||
/**
|
||||
* Name of the cookie used to store the authentication token.
|
||||
*/
|
||||
public static final String COOKIE_NAME = "auth_token";
|
||||
|
||||
/**
|
||||
* Name of the attribute containing the principal.
|
||||
*/
|
||||
public static final String PRINCIPAL_ATTRIBUTE = "principal";
|
||||
|
||||
/**
|
||||
* Lifetime of the authentication token in seconds, since login.
|
||||
*/
|
||||
@ -66,68 +32,40 @@ public class TokenBasedSecurityFilter implements Filter {
|
||||
/**
|
||||
* Lifetime of the authentication token in seconds, since last connection.
|
||||
*/
|
||||
public static final int TOKEN_SESSION_LIFETIME = 3600 * 24;
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
// NOP
|
||||
}
|
||||
private static final int TOKEN_SESSION_LIFETIME = 3600 * 24;
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
// NOP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest req, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
|
||||
// Get the value of the client authentication token
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
String authToken = null;
|
||||
if (request.getCookies() != null) {
|
||||
for (Cookie cookie : request.getCookies()) {
|
||||
if (COOKIE_NAME.equals(cookie.getName())) {
|
||||
authToken = cookie.getValue();
|
||||
/**
|
||||
* Extracts and returns an authentication token from a cookie list.
|
||||
*
|
||||
* @param cookies Cookie list
|
||||
* @return nullable auth token
|
||||
*/
|
||||
private String extractAuthToken(Cookie[] cookies) {
|
||||
if (cookies != null) {
|
||||
for (Cookie cookie : cookies) {
|
||||
if (COOKIE_NAME.equals(cookie.getName()) && !cookie.getValue().isEmpty()) {
|
||||
return cookie.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the corresponding server token
|
||||
AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao();
|
||||
AuthenticationToken authenticationToken = null;
|
||||
if (authToken != null) {
|
||||
authenticationToken = authenticationTokenDao.get(authToken);
|
||||
}
|
||||
|
||||
if (authenticationToken == null) {
|
||||
injectAnonymousUser(request);
|
||||
} else {
|
||||
// Check if the token is still valid
|
||||
if (isTokenExpired(authenticationToken)) {
|
||||
try {
|
||||
injectAnonymousUser(request);
|
||||
|
||||
// Destroy the expired token
|
||||
authenticationTokenDao.delete(authToken);
|
||||
} catch (Exception e) {
|
||||
if (log.isErrorEnabled()) {
|
||||
log.error(MessageFormat.format("Error deleting authentication token {0} ", authToken), e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check if the user is still valid
|
||||
UserDao userDao = new UserDao();
|
||||
User user = userDao.getById(authenticationToken.getUserId());
|
||||
if (user != null && user.getDeleteDate() == null) {
|
||||
injectAuthenticatedUser(request, user);
|
||||
} else {
|
||||
injectAnonymousUser(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes an expired authentication token.
|
||||
*
|
||||
* @param authTokenID auth token ID
|
||||
*/
|
||||
private void handleExpiredToken(AuthenticationTokenDao dao, String authTokenID) {
|
||||
try {
|
||||
dao.delete(authTokenID);
|
||||
} catch (Exception e) {
|
||||
if (LOG.isErrorEnabled())
|
||||
LOG.error(MessageFormat.format("Error deleting authentication token {0} ", authTokenID), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the token is expired.
|
||||
*
|
||||
@ -146,51 +84,27 @@ public class TokenBasedSecurityFilter implements Filter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject an authenticated user into the request attributes.
|
||||
*
|
||||
* @param request HTTP request
|
||||
* @param user User to inject
|
||||
*/
|
||||
private void injectAuthenticatedUser(HttpServletRequest request, User user) {
|
||||
UserPrincipal userPrincipal = new UserPrincipal(user.getId(), user.getUsername());
|
||||
|
||||
// Add groups
|
||||
GroupDao groupDao = new GroupDao();
|
||||
Set<String> groupRoleIdSet = new HashSet<>();
|
||||
List<GroupDto> groupDtoList = groupDao.findByCriteria(new GroupCriteria()
|
||||
.setUserId(user.getId())
|
||||
.setRecursive(true), null);
|
||||
Set<String> groupIdSet = Sets.newHashSet();
|
||||
for (GroupDto groupDto : groupDtoList) {
|
||||
groupIdSet.add(groupDto.getId());
|
||||
if (groupDto.getRoleId() != null) {
|
||||
groupRoleIdSet.add(groupDto.getRoleId());
|
||||
}
|
||||
@Override
|
||||
protected User authenticate(HttpServletRequest request) {
|
||||
// Get the value of the client authentication token
|
||||
String authTokenId = extractAuthToken(request.getCookies());
|
||||
if (authTokenId == null) {
|
||||
return null;
|
||||
}
|
||||
userPrincipal.setGroupIdSet(groupIdSet);
|
||||
|
||||
// Add base functions
|
||||
groupRoleIdSet.add(user.getRoleId());
|
||||
RoleBaseFunctionDao userBaseFuction = new RoleBaseFunctionDao();
|
||||
Set<String> baseFunctionSet = userBaseFuction.findByRoleId(groupRoleIdSet);
|
||||
userPrincipal.setBaseFunctionSet(baseFunctionSet);
|
||||
|
||||
// Add email
|
||||
userPrincipal.setEmail(user.getEmail());
|
||||
|
||||
request.setAttribute(PRINCIPAL_ATTRIBUTE, userPrincipal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject an anonymous user into the request attributes.
|
||||
*
|
||||
* @param request HTTP request
|
||||
*/
|
||||
private void injectAnonymousUser(HttpServletRequest request) {
|
||||
AnonymousPrincipal anonymousPrincipal = new AnonymousPrincipal();
|
||||
anonymousPrincipal.setDateTimeZone(DateTimeZone.forID(Constants.DEFAULT_TIMEZONE_ID));
|
||||
// Get the corresponding server token
|
||||
AuthenticationTokenDao authTokenDao = new AuthenticationTokenDao();
|
||||
AuthenticationToken authToken = authTokenDao.get(authTokenId);
|
||||
if (authToken == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
request.setAttribute(PRINCIPAL_ATTRIBUTE, anonymousPrincipal);
|
||||
if (isTokenExpired(authToken)) {
|
||||
handleExpiredToken(authTokenDao, authTokenId);
|
||||
return null;
|
||||
}
|
||||
|
||||
authTokenDao.updateLastConnectionDate(authToken.getId());
|
||||
return new UserDao().getById(authToken.getUserId());
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import java.net.URI;
|
||||
import javax.ws.rs.core.Application;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import com.sismics.util.filter.HeaderBasedSecurityFilter;
|
||||
import org.glassfish.grizzly.http.server.HttpServer;
|
||||
import org.glassfish.grizzly.servlet.ServletRegistration;
|
||||
import org.glassfish.grizzly.servlet.WebappContext;
|
||||
@ -62,7 +63,8 @@ public abstract class BaseJerseyTest extends JerseyTest {
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
System.setProperty("docs.header_authentication", "true");
|
||||
|
||||
clientUtil = new ClientUtil(target());
|
||||
|
||||
httpServer = HttpServer.createSimpleServer(getClass().getResource("/").getFile(), "localhost", getPort());
|
||||
@ -71,6 +73,8 @@ public abstract class BaseJerseyTest extends JerseyTest {
|
||||
.addMappingForUrlPatterns(null, "/*");
|
||||
context.addFilter("tokenBasedSecurityFilter", TokenBasedSecurityFilter.class)
|
||||
.addMappingForUrlPatterns(null, "/*");
|
||||
context.addFilter("headerBasedSecurityFilter", HeaderBasedSecurityFilter.class)
|
||||
.addMappingForUrlPatterns(null, "/*");
|
||||
ServletRegistration reg = context.addServlet("jerseyServlet", ServletContainer.class);
|
||||
reg.setInitParameter("jersey.config.server.provider.packages", "com.sismics.docs.rest.resource");
|
||||
reg.setInitParameter("jersey.config.server.provider.classnames", "org.glassfish.jersey.media.multipart.MultiPartFeature");
|
||||
|
@ -1,3 +1,3 @@
|
||||
api.current_version=${project.version}
|
||||
api.min_version=1.0
|
||||
db.version=9
|
||||
db.version=10
|
@ -14,15 +14,12 @@ import javax.json.JsonArrayBuilder;
|
||||
import javax.json.JsonObjectBuilder;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.Query;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import com.sismics.docs.core.constant.ConfigType;
|
||||
import com.sismics.docs.core.constant.PermType;
|
||||
import com.sismics.docs.core.dao.jpa.AclDao;
|
||||
import com.sismics.docs.core.dao.jpa.TagDao;
|
||||
import com.sismics.docs.core.dao.jpa.*;
|
||||
import com.sismics.docs.core.dao.jpa.criteria.TagCriteria;
|
||||
import com.sismics.docs.core.dao.jpa.dto.AclDto;
|
||||
import com.sismics.docs.core.dao.jpa.dto.TagDto;
|
||||
@ -33,8 +30,6 @@ import org.apache.log4j.Level;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.sismics.docs.core.dao.jpa.FileDao;
|
||||
import com.sismics.docs.core.dao.jpa.UserDao;
|
||||
import com.sismics.docs.core.model.context.AppContext;
|
||||
import com.sismics.docs.core.model.jpa.File;
|
||||
import com.sismics.docs.core.model.jpa.User;
|
||||
@ -70,32 +65,58 @@ public class AppResource extends BaseResource {
|
||||
* @apiGroup App
|
||||
* @apiSuccess {String} current_version API current version
|
||||
* @apiSuccess {String} min_version API minimum version
|
||||
* @apiSuccess {Boolean} guest_login True if guest login is enabled
|
||||
* @apiSuccess {String} total_memory Allocated JVM memory (in bytes)
|
||||
* @apiSuccess {String} free_memory Free JVM memory (in bytes)
|
||||
* @apiError (client) ForbiddenError Access denied
|
||||
* @apiPermission user
|
||||
* @apiPermission none
|
||||
* @apiVersion 1.5.0
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
@GET
|
||||
public Response info() {
|
||||
if (!authenticate()) {
|
||||
throw new ForbiddenClientException();
|
||||
}
|
||||
|
||||
ResourceBundle configBundle = ConfigUtil.getConfigBundle();
|
||||
String currentVersion = configBundle.getString("api.current_version");
|
||||
String minVersion = configBundle.getString("api.min_version");
|
||||
Boolean guestLogin = ConfigUtil.getConfigBooleanValue(ConfigType.GUEST_LOGIN);
|
||||
|
||||
JsonObjectBuilder response = Json.createObjectBuilder()
|
||||
.add("current_version", currentVersion.replace("-SNAPSHOT", ""))
|
||||
.add("min_version", minVersion)
|
||||
.add("guest_login", guestLogin)
|
||||
.add("total_memory", Runtime.getRuntime().totalMemory())
|
||||
.add("free_memory", Runtime.getRuntime().freeMemory());
|
||||
|
||||
return Response.ok().entity(response.build()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable guest login.
|
||||
*
|
||||
* @api {post} /app/guest_login Enable/disable guest login
|
||||
* @apiName PostAppGuestLogin
|
||||
* @apiGroup App
|
||||
* @apiParam {Boolean} enabled If true, enable guest login
|
||||
* @apiError (client) ForbiddenError Access denied
|
||||
* @apiPermission admin
|
||||
* @apiVersion 1.5.0
|
||||
*
|
||||
* @param enabled If true, enable guest login
|
||||
* @return Response
|
||||
*/
|
||||
@POST
|
||||
@Path("guest_login")
|
||||
public Response guestLogin(@FormParam("enabled") Boolean enabled) {
|
||||
if (!authenticate()) {
|
||||
throw new ForbiddenClientException();
|
||||
}
|
||||
checkBaseFunction(BaseFunction.ADMIN);
|
||||
|
||||
ConfigDao configDao = new ConfigDao();
|
||||
configDao.update(ConfigType.GUEST_LOGIN, enabled.toString());
|
||||
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the application logs.
|
||||
@ -325,7 +346,7 @@ public class AppResource extends BaseResource {
|
||||
/**
|
||||
* Recompute the quota for each user.
|
||||
*
|
||||
* @api {post} /app/batch/recompute_quote Recompute user quotas
|
||||
* @api {post} /app/batch/recompute_quota Recompute user quotas
|
||||
* @apiName PostAppBatchRecomputeQuota
|
||||
* @apiGroup App
|
||||
* @apiSuccess {String} status Status OK
|
||||
@ -385,7 +406,7 @@ public class AppResource extends BaseResource {
|
||||
/**
|
||||
* Add base ACLs to tags.
|
||||
*
|
||||
* @api {post} /app/batch/recompute_quote Add base ACL to tags
|
||||
* @api {post} /app/batch/tag_acls Add base ACL to tags
|
||||
* @apiDescription This resource must be used after migrating to 1.5.
|
||||
* It will not do anything if base ACL are already present on tags.
|
||||
* @apiName PostAppBatchTagAcls
|
||||
|
@ -1,19 +1,18 @@
|
||||
package com.sismics.docs.rest.resource;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.sismics.docs.rest.constant.BaseFunction;
|
||||
import com.sismics.rest.exception.ForbiddenClientException;
|
||||
import com.sismics.security.IPrincipal;
|
||||
import com.sismics.security.UserPrincipal;
|
||||
import com.sismics.util.filter.TokenBasedSecurityFilter;
|
||||
import com.sismics.util.filter.SecurityFilter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Base class of REST resources.
|
||||
@ -67,7 +66,7 @@ public abstract class BaseResource {
|
||||
* @return True if the user is authenticated and not anonymous
|
||||
*/
|
||||
protected boolean authenticate() {
|
||||
Principal principal = (Principal) request.getAttribute(TokenBasedSecurityFilter.PRINCIPAL_ATTRIBUTE);
|
||||
Principal principal = (Principal) request.getAttribute(SecurityFilter.PRINCIPAL_ATTRIBUTE);
|
||||
if (principal != null && principal instanceof IPrincipal) {
|
||||
this.principal = (IPrincipal) principal;
|
||||
return !this.principal.isAnonymous();
|
||||
|
@ -22,6 +22,8 @@ import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.NewCookie;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import com.sismics.docs.core.constant.ConfigType;
|
||||
import com.sismics.docs.core.util.ConfigUtil;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
@ -150,7 +152,7 @@ public class UserResource extends BaseResource {
|
||||
* @apiParam {String{8..50}} password Password
|
||||
* @apiParam {String{1..100}} email E-mail
|
||||
* @apiSuccess {String} status Status OK
|
||||
* @apiError (client) ForbiddenError Access denied
|
||||
* @apiError (client) ForbiddenError Access denied or connected as guest
|
||||
* @apiError (client) ValidationError Validation error
|
||||
* @apiPermission user
|
||||
* @apiVersion 1.5.0
|
||||
@ -163,7 +165,7 @@ public class UserResource extends BaseResource {
|
||||
public Response update(
|
||||
@FormParam("password") String password,
|
||||
@FormParam("email") String email) {
|
||||
if (!authenticate()) {
|
||||
if (!authenticate() || principal.isGuest()) {
|
||||
throw new ForbiddenClientException();
|
||||
}
|
||||
|
||||
@ -301,7 +303,7 @@ public class UserResource extends BaseResource {
|
||||
* @apiName PostUserLogin
|
||||
* @apiGroup User
|
||||
* @apiParam {String} username Username
|
||||
* @apiParam {String} password Password
|
||||
* @apiParam {String} password Password (optional for guest login)
|
||||
* @apiParam {String} code TOTP validation code
|
||||
* @apiParam {Boolean} remember If true, create a long lasted token
|
||||
* @apiSuccess {String} auth_token A cookie named auth_token containing the token ID
|
||||
@ -328,7 +330,16 @@ public class UserResource extends BaseResource {
|
||||
|
||||
// Get the user
|
||||
UserDao userDao = new UserDao();
|
||||
User user = userDao.authenticate(username, password);
|
||||
User user = null;
|
||||
if (Constants.GUEST_USER_ID.equals(username)) {
|
||||
if (ConfigUtil.getConfigBooleanValue(ConfigType.GUEST_LOGIN)) {
|
||||
// Login as guest
|
||||
user = userDao.getActiveByUsername(Constants.GUEST_USER_ID);
|
||||
}
|
||||
} else {
|
||||
// Login as a normal user
|
||||
user = userDao.authenticate(username, password);
|
||||
}
|
||||
if (user == null) {
|
||||
throw new ForbiddenClientException();
|
||||
}
|
||||
@ -429,7 +440,7 @@ public class UserResource extends BaseResource {
|
||||
* @apiName DeleteUser
|
||||
* @apiGroup User
|
||||
* @apiSuccess {String} status Status OK
|
||||
* @apiError (client) ForbiddenError Access denied or the admin user cannot be deleted
|
||||
* @apiError (client) ForbiddenError Access denied or the user cannot be deleted
|
||||
* @apiPermission user
|
||||
* @apiVersion 1.5.0
|
||||
*
|
||||
@ -442,8 +453,8 @@ public class UserResource extends BaseResource {
|
||||
}
|
||||
|
||||
// Ensure that the admin user is not deleted
|
||||
if (hasBaseFunction(BaseFunction.ADMIN)) {
|
||||
throw new ClientException("ForbiddenError", "The admin user cannot be deleted");
|
||||
if (hasBaseFunction(BaseFunction.ADMIN) || principal.isGuest()) {
|
||||
throw new ClientException("ForbiddenError", "This user cannot be deleted");
|
||||
}
|
||||
|
||||
// Find linked data
|
||||
@ -486,7 +497,7 @@ public class UserResource extends BaseResource {
|
||||
* @apiName DeleteUserUsername
|
||||
* @apiGroup User
|
||||
* @apiSuccess {String} status Status OK
|
||||
* @apiError (client) ForbiddenError Access denied or the admin user cannot be deleted
|
||||
* @apiError (client) ForbiddenError Access denied or the user cannot be deleted
|
||||
* @apiError (client) UserNotFound The user does not exist
|
||||
* @apiPermission admin
|
||||
* @apiVersion 1.5.0
|
||||
@ -501,7 +512,12 @@ public class UserResource extends BaseResource {
|
||||
throw new ForbiddenClientException();
|
||||
}
|
||||
checkBaseFunction(BaseFunction.ADMIN);
|
||||
|
||||
|
||||
// Cannot delete the guest user
|
||||
if (Constants.GUEST_USER_ID.equals(username)) {
|
||||
throw new ClientException("ForbiddenError", "The guest user cannot be deleted");
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
UserDao userDao = new UserDao();
|
||||
User user = userDao.getActiveByUsername(username);
|
||||
@ -768,18 +784,21 @@ public class UserResource extends BaseResource {
|
||||
String authToken = getAuthToken();
|
||||
|
||||
JsonArrayBuilder sessions = Json.createArrayBuilder();
|
||||
AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao();
|
||||
|
||||
for (AuthenticationToken authenticationToken : authenticationTokenDao.getByUserId(principal.getId())) {
|
||||
JsonObjectBuilder session = Json.createObjectBuilder()
|
||||
.add("create_date", authenticationToken.getCreationDate().getTime())
|
||||
.add("ip", JsonUtil.nullable(authenticationToken.getIp()))
|
||||
.add("user_agent", JsonUtil.nullable(authenticationToken.getUserAgent()));
|
||||
if (authenticationToken.getLastConnectionDate() != null) {
|
||||
session.add("last_connection_date", authenticationToken.getLastConnectionDate().getTime());
|
||||
// The guest user cannot see other sessions
|
||||
if (!principal.isGuest()) {
|
||||
AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao();
|
||||
for (AuthenticationToken authenticationToken : authenticationTokenDao.getByUserId(principal.getId())) {
|
||||
JsonObjectBuilder session = Json.createObjectBuilder()
|
||||
.add("create_date", authenticationToken.getCreationDate().getTime())
|
||||
.add("ip", JsonUtil.nullable(authenticationToken.getIp()))
|
||||
.add("user_agent", JsonUtil.nullable(authenticationToken.getUserAgent()));
|
||||
if (authenticationToken.getLastConnectionDate() != null) {
|
||||
session.add("last_connection_date", authenticationToken.getLastConnectionDate().getTime());
|
||||
}
|
||||
session.add("current", authenticationToken.getId().equals(authToken));
|
||||
sessions.add(session);
|
||||
}
|
||||
session.add("current", authenticationToken.getId().equals(authToken));
|
||||
sessions.add(session);
|
||||
}
|
||||
|
||||
JsonObjectBuilder response = Json.createObjectBuilder()
|
||||
@ -795,7 +814,7 @@ public class UserResource extends BaseResource {
|
||||
* @apiName DeleteUserSession
|
||||
* @apiGroup User
|
||||
* @apiSuccess {String} status Status OK
|
||||
* @apiError (client) ForbiddenError Access denied
|
||||
* @apiError (client) ForbiddenError Access denied or connected as guest
|
||||
* @apiPermission user
|
||||
* @apiVersion 1.5.0
|
||||
*
|
||||
@ -804,10 +823,10 @@ public class UserResource extends BaseResource {
|
||||
@DELETE
|
||||
@Path("session")
|
||||
public Response deleteSession() {
|
||||
if (!authenticate()) {
|
||||
if (!authenticate() || principal.isGuest()) {
|
||||
throw new ForbiddenClientException();
|
||||
}
|
||||
|
||||
|
||||
// Get the value of the session token
|
||||
String authToken = getAuthToken();
|
||||
|
||||
@ -830,7 +849,7 @@ public class UserResource extends BaseResource {
|
||||
* @apiName PostUserEnableTotp
|
||||
* @apiGroup User
|
||||
* @apiSuccess {String} secret Secret TOTP seed to initiate the algorithm
|
||||
* @apiError (client) ForbiddenError Access denied
|
||||
* @apiError (client) ForbiddenError Access denied or connected as guest
|
||||
* @apiPermission user
|
||||
* @apiVersion 1.5.0
|
||||
*
|
||||
@ -839,7 +858,7 @@ public class UserResource extends BaseResource {
|
||||
@POST
|
||||
@Path("enable_totp")
|
||||
public Response enableTotp() {
|
||||
if (!authenticate()) {
|
||||
if (!authenticate() || principal.isGuest()) {
|
||||
throw new ForbiddenClientException();
|
||||
}
|
||||
|
||||
@ -866,7 +885,7 @@ public class UserResource extends BaseResource {
|
||||
* @apiGroup User
|
||||
* @apiParam {String{1..100}} password Password
|
||||
* @apiSuccess {String} status Status OK
|
||||
* @apiError (client) ForbiddenError Access denied
|
||||
* @apiError (client) ForbiddenError Access denied or connected as guest
|
||||
* @apiError (client) ValidationError Validation error
|
||||
* @apiPermission user
|
||||
* @apiVersion 1.5.0
|
||||
@ -877,7 +896,7 @@ public class UserResource extends BaseResource {
|
||||
@POST
|
||||
@Path("disable_totp")
|
||||
public Response disableTotp(@FormParam("password") String password) {
|
||||
if (!authenticate()) {
|
||||
if (!authenticate() || principal.isGuest()) {
|
||||
throw new ForbiddenClientException();
|
||||
}
|
||||
|
||||
|
@ -7,12 +7,17 @@ module.exports = function(grunt) {
|
||||
init: ['dist'],
|
||||
after: ['dist/style.css', 'dist/docs.js', 'dist/share.js', 'dist/less.css', 'dist/app']
|
||||
},
|
||||
ngmin: {
|
||||
ngAnnotate: {
|
||||
options: {
|
||||
singleQuotes: true
|
||||
},
|
||||
dist: {
|
||||
expand: true,
|
||||
cwd: 'src',
|
||||
src: ['app/**/*.js'],
|
||||
dest: 'dist'
|
||||
files: [{
|
||||
expand: true,
|
||||
cwd: 'src',
|
||||
src: ['app/**/*.js'],
|
||||
dest: 'dist'
|
||||
}]
|
||||
}
|
||||
},
|
||||
concat: {
|
||||
@ -110,12 +115,12 @@ module.exports = function(grunt) {
|
||||
grunt.loadNpmTasks('grunt-htmlrefs');
|
||||
grunt.loadNpmTasks('grunt-css');
|
||||
grunt.loadNpmTasks('grunt-contrib-less');
|
||||
grunt.loadNpmTasks('grunt-ngmin');
|
||||
grunt.loadNpmTasks('grunt-ng-annotate');
|
||||
grunt.loadNpmTasks('grunt-text-replace');
|
||||
grunt.loadNpmTasks('grunt-apidoc');
|
||||
|
||||
// Default tasks.
|
||||
grunt.registerTask('default', ['clean:init', 'ngmin', 'concat:docs', 'concat:share', 'less', 'concat:css', 'cssmin',
|
||||
grunt.registerTask('default', ['clean:init', 'ngAnnotate', 'concat:docs', 'concat:share', 'less', 'concat:css', 'cssmin',
|
||||
'uglify:docs', 'uglify:share', 'copy', 'clean:after', 'cleanempty', 'htmlrefs:index', 'htmlrefs:share', 'replace', 'apidoc']);
|
||||
|
||||
};
|
@ -26,18 +26,33 @@
|
||||
<url-pattern>*.jsp</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
<!-- This filter is used to secure URLs -->
|
||||
<!-- These filters are used to secure URLs -->
|
||||
<filter>
|
||||
<filter-name>tokenBasedSecurityFilter</filter-name>
|
||||
<filter-class>com.sismics.util.filter.TokenBasedSecurityFilter</filter-class>
|
||||
<async-supported>true</async-supported>
|
||||
</filter>
|
||||
|
||||
<filter>
|
||||
<filter-name>headerBasedSecurityFilter</filter-name>
|
||||
<filter-class>com.sismics.util.filter.HeaderBasedSecurityFilter</filter-class>
|
||||
<async-supported>true</async-supported>
|
||||
<init-param>
|
||||
<param-name>enabled</param-name>
|
||||
<param-value>false</param-value>
|
||||
</init-param>
|
||||
</filter>
|
||||
|
||||
<filter-mapping>
|
||||
<filter-name>tokenBasedSecurityFilter</filter-name>
|
||||
<url-pattern>/api/*</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
<filter-mapping>
|
||||
<filter-name>headerBasedSecurityFilter</filter-name>
|
||||
<url-pattern>/api/*</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
<!-- Jersey -->
|
||||
<servlet>
|
||||
<servlet-name>JerseyServlet</servlet-name>
|
||||
|
46
docs-web/src/main/webapp/header.md
Normal file
46
docs-web/src/main/webapp/header.md
Normal file
@ -0,0 +1,46 @@
|
||||
The web client and Android application for **Sismics Docs** are only examples
|
||||
of what is possible with the provided REST API. Everything you see in those apps are
|
||||
accessible using the API.
|
||||
|
||||
This documentation is divided in two parts. The first will get you started on essentials
|
||||
steps like authentication and the second part is a full reference of every endpoints.
|
||||
|
||||
## API URL
|
||||
The base URL depends on your server. If your instance of Docs is accessible through
|
||||
`https://docs.mycompany.com`, then the base API URL is `https://docs.mycompany.com/api`.
|
||||
|
||||
## Verbs and status codes
|
||||
The API uses restful verbs.
|
||||
|
||||
| Verb | Description |
|
||||
|---|---|
|
||||
| `GET` | Select one or more items |
|
||||
| `PUT` | Create a new item |
|
||||
| `POST` | Update an item |
|
||||
| `DELETE` | Delete an item |
|
||||
|
||||
Successful calls return a HTTP code 200, anything else if an error.
|
||||
|
||||
## Dates
|
||||
All dates are returned in UNIX timestamp format in milliseconds.
|
||||
|
||||
## Authentication
|
||||
#### **Step 1: [POST /user/login](#api-User-PostUserLogin)**
|
||||
|
||||
A call to this endpoint will return a cookie header like this:
|
||||
```
|
||||
HTTP Response:
|
||||
Set-Cookie: auth_token=64085630-2ae6-415c-9a92-4b22c107eaa4
|
||||
```
|
||||
|
||||
#### **Step 2: Authenticated API calls**
|
||||
|
||||
All following API calls must have a cookie header supplying the given token, like this:
|
||||
```
|
||||
HTTP Request:
|
||||
Cookie: auth_token=64085630-2ae6-415c-9a92-4b22c107eaa4
|
||||
```
|
||||
|
||||
#### **Step 3: [POST /user/logout](#api-User-PostUserLogout)**
|
||||
|
||||
A call to this API with a given `auth_token` cookie will make it unusable for other calls.
|
@ -28,7 +28,11 @@
|
||||
"App",
|
||||
"Theme",
|
||||
"Vocabulary"
|
||||
]
|
||||
],
|
||||
"header": {
|
||||
"title": "Getting started",
|
||||
"filename": "header.md"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"grunt": "^1.0.1",
|
||||
@ -41,7 +45,7 @@
|
||||
"grunt-contrib-uglify": "^1.0.1",
|
||||
"grunt-css": "^0.5.4",
|
||||
"grunt-htmlrefs": "^0.5.0",
|
||||
"grunt-ngmin": "0.0.3",
|
||||
"grunt-ng-annotate": "^2.0.2",
|
||||
"grunt-text-replace": "^0.4.0",
|
||||
"protractor": "^3.3.0",
|
||||
"selenium": "^2.20.0"
|
||||
|
@ -106,12 +106,12 @@ angular.module('docs',
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('settings.theme', {
|
||||
url: '/theme',
|
||||
.state('settings.config', {
|
||||
url: '/config',
|
||||
views: {
|
||||
'settings': {
|
||||
templateUrl: 'partial/docs/settings.theme.html',
|
||||
controller: 'SettingsTheme'
|
||||
templateUrl: 'partial/docs/settings.config.html',
|
||||
controller: 'SettingsConfig'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -3,12 +3,24 @@
|
||||
/**
|
||||
* Login controller.
|
||||
*/
|
||||
angular.module('docs').controller('Login', function($scope, $rootScope, $state, $dialog, User) {
|
||||
angular.module('docs').controller('Login', function(Restangular, $scope, $rootScope, $state, $dialog, User) {
|
||||
$scope.codeRequired = false;
|
||||
|
||||
/**
|
||||
* Login.
|
||||
*/
|
||||
// Get the app configuration
|
||||
Restangular.one('app').get().then(function(data) {
|
||||
$scope.app = data;
|
||||
});
|
||||
|
||||
// Login as guest
|
||||
$scope.loginAsGuest = function() {
|
||||
$scope.user = {
|
||||
username: 'guest',
|
||||
password: ''
|
||||
};
|
||||
$scope.login();
|
||||
};
|
||||
|
||||
// Login
|
||||
$scope.login = function() {
|
||||
User.login($scope.user).then(function() {
|
||||
User.userInfo(true).then(function(data) {
|
||||
|
@ -1,9 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Settings theme page controller.
|
||||
* Settings config page controller.
|
||||
*/
|
||||
angular.module('docs').controller('SettingsTheme', function($scope, $rootScope, Restangular) {
|
||||
angular.module('docs').controller('SettingsConfig', function($scope, $rootScope, Restangular) {
|
||||
// Get the app configuration
|
||||
Restangular.one('app').get().then(function(data) {
|
||||
$scope.app = data;
|
||||
});
|
||||
|
||||
// Enable/disable guest login
|
||||
$scope.changeGuestLogin = function(enabled) {
|
||||
Restangular.one('app').post('guest_login', {
|
||||
enabled: enabled
|
||||
}).then(function() {
|
||||
$scope.app.guest_login = enabled;
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch the current theme configuration
|
||||
Restangular.one('theme').get().then(function(data) {
|
||||
$scope.theme = data;
|
@ -60,7 +60,7 @@
|
||||
<script src="app/docs/controller/settings/Settings.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/settings/SettingsDefault.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/settings/SettingsAccount.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/settings/SettingsTheme.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/settings/SettingsConfig.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/settings/SettingsSecurity.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/settings/SettingsSecurityModalDisableTotp.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/settings/SettingsSession.js" type="text/javascript"></script>
|
||||
@ -125,12 +125,13 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/settings/account" title="Logged in as {{ userInfo.username }}">
|
||||
<a href="{{ userInfo.username == 'guest' ? '#/user/guest' : '#/settings/account' }}"
|
||||
title="Logged in as {{ userInfo.username }}">
|
||||
<span class="glyphicon glyphicon-user"></span>
|
||||
{{ userInfo.username }}
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: $uiRoute}" ui-route="/settings.*">
|
||||
<li ng-class="{active: $uiRoute}" ui-route="/settings.*" ng-show="userInfo.username != 'guest'">
|
||||
<a href="#/settings/account">
|
||||
<span class="glyphicon glyphicon-cog"></span> Settings
|
||||
</a>
|
||||
|
@ -46,6 +46,19 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-if="!documents">
|
||||
<td colspan="3" class="text-center">
|
||||
<img src="img/loader.gif" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-if="totalDocuments == 0">
|
||||
<td colspan="3" class="text-center">
|
||||
<span ng-if="search.length == 0">No document in the database</span>
|
||||
<span ng-if="search.length > 0">No matches for <strong>"{{ search }}"</strong></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-click="viewDocument(document.id)" ng-repeat="document in documents" ng-class="{ active: $stateParams.id == document.id }">
|
||||
<td>
|
||||
{{ document.title }} ({{ document.file_count }})
|
||||
@ -79,8 +92,11 @@
|
||||
used on {{ userInfo.storage_quota / 1000000 | number: 0 }}MB
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
{{ totalDocuments }} document{{ totalDocuments > 1 ? 's' : '' }} found
|
||||
<div class="text-right" >
|
||||
<span ng-if="totalDocuments">
|
||||
{{ totalDocuments }} document{{ totalDocuments > 1 ? 's' : '' }} found
|
||||
</span>
|
||||
<span ng-if="!totalDocuments"> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -35,6 +35,12 @@
|
||||
<button type="submit" class="btn btn-primary btn-block" ng-click="login()">
|
||||
<span class="glyphicon glyphicon-ok"></span> Sign in
|
||||
</button>
|
||||
|
||||
<p class="text-center lead" ng-if="app.guest_login"> </p>
|
||||
|
||||
<button type="submit" class="btn btn-default btn-block" ng-if="app.guest_login" ng-click="loginAsGuest()">
|
||||
<span class="glyphicon glyphicon-user"></span> Login as guest
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,3 +1,18 @@
|
||||
<h1>
|
||||
Guest <small>access</small>
|
||||
<span class="label" ng-class="{ 'label-success': app.guest_login, 'label-danger': !app.guest_login }">
|
||||
{{ app.guest_login ? 'Enabled' : 'Disabled' }}
|
||||
</span>
|
||||
</h1>
|
||||
<p>
|
||||
Guest access is a mode where anyone can access {{ appName }} without password.<br/>
|
||||
Like a normal user, the guest user can only access its documents and those accessible through permissions.<br/>
|
||||
</p>
|
||||
<div ng-if="app">
|
||||
<button ng-if="!app.guest_login" class="btn btn-primary" ng-click="changeGuestLogin(true)">Enable guest access</button>
|
||||
<button ng-if="app.guest_login" class="btn btn-danger" ng-click="changeGuestLogin(false)">Disable guest access</button>
|
||||
</div>
|
||||
|
||||
<h1>Theme <small>customization</small></h1>
|
||||
<form class="form-horizontal" name="editColorForm" novalidate>
|
||||
<div class="form-group">
|
@ -1,4 +1,4 @@
|
||||
<h1>Groups <small>management</small> <a class="btn btn-primary" href="#/settings/group/add">Add</a></h1>
|
||||
<h1>Groups <small>management</small> <a class="btn btn-primary" href="#/settings/group/add">Add a group</a></h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 well">
|
||||
|
@ -9,14 +9,14 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" ng-show="isAdmin"><strong>General settings</strong></div>
|
||||
<div class="panel panel-default" ng-show="isAdmin">
|
||||
<div class="panel-heading"><strong>General settings</strong></div>
|
||||
<ul class="list-group">
|
||||
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/user.*" href="#/settings/user">Users</a>
|
||||
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/group.*" href="#/settings/group">Groups</a>
|
||||
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/vocabulary.*" href="#/settings/vocabulary">Vocabularies</a>
|
||||
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/theme" href="#/settings/theme">Theme</a>
|
||||
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/log" href="#/settings/log">Server logs</a>
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/user.*" href="#/settings/user">Users</a>
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/group.*" href="#/settings/group">Groups</a>
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/vocabulary.*" href="#/settings/vocabulary">Vocabularies</a>
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/config" href="#/settings/config">Configuration</a>
|
||||
<a class="list-group-item" ng-class="{active: $uiRoute}" ui-route="/settings/log" href="#/settings/log">Server logs</a>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,10 +13,12 @@
|
||||
<td>{{ session.create_date | date: 'yyyy-MM-dd HH:mm' }}</td>
|
||||
<td>{{ session.last_connection_date | date: 'yyyy-MM-dd HH:mm' }}</td>
|
||||
<td title="{{ session.user_agent }}">{{ session.ip }}</td>
|
||||
<td><span ng-show="session.current" class="glyphicon glyphicon-ok"></span></td>
|
||||
<td>
|
||||
<span ng-show="session.current" class="glyphicon glyphicon-ok" title="This is the current session"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-warning" ng-click="deleteSession()">Clear all other sessions</button>
|
||||
<button type="submit" class="btn btn-warning" title="All other devices connected to this account will be disconnected" ng-click="deleteSession()">Clear all other sessions</button>
|
||||
</div>
|
@ -62,7 +62,7 @@
|
||||
<span class="help-block" ng-show="editUserForm.storage_quota.$error.pattern">Number required</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !editUserForm.password.$valid, success: editUserForm.password.$valid }">
|
||||
<div class="form-group" ng-if="user.username != 'guest'" ng-class="{ 'has-error': !editUserForm.password.$valid, success: editUserForm.password.$valid }">
|
||||
<label class="col-sm-2 control-label" for="inputPassword">Password</label>
|
||||
|
||||
<div class="col-sm-7">
|
||||
@ -76,7 +76,7 @@
|
||||
<span class="help-block" ng-show="editUserForm.password.$error.maxlength">Too long</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !editUserForm.passwordconfirm.$valid, success: editUserForm.passwordconfirm.$valid }">
|
||||
<div class="form-group" ng-if="user.username != 'guest'" ng-class="{ 'has-error': !editUserForm.passwordconfirm.$valid, success: editUserForm.passwordconfirm.$valid }">
|
||||
<label class="col-sm-2 -label" for="inputPasswordConfirm">Password (confirm)</label>
|
||||
|
||||
<div class="col-sm-7">
|
||||
@ -94,7 +94,7 @@
|
||||
<button type="submit" class="btn btn-primary" ng-click="edit()" ng-disabled="!editUserForm.$valid">
|
||||
<span class="glyphicon glyphicon-pencil"></span> {{ isEdit() ? 'Edit' : 'Add' }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit()">
|
||||
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit() && user.username != 'guest'">
|
||||
<span class="glyphicon glyphicon-trash"></span> Delete
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<h1>Users <small>management</small> <a class="btn btn-primary" href="#/settings/user/add">Add</a></h1>
|
||||
<h1>Users <small>management</small> <a class="btn btn-primary" href="#/settings/user/add">Add a user</a></h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 well">
|
||||
|
@ -1,3 +1,3 @@
|
||||
api.current_version=${project.version}
|
||||
api.min_version=1.0
|
||||
db.version=9
|
||||
db.version=10
|
@ -1,3 +1,3 @@
|
||||
api.current_version=${project.version}
|
||||
api.min_version=1.0
|
||||
db.version=9
|
||||
db.version=10
|
@ -1,5 +1,13 @@
|
||||
package com.sismics.docs.rest;
|
||||
|
||||
import com.sismics.docs.core.constant.PermType;
|
||||
import com.sismics.docs.core.dao.jpa.AclDao;
|
||||
import com.sismics.util.context.ThreadLocalContext;
|
||||
import com.sismics.util.filter.TokenBasedSecurityFilter;
|
||||
import com.sismics.util.jpa.EMF;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.json.JsonArray;
|
||||
import javax.json.JsonObject;
|
||||
import javax.persistence.EntityManager;
|
||||
@ -9,15 +17,6 @@ import javax.ws.rs.core.Form;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import com.sismics.docs.core.constant.PermType;
|
||||
import com.sismics.docs.core.dao.jpa.AclDao;
|
||||
import com.sismics.util.context.ThreadLocalContext;
|
||||
import com.sismics.util.jpa.EMF;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.sismics.util.filter.TokenBasedSecurityFilter;
|
||||
|
||||
|
||||
/**
|
||||
* Test the app resource.
|
||||
@ -35,17 +34,15 @@ public class TestAppResource extends BaseJerseyTest {
|
||||
|
||||
// Check the application info
|
||||
JsonObject json = target().path("/app").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||
.get(JsonObject.class);
|
||||
String currentVersion = json.getString("current_version");
|
||||
Assert.assertNotNull(currentVersion);
|
||||
String minVersion = json.getString("min_version");
|
||||
Assert.assertNotNull(minVersion);
|
||||
Assert.assertNotNull(json.getString("current_version"));
|
||||
Assert.assertNotNull(json.getString("min_version"));
|
||||
Long freeMemory = json.getJsonNumber("free_memory").longValue();
|
||||
Assert.assertTrue(freeMemory > 0);
|
||||
Long totalMemory = json.getJsonNumber("total_memory").longValue();
|
||||
Assert.assertTrue(totalMemory > 0 && totalMemory > freeMemory);
|
||||
|
||||
Assert.assertFalse(json.getBoolean("guest_login"));
|
||||
|
||||
// Rebuild Lucene index
|
||||
Response response = target().path("/app/batch/reindex").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||
@ -127,4 +124,69 @@ public class TestAppResource extends BaseJerseyTest {
|
||||
Long date4 = logs.getJsonObject(9).getJsonNumber("date").longValue();
|
||||
Assert.assertTrue(date3 >= date4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the guest login.
|
||||
*/
|
||||
@Test
|
||||
public void testGuestLogin() {
|
||||
// Login admin
|
||||
String adminToken = clientUtil.login("admin", "admin", false);
|
||||
|
||||
// Try to login as guest
|
||||
Response response = target().path("/user/login").request()
|
||||
.post(Entity.form(new Form()
|
||||
.param("username", "guest")));
|
||||
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
|
||||
|
||||
// Enable guest login
|
||||
target().path("/app/guest_login").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||
.post(Entity.form(new Form()
|
||||
.param("enabled", "true")), JsonObject.class);
|
||||
|
||||
// Login as guest
|
||||
String guestToken = clientUtil.login("guest", "", false);
|
||||
|
||||
// Guest cannot delete himself
|
||||
response = target().path("/user").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
|
||||
.delete();
|
||||
Assert.assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
|
||||
|
||||
// Guest cannot see opened sessions
|
||||
JsonObject json = target().path("/user/session").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
|
||||
.get(JsonObject.class);
|
||||
Assert.assertEquals(0, json.getJsonArray("sessions").size());
|
||||
|
||||
// Guest cannot delete opened sessions
|
||||
response = target().path("/user/session").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
|
||||
.delete();
|
||||
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
|
||||
|
||||
// Guest cannot enable TOTP
|
||||
response = target().path("/user/enable_totp").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
|
||||
.post(Entity.form(new Form()));
|
||||
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
|
||||
|
||||
// Guest cannot disable TOTP
|
||||
response = target().path("/user/disable_totp").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
|
||||
.post(Entity.form(new Form()));
|
||||
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
|
||||
|
||||
// Guest cannot update itself
|
||||
response = target().path("/user").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
|
||||
.post(Entity.form(new Form()));
|
||||
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
|
||||
|
||||
// Guest can see its documents
|
||||
target().path("/document/list").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken)
|
||||
.get(JsonObject.class);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import javax.ws.rs.core.Form;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import com.sismics.util.filter.HeaderBasedSecurityFilter;
|
||||
import org.junit.Assert;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
@ -28,7 +29,7 @@ public class TestSecurity extends BaseJerseyTest {
|
||||
clientUtil.createUser("testsecurity");
|
||||
|
||||
// Changes a user's email KO : the user is not connected
|
||||
Response response = target().path("/user/update").request()
|
||||
Response response = target().path("/user").request()
|
||||
.post(Entity.form(new Form().param("email", "testsecurity2@docs.com")));
|
||||
Assert.assertEquals(Status.FORBIDDEN, Status.fromStatusCode(response.getStatus()));
|
||||
JsonObject json = response.readEntity(JsonObject.class);
|
||||
@ -73,4 +74,29 @@ public class TestSecurity extends BaseJerseyTest {
|
||||
// User testsecurity logs out
|
||||
clientUtil.logout(testSecurityToken);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHeaderBasedAuthentication() {
|
||||
clientUtil.createUser("header_auth_test");
|
||||
|
||||
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), target()
|
||||
.path("/user/session")
|
||||
.request()
|
||||
.get()
|
||||
.getStatus());
|
||||
|
||||
Assert.assertEquals(Status.OK.getStatusCode(), target()
|
||||
.path("/user/session")
|
||||
.request()
|
||||
.header(HeaderBasedSecurityFilter.AUTHENTICATED_USER_HEADER, "header_auth_test")
|
||||
.get()
|
||||
.getStatus());
|
||||
|
||||
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), target()
|
||||
.path("/user/session")
|
||||
.request()
|
||||
.header(HeaderBasedSecurityFilter.AUTHENTICATED_USER_HEADER, "idontexist")
|
||||
.get()
|
||||
.getStatus());
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ hibernate.connection.username=sa
|
||||
hibernate.connection.password=
|
||||
hibernate.hbm2ddl.auto=
|
||||
hibernate.dialect=org.hibernate.dialect.HSQLDialect
|
||||
hibernate.show_sql=true
|
||||
hibernate.show_sql=false
|
||||
hibernate.format_sql=false
|
||||
hibernate.max_fetch_depth=5
|
||||
hibernate.cache.use_second_level_cache=false
|
||||
|
@ -1,10 +1,10 @@
|
||||
log4j.rootCategory=DEBUG, CONSOLE, MEMORY
|
||||
log4j.rootCategory=INFO, CONSOLE, MEMORY
|
||||
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
|
||||
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
|
||||
log4j.appender.CONSOLE.layout.ConversionPattern=%d{DATE} %p %l %m %n
|
||||
log4j.appender.MEMORY=com.sismics.util.log4j.MemoryAppender
|
||||
log4j.appender.MEMORY.size=1000
|
||||
|
||||
log4j.logger.com.sismics=DEBUG
|
||||
log4j.logger.com.sismics=INFO
|
||||
log4j.logger.org.hibernate=INFO
|
||||
log4j.logger.org.apache.pdfbox=INFO
|
Loading…
Reference in New Issue
Block a user