Merge pull request #313 from ivandiazwm/master

Replace text editor, add automated image uploading, fix max-size issue
This commit is contained in:
Ivan Diaz 2018-09-22 13:34:16 -03:00 committed by GitHub
commit 158bb099e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
138 changed files with 584 additions and 382 deletions

View File

@ -12,8 +12,8 @@ before_install:
- rvm use 2.2 --install --binary --fuzzy
- ruby --version
- mysql -e 'CREATE DATABASE development;'
- nvm install 4.4.7
- npm install -g npm@2.15.8
- nvm install 6.14.4
- npm install -g npm@6.1.0
- npm install -g mocha
- cd client
- npm install

View File

@ -56,6 +56,7 @@ Just as there is a `gulp dev` task for development, there is also a `gulp prod`
- `make db` access to mysql database console
- `make sh` access to backend docker container bash
- `make test` run phpunit tests
- `make doc` to build the documentation (requires `apidoc`)
Server api runs on `http://localhost:8080/`
Also, there's a *phpmyadmin* instance running on `http://localhost:6060/`,

View File

@ -58,16 +58,15 @@
"axios": "^0.18.0",
"chart.js": "^2.4.0",
"classnames": "^2.2.5",
"draft-js": "^0.10.5",
"draftjs-to-html": "^0.8.4",
"history": "^3.0.0",
"html-to-draftjs": "^1.4.0",
"html-to-text": "^4.0.0",
"keycode": "^2.1.4",
"localStorage": "^1.0.3",
"lodash": "^3.10.0",
"messageformat": "^0.2.2",
"qs": "^6.5.2",
"quill-image-resize-module-react": "^3.0.0",
"random-string": "^0.2.0",
"react": "^15.4.2",
"react-chartjs-2": "^2.0.0",
"react-document-title": "^1.0.2",
@ -75,6 +74,7 @@
"react-draft-wysiwyg": "^1.12.13",
"react-google-recaptcha": "^0.5.2",
"react-motion": "^0.4.7",
"react-quill": "^1.3.1",
"react-redux": "^4.4.5",
"react-router": "^3.0.2",
"react-router-redux": "^4.0.7",

View File

@ -1,17 +0,0 @@
'use strict';
var babel = require('babel-core');
module.exports = {
process: function(src, filename) {
// Ignore files other than .js, .es, .jsx or .es6
if (!babel.canCompile(filename)) {
return '';
}
// Ignore all files within node_modules
if (filename.indexOf('node_modules') === -1) {
return babel.transform(src, {filename: filename}).code;
}
return src;
}
};

View File

@ -1,4 +1,6 @@
import React from 'react';
import _ from 'lodash';
import {connect} from 'react-redux';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
@ -9,12 +11,14 @@ import Form from 'core-components/form';
import FormField from 'core-components/form-field';
import SubmitButton from 'core-components/submit-button';
import Button from 'core-components/button';
import TextEditor from 'core-components/text-editor';
class ArticleAddModal extends React.Component {
static propTypes = {
topicId: React.PropTypes.number.isRequired,
topicName: React.PropTypes.string.isRequired,
position: React.PropTypes.number.isRequired
position: React.PropTypes.number.isRequired,
allowAttachments: React.PropTypes.bool
};
state = {
@ -27,7 +31,7 @@ class ArticleAddModal extends React.Component {
<Header title={i18n('ADD_ARTICLE')} description={i18n('ADD_ARTICLE_DESCRIPTION', {category: this.props.topicName})} />
<Form onSubmit={this.onAddNewArticleFormSubmit.bind(this)} loading={this.state.loading}>
<FormField name="title" label={i18n('TITLE')} field="input" fieldProps={{size: 'large'}} validation="TITLE" required/>
<FormField name="content" label={i18n('CONTENT')} field="textarea" validation="TEXT_AREA" required/>
<FormField name="content" label={i18n('CONTENT')} field="textarea" validation="TEXT_AREA" required fieldProps={{allowImages: this.props.allowAttachments}}/>
<SubmitButton type="secondary">{i18n('ADD_ARTICLE')}</SubmitButton>
<Button className="article-add-modal__cancel-button" type="link" onClick={(event) => {
event.preventDefault();
@ -45,12 +49,12 @@ class ArticleAddModal extends React.Component {
API.call({
path: '/article/add',
data: {
dataAsForm: true,
data: _.extend(TextEditor.getContentFormData(form.content), {
title: form.title,
content: form.content,
topicId: this.props.topicId,
position: this.props.position
}
})
}).then(() => {
ModalContainer.closeModal();
@ -64,5 +68,8 @@ class ArticleAddModal extends React.Component {
});
}
}
export default ArticleAddModal;
export default connect((store) => {
return {
allowAttachments: store.config['allow-attachments']
};
})(ArticleAddModal);

View File

@ -29,6 +29,7 @@ class TicketViewer extends React.Component {
assignmentAllowed: React.PropTypes.bool,
staffMembers: React.PropTypes.array,
staffMembersLoaded: React.PropTypes.bool,
allowAttachments: React.PropTypes.bool,
userId: React.PropTypes.number,
userStaff: React.PropTypes.bool,
userDepartments: React.PropTypes.array,
@ -214,7 +215,7 @@ class TicketViewer extends React.Component {
{this.renderCustomResponses()}
<div className="ticket-viewer__response-field row">
<Form {...this.getCommentFormProps()}>
<FormField name="content" validation="TEXT_AREA" required field="textarea" />
<FormField name="content" validation="TEXT_AREA" required field="textarea" fieldProps={{allowImages: this.props.allowAttachments}}/>
{(this.props.allowAttachments) ? <FormField name="file" field="file"/> : null}
<div className="ticket-viewer__response-buttons">
<SubmitButton type="secondary">{i18n('RESPOND_TICKET')}</SubmitButton>
@ -386,7 +387,7 @@ class TicketViewer extends React.Component {
dataAsForm: true,
data: _.extend({
ticketNumber: this.props.ticket.ticketNumber
}, formState)
}, formState, TextEditor.getContentFormData(formState.content))
}).then(this.onCommentSuccess.bind(this), this.onCommentFail.bind(this));
}

View File

@ -22,7 +22,8 @@ class AdminPanelViewArticle extends React.Component {
static propTypes = {
topics: React.PropTypes.array,
loading: React.PropTypes.bool
loading: React.PropTypes.bool,
allowAttachments: React.PropTypes.bool
};
static defaultProps = {
@ -95,7 +96,7 @@ class AdminPanelViewArticle extends React.Component {
</Button>
</div>
<FormField name="title" label={i18n('TITLE')} />
<FormField name="content" label={i18n('CONTENT')} field="textarea" />
<FormField name="content" label={i18n('CONTENT')} field="textarea" fieldProps={{allowImages: this.props.allowAttachments}}/>
</Form>
);
}
@ -129,11 +130,11 @@ class AdminPanelViewArticle extends React.Component {
onFormSubmit(form) {
API.call({
path: '/article/edit',
data: {
dataAsForm: true,
data: _.extend(TextEditor.getContentFormData(form.content), {
articleId: this.findArticle().id,
title: form.title,
content: form.content
}
title: form.title
})
}).then(() => {
this.props.dispatch(ArticlesActions.retrieveArticles());
this.setState({
@ -162,6 +163,7 @@ class AdminPanelViewArticle extends React.Component {
export default connect((store) => {
return {
allowAttachments: store.config['allow-attachments'],
topics: store.articles.topics,
loading: store.articles.loading
};

View File

@ -1,6 +1,6 @@
import React from 'react';
import _ from 'lodash';
import {connect} from 'react-redux';
import {connect} from 'react-redux';
import history from 'lib-app/history';
import i18n from 'lib-app/i18n';
@ -57,7 +57,13 @@ class CreateTicketForm extends React.Component {
size: 'medium'
}}/>
</div>
<FormField label={i18n('CONTENT')} name="content" validation="TEXT_AREA" required field="textarea" />
<FormField
label={i18n('CONTENT')}
name="content"
validation="TEXT_AREA"
fieldProps={{allowImages: this.props.allowAttachments}}
required
field="textarea" />
{(this.props.allowAttachments) ? this.renderFileUpload() : null}
{(!this.props.userLogged) ? this.renderCaptcha() : null}
<SubmitButton>{i18n('CREATE_TICKET')}</SubmitButton>
@ -125,7 +131,7 @@ class CreateTicketForm extends React.Component {
API.call({
path: '/ticket/create',
dataAsForm: true,
data: _.extend({}, formState, {
data: _.extend({}, formState, TextEditor.getContentFormData(formState.content), {
captcha: captcha && captcha.getValue(),
departmentId: SessionStore.getDepartments()[formState.departmentIndex].id
})

View File

@ -13,6 +13,66 @@ class MainSignUpPage extends React.Component {
</div>
);
}
renderMessage() {
switch (this.state.message) {
case 'success':
return <Message type="success">{i18n('SIGNUP_SUCCESS')}</Message>;
case 'fail':
return <Message type="error">{i18n('EMAIL_EXISTS')}</Message>;
default:
return null;
}
}
getFormProps() {
return {
loading: this.state.loading,
className: 'signup-widget__form',
onSubmit: this.onSignupFormSubmit.bind(this)
};
}
getInputProps(password) {
return {
className: 'signup-widget__input',
fieldProps: {
size: 'medium',
password: password
}
};
}
onSignupFormSubmit(formState) {
const captcha = this.refs.captcha.getWrappedInstance();
if (!captcha.getValue()) {
captcha.focus();
} else {
this.setState({
loading: true
});
API.call({
path: '/user/signup',
data: _.extend({captcha: captcha.getValue()}, formState)
}).then(this.onSignupSuccess.bind(this)).catch(this.onSignupFail.bind(this));
}
}
onSignupSuccess() {
this.setState({
loading: false,
message: 'success'
});
}
onSignupFail() {
this.setState({
loading: false,
message: 'fail'
});
}
}
export default MainSignUpPage;

View File

@ -1,13 +1,11 @@
// MOCKS
const ValidationFactoryMock = require('lib-app/__mocks__/validations/validation-factory-mock');
const TextEditorMock = require('core-components/__mocks__/text-editor-mock');
const FormField = ReactMock();
// COMPONENT
const Form = requireUnit('core-components/form', {
'lib-app/validations/validator-factory': ValidationFactoryMock,
'core-components/form-field': FormField,
'core-components/text-editor': TextEditorMock
});
describe('Form component', function () {
@ -187,18 +185,6 @@ describe('Form component', function () {
expect(form.props.onSubmit).to.not.have.been.called;
});
it('should transform TextEdit value to HTML', function () {
form.state.form.first = TextEditorMock.createEmpty();
TestUtils.Simulate.submit(ReactDOM.findDOMNode(form));
expect(TextEditorMock.getHTMLFromEditorState).to.have.been.calledWith(form.state.form.first);
expect(form.props.onSubmit).to.have.been.calledWith({
first: 'HTML_CODE',
second: 'value2',
third: 'value3'
});
});
it('should focus the first field with error', function () {
ValidationFactoryMock.validators.defaultValidatorMock.performValidation = stub().returns('MOCK_ERROR');
ValidationFactoryMock.validators.customValidatorMock.performValidation = stub().returns('MOCK_ERROR_2');

View File

@ -6,7 +6,6 @@ import {reactDFS, renderChildrenWithProps} from 'lib-core/react-dfs';
import ValidationFactory from 'lib-app/validations/validator-factory';
import FormField from 'core-components/form-field';
import TextEditor from 'core-components/text-editor';
class Form extends React.Component {
@ -160,13 +159,7 @@ class Form extends React.Component {
handleSubmit(event) {
event.preventDefault();
const form = _.mapValues(this.getFormValue(), (field) => {
if (TextEditor.isEditorState(field)) {
return TextEditor.getHTMLFromEditorState(field);
} else {
return field;
}
});
const form = this.getFormValue();
if (this.hasFormErrors()) {
this.updateErrors(this.getAllFieldErrors(), this.focusFirstErrorField.bind(this));
@ -180,10 +173,7 @@ class Form extends React.Component {
form[fieldName] = event.target.value;
this.setState({
form: form
});
if(this.props.values === undefined) this.setState({form});
if (this.props.onChange) {
this.props.onChange(form);
@ -213,7 +203,7 @@ class Form extends React.Component {
}
getFormValue() {
return this.props.values || this.state.form;
return (this.props.values !== undefined) ? this.props.values : this.state.form;
}
focusFirstErrorField() {

View File

@ -1,62 +1,64 @@
import React from 'react';
import classNames from 'classnames';
import {Editor} from 'react-draft-wysiwyg';
import {EditorState, ContentState, convertToRaw} from 'draft-js';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import ReactQuill, { Quill } from 'react-quill';
import ImageResize from 'quill-image-resize-module-react';
import {isIE} from 'lib-core/navigator';
import Base64ImageParser from 'lib-core/base64-image-parser';
Quill.register('modules/ImageResize', ImageResize);
class TextEditor extends React.Component {
static propTypes = {
errored: React.PropTypes.bool,
onChange: React.PropTypes.func,
value: React.PropTypes.oneOfType([
React.PropTypes.object, React.PropTypes.string
])
value: React.PropTypes.string,
allowImages: React.PropTypes.bool
};
static createEmpty() {
if(isIE()) return '';
return EditorState.createEmpty();
return '';
}
static getEditorStateFromHTML(htmlString) {
if(isIE()) return htmlString;
const blocksFromHTML = htmlToDraft(htmlString);
const state = ContentState.createFromBlockArray(
blocksFromHTML.contentBlocks,
blocksFromHTML.entityMap
);
return EditorState.createWithContent(state);
return htmlString;
}
static getHTMLFromEditorState(editorState) {
if(isIE()) return editorState;
return draftToHtml(convertToRaw(editorState.getCurrentContent()));
return editorState;
}
static isEditorState(editorState) {
if(isIE()) return typeof editorState === 'String';
return editorState && editorState.getCurrentContent;
return typeof editorState === 'String';
}
static getContentFormData(content) {
const images = Base64ImageParser.getImagesSrc(content).map(Base64ImageParser.dataURLtoFile);
const contentFormData = {
'content': Base64ImageParser.removeImagesSrc(content),
'images': images.length,
};
images.forEach((image, index) => contentFormData[`image_${index}`] = image);
return contentFormData;
}
state = {
value: TextEditor.createEmpty(),
value: '',
focused: false
};
render() {
return (
<div className={this.getClass()}>
{isIE() ? this.renderTextArea() : this.renderDraftJS()}
{isIE() ? this.renderTextArea() : this.renderQuill()}
</div>
);
}
renderDraftJS() {
return <Editor {...this.getEditorProps()} />;
renderQuill() {
return <ReactQuill {...this.getEditorProps()} />
}
renderTextArea() {
@ -67,7 +69,7 @@ class TextEditor extends React.Component {
onFocus={this.onEditorFocus.bind(this)}
onBlur={this.onBlur.bind(this)}
ref="editor"
value={this.props.value || this.state.value}
value={this.props.value}
/>
);
}
@ -87,46 +89,36 @@ class TextEditor extends React.Component {
getEditorProps() {
return {
wrapperClassName: 'text-editor__editor',
editorState: this.props.value || this.state.value,
ref: 'editor',
toolbar: this.getToolbarOptions(),
onEditorStateChange: this.onEditorChange.bind(this),
className: 'text-editor__editor',
value: (this.props.value !== undefined) ? this.props.value : this.state.value,
ref: "editor",
modules: this.getModulesOptions(),
onChange: this.onEditorChange.bind(this),
onFocus: this.onEditorFocus.bind(this),
onBlur: this.onBlur.bind(this)
onBlur: this.onBlur.bind(this),
onKeyDown: (e) => { if(e.key == "Tab") { e.preventDefault(); e.stopPropagation(); }}
};
}
getToolbarOptions() {
getModulesOptions() {
return {
options: ['inline', 'blockType', 'list', 'link', 'image', 'textAlign'],
inline: {
inDropdown: false,
options: ['bold', 'italic', 'underline', 'strikethrough', 'monospace']
},
blockType: {
inDropdown: true,
options: [ 'Normal', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Blockquote']
},
list: {
inDropdown: false,
options: ['unordered', 'ordered']
},
image: {
urlEnabled: true,
uploadEnabled: false,
alignmentEnabled: false
},
textAlign: {
inDropdown: false,
options: ['left', 'center', 'right', 'justify'],
toolbar: {
container: [
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ align: [] }],
['bold', 'italic', 'underline','strike', 'blockquote'],
[{'list': 'ordered'}, {'list': 'bullet'}],
['blockquote', 'code-block' ],
(this.props.allowImages) ? ['link', 'image'] : ['link']
],
},
ImageResize: {parchment: Quill.import('parchment')},
};
}
onEditorChange(value) {
if(isIE()) value = value.target.value;
this.setState({value});
if(this.props.value === undefined) this.setState({value});
if (this.props.onChange) {
this.props.onChange({target: {value}});
@ -151,11 +143,7 @@ class TextEditor extends React.Component {
focus() {
if (this.refs.editor) {
if(isIE()) {
this.refs.editor.focus();
} else {
this.refs.editor.focusEditor();
}
this.refs.editor.focus();
}
}
}

View File

@ -7,13 +7,8 @@
border: 1px solid $grey;
border-radius: 3px;
.DraftEditor-root {
height: 200px;
padding-left: 10px;
}
.public-DraftEditor-content {
height: 185px;
.ql-container {
min-height: 200px;
}
}

View File

@ -2,7 +2,7 @@ import React from 'react'
import {Motion, spring} from 'react-motion';
class Tooltip extends React.Component {
static propTypes = {
children: React.PropTypes.node,
content: React.PropTypes.node,
@ -105,4 +105,4 @@ class Tooltip extends React.Component {
}
}
export default Tooltip;
export default Tooltip;

View File

@ -21,7 +21,7 @@ module.exports = [
'session-prefix': 'opensupports-z6ctpq2winvfhchX2_',
'maintenance-mode': false,
'allow-attachments': true,
'max-size': 500,
'max-size': 1,
'departments': [
{id: 1, name: 'Sales Support', owners: 2},
{id: 2, name: 'Technical Issues', owners: 5},

View File

@ -323,6 +323,7 @@ export default {
'INVALID_EMAIL_OR_TICKET_NUMBER': 'Número de e-mail ou chamado inválido',
'INVALID_FILE': 'arquivo inválido',
'ERRORS_FOUND': 'Erros encontrados',
'ERROR_IMAGE_SIZE': 'Nenhuma imagem pode ter um tamanho maior que {size} MB',
//MESSAGES
'SIGNUP_SUCCESS': 'Você se registrou com sucesso em nosso sistema de suporte.',

View File

@ -323,6 +323,7 @@ export default {
'INVALID_EMAIL_OR_TICKET_NUMBER': '電子郵件或機票號無效',
'INVALID_FILE': '無效文件',
'ERRORS_FOUND': '發現錯誤',
'ERROR_IMAGE_SIZE': '没有图像的大小可以超过{size}MB',
//MESSAGES
'SIGNUP_SUCCESS': '您已在我們的支持系統中成功註冊',

View File

@ -323,6 +323,7 @@ export default {
'INVALID_EMAIL_OR_TICKET_NUMBER': 'Ungültige E-Mail-Adresse oder Ticketnummer!',
'INVALID_FILE': 'Ungültige Datei!',
'ERRORS_FOUND': 'Fehler gefunden!',
'ERROR_IMAGE_SIZE': 'Kein Bild darf größer als {size} MB sein',
//MESSAGES
'SIGNUP_SUCCESS': 'Sie haben sich erfolgreich in unserem Support-System registriert.',

View File

@ -324,6 +324,7 @@ export default {
'INVALID_EMAIL_OR_TICKET_NUMBER': 'Invalid email or ticket number',
'INVALID_FILE': 'Invalid file',
'ERRORS_FOUND': 'Errors found',
'ERROR_IMAGE_SIZE': 'No image can have a size greater than {size} MB',
//MESSAGES
'SIGNUP_SUCCESS': 'You have registered successfully in our support system.',

View File

@ -348,6 +348,7 @@ export default {
'WILL_RECOVER_EMAIL_TEMPLATE': 'Esta plantilla de correo electrónico se recuperará a su valor predeterminado en este idioma.',
'SUCCESS_IMPORTING_CSV_DESCRIPTION': 'El archivo CSV se ha importado correctamente',
'SUCCESS_DELETING_ALL_USERS': 'Los usuarios se han eliminado correctamente',
'ERROR_IMAGE_SIZE': 'Ninguna imagen puede tener un tamaño superior a {size} MB',
'LAST_7_DAYS': 'Últimos 7 dias',
'LAST_30_DAYS': 'Últimos 30 dias',

View File

@ -323,6 +323,7 @@ export default {
'INVALID_EMAIL_OR_TICKET_NUMBER': 'Numéro de courriel ou de ticket invalide',
'INVALID_FILE': 'Fichier invalide',
'ERRORS_FOUND': 'Des erreurs sont survenues',
'ERROR_IMAGE_SIZE': 'Aucune image ne peut avoir une taille supérieure à {size} MB',
//MESSAGES
'SIGNUP_SUCCESS': 'Vous êtes inscrit avec succès dans notre système de support.',

View File

@ -324,6 +324,7 @@
'INVALID_EMAIL_OR_TICKET_NUMBER': 'Μη έγκυρη ηλεκτρονική διεύθυνση ή αριθμός εισιτηρίου',
'INVALID_FILE': 'Μη έγκυρο αρχείο',
'ERRORS_FOUND': 'Βρέθηκαν Σφάλματα',
'ERROR_IMAGE_SIZE': 'Καμία εικόνα δεν μπορεί να έχει μέγεθος μεγαλύτερο από {size} MB',
//MESSAGES
'SIGNUP_SUCCESS': 'Έχετε εγγραφεί με επιτυχία στο σύστημα υποστήριξης μας.',

View File

@ -323,6 +323,7 @@ export default {
'INVALID_EMAIL_OR_TICKET_NUMBER': 'अमान्य ईमेल या टिकट नंबर',
'INVALID_FILE': 'अवैध फाइल',
'ERRORS_FOUND': 'त्रुटियां मिलीं',
'ERROR_IMAGE_SIZE': 'कोई छवि {size} एमबी से अधिक आकार नहीं हो सकती है',
//MESSAGES
'SIGNUP_SUCCESS': 'आप हमारे समर्थन प्रणाली में सफलतापूर्वक दर्ज कर लिया है।',

View File

@ -323,6 +323,7 @@ export default {
'INVALID_EMAIL_OR_TICKET_NUMBER': 'E-mail o numero di ticket non validi',
'INVALID_FILE': 'File non valido',
'ERRORS_FOUND': 'Trovati errori',
'ERROR_IMAGE_SIZE': 'Nessuna immagine può avere una dimensione superiore a {size} MB',
//MESSAGES
'SIGNUP_SUCCESS': 'È stato registrato con successo nel nostro sistema di supporto.',

View File

@ -323,6 +323,7 @@ export default {
'INVALID_EMAIL_OR_TICKET_NUMBER': '電子メールまたはチケット番号が無効です',
'INVALID_FILE': '無効なファイル',
'ERRORS_FOUND': 'エラーが見つかりました',
'ERROR_IMAGE_SIZE': 'イメージのサイズが{size} MBを超えることはできません',
//MESSAGES
'SIGNUP_SUCCESS': 'あなたは私たちのサポートシステムに正常に登録しました。',

View File

@ -324,6 +324,7 @@ export default {
'INVALID_EMAIL_OR_TICKET_NUMBER': 'Ongeldig e-mailadres of incidentnummer',
'INVALID_FILE': 'Ongeldig bestand',
'ERRORS_FOUND': 'Er is een fout opgetreden',
'ERROR_IMAGE_SIZE': 'Geen enkele afbeelding kan groter zijn dan {size} MB',
//MESSAGES
'SIGNUP_SUCCESS': 'U hebt zich succesvol geregistreerd in ons ondersteuningssysteem.',

View File

@ -323,6 +323,7 @@ export default {
'INVALID_EMAIL_OR_TICKET_NUMBER': 'Número de e-mail ou bilhete inválido',
'INVALID_FILE': 'arquivo inválido',
'ERRORS_FOUND': 'Erros encontrados',
'ERROR_IMAGE_SIZE': 'Nenhuma imagem pode ter um tamanho maior que {size} MB',
//MESSAGES
'SIGNUP_SUCCESS': 'Você se registrou com sucesso em nosso sistema de suporte.',

View File

@ -322,6 +322,7 @@ export default {
'INVALID_EMAIL_OR_TICKET_NUMBER': 'Неправильный номер эл. Почты или номера билета.',
'INVALID_FILE': 'неверный файл',
'ERRORS_FOUND': 'Ошибки найдены',
'ERROR_IMAGE_SIZE': 'Изображение не может иметь размер больше {size} МБ',
//MESSAGES
'SIGNUP_SUCCESS': 'Вы успешно зарегистрировались в нашей системе поддержки.',

View File

@ -323,6 +323,7 @@ export default {
'INVALID_EMAIL_OR_TICKET_NUMBER': 'Geçersiz e-posta veya bilet numarası',
'INVALID_FILE': 'geçersiz dosya',
'ERRORS_FOUND': 'Hatalar bulundu',
'ERROR_IMAGE_SIZE': 'Hiçbir resmin boyutu {size} MB\'den büyük olabilir',
//MESSAGES
'SIGNUP_SUCCESS': 'Destek sistemimize başarılı bir şekilde kayıt oldunuz.',

View File

@ -61,6 +61,7 @@ class SessionStore {
this.setItem('user-system-enabled', configs['user-system-enabled']);
this.setItem('allow-attachments', configs['allow-attachments']);
this.setItem('maintenance-mode', configs['maintenance-mode']);
this.setItem('max-size', configs['max-size']);
}
getConfigs() {
@ -75,7 +76,8 @@ class SessionStore {
registration: (this.getItem('registration') * 1),
'user-system-enabled': (this.getItem('user-system-enabled') * 1),
'allow-attachments': (this.getItem('allow-attachments') * 1),
'maintenance-mode': (this.getItem('maintenance-mode') * 1)
'maintenance-mode': (this.getItem('maintenance-mode') * 1),
'max-size': this.getItem('max-size'),
};
}

View File

@ -0,0 +1,24 @@
import _ from 'lodash';
import Validator from 'lib-app/validations/validator';
import SessionStore from 'lib-app/session-store';
import Base64ImageParser from 'lib-core/base64-image-parser';
class ImageSizeValidator extends Validator {
constructor(errorKey = 'ERROR_IMAGE_SIZE', validator = null) {
super(validator);
this.maxSize = 1;
this.errorKey = errorKey;
}
validate(value = '', form = {}) {
let images = Base64ImageParser.getImagesSrc(value).map(Base64ImageParser.dataURLtoFile);
if(_.some(images, f => f.size > 1048576 * SessionStore.getItem('max-size'))) {
return this.getError(this.errorKey, {size: SessionStore.getItem('max-size')});
}
}
}
export default ImageSizeValidator;

View File

@ -1,22 +1,20 @@
import TextEditor from 'core-components/text-editor';
import Validator from 'lib-app/validations/validator';
class LengthValidator extends Validator {
constructor(length, errorKey = 'INVALID_VALUE', validator = null) {
super(validator);
this.minlength = length;
this.errorKey = errorKey;
}
validate(value = '', form = {}) {
if (TextEditor.isEditorState(value)) {
value = value.getCurrentContent().getPlainText();
}
let div = document.createElement("div");
div.innerHTML = value;
let text = div.textContent || div.innerText || "";
if (value.length < this.minlength) return this.getError(this.errorKey);
if (text.length < this.minlength) return this.getError(this.errorKey);
}
}
export default LengthValidator;
export default LengthValidator;

View File

@ -3,13 +3,14 @@ import EmailValidator from 'lib-app/validations/email-validator';
import RepeatPasswordValidator from 'lib-app/validations/repeat-password-validator';
import LengthValidator from 'lib-app/validations/length-validator';
import ListValidator from 'lib-app/validations/list-validator';
import ImageSizeValidator from 'lib-app/validations/image-size-validator';
let validators = {
'DEFAULT': new Validator(),
'NAME': new LengthValidator(2, 'ERROR_NAME'),
'TITLE': new LengthValidator(1, 'ERROR_TITLE'),
'EMAIL': new EmailValidator(),
'TEXT_AREA': new LengthValidator(10, 'ERROR_CONTENT_SHORT'),
'TEXT_AREA': new ImageSizeValidator(undefined, new LengthValidator(10, 'ERROR_CONTENT_SHORT')),
'PASSWORD': new LengthValidator(6, 'ERROR_PASSWORD'),
'REPEAT_PASSWORD': new RepeatPasswordValidator(),
'URL': new LengthValidator(5, 'ERROR_URL'),

View File

@ -6,7 +6,7 @@ class Validator {
constructor(validator = null) {
this.previousValidator = validator;
}
performValidation(value, form) {
let error;
@ -27,9 +27,9 @@ class Validator {
if (value.length === 0) return this.getError('ERROR_EMPTY');
}
getError(errorKey) {
return i18n(errorKey);
getError(errorKey, params) {
return i18n(errorKey, params);
}
}
export default Validator
export default Validator

View File

@ -0,0 +1,42 @@
import randomString from 'random-string';
export default {
removeImagesSrc(str) {
let index=-1;
str = str.replace(/src="(data:image\/[^;]+;base64[^"]+)"/g, () => {
index++;
return `src="IMAGE_PATH_${index}"`;
});
return str;
},
getImagesSrc(str) {
let m,
urls = [],
rex = /src="(data:image\/[^;]+;base64[^"]+)"/g;
while ( m = rex.exec( str ) ) {
urls.push( m[1] );
}
return urls;
},
dataURLtoFile(dataurl) {
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
let extension=".jpg";
if(dataurl.indexOf("image/png") !== -1) extension=".png";
else if(dataurl.indexOf("image/tiff") !== -1) extension=".tiff";
else if(dataurl.indexOf("image/bmp") !== -1) extension=".bmp";
else if(dataurl.indexOf("image/gif") !== -1) extension=".gif";
return new File([u8arr], randomString() + extension, {type:mime});
},
};

View File

@ -4,6 +4,7 @@ var jsdom = require('jsdom').jsdom;
global.document = jsdom('<html><body></body></html>');
global.window = document.defaultView;
global.Node = global.window.Node;
global.navigator = {
userAgent: 'node.js'
};

View File

@ -4,6 +4,7 @@
@import 'scss/base';
@import 'scss/font_awesome/font-awesome';
@import 'scss/react-draft-wysiwyg';
@import 'scss/quill.snow.min';
@import 'core-components/*';
@import 'app-components/*';

7
client/src/scss/quill.snow.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -38,3 +38,30 @@ This request will return you the session data with an `userId` and a `token`. Yo
If you don't pass the userId and token, a `NO_PERMISSION` error will be returned.
Additionally, if there are no users (only staff members), you can check a ticket you created by providing your email and the ticketNumber to the `/ticket/check` path. This path will return you a `token` and `ticketNumber` you will use to comment, retrieve, or do any other operations to the ticket.
## File Attachments
We have two settings for file attachment:
* *allow-attachments* setting flag indicates if users can attach files.
* *max-size* setting indicates what is the file size limit in MB.
When you want to attach images to a ticket, comment or article; you can place the string `IMAGE_PATH_i` inside the parameter `content`.
`IMAGE_PATH_i` indicates that it should be replaced with the path of the image of index `i` (zero-indexed).
You may also include the `images` parameter indicating the number of images; and `image_i` parameters, which contain the image file object of index `i`.
For example
```
/article/add
title = 'article title'
content = 'this is an article <img src="IMAGE_PATH_0"/> with two images <img src="IMAGE_PATH_1"/>'
position = 1
topicId = 1
images = 2
image_0 = <File>
image_1 = <File>
```
This request will upload `image_0` and `image_1`. After that, it will replace `IMAGE_PATH_0` and `IMAGE_PATH_1` with the corresponding urls for each image. The rest of the request will operate normal.
**Please remember that `max-size` setting applies also to images.**

View File

@ -37,3 +37,6 @@ db:
sh:
@docker exec -it opensupports-srv bash
doc:
@apidoc -i models/ -i data/ -i libs/ -i controllers/ -o apidoc/

View File

@ -1,82 +0,0 @@
/**
* @api {post} /staff/get Get staff
* @apiVersion 4.0.0
*
* @apiName Get staff
*
* @apiGroup Staff
*
* @apiDescription This path retrieves information about a staff member.
*
* @apiPermission staff1
*
* @apiParam {Number} staffId The id of the staff member to be searched.
*
* @apiUse NO_PERMISSION
*
* @apiSuccess {Object} data Information about a staff member
* @apiSuccess {String} data.name Staff id
* @apiSuccess {String} data.email Staff id
* @apiSuccess {String} data.profilePic Staff id
* @apiSuccess {Number} data.level Staff id
* @apiSuccess {Boolean} data.staff Staff id
* @apiSuccess {[Department](#api-Data_Structures-ObjectDepartment)[]} data.departments Array of departments that has assigned.
* @apiSuccess {[Ticket](#api-Data_Structures-ObjectTicket)[]} data.tickets Array of tickets that has assigned.
*
*/
/**
* @api {get} /system/download Download file
* @apiVersion 4.0.0
*
* @apiName Download file
*
* @apiGroup System
*
* @apiDescription This path downloads a file.
*
* @apiPermission any
*
* @apiParam {String} file The filename to be downloaded.
*
*
* @apiSuccess {Object} file File content
*
*/
/**
* @api {post} /system/init-settings Init settings
* @apiVersion 4.0.0
*
* @apiName Init settings
*
* @apiGroup System
*
* @apiDescription This path sets the initial settings. It can only be used once during installation.
*
* @apiPermission any
*
* @apiParam {String} language Indicates the default language of the system.
* @apiParam {String} user-system-enabled Indicates if the user system should be enabled.
* @apiParam {String} registration Indicates if the registration should be enabled.
*
* @apiUse INVALID_LANGUAGE
* @apiUse INIT_SETTINGS_DONE
*
* @apiSuccess {Object} data Empty object
*
*/
/**
* @api {OBJECT} Staff Staff
* @apiVersion 4.0.0
* @apiGroup Data Structures
* @apiParam {String} name Name of the staff member.
* @apiParam {String} email Email of the staff member.
* @apiParam {String} profilePic profilePic url of the staff member.
* @apiParam {Number} level Level of the staff member.
* @apiParam {Object[]} departments The departments the staff member has assigned.
* @apiParam {[Ticket](#api-Data_Structures-ObjectTicket)[]} tickets The tickets the staff member has assigned.
* @apiParam {Number} lastLogin The last login of the staff member.
*/

View File

@ -1,10 +1,10 @@
{
"name": "OpenSupports API Documentation",
"version": "4.1.0",
"version": "4.3.0",
"title": "OpenSupports API Documentation",
"description": "Backend API documentation for developers.",
"header": {
"title": "API Standards",
"filename": "API_STANDARD.md"
}
}
}

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /article/add-topic Add topic
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Add topic
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /article/add Add article
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Add article
*
@ -18,11 +18,14 @@ DataValidator::with('CustomValidations', true);
* @apiParam {String} content Content of the new article.
* @apiParam {Number} position Position of the new article.
* @apiParam {Number} topicId Id of the articles's topic.
* @apiParam {Number} images The number of images in the content
* @apiParam image_i The image file of index `i` (mutiple params accepted)
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_NAME
* @apiUse INVALID_CONTENT
* @apiUse INVALID_TOPIC
* @apiUse INVALID_FILE
*
* @apiSuccess {Object} data Article info
* @apiSuccess {Number} data.articleId Article id
@ -53,10 +56,16 @@ class AddArticleController extends Controller {
}
public function handler() {
$content = Controller::request('content', true);
$fileUploader = FileUploader::getInstance();
$fileUploader->setPermission(FileManager::PERMISSION_ARTICLE);
$imagePaths = $this->uploadImages(true);
$article = new Article();
$article->setProperties([
'title' => Controller::request('title'),
'content' => Controller::request('content', true),
'content' => $this->replaceWithImagePaths($imagePaths, $content),
'lastEdited' => Date::getCurrentDate(),
'position' => Controller::request('position') || 1
]);

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /article/delete-topic Delete topic
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Delete topic
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /article/delete Delete article
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Delete article
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /article/edit-topic Edit topic
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Edit topic
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /article/edit Edit article
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Edit a article
*
@ -19,9 +19,12 @@ DataValidator::with('CustomValidations', true);
* @apiParam {String} content The new content of the article. Optional.
* @apiParam {String} title The new title of the article. Optional.
* @apiParam {Number} position The new position of the article. Optional.
*
* @apiParam {Number} images The number of images in the content
* @apiParam image_i The image file of index `i` (mutiple params accepted)
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_TOPIC
* @apiUse INVALID_FILE
*
* @apiSuccess {Object} data Empty object
*
@ -58,7 +61,13 @@ class EditArticleController extends Controller {
}
if(Controller::request('content')) {
$article->content = Controller::request('content', true);
$fileUploader = FileUploader::getInstance();
$fileUploader->setPermission(FileManager::PERMISSION_ARTICLE);
$content = Controller::request('content', true);
$imagePaths = $this->uploadImages(true);
$article->content = $this->replaceWithImagePaths($imagePaths, $content);
}
if(Controller::request('title')) {
@ -77,4 +86,4 @@ class EditArticleController extends Controller {
Response::respondSuccess();
}
}
}

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /article/get-all Get all articles
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get all articles
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /staff/add Add staff
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Add staff
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /staff/assign-ticket Assign ticket
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Assign ticket
*

View File

@ -4,7 +4,7 @@ use RedBeanPHP\Facade as RedBean;
/**
* @api {post} /staff/delete Delete staff
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Delete staff
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /staff/edit Edit staff
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Edit staff
*
@ -114,6 +114,9 @@ class EditStaffController extends Controller {
}
}
$fileUploader = FileUploader::getInstance();
$fileUploader->setPermission(FileManager::PERMISSION_PROFILE);
if($fileUploader = $this->uploadFile(true)) {
$this->staffInstance->profilePic = ($fileUploader instanceof FileUploader) ? $fileUploader->getFileName() : null;
}

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /staff/get-all-tickets Get all tickets
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get all tickets
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /staff/get-all Get all staffs
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get all staffs
*

View File

@ -4,7 +4,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /staff/get-new-tickets Get new tickets
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get new tickets
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /staff/get-tickets Get tickets
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get tickets
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /staff/get Get staff
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get staff
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /staff/last-events Get last events
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get last events
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /staff/search-tickets Search tickets
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Search tickets
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /staff/un-assign-ticket Un-assign ticket
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Un-assign ticket
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /system/add-api-key Add APIKey
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Add APIKey
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /system/add-department Add department
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Add department
*

View File

@ -3,7 +3,7 @@ use Ifsnop\Mysqldump as IMysqldump;
/**
* @api {post} /system/backup-database Backup database
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Backup database
*

View File

@ -2,7 +2,7 @@
/**
* @api {post} /system/check-requirements Checks requirements
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Check requirements
*

View File

@ -2,7 +2,7 @@
/**
* @api {post} /system/csv-import CSV import
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName CSV import
*

View File

@ -3,7 +3,7 @@ use RedBeanPHP\Facade as RedBean;
/**
* @api {post} /system/delete-all-users Delete all users
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Delete all users
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /system/delete-api-key Delete APIKey
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Delete APIKey
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /system/delete-department Delete department
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Delete department
*

View File

@ -2,7 +2,7 @@
/**
* @api {post} /system/disable-registration Disable registration
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Disable registration
*

View File

@ -2,7 +2,7 @@
/**
* @api {post} /system/disable-user-system Disable user system
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Disable user system
*

View File

@ -4,7 +4,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {get} /system/download Download file
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Download file
*
@ -42,36 +42,32 @@ class DownloadController extends Controller {
$fileName = Controller::request('file');
$isStaffProfilePic = !Staff::getDataStore($fileName, 'profilePic')->isNull();
if(!$isStaffProfilePic) {
$session = Session::getInstance();
$loggedUser = Controller::getLoggedUser();
$fileDownloader = FileDownloader::getInstance();
$fileDownloader->setFileName($fileName);
if(!$session->sessionExists()) {
Response::respond403();
return;
}
$session = Session::getInstance();
$ticket = Ticket::getTicket($fileName, 'file');
if($ticket->isNull() || ($this->isNotAuthor($ticket, $loggedUser) && $this->isNotDepartmentOwner($ticket, $loggedUser))) {
$ticketEvent = Ticketevent::getDataStore($fileName, 'file');
if($ticketEvent->isNull()) {
Response::respond403();
return;
}
$ticket = $ticketEvent->ticket;
if($this->isNotAuthor($ticket, $loggedUser) && $this->isNotDepartmentOwner($ticket, $loggedUser)) {
Response::respond403();
return;
}
if(!$session->isStaffLogged()) {
switch($fileDownloader->getFilePermission()) {
case FileManager::PERMISSION_TICKET:
$ticketNumber = $fileDownloader->getTicketNumber();
$ticket = Ticket::getByTicketNumber($ticketNumber);
if($this->isNotAuthor($ticket, Controller::getLoggedUser())) {
return Response::respond403();
}
break;
case FileManager::PERMISSION_ARTICLE:
if(Controller::isUserSystemEnabled() && !$session->sessionExists()) {
return Response::respond403();
}
break;
case FileManager::PERMISSION_PROFILE:
break;
default:
return Response::respond403();
}
}
$fileDownloader = FileDownloader::getInstance();
$fileDownloader->setFileName($fileName);
$fileDownloader->download();
exit();
}
@ -82,7 +78,7 @@ class DownloadController extends Controller {
if($session->getTicketNumber()) {
return $session->getTicketNumber() !== $ticket->ticketNumber;
} else {
return $loggedUser->level >= 1 || $ticket->author->id !== $loggedUser->id;
return $ticket->author->id !== $loggedUser->id || ($loggedUser instanceof Staff) !== $ticket->authorToArray()['staff'];
}
}

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /system/edit-department Edit department
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Edit department
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /system/edit-mail-template Edit mail template
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Edit mail template
*

View File

@ -2,7 +2,7 @@
/**
* @api {post} /system/edit-settings Edit settings
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Edit settings
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /system/enable-registration Enable registration
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Enable registration
*

View File

@ -2,7 +2,7 @@
/**
* @api {post} /system/enable-user-system Enable user system
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Enable user system
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /system/get-api-keys Get APIKeys
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get APIKeys
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /system/get-logs Get logs
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get logs
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /system/get-mail-templates Get mail templates
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get mail templates
*

View File

@ -2,7 +2,7 @@
/**
* @api {post} /system/get-settings Get settings
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get settings
*

View File

@ -4,7 +4,7 @@ use RedBeanPHP\Facade as RedBean;
/**
* @api {post} /system/get-stats Get stats
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get stats
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /system/init-admin Init admin
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Init admin
*

View File

@ -4,7 +4,7 @@ use RedBeanPHP\Facade as RedBean;
/**
* @api {post} /system/init-database Init database
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Init database
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /system/init-settings Init settings
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Init settings
*
@ -77,7 +77,7 @@ class InitSettingsController extends Controller {
'maintenance-mode' => 0,
'layout' => 'boxed',
'allow-attachments' => !!Controller::request('allow-attachments'),
'max-size' => 1024,
'max-size' => 1,
'title' => Controller::request('title') ? Controller::request('title') : 'Support Center',
'url' => Controller::request('url') ? Controller::request('url') : ('http://' . $_SERVER['HTTP_HOST']),
'registration' => !!Controller::request('registration'),

View File

@ -3,7 +3,7 @@ use RedBeanPHP\Facade as RedBean;
/**
* @api {post} /system/installation-done Installation done
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Installation done
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /system/recover-mail-template Recover mail template
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Recover mail template
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /system/test-smtp Test SMTP Connection
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Test SMTP Connection
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/add-custom-response Add custom responses
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Add a custom response
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/change-department Change department
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Change department
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /ticket/change-priority Change priority
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Change priority
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/check Check ticket
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Check ticket
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/close Close ticket
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Close
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/comment Comment ticket
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Comment ticket
*
@ -16,11 +16,15 @@ DataValidator::with('CustomValidations', true);
*
* @apiParam {String} content Content of the comment.
* @apiParam {Number} ticketNumber The number of the ticket to comment.
* @apiParam {Number} images The number of images in the content
* @apiParam image_i The image file of index `i` (mutiple params accepted)
* @apiParam file The file you with to upload.
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_CONTENT
* @apiUse INVALID_TICKET
* @apiUse INVALID_TOKEN
* @apiUse INVALID_FILE
*
* @apiSuccess {Object} data Empty object
*
@ -105,11 +109,14 @@ class CommentController extends Controller {
}
private function storeComment() {
$fileUploader = $this->uploadFile();
$fileUploader = FileUploader::getInstance();
$fileUploader->setPermission(FileManager::PERMISSION_TICKET, $this->ticket->ticketNumber);
$imagePaths = $this->uploadImages(Controller::isStaffLogged());
$fileUploader = $this->uploadFile(Controller::isStaffLogged());
$comment = Ticketevent::getEvent(Ticketevent::COMMENT);
$comment->setProperties(array(
'content' => $this->content,
'content' => $this->replaceWithImagePaths($imagePaths, $this->content),
'file' => ($fileUploader instanceof FileUploader) ? $fileUploader->getFileName() : null,
'date' => Date::getCurrentDate()
));

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/create Create ticket
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Create ticket
*
@ -19,7 +19,9 @@ DataValidator::with('CustomValidations', true);
* @apiParam {Number} departmentId The id of the department of the current ticket.
* @apiParam {String} language The language of the ticket.
* @apiParam {String} email The email of the user who created the ticket.
* @apiParam {String} name The Name of the author of the ticket.
* @apiParam {Number} images The number of images in the content
* @apiParam image_i The image file of index `i` (mutiple params accepted)
* @apiParam file The file you with to upload.
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_TITLE
@ -28,6 +30,7 @@ DataValidator::with('CustomValidations', true);
* @apiUse INVALID_LANGUAGE
* @apiUse INVALID_CAPTCHA
* @apiUse INVALID_EMAIL
* @apiUse INVALID_FILE
*
* @apiSuccess {Object} data Information of the new ticket
* @apiSuccess {Number} data.ticketNumber Number of the new ticket
@ -69,7 +72,7 @@ class CreateController extends Controller {
]
];
if(!Controller::isUserSystemEnabled()) {
if(!Controller::isUserSystemEnabled() && !Controller::isStaffLogged()) {
$validations['permission'] = 'any';
$validations['requestData']['captcha'] = [
'validation' => DataValidator::captcha(),
@ -118,13 +121,17 @@ class CreateController extends Controller {
private function storeTicket() {
$department = Department::getDataStore($this->departmentId);
$author = Controller::getLoggedUser();
$fileUploader = $this->uploadFile();
$ticket = new Ticket();
$fileUploader = FileUploader::getInstance();
$fileUploader->setPermission(FileManager::PERMISSION_TICKET, $ticket->generateUniqueTicketNumber());
$imagePaths = $this->uploadImages(Controller::isStaffLogged());
$fileUploader = $this->uploadFile(Controller::isStaffLogged());
$ticket->setProperties(array(
'title' => $this->title,
'content' => $this->content,
'content' => $this->replaceWithImagePaths($imagePaths, $this->content),
'language' => $this->language,
'department' => $department,
'file' => ($fileUploader instanceof FileUploader) ? $fileUploader->getFileName() : null,

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/delete-custom-response Delete custom response
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Delete custom response
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/edit-custom-response Edit custom response
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Edit custom response
*

View File

@ -4,7 +4,7 @@ DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/get-custom-responses Get custom responses
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get custom responses
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/get Get ticket
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Get ticket
*

View File

@ -3,7 +3,7 @@ use Respect\Validation\Validator as DataValidator;
/**
* @api {post} /ticket/re-open Reopen ticket
* @apiVersion 4.2.0
* @apiVersion 4.3.0
*
* @apiName Reopen ticket
*

Some files were not shown because too many files have changed in this diff Show More