Merged in os-147-151-file-architecture-and-backup (pull request #123)

Os 147 151 file architecture and backup
This commit is contained in:
Ivan Diaz 2017-01-15 00:02:45 -03:00
commit bfcb02b311
37 changed files with 531 additions and 49 deletions

View File

@ -2,6 +2,7 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import i18n from 'lib-app/i18n'; import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
import DateTransformer from 'lib-core/date-transformer'; import DateTransformer from 'lib-core/date-transformer';
import Icon from 'core-components/icon'; import Icon from 'core-components/icon';
@ -161,7 +162,7 @@ class TicketEvent extends React.Component {
} }
return ( return (
<div className="ticket-viewer__file"> <div className="ticket-event__file">
{node} {node}
</div> </div>
) )
@ -222,9 +223,26 @@ class TicketEvent extends React.Component {
const fileName = filePath.replace(/^.*[\\\/]/, ''); const fileName = filePath.replace(/^.*[\\\/]/, '');
return ( return (
<a href={filePath} target="_blank">{fileName}</a> <span onClick={this.onFileClick.bind(this, filePath)}>{fileName}</span>
) )
} }
onFileClick(filePath) {
API.call({
path: '/system/download',
plain: true,
data: {
file: filePath
}
}).then((result) => {
let contentType = 'application/octet-stream';
let link = document.createElement('a');
let blob = new Blob([result], {'type': contentType});
link.href = window.URL.createObjectURL(blob);
link.download = filePath;
link.click();
});
}
} }
export default TicketEvent; export default TicketEvent;

View File

@ -91,6 +91,14 @@
} }
} }
&__file {
background-color: $very-light-grey;
cursor: pointer;
text-align: right;
padding: 5px 10px;
font-size: 12px;
}
&_staff { &_staff {
.ticket-event__icon { .ticket-event__icon {
background-color: $primary-blue; background-color: $primary-blue;

View File

@ -190,6 +190,7 @@ class TicketViewer extends React.Component {
<div className="ticket-viewer__response-field row"> <div className="ticket-viewer__response-field row">
<Form {...this.getCommentFormProps()}> <Form {...this.getCommentFormProps()}>
<FormField name="content" validation="TEXT_AREA" required field="textarea" /> <FormField name="content" validation="TEXT_AREA" required field="textarea" />
<FormField name="file" field="file"/>
<SubmitButton>{i18n('RESPOND_TICKET')}</SubmitButton> <SubmitButton>{i18n('RESPOND_TICKET')}</SubmitButton>
</Form> </Form>
</div> </div>
@ -234,10 +235,12 @@ class TicketViewer extends React.Component {
loading: this.state.loading, loading: this.state.loading,
onChange: (formState) => {this.setState({ onChange: (formState) => {this.setState({
commentValue: formState.content, commentValue: formState.content,
commentFile: formState.file,
commentEdited: true commentEdited: true
})}, })},
values: { values: {
'content': this.state.commentValue 'content': this.state.commentValue,
'file': this.state.commentFile
} }
}; };
} }

View File

@ -48,13 +48,6 @@
margin-top: 10px; margin-top: 10px;
} }
&__file {
background-color: $very-light-grey;
text-align: right;
padding: 5px 10px;
font-size: 12px;
}
&__comments { &__comments {
position: relative; position: relative;
} }

View File

@ -56,6 +56,9 @@ class CreateTicketForm extends React.Component {
}}/> }}/>
</div> </div>
<FormField label={i18n('CONTENT')} name="content" validation="TEXT_AREA" required field="textarea" /> <FormField label={i18n('CONTENT')} name="content" validation="TEXT_AREA" required field="textarea" />
<div className="create-ticket-form__file">
<FormField name="file" field="file" />
</div>
{(!this.props.userLogged) ? this.renderCaptcha() : null} {(!this.props.userLogged) ? this.renderCaptcha() : null}
<SubmitButton>Create Ticket</SubmitButton> <SubmitButton>Create Ticket</SubmitButton>
</Form> </Form>

View File

@ -1,5 +1,9 @@
.create-ticket-form { .create-ticket-form {
&__file {
text-align: left;
}
&__message { &__message {
margin-top: 20px; margin-top: 20px;
} }

View File

@ -0,0 +1,40 @@
import React from 'react';
import Button from 'core-components/button';
import Icon from 'core-components/icon';
class FileUploader extends React.Component {
static propTypes = {
text: React.PropTypes.string,
value: React.PropTypes.object,
onChange: React.PropTypes.func
};
static defaultProps = {
text: 'Upload file'
};
render() {
return (
<label className="file-uploader">
<input className="file-uploader__input" type="file" multiple={false} onChange={this.onChange.bind(this)}/>
<span className="file-uploader__custom" tabIndex="0">
<Icon className="file-uploader__icon" name="upload" /> {this.props.text}
</span>
<span className="file-uploader__value">{this.props.value && this.props.value.name}</span>
</label>
);
}
onChange(event) {
if(this.props.onChange) {
this.props.onChange({
target: {
value: event.target.files[0]
}
});
}
}
}
export default FileUploader;

View File

@ -0,0 +1,31 @@
@import "../scss/vars";
.file-uploader {
&__input {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
&__custom {
display: inline-block;
cursor: pointer;
color: white;
background-color: $primary-red;
line-height: 35px;
padding: 0 15px;
}
&__icon {
margin-right: 15px;
}
&__value {
margin-left: 10px;
color: $dark-grey;
}
}

View File

@ -9,6 +9,7 @@ import Checkbox from 'core-components/checkbox';
import CheckboxGroup from 'core-components/checkbox-group'; import CheckboxGroup from 'core-components/checkbox-group';
import TextEditor from 'core-components/text-editor'; import TextEditor from 'core-components/text-editor';
import InfoTooltip from 'core-components/info-tooltip'; import InfoTooltip from 'core-components/info-tooltip';
import FileUploader from 'core-components/file-uploader';
class FormField extends React.Component { class FormField extends React.Component {
static contextTypes = { static contextTypes = {
@ -24,7 +25,7 @@ class FormField extends React.Component {
error: React.PropTypes.string, error: React.PropTypes.string,
infoMessage: React.PropTypes.node, infoMessage: React.PropTypes.node,
value: React.PropTypes.any, value: React.PropTypes.any,
field: React.PropTypes.oneOf(['input', 'textarea', 'select', 'checkbox', 'checkbox-group']), field: React.PropTypes.oneOf(['input', 'textarea', 'select', 'checkbox', 'checkbox-group', 'file']),
fieldProps: React.PropTypes.object fieldProps: React.PropTypes.object
}; };
@ -84,7 +85,8 @@ class FormField extends React.Component {
'textarea': TextEditor, 'textarea': TextEditor,
'select': DropDown, 'select': DropDown,
'checkbox': Checkbox, 'checkbox': Checkbox,
'checkbox-group': CheckboxGroup 'checkbox-group': CheckboxGroup,
'file': FileUploader
}[this.props.field]; }[this.props.field];
if(this.props.decorator) { if(this.props.decorator) {
@ -142,7 +144,8 @@ class FormField extends React.Component {
getDivTypes() { getDivTypes() {
return [ return [
'textarea', 'textarea',
'checkbox-group' 'checkbox-group',
'file'
]; ];
} }

View File

@ -110,6 +110,14 @@ module.exports = [
}; };
} }
}, },
{
path: '/system/download',
time: 100,
contentType: 'application/octet-stream',
response: function () {
return 'text content';
}
},
{ {
path: '/system/get-mail-templates', path: '/system/get-mail-templates',
time: 100, time: 100,

View File

@ -166,7 +166,7 @@ module.exports = [
name: 'Sales Support' name: 'Sales Support'
}, },
date: '201504090001', date: '201504090001',
file: 'http://www.opensupports.com/some_file.zip', file: 'some_file.txt',
language: 'en', language: 'en',
unread: false, unread: false,
closed: false, closed: false,
@ -385,7 +385,7 @@ module.exports = [
name: 'Technical Issues' name: 'Technical Issues'
}, },
date: '201604161427', date: '201604161427',
file: 'http://www.opensupports.com/some_file.zip', file: 'some_file.txt',
language: 'en', language: 'en',
unread: true, unread: true,
closed: false, closed: false,
@ -504,7 +504,7 @@ module.exports = [
name: 'Sales Support' name: 'Sales Support'
}, },
date: '201604150849', date: '201604150849',
file: 'http://www.opensupports.com/some_file.zip', file: 'some_file.txt',
language: 'en', language: 'en',
unread: false, unread: false,
closed: false, closed: false,
@ -607,7 +607,7 @@ module.exports = [
name: 'Sales Support' name: 'Sales Support'
}, },
date: '201504091032', date: '201504091032',
file: 'http://www.opensupports.com/some_file.zip', file: 'some_file.txt',
language: 'en', language: 'en',
unread: false, unread: false,
closed: false, closed: false,

View File

@ -12,13 +12,13 @@ function processData (data) {
} }
module.exports = { module.exports = {
call: function ({path, data}) { call: function ({path, data, plain}) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
APIUtils.post(root + path, processData(data)) APIUtils.post(root + path, processData(data))
.then(function (result) { .then(function (result) {
console.log(result); console.log(result);
if (result.status === 'success') { if (plain || result.status === 'success') {
resolve(result); resolve(result);
} else if (reject) { } else if (reject) {
reject(result); reject(result);

View File

@ -24,7 +24,7 @@ fixtures.add(require('data/fixtures/article-fixtures'));
_.each(fixtures.getAll(), function (fixture) { _.each(fixtures.getAll(), function (fixture) {
mockjax({ mockjax({
contentType: 'application/json', contentType: fixture.contentType || 'application/json',
url: 'http://localhost:3000/api' + fixture.path, url: 'http://localhost:3000/api' + fixture.path,
responseTime: fixture.time || 500, responseTime: fixture.time || 500,
response: function (settings) { response: function (settings) {

View File

@ -9,7 +9,8 @@ const APIUtils = {
url: path, url: path,
method: method, method: method,
data: data, data: data,
dataType: 'json' processData: false,
contentType: false
}) })
.done(resolve) .done(resolve)
.fail((jqXHR, textStatus) => { .fail((jqXHR, textStatus) => {

View File

@ -4,7 +4,8 @@
"respect/validation": "^1.1", "respect/validation": "^1.1",
"phpmailer/phpmailer": "^5.2", "phpmailer/phpmailer": "^5.2",
"google/recaptcha": "~1.1", "google/recaptcha": "~1.1",
"gabordemooij/redbean": "^4.3" "gabordemooij/redbean": "^4.3",
"ifsnop/mysqldump-php": "2.*"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "5.0.*" "phpunit/phpunit": "5.0.*"

View File

@ -16,6 +16,8 @@ require_once 'system/add-api-key.php';
require_once 'system/delete-api-key.php'; require_once 'system/delete-api-key.php';
require_once 'system/get-all-keys.php'; require_once 'system/get-all-keys.php';
require_once 'system/delete-all-users.php'; require_once 'system/delete-all-users.php';
require_once 'system/backup-database.php';
require_once 'system/download.php';
$systemControllerGroup = new ControllerGroup(); $systemControllerGroup = new ControllerGroup();
$systemControllerGroup->setGroupPath('/system'); $systemControllerGroup->setGroupPath('/system');
@ -37,5 +39,7 @@ $systemControllerGroup->addController(new AddAPIKeyController);
$systemControllerGroup->addController(new DeleteAPIKeyController); $systemControllerGroup->addController(new DeleteAPIKeyController);
$systemControllerGroup->addController(new GetAllKeyController); $systemControllerGroup->addController(new GetAllKeyController);
$systemControllerGroup->addController(new DeleteAllUsersController); $systemControllerGroup->addController(new DeleteAllUsersController);
$systemControllerGroup->addController(new BackupDatabaseController);
$systemControllerGroup->addController(new DownloadController);
$systemControllerGroup->finalize(); $systemControllerGroup->finalize();

View File

@ -0,0 +1,30 @@
<?php
use Ifsnop\Mysqldump as IMysqldump;
class BackupDatabaseController extends Controller {
const PATH = '/backup-database';
public function validations() {
return [
'permission' => 'staff_3',
'requestData' => []
];
}
public function handler() {
global $mysql_host;
global $mysql_database;
global $mysql_user;
global $mysql_password;
$fileDownloader = FileDownloader::getInstance();
$fileDownloader->setFileName('backup.sql');
$mysqlDump = new IMysqldump\Mysqldump('mysql:host='. $mysql_host .';dbname=' . $mysql_database, $mysql_user, $mysql_password);
$mysqlDump->start($fileDownloader->getFullFilePath());
if($fileDownloader->download()) {
$fileDownloader->eraseFile();
}
}
}

View File

@ -0,0 +1,54 @@
<?php
use Ifsnop\Mysqldump as IMysqldump;
use Respect\Validation\Validator as DataValidator;
class DownloadController extends Controller {
const PATH = '/download';
public function validations() {
return [
'permission' => 'user',
'requestData' => [
'file' => [
'validation' => DataValidator::alnum('_.-')->noWhitespace(),
'error' => ERRORS::INVALID_FILE
]
]
];
}
public function handler() {
$fileName = Controller::request('file');
$loggedUser = Controller::getLoggedUser();
$ticket = Ticket::getTicket($fileName, 'file');
if($ticket->isNull() || ($this->isNotAuthor($ticket, $loggedUser) && $this->isNotOwner($ticket, $loggedUser))) {
$ticketEvent = Ticketevent::getDataStore($fileName, 'file');
if($ticketEvent->isNull()) {
print '';
return;
}
$ticket = $ticketEvent->ticket;
if($this->isNotAuthor($ticket, $loggedUser) && $this->isNotOwner($ticket, $loggedUser)) {
print '';
return;
}
}
$fileDownloader = FileDownloader::getInstance();
$fileDownloader->setFileName($fileName);
$fileDownloader->download();
}
private function isNotAuthor($ticket, $loggedUser) {
return Controller::isStaffLogged() || $ticket->author->id !== $loggedUser->id;
}
private function isNotOwner($ticket, $loggedUser) {
return !Controller::isStaffLogged() || !$ticket->owner || $ticket->owner->id !== $loggedUser->id;
}
}

View File

@ -38,11 +38,15 @@ class InitSettingsController extends Controller {
'maintenance-mode' => 0, 'maintenance-mode' => 0,
'layout' => 'boxed', 'layout' => 'boxed',
'allow-attachments' => 0, 'allow-attachments' => 0,
'max-size' => 0, 'max-size' => 1024,
'title' => 'Support Center', 'title' => 'Support Center',
'url' => 'http://www.opensupports.com/support', 'url' => 'http://www.opensupports.com/support',
'registration' => true, 'registration' => true,
'last-stat-day' => '20170101' //TODO: get current date 'last-stat-day' => '20170101', //TODO: get current date
'ticket-gap' => Hashing::generateRandomPrime(100000, 999999),
'file-gap' => Hashing::generateRandomPrime(100000, 999999),
'file-first-number' => Hashing::generateRandomNumber(100000, 999999),
'file-quantity' => 0
]); ]);
} }

View File

@ -50,6 +50,7 @@ class CommentController extends Controller {
$comment = Ticketevent::getEvent(Ticketevent::COMMENT); $comment = Ticketevent::getEvent(Ticketevent::COMMENT);
$comment->setProperties(array( $comment->setProperties(array(
'content' => $this->content, 'content' => $this->content,
'file' => $this->uploadFile(),
'date' => Date::getCurrentDate() 'date' => Date::getCurrentDate()
)); ));

View File

@ -60,7 +60,7 @@ class CreateController extends Controller {
'language' => $this->language, 'language' => $this->language,
'author' => $author, 'author' => $author,
'department' => $department, 'department' => $department,
'file' => '', 'file' => $this->uploadFile(),
'date' => Date::getCurrentDate(), 'date' => Date::getCurrentDate(),
'unread' => false, 'unread' => false,
'unreadStaff' => true, 'unreadStaff' => true,

View File

@ -37,4 +37,5 @@ class ERRORS {
const INVALID_BODY = 'INVALID_BODY'; const INVALID_BODY = 'INVALID_BODY';
const INVALID_PERIOD = 'INVALID_PERIOD'; const INVALID_PERIOD = 'INVALID_PERIOD';
const NAME_ALREADY_USED = 'NAME_ALREADY_USED'; const NAME_ALREADY_USED = 'NAME_ALREADY_USED';
const INVALID_FILE = 'INVALID_FILE';
} }

0
server/files/.gitkeep Normal file
View File

View File

@ -18,6 +18,10 @@ include_once 'libs/Hashing.php';
include_once 'libs/MailSender.php'; include_once 'libs/MailSender.php';
include_once 'libs/Date.php'; include_once 'libs/Date.php';
include_once 'libs/DataStoreList.php'; include_once 'libs/DataStoreList.php';
include_once 'libs/LinearCongruentialGenerator.php';
include_once 'libs/FileManager.php';
include_once 'libs/FileDownloader.php';
include_once 'libs/FileUploader.php';
// LOAD DATA // LOAD DATA
spl_autoload_register(function ($class) { spl_autoload_register(function ($class) {

View File

@ -60,4 +60,26 @@ abstract class Controller {
public static function getAppInstance() { public static function getAppInstance() {
return \Slim\Slim::getInstance(); return \Slim\Slim::getInstance();
} }
public function uploadFile() {
if(!isset($_FILES['file'])) return '';
$maxSize = Setting::getSetting('max-size')->getValue();
$fileGap = Setting::getSetting('file-gap')->getValue();
$fileFirst = Setting::getSetting('file-first-number')->getValue();
$fileQuantity = Setting::getSetting('file-quantity');
$fileUploader = FileUploader::getInstance();
$fileUploader->setMaxSize($maxSize);
$fileUploader->setGeneratorValues($fileGap, $fileFirst, $fileQuantity->getValue());
if($fileUploader->upload($_FILES['file'])) {
$fileQuantity->value++;
$fileQuantity->store();
return $fileUploader->getFileName();
} else {
throw new Exception(ERRORS::INVALID_FILE);
}
}
} }

View File

@ -0,0 +1,40 @@
<?php
class FileDownloader extends FileManager {
private static $instance = null;
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new FileDownloader();
}
return self::$instance;
}
private function __construct() {}
public function download() {
$fullFilePath = $this->getFullFilePath();
if(file_exists($fullFilePath) && is_file($fullFilePath)) {
header('Cache-control: private');
header('Content-Type: application/octet-stream');
header('Content-Length: '.filesize($fullFilePath));
header('Content-Disposition: filename='. $this->getFileName());
flush();
$file = fopen($fullFilePath, 'r');
print fread($file, filesize($fullFilePath));
fclose($file);
return true;
} else {
return false;
}
}
public function eraseFile() {
unlink($this->getLocalPath() . $this->getFileName());
}
}

View File

@ -0,0 +1,26 @@
<?php
abstract class FileManager {
private $fileName;
private $localPath = 'files/';
public function setLocalPath($localPath) {
$this->localPath = $localPath;
}
public function setFileName($fileName) {
$this->fileName = $fileName;
}
public function getLocalPath() {
return $this->localPath;
}
public function getFileName() {
return $this->fileName;
}
public function getFullFilePath() {
return $this->getLocalPath() . $this->getFileName();
}
}

View File

@ -1,5 +1,11 @@
<?php <?php
class FileUploader {
class FileUploader extends FileManager {
private $maxSize = 1024;
private $linearCongruentialGenerator;
private $linearCongruentialGeneratorOffset;
private $fileName;
private static $instance = null; private static $instance = null;
public static function getInstance() { public static function getInstance() {
@ -12,7 +18,44 @@ class FileUploader {
private function __construct() {} private function __construct() {}
public function upload() { public function upload($file) {
// TODO: Implement file upload features $this->setNewName($file['name']);
if($file['size'] > (1024 * $this->maxSize)) {
return false;
}
move_uploaded_file($file['tmp_name'], $this->getLocalPath() . $this->getFileName());
return true;
} }
private function setNewName($fileName) {
$newName = $fileName;
$newName = strtolower($newName);
$newName = preg_replace('/\s+/', '_', $newName);
if ($this->linearCongruentialGenerator instanceof LinearCongruentialGenerator) {
$newName = $this->linearCongruentialGenerator->generate($this->linearCongruentialGeneratorOffset) . '_' . $newName;
}
$this->fileName = $newName;
}
public function setGeneratorValues($gap, $first, $offset) {
$this->linearCongruentialGenerator = new LinearCongruentialGenerator();
$this->linearCongruentialGeneratorOffset = $offset;
$this->linearCongruentialGenerator->setGap($gap);
$this->linearCongruentialGenerator->setFirst($first);
}
public function setMaxSize($maxSize) {
$this->maxSize = $maxSize;
}
public function getFileName() {
return $this->fileName;
}
} }

View File

@ -7,10 +7,36 @@ class Hashing {
public static function verifyPassword($password, $hash) { public static function verifyPassword($password, $hash) {
return password_verify($password, $hash); return password_verify($password, $hash);
} }
public static function generateRandomToken() { public static function generateRandomToken() {
return md5(uniqid(rand())); return md5(uniqid(rand()));
} }
public static function getRandomTicketNumber($min,$max) {
return rand($min,$max); public static function generateRandomNumber($min, $max) {
return rand($min, $max);
}
public static function generateRandomPrime($min, $max) {
$number = Hashing::generateRandomNumber($min, $max);
while(!Hashing::isPrime($number)) {
$number = Hashing::generateRandomNumber($min, $max);
}
return $number;
}
public static function isPrime($number) {
$sqrt = sqrt($number);
$prime = true;
for($i = 0; $i < $sqrt; $i++) {
if($sqrt % 2 === 0) {
$prime = false;
break;
}
}
return $prime;
} }
} }

View File

@ -0,0 +1,31 @@
<?php
class LinearCongruentialGenerator {
private $gap;
private $first;
private $min = 100000;
private $max = 999999;
public function setRange($min, $max) {
$this->min = $min;
$this->max = $max;
}
public function setGap($gap) {
if(!Hashing::isPrime($gap)) throw new Exception('LinearCongruentialGenerator: gap must be prime');
$this->gap = $gap;
}
public function setFirst($first) {
$this->first = $first;
}
public function generate($offset) {
if($offset) return ($this->first - $this->min + $offset * $this->gap) % ($this->max - $this->min + 1) + $this->min;
else return $this->generateFirst();
}
public function generateFirst() {
return Hashing::generateRandomNumber($this->min, $this->max);
}
}

View File

@ -46,17 +46,16 @@ class Ticket extends DataStore {
} }
public function generateUniqueTicketNumber() { public function generateUniqueTicketNumber() {
$linearCongruentialGenerator = new LinearCongruentialGenerator();
$ticketQuantity = Ticket::count(); $ticketQuantity = Ticket::count();
$minValue = 100000;
$maxValue = 999999;
if ($ticketQuantity === 0) { if ($ticketQuantity === 0) {
$ticketNumber = Hashing::getRandomTicketNumber($minValue, $maxValue); $ticketNumber = $linearCongruentialGenerator->generateFirst();
} else { } else {
$firstTicketNumber = Ticket::getTicket(1)->ticketNumber; $linearCongruentialGenerator->setGap(Setting::getSetting('ticket-gap')->value);
$gap = 176611; //TODO: USE RANDOM PRIME INSTEAD $linearCongruentialGenerator->setFirst(Ticket::getTicket(1)->ticketNumber);
$ticketNumber = ($firstTicketNumber - $minValue + $ticketQuantity * $gap) % ($maxValue - $minValue + 1) + $minValue; $ticketNumber = $linearCongruentialGenerator->generate($ticketQuantity);
} }
return $ticketNumber; return $ticketNumber;

View File

@ -58,3 +58,4 @@ require './system/get-stats.rb'
require './system/add-api-key.rb' require './system/add-api-key.rb'
require './system/delete-api-key.rb' require './system/delete-api-key.rb'
require './system/get-all-keys.rb' require './system/get-all-keys.rb'
require './system/file-upload-download.rb'

View File

@ -1,5 +1,12 @@
$agent = Mechanize.new $agent = Mechanize.new
def plainRequest(path, data = {})
uri = 'http://localhost:8080' + path
response = $agent.post(uri, data)
return response
end
def request(path, data = {}) def request(path, data = {})
uri = 'http://localhost:8080' + path uri = 'http://localhost:8080' + path
response = $agent.post(uri, data) response = $agent.post(uri, data)

View File

@ -10,7 +10,7 @@ describe'system/edit-settings' do
"time-zone" => -3, "time-zone" => -3,
"layout" => 'full-width', "layout" => 'full-width',
"allow-attachments" => 1, "allow-attachments" => 1,
"max-size" => 2, "max-size" => 2048,
"language" => 'en', "language" => 'en',
"no-reply-email" => 'testemail@hotmail.com' "no-reply-email" => 'testemail@hotmail.com'
}) })
@ -27,7 +27,7 @@ describe'system/edit-settings' do
(row['value']).should.equal('full-width') (row['value']).should.equal('full-width')
row = $database.getRow('setting', 'max-size', 'name') row = $database.getRow('setting', 'max-size', 'name')
(row['value']).should.equal('2') (row['value']).should.equal('2048')
row = $database.getRow('setting', 'language', 'name') row = $database.getRow('setting', 'language', 'name')
(row['value']).should.equal('en') (row['value']).should.equal('en')

View File

@ -0,0 +1,74 @@
describe 'File Upload and Download' do
request('/user/logout')
Scripts.login('creator@os4.com', 'creator')
it 'should upload file when creating ticket' do
file = File.new('../server/files/upload.txt', 'w+')
file.puts('file content')
file.close
result = request('/ticket/create', {
'csrf_userid' => $csrf_userid,
'csrf_token' => $csrf_token,
'title' => 'Ticket with file',
'content' => 'this is a ticket that contains a file',
'language' => 'en',
'departmentId' => 1,
'file' => File.open( "../server/files/upload.txt")
})
(result['status']).should.equal('success')
ticket = $database.getLastRow('ticket')
(ticket['file'].include? 'upload.txt').should.equal(true)
(File.exist? ('../server/files/' + ticket['file'])).should.equal(true)
end
it 'should download file if author is logged' do
ticket = $database.getLastRow('ticket')
file = File.open("../server/files/" + ticket['file'])
result = plainRequest('/system/download', {
'csrf_userid' => $csrf_userid,
'csrf_token' => $csrf_token,
'file' => ticket['file']
})
(result.body).should.equal(file.read)
end
it 'should not download if author is not logged' do
request('/user/logout')
Scripts.login('staff@opensupports.com', 'staff', true)
ticket = $database.getLastRow('ticket')
result = plainRequest('/system/download', {
'csrf_userid' => $csrf_userid,
'csrf_token' => $csrf_token,
'file' => ticket['file']
})
(result.body).should.equal('')
end
it 'should download if owner is logged' do
ticket = $database.getLastRow('ticket')
file = File.open("../server/files/" + ticket['file'])
request('/staff/assign-ticket', {
'csrf_userid' => $csrf_userid,
'csrf_token' => $csrf_token,
'ticketNumber' => ticket['ticket_number']
})
result = plainRequest('/system/download', {
'csrf_userid' => $csrf_userid,
'csrf_token' => $csrf_token,
'file' => ticket['file']
})
(result.body).should.equal(file.read)
end
end

View File

@ -25,10 +25,10 @@ describe'/system/get-stats' do
$database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday3 + ", NULL, NULL);") $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday3 + ", NULL, NULL);")
end end
for i in 0..8 for i in 0..8
$database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, " + yesterday3 + ", NULL, NULL, 1);") $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, NULL, " + yesterday3 + ", NULL, NULL, 1);")
end end
for i in 0..4 for i in 0..4
$database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday3 + ", NULL, NULL, 1);") $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, NULL, " + yesterday3 + ", NULL, NULL, 1);")
end end
#day 2 #day 2
@ -45,10 +45,10 @@ describe'/system/get-stats' do
$database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday2 + ", NULL, NULL);") $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday2 + ", NULL, NULL);")
end end
for i in 0..10 for i in 0..10
$database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, " + yesterday2 + ", NULL, NULL, 1);") $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, NULL, " + yesterday2 + ", NULL, NULL, 1);")
end end
for i in 0..2 for i in 0..2
$database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday2 + ", NULL, NULL, 1);") $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, NULL, " + yesterday2 + ", NULL, NULL, 1);")
end end
#day 3 #day 3
@ -65,10 +65,10 @@ describe'/system/get-stats' do
$database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday + ", NULL, NULL);") $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday + ", NULL, NULL);")
end end
for i in 0..3 for i in 0..3
$database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, " + yesterday + ", NULL, NULL, 1);") $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, NULL, " + yesterday + ", NULL, NULL, 1);")
end end
for i in 0..7 for i in 0..7
$database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday + ", NULL, NULL, 1);") $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, NULL, " + yesterday + ", NULL, NULL, 1);")
end end
@result = request('/system/get-stats', { @result = request('/system/get-stats', {

View File

@ -147,13 +147,15 @@ describe '/ticket/create' do
csrf_token: $csrf_token csrf_token: $csrf_token
}) })
ticket_number_gap = $database.getRow('setting', 'ticket-gap', 'name')['value'].to_i
ticket0 = $database.getRow('ticket','Winter is coming','title')['ticket_number'].to_i ticket0 = $database.getRow('ticket','Winter is coming','title')['ticket_number'].to_i
ticket1 = $database.getRow('ticket','Winter is coming1','title')['ticket_number'].to_i ticket1 = $database.getRow('ticket','Winter is coming1','title')['ticket_number'].to_i
ticket2 = $database.getRow('ticket','Winter is coming2','title')['ticket_number'].to_i ticket2 = $database.getRow('ticket','Winter is coming2','title')['ticket_number'].to_i
ticket3 = $database.getRow('ticket','Winter is coming3','title')['ticket_number'].to_i ticket3 = $database.getRow('ticket','Winter is coming3','title')['ticket_number'].to_i
(ticket1).should.equal((ticket0 - 100000 + 1 * 176611) % 900000 + 100000) (ticket1).should.equal((ticket0 - 100000 + 1 * ticket_number_gap) % 900000 + 100000)
(ticket2).should.equal((ticket0 - 100000 + 2 * 176611) % 900000 + 100000) (ticket2).should.equal((ticket0 - 100000 + 2 * ticket_number_gap) % 900000 + 100000)
(ticket3).should.equal((ticket0 - 100000 + 3 * 176611) % 900000 + 100000) (ticket3).should.equal((ticket0 - 100000 + 3 * ticket_number_gap) % 900000 + 100000)
end end
end end