Skip to content
Snippets Groups Projects
Commit ba0bf577 authored by Juanjo Menent's avatar Juanjo Menent
Browse files

Merge pull request #3353 from GPCsolutions/sentry

NEW Sentry logging support
parents 7924a870 1618cbc5
Branches
Tags
No related merge requests found
Showing
with 2608 additions and 5 deletions
...@@ -25,6 +25,7 @@ odtPHP 1.0.1 GPL-2+ b Yes ...@@ -25,6 +25,7 @@ odtPHP 1.0.1 GPL-2+ b Yes
PHPExcel 1.8.0 LGPL-2.1+ Yes Read/Write XLS files, read ODS files PHPExcel 1.8.0 LGPL-2.1+ Yes Read/Write XLS files, read ODS files
php-iban 1.4.7 LGPL-3+ Yes Parse and validate IBAN (and IIBAN) bank account information in PHP php-iban 1.4.7 LGPL-3+ Yes Parse and validate IBAN (and IIBAN) bank account information in PHP
PHPPrintIPP 1.3 GPL-2+ Yes Library to send print IPP requests PHPPrintIPP 1.3 GPL-2+ Yes Library to send print IPP requests
Raven-php 0.12.1 MIT License Yes Used for server-side error logging with Sentry logger
Restler 3.0 LGPL-3+ Yes Library to develop REST Web services Restler 3.0 LGPL-3+ Yes Library to develop REST Web services
TCPDF 6.2.6 LGPL-3+ Yes PDF generation TCPDF 6.2.6 LGPL-3+ Yes PDF generation
TCPDI 1.0.0 LGPL-3+ / Apache 2.0 Yes FPDI replacement TCPDI 1.0.0 LGPL-3+ / Apache 2.0 Yes FPDI replacement
...@@ -50,7 +51,8 @@ jQuery TableDnD 0.6 GPL and MIT License Yes ...@@ -50,7 +51,8 @@ jQuery TableDnD 0.6 GPL and MIT License Yes
jQuery Timepicker 1.1.0 GPL and MIT License Yes JS library Timepicker addon for Datepicker jQuery Timepicker 1.1.0 GPL and MIT License Yes JS library Timepicker addon for Datepicker
jQuery Tiptip 1.3 GPL and MIT License Yes JS library for tooltips jQuery Tiptip 1.3 GPL and MIT License Yes JS library for tooltips
jsGantt 1.2 BSD License Yes JS library (to build Gantt reports) jsGantt 1.2 BSD License Yes JS library (to build Gantt reports)
JsTimezoneDetect 1.0.4 MIT Licence Yes JS library to detect user timezone JsTimezoneDetect 1.0.4 MIT License Yes JS library to detect user timezone
Raven.js 1.1.19 MIT License Yes Used for client-side error logging with Sentry logger
For licenses compatibility informations: For licenses compatibility informations:
http://www.fsf.org/licensing/licenses/index_html http://www.fsf.org/licensing/licenses/index_html
......
...@@ -18,7 +18,8 @@ ...@@ -18,7 +18,8 @@
"mobiledetect/mobiledetectlib": "2.8.3", "mobiledetect/mobiledetectlib": "2.8.3",
"phpoffice/phpexcel": "1.8.0", "phpoffice/phpexcel": "1.8.0",
"restler/framework": "^3.0", "restler/framework": "^3.0",
"tecnick.com/tcpdf": "6.2.6" "tecnick.com/tcpdf": "6.2.6",
"raven/raven": "^0.12.0"
}, },
"suggest": { "suggest": {
"ext-mysqlnd": "To use with MySQL or MariaDB", "ext-mysqlnd": "To use with MySQL or MariaDB",
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"hash": "ca5a74259e0d1773089838908d58cb31", "hash": "32e1fa78cc95c32b154a38e07706874c",
"packages": [ "packages": [
{ {
"name": "ccampbell/chromephp", "name": "ccampbell/chromephp",
...@@ -199,6 +199,60 @@ ...@@ -199,6 +199,60 @@
], ],
"time": "2014-03-02 15:22:49" "time": "2014-03-02 15:22:49"
}, },
{
"name": "raven/raven",
"version": "0.12.1",
"source": {
"type": "git",
"url": "https://github.com/getsentry/raven-php.git",
"reference": "b325984c792ff89f985b73da9a3ad8ed8b520bca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getsentry/raven-php/zipball/b325984c792ff89f985b73da9a3ad8ed8b520bca",
"reference": "b325984c792ff89f985b73da9a3ad8ed8b520bca",
"shasum": ""
},
"require": {
"ext-curl": "*",
"php": ">=5.2.4"
},
"require-dev": {
"fabpot/php-cs-fixer": "^1.8.0",
"phpunit/phpunit": "^4.6.6"
},
"bin": [
"bin/raven"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.12.x-dev"
}
},
"autoload": {
"psr-0": {
"Raven_": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD"
],
"authors": [
{
"name": "David Cramer",
"email": "dcramer@gmail.com"
}
],
"description": "A PHP client for Sentry (http://getsentry.com)",
"homepage": "http://getsentry.com",
"keywords": [
"log",
"logging"
],
"time": "2015-08-25 22:38:46"
},
{ {
"name": "restler/framework", "name": "restler/framework",
"version": "3.0.0", "version": "3.0.0",
......
...@@ -106,6 +106,15 @@ if ($action == 'set') ...@@ -106,6 +106,15 @@ if ($action == 'set')
$activeModules = $newActiveModules; $activeModules = $newActiveModules;
dolibarr_set_const($db, 'SYSLOG_HANDLERS', json_encode($activeModules), 'chaine',0,'',0); dolibarr_set_const($db, 'SYSLOG_HANDLERS', json_encode($activeModules), 'chaine',0,'',0);
// Check configuration
foreach ($activeModules as $modulename) {
/**
* @var LogHandler
*/
$module = new $modulename;
$error = $module->checkConfiguration();
}
if (! $error) if (! $error)
{ {
...@@ -115,7 +124,8 @@ if ($action == 'set') ...@@ -115,7 +124,8 @@ if ($action == 'set')
else else
{ {
$db->rollback(); $db->rollback();
setEventMessage($langs->trans("Error"),'errors'); setEventMessage($error, 'errors');
} }
} }
......
<?php
/* Copyright (C) 2004-2005 Rodolphe Quiedeville <rodolphe@quiedeville.org>
* Copyright (C) 2004-2009 Laurent Destailleur <eldy@users.sourceforge.net>
* Copyright (C) 2015 Raphaël Doursenaud <rdoursenaud@gpcsolutions.fr>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
require_once DOL_DOCUMENT_ROOT . '/includes/autoload.php';
require_once DOL_DOCUMENT_ROOT . '/core/modules/syslog/logHandler.php';
/**
* Class to manage logging to Sentry
*
* @see https://docs.getsentry.com/on-premise/clients/php/
*/
class mod_syslog_sentry extends LogHandler implements LogHandlerInterface
{
/**
* @var string Log handler code
*/
public $code = 'sentry';
/**
* Return name of logger
*
* @return string Name of logger
*/
public function getName()
{
return 'Sentry';
}
/**
* Version of the module ('x.y.z' or 'dolibarr' or 'experimental' or 'development')
*
* @return string
*/
public function getVersion()
{
return 'dolibarr';
}
/**
* Content of the info tooltip.
*
* @return false|string
*/
public function getInfo()
{
global $langs;
return $langs->trans('SyslogSentryFromProject');
}
/**
* Is the module active ?
*
* @return int
*/
public function isActive()
{
return 1;
}
/**
* Return array of configuration data
*
* @return array Return array of configuration data
*/
public function configure()
{
global $langs;
return array(
array(
'constant' => 'SYSLOG_SENTRY_DSN',
'name' => $langs->trans('SyslogSentryDSN'),
'default' => '',
'attr' => 'size="100" placeholder="https://<key>:<secret>@app.getsentry.com/<project>"'
)
);
}
/**
* Return if configuration is valid
*
* @return array Array of errors. Empty array if ok.
*/
public function checkConfiguration()
{
global $conf;
$errors = array();
$dsn = $conf->global->SYSLOG_SENTRY_DSN;
try {
$client = new Raven_Client(
$dsn,
array('curl_method' => 'sync')
);
} catch (InvalidArgumentException $ex) {
$errors[] = "ERROR: There was an error parsing your DSN:\n " . $ex->getMessage();
}
if (!$errors) {
// Send test event and check for errors
$client->captureMessage('TEST: Sentry syslog configuration check', null, Raven_Client::DEBUG);
$last_error = $client->getLastError();
if ($last_error) {
$errors[] = $last_error;
}
}
if (!$errors) {
// Install handlers
$error_handler = new Raven_ErrorHandler($client);
$error_handler->registerExceptionHandler();
$error_handler->registerErrorHandler();
$error_handler->registerShutdownFunction();
}
return $errors;
}
/**
* Export the message
*
* @param array $content Array containing the info about the message
* @return void
*/
public function export($content)
{
global $conf;
$dsn = $conf->global->SYSLOG_SENTRY_DSN;
$client = new Raven_Client(
$dsn,
array('curl_method' => 'exec')
);
$client->user_context(array(
'username' => ($content['user'] ? $content['user'] : ''),
'ip_address' => $content['ip']
));
$client->tags_context(array(
'version' => DOL_VERSION
));
$client->registerSeverityMap(array(
LOG_EMERG => Raven_Client::FATAL,
LOG_ALERT => Raven_Client::FATAL,
LOG_CRIT => Raven_Client::ERROR,
LOG_ERR => Raven_Client::ERROR,
LOG_WARNING => Raven_Client::WARNING,
LOG_NOTICE => Raven_Client::WARNING,
LOG_INFO => Raven_Client::INFO,
LOG_DEBUG => Raven_Client::DEBUG,
));
if (substr($content['message'], 0, 3) === 'sql') {
global $db;
$query = substr($content['message'], 4, strlen($content['message']));
$client->captureQuery(
$query,
$client->translateSeverity($content['level']),
$db->type
);
} else {
$client->captureMessage(
$content['message'],
null,
$client->translateSeverity($content['level'])
);
}
}
}
../raven/raven/bin/raven
\ No newline at end of file
Copyright (c) 2015 Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
...@@ -6,6 +6,7 @@ $vendorDir = dirname(dirname(__FILE__)); ...@@ -6,6 +6,7 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname(dirname($vendorDir)); $baseDir = dirname(dirname($vendorDir));
return array( return array(
'Raven_' => array($vendorDir . '/raven/raven/lib'),
'PHPExcel' => array($vendorDir . '/phpoffice/phpexcel/Classes'), 'PHPExcel' => array($vendorDir . '/phpoffice/phpexcel/Classes'),
'Luracast\\Restler' => array($vendorDir . '/restler/framework'), 'Luracast\\Restler' => array($vendorDir . '/restler/framework'),
'Detection' => array($vendorDir . '/mobiledetect/mobiledetectlib/namespaced'), 'Detection' => array($vendorDir . '/mobiledetect/mobiledetectlib/namespaced'),
......
...@@ -340,5 +340,61 @@ ...@@ -340,5 +340,61 @@
"pdf417", "pdf417",
"qrcode" "qrcode"
] ]
},
{
"name": "raven/raven",
"version": "0.12.1",
"version_normalized": "0.12.1.0",
"source": {
"type": "git",
"url": "https://github.com/getsentry/raven-php.git",
"reference": "b325984c792ff89f985b73da9a3ad8ed8b520bca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getsentry/raven-php/zipball/b325984c792ff89f985b73da9a3ad8ed8b520bca",
"reference": "b325984c792ff89f985b73da9a3ad8ed8b520bca",
"shasum": ""
},
"require": {
"ext-curl": "*",
"php": ">=5.2.4"
},
"require-dev": {
"fabpot/php-cs-fixer": "^1.8.0",
"phpunit/phpunit": "^4.6.6"
},
"time": "2015-08-25 22:38:46",
"bin": [
"bin/raven"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.12.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-0": {
"Raven_": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD"
],
"authors": [
{
"name": "David Cramer",
"email": "dcramer@gmail.com"
}
],
"description": "A PHP client for Sentry (http://getsentry.com)",
"homepage": "http://getsentry.com",
"keywords": [
"log",
"logging"
]
} }
] ]
{
"name": "raven-js",
"version": "1.1.19",
"dependencies": {},
"main": "dist/raven.js",
"ignore": {},
"homepage": "https://github.com/getsentry/raven-js",
"_release": "1.1.19",
"_resolution": {
"type": "version",
"tag": "1.1.19",
"commit": "82b9c07b7545c6c10e297709a741eaa9b75f64e8"
},
"_source": "git://github.com/getsentry/raven-js.git",
"_target": "~1.1.19",
"_originalSource": "raven-js",
"_direct": true
}
\ No newline at end of file
.DS_Store
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes
docs/html
docs/doctrees
build
node_modules
npm-debug.log
scratch/
*.pyc
.idea
aws.json
{
"es3": true,
"globalstrict": true,
"browser": true,
"predef": [
"TraceKit",
"console"
]
}
# language doesn't matter, we're only using phantom.js
language: node_js
node_js:
- "0.10"
script:
- ./node_modules/.bin/grunt test build
notifications:
irc: "irc.freenode.org#sentry"
https://github.com/getsentry/raven-js/graphs/contributors
module.exports = function(grunt) {
"use strict";
var _ = require('lodash');
var path = require('path');
var coreFiles = [
'template/_header.js',
'vendor/**/*.js',
'src/**/*.js',
'template/_footer.js'
];
var plugins = grunt.option('plugins');
// Create plugin paths and verify hey exist
plugins = _.map(plugins ? plugins.split(',') : [], function (plugin) {
var path = 'plugins/' + plugin + '.js';
if(!grunt.file.exists(path))
throw new Error("Plugin '" + plugin + "' not found in plugins directory.");
return path;
});
// Taken from http://dzone.com/snippets/calculate-all-combinations
var combine = function (a) {
var fn = function (n, src, got, all) {
if (n === 0) {
all.push(got);
return;
}
for (var j = 0; j < src.length; j++) {
fn(n - 1, src.slice(j + 1), got.concat([src[j]]), all);
}
};
var all = [a];
for (var i = 0; i < a.length; i++) {
fn(i, a, [], all);
}
return all;
};
var pluginCombinations = combine(grunt.file.expand('plugins/*.js'));
var pluginConcatFiles = _.reduce(pluginCombinations, function (dict, comb) {
var key = _.map(comb, function (plugin) {
return path.basename(plugin, '.js');
});
key.sort();
var dest = path.join('build/', key.join(','), '/raven.js');
dict[dest] = coreFiles.concat(comb);
return dict;
}, {});
var gruntConfig = {
pkg: grunt.file.readJSON('package.json'),
aws: grunt.file.exists('aws.json') ? grunt.file.readJSON('aws.json'): {},
clean: ['build'],
concat: {
options: {
separator: '\n',
banner: grunt.file.read('template/_copyright.js'),
process: true
},
core: {
src: coreFiles.concat(plugins),
dest: 'build/raven.js'
},
all: {
files: pluginConcatFiles
}
},
uglify: {
options: {
sourceMap: function (dest) {
return path.join(path.dirname(dest),
path.basename(dest, '.js')) +
'.map';
},
sourceMappingURL: function (dest) {
return path.basename(dest, '.js') + '.map';
},
preserveComments: 'some'
},
dist: {
src: ['build/**/*.js'],
ext: '.min.js',
expand: true
}
},
fixSourceMaps: {
all: ['build/**/*.map']
},
jshint: {
options: {
jshintrc: '.jshintrc'
},
all: ['Gruntfile.js', 'src/**/*.js', 'plugins/**/*.js']
},
mocha: {
all: {
options: {
mocha: {
ignoreLeaks: true,
grep: grunt.option('grep')
},
log: true,
reporter: 'Dot',
run: true
},
src: ['test/index.html'],
nonull: true
}
},
release: {
options: {
npm: false,
commitMessage: 'Release <%= version %>'
}
},
s3: {
options: {
key: '<%= aws.key %>',
secret: '<%= aws.secret %>',
bucket: '<%= aws.bucket %>',
access: 'public-read',
// Limit concurrency
maxOperations: 20,
headers: {
// Surrogate-Key header for Fastly to purge by release
'x-amz-meta-surrogate-key': '<%= pkg.release %>'
}
},
all: {
upload: [{
src: 'build/**/*',
dest: '<%= pkg.release %>/',
rel: 'build/'
}]
}
},
connect: {
test: {
options: {
port: 8000,
debug: true,
keepalive: true
}
},
docs: {
options: {
port: 8000,
debug: true,
base: 'docs/html',
keepalive: true
}
}
},
copy: {
dist: {
expand: true,
flatten: true,
cwd: 'build/',
src: '**',
dest: 'dist/'
}
}
};
grunt.initConfig(gruntConfig);
// Custom Grunt tasks
grunt.registerTask('version', function() {
var pkg = grunt.config.get('pkg');
if (grunt.option('dev')) {
pkg.release = 'dev';
pkg.version = grunt.config.get('gitinfo').local.branch.current.shortSHA;
} else {
pkg.release = pkg.version;
}
grunt.config.set('pkg', pkg);
});
grunt.registerMultiTask('fixSourceMaps', function () {
this.files.forEach(function (f) {
var result;
var sources = f.src.filter(function (filepath) {
if (!grunt.file.exists(filepath)) {
grunt.log.warn('Source file "' + filepath + '" not found.');
return false;
} else {
return true;
}
}).forEach(function (filepath) {
var base = path.dirname(filepath);
var sMap = grunt.file.readJSON(filepath);
sMap.file = path.relative(base, sMap.file);
sMap.sources = _.map(sMap.sources, path.relative.bind(path, base));
grunt.file.write(filepath, JSON.stringify(sMap));
// Print a success message.
grunt.log.writeln('File "' + filepath + '" fixed.');
});
});
});
// Grunt contrib tasks
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-contrib-copy');
// 3rd party Grunt tasks
grunt.loadNpmTasks('grunt-mocha');
grunt.loadNpmTasks('grunt-release');
grunt.loadNpmTasks('grunt-s3');
grunt.loadNpmTasks('grunt-gitinfo');
// Build tasks
grunt.registerTask('_prep', ['clean', 'gitinfo', 'version']);
grunt.registerTask('concat.core', ['_prep', 'concat:core']);
grunt.registerTask('concat.all', ['_prep', 'concat:all']);
grunt.registerTask('build.core', ['concat.core', 'uglify', 'fixSourceMaps']);
grunt.registerTask('build.all', ['concat.all', 'uglify', 'fixSourceMaps']);
grunt.registerTask('build', ['build.all']);
grunt.registerTask('dist', ['build.core', 'copy:dist']);
// Test task
grunt.registerTask('test', ['jshint', 'mocha']);
// Webserver tasks
grunt.registerTask('run:test', ['connect:test']);
grunt.registerTask('run:docs', ['connect:docs']);
grunt.registerTask('publish', ['test', 'build.all', 's3']);
grunt.registerTask('default', ['test']);
};
Copyright (c) 2014 Matt Robenolt and other contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
develop: update-submodules
npm install .
update-submodules:
git submodule init
git submodule update
docs:
cd docs; $(MAKE) html
docs-live:
while true; do \
sleep 2; \
$(MAKE) docs; \
done
clean:
rm -rf docs/html
.PHONY: develop update-submodules docs docs-live clean
# Raven.js [![Build Status](https://travis-ci.org/getsentry/raven-js.svg?branch=master)](https://travis-ci.org/getsentry/raven-js)
Raven.js is a tiny standalone JavaScript client for [Sentry](https://www.getsentry.com/).
**Raven.js v1.1 requires Sentry v6.0 or later.**
## Resources
* [Download](http://ravenjs.com)
* [Documentation](https://raven-js.readthedocs.org)
* [Bug Tracker](https://github.com/getsentry/raven-js/issues)
* [IRC](irc://chat.freenode.net/sentry) (chat.freenode.net, #sentry)
* Follow [@mattrobenolt](https://twitter.com/mattrobenolt) on Twitter for updates
{
"name": "raven-js",
"version": "1.1.19",
"dependencies": {},
"main": "dist/raven.js",
"ignore": {}
}
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment