From b75660ec1d885675d99b8d6712f43f9157a06405 Mon Sep 17 00:00:00 2001 From: ivan <ivan@opensupports.com> Date: Tue, 10 Jan 2017 16:45:58 -0300 Subject: [PATCH 01/21] Max Red - Created first layout and functionality of ToggleList component [skip ci] --- client/src/app-components/toggle-list.js | 52 ++++++++++++++++++++++ client/src/app-components/toggle-list.scss | 20 +++++++++ 2 files changed, 72 insertions(+) create mode 100644 client/src/app-components/toggle-list.js create mode 100644 client/src/app-components/toggle-list.scss diff --git a/client/src/app-components/toggle-list.js b/client/src/app-components/toggle-list.js new file mode 100644 index 00000000..6df36dbe --- /dev/null +++ b/client/src/app-components/toggle-list.js @@ -0,0 +1,52 @@ +import React from 'react'; +import _ from 'lodash'; +import classNames from 'classnames'; + +class ToggleList extends React.Component { + static propTypes = { + items: React.PropTypes.arrayOf(React.PropTypes.shape({ + content: React.PropTypes.node + })) + }; + + state = { + selected: [1, 3] + }; + + render() { + return ( + <div className="toggle-list"> + {this.props.items.map(this.renderItem.bind(this))} + </div> + ); + } + + renderItem(obj, index) { + + return ( + <div className={this.getItemClass(index)} onClick={this.selectItem.bind(this, index)} key={index}> + {obj.content} + </div> + ); + } + + getItemClass(index) { + let classes = { + 'toggle-list__item': true, + 'toggle-list__first-item': (index === 0), + 'toggle-list__selected': (_.includes(this.state.selected, index)) + }; + + return classNames(classes); + } + + selectItem(index) { + let actual = _.clone(this.state.selected); + (_.includes(this.state.selected, index)) ? (_.remove(actual, t => t == index)) : (actual.push(index)); + this.setState({ + selected: actual + }); + } +} + +export default ToggleList; diff --git a/client/src/app-components/toggle-list.scss b/client/src/app-components/toggle-list.scss new file mode 100644 index 00000000..b9d73082 --- /dev/null +++ b/client/src/app-components/toggle-list.scss @@ -0,0 +1,20 @@ +@import "../scss/vars"; + +.toggle-list { + + &__item { + border: 1px $light-grey solid; + border-left: none; + width: 120px; + height: 120px; + display: inline-block; + } + + &__selected { + background-color: $light-grey; + } + + &__first-item { + border: 1px $light-grey solid; + } +} \ No newline at end of file From 6fc460d4b6c866fa909d3d4e023bb2bd118cfcc3 Mon Sep 17 00:00:00 2001 From: ivan <ivan@opensupports.com> Date: Tue, 10 Jan 2017 18:27:53 -0300 Subject: [PATCH 02/21] Max Red - Modified some things, I don't know which [skip ci] --- client/src/app-components/toggle-list.js | 16 ++++++++++++++-- client/src/app-components/toggle-list.scss | 5 +++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/client/src/app-components/toggle-list.js b/client/src/app-components/toggle-list.js index 6df36dbe..f90a26ae 100644 --- a/client/src/app-components/toggle-list.js +++ b/client/src/app-components/toggle-list.js @@ -6,7 +6,8 @@ class ToggleList extends React.Component { static propTypes = { items: React.PropTypes.arrayOf(React.PropTypes.shape({ content: React.PropTypes.node - })) + })), + onChange: React.PropTypes.func }; state = { @@ -42,10 +43,21 @@ class ToggleList extends React.Component { selectItem(index) { let actual = _.clone(this.state.selected); - (_.includes(this.state.selected, index)) ? (_.remove(actual, t => t == index)) : (actual.push(index)); + + _.includes(this.state.selected, index) ? _.remove(actual, t => t == index) : actual.push(index); + + console.log(actual); this.setState({ selected: actual }); + + if (this.props.onChange) { + this.props.onChange({ + target: { + value: actual + } + }); + } } } diff --git a/client/src/app-components/toggle-list.scss b/client/src/app-components/toggle-list.scss index b9d73082..7bf244cb 100644 --- a/client/src/app-components/toggle-list.scss +++ b/client/src/app-components/toggle-list.scss @@ -5,9 +5,10 @@ &__item { border: 1px $light-grey solid; border-left: none; - width: 120px; + width: 180px; height: 120px; display: inline-block; + transition: background-color 0.4s ease; } &__selected { @@ -17,4 +18,4 @@ &__first-item { border: 1px $light-grey solid; } -} \ No newline at end of file +} From 845939f08981154810ee5bcea042530ea03ac94d Mon Sep 17 00:00:00 2001 From: AntonyAntonio <guillermo@opensupports.com> Date: Wed, 11 Jan 2017 04:25:26 -0300 Subject: [PATCH 03/21] Guillermo - stats architecture [skip ci] --- server/controllers/system.php | 2 ++ server/controllers/system/edit-settings.php | 1 - server/controllers/system/get-stats.php | 34 +++++++++++++++++++++ server/controllers/system/init-settings.php | 3 +- server/models/Log.php | 9 ++++-- server/models/Staff.php | 3 +- server/models/Stat.php | 17 +++++++++++ 7 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 server/controllers/system/get-stats.php create mode 100644 server/models/Stat.php diff --git a/server/controllers/system.php b/server/controllers/system.php index 2cde4fe2..534f556e 100644 --- a/server/controllers/system.php +++ b/server/controllers/system.php @@ -9,6 +9,7 @@ require_once 'system/get-logs.php'; require_once 'system/get-mail-templates.php'; require_once 'system/edit-mail-template.php'; require_once 'system/recover-mail-template.php'; +require_once 'system/get-stats.php'; $systemControllerGroup = new ControllerGroup(); $systemControllerGroup->setGroupPath('/system'); @@ -23,5 +24,6 @@ $systemControllerGroup->addController(new GetLogsController); $systemControllerGroup->addController(new GetMailTemplatesController); $systemControllerGroup->addController(new EditMailTemplateController); $systemControllerGroup->addController(new RecoverMailTemplateController); +$systemControllerGroup->addController(new GetStatsController); $systemControllerGroup->finalize(); \ No newline at end of file diff --git a/server/controllers/system/edit-settings.php b/server/controllers/system/edit-settings.php index c78f1f97..22454e62 100644 --- a/server/controllers/system/edit-settings.php +++ b/server/controllers/system/edit-settings.php @@ -1,5 +1,4 @@ <?php -use Respect\Validation\Validator as DataValidator; class EditSettingsController extends Controller { const PATH = '/edit-settings'; diff --git a/server/controllers/system/get-stats.php b/server/controllers/system/get-stats.php new file mode 100644 index 00000000..d8349663 --- /dev/null +++ b/server/controllers/system/get-stats.php @@ -0,0 +1,34 @@ +<?php +class GetStatsController extends Controller { + const PATH = '/get-stats'; + + public function validations() { + return [ + 'permission' => 'staff_1', + 'requestData' => [] + ]; + } + + public function handler() { + $begin = new DateTime(Setting::getSetting('last-stat-day')->value); + $end = new DateTime(Date::getCurrentDate()); + + $interval = new DateInterval('P1D'); + $dateRange = new DatePeriod($begin, $interval ,$end); + + foreach($dateRange as $date){ + $numberOfTickets = Log::count('type=? AND date LIKE ?',['CREATE_TICKET', $date->format('Ymd') . '%']); + $stat = new Stat(); + + $stat->setProperties([ + 'date' => $date->format('Ymd'), + 'type' => 'CREATE_TICKET', + 'general' => 1, + 'value' => $numberOfTickets, + ]); + $stat->store(); + } + + Response::respondSuccess(); + } +} \ No newline at end of file diff --git a/server/controllers/system/init-settings.php b/server/controllers/system/init-settings.php index 98fce211..89842b5c 100644 --- a/server/controllers/system/init-settings.php +++ b/server/controllers/system/init-settings.php @@ -40,7 +40,8 @@ class InitSettingsController extends Controller { 'allow-attachments' => 0, 'max-size' => 0, 'title' => 'Support Center', - 'url' => 'http://www.opensupports.com/support' + 'url' => 'http://www.opensupports.com/support', + 'last-stat-day' => '20170101'//TODO: get current date ]); } diff --git a/server/models/Log.php b/server/models/Log.php index ebe24469..966f3b3a 100644 --- a/server/models/Log.php +++ b/server/models/Log.php @@ -9,7 +9,8 @@ class Log extends DataStore { 'type', 'authorUser', 'authorStaff', - 'to' + 'to', + 'date' ]; } @@ -22,7 +23,8 @@ class Log extends DataStore { $log->setProperties(array( 'type' => $type, - 'to' => $to + 'to' => $to, + 'date' => Date::getCurrentDate() )); if($author instanceof User) { @@ -44,7 +46,8 @@ class Log extends DataStore { 'name' => $author->name, 'id' => $author->id, 'staff' => $author instanceof Staff - ] + ], + 'date' => $this->date ]; } } \ No newline at end of file diff --git a/server/models/Staff.php b/server/models/Staff.php index cca0bc4c..9ec69341 100644 --- a/server/models/Staff.php +++ b/server/models/Staff.php @@ -18,7 +18,8 @@ class Staff extends DataStore { 'level', 'sharedDepartmentList', 'sharedTicketList', - 'lastLogin' + 'lastLogin', + 'sharedStatList' ]; } diff --git a/server/models/Stat.php b/server/models/Stat.php new file mode 100644 index 00000000..08e90572 --- /dev/null +++ b/server/models/Stat.php @@ -0,0 +1,17 @@ +<?php +class Stat extends DataStore { + const TABLE = 'stat'; + + public static function getProps() { + return array ( + 'date', + 'type', + 'general', + 'value' + ); + } + + public function getDefaultProps() { + return array(); + } +} \ No newline at end of file From 868d08ecb235622893bbf479af490a1817525e17 Mon Sep 17 00:00:00 2001 From: AntonyAntonio <guillermo@opensupports.com> Date: Thu, 12 Jan 2017 02:50:45 -0300 Subject: [PATCH 04/21] Guillermo - stats architecture [skip ci] --- server/controllers/system/get-stats.php | 138 +++++++++++++++++++++--- server/data/ERRORS.php | 1 + server/models/Staff.php | 5 +- server/models/Stat.php | 8 ++ 4 files changed, 134 insertions(+), 18 deletions(-) diff --git a/server/controllers/system/get-stats.php b/server/controllers/system/get-stats.php index d8349663..5c83c8d7 100644 --- a/server/controllers/system/get-stats.php +++ b/server/controllers/system/get-stats.php @@ -1,34 +1,140 @@ <?php +use Respect\Validation\Validator as DataValidator; + class GetStatsController extends Controller { const PATH = '/get-stats'; public function validations() { return [ 'permission' => 'staff_1', - 'requestData' => [] + 'requestData' => [ + 'period' => [ + 'validation' => DataValidator::in(['week', 'month', 'quarter', 'year']), + 'error' => ERRORS::INVALID_PERIOD + ] + ] ]; } public function handler() { - $begin = new DateTime(Setting::getSetting('last-stat-day')->value); - $end = new DateTime(Date::getCurrentDate()); + $this->generationNewStats(); - $interval = new DateInterval('P1D'); - $dateRange = new DatePeriod($begin, $interval ,$end); + $staffId = Controller::request('staffId'); - foreach($dateRange as $date){ - $numberOfTickets = Log::count('type=? AND date LIKE ?',['CREATE_TICKET', $date->format('Ymd') . '%']); - $stat = new Stat(); + if($staffId) { + if($staffId !== Controller::getLoggedUser()->id && !Controller::isStaffLogged(3)) { + Response::respondError(ERRORS::NO_PERMISSION); + return; + } - $stat->setProperties([ - 'date' => $date->format('Ymd'), - 'type' => 'CREATE_TICKET', - 'general' => 1, - 'value' => $numberOfTickets, - ]); - $stat->store(); + $this->getStaffStat(); + } else { + $this->getGeneralStat(); + } + } + + public function generationNewStats() { + $lastStatDay = Setting::getSetting('last-stat-day'); + $previousCurrentDate = floor(Date::getCurrentDate()/10000)-1; + + if($lastStatDay->value !== $previousCurrentDate) { + + $begin = new DateTime($lastStatDay->value); + $end = new DateTime($previousCurrentDate); + + $interval = new DateInterval('P1D'); + $dateRange = new DatePeriod($begin, $interval ,$end); + + $staffList = Staff::getAll(); + + foreach($dateRange as $date) { + $this->generateGeneralStat('CREATE_TICKET', $date); + $this->generateGeneralStat('CLOSE', $date); + $this->generateGeneralStat('SIGNUP', $date); + $this->generateGeneralStat('COMMENT', $date); + + foreach($staffList as $staff) { + $assignments = Ticketevent::count('type=? AND author_staff_id=? AND date LIKE ?',['ASSIGN',$staff->id, $date->format('Ymd') . '%']); + $closed = Ticketevent::count('type=? AND author_staff_id=? AND date LIKE ?',['CLOSE',$staff->id, $date->format('Ymd') . '%']); + + $statAssign = new Stat(); + $statAssign->setProperties([ + 'date' => $date->format('Ymd'), + 'type' => 'ASSIGN', + 'general' => 0, + 'value' => $assignments, + ]); + + $statClose = new Stat(); + $statClose->setProperties([ + 'date' => $date->format('Ymd'), + 'type' => 'CLOSE', + 'general' => 0, + 'value' => $closed, + ]); + + $staff->ownStatList->add($statAssign); + $staff->ownStatList->add($statClose); + + $staff->store(); + } + } + + $lastStatDay->value = $previousCurrentDate; + $lastStatDay->store(); + } + } + + public function generateGeneralStat($type, $date) { + $value = Log::count('type=? AND date LIKE ?',[$type, $date->format('Ymd') . '%']); + $stat = new Stat(); + + $stat->setProperties([ + 'date' => $date->format('Ymd'), + 'type' => $type, + 'general' => 1, + 'value' => $value, + ]); + + $stat->store(); + } + + public function getGeneralStat() { + $daysToRetrieve = $this->getDaysToRetrieve(); + + $statList = Stat::find('general=\'1\' ORDER BY id desc LIMIT ? ', [4 * $daysToRetrieve]); + + Response::respondSuccess($statList->toArray()); + } + + public function getStaffStat() { + $staffId = Controller::request('staffId'); + $daysToRetrieve = $this->getDaysToRetrieve(); + + $statList = Stat::find('general=\'0\' AND staff_id=? ORDER BY id desc LIMIT ? ', [$staffId, 4 * $daysToRetrieve]); + + Response::respondSuccess($statList->toArray()); + } + + public function getDaysToRetrieve() { + $period = Controller::request('period'); + $daysToRetrieve = 0; + + switch ($period) { + case 'week': + $daysToRetrieve = 7; + break; + case 'month': + $daysToRetrieve = 30; + break; + case 'quarter': + $daysToRetrieve = 90; + break; + case 'year': + $daysToRetrieve = 365; + break; } - Response::respondSuccess(); + return $daysToRetrieve; } } \ No newline at end of file diff --git a/server/data/ERRORS.php b/server/data/ERRORS.php index e0052543..4b7507cf 100644 --- a/server/data/ERRORS.php +++ b/server/data/ERRORS.php @@ -35,4 +35,5 @@ class ERRORS { const INVALID_TEMPLATE = 'INVALID_TEMPLATE'; const INVALID_SUBJECT = 'INVALID_SUBJECT'; const INVALID_BODY = 'INVALID_BODY'; + const INVALID_PERIOD = 'INVALID_PERIOD'; } diff --git a/server/models/Staff.php b/server/models/Staff.php index 9ec69341..13c3d7a8 100644 --- a/server/models/Staff.php +++ b/server/models/Staff.php @@ -19,13 +19,14 @@ class Staff extends DataStore { 'sharedDepartmentList', 'sharedTicketList', 'lastLogin', - 'sharedStatList' + 'ownStatList' ]; } public function getDefaultProps() { return [ - 'level' => 1 + 'level' => 1, + 'ownStatList' => new DataStoreList() ]; } diff --git a/server/models/Stat.php b/server/models/Stat.php index 08e90572..97f85649 100644 --- a/server/models/Stat.php +++ b/server/models/Stat.php @@ -14,4 +14,12 @@ class Stat extends DataStore { public function getDefaultProps() { return array(); } + public function toArray() { + return [ + 'date' => $this->date, + 'type' => $this->type, + 'general' => $this->general, + 'value' => $this->value + ]; + } } \ No newline at end of file From be4999009184e8c88f2d106ecf86d8d75e930ecd Mon Sep 17 00:00:00 2001 From: ivan <ivan@opensupports.com> Date: Thu, 12 Jan 2017 03:02:56 -0300 Subject: [PATCH 05/21] Ivan - Add files folder [skip ci] --- server/files/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 server/files/.gitkeep diff --git a/server/files/.gitkeep b/server/files/.gitkeep new file mode 100644 index 00000000..e69de29b From b8d24b601dc2986dec74034a6d8cb59ccf0137b7 Mon Sep 17 00:00:00 2001 From: AntonyAntonio <guillermo@opensupports.com> Date: Thu, 12 Jan 2017 16:30:58 -0300 Subject: [PATCH 06/21] Guillermo - stats architecture [skip ci] --- server/controllers/system/get-stats.php | 7 +- server/libs/Date.php | 4 + tests/init.rb | 2 + tests/libs.rb | 4 + tests/system/get-stats.rb | 250 ++++++++++++++++++++++++ 5 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 tests/system/get-stats.rb diff --git a/server/controllers/system/get-stats.php b/server/controllers/system/get-stats.php index 5c83c8d7..ff1fdf3a 100644 --- a/server/controllers/system/get-stats.php +++ b/server/controllers/system/get-stats.php @@ -35,12 +35,13 @@ class GetStatsController extends Controller { public function generationNewStats() { $lastStatDay = Setting::getSetting('last-stat-day'); - $previousCurrentDate = floor(Date::getCurrentDate()/10000)-1; + $previousCurrentDate = floor(Date::getPreviousDate() / 10000); + $currentDate = floor(Date::getCurrentDate() / 10000); if($lastStatDay->value !== $previousCurrentDate) { $begin = new DateTime($lastStatDay->value); - $end = new DateTime($previousCurrentDate); + $end = new DateTime($currentDate); $interval = new DateInterval('P1D'); $dateRange = new DatePeriod($begin, $interval ,$end); @@ -80,7 +81,7 @@ class GetStatsController extends Controller { } } - $lastStatDay->value = $previousCurrentDate; + $lastStatDay->value = $currentDate; $lastStatDay->store(); } } diff --git a/server/libs/Date.php b/server/libs/Date.php index c3e8ba0c..6921789f 100644 --- a/server/libs/Date.php +++ b/server/libs/Date.php @@ -3,4 +3,8 @@ class Date { public static function getCurrentDate() { return date('YmdHi'); } + + public static function getPreviousDate() { + return date('YmdHi', strtotime(' -1 day ')); + } } diff --git a/tests/init.rb b/tests/init.rb index 3eb51877..b017ab09 100644 --- a/tests/init.rb +++ b/tests/init.rb @@ -5,6 +5,7 @@ require 'uri' require 'mysql' require 'json' require 'mechanize' +require 'date' require './libs.rb' require './scripts.rb' @@ -51,3 +52,4 @@ require './staff/last-events.rb' require './system/get-mail-templates.rb' require './system/edit-mail-template.rb' require './system/recover-mail-template.rb' +require './system/get-stats.rb' diff --git a/tests/libs.rb b/tests/libs.rb index f5496d48..8b832429 100644 --- a/tests/libs.rb +++ b/tests/libs.rb @@ -29,6 +29,10 @@ class Database return queryResponse.fetch_hash end + + def query(query_string) + return @connection.query(query_string); + end end $database = Database.new diff --git a/tests/system/get-stats.rb b/tests/system/get-stats.rb new file mode 100644 index 00000000..28f79541 --- /dev/null +++ b/tests/system/get-stats.rb @@ -0,0 +1,250 @@ +describe'/system/get-stats' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + it 'should get stats' do + + d = Date.today.prev_day + yesterday = d.strftime("%Y%m%d%H%M") + d = Date.today.prev_day.prev_day + yesterday2 = d.strftime("%Y%m%d%H%M") + d = Date.today.prev_day.prev_day.prev_day + yesterday3 = d.strftime("%Y%m%d%H%M") + + #day 1 + for i in 0..5 + $database.query("INSERT INTO log VALUES('', 'SIGNUP', NULL, " + yesterday3 + ", NULL, NULL);") + end + for i in 0..0 + $database.query("INSERT INTO log VALUES('', 'CREATE_TICKET', NULL, " + yesterday3 + ", NULL, NULL);") + end + for i in 0..1 + $database.query("INSERT INTO log VALUES('', 'CLOSE', NULL, " + yesterday3 + ", NULL, NULL);") + end + for i in 0..2 + $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday3 + ", NULL, NULL);") + end + for i in 0..8 + $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, " + yesterday3 + ", NULL, NULL, 1);") + end + for i in 0..4 + $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday3 + ", NULL, NULL, 1);") + end + + #day 2 + for i in 0..7 + $database.query("INSERT INTO log VALUES('', 'SIGNUP', NULL, " + yesterday2 + ", NULL, NULL);") + end + for i in 0..2 + $database.query("INSERT INTO log VALUES('', 'CREATE_TICKET', NULL, " + yesterday2 + ", NULL, NULL);") + end + for i in 0..9 + $database.query("INSERT INTO log VALUES('', 'CLOSE', NULL, " + yesterday2 + ", NULL, NULL);") + end + for i in 0..2 + $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday2 + ", NULL, NULL);") + end + for i in 0..10 + $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, " + yesterday2 + ", NULL, NULL, 1);") + end + for i in 0..2 + $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday2 + ", NULL, NULL, 1);") + end + + #day 3 + for i in 0..0 + $database.query("INSERT INTO log VALUES('', 'SIGNUP', NULL, " + yesterday + ", NULL, NULL);") + end + for i in 0..1 + $database.query("INSERT INTO log VALUES('', 'CREATE_TICKET', NULL, " + yesterday + ", NULL, NULL);") + end + for i in 0..4 + $database.query("INSERT INTO log VALUES('', 'CLOSE', NULL, " + yesterday + ", NULL, NULL);") + end + for i in 0..7 + $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday + ", NULL, NULL);") + end + for i in 0..3 + $database.query("INSERT INTO ticketevent VALUES('', 'CLOSE', NULL, " + yesterday + ", NULL, NULL, 1);") + end + for i in 0..7 + $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday + ", NULL, NULL, 1);") + end + + result= request('/system/get-stats', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + period: 'week' + }) + + (result['status']).should.equal('success') + + row = $database.getRow('stat', 65, 'id') + + (row['date']).should.equal('20170109') + (row['type']).should.equal('CREATE_TICKET') + (row['general']).should.equal('1') + (row['value']).should.equal('1') + + row = $database.getRow('stat', 66, 'id') + + (row['date']).should.equal('20170109') + (row['type']).should.equal('CLOSE') + (row['general']).should.equal('1') + (row['value']).should.equal('2') + + row = $database.getRow('stat', 67, 'id') + + (row['date']).should.equal('20170109') + (row['type']).should.equal('SIGNUP') + (row['general']).should.equal('1') + (row['value']).should.equal('6') + + row = $database.getRow('stat', 68, 'id') + + (row['date']).should.equal('20170109') + (row['type']).should.equal('COMMENT') + (row['general']).should.equal('1') + (row['value']).should.equal('3') + + row = $database.getRow('stat', 69, 'id') + + (row['date']).should.equal('20170109') + (row['type']).should.equal('ASSIGN') + (row['general']).should.equal('0') + (row['value']).should.equal('5') + + row = $database.getRow('stat', 70, 'id') + + (row['date']).should.equal('20170109') + (row['type']).should.equal('CLOSE') + (row['general']).should.equal('0') + (row['value']).should.equal('9') + + row = $database.getRow('stat', 71, 'id') + + (row['date']).should.equal('20170109') + (row['type']).should.equal('ASSIGN') + (row['general']).should.equal('0') + (row['value']).should.equal('0') + + row = $database.getRow('stat', 72, 'id') + + (row['date']).should.equal('20170109') + (row['type']).should.equal('CLOSE') + (row['general']).should.equal('0') + (row['value']).should.equal('0') + + row = $database.getRow('stat', 73, 'id') + + (row['date']).should.equal('20170110') + (row['type']).should.equal('CREATE_TICKET') + (row['general']).should.equal('1') + (row['value']).should.equal('3') + + row = $database.getRow('stat', 74, 'id') + + (row['date']).should.equal('20170110') + (row['type']).should.equal('CLOSE') + (row['general']).should.equal('1') + (row['value']).should.equal('10') + + row = $database.getRow('stat', 75, 'id') + + (row['date']).should.equal('20170110') + (row['type']).should.equal('SIGNUP') + (row['general']).should.equal('1') + (row['value']).should.equal('8') + + row = $database.getRow('stat', 76, 'id') + + (row['date']).should.equal('20170110') + (row['type']).should.equal('COMMENT') + (row['general']).should.equal('1') + (row['value']).should.equal('3') + + row = $database.getRow('stat', 77, 'id') + + (row['date']).should.equal('20170110') + (row['type']).should.equal('ASSIGN') + (row['general']).should.equal('0') + (row['value']).should.equal('3') + + row = $database.getRow('stat', 78, 'id') + + (row['date']).should.equal('20170110') + (row['type']).should.equal('CLOSE') + (row['general']).should.equal('0') + (row['value']).should.equal('11') + + row = $database.getRow('stat', 79, 'id') + + (row['date']).should.equal('20170110') + (row['type']).should.equal('ASSIGN') + (row['general']).should.equal('0') + (row['value']).should.equal('0') + + row = $database.getRow('stat', 80, 'id') + + (row['date']).should.equal('20170110') + (row['type']).should.equal('CLOSE') + (row['general']).should.equal('0') + (row['value']).should.equal('0') + + row = $database.getRow('stat', 81, 'id') + + (row['date']).should.equal('20170111') + (row['type']).should.equal('CREATE_TICKET') + (row['general']).should.equal('1') + (row['value']).should.equal('2') + + row = $database.getRow('stat', 82, 'id') + + (row['date']).should.equal('20170111') + (row['type']).should.equal('CLOSE') + (row['general']).should.equal('1') + (row['value']).should.equal('5') + + row = $database.getRow('stat', 83, 'id') + + (row['date']).should.equal('20170111') + (row['type']).should.equal('SIGNUP') + (row['general']).should.equal('1') + (row['value']).should.equal('1') + + row = $database.getRow('stat', 84, 'id') + + (row['date']).should.equal('20170111') + (row['type']).should.equal('COMMENT') + (row['general']).should.equal('1') + (row['value']).should.equal('8') + + row = $database.getRow('stat', 85, 'id') + + (row['date']).should.equal('20170111') + (row['type']).should.equal('ASSIGN') + (row['general']).should.equal('0') + (row['value']).should.equal('8') + + row = $database.getRow('stat', 86, 'id') + + (row['date']).should.equal('20170111') + (row['type']).should.equal('CLOSE') + (row['general']).should.equal('0') + (row['value']).should.equal('4') + + row = $database.getRow('stat', 87, 'id') + + (row['date']).should.equal('20170111') + (row['type']).should.equal('ASSIGN') + (row['general']).should.equal('0') + (row['value']).should.equal('0') + + row = $database.getRow('stat', 88, 'id') + + (row['date']).should.equal('20170111') + (row['type']).should.equal('CLOSE') + (row['general']).should.equal('0') + (row['value']).should.equal('0') + end +end From 134540fed957efa2e6e03fb216fbfcb48691f1f0 Mon Sep 17 00:00:00 2001 From: ivan <ivan@opensupports.com> Date: Thu, 12 Jan 2017 16:45:01 -0300 Subject: [PATCH 07/21] Ivan - Add Database Backup, FileDownloader, FileUploader and LinearCongruentialGenerator classes [skip ci] --- server/composer.json | 3 +- server/controllers/system.php | 4 ++ server/controllers/system/backup-database.php | 30 +++++++++++++ server/controllers/system/download.php | 24 ++++++++++ server/controllers/system/init-settings.php | 5 ++- server/data/ERRORS.php | 1 + server/index.php | 4 ++ server/libs/FileDownloader.php | 40 +++++++++++++++++ server/libs/FileManager.php | 26 +++++++++++ server/libs/FileUploader.php | 44 +++++++++++++++++-- server/libs/Hashing.php | 30 ++++++++++++- server/libs/LinearCongruentialGenerator.php | 30 +++++++++++++ server/models/Ticket.php | 15 +++---- tests/ticket/create.rb | 8 ++-- 14 files changed, 246 insertions(+), 18 deletions(-) create mode 100644 server/controllers/system/backup-database.php create mode 100644 server/controllers/system/download.php create mode 100644 server/libs/FileDownloader.php create mode 100644 server/libs/FileManager.php create mode 100644 server/libs/LinearCongruentialGenerator.php diff --git a/server/composer.json b/server/composer.json index 71e09ba2..a506cdee 100644 --- a/server/composer.json +++ b/server/composer.json @@ -4,7 +4,8 @@ "respect/validation": "^1.1", "phpmailer/phpmailer": "^5.2", "google/recaptcha": "~1.1", - "gabordemooij/redbean": "^4.3" + "gabordemooij/redbean": "^4.3", + "ifsnop/mysqldump-php": "2.*" }, "require-dev": { "phpunit/phpunit": "5.0.*" diff --git a/server/controllers/system.php b/server/controllers/system.php index 2cde4fe2..e66ecc5d 100644 --- a/server/controllers/system.php +++ b/server/controllers/system.php @@ -9,6 +9,8 @@ require_once 'system/get-logs.php'; require_once 'system/get-mail-templates.php'; require_once 'system/edit-mail-template.php'; require_once 'system/recover-mail-template.php'; +require_once 'system/backup-database.php'; +require_once 'system/download.php'; $systemControllerGroup = new ControllerGroup(); $systemControllerGroup->setGroupPath('/system'); @@ -23,5 +25,7 @@ $systemControllerGroup->addController(new GetLogsController); $systemControllerGroup->addController(new GetMailTemplatesController); $systemControllerGroup->addController(new EditMailTemplateController); $systemControllerGroup->addController(new RecoverMailTemplateController); +$systemControllerGroup->addController(new BackupDatabaseController); +$systemControllerGroup->addController(new DownloadController); $systemControllerGroup->finalize(); \ No newline at end of file diff --git a/server/controllers/system/backup-database.php b/server/controllers/system/backup-database.php new file mode 100644 index 00000000..82b1fccf --- /dev/null +++ b/server/controllers/system/backup-database.php @@ -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(); + } + } +} \ No newline at end of file diff --git a/server/controllers/system/download.php b/server/controllers/system/download.php new file mode 100644 index 00000000..0b481bf8 --- /dev/null +++ b/server/controllers/system/download.php @@ -0,0 +1,24 @@ +<?php +use Ifsnop\Mysqldump as IMysqldump; +use Respect\Validation\Validator as DataValidator; + +class DownloadController extends Controller { + const PATH = '/download'; + + public function validations() { + return [ + 'permission' => 'staff_1', + 'requestData' => [ + 'file' => [ + 'validation' => DataValidator::alnum('_.')->noWhitespace() + ] + ] + ]; + } + + public function handler() { + $fileDownloader = FileDownloader::getInstance(); + $fileDownloader->setFileName(Controller::request('file')); + $fileDownloader->download(); + } +} \ No newline at end of file diff --git a/server/controllers/system/init-settings.php b/server/controllers/system/init-settings.php index 98fce211..6613b17a 100644 --- a/server/controllers/system/init-settings.php +++ b/server/controllers/system/init-settings.php @@ -40,7 +40,10 @@ class InitSettingsController extends Controller { 'allow-attachments' => 0, 'max-size' => 0, 'title' => 'Support Center', - 'url' => 'http://www.opensupports.com/support' + 'url' => 'http://www.opensupports.com/support', + 'ticket-gap' => Hashing::generateRandomPrime(100000, 999999), + 'file-gap' => Hashing::generateRandomPrime(100000, 999999), + 'file-first-number' => Hashing::generateRandomNumber(100000, 999999), ]); } diff --git a/server/data/ERRORS.php b/server/data/ERRORS.php index e0052543..bdd16d22 100644 --- a/server/data/ERRORS.php +++ b/server/data/ERRORS.php @@ -35,4 +35,5 @@ class ERRORS { const INVALID_TEMPLATE = 'INVALID_TEMPLATE'; const INVALID_SUBJECT = 'INVALID_SUBJECT'; const INVALID_BODY = 'INVALID_BODY'; + const INVALID_FILE = 'INVALID_FILE'; } diff --git a/server/index.php b/server/index.php index a9056e1e..d8508ba3 100644 --- a/server/index.php +++ b/server/index.php @@ -18,6 +18,10 @@ include_once 'libs/Hashing.php'; include_once 'libs/MailSender.php'; include_once 'libs/Date.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 spl_autoload_register(function ($class) { diff --git a/server/libs/FileDownloader.php b/server/libs/FileDownloader.php new file mode 100644 index 00000000..6f49ca64 --- /dev/null +++ b/server/libs/FileDownloader.php @@ -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()); + } +} \ No newline at end of file diff --git a/server/libs/FileManager.php b/server/libs/FileManager.php new file mode 100644 index 00000000..fd05bb13 --- /dev/null +++ b/server/libs/FileManager.php @@ -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(); + } +} \ No newline at end of file diff --git a/server/libs/FileUploader.php b/server/libs/FileUploader.php index 55eb9bbc..dfc9c09b 100644 --- a/server/libs/FileUploader.php +++ b/server/libs/FileUploader.php @@ -1,5 +1,10 @@ <?php -class FileUploader { + +class FileUploader extends FileManager { + private $maxSize = 1024; + private $linearCongruentialGenerator; + private $linearCongruentialGeneratorOffset; + private static $instance = null; public static function getInstance() { @@ -12,7 +17,40 @@ class FileUploader { private function __construct() {} - public function upload() { - // TODO: Implement file upload features + public function upload($file) { + $newFileName = $this->generateNewName($file['name']); + + if($file['size'] > (1024 * $this->maxSize)) { + return false; + } + + move_uploaded_file($file['tmp_name'], $this->getLocalPath() . $newFileName); + + return true; } + + private function generateNewName($fileName) { + $newName = $fileName; + $newName = strtolower($newName); + $newName = preg_replace('/\s+/', '_', $newName); + + if ($this->linearCongruentialGenerator instanceof LinearCongruentialGenerator) { + $newName = $this->linearCongruentialGenerator->generate($this->linearCongruentialGeneratorOffset) . '_' . $newName; + } + + return $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; + } + } \ No newline at end of file diff --git a/server/libs/Hashing.php b/server/libs/Hashing.php index aa402cf5..da076181 100644 --- a/server/libs/Hashing.php +++ b/server/libs/Hashing.php @@ -7,10 +7,36 @@ class Hashing { public static function verifyPassword($password, $hash) { return password_verify($password, $hash); } + public static function generateRandomToken() { 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; } } \ No newline at end of file diff --git a/server/libs/LinearCongruentialGenerator.php b/server/libs/LinearCongruentialGenerator.php new file mode 100644 index 00000000..aafddb16 --- /dev/null +++ b/server/libs/LinearCongruentialGenerator.php @@ -0,0 +1,30 @@ +<?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) { + return ($this->first - $this->min + $offset * $this->gap) % ($this->max - $this->min + 1) + $this->min; + } + + public function generateFirst() { + return Hashing::generateRandomNumber($this->min, $this->max); + } +} \ No newline at end of file diff --git a/server/models/Ticket.php b/server/models/Ticket.php index 315434e8..88351ea6 100644 --- a/server/models/Ticket.php +++ b/server/models/Ticket.php @@ -46,17 +46,16 @@ class Ticket extends DataStore { } public function generateUniqueTicketNumber() { + $linearCongruentialGenerator = new LinearCongruentialGenerator(); $ticketQuantity = Ticket::count(); - $minValue = 100000; - $maxValue = 999999; - + if ($ticketQuantity === 0) { - $ticketNumber = Hashing::getRandomTicketNumber($minValue, $maxValue); + $ticketNumber = $linearCongruentialGenerator->generateFirst(); } else { - $firstTicketNumber = Ticket::getTicket(1)->ticketNumber; - $gap = 176611; //TODO: USE RANDOM PRIME INSTEAD - - $ticketNumber = ($firstTicketNumber - $minValue + $ticketQuantity * $gap) % ($maxValue - $minValue + 1) + $minValue; + $linearCongruentialGenerator->setGap(Setting::getSetting('ticket-gap')->value); + $linearCongruentialGenerator->setFirst(Ticket::getTicket(1)->ticketNumber); + + $ticketNumber = $linearCongruentialGenerator->generate($ticketQuantity); } return $ticketNumber; diff --git a/tests/ticket/create.rb b/tests/ticket/create.rb index 481e0b50..fbc6be44 100644 --- a/tests/ticket/create.rb +++ b/tests/ticket/create.rb @@ -147,13 +147,15 @@ describe '/ticket/create' do 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 ticket1 = $database.getRow('ticket','Winter is coming1','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 - (ticket1).should.equal((ticket0 - 100000 + 1 * 176611) % 900000 + 100000) - (ticket2).should.equal((ticket0 - 100000 + 2 * 176611) % 900000 + 100000) - (ticket3).should.equal((ticket0 - 100000 + 3 * 176611) % 900000 + 100000) + (ticket1).should.equal((ticket0 - 100000 + 1 * ticket_number_gap) % 900000 + 100000) + (ticket2).should.equal((ticket0 - 100000 + 2 * ticket_number_gap) % 900000 + 100000) + (ticket3).should.equal((ticket0 - 100000 + 3 * ticket_number_gap) % 900000 + 100000) end end From 1d514dda580be4efe78983c4ca0629347f00330c Mon Sep 17 00:00:00 2001 From: AntonyAntonio <guillermo@opensupports.com> Date: Thu, 12 Jan 2017 17:06:41 -0300 Subject: [PATCH 08/21] Guillermo - path-delete-all-users [skip ci] --- server/controllers/system.php | 2 ++ .../controllers/system/delete-all-users.php | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 server/controllers/system/delete-all-users.php diff --git a/server/controllers/system.php b/server/controllers/system.php index e5423a5a..4a908bc1 100644 --- a/server/controllers/system.php +++ b/server/controllers/system.php @@ -11,6 +11,7 @@ require_once 'system/edit-mail-template.php'; require_once 'system/recover-mail-template.php'; require_once 'system/disable-registration.php'; require_once 'system/enable-registration.php'; +require_once 'system/delete-all-users.php'; $systemControllerGroup = new ControllerGroup(); $systemControllerGroup->setGroupPath('/system'); @@ -27,5 +28,6 @@ $systemControllerGroup->addController(new EditMailTemplateController); $systemControllerGroup->addController(new RecoverMailTemplateController); $systemControllerGroup->addController(new DisableRegistrationController); $systemControllerGroup->addController(new EnableRegistrationController); +$systemControllerGroup->addController(new DeleteAllUsersController); $systemControllerGroup->finalize(); \ No newline at end of file diff --git a/server/controllers/system/delete-all-users.php b/server/controllers/system/delete-all-users.php new file mode 100644 index 00000000..63749e59 --- /dev/null +++ b/server/controllers/system/delete-all-users.php @@ -0,0 +1,32 @@ +<?php +use RedBeanPHP\Facade as RedBean; + +class DeleteAllUsersController extends Controller { + const PATH = '/delete-all-users'; + + public function validations() { + return [ + 'permission' => 'staff_3', + 'requestData' => [] + ]; + } + + public function handler() { + $password = Controller::request('password'); + + if(!Hashing::verifyPassword($password, Controller::getLoggedUser()->password)) { + Response::respondError(ERRORS::INVALID_PASSWORD); + return; + } + + Redbean::exec('SET FOREIGN_KEY_CHECKS = 0;'); + RedBean::wipe(SessionCookie::TABLE); + RedBean::wipe(User::TABLE); + RedBean::wipe(Ticket::TABLE); + RedBean::wipe(Ticketevent::TABLE); + RedBean::wipe('ticket_user'); + Redbean::exec('SET FOREIGN_KEY_CHECKS = 1;'); + + Response::respondSuccess(); + } +} \ No newline at end of file From 5f5a2be76beae00f7d93b2b3b26648be9cbc3228 Mon Sep 17 00:00:00 2001 From: AntonyAntonio <guillermo@opensupports.com> Date: Thu, 12 Jan 2017 18:26:06 -0300 Subject: [PATCH 09/21] Guillermo - stast architecture [skip ci] --- tests/system/get-stats.rb | 205 +++++++++----------------------------- 1 file changed, 46 insertions(+), 159 deletions(-) diff --git a/tests/system/get-stats.rb b/tests/system/get-stats.rb index 28f79541..a86531f0 100644 --- a/tests/system/get-stats.rb +++ b/tests/system/get-stats.rb @@ -71,180 +71,67 @@ describe'/system/get-stats' do $database.query("INSERT INTO ticketevent VALUES('', 'ASSIGN', NULL, " + yesterday + ", NULL, NULL, 1);") end - result= request('/system/get-stats', { + @result = request('/system/get-stats', { csrf_userid: $csrf_userid, csrf_token: $csrf_token, period: 'week' }) - (result['status']).should.equal('success') + def assertData(position, date, type, value) + (@result['data'][position]['date']).should.equal(date) + (@result['data'][position]['type']).should.equal(type) + (@result['data'][position]['value']).should.equal(value) + end - row = $database.getRow('stat', 65, 'id') + assertData(11, '20170109', 'CREATE_TICKET', '1') + assertData(10, '20170109', 'CLOSE', '2') + assertData(9, '20170109', 'SIGNUP', '6') + assertData(8, '20170109', 'COMMENT', '3') - (row['date']).should.equal('20170109') - (row['type']).should.equal('CREATE_TICKET') - (row['general']).should.equal('1') - (row['value']).should.equal('1') - row = $database.getRow('stat', 66, 'id') + assertData(7, '20170110', 'CREATE_TICKET', '3') + assertData(6, '20170110', 'CLOSE', '10') + assertData(5, '20170110', 'SIGNUP', '8') + assertData(4, '20170110', 'COMMENT', '3') - (row['date']).should.equal('20170109') - (row['type']).should.equal('CLOSE') - (row['general']).should.equal('1') - (row['value']).should.equal('2') + assertData(3, '20170111', 'CREATE_TICKET', '2') + assertData(2, '20170111', 'CLOSE', '5') + assertData(1, '20170111', 'SIGNUP', '1') + assertData(0, '20170111', 'COMMENT', '8') - row = $database.getRow('stat', 67, 'id') - (row['date']).should.equal('20170109') - (row['type']).should.equal('SIGNUP') - (row['general']).should.equal('1') - (row['value']).should.equal('6') + @result = request('/system/get-stats', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + period: 'week', + staffId: '1' + }) + assertData(0, '20170111', 'CLOSE', '4') + assertData(1, '20170111', 'ASSIGN', '8') + assertData(2, '20170110', 'CLOSE', '11') + assertData(3, '20170110', 'ASSIGN', '3') - row = $database.getRow('stat', 68, 'id') + assertData(4, '20170109', 'CLOSE', '9') + assertData(5, '20170109', 'ASSIGN', '5') + assertData(6, '20170108', 'CLOSE', '0') + assertData(7, '20170108', 'ASSIGN', '0') - (row['date']).should.equal('20170109') - (row['type']).should.equal('COMMENT') - (row['general']).should.equal('1') - (row['value']).should.equal('3') + assertData(8, '20170107', 'CLOSE', '0') + assertData(9, '20170107', 'ASSIGN', '0') + assertData(10, '20170106', 'CLOSE', '0') + assertData(11, '20170106', 'ASSIGN', '0') - row = $database.getRow('stat', 69, 'id') + assertData(12, '20170105', 'CLOSE', '0') + assertData(13, '20170105', 'ASSIGN', '0') + assertData(14, '20170104', 'CLOSE', '0') + assertData(15, '20170104', 'ASSIGN', '0') - (row['date']).should.equal('20170109') - (row['type']).should.equal('ASSIGN') - (row['general']).should.equal('0') - (row['value']).should.equal('5') + assertData(16, '20170103', 'CLOSE', '0') + assertData(17, '20170103', 'ASSIGN', '0') + assertData(18, '20170102', 'CLOSE', '0') + assertData(19, '20170102', 'ASSIGN', '0') - row = $database.getRow('stat', 70, 'id') - - (row['date']).should.equal('20170109') - (row['type']).should.equal('CLOSE') - (row['general']).should.equal('0') - (row['value']).should.equal('9') - - row = $database.getRow('stat', 71, 'id') - - (row['date']).should.equal('20170109') - (row['type']).should.equal('ASSIGN') - (row['general']).should.equal('0') - (row['value']).should.equal('0') - - row = $database.getRow('stat', 72, 'id') - - (row['date']).should.equal('20170109') - (row['type']).should.equal('CLOSE') - (row['general']).should.equal('0') - (row['value']).should.equal('0') - - row = $database.getRow('stat', 73, 'id') - - (row['date']).should.equal('20170110') - (row['type']).should.equal('CREATE_TICKET') - (row['general']).should.equal('1') - (row['value']).should.equal('3') - - row = $database.getRow('stat', 74, 'id') - - (row['date']).should.equal('20170110') - (row['type']).should.equal('CLOSE') - (row['general']).should.equal('1') - (row['value']).should.equal('10') - - row = $database.getRow('stat', 75, 'id') - - (row['date']).should.equal('20170110') - (row['type']).should.equal('SIGNUP') - (row['general']).should.equal('1') - (row['value']).should.equal('8') - - row = $database.getRow('stat', 76, 'id') - - (row['date']).should.equal('20170110') - (row['type']).should.equal('COMMENT') - (row['general']).should.equal('1') - (row['value']).should.equal('3') - - row = $database.getRow('stat', 77, 'id') - - (row['date']).should.equal('20170110') - (row['type']).should.equal('ASSIGN') - (row['general']).should.equal('0') - (row['value']).should.equal('3') - - row = $database.getRow('stat', 78, 'id') - - (row['date']).should.equal('20170110') - (row['type']).should.equal('CLOSE') - (row['general']).should.equal('0') - (row['value']).should.equal('11') - - row = $database.getRow('stat', 79, 'id') - - (row['date']).should.equal('20170110') - (row['type']).should.equal('ASSIGN') - (row['general']).should.equal('0') - (row['value']).should.equal('0') - - row = $database.getRow('stat', 80, 'id') - - (row['date']).should.equal('20170110') - (row['type']).should.equal('CLOSE') - (row['general']).should.equal('0') - (row['value']).should.equal('0') - - row = $database.getRow('stat', 81, 'id') - - (row['date']).should.equal('20170111') - (row['type']).should.equal('CREATE_TICKET') - (row['general']).should.equal('1') - (row['value']).should.equal('2') - - row = $database.getRow('stat', 82, 'id') - - (row['date']).should.equal('20170111') - (row['type']).should.equal('CLOSE') - (row['general']).should.equal('1') - (row['value']).should.equal('5') - - row = $database.getRow('stat', 83, 'id') - - (row['date']).should.equal('20170111') - (row['type']).should.equal('SIGNUP') - (row['general']).should.equal('1') - (row['value']).should.equal('1') - - row = $database.getRow('stat', 84, 'id') - - (row['date']).should.equal('20170111') - (row['type']).should.equal('COMMENT') - (row['general']).should.equal('1') - (row['value']).should.equal('8') - - row = $database.getRow('stat', 85, 'id') - - (row['date']).should.equal('20170111') - (row['type']).should.equal('ASSIGN') - (row['general']).should.equal('0') - (row['value']).should.equal('8') - - row = $database.getRow('stat', 86, 'id') - - (row['date']).should.equal('20170111') - (row['type']).should.equal('CLOSE') - (row['general']).should.equal('0') - (row['value']).should.equal('4') - - row = $database.getRow('stat', 87, 'id') - - (row['date']).should.equal('20170111') - (row['type']).should.equal('ASSIGN') - (row['general']).should.equal('0') - (row['value']).should.equal('0') - - row = $database.getRow('stat', 88, 'id') - - (row['date']).should.equal('20170111') - (row['type']).should.equal('CLOSE') - (row['general']).should.equal('0') - (row['value']).should.equal('0') + assertData(20, '20170101', 'CLOSE', '0') + assertData(21, '20170101', 'ASSIGN', '0') end end From f2401dcec718b09c44f02d8c644357c8b8c33aa6 Mon Sep 17 00:00:00 2001 From: ivan <ivan@opensupports.com> Date: Thu, 12 Jan 2017 18:29:50 -0300 Subject: [PATCH 10/21] Ivan - Fix file uploader issues, add file upload in ticket creation [skip ci] --- server/controllers/system/init-settings.php | 3 ++- server/controllers/ticket/create.php | 24 ++++++++++++++++++++- server/libs/FileUploader.php | 13 +++++++---- server/libs/LinearCongruentialGenerator.php | 3 ++- tests/system/edit-settings.rb | 4 ++-- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/server/controllers/system/init-settings.php b/server/controllers/system/init-settings.php index 6613b17a..a78a6c51 100644 --- a/server/controllers/system/init-settings.php +++ b/server/controllers/system/init-settings.php @@ -38,12 +38,13 @@ class InitSettingsController extends Controller { 'maintenance-mode' => 0, 'layout' => 'boxed', 'allow-attachments' => 0, - 'max-size' => 0, + 'max-size' => 1024, 'title' => 'Support Center', 'url' => 'http://www.opensupports.com/support', 'ticket-gap' => Hashing::generateRandomPrime(100000, 999999), 'file-gap' => Hashing::generateRandomPrime(100000, 999999), 'file-first-number' => Hashing::generateRandomNumber(100000, 999999), + 'file-quantity' => 0 ]); } diff --git a/server/controllers/ticket/create.php b/server/controllers/ticket/create.php index ac9f4c53..b9409361 100644 --- a/server/controllers/ticket/create.php +++ b/server/controllers/ticket/create.php @@ -60,7 +60,7 @@ class CreateController extends Controller { 'language' => $this->language, 'author' => $author, 'department' => $department, - 'file' => '', + 'file' => $this->uploadFile(), 'date' => Date::getCurrentDate(), 'unread' => false, 'unreadStaff' => true, @@ -75,4 +75,26 @@ class CreateController extends Controller { $this->ticketNumber = $ticket->ticketNumber; } + + private 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); + } + } } diff --git a/server/libs/FileUploader.php b/server/libs/FileUploader.php index dfc9c09b..8e01e089 100644 --- a/server/libs/FileUploader.php +++ b/server/libs/FileUploader.php @@ -4,6 +4,7 @@ class FileUploader extends FileManager { private $maxSize = 1024; private $linearCongruentialGenerator; private $linearCongruentialGeneratorOffset; + private $fileName; private static $instance = null; @@ -18,18 +19,18 @@ class FileUploader extends FileManager { private function __construct() {} public function upload($file) { - $newFileName = $this->generateNewName($file['name']); + $this->setNewName($file['name']); if($file['size'] > (1024 * $this->maxSize)) { return false; } - move_uploaded_file($file['tmp_name'], $this->getLocalPath() . $newFileName); + move_uploaded_file($file['tmp_name'], $this->getLocalPath() . $this->getFileName()); return true; } - private function generateNewName($fileName) { + private function setNewName($fileName) { $newName = $fileName; $newName = strtolower($newName); $newName = preg_replace('/\s+/', '_', $newName); @@ -38,7 +39,7 @@ class FileUploader extends FileManager { $newName = $this->linearCongruentialGenerator->generate($this->linearCongruentialGeneratorOffset) . '_' . $newName; } - return $newName; + $this->fileName = $newName; } public function setGeneratorValues($gap, $first, $offset) { @@ -52,5 +53,9 @@ class FileUploader extends FileManager { public function setMaxSize($maxSize) { $this->maxSize = $maxSize; } + + public function getFileName() { + return $this->fileName; + } } \ No newline at end of file diff --git a/server/libs/LinearCongruentialGenerator.php b/server/libs/LinearCongruentialGenerator.php index aafddb16..88df4141 100644 --- a/server/libs/LinearCongruentialGenerator.php +++ b/server/libs/LinearCongruentialGenerator.php @@ -21,7 +21,8 @@ class LinearCongruentialGenerator { } public function generate($offset) { - return ($this->first - $this->min + $offset * $this->gap) % ($this->max - $this->min + 1) + $this->min; + if($offset) return ($this->first - $this->min + $offset * $this->gap) % ($this->max - $this->min + 1) + $this->min; + else return $this->generateFirst(); } public function generateFirst() { diff --git a/tests/system/edit-settings.rb b/tests/system/edit-settings.rb index 0b2b77aa..69223599 100644 --- a/tests/system/edit-settings.rb +++ b/tests/system/edit-settings.rb @@ -10,7 +10,7 @@ describe'system/edit-settings' do "time-zone" => -3, "layout" => 'full-width', "allow-attachments" => 1, - "max-size" => 2, + "max-size" => 2048, "language" => 'en', "no-reply-email" => 'testemail@hotmail.com' }) @@ -27,7 +27,7 @@ describe'system/edit-settings' do (row['value']).should.equal('full-width') row = $database.getRow('setting', 'max-size', 'name') - (row['value']).should.equal('2') + (row['value']).should.equal('2048') row = $database.getRow('setting', 'language', 'name') (row['value']).should.equal('en') From 71984384ccddee35723f2966e7057dbd27b673bf Mon Sep 17 00:00:00 2001 From: ivan <ivan@opensupports.com> Date: Thu, 12 Jan 2017 20:30:44 -0300 Subject: [PATCH 11/21] Ivan - Add validations before download and upload on comment [skip ci] --- server/controllers/system/download.php | 36 +++++++++++++++++++++++--- server/controllers/ticket/comment.php | 1 + server/controllers/ticket/create.php | 22 ---------------- server/libs/Controller.php | 22 ++++++++++++++++ 4 files changed, 56 insertions(+), 25 deletions(-) diff --git a/server/controllers/system/download.php b/server/controllers/system/download.php index 0b481bf8..8c6699b3 100644 --- a/server/controllers/system/download.php +++ b/server/controllers/system/download.php @@ -7,18 +7,48 @@ class DownloadController extends Controller { public function validations() { return [ - 'permission' => 'staff_1', + 'permission' => 'user', 'requestData' => [ 'file' => [ - 'validation' => DataValidator::alnum('_.')->noWhitespace() + '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(Controller::request('file')); + $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; + } } \ No newline at end of file diff --git a/server/controllers/ticket/comment.php b/server/controllers/ticket/comment.php index b51414c2..6a043757 100644 --- a/server/controllers/ticket/comment.php +++ b/server/controllers/ticket/comment.php @@ -50,6 +50,7 @@ class CommentController extends Controller { $comment = Ticketevent::getEvent(Ticketevent::COMMENT); $comment->setProperties(array( 'content' => $this->content, + 'file' => $this->uploadFile(), 'date' => Date::getCurrentDate() )); diff --git a/server/controllers/ticket/create.php b/server/controllers/ticket/create.php index b9409361..fbcd9203 100644 --- a/server/controllers/ticket/create.php +++ b/server/controllers/ticket/create.php @@ -75,26 +75,4 @@ class CreateController extends Controller { $this->ticketNumber = $ticket->ticketNumber; } - - private 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); - } - } } diff --git a/server/libs/Controller.php b/server/libs/Controller.php index 3c8a3ca9..b82bdb0f 100644 --- a/server/libs/Controller.php +++ b/server/libs/Controller.php @@ -60,4 +60,26 @@ abstract class Controller { public static function getAppInstance() { 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); + } + } } \ No newline at end of file From 139a474693bc855a84fdfeba0814030380648657 Mon Sep 17 00:00:00 2001 From: ivan <ivan@opensupports.com> Date: Fri, 13 Jan 2017 10:33:17 -0300 Subject: [PATCH 12/21] Max Red - beautify code and set default selected indices to an empty array [skip ci] --- .../toggle-list.js | 13 ++++++------- .../toggle-list.scss | 0 2 files changed, 6 insertions(+), 7 deletions(-) rename client/src/{app-components => core-components}/toggle-list.js (77%) rename client/src/{app-components => core-components}/toggle-list.scss (100%) diff --git a/client/src/app-components/toggle-list.js b/client/src/core-components/toggle-list.js similarity index 77% rename from client/src/app-components/toggle-list.js rename to client/src/core-components/toggle-list.js index f90a26ae..8dba39a7 100644 --- a/client/src/app-components/toggle-list.js +++ b/client/src/core-components/toggle-list.js @@ -11,7 +11,7 @@ class ToggleList extends React.Component { }; state = { - selected: [1, 3] + selected: [] }; render() { @@ -35,26 +35,25 @@ class ToggleList extends React.Component { let classes = { 'toggle-list__item': true, 'toggle-list__first-item': (index === 0), - 'toggle-list__selected': (_.includes(this.state.selected, index)) + 'toggle-list__selected': _.includes(this.state.selected, index) }; return classNames(classes); } selectItem(index) { - let actual = _.clone(this.state.selected); + let newSelected = _.clone(this.state.selected); - _.includes(this.state.selected, index) ? _.remove(actual, t => t == index) : actual.push(index); + _.includes(this.state.selected, index) ? _.remove(newSelected, _index => _index == index) : newSelected.push(index); - console.log(actual); this.setState({ - selected: actual + selected: newSelected }); if (this.props.onChange) { this.props.onChange({ target: { - value: actual + value: newSelected } }); } diff --git a/client/src/app-components/toggle-list.scss b/client/src/core-components/toggle-list.scss similarity index 100% rename from client/src/app-components/toggle-list.scss rename to client/src/core-components/toggle-list.scss From 087a24c75c30ff4d75a9d9cd0e9db2ae5c55181d Mon Sep 17 00:00:00 2001 From: AntonyAntonio <guillermo@opensupports.com> Date: Fri, 13 Jan 2017 13:46:57 -0300 Subject: [PATCH 13/21] Guillermo - stast architecture [skip ci] --- tests/system/get-stats.rb | 91 ++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/tests/system/get-stats.rb b/tests/system/get-stats.rb index a86531f0..406c4516 100644 --- a/tests/system/get-stats.rb +++ b/tests/system/get-stats.rb @@ -83,21 +83,44 @@ describe'/system/get-stats' do (@result['data'][position]['value']).should.equal(value) end - assertData(11, '20170109', 'CREATE_TICKET', '1') - assertData(10, '20170109', 'CLOSE', '2') - assertData(9, '20170109', 'SIGNUP', '6') - assertData(8, '20170109', 'COMMENT', '3') + d = Date.today.prev_day + yesterday = d.strftime("%Y%m%d") + d = Date.today.prev_day.prev_day + yesterday2 = d.strftime("%Y%m%d") + d = Date.today.prev_day.prev_day.prev_day + yesterday3 = d.strftime("%Y%m%d") + d = Date.today.prev_day.prev_day.prev_day.prev_day + yesterday4 = d.strftime("%Y%m%d") + d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day + yesterday5 = d.strftime("%Y%m%d") + d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day + yesterday6 = d.strftime("%Y%m%d") + d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day + yesterday7 = d.strftime("%Y%m%d") + d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day + yesterday8 = d.strftime("%Y%m%d") + d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day + yesterday9 = d.strftime("%Y%m%d") + d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day + yesterday10 = d.strftime("%Y%m%d") + d = Date.today.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day.prev_day + yesterday11 = d.strftime("%Y%m%d") + + assertData(11, yesterday3, 'CREATE_TICKET', '1') + assertData(10, yesterday3, 'CLOSE', '2') + assertData(9, yesterday3, 'SIGNUP', '6') + assertData(8, yesterday3, 'COMMENT', '3') - assertData(7, '20170110', 'CREATE_TICKET', '3') - assertData(6, '20170110', 'CLOSE', '10') - assertData(5, '20170110', 'SIGNUP', '8') - assertData(4, '20170110', 'COMMENT', '3') + assertData(7, yesterday2, 'CREATE_TICKET', '3') + assertData(6, yesterday2, 'CLOSE', '10') + assertData(5, yesterday2, 'SIGNUP', '8') + assertData(4, yesterday2, 'COMMENT', '3') - assertData(3, '20170111', 'CREATE_TICKET', '2') - assertData(2, '20170111', 'CLOSE', '5') - assertData(1, '20170111', 'SIGNUP', '1') - assertData(0, '20170111', 'COMMENT', '8') + assertData(3, yesterday, 'CREATE_TICKET', '2') + assertData(2, yesterday, 'CLOSE', '5') + assertData(1, yesterday, 'SIGNUP', '1') + assertData(0, yesterday, 'COMMENT', '8') @result = request('/system/get-stats', { @@ -106,32 +129,32 @@ describe'/system/get-stats' do period: 'week', staffId: '1' }) - assertData(0, '20170111', 'CLOSE', '4') - assertData(1, '20170111', 'ASSIGN', '8') - assertData(2, '20170110', 'CLOSE', '11') - assertData(3, '20170110', 'ASSIGN', '3') + assertData(0, yesterday, 'CLOSE', '4') + assertData(1, yesterday, 'ASSIGN', '8') + assertData(2, yesterday2, 'CLOSE', '11') + assertData(3, yesterday2, 'ASSIGN', '3') - assertData(4, '20170109', 'CLOSE', '9') - assertData(5, '20170109', 'ASSIGN', '5') - assertData(6, '20170108', 'CLOSE', '0') - assertData(7, '20170108', 'ASSIGN', '0') + assertData(4, yesterday3, 'CLOSE', '9') + assertData(5, yesterday3, 'ASSIGN', '5') + assertData(6, yesterday4, 'CLOSE', '0') + assertData(7, yesterday4, 'ASSIGN', '0') - assertData(8, '20170107', 'CLOSE', '0') - assertData(9, '20170107', 'ASSIGN', '0') - assertData(10, '20170106', 'CLOSE', '0') - assertData(11, '20170106', 'ASSIGN', '0') + assertData(8, yesterday5, 'CLOSE', '0') + assertData(9, yesterday5, 'ASSIGN', '0') + assertData(10, yesterday6, 'CLOSE', '0') + assertData(11, yesterday6, 'ASSIGN', '0') - assertData(12, '20170105', 'CLOSE', '0') - assertData(13, '20170105', 'ASSIGN', '0') - assertData(14, '20170104', 'CLOSE', '0') - assertData(15, '20170104', 'ASSIGN', '0') + assertData(12, yesterday7, 'CLOSE', '0') + assertData(13, yesterday7, 'ASSIGN', '0') + assertData(14, yesterday8, 'CLOSE', '0') + assertData(15, yesterday8, 'ASSIGN', '0') - assertData(16, '20170103', 'CLOSE', '0') - assertData(17, '20170103', 'ASSIGN', '0') - assertData(18, '20170102', 'CLOSE', '0') - assertData(19, '20170102', 'ASSIGN', '0') + assertData(16, yesterday9, 'CLOSE', '0') + assertData(17, yesterday9, 'ASSIGN', '0') + assertData(18, yesterday10, 'CLOSE', '0') + assertData(19, yesterday10, 'ASSIGN', '0') - assertData(20, '20170101', 'CLOSE', '0') - assertData(21, '20170101', 'ASSIGN', '0') + assertData(20, yesterday11, 'CLOSE', '0') + assertData(21, yesterday11, 'ASSIGN', '0') end end From db63948db700e5fd6a257d3afc663b5617cee49c Mon Sep 17 00:00:00 2001 From: Ivan Diaz <ivan@opensupports.com> Date: Fri, 13 Jan 2017 14:40:21 -0300 Subject: [PATCH 14/21] Ivan - Add file upload test [skip ci] --- tests/system/fiile-upload-download.rb | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/system/fiile-upload-download.rb diff --git a/tests/system/fiile-upload-download.rb b/tests/system/fiile-upload-download.rb new file mode 100644 index 00000000..ebaa8345 --- /dev/null +++ b/tests/system/fiile-upload-download.rb @@ -0,0 +1,24 @@ +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('upload.txt', 'w') + file.puts('file content') + + 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', + 'file' => file + }) + (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 +end From f79913de975f2418dca3be8330e6c8785ad60330 Mon Sep 17 00:00:00 2001 From: AntonyAntonio <guillermo@opensupports.com> Date: Fri, 13 Jan 2017 15:50:35 -0300 Subject: [PATCH 15/21] Guillermo - registration api keys [skip ci] --- server/controllers/system.php | 6 +++ server/controllers/system/add-api-key.php | 41 ++++++++++++++++++++ server/controllers/system/delete-api-key.php | 32 +++++++++++++++ server/controllers/system/get-all-keys.php | 19 +++++++++ server/data/ERRORS.php | 1 + server/models/APIKey.php | 18 +++++++++ 6 files changed, 117 insertions(+) create mode 100644 server/controllers/system/add-api-key.php create mode 100644 server/controllers/system/delete-api-key.php create mode 100644 server/controllers/system/get-all-keys.php create mode 100644 server/models/APIKey.php diff --git a/server/controllers/system.php b/server/controllers/system.php index 8bc393cb..1d8fa889 100644 --- a/server/controllers/system.php +++ b/server/controllers/system.php @@ -12,6 +12,9 @@ require_once 'system/recover-mail-template.php'; require_once 'system/get-stats.php'; require_once 'system/disable-registration.php'; require_once 'system/enable-registration.php'; +require_once 'system/add-api-key.php'; +require_once 'system/delete-api-key.php'; +require_once 'system/get-all-keys.php'; $systemControllerGroup = new ControllerGroup(); $systemControllerGroup->setGroupPath('/system'); @@ -29,5 +32,8 @@ $systemControllerGroup->addController(new RecoverMailTemplateController); $systemControllerGroup->addController(new DisableRegistrationController); $systemControllerGroup->addController(new EnableRegistrationController); $systemControllerGroup->addController(new GetStatsController); +$systemControllerGroup->addController(new AddAPIKeyController); +$systemControllerGroup->addController(new DeleteAPIKeyController); +$systemControllerGroup->addController(new GetAllKeyController); $systemControllerGroup->finalize(); \ No newline at end of file diff --git a/server/controllers/system/add-api-key.php b/server/controllers/system/add-api-key.php new file mode 100644 index 00000000..8bbb4b78 --- /dev/null +++ b/server/controllers/system/add-api-key.php @@ -0,0 +1,41 @@ +<?php +use Respect\Validation\Validator as DataValidator; + +class AddAPIKeyController extends Controller { + const PATH = '/add-api-key'; + + public function validations() { + return [ + 'permission' => 'staff_3', + 'requestData' => [ + 'name' => [ + 'validation' => DataValidator::length(2, 55)->alpha(), + 'error' => ERRORS::INVALID_NAME + ] + ] + ]; + } + + public function handler() { + $apiInstance = new APIKey(); + + $name = Controller::request('name'); + + $keyInstance = APIKey::getDataStore($name, 'name'); + + if($keyInstance->isNull()){ + $token = Hashing::generateRandomToken(); + + $apiInstance->setProperties([ + 'name' => $name, + 'key' => $token + ]); + + $apiInstance->store(); + Response::respondSuccess($token); + } else { + Response::respondError(ERRORS::NAME_ALREADY_USED); + } + + } +} \ No newline at end of file diff --git a/server/controllers/system/delete-api-key.php b/server/controllers/system/delete-api-key.php new file mode 100644 index 00000000..8ee03152 --- /dev/null +++ b/server/controllers/system/delete-api-key.php @@ -0,0 +1,32 @@ +<?php +use Respect\Validation\Validator as DataValidator; + +class DeleteAPIKeyController extends Controller { + const PATH = '/delete-api-key'; + + public function validations() { + return [ + 'permission' => 'staff_3', + 'requestData' => [ + 'name' => [ + 'validation' => DataValidator::length(2, 55)->alpha(), + 'error' => ERRORS::INVALID_NAME + ] + ] + ]; + } + + public function handler() { + $name = Controller::request('name'); + + $keyInstance = APIKey::getDataStore($name, 'name'); + + if($keyInstance->isNull()) { + Response::respondError(ERRORS::INVALID_NAME); + return; + } + + $keyInstance->delete(); + Response::respondSuccess(); + } +} \ No newline at end of file diff --git a/server/controllers/system/get-all-keys.php b/server/controllers/system/get-all-keys.php new file mode 100644 index 00000000..35206374 --- /dev/null +++ b/server/controllers/system/get-all-keys.php @@ -0,0 +1,19 @@ +<?php +use Respect\Validation\Validator as DataValidator; + +class GetAllKeyController extends Controller { + const PATH = '/get-all-keys'; + + public function validations() { + return [ + 'permission' => 'staff_3', + 'requestData' => [] + ]; + } + + public function handler() { + $apiList = APIKey::getAll(); + + Response::respondSuccess($apiList->toArray()); + } +} \ No newline at end of file diff --git a/server/data/ERRORS.php b/server/data/ERRORS.php index 4b7507cf..06211649 100644 --- a/server/data/ERRORS.php +++ b/server/data/ERRORS.php @@ -36,4 +36,5 @@ class ERRORS { const INVALID_SUBJECT = 'INVALID_SUBJECT'; const INVALID_BODY = 'INVALID_BODY'; const INVALID_PERIOD = 'INVALID_PERIOD'; + const NAME_ALREADY_USED = 'NAME_ALREADY_USED'; } diff --git a/server/models/APIKey.php b/server/models/APIKey.php new file mode 100644 index 00000000..c0b751fa --- /dev/null +++ b/server/models/APIKey.php @@ -0,0 +1,18 @@ +<?php + +class APIKey extends DataStore { + const TABLE = 'apikey'; + + public static function getProps() { + return [ + 'name', + 'key' + ]; + } + public function toArray() { + return [ + 'name' => $this->name, + 'key' => $this->key + ]; + } +} \ No newline at end of file From 0108414a707904732efe9a9f86967a7fd5504b6d Mon Sep 17 00:00:00 2001 From: AntonyAntonio <guillermo@opensupports.com> Date: Fri, 13 Jan 2017 17:06:49 -0300 Subject: [PATCH 16/21] Guillermo - registration api keys [skip ci] --- server/controllers/system/add-api-key.php | 4 +-- server/controllers/user/signup.php | 3 ++- server/libs/validations/captcha.php | 3 ++- server/models/APIKey.php | 4 +-- tests/init.rb | 3 +++ tests/scripts.rb | 8 ++++++ tests/system/add-api-key.rb | 30 +++++++++++++++++++++++ tests/system/delete-api-key.rb | 30 +++++++++++++++++++++++ tests/system/get-all-keys.rb | 26 ++++++++++++++++++++ 9 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 tests/system/add-api-key.rb create mode 100644 tests/system/delete-api-key.rb create mode 100644 tests/system/get-all-keys.rb diff --git a/server/controllers/system/add-api-key.php b/server/controllers/system/add-api-key.php index 8bbb4b78..2f32d66d 100644 --- a/server/controllers/system/add-api-key.php +++ b/server/controllers/system/add-api-key.php @@ -9,7 +9,7 @@ class AddAPIKeyController extends Controller { 'permission' => 'staff_3', 'requestData' => [ 'name' => [ - 'validation' => DataValidator::length(2, 55)->alpha(), + 'validation' => DataValidator::length(2, 55)->alnum(), 'error' => ERRORS::INVALID_NAME ] ] @@ -28,7 +28,7 @@ class AddAPIKeyController extends Controller { $apiInstance->setProperties([ 'name' => $name, - 'key' => $token + 'token' => $token ]); $apiInstance->store(); diff --git a/server/controllers/user/signup.php b/server/controllers/user/signup.php index 48e6e1c6..cee72f1b 100644 --- a/server/controllers/user/signup.php +++ b/server/controllers/user/signup.php @@ -37,6 +37,7 @@ class SignUpController extends Controller { public function handler() { $this->storeRequestData(); + $apiKey = APIKey::getDataStore(Controller::request('apiKey'), 'token'); $existentUser = User::getUser($this->userEmail, 'email'); @@ -51,7 +52,7 @@ class SignUpController extends Controller { return; } - if (!Setting::getSetting('registration')->value) { + if (!Setting::getSetting('registration')->value && $apiKey->isNull() ) { Response::respondError(ERRORS::NO_PERMISSION); return; } diff --git a/server/libs/validations/captcha.php b/server/libs/validations/captcha.php index 437d92d8..7ac805c2 100644 --- a/server/libs/validations/captcha.php +++ b/server/libs/validations/captcha.php @@ -8,8 +8,9 @@ class Captcha extends AbstractRule { public function validate($reCaptchaResponse) { $reCaptchaPrivateKey = \Setting::getSetting('recaptcha-private')->getValue(); + $apiKey = \APIKey::getDataStore(\Controller::request('apiKey'), 'token'); - if (!$reCaptchaPrivateKey) return true; + if (!$reCaptchaPrivateKey || !$apiKey->isNull()) return true; $reCaptcha = new \ReCaptcha\ReCaptcha($reCaptchaPrivateKey); $reCaptchaValidation = $reCaptcha->verify($reCaptchaResponse, $_SERVER['REMOTE_ADDR']); diff --git a/server/models/APIKey.php b/server/models/APIKey.php index c0b751fa..2cfc3783 100644 --- a/server/models/APIKey.php +++ b/server/models/APIKey.php @@ -6,13 +6,13 @@ class APIKey extends DataStore { public static function getProps() { return [ 'name', - 'key' + 'token' ]; } public function toArray() { return [ 'name' => $this->name, - 'key' => $this->key + 'token' => $this->token ]; } } \ No newline at end of file diff --git a/tests/init.rb b/tests/init.rb index cae53d63..aaf24e28 100644 --- a/tests/init.rb +++ b/tests/init.rb @@ -55,3 +55,6 @@ require './system/recover-mail-template.rb' require './system/disable-registration.rb' require './system/enable-registration.rb' require './system/get-stats.rb' +require './system/add-api-key.rb' +require './system/delete-api-key.rb' +require './system/get-all-keys.rb' diff --git a/tests/scripts.rb b/tests/scripts.rb index 4cf57fcf..9b5eb892 100644 --- a/tests/scripts.rb +++ b/tests/scripts.rb @@ -44,4 +44,12 @@ class Scripts result['data'] end + + def self.createAPIKey(name) + result = request('/system/add-api-key', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + name: name + }) + end end diff --git a/tests/system/add-api-key.rb b/tests/system/add-api-key.rb new file mode 100644 index 00000000..cf8c86d5 --- /dev/null +++ b/tests/system/add-api-key.rb @@ -0,0 +1,30 @@ +describe'system/add-api-key' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + it 'should add API key' do + result= request('/system/add-api-key', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + name: 'new API' + }) + + (result['status']).should.equal('success') + + row = $database.getRow('apikey', 1, 'id') + + (row['name']).should.equal('new API') + (result['data']).should.equal(row['token']) + + end + it 'should not add API key' do + result= request('/system/add-api-key', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + name: 'new API' + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('NAME_ALREADY_USED') + end +end diff --git a/tests/system/delete-api-key.rb b/tests/system/delete-api-key.rb new file mode 100644 index 00000000..21553cfb --- /dev/null +++ b/tests/system/delete-api-key.rb @@ -0,0 +1,30 @@ +describe'system/delete-api-key' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + it 'should not delete API key' do + result= request('/system/delete-api-key', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + name: 'new PIA' + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_NAME') + end + + it 'should delete API key' do + result= request('/system/delete-api-key', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + name: 'new API' + }) + + (result['status']).should.equal('success') + + row = $database.getRow('apikey', 1, 'id') + + (row).should.equal(nil) + end + +end diff --git a/tests/system/get-all-keys.rb b/tests/system/get-all-keys.rb new file mode 100644 index 00000000..a604af3d --- /dev/null +++ b/tests/system/get-all-keys.rb @@ -0,0 +1,26 @@ +describe'system/get-all-keys' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + it 'should get all API keys' do + Scripts.createAPIKey('namekey1') + Scripts.createAPIKey('namekey2') + Scripts.createAPIKey('namekey3') + Scripts.createAPIKey('namekey4') + Scripts.createAPIKey('namekey5') + + result= request('/system/get-all-keys', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + }) + + (result['status']).should.equal('success') + (result['data'][0]['name']).should.equal('namekey1') + (result['data'][1]['name']).should.equal('namekey2') + (result['data'][2]['name']).should.equal('namekey3') + (result['data'][3]['name']).should.equal('namekey4') + (result['data'][4]['name']).should.equal('namekey5') + + end + +end From dffe4a87a0b0062085b4fdeee5b209998688f333 Mon Sep 17 00:00:00 2001 From: Ivan Diaz <ivan@opensupports.com> Date: Fri, 13 Jan 2017 21:58:59 -0300 Subject: [PATCH 17/21] Ivan - Add file upload and download test [skip ci] --- server/controllers/system/init-settings.php | 2 +- tests/init.rb | 1 + tests/libs.rb | 7 ++ tests/system/fiile-upload-download.rb | 24 ------- tests/system/file-upload-download.rb | 74 +++++++++++++++++++++ tests/system/get-stats.rb | 12 ++-- 6 files changed, 89 insertions(+), 31 deletions(-) delete mode 100644 tests/system/fiile-upload-download.rb create mode 100644 tests/system/file-upload-download.rb diff --git a/server/controllers/system/init-settings.php b/server/controllers/system/init-settings.php index 31536cba..b5d7c6d3 100644 --- a/server/controllers/system/init-settings.php +++ b/server/controllers/system/init-settings.php @@ -42,7 +42,7 @@ class InitSettingsController extends Controller { 'title' => 'Support Center', 'url' => 'http://www.opensupports.com/support', '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), diff --git a/tests/init.rb b/tests/init.rb index cae53d63..f98e9cda 100644 --- a/tests/init.rb +++ b/tests/init.rb @@ -55,3 +55,4 @@ require './system/recover-mail-template.rb' require './system/disable-registration.rb' require './system/enable-registration.rb' require './system/get-stats.rb' +require './system/file-upload-download.rb' diff --git a/tests/libs.rb b/tests/libs.rb index 8b832429..fef8e646 100644 --- a/tests/libs.rb +++ b/tests/libs.rb @@ -1,5 +1,12 @@ $agent = Mechanize.new +def plainRequest(path, data = {}) + uri = 'http://localhost:8080' + path + response = $agent.post(uri, data) + + return response +end + def request(path, data = {}) uri = 'http://localhost:8080' + path response = $agent.post(uri, data) diff --git a/tests/system/fiile-upload-download.rb b/tests/system/fiile-upload-download.rb deleted file mode 100644 index ebaa8345..00000000 --- a/tests/system/fiile-upload-download.rb +++ /dev/null @@ -1,24 +0,0 @@ -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('upload.txt', 'w') - file.puts('file content') - - 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', - 'file' => file - }) - (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 -end diff --git a/tests/system/file-upload-download.rb b/tests/system/file-upload-download.rb new file mode 100644 index 00000000..44bc3c37 --- /dev/null +++ b/tests/system/file-upload-download.rb @@ -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 diff --git a/tests/system/get-stats.rb b/tests/system/get-stats.rb index 406c4516..04d31e83 100644 --- a/tests/system/get-stats.rb +++ b/tests/system/get-stats.rb @@ -25,10 +25,10 @@ describe'/system/get-stats' do $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday3 + ", NULL, NULL);") end 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 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 #day 2 @@ -45,10 +45,10 @@ describe'/system/get-stats' do $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday2 + ", NULL, NULL);") end 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 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 #day 3 @@ -65,10 +65,10 @@ describe'/system/get-stats' do $database.query("INSERT INTO log VALUES('', 'COMMENT', NULL, " + yesterday + ", NULL, NULL);") end 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 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 @result = request('/system/get-stats', { From d575882bf2b2009ea3cde94067cf0fae4616bd52 Mon Sep 17 00:00:00 2001 From: Ivan Diaz <ivan@opensupports.com> Date: Sat, 14 Jan 2017 16:39:22 -0300 Subject: [PATCH 18/21] Ivan - Add file uploader component [skip ci] --- .../create-ticket-form.js | 3 ++ .../create-ticket-form.scss | 4 ++ client/src/core-components/file-uploader.js | 40 +++++++++++++++++++ client/src/core-components/file-uploader.scss | 31 ++++++++++++++ client/src/core-components/form-field.js | 9 +++-- client/src/lib-core/APIUtils.js | 3 +- 6 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 client/src/core-components/file-uploader.js create mode 100644 client/src/core-components/file-uploader.scss diff --git a/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js b/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js index 83b012bc..f8898b8f 100644 --- a/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js +++ b/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js @@ -56,6 +56,9 @@ class CreateTicketForm extends React.Component { }}/> </div> <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} <SubmitButton>Create Ticket</SubmitButton> </Form> diff --git a/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.scss b/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.scss index 045c290c..bc7d906e 100644 --- a/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.scss +++ b/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.scss @@ -1,5 +1,9 @@ .create-ticket-form { + &__file { + text-align: left; + } + &__message { margin-top: 20px; } diff --git a/client/src/core-components/file-uploader.js b/client/src/core-components/file-uploader.js new file mode 100644 index 00000000..c18771ad --- /dev/null +++ b/client/src/core-components/file-uploader.js @@ -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; \ No newline at end of file diff --git a/client/src/core-components/file-uploader.scss b/client/src/core-components/file-uploader.scss new file mode 100644 index 00000000..b6c766e7 --- /dev/null +++ b/client/src/core-components/file-uploader.scss @@ -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; + } +} \ No newline at end of file diff --git a/client/src/core-components/form-field.js b/client/src/core-components/form-field.js index c913e959..d12dbe47 100644 --- a/client/src/core-components/form-field.js +++ b/client/src/core-components/form-field.js @@ -9,6 +9,7 @@ import Checkbox from 'core-components/checkbox'; import CheckboxGroup from 'core-components/checkbox-group'; import TextEditor from 'core-components/text-editor'; import InfoTooltip from 'core-components/info-tooltip'; +import FileUploader from 'core-components/file-uploader'; class FormField extends React.Component { static contextTypes = { @@ -24,7 +25,7 @@ class FormField extends React.Component { error: React.PropTypes.string, infoMessage: React.PropTypes.node, 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 }; @@ -84,7 +85,8 @@ class FormField extends React.Component { 'textarea': TextEditor, 'select': DropDown, 'checkbox': Checkbox, - 'checkbox-group': CheckboxGroup + 'checkbox-group': CheckboxGroup, + 'file': FileUploader }[this.props.field]; if(this.props.decorator) { @@ -142,7 +144,8 @@ class FormField extends React.Component { getDivTypes() { return [ 'textarea', - 'checkbox-group' + 'checkbox-group', + 'file' ]; } diff --git a/client/src/lib-core/APIUtils.js b/client/src/lib-core/APIUtils.js index 2ba05eea..65b52ae6 100644 --- a/client/src/lib-core/APIUtils.js +++ b/client/src/lib-core/APIUtils.js @@ -9,7 +9,8 @@ const APIUtils = { url: path, method: method, data: data, - dataType: 'json' + processData: false, + contentType: false }) .done(resolve) .fail((jqXHR, textStatus) => { From 35d9a166cfbe28c212e9873f97cb6f356686d4d9 Mon Sep 17 00:00:00 2001 From: Ivan Diaz <ivan@opensupports.com> Date: Sat, 14 Jan 2017 21:44:20 -0300 Subject: [PATCH 19/21] Ivan - Add file download component [skip ci] --- client/src/app-components/ticket-event.js | 22 ++++++++++++++++++-- client/src/app-components/ticket-event.scss | 8 +++++++ client/src/app-components/ticket-viewer.js | 5 ++++- client/src/app-components/ticket-viewer.scss | 7 ------- client/src/data/fixtures/system-fixtures.js | 8 +++++++ client/src/data/fixtures/user-fixtures.js | 8 +++---- client/src/lib-app/api-call.js | 4 ++-- client/src/lib-app/fixtures-loader.js | 2 +- server/controllers/system/download.php | 2 +- 9 files changed, 48 insertions(+), 18 deletions(-) diff --git a/client/src/app-components/ticket-event.js b/client/src/app-components/ticket-event.js index bdcd9b2e..719f417d 100644 --- a/client/src/app-components/ticket-event.js +++ b/client/src/app-components/ticket-event.js @@ -2,6 +2,7 @@ import React from 'react'; import classNames from 'classnames'; import i18n from 'lib-app/i18n'; +import API from 'lib-app/api-call'; import DateTransformer from 'lib-core/date-transformer'; import Icon from 'core-components/icon'; @@ -161,7 +162,7 @@ class TicketEvent extends React.Component { } return ( - <div className="ticket-viewer__file"> + <div className="ticket-event__file"> {node} </div> ) @@ -222,9 +223,26 @@ class TicketEvent extends React.Component { const fileName = filePath.replace(/^.*[\\\/]/, ''); 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; diff --git a/client/src/app-components/ticket-event.scss b/client/src/app-components/ticket-event.scss index 50105bbe..328d16e1 100644 --- a/client/src/app-components/ticket-event.scss +++ b/client/src/app-components/ticket-event.scss @@ -91,6 +91,14 @@ } } + &__file { + background-color: $very-light-grey; + cursor: pointer; + text-align: right; + padding: 5px 10px; + font-size: 12px; + } + &_staff { .ticket-event__icon { background-color: $primary-blue; diff --git a/client/src/app-components/ticket-viewer.js b/client/src/app-components/ticket-viewer.js index 6b6f1362..c00b875a 100644 --- a/client/src/app-components/ticket-viewer.js +++ b/client/src/app-components/ticket-viewer.js @@ -190,6 +190,7 @@ class TicketViewer extends React.Component { <div className="ticket-viewer__response-field row"> <Form {...this.getCommentFormProps()}> <FormField name="content" validation="TEXT_AREA" required field="textarea" /> + <FormField name="file" field="file"/> <SubmitButton>{i18n('RESPOND_TICKET')}</SubmitButton> </Form> </div> @@ -234,10 +235,12 @@ class TicketViewer extends React.Component { loading: this.state.loading, onChange: (formState) => {this.setState({ commentValue: formState.content, + commentFile: formState.file, commentEdited: true })}, values: { - 'content': this.state.commentValue + 'content': this.state.commentValue, + 'file': this.state.commentFile } }; } diff --git a/client/src/app-components/ticket-viewer.scss b/client/src/app-components/ticket-viewer.scss index 9f03fbf7..2a6658ed 100644 --- a/client/src/app-components/ticket-viewer.scss +++ b/client/src/app-components/ticket-viewer.scss @@ -48,13 +48,6 @@ margin-top: 10px; } - &__file { - background-color: $very-light-grey; - text-align: right; - padding: 5px 10px; - font-size: 12px; - } - &__comments { position: relative; } diff --git a/client/src/data/fixtures/system-fixtures.js b/client/src/data/fixtures/system-fixtures.js index b67d450f..ac2a5bbd 100644 --- a/client/src/data/fixtures/system-fixtures.js +++ b/client/src/data/fixtures/system-fixtures.js @@ -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', time: 100, diff --git a/client/src/data/fixtures/user-fixtures.js b/client/src/data/fixtures/user-fixtures.js index 484d7c95..2ff9801b 100644 --- a/client/src/data/fixtures/user-fixtures.js +++ b/client/src/data/fixtures/user-fixtures.js @@ -166,7 +166,7 @@ module.exports = [ name: 'Sales Support' }, date: '201504090001', - file: 'http://www.opensupports.com/some_file.zip', + file: 'some_file.txt', language: 'en', unread: false, closed: false, @@ -385,7 +385,7 @@ module.exports = [ name: 'Technical Issues' }, date: '201604161427', - file: 'http://www.opensupports.com/some_file.zip', + file: 'some_file.txt', language: 'en', unread: true, closed: false, @@ -504,7 +504,7 @@ module.exports = [ name: 'Sales Support' }, date: '201604150849', - file: 'http://www.opensupports.com/some_file.zip', + file: 'some_file.txt', language: 'en', unread: false, closed: false, @@ -607,7 +607,7 @@ module.exports = [ name: 'Sales Support' }, date: '201504091032', - file: 'http://www.opensupports.com/some_file.zip', + file: 'some_file.txt', language: 'en', unread: false, closed: false, diff --git a/client/src/lib-app/api-call.js b/client/src/lib-app/api-call.js index f65ba752..032c5bc0 100644 --- a/client/src/lib-app/api-call.js +++ b/client/src/lib-app/api-call.js @@ -12,13 +12,13 @@ function processData (data) { } module.exports = { - call: function ({path, data}) { + call: function ({path, data, plain}) { return new Promise(function (resolve, reject) { APIUtils.post(root + path, processData(data)) .then(function (result) { console.log(result); - if (result.status === 'success') { + if (plain || result.status === 'success') { resolve(result); } else if (reject) { reject(result); diff --git a/client/src/lib-app/fixtures-loader.js b/client/src/lib-app/fixtures-loader.js index 1850cb34..dc02c382 100644 --- a/client/src/lib-app/fixtures-loader.js +++ b/client/src/lib-app/fixtures-loader.js @@ -24,7 +24,7 @@ fixtures.add(require('data/fixtures/article-fixtures')); _.each(fixtures.getAll(), function (fixture) { mockjax({ - contentType: 'application/json', + contentType: fixture.contentType || 'application/json', url: 'http://localhost:3000/api' + fixture.path, responseTime: fixture.time || 500, response: function (settings) { diff --git a/server/controllers/system/download.php b/server/controllers/system/download.php index 8c6699b3..278ddc1b 100644 --- a/server/controllers/system/download.php +++ b/server/controllers/system/download.php @@ -10,7 +10,7 @@ class DownloadController extends Controller { 'permission' => 'user', 'requestData' => [ 'file' => [ - 'validation' => DataValidator::alnum('_.')->noWhitespace(), + 'validation' => DataValidator::alnum('_.-')->noWhitespace(), 'error' => ERRORS::INVALID_FILE ] ] From 575f34256aa6e7e77849e4acd53951a6b1db868d Mon Sep 17 00:00:00 2001 From: Ivan Diaz <ivan@opensupports.com> Date: Sat, 14 Jan 2017 23:51:31 -0300 Subject: [PATCH 20/21] Ivan - Fix unit testing backend --- server/tests/__mocks__/APIKeyMock.php | 12 ++++++++++++ server/tests/__mocks__/UserMock.php | 2 ++ server/tests/libs/validations/captchaTest.php | 4 ++++ 3 files changed, 18 insertions(+) create mode 100644 server/tests/__mocks__/APIKeyMock.php diff --git a/server/tests/__mocks__/APIKeyMock.php b/server/tests/__mocks__/APIKeyMock.php new file mode 100644 index 00000000..7ef32d77 --- /dev/null +++ b/server/tests/__mocks__/APIKeyMock.php @@ -0,0 +1,12 @@ +<?php +include_once 'tests/__mocks__/NullDataStoreMock.php'; + +class APIKey extends \Mock { + public static $functionList = array(); + + public static function initStubs() { + parent::setStatics(array( + 'getDataStore' => parent::stub()->returns(new NullDataStore()), + )); + } +} \ No newline at end of file diff --git a/server/tests/__mocks__/UserMock.php b/server/tests/__mocks__/UserMock.php index c8edb25a..d7913a23 100644 --- a/server/tests/__mocks__/UserMock.php +++ b/server/tests/__mocks__/UserMock.php @@ -5,6 +5,7 @@ class User extends \Mock { public static function initStubs() { parent::setStatics(array( 'authenticate' => parent::stub()->returns(self::getUserInstanceMock()), + 'getDataStore' => parent::stub()->returns(self::getUserInstanceMock()) )); } @@ -18,6 +19,7 @@ class User extends \Mock { $mockUserInstance->id = 'MOCK_ID'; $mockUserInstance->email = 'MOCK_EMAIL'; $mockUserInstance->password = 'MOCK_PASSWORD'; + $mockUserInstance->verificationToken = null; return $mockUserInstance; } diff --git a/server/tests/libs/validations/captchaTest.php b/server/tests/libs/validations/captchaTest.php index e2e86eaf..c85bfcf7 100644 --- a/server/tests/libs/validations/captchaTest.php +++ b/server/tests/libs/validations/captchaTest.php @@ -2,6 +2,8 @@ include_once 'tests/__lib__/Mock.php'; include_once 'tests/__mocks__/RespectMock.php'; include_once 'tests/__mocks__/SettingMock.php'; +include_once 'tests/__mocks__/APIKeyMock.php'; +include_once 'tests/__mocks__/ControllerMock.php'; include_once 'tests/__mocks__/ReCaptchaMock.php'; include_once 'libs/validations/captcha.php'; @@ -10,6 +12,8 @@ class CaptchaValidationTest extends PHPUnit_Framework_TestCase { protected function setUp() { Setting::initStubs(); + Controller::initStubs(); + APIKey::initStubs(); \ReCaptcha\ReCaptcha::initVerify(); $_SERVER['REMOTE_ADDR'] = 'MOCK_REMOTE'; From e8e29d8701ff06228291fdd6bd95cca8cd5ef3ea Mon Sep 17 00:00:00 2001 From: Ivan Diaz <ivan@opensupports.com> Date: Sun, 15 Jan 2017 01:36:04 -0300 Subject: [PATCH 21/21] Ivan - Fix static ticketevent --- server/models/APIKey.php | 1 + server/models/Ticketevent.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/models/APIKey.php b/server/models/APIKey.php index 2cfc3783..91218cac 100644 --- a/server/models/APIKey.php +++ b/server/models/APIKey.php @@ -9,6 +9,7 @@ class APIKey extends DataStore { 'token' ]; } + public function toArray() { return [ 'name' => $this->name, diff --git a/server/models/Ticketevent.php b/server/models/Ticketevent.php index 4bd7ebb1..a8a23fed 100644 --- a/server/models/Ticketevent.php +++ b/server/models/Ticketevent.php @@ -36,7 +36,7 @@ class Ticketevent extends DataStore { return $ticketEvent; } - public function getProps() { + public static function getProps() { return [ 'type', 'content',