1
0
mirror of https://github.com/freescout-helpdesk/freescout.git synced 2024-11-23 10:52:31 +01:00

Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist

This commit is contained in:
FreeScout 2023-10-04 22:18:40 -07:00
commit c9d5f9f7fa
46 changed files with 4479 additions and 743 deletions

20
.github/workflows/lint-php.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: PHP Code Sniffer
on:
workflow_dispatch:
jobs:
build:
name: Lint PHP
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
tools: phpcs
- name: Run check
run: phpcs

59
.github/workflows/test-pgsql.yml vendored Normal file
View File

@ -0,0 +1,59 @@
name: Test App (PostgreSQL)
on:
push:
branches:
- master
workflow_dispatch:
jobs:
test:
name: Test App (PostgreSQL)
runs-on: ubuntu-latest
env:
DB_CONNECTION: testing_pgsql
services:
postgres:
image: postgres:latest
env:
POSTGRES_USER: freescout-test
POSTGRES_PASSWORD: freescout-test
POSTGRES_DB: freescout-test
ports:
- 5432:5432
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
matrix:
php: ['7.3', '7.4', '8.0', '8.1', '8.2']
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: pgsql, mbstring, xml, imap, zip, gd, curl, intl, json
- name: Install composer dependencies
run: composer install --ignore-platform-reqs --no-interaction
- name: Migrate and seed the database
run: |
php${{ matrix.php }} artisan migrate --force -n --database=testing_pgsql
php${{ matrix.php }} artisan db:seed --force -n --database=testing_pgsql
env:
DB_PORT: ${{ job.services.postgres.ports[5432] }}
- name: Run PHP tests
run: php${{ matrix.php }} ./vendor/bin/phpunit
env:
DB_PORT: ${{ job.services.postgres.ports[5432] }}

View File

@ -1,20 +1,45 @@
name: Test
name: Test App (MySQL)
on:
push:
branches:
- 'dist'
- master
workflow_dispatch:
jobs:
test:
name: Test
name: Test App (MySQL)
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.3', '7.4', '8.0', '8.1', '8.2']
steps:
- name: Install Wget
run: sudo apt install wget
- name: Download install
run: wget https://raw.githubusercontent.com/freescout-helpdesk/freescout/dist/tools/install.sh
- name: chmod installer
run: chmod u+x install.sh
- name: Run installer
run: sudo ./install.sh
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mysql, mbstring, xml, imap, zip, gd, curl, intl, json
- name: Start MySQL
run: |
sudo systemctl start mysql
- name: Setup database
run: |
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `freescout-test`;'
mysql -uroot -proot -e "CREATE USER 'freescout-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'freescout-test';"
mysql -uroot -proot -e "GRANT ALL ON \`freescout-test\`.* TO 'freescout-test'@'localhost';"
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
- name: Install composer dependencies
run: composer install --ignore-platform-reqs --no-interaction
- name: Migrate and seed the database
run: |
php${{ matrix.php }} artisan migrate --force -n --database=testing
php${{ matrix.php }} artisan db:seed --force -n --database=testing
- name: Run PHP tests
run: php${{ matrix.php }} ./vendor/bin/phpunit

3
.gitignore vendored
View File

@ -32,4 +32,5 @@ Thumbs.db
/storage/.installed
/tools
.well-known
/resources/lang/module.*
/resources/lang/module.*
.phpunit.result.cache

View File

@ -17,9 +17,10 @@ class TokenAuth
*/
public function handle($request, Closure $next)
{
// This is needed to restore authentication when app session expires.
if (!$request->user() && !empty($request->auth_token) && $request->cookie('in_app')) {
try {
$user = User::where(\DB::raw('md5(CONCAT(id, "'.config('app.key').'"))') , $request->auth_token)
$user = User::where(\DB::raw('md5(CONCAT(id, created_at, "'.config('app.key').'"))'), $request->auth_token)
->first();
} catch (\Exception $e) {
\Helper::logException($e, '[TokenAuth]');

View File

@ -259,7 +259,7 @@ class SendReplyToCustomer implements ShouldQueue
if (!$new && !$is_forward) {
$subject = 'Re: '.$subject;
}
$subject = \Eventy::filter('email.reply_to_customer.subject', $subject, $this->conversation);
$subject = \Eventy::filter('email.reply_to_customer.subject', $subject, $this->conversation, $this->last_thread);
$this->threads = \Eventy::filter('email.reply_to_customer.threads', $this->threads, $this->conversation, $mailbox);
$headers['X-FreeScout-Mail-Type'] = 'customer.message';

View File

@ -39,6 +39,8 @@ class Helper
*/
const DIR_PERMISSIONS = 0755;
public static $csp_nonce = null;
/**
* Stores list of global entities (for caching).
*/
@ -1997,4 +1999,35 @@ class Helper
return array_merge($default_params, $params);
}
public static function cspNonce()
{
if (self::$csp_nonce === null) {
self::$csp_nonce = \Str::random(25);
}
return self::$csp_nonce;
}
public static function cspMetaTag()
{
if (!config('app.csp_enabled')) {
return '';
}
$nonce = \Helper::cspNonce();
return "<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'self' 'nonce-".$nonce."' "
.config('app.csp_script_src').' '.\Eventy::filter('csp.script_src', '')."\">
<meta property=\"csp-nonce\" id=\"csp-nonce\" content=\"".$nonce."\">";
}
public static function cspNonceAttr()
{
if (!config('app.csp_enabled')) {
return '';
}
return ' nonce="'.\Helper::cspNonce().'"';
}
}

View File

@ -1118,7 +1118,7 @@ class User extends Authenticatable
public function getAuthToken()
{
return md5($this->id.config('app.key'));
return md5($this->id.''.$this->created_at.config('app.key'));
}
public static function findNonDeleted($id, $extended = false)

View File

@ -45,7 +45,7 @@
"filp/whoops": "2.14.5",
"fzaninotto/faker": "v1.9.2",
"mockery/mockery": "1.1.0",
"phpunit/phpunit": "6.5.8",
"phpunit/phpunit": "9.5.28",
"symfony/polyfill-ctype": "v1.10.*",
"vlucas/phpdotenv": "v2.5.1",
@ -69,12 +69,12 @@
"guzzlehttp/guzzle": "6.5.8",
"guzzlehttp/psr7": "1.9.1",
"tedivm/jshrink": "1.4.0",
"nikic/php-parser": "v4.1.0",
"nikic/php-parser": "^4.1",
"doctrine/annotations": "v1.4.*",
"doctrine/cache": "v1.6.*",
"doctrine/collections": "v1.4.*",
"doctrine/instantiator": "1.0.5",
"myclabs/deep-copy": "1.7.0",
"doctrine/instantiator": "1.3.1",
"myclabs/deep-copy": "1.10.1",
"webmozart/assert": "1.3.0",
"psr/container": "1.0.0",
"psr/http-message": "1.0.1"
@ -105,6 +105,7 @@
"Codedge\\Updater\\SourceRepositoryTypes\\": "overrides/codedge/laravel-selfupdater/src/SourceRepositoryTypes/",
"Barryvdh\\TranslationManager\\": "overrides/barryvdh/laravel-translation-manager/src/",
"Illuminate\\Foundation\\": "overrides/laravel/framework/src/Illuminate/Foundation/",
"Illuminate\\Foundation\\Testing\\": "overrides/laravel/framework/src/Illuminate/Foundation/Testing/",
"Illuminate\\Foundation\\Http\\Middleware\\": "overrides/laravel/framework/src/Illuminate/Foundation/Http/Middleware/",
"Illuminate\\Routing\\": "overrides/laravel/framework/src/Illuminate/Routing/",
"Illuminate\\Broadcasting\\Broadcasters\\": "overrides/laravel/framework/src/Illuminate/Broadcasting/Broadcasters/",
@ -122,6 +123,7 @@
"Symfony\\Component\\HttpFoundation\\": "overrides/symfony/http-foundation/",
"Symfony\\Component\\HttpFoundation\\File\\MimeType\\": "overrides/symfony/http-foundation/File/MimeType/",
"Chumper\\Zipper\\Repositories\\": "overrides/chumper/zipper/src/Chumper/Zipper/Repositories/",
"Faker\\Provider\\": "overrides/fzaninotto/faker/src/Faker/Provider/",
"Illuminate\\Support\\": "overrides/laravel/framework/src/Illuminate/Support/",
"Illuminate\\Http\\": "overrides/laravel/framework/src/Illuminate/Http/",
@ -135,6 +137,7 @@
"Illuminate\\Config\\": "overrides/laravel/framework/src/Illuminate/Config/",
"Doctrine\\DBAL\\Driver\\": "overrides/doctrine/dbal/lib/Doctrine/DBAL/Driver/",
"Doctrine\\DBAL\\Schema\\": "overrides/doctrine/dbal/lib/Doctrine/DBAL/Schema/",
"Doctrine\\DBAL\\Platforms\\": "overrides/doctrine/dbal/lib/Doctrine/DBAL/Platforms/",
"Symfony\\Component\\Finder\\Iterator\\": "overrides/symfony/finder/Iterator/",
"Symfony\\Component\\Finder\\": "overrides/symfony/finder/",
"Symfony\\Component\\Console\\Helper\\": "overrides/symfony/console/Helper/",
@ -189,6 +192,7 @@
"vendor/barryvdh/laravel-translation-manager/src/Controller.php",
"vendor/laravel/framework/src/Illuminate/Foundation/ProviderRepository.php",
"vendor/laravel/framework/src/Illuminate/Foundation/PackageManifest.php",
"vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php",
"vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php",
"vendor/laravel/framework/src/Illuminate/Routing/UrlGenerator.php",
"vendor/laravel/framework/src/Illuminate/Routing/RouteSignatureParameters.php",
@ -213,6 +217,7 @@
"vendor/symfony/http-foundation/Cookie.php",
"vendor/symfony/http-foundation/File/MimeType/FileBinaryMimeTypeGuesser.php",
"vendor/chumper/zipper/src/Chumper/Zipper/Repositories/ZipRepository.php",
"vendor/fzaninotto/faker/src/Faker/Provider/Base.php",
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php",
"vendor/laravel/framework/src/Illuminate/Support/Collection.php",
@ -239,6 +244,7 @@
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php",
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatementImplementations.php",
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php",
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php",
"vendor/laravel/framework/src/Illuminate/Support/Carbon.php",
"vendor/laravel/framework/src/Illuminate/Support/Str.php",
"vendor/laravel/framework/src/Illuminate/Support/ViewErrorBag.php",
@ -257,6 +263,7 @@
"vendor/symfony/finder/Iterator/DateRangeFilterIterator.php",
"vendor/symfony/console/Helper/HelperSet.php",
"vendor/maximebf/debugbar/src/DebugBar/DebugBar.php",
"vendor/maximebf/debugbar/src/DebugBar/JavascriptRenderer.php",
"vendor/maximebf/debugbar/src/DebugBar/DataFormatter/DataFormatter.php",
"vendor/laravel/framework/src/Illuminate/Cache/Console/ClearCommand.php",
"vendor/vlucas/phpdotenv/src/Loader.php",

1265
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ return [
| or any other location as required by the application or its packages.
*/
'version' => '1.8.100',
'version' => '1.8.101',
/*
|--------------------------------------------------------------------------
@ -264,6 +264,7 @@ return [
|--------------------------------------------------------------------------
| File types which should be viewed in the browser instead of downloading.
| SVG images are not viewable to avid XSS.
| The list should be in sync with /storage/app/public/uploads/.htaccess and nginx config.
|-------------------------------------------------------------------------
*/
'viewable_attachments' => env('APP_VIEWABLE_ATTACHMENTS', ['jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp', 'apng', 'bmp', 'gif', 'ico', 'cur', 'png', 'tif', 'tiff', 'webp', 'pdf', 'txt', 'diff', 'patch', 'json', 'mp3', 'wav', 'ogg', 'wma']),
@ -443,6 +444,14 @@ return [
*/
'show_only_assigned_conversations' => env('APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS', ''),
/*
|--------------------------------------------------------------------------
| Enable Content-Security-Policy meta tag to prevent possible XSS attacks.
|-------------------------------------------------------------------------
*/
'csp_enabled' => env('APP_CSP_ENABLED', false),
'csp_script_src' => env('APP_CSP_SCRIPT_SRC', ''),
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers

View File

@ -58,15 +58,32 @@ return [
],
'testing' => [
'driver' => 'mysql',
'host' => env('DB_TEST_HOST', 'localhost'),
'database' => env('DB_TEST_DATABASE', 'homestead_test'),
'username' => env('DB_TEST_USERNAME', 'homestead'),
'password' => env('DB_TEST_PASSWORD', 'secret'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => false,
'driver' => 'mysql',
//'url' => env('DB_TEST_DATABASE_URL'),
'host' => '127.0.0.1',
'database' => 'freescout-test',
'username' => env('DB_TEST_USERNAME', 'freescout-test'),
'password' => env('DB_TEST_PASSWORD', 'freescout-test'),
//'port' => env('DB_TEST_PORT', '3306'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
//'prefix_indexes' => true,
'strict' => false,
'engine' => null,
],
'testing_pgsql' => [
'driver' => 'pgsql',
'host' => 'localhost',
'port' => '5432',
'database' => 'freescout-test',
'username' => env('DB_TEST_USERNAME', 'freescout-test'),
'password' => env('DB_TEST_PASSWORD', 'freescout-test'),
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
'sslmode' => 'prefer',
],
'pgsql' => [

View File

@ -54,7 +54,7 @@ class JavascriptRenderer extends BaseJavascriptRenderer
$html .= "<script type='text/javascript' src='{$jsRoute}'></script>";
if ($this->isJqueryNoConflictEnabled()) {
$html .= '<script type="text/javascript">jQuery.noConflict(true);</script>' . "\n";
$html .= '<script type="text/javascript" '.\Helper::cspNonceAttr().'>jQuery.noConflict(true);</script>' . "\n";
}
$html .= $this->getInlineHtml();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,616 @@
<?php
namespace Faker\Provider;
use Faker\Generator;
use Faker\DefaultGenerator;
use Faker\UniqueGenerator;
use Faker\ValidGenerator;
class Base
{
/**
* @var \Faker\Generator
*/
protected $generator;
/**
* @var \Faker\UniqueGenerator
*/
protected $unique;
/**
* @param \Faker\Generator $generator
*/
public function __construct(Generator $generator)
{
$this->generator = $generator;
}
/**
* Returns a random number between 0 and 9
*
* @return integer
*/
public static function randomDigit()
{
return mt_rand(0, 9);
}
/**
* Returns a random number between 1 and 9
*
* @return integer
*/
public static function randomDigitNotNull()
{
return mt_rand(1, 9);
}
/**
* Generates a random digit, which cannot be $except
*
* @param int $except
* @return int
*/
public static function randomDigitNot($except)
{
$result = self::numberBetween(0, 8);
if ($result >= $except) {
$result++;
}
return $result;
}
/**
* Returns a random integer with 0 to $nbDigits digits.
*
* The maximum value returned is mt_getrandmax()
*
* @param integer $nbDigits Defaults to a random number between 1 and 9
* @param boolean $strict Whether the returned number should have exactly $nbDigits
* @example 79907610
*
* @return integer
*/
public static function randomNumber($nbDigits = null, $strict = false)
{
if (!is_bool($strict)) {
throw new \InvalidArgumentException('randomNumber() generates numbers of fixed width. To generate numbers between two boundaries, use numberBetween() instead.');
}
if (null === $nbDigits) {
$nbDigits = static::randomDigitNotNull();
}
$max = pow(10, $nbDigits) - 1;
if ($max > mt_getrandmax()) {
throw new \InvalidArgumentException('randomNumber() can only generate numbers up to mt_getrandmax()');
}
if ($strict) {
return mt_rand(pow(10, $nbDigits - 1), $max);
}
return mt_rand(0, $max);
}
/**
* Return a random float number
*
* @param int $nbMaxDecimals
* @param int|float $min
* @param int|float $max
* @example 48.8932
*
* @return float
*/
public static function randomFloat($nbMaxDecimals = null, $min = 0, $max = null)
{
if (null === $nbMaxDecimals) {
$nbMaxDecimals = static::randomDigit();
}
if (null === $max) {
$max = static::randomNumber();
if ($min > $max) {
$max = $min;
}
}
if ($min > $max) {
$tmp = $min;
$min = $max;
$max = $tmp;
}
return round($min + mt_rand() / mt_getrandmax() * ($max - $min), $nbMaxDecimals);
}
/**
* Returns a random number between $int1 and $int2 (any order)
*
* @param integer $int1 default to 0
* @param integer $int2 defaults to 32 bit max integer, ie 2147483647
* @example 79907610
*
* @return integer
*/
public static function numberBetween($int1 = 0, $int2 = 2147483647)
{
$min = $int1 < $int2 ? $int1 : $int2;
$max = $int1 < $int2 ? $int2 : $int1;
return mt_rand($min, $max);
}
/**
* Returns the passed value
*
* @param mixed $value
*
* @return mixed
*/
public static function passthrough($value)
{
return $value;
}
/**
* Returns a random letter from a to z
*
* @return string
*/
public static function randomLetter()
{
return chr(mt_rand(97, 122));
}
/**
* Returns a random ASCII character (excluding accents and special chars)
*/
public static function randomAscii()
{
return chr(mt_rand(33, 126));
}
/**
* Returns randomly ordered subsequence of $count elements from a provided array
*
* @param array $array Array to take elements from. Defaults to a-c
* @param integer $count Number of elements to take.
* @param boolean $allowDuplicates Allow elements to be picked several times. Defaults to false
* @throws \LengthException When requesting more elements than provided
*
* @return array New array with $count elements from $array
*/
public static function randomElements($array = array('a', 'b', 'c'), $count = 1, $allowDuplicates = false)
{
$traversables = array();
if ($array instanceof \Traversable) {
foreach ($array as $element) {
$traversables[] = $element;
}
}
$arr = count($traversables) ? $traversables : $array;
$allKeys = array_keys($arr);
$numKeys = count($allKeys);
if (!$allowDuplicates && $numKeys < $count) {
throw new \LengthException(sprintf('Cannot get %d elements, only %d in array', $count, $numKeys));
}
$highKey = $numKeys - 1;
$keys = $elements = array();
$numElements = 0;
while ($numElements < $count) {
$num = mt_rand(0, $highKey);
if (!$allowDuplicates) {
if (isset($keys[$num])) {
continue;
}
$keys[$num] = true;
}
$elements[] = $arr[$allKeys[$num]];
$numElements++;
}
return $elements;
}
/**
* Returns a random element from a passed array
*
* @param array $array
* @return mixed
*/
public static function randomElement($array = array('a', 'b', 'c'))
{
if (!$array || ($array instanceof \Traversable && !count($array))) {
return null;
}
$elements = static::randomElements($array, 1);
return $elements[0];
}
/**
* Returns a random key from a passed associative array
*
* @param array $array
* @return int|string|null
*/
public static function randomKey($array = array())
{
if (!$array) {
return null;
}
$keys = array_keys($array);
$key = $keys[mt_rand(0, count($keys) - 1)];
return $key;
}
/**
* Returns a shuffled version of the argument.
*
* This function accepts either an array, or a string.
*
* @example $faker->shuffle([1, 2, 3]); // [2, 1, 3]
* @example $faker->shuffle('hello, world'); // 'rlo,h eold!lw'
*
* @see shuffleArray()
* @see shuffleString()
*
* @param array|string $arg The set to shuffle
* @return array|string The shuffled set
*/
public static function shuffle($arg = '')
{
if (is_array($arg)) {
return static::shuffleArray($arg);
}
if (is_string($arg)) {
return static::shuffleString($arg);
}
throw new \InvalidArgumentException('shuffle() only supports strings or arrays');
}
/**
* Returns a shuffled version of the array.
*
* This function does not mutate the original array. It uses the
* FisherYates algorithm, which is unbiased, together with a Mersenne
* twister random generator. This function is therefore more random than
* PHP's shuffle() function, and it is seedable.
*
* @link http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
*
* @example $faker->shuffleArray([1, 2, 3]); // [2, 1, 3]
*
* @param array $array The set to shuffle
* @return array The shuffled set
*/
public static function shuffleArray($array = array())
{
$shuffledArray = array();
$i = 0;
reset($array);
foreach ($array as $key => $value) {
if ($i == 0) {
$j = 0;
} else {
$j = mt_rand(0, $i);
}
if ($j == $i) {
$shuffledArray[]= $value;
} else {
$shuffledArray[]= $shuffledArray[$j];
$shuffledArray[$j] = $value;
}
$i++;
}
return $shuffledArray;
}
/**
* Returns a shuffled version of the string.
*
* This function does not mutate the original string. It uses the
* FisherYates algorithm, which is unbiased, together with a Mersenne
* twister random generator. This function is therefore more random than
* PHP's shuffle() function, and it is seedable. Additionally, it is
* UTF8 safe if the mb extension is available.
*
* @link http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
*
* @example $faker->shuffleString('hello, world'); // 'rlo,h eold!lw'
*
* @param string $string The set to shuffle
* @param string $encoding The string encoding (defaults to UTF-8)
* @return string The shuffled set
*/
public static function shuffleString($string = '', $encoding = 'UTF-8')
{
if (function_exists('mb_strlen')) {
// UTF8-safe str_split()
$array = array();
$strlen = mb_strlen($string, $encoding);
for ($i = 0; $i < $strlen; $i++) {
$array []= mb_substr($string, $i, 1, $encoding);
}
} else {
$array = str_split($string, 1);
}
return implode('', static::shuffleArray($array));
}
private static function replaceWildcard($string, $wildcard = '#', $callback = 'static::randomDigit')
{
if (($pos = strpos($string, $wildcard)) === false) {
return $string;
}
for ($i = $pos, $last = strrpos($string, $wildcard, $pos) + 1; $i < $last; $i++) {
if ($string[$i] === $wildcard) {
if (strspn($callback, 'static::')) {
$string[$i] = call_user_func([static::class, str_replace('static::', '', $callback)]);
} else {
$string[$i] = call_user_func($callback);
}
}
}
return $string;
}
/**
* Replaces all hash sign ('#') occurrences with a random number
* Replaces all percentage sign ('%') occurrences with a not null number
*
* @param string $string String that needs to bet parsed
* @return string
*/
public static function numerify($string = '###')
{
// instead of using randomDigit() several times, which is slow,
// count the number of hashes and generate once a large number
$toReplace = array();
if (($pos = strpos($string, '#')) !== false) {
for ($i = $pos, $last = strrpos($string, '#', $pos) + 1; $i < $last; $i++) {
if ($string[$i] === '#') {
$toReplace[] = $i;
}
}
}
if ($nbReplacements = count($toReplace)) {
$maxAtOnce = strlen((string) mt_getrandmax()) - 1;
$numbers = '';
$i = 0;
while ($i < $nbReplacements) {
$size = min($nbReplacements - $i, $maxAtOnce);
$numbers .= str_pad(static::randomNumber($size), $size, '0', STR_PAD_LEFT);
$i += $size;
}
for ($i = 0; $i < $nbReplacements; $i++) {
$string[$toReplace[$i]] = $numbers[$i];
}
}
$string = self::replaceWildcard($string, '%', 'static::randomDigitNotNull');
return $string;
}
/**
* Replaces all question mark ('?') occurrences with a random letter
*
* @param string $string String that needs to bet parsed
* @return string
*/
public static function lexify($string = '????')
{
return self::replaceWildcard($string, '?', 'static::randomLetter');
}
/**
* Replaces hash signs ('#') and question marks ('?') with random numbers and letters
* An asterisk ('*') is replaced with either a random number or a random letter
*
* @param string $string String that needs to bet parsed
* @return string
*/
public static function bothify($string = '## ??')
{
$string = self::replaceWildcard($string, '*', function () {
return mt_rand(0, 1) ? '#' : '?';
});
return static::lexify(static::numerify($string));
}
/**
* Replaces * signs with random numbers and letters and special characters
*
* @example $faker->asciify(''********'); // "s5'G!uC3"
*
* @param string $string String that needs to bet parsed
* @return string
*/
public static function asciify($string = '****')
{
return preg_replace_callback('/\*/u', 'static::randomAscii', $string);
}
/**
* Transforms a basic regular expression into a random string satisfying the expression.
*
* @example $faker->regexify('[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}'); // sm0@y8k96a.ej
*
* Regex delimiters '/.../' and begin/end markers '^...$' are ignored.
*
* Only supports a small subset of the regex syntax. For instance,
* unicode, negated classes, unbounded ranges, subpatterns, back references,
* assertions, recursive patterns, and comments are not supported. Escaping
* support is extremely fragile.
*
* This method is also VERY slow. Use it only when no other formatter
* can generate the fake data you want. For instance, prefer calling
* `$faker->email` rather than `regexify` with the previous regular
* expression.
*
* Also note than `bothify` can probably do most of what this method does,
* but much faster. For instance, for a dummy email generation, try
* `$faker->bothify('?????????@???.???')`.
*
* @see https://github.com/icomefromthenet/ReverseRegex for a more robust implementation
*
* @param string $regex A regular expression (delimiters are optional)
* @return string
*/
public static function regexify($regex = '')
{
// ditch the anchors
$regex = preg_replace('/^\/?\^?/', '', $regex);
$regex = preg_replace('/\$?\/?$/', '', $regex);
// All {2} become {2,2}
$regex = preg_replace('/\{(\d+)\}/', '{\1,\1}', $regex);
// Single-letter quantifiers (?, *, +) become bracket quantifiers ({0,1}, {0,rand}, {1, rand})
$regex = preg_replace('/(?<!\\\)\?/', '{0,1}', $regex);
$regex = preg_replace('/(?<!\\\)\*/', '{0,' . static::randomDigitNotNull() . '}', $regex);
$regex = preg_replace('/(?<!\\\)\+/', '{1,' . static::randomDigitNotNull() . '}', $regex);
// [12]{1,2} becomes [12] or [12][12]
$regex = preg_replace_callback('/(\[[^\]]+\])\{(\d+),(\d+)\}/', function ($matches) {
return str_repeat($matches[1], Base::randomElement(range($matches[2], $matches[3])));
}, $regex);
// (12|34){1,2} becomes (12|34) or (12|34)(12|34)
$regex = preg_replace_callback('/(\([^\)]+\))\{(\d+),(\d+)\}/', function ($matches) {
return str_repeat($matches[1], Base::randomElement(range($matches[2], $matches[3])));
}, $regex);
// A{1,2} becomes A or AA or \d{3} becomes \d\d\d
$regex = preg_replace_callback('/(\\\?.)\{(\d+),(\d+)\}/', function ($matches) {
return str_repeat($matches[1], Base::randomElement(range($matches[2], $matches[3])));
}, $regex);
// (this|that) becomes 'this' or 'that'
$regex = preg_replace_callback('/\((.*?)\)/', function ($matches) {
return Base::randomElement(explode('|', str_replace(array('(', ')'), '', $matches[1])));
}, $regex);
// All A-F inside of [] become ABCDEF
$regex = preg_replace_callback('/\[([^\]]+)\]/', function ($matches) {
return '[' . preg_replace_callback('/(\w|\d)\-(\w|\d)/', function ($range) {
return implode('', range($range[1], $range[2]));
}, $matches[1]) . ']';
}, $regex);
// All [ABC] become B (or A or C)
$regex = preg_replace_callback('/\[([^\]]+)\]/', function ($matches) {
return Base::randomElement(str_split($matches[1]));
}, $regex);
// replace \d with number and \w with letter and . with ascii
$regex = preg_replace_callback('/\\\w/', 'static::randomLetter', $regex);
$regex = preg_replace_callback('/\\\d/', 'static::randomDigit', $regex);
$regex = preg_replace_callback('/(?<!\\\)\./', 'static::randomAscii', $regex);
// remove remaining backslashes
$regex = str_replace('\\', '', $regex);
// phew
return $regex;
}
/**
* Converts string to lowercase.
* Uses mb_string extension if available.
*
* @param string $string String that should be converted to lowercase
* @return string
*/
public static function toLower($string = '')
{
return extension_loaded('mbstring') ? mb_strtolower($string, 'UTF-8') : strtolower($string);
}
/**
* Converts string to uppercase.
* Uses mb_string extension if available.
*
* @param string $string String that should be converted to uppercase
* @return string
*/
public static function toUpper($string = '')
{
return extension_loaded('mbstring') ? mb_strtoupper($string, 'UTF-8') : strtoupper($string);
}
/**
* Chainable method for making any formatter optional.
*
* @param float|integer $weight Set the probability of receiving a null value.
* "0" will always return null, "1" will always return the generator.
* If $weight is an integer value, then the same system works
* between 0 (always get false) and 100 (always get true).
* @return mixed|null
*/
public function optional($weight = 0.5, $default = null)
{
// old system based on 0.1 <= $weight <= 0.9
// TODO: remove in v2
if ($weight > 0 && $weight < 1 && mt_rand() / mt_getrandmax() <= $weight) {
return $this->generator;
}
// new system with percentage
if (is_int($weight) && mt_rand(1, 100) <= $weight) {
return $this->generator;
}
return new DefaultGenerator($default);
}
/**
* Chainable method for making any formatter unique.
*
* <code>
* // will never return twice the same value
* $faker->unique()->randomElement(array(1, 2, 3));
* </code>
*
* @param boolean $reset If set to true, resets the list of existing values
* @param integer $maxRetries Maximum number of retries to find a unique value,
* After which an OverflowException is thrown.
* @throws \OverflowException When no unique value can be found by iterating $maxRetries times
*
* @return UniqueGenerator A proxy class returning only non-existing values
*/
public function unique($reset = false, $maxRetries = 10000)
{
if ($reset || !$this->unique) {
$this->unique = new UniqueGenerator($this->generator, $maxRetries);
}
return $this->unique;
}
/**
* Chainable method for forcing any formatter to return only valid values.
*
* The value validity is determined by a function passed as first argument.
*
* <code>
* $values = array();
* $evenValidator = function ($digit) {
* return $digit % 2 === 0;
* };
* for ($i=0; $i < 10; $i++) {
* $values []= $faker->valid($evenValidator)->randomDigit;
* }
* print_r($values); // [0, 4, 8, 4, 2, 6, 0, 8, 8, 6]
* </code>
*
* @param Closure $validator A function returning true for valid values
* @param integer $maxRetries Maximum number of retries to find a unique value,
* After which an OverflowException is thrown.
* @throws \OverflowException When no valid value can be found by iterating $maxRetries times
*
* @return ValidGenerator A proxy class returning only valid values
*/
public function valid($validator = null, $maxRetries = 10000)
{
return new ValidGenerator($this->generator, $validator, $maxRetries);
}
}

View File

@ -0,0 +1,200 @@
<?php
namespace Illuminate\Foundation\Testing;
use Mockery;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Facade;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Console\Application as Artisan;
use PHPUnit\Framework\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use Concerns\InteractsWithContainer,
Concerns\MakesHttpRequests,
Concerns\InteractsWithAuthentication,
Concerns\InteractsWithConsole,
Concerns\InteractsWithDatabase,
Concerns\InteractsWithExceptionHandling,
Concerns\InteractsWithSession,
Concerns\MocksApplicationServices;
/**
* The Illuminate application instance.
*
* @var \Illuminate\Foundation\Application
*/
protected $app;
/**
* The callbacks that should be run after the application is created.
*
* @var array
*/
protected $afterApplicationCreatedCallbacks = [];
/**
* The callbacks that should be run before the application is destroyed.
*
* @var array
*/
protected $beforeApplicationDestroyedCallbacks = [];
/**
* Indicates if we have made it through the base setUp function.
*
* @var bool
*/
protected $setUpHasRun = false;
/**
* Creates the application.
*
* Needs to be implemented by subclasses.
*
* @return \Symfony\Component\HttpKernel\HttpKernelInterface
*/
abstract public function createApplication();
/**
* Setup the test environment.
*
* @return void
*/
protected function setUp() : void
{
if (! $this->app) {
$this->refreshApplication();
}
$this->setUpTraits();
foreach ($this->afterApplicationCreatedCallbacks as $callback) {
call_user_func($callback);
}
Facade::clearResolvedInstances();
Model::setEventDispatcher($this->app['events']);
$this->setUpHasRun = true;
}
/**
* Refresh the application instance.
*
* @return void
*/
protected function refreshApplication()
{
$this->app = $this->createApplication();
}
/**
* Boot the testing helper traits.
*
* @return array
*/
protected function setUpTraits()
{
$uses = array_flip(class_uses_recursive(static::class));
if (isset($uses[RefreshDatabase::class])) {
$this->refreshDatabase();
}
if (isset($uses[DatabaseMigrations::class])) {
$this->runDatabaseMigrations();
}
if (isset($uses[DatabaseTransactions::class])) {
$this->beginDatabaseTransaction();
}
if (isset($uses[WithoutMiddleware::class])) {
$this->disableMiddlewareForAllTests();
}
if (isset($uses[WithoutEvents::class])) {
$this->disableEventsForAllTests();
}
if (isset($uses[WithFaker::class])) {
$this->setUpFaker();
}
return $uses;
}
/**
* Clean up the testing environment before the next test.
*
* @return void
*/
protected function tearDown() : void
{
if ($this->app) {
foreach ($this->beforeApplicationDestroyedCallbacks as $callback) {
call_user_func($callback);
}
$this->app->flush();
$this->app = null;
}
$this->setUpHasRun = false;
if (property_exists($this, 'serverVariables')) {
$this->serverVariables = [];
}
if (property_exists($this, 'defaultHeaders')) {
$this->defaultHeaders = [];
}
if (class_exists('Mockery')) {
if ($container = Mockery::getContainer()) {
$this->addToAssertionCount($container->mockery_getExpectationCount());
}
Mockery::close();
}
if (class_exists(Carbon::class)) {
Carbon::setTestNow();
}
$this->afterApplicationCreatedCallbacks = [];
$this->beforeApplicationDestroyedCallbacks = [];
Artisan::forgetBootstrappers();
}
/**
* Register a callback to be run after the application is created.
*
* @param callable $callback
* @return void
*/
public function afterApplicationCreated(callable $callback)
{
$this->afterApplicationCreatedCallbacks[] = $callback;
if ($this->setUpHasRun) {
call_user_func($callback);
}
}
/**
* Register a callback to be run before the application is destroyed.
*
* @param callable $callback
* @return void
*/
protected function beforeApplicationDestroyed(callable $callback)
{
$this->beforeApplicationDestroyedCallbacks[] = $callback;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -128,8 +128,7 @@ class HtmlDumper extends CliDumper
return $this->dumpHeader;
}
$line = str_replace('{$options}', json_encode($this->displayOptions, \JSON_FORCE_OBJECT), <<<'EOHTML'
<script>
$line = str_replace('{$options}', json_encode($this->displayOptions, \JSON_FORCE_OBJECT), '<script '.\Helper::cspNonceAttr().'>'.<<<'EOHTML'
Sfdump = window.Sfdump || (function (doc) {
var refStyle = doc.createElement('style'),

162
phpcs.xml Normal file
View File

@ -0,0 +1,162 @@
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="PHP_CodeSniffer" xsi:noNamespaceSchemaLocation="phpcs.xsd">
<description>Coding Standard</description>
<file>app</file>
<file>config</file>
<file>public</file>
<file>resources</file>
<file>routes</file>
<file>tests</file>
<arg name="basepath" value="."/>
<arg name="colors"/>
<arg name="parallel" value="75"/>
<arg value="spv"/>
<ini name="memory_limit" value="128M"/>
<rule ref="Generic.Classes.DuplicateClassName">
<exclude name="Generic.CodeAnalysis.EmptyStatement.DetectedIf"/>
</rule>
<rule ref="Generic.CodeAnalysis.EmptyStatement"/>
<rule ref="Generic.CodeAnalysis.ForLoopShouldBeWhileLoop"/>
<rule ref="Generic.CodeAnalysis.ForLoopWithTestFunctionCall"/>
<rule ref="Generic.CodeAnalysis.JumbledIncrementer"/>
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
<rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
<rule ref="Generic.CodeAnalysis.UnusedFunctionParameter">
<exclude-pattern>/app/Http/Resources/*\.php</exclude-pattern>
</rule>
<rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
<rule ref="Generic.Commenting.DocComment">
<exclude name="Generic.Commenting.DocComment.TagValueIndent"/>
<exclude name="Generic.Commenting.DocComment.NonParamGroup"/>
</rule>
<rule ref="Generic.ControlStructures.InlineControlStructure"/>
<rule ref="Generic.Files.ByteOrderMark"/>
<rule ref="Generic.Files.LineEndings">
<exclude name="Generic.Files.LineEndings.InvalidEOLChar"/>
</rule>
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
<rule ref="Generic.Formatting.SpaceAfterCast"/>
<rule ref="Generic.Functions.CallTimePassByReference"/>
<rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
<rule ref="Generic.Functions.OpeningFunctionBraceBsdAllman"/>
<rule ref="Generic.Metrics.CyclomaticComplexity">
<properties>
<property name="complexity" value="20"/>
<property name="absoluteComplexity" value="50"/>
</properties>
</rule>
<rule ref="Generic.Metrics.NestingLevel">
<properties>
<property name="nestingLevel" value="5"/>
<property name="absoluteNestingLevel" value="15"/>
</properties>
</rule>
<rule ref="Generic.NamingConventions.ConstructorName"/>
<rule ref="Generic.PHP.LowerCaseConstant"/>
<rule ref="Generic.PHP.DeprecatedFunctions"/>
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
<rule ref="Generic.PHP.ForbiddenFunctions"/>
<rule ref="Generic.PHP.NoSilencedErrors"/>
<rule ref="Generic.WhiteSpace.DisallowTabIndent"/>
<rule ref="Generic.WhiteSpace.ScopeIndent">
<properties>
<property name="indent" value="4"/>
<property name="tabIndent" value="true"/>
</properties>
</rule>
<rule ref="MySource.PHP.EvalObjectFactory"/>
<rule ref="PSR1.Classes.ClassDeclaration"/>
<rule ref="PSR1.Files.SideEffects"/>
<rule ref="PSR2.Classes.ClassDeclaration"/>
<rule ref="PSR2.Classes.PropertyDeclaration"/>
<rule ref="PSR2.ControlStructures.ElseIfDeclaration"/>
<rule ref="PSR2.ControlStructures.SwitchDeclaration"/>
<rule ref="PSR2.Files.EndFileNewline"/>
<rule ref="PSR2.Methods.MethodDeclaration"/>
<rule ref="PSR2.Namespaces.NamespaceDeclaration"/>
<rule ref="PSR2.Namespaces.UseDeclaration"/>
<rule ref="PSR1">
<exclude-pattern>*.php</exclude-pattern>
<exclude name="PSR1.Methods.CamelCapsMethodName.NotCamelCaps"/>
<exclude-pattern>database/*</exclude-pattern>
</rule>
<rule ref="PSR2">
<exclude name="PSR2.ControlStructures.SwitchDeclaration.BodyOnNextLineCASE" />
</rule>
<rule ref="Squiz.Arrays.ArrayDeclaration">
<exclude name="Squiz.Arrays.ArrayDeclaration.ValueNotAligned" />
<exclude name="Squiz.Arrays.ArrayDeclaration.KeyNotAligned" />
<exclude name="Squiz.Arrays.ArrayDeclaration.DoubleArrowNotAligned" />
<exclude name="Squiz.Arrays.ArrayDeclaration.ValueNotAligned" />
<exclude name="Squiz.Arrays.ArrayDeclaration.CloseBraceNotAligned" />
<exclude name="Squiz.Arrays.ArrayDeclaration.ValueNoNewline" />
<exclude name="Squiz.Arrays.ArrayDeclaration.MultiLineNotAllowed" />
<exclude name="Squiz.Arrays.ArrayDeclaration.SingleLineNotAllowed" />
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.NewlineBeforeOpenBrace" />
<exclude name="Squiz.Arrays.ArrayDeclaration.NoKeySpecified" />
<exclude name="Squiz.Arrays.ArrayDeclaration.KeySpecified" />
</rule>
<rule ref="Squiz.PHP.DisallowSizeFunctionsInLoops"/>
<rule ref="Squiz.PHP.DiscouragedFunctions">
<properties>
<property name="error" value="true"/>
</properties>
</rule>
<rule ref="Squiz.PHP.Eval"/>
<rule ref="Squiz.PHP.GlobalKeyword"/>
<rule ref="Squiz.PHP.LowercasePHPFunctions"/>
<rule ref="Squiz.PHP.NonExecutableCode"/>
<rule ref="Squiz.Scope.MemberVarScope"/>
<rule ref="Squiz.Scope.MethodScope"/>
<rule ref="Squiz.Scope.StaticThisUsage"/>
<rule ref="Squiz.WhiteSpace.CastSpacing"/>
<rule ref="Squiz.WhiteSpace.LanguageConstructSpacing"/>
<rule ref="Squiz.WhiteSpace.LogicalOperatorSpacing"/>
<rule ref="Squiz.WhiteSpace.ObjectOperatorSpacing">
<properties>
<property name="ignoreNewlines" value="true"/>
</properties>
</rule>
<rule ref="Squiz.WhiteSpace.OperatorSpacing">
<properties>
<property name="ignoreNewlines" value="true"/>
</properties>
</rule>
<rule ref="Squiz.WhiteSpace.PropertyLabelSpacing"/>
<rule ref="Squiz.WhiteSpace.ScopeClosingBrace"/>
<rule ref="Squiz.WhiteSpace.ScopeKeywordSpacing"/>
<rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
<rule ref="Zend.Files.ClosingTag"/>
<rule ref="PSR2.ControlStructures.ControlStructureSpacing">
<properties>
<property name="maxLineLength" value="1000" />
</properties>
</rule>
<exclude-pattern>*/.phpstorm.meta.php</exclude-pattern>
<exclude-pattern>*/_ide_helper.php</exclude-pattern>
<exclude-pattern>*/database/*</exclude-pattern>
<exclude-pattern>*/cache/*</exclude-pattern>
<exclude-pattern>*/*.js</exclude-pattern>
<exclude-pattern>*/*.css</exclude-pattern>
<exclude-pattern>*/*.xml</exclude-pattern>
<exclude-pattern>*/*.blade.php</exclude-pattern>
<exclude-pattern>*/autoload.php</exclude-pattern>
<exclude-pattern>*/storage/*</exclude-pattern>
<exclude-pattern>*/docs/*</exclude-pattern>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/migrations/*</exclude-pattern>
<exclude-pattern>*/config/*</exclude-pattern>
<exclude-pattern>*/public/index.php</exclude-pattern>
<exclude-pattern>*/*.blade.php</exclude-pattern>
<exclude-pattern>*/Console/Kernel.php</exclude-pattern>
<exclude-pattern>*/Exceptions/Handler.php</exclude-pattern>
<exclude-pattern>*/Http/Kernel.php</exclude-pattern>
<exclude-pattern>*/Providers/*</exclude-pattern>
<exclude-pattern>*/resources/lang/*</exclude-pattern>
</ruleset>

121
public/js/main.js vendored
View File

@ -308,13 +308,31 @@ $(document).ready(function(){
}, 100);
});
$('#logout-link').click(function(e) {
$('#logout-form').submit();
e.preventDefault();
});
//applyVoidLinks();
$('ul.customer-contacts a.contact-main').click(function(e) {
copyToClipboard($(this).text());
e.preventDefault();
});
// Dirty JS hack because there was no way found to expand outer container when sidebar grows.
if ($('#conv-layout-customer').length && $(window).outerWidth() >= 1100 && $('.conv-sidebar-block').length > 2) {
adjustCustomerSidebarHeight();
setTimeout(adjustCustomerSidebarHeight, 2000);
}
}
});
/*function applyVoidLinks()
{
$('a.void-link').click(function(e) {
e.preventDefault();
});
}*/
function initMuteMailbox()
{
$('.mailbox-mute-trigger, .mailbox-mute-trigger span').click(function(e) {
@ -606,11 +624,13 @@ function fsFixEditorCodeSaving($el)
function permissionsInit()
{
$(document).ready(function(){
$('.sel-all').click(function(event) {
$('.sel-all').click(function(e) {
$("#permissions-fields input").attr('checked', 'checked');
e.preventDefault();
});
$('.sel-none').click(function(event) {
$('.sel-none').click(function(e) {
$("#permissions-fields input").removeAttr('checked');
e.preventDefault();
});
});
}
@ -813,7 +833,7 @@ function logsInit()
function multiInputInit()
{
$(document).ready(function() {
$('.multi-add').click(function() {
$('.multi-add').click(function(e) {
var clone = $(this).parents('.multi-container:first').children('.multi-item:first').clone(true, true);
var index = parseInt($(this).parents('.multi-container:first').children('.block-help:last').attr('data-max-i'));
if (isNaN(index)) {
@ -833,13 +853,16 @@ function multiInputInit()
clone.insertAfter($(this).parents('.multi-container:first').children('.multi-item:last'));
$(this).parents('.multi-container:first').children('.block-help:last').attr('data-max-i', index);
e.preventDefault();
});
$('.multi-remove').click(function() {
$('.multi-remove').click(function(e) {
if ($(this).parents('.multi-container:first').children('.multi-item').length > 1) {
$(this).parents('.multi-item:first').remove();
} else {
$(this).parents('.multi-item:first').children(':input').val('');
}
e.preventDefault();
});
} );
}
@ -1263,7 +1286,7 @@ function initConversation()
}
// Create new email conversation
function switchToNewEmailConversation(type_email)
function switchToNewEmailConversation()
{
$('#email-conv-switch').addClass('active');
$('#phone-conv-switch').removeClass('active');
@ -1276,7 +1299,7 @@ function switchToNewEmailConversation(type_email)
$('.conv-block:first').removeClass('conv-note-block').removeClass('conv-phone-block');
$('#form-create :input[name="is_note"]:first').val(0);
$('#form-create :input[name="is_phone"]:first').val(0);
$('#form-create :input[name="type"]:first').val(type_email);
$('#form-create :input[name="type"]:first').val(1);
}
// Create new phone conversation
@ -1518,9 +1541,14 @@ function showAttachments(data)
attachments_container.prepend(input_html);
// Links
var attachment_html = '<li class="atachment-upload-'+attachment.id+' attachment-loaded"><a href="'+attachment.url+'" class="break-words" target="_blank">'+attachment.name+'<span class="ellipsis">…</span> </a> <span class="text-help">('+formatBytes(attachment.size)+')</span> <i class="glyphicon glyphicon-remove" onclick="removeAttachment(\''+attachment.id+'\')"></i></li>';
var attachment_html = '<li class="atachment-upload-'+attachment.id+' attachment-loaded"><a href="'+attachment.url+'" class="break-words" target="_blank">'+attachment.name+'<span class="ellipsis">…</span> </a> <span class="text-help">('+formatBytes(attachment.size)+')</span> <i class="glyphicon glyphicon-remove" data-attachment-id="'+attachment.id+'"></i></li>';
attachments_container.find('ul:first').append(attachment_html);
// Delete attachment
$('li.attachment-loaded .glyphicon-remove:first').click(function(e) {
removeAttachment($(this).attr('data-attachment-id'));
});
attachments_container.show();
}
}
@ -1734,8 +1762,14 @@ function editorSendFile(file, attach, is_conv, editor_id, container)
// Show loader
if (attach) {
var attachment_html = '<li class="atachment-upload-'+attachment_dummy_id+'"><img src="'+Vars.public_url+'/img/loader-tiny.gif" width="16" height="16"/> <a href="javascript:void(0);" class="break-words disabled" target="_blank">'+file.name+'<span class="ellipsis">…</span> </a> <span class="text-help">('+formatBytes(file.size)+')</span> <i class="glyphicon glyphicon-remove" onclick="removeAttachment(\''+attachment_dummy_id+'\')"></i></li>';
var attachment_html = '<li class="atachment-upload-'+attachment_dummy_id+'"><img src="'+Vars.public_url+'/img/loader-tiny.gif" width="16" height="16"/> <a href="#" class="break-words disabled" target="_blank">'+file.name+'<span class="ellipsis">…</span> </a> <span class="text-help">('+formatBytes(file.size)+')</span> <i class="glyphicon glyphicon-remove" data-attachment-id="'+attachment_dummy_id+'"></i></li>';
attachments_container.children('ul:first').append(attachment_html);
// Delete attachment
$('li.atachment-upload-'+attachment_dummy_id+' .glyphicon-remove:first').click(function(e) {
removeAttachment($(this).attr('data-attachment-id'));
});
attachments_container.show();
} else {
loaderShow();
@ -1840,9 +1874,16 @@ function initNewConversation(is_phone)
if (typeof(is_phone) != "undefined") {
switchToNewPhoneConversation();
}
$('#toggle-email').click(function() {
$('#toggle-email').click(function(e) {
$('#field-to_email').show();
$(this).hide();
e.preventDefault();
});
$('#email-conv-switch').click(function() {
switchToNewEmailConversation();
});
$('#phone-conv-switch').click(function() {
switchToNewPhoneConversation();
});
});
}
@ -1951,10 +1992,11 @@ function initReplyForm(load_attachments, init_customer_selector, is_new_conv)
}
// Show CC
$('#toggle-cc').click(function() {
$('#toggle-cc').click(function(e) {
$('.field-cc').removeClass('hidden');
$(this).parent().remove();
initRecipientSelector();
e.preventDefault();
});
// After send
@ -2043,7 +2085,12 @@ function initReplyForm(load_attachments, init_customer_selector, is_new_conv)
});
e.preventDefault();
})
});
$('#conv-subject .switch-to-note').click(function(e) {
switchToNote();
e.preventDefault();
});
});
}
@ -2359,6 +2406,15 @@ function showAjaxError(response, no_autohide)
}
}
function initAfterSendModal(modal)
{
$(document).ready(function() {
modal.children().find(".after-send-save:first").click(function(e) {
saveAfterSend(e.target);
});
});
}
// Save default redirect
function saveAfterSend(el)
{
@ -4146,6 +4202,15 @@ function editThread(button)
thread_container.children().hide();
thread_container.prepend(response.html);
summernoteInit(thread_container.find('.thread-editor:first'));
thread_container.children().find('.thread-editor-cancel:first').click(function(e) {
cancelThreadEdit(e.target);
e.preventDefault();
});
thread_container.children().find('.thread-editor-save:first').click(function(e) {
saveThreadEdit(e.target);
e.preventDefault();
});
} else {
showAjaxError(response);
}
@ -4971,6 +5036,14 @@ function inApp(topic, token)
if (!getCookie('in_app')) {
setCookie('in_app', '1');
}
$('#navbar-back').click(function(e) {
goBack();
e.preventDefault();
});
$('a.in-app-switcher').click(function(e) {
switchHelpdeskUrl();
e.preventDefault();
});
});
}
@ -5209,3 +5282,27 @@ function closeAllModals()
function replaceAll(text, search, replacement) {
return text.split(search).join(replacement);
}
function initLogsTable()
{
$(document).ready(function () {
$('.table-container tr').on('click', function () {
$('#' + $(this).data('display')).toggle();
});
$('#table-log').DataTable({
"order": [$('#table-log').data('orderingIndex'), 'desc'],
"stateSave": true,
"stateSaveCallback": function (settings, data) {
window.localStorage.setItem("datatable", JSON.stringify(data));
},
"stateLoadCallback": function (settings) {
var data = JSON.parse(window.localStorage.getItem("datatable"));
if (data) data.start = 0;
return data;
}
});
$('#delete-log, #clean-log, #delete-all-log').click(function () {
return confirm('Are you sure?');
});
});
}

View File

@ -130,6 +130,7 @@
"Check Frequency": "Überprüfungshäufigkeit",
"Check Interval (minutes)": "Überprüfungsintervall (Minuten)",
"Check for updates": "Auf Aktualisierungen prüfen",
"Check mail settings in \"Manage » Settings » Mail Settings\"": "E-Mail Einstellungen in \"Verwalten » Einstellungen » E-Mail Einstellungen\" überprüfen",
"Checking": "Prüfen",
"City": "Stadt",
"Clear": "Leeren",
@ -151,6 +152,7 @@
"Confirm password": "Passwort bestätigen",
"Congratulations! Your application can send emails!": "Glückwunsch! Die Anwendung kann E-Mails versenden.",
"Congratulations! Your mailbox can send emails!": "Glückwunsch! Das Postfach kann E-Mails versenden.",
"Connect": "Verbinden",
"Connected, but no IMAP folders found": "Verbunden, aber keine IMAP Ordner gefunden",
"Connecting": "Verbinden",
"Connection Settings": "Verbindungseinstellungen",
@ -170,6 +172,7 @@
"Conversations I'm following": "Gespräche denen ich folge",
"Conversations deleted": "Gelöschte Gespräche",
"Conversations merged": "Gespräch zusammengelegt",
"Copy": "Kopieren",
"Country": "Land",
"Create": "Erstellen",
"Create Mailbox": "Postach erstellen",
@ -278,6 +281,7 @@
"Error sending email to user": "Fehler beim Senden der E-Mail an den Benutzer",
"Error sending invitation email to user": "Fehler beim Senden der Einladungs-E-Mail an den Benutzer",
"Error sending password changed notification to user": "Fehler beim Senden der Benachrichtigung über die Passwortänderung an den Benutzer",
"Every :number minutes": "Alle :number Minuten",
"Every minute": "Jede Minute",
"Extra Recipients": "Weitere Empfänger",
"Failed At": "Fehler bei",
@ -286,6 +290,7 @@
"Failed jobs restarted": "Fehlerhafte Aufgaben neugestartet",
"Failed login": "Fehlerhafter Login",
"Fancy Template": "Schicke Vorlage",
"Fax": "Fax",
"Female": "weiblich",
"Fetch Emails": "Empfange E-Mails",
"Fetch Errors": "Fehler sammeln",
@ -303,6 +308,7 @@
"Forgot Your Password?": "Passwort vergessen?",
"Forward": "Weiterleiten",
"Free open source help desk & shared mailbox": "Kostenloser, quelloffener Helpdesk & geteiltes Postfach",
"From": "Von",
"From Name": "Absendername",
"From name": "Absendername",
"Full Name": "Vollständiger Name",
@ -328,6 +334,7 @@
"If you are sure, type :delete and click the red button.": "Wenn Sie sicher sind geben Sie :delete ein und klicken den roten Button.",
"If you are trying to update a conversation, remember you must respond from the same email address that's on your account. To send your update, please try again and send from your account email address (the email you login with).": "Zum Aktualisieren eines Gesprächs muss von der gleichen E-Mail-Adresse wie in Ihrem Benutzerkonto geantwortet werden. Zum Aktualisieren bitte erneut versuchen und von der E-Mail-Adresse des Benutzerkontos aus senden (von der E-Mail, die zum Login benutzt wird).",
"If you want to send system emails via webmail providers (Gmail, Yahoo, etc), use only SMTP method and make sure that SMTP username is equal to 'Mail From', otherwise webmail provider won't send emails.": "Zum Senden von E-Mails über Webmail-Anbieter (Gmail, Yahoo usw.) bitte SMTP benutzen und sicherstellen, dass der SMTP Benutzername der Absenderadresse entspricht, da ansonsten der Webmail-Anbieter keine E-Mails versenden wird.",
"Image will be re-sized to 200x200. JPG, GIF, PNG accepted.": "Bild wird auf 200x200 geändert. JPG, GIF, PNG akzeptiert.",
"Image will be re-sized to :dimensions. JPG, GIF, PNG accepted.": "Das Bild wird vergrößert\/-kleinert auf :dimensions. JPG, GIF und PNG werden akzeptiert.",
"Inactive": "Inaktiv",
"Include the last message": "Letzte Nachricht hinzufügen",
@ -340,6 +347,7 @@
"Install Module": "Modul installieren",
"Installed Modules": "Installierte Module",
"Installing": "Installieren",
"Invalid license key": "Ungültiger Lizenzschlüssel",
"Invite email has been resent": "Einladungsemail wurde erneut gesendet",
"Invite email has been sent": "Einladungsemail wurde gesendet",
"Invited": "Eingeladen",
@ -361,6 +369,7 @@
"License key has been revoked": "Lizenzschlüssel wurde widerrufen",
"License key has expired": "Lizenz ist abgelaufen",
"License key has not been activated yet": "Lizenzschlüssel wurde noch nicht aktiviert",
"License key is activated on another domain.": "Lizenzschlüssel ist auf einer anderen Domain aktiviert.",
"License successfully Deactivated!": "Lizenz erfolgreich Deaktiviert!",
"License successfully activated!": "Lizenzschlüssel erfolgreich angewendet und aktiviert!",
"List": "Liste",
@ -379,6 +388,7 @@
"Logs Monitoring": "Logs überwachen",
"Logs to monitor": "Zu überwachende Logs",
"Lost internet connection": "Internetverbindung unterbrochen",
"Mail Date & Time": "E-Mail Datum & Zeit",
"Mail From": "E-Mail von",
"Mail Settings": "E-Mail-Einstellungen",
"Mailbox": "Postfach",
@ -435,6 +445,7 @@
"New User": "Neuer Benutzer",
"New message": "Neue Nachricht",
"Newer": "Aktueller",
"Next Attempt": "Nächster Versuch",
"Next Conversation #": "Nächstes Gespräch #",
"Next Page": "Nächste Seite",
"Next active conversation": "Nächstes aktives Gespräch",
@ -750,7 +761,6 @@
"You've already sent this message just recently.": "Die Nachricht wurde grade eben bereits gesendet.",
"Your Email": "Ihre Email",
"Your Profile": "Ihr Profil",
"Your domain is deactivated": "Ihre Domain ist deaktiviert",
"Your email update couldn't be processed": "Die E-Mail-Aktualisierung konnte nicht durchgeführt werden",
"Your password must be at least 8 characters": "Ihr Passwort muss mindestens 8 Zeichen haben.",
"ZIP": "Postleitzahl",

View File

@ -11,7 +11,7 @@
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary" data-loading-text="{{ __('Saving') }}…" onclick="saveAfterSend(this)">
<button type="submit" class="btn btn-primary after-send-save" data-loading-text="{{ __('Saving') }}…">
{{ __('Save') }}
</button>

View File

@ -31,8 +31,8 @@
<h2>{{ __("New Conversation") }}</h2>
<div class="btn-group">
<button type="button" class="btn btn-default active" id="email-conv-switch" onclick="switchToNewEmailConversation({{ App\Conversation::TYPE_EMAIL }})"><i class="glyphicon glyphicon-envelope"></i></button>
<button type="button" class="btn btn-default" id="phone-conv-switch" onclick="switchToNewPhoneConversation()"><i class="glyphicon glyphicon-earphone"></i></button>
<button type="button" class="btn btn-default active" id="email-conv-switch"><i class="glyphicon glyphicon-envelope"></i></button>
<button type="button" class="btn btn-default" id="phone-conv-switch"><i class="glyphicon glyphicon-earphone"></i></button>
</div>
</div>
@ -129,7 +129,7 @@
</div>
<div class="col-sm-9 col-sm-offset-2 toggle-field phone-conv-fields" id="toggle-email">
<a href="javascript:void(0);">{{ __('Add Email') }}</a>
<a href="#">{{ __('Add Email') }}</a>
</div>
</div>
@ -202,7 +202,7 @@
</div>
<div class="col-sm-9 col-sm-offset-2 email-conv-fields toggle-field @if ($conversation->cc && $conversation->bcc) hidden @endif">
<a href="javascript:void(0);" class="help-link" id="toggle-cc">Cc/Bcc</a>
<a href="#" class="help-link" id="toggle-cc">Cc/Bcc</a>
</div>
<div class="form-group{{ $errors->has('subject') ? ' has-error' : '' }}">

View File

@ -41,12 +41,12 @@
<button type="button" class="btn btn-primary btn-send-menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><small class="glyphicon glyphicon-chevron-down"></small></button>
<ul class="dropdown-menu dropdown-menu-right dropdown-after-send">
@action('conversation.prepend_send_dropdown', $conversation, $mailbox, $new_converstion ?? false)
<li @if ($after_send == App\MailboxUser::AFTER_SEND_STAY) class="active" @endif><a href="javascript:void(0)" data-after-send="{{ App\MailboxUser::AFTER_SEND_STAY }}">{{ __('Send and stay on page') }}</a></li>
<li @if ($after_send == App\MailboxUser::AFTER_SEND_STAY) class="active" @endif><a href="#" data-after-send="{{ App\MailboxUser::AFTER_SEND_STAY }}">{{ __('Send and stay on page') }}</a></li>
<li @if ($after_send == App\MailboxUser::AFTER_SEND_NEXT) class="active" @endif><a href="#" data-after-send="{{ App\MailboxUser::AFTER_SEND_NEXT }}">{{ __('Send and next active') }}</a></li>
<li @if ($after_send == App\MailboxUser::AFTER_SEND_FOLDER) class="active" @endif><a href="#" data-after-send="{{ App\MailboxUser::AFTER_SEND_FOLDER }}">{{ __('Send and back to folder') }}</a></li>
@if (empty($new_converstion))
<li class="divider"></li>
<li><a href="#" class="after-send-change" data-modal-body="#after-send-change-body" data-modal-title="{{ __('Default Redirect') }}" data-no-close-btn="true" data-modal-no-footer="true">{{ __('Change default redirect') }}</a></li>
<li><a href="#" class="after-send-change" data-modal-body="#after-send-change-body" data-modal-title="{{ __('Default Redirect') }}" data-no-close-btn="true" data-modal-no-footer="true" data-modal-on-show="initAfterSendModal">{{ __('Change default redirect') }}</a></li>
@endif
@if (empty($new_converstion))
<li class="divider"></li>
@ -77,7 +77,7 @@
<div class="form-group">
<div class="col-sm-9 col-sm-offset-3">
<button type="submit" class="btn btn-primary" data-loading-text="{{ __('Saving') }}…" onclick="saveAfterSend(this)">
<button type="button" class="btn btn-primary after-send-save" data-loading-text="{{ __('Saving') }}…">
{{ __('Save') }}
</button>

View File

@ -2,8 +2,8 @@
<textarea class="form-control thread-editor" rows="8">{!! htmlspecialchars($thread->body) !!}</textarea>
<div class="thread-editor-statusbar">
<a href="#" class="btn btn-link link-grey" onclick="cancelThreadEdit(this);return false;">{{ __('Cancel') }}</a>
<button type="submit" class="btn btn-primary" data-loading-text="{{ __('Saving') }}…" onclick="saveThreadEdit(this)">
<a href="#" class="btn btn-link link-grey thread-editor-cancel">{{ __('Cancel') }}</a>
<button type="submit" class="btn btn-primary thread-editor-save" data-loading-text="{{ __('Saving') }}…">
{{ __('Save') }}
</button>
</div>

View File

@ -38,8 +38,8 @@
[{{ __('Draft') }}]
</div>
<div class="btn-group btn-group-xs draft-actions">
<a class="btn btn-default edit-draft-trigger" href="javascript:void(0);">{{ __('Edit') }}</a>
<a class="btn btn-default discard-draft-trigger" href="javascript:void(0)">{{ __('Discard') }}</a>
<a class="btn btn-default edit-draft-trigger" href="#">{{ __('Edit') }}</a>
<a class="btn btn-default discard-draft-trigger" href="#">{{ __('Discard') }}</a>
</div>
</div>
<div class="thread-info">
@ -299,7 +299,6 @@
@if ($thread->isNote() && !$thread->first && Auth::user()->can('delete', $thread))
<li><a href="#" class="thread-delete-trigger" role="button" data-loading-text="{{ __("Delete") }}…">{{ __("Delete") }}</a></li>
@endif
{{--<li><a href="javascript:alert('todo: implement hiding threads');void(0);" title="" class="thread-hide-trigger">{{ __("Hide") }} (todo)</a></li>--}}
<li><a href="{{ route('conversations.create', ['mailbox_id' => $mailbox->id]) }}?from_thread_id={{ $thread->id }}" title="{{ __("Start a conversation from this thread") }}" class="new-conv" role="button">{{ __("New Conversation") }}</a></li>
@if ($thread->isCustomerMessage())
<li><a href="{{ route('conversations.clone_conversation', ['mailbox_id' => $mailbox->id, 'from_thread_id' => $thread->id]) }}" title="{{ __("Clone a conversation from this thread") }}" class="new-conv" role="button">{{ __("Clone Conversation") }}</a></li>

View File

@ -18,7 +18,6 @@
@endif
@endforeach
@endif
{{--<li class="menu-padded"><a href="javascript: alert('todo: implement recent search');void(0);" class="help-link">{{ __('more') }}</a></li>--}}
<li class="no-link"><span class="text-help">{{ __('Filters') }}</span></li>
@foreach ($filters_list as $filter)
<li class="menu-link menu-padded">

View File

@ -35,7 +35,7 @@
<span class="hidden-xs conv-action glyphicon glyphicon-trash conv-delete-forever" data-toggle="tooltip" data-placement="bottom" title="{{ __("Delete Forever") }}" aria-label="{{ __("Delete Forever") }}" role="button"></span>
@endif
@endif
@action('conversation.action_buttons', $conversation, $mailbox){{--<span class="conv-run-workflow conv-action glyphicon glyphicon-flash" data-toggle="tooltip" data-placement="bottom" title="{{ __("Run Workflow") }}" onclick="alert('todo: implement workflows')" data-toggle="tooltip" aria-label="{{ __("Run Workflow") }}" role="button"></span>--}}
@action('conversation.action_buttons', $conversation, $mailbox)
<div class="dropdown conv-action" data-toggle="tooltip" title="{{ __("More Actions") }}">
<span class="conv-action glyphicon glyphicon-option-horizontal dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" aria-label="{{ __("More Actions") }}"></span>
@ -232,14 +232,14 @@
<div class="form-group cc-toggler @if (empty($to_customers) && !$cc && !$bcc) cc-shifted @endif @if ($cc && $bcc) hidden @endif">
<label class="control-label"></label>
<div class="conv-reply-field">
<a href="javascript:void(0);" class="help-link" id="toggle-cc">Cc/Bcc</a>
<a href="#" class="help-link" id="toggle-cc">Cc/Bcc</a>
</div>
</div>
@if (!empty($threads[0]) && $threads[0]->type == App\Thread::TYPE_NOTE && $threads[0]->created_by_user_id != Auth::user()->id && $threads[0]->created_by_user)
<div class="alert alert-warning alert-switch-to-note">
<i class="glyphicon glyphicon-exclamation-sign"></i>
{!! __('This reply will go to the customer. :%switch_start%Switch to a note:switch_end if you are replying to :user_name.', ['%switch_start%' => '<a href="javascript:switchToNote();void(0);">', 'switch_end' => '</a>', 'user_name' => htmlspecialchars($threads[0]->created_by_user->getFullName()) ]) !!}
{!! __('This reply will go to the customer. :%switch_start%Switch to a note:switch_end if you are replying to :user_name.', ['%switch_start%' => '<a href="#" class="switch-to-note">', 'switch_end' => '</a>', 'user_name' => htmlspecialchars($threads[0]->created_by_user->getFullName()) ]) !!}
</div>
@endif
@ -270,7 +270,7 @@
<div class="conv-customer-block conv-sidebar-block">
@include('customers/profile_snippet', ['customer' => $customer, 'main_email' => $conversation->customer_email, 'conversation' => $conversation])
<div class="dropdown customer-trigger" data-toggle="tooltip" title="{{ __("Settings") }}">
<a href="javascript:void(0)" class="dropdown-toggle glyphicon glyphicon-cog" data-toggle="dropdown" ></a>
<a href="#" class="dropdown-toggle glyphicon glyphicon-cog" data-toggle="dropdown" ></a>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li role="presentation"><a href="{{ route('customers.update', ['id' => $customer->id]) }}" tabindex="-1" role="menuitem">{{ __("Edit Profile") }}</a></li>
@if (!$conversation->isChat())

View File

@ -37,13 +37,13 @@
<div class="multi-item {{ $errors->has('emails.'.$i) ? ' has-error' : '' }}">
<div>
<input type="email" class="form-control input-sized-lg" name="emails[]" value="{{ $email }}" maxlength="191">
<a href="javascript:void(0)" class="multi-remove" tabindex="-1"><i class="glyphicon glyphicon-remove"></i></a>
<a href="#" class="multi-remove" tabindex="-1"><i class="glyphicon glyphicon-remove"></i></a>
</div>
@include('partials/field_error', ['field'=>'emails.'.$i])
</div>
@endforeach
<p class="block-help"><a href="javascript:void(0)" class="multi-add " tabindex="-1">{{ __('Add an email address') }}</a></p>
<p class="block-help"><a href="#" class="multi-add " tabindex="-1">{{ __('Add an email address') }}</a></p>
</div>
{{-- @include('partials/field_error', ['field'=>'emails.*']) --}}
@ -67,12 +67,12 @@
</select>
<input type="tel" class="form-control " name="phones[{{ $i }}][value]" value="{{ $phone['value'] }}">
</div>
<a href="javascript:void(0)" class="multi-remove" tabindex="-1"><i class="glyphicon glyphicon-remove"></i></a>
<a href="#" class="multi-remove" tabindex="-1"><i class="glyphicon glyphicon-remove"></i></a>
</div>
</div>
@endif
@endforeach
<p class="block-help" data-max-i="{{ $i }}"><a href="javascript:void(0)" class="multi-add" tabindex="-1">{{ __('Add a phone number') }}</a></p>
<p class="block-help" data-max-i="{{ $i }}"><a href="#" class="multi-add" tabindex="-1">{{ __('Add a phone number') }}</a></p>
</div>
@include('partials/field_error', ['field'=>'phones'])
@ -108,11 +108,11 @@
<div class="multi-item">
<div>
<input type="url" class="form-control input-sized-lg" name="websites[]" value="{{ $website }}" maxlength="100">
<a href="javascript:void(0)" class="multi-remove" tabindex="-1"><i class="glyphicon glyphicon-remove"></i></a>
<a href="#" class="multi-remove" tabindex="-1"><i class="glyphicon glyphicon-remove"></i></a>
</div>
</div>
@endforeach
<p class="block-help"><a href="javascript:void(0)" class="multi-add" tabindex="-1">{{ __('Add a website') }}</a></p>
<p class="block-help"><a href="#" class="multi-add" tabindex="-1">{{ __('Add a website') }}</a></p>
</div>
@include('partials/field_error', ['field'=>'websites'])
@ -138,12 +138,12 @@
<span class="input-group-btn" style="width:0px;"></span>
<input type="text" class="form-control" name="social_profiles[{{ $i }}][value]" value="{{ $social_profile['value'] }}">
</div>
<a href="javascript:void(0)" class="multi-remove" tabindex="-1"><i class="glyphicon glyphicon-remove"></i></a>
<a href="#" class="multi-remove" tabindex="-1"><i class="glyphicon glyphicon-remove"></i></a>
</div>
</div>
@endif
@endforeach
<p class="block-help" data-max-i="{{ $i }}"><a href="javascript:void(0)" class="multi-add" tabindex="-1">{{ __('Add a social profile') }}</a></p>
<p class="block-help" data-max-i="{{ $i }}"><a href="#" class="multi-add" tabindex="-1">{{ __('Add a social profile') }}</a></p>
</div>
@include('partials/field_error', ['field'=>'social_profiles'])
@ -187,14 +187,14 @@
</select>
<div class="block-help small margin-bottom-0">
<a href="javascript:$('#address-collapse').toggleClass('hidden');void(0);">{{ __('Address') }} <span class="caret"></span></a>
<a href="#address-collapse" data-toggle="collapse">{{ __('Address') }} <span class="caret"></span></a>
</div>
@include('partials/field_error', ['field'=>'country'])
</div>
</div>
<div id="address-collapse" @if (empty(old('state', $customer->state)) && empty(old('city', $customer->city)) && empty(old('address', $customer->address)) && empty(old('zip', $customer->zip)))class="hidden" @endif>
<div id="address-collapse" @if (empty(old('state', $customer->state)) && empty(old('city', $customer->city)) && empty(old('address', $customer->address)) && empty(old('zip', $customer->zip)))@else class="collapse in" @endif>
<div class="form-group{{ $errors->has('state') ? ' has-error' : '' }}">
<label for="state" class="col-sm-2 control-label">{{ __('State') }}</label>

View File

@ -3,7 +3,7 @@
@endphp
@if ($profile_menu)
<div class="dropdown customer-profile-menu">
<a href="javascript:void(0)" class="dropdown-toggle glyphicon glyphicon-cog link-grey" data-toggle="dropdown"></a>
<a href="#" class="dropdown-toggle glyphicon glyphicon-cog link-grey" data-toggle="dropdown"></a>
<ul class="dropdown-menu dropdown-menu-right">
{!! $profile_menu !!}
</ul>

View File

@ -17,7 +17,7 @@
@foreach ($customer->emails as $email)
@if ($email->email == $main_email)
<li class="customer-email">
<a href="javascript:copyToClipboard('{{ $email->email }}')" class="contact-main" data-toggle="tooltip" title="{{ __('Copy') }}">{{ $email->email }}</a>
<a href="#" class="contact-main" data-toggle="tooltip" title="{{ __('Copy') }}">{{ $email->email }}</a>
</li>
@endif
@endforeach
@ -25,7 +25,7 @@
@foreach ($customer->emails as $email)
@if (empty($main_email) || $email->email != $main_email)
<li class="customer-email">
<a href="javascript:copyToClipboard('{{ $email->email }}')" class="contact-main" data-toggle="tooltip" title="{{ __('Copy') }}">{{ $email->email }}</a>
<a href="#" class="contact-main" data-toggle="tooltip" title="{{ __('Copy') }}">{{ $email->email }}</a>
</li>
@endif
@endforeach

View File

@ -8,6 +8,7 @@
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
{!! \Helper::cspMetaTag() !!}
<title>@if ($__env->yieldContent('title_full'))@yield('title_full') @elseif ($__env->yieldContent('title'))@yield('title') - {{ config('app.name', 'FreeScout') }} @else{{ config('app.name', 'FreeScout') }}@endif</title>
@ -48,7 +49,6 @@
<div class="container">
<div class="navbar-header">
<!-- Collapsed Hamburger -->
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#app-navbar-collapse" aria-expanded="false">
<span class="sr-only">{{ __('Toggle Navigation') }}</span>
<span class="icon-bar"></span>
@ -56,21 +56,18 @@
<span class="icon-bar"></span>
</button>
<!-- Branding Image -->
@if (\Helper::isInApp() && \Helper::isRoute('conversations.view'))
<a class="navbar-brand" href="javascript: goBack(); void(0);" title="{{ __('Back') }}">
<a class="navbar-brand" id="navbar-back" href="#" title="{{ __('Back') }}">
<i class="glyphicon glyphicon-arrow-left"></i>
</a>
@else
<a class="navbar-brand" href="{{ route('dashboard') }}" title="{{ __('Dashboard') }}">
<img src="@filter('layout.header_logo', asset('img/logo-brand.svg'))" height="100%" alt="" />
{{-- config('app.name', 'FreeScout') --}}
</a>
@endif
</div>
<div class="collapse navbar-collapse" id="app-navbar-collapse">
<!-- Left Side Of Navbar -->
<ul class="nav navbar-nav">
@php
$mailboxes = Auth::user()->mailboxesCanView(true);
@ -191,9 +188,7 @@
@action('menu_right.user.after_profile')
<li class="divider"></li>
<li>
<a href="{{ route('logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
<a href="#" id="logout-link">
{{ __('Log Out') }}
</a>
@ -203,7 +198,7 @@
</li>
<li class="divider hidden in-app-switcher"></li>
<li>
<a href="javascript:switchHelpdeskUrl();void(0);" class="hidden in-app-switcher">{{ __('Switch Helpdesk URL') }}</a>
<a href="#" class="hidden in-app-switcher">{{ __('Switch Helpdesk URL') }}</a>
</li>
</ul>
</li>
@ -262,7 +257,7 @@
{!! \Eventy::filter('footer.text', '') !!}
@endif
@if (!Auth::user())
<a href="javascript:switchHelpdeskUrl();void(0);" class="hidden in-app-switcher"><br/>{{ __('Switch Helpdesk URL') }}</a>
<a href="#" class="hidden in-app-switcher"><br/>{{ __('Switch Helpdesk URL') }}</a>
@endif
{{-- Show version to admin only --}}
@if (Auth::user() && Auth::user()->isAdmin())
@ -296,7 +291,7 @@
}
@endphp
@yield('javascripts')
<script type="text/javascript">
<script type="text/javascript" {!! \Helper::cspNonceAttr() !!}>
@if (\Helper::isInApp())
@if (Auth::user())
fs_in_app_data['token'] = '{{ Auth::user()->getAuthToken() }}';

View File

@ -65,7 +65,7 @@
<label for="users" class="col-sm-4 control-label">{{ __('Who Else Will Use This Mailbox') }}</label>
<div class="col-sm-6 control-padded">
<div><a href="javascript:void(0)" class="sel-all">{{ __('all') }}</a> / <a href="javascript:void(0)" class="sel-none">{{ __('none') }}</a></div>
<div><a href="#" class="sel-all">{{ __('all') }}</a> / <a href="#" class="sel-none">{{ __('none') }}</a></div>
<fieldset id="permissions-fields">
@foreach ($users as $user)

View File

@ -25,7 +25,7 @@
{{ csrf_field() }}
<p><a href="javascript:void(0)" class="sel-all">{{ __('all') }}</a> / <a href="javascript:void(0)" class="sel-none">{{ __('none') }}</a></p>
<p><a href="#" class="sel-all">{{ __('all') }}</a> / <a href="#" class="sel-none">{{ __('none') }}</a></p>
<fieldset id="permissions-fields">
@foreach ($users as $user)

View File

@ -15,7 +15,7 @@
@action('mailboxes.settings.menu', $mailbox)
@if (!empty($is_dropdown))
<li class="divider"></li>
<li><a href="{{ route('conversations.ajax_html', ['action' => 'default_redirect']) }}?mailbox_id={{ $mailbox->id }}" data-trigger="modal" data-modal-title="{{ __("Default Redirect") }}" data-modal-no-footer="true" data-modal-on-show="initDefaultRedirect" role="button"><i class="glyphicon glyphicon-share-alt"></i> {{ __('Default Redirect') }}</span></a></li>
<li><a href="{{ route('conversations.ajax_html', ['action' => 'default_redirect']) }}?mailbox_id={{ $mailbox->id }}" data-trigger="modal" data-modal-title="{{ __("Default Redirect") }}" data-modal-no-footer="true" data-modal-on-show="initAfterSendModal" role="button"><i class="glyphicon glyphicon-share-alt"></i> {{ __('Default Redirect') }}</span></a></li>
@endif
@if (!empty($is_dropdown))
<li class="divider"></li>

View File

@ -81,10 +81,10 @@
</div>
<div class="row-container margin-top">
<a href="javascript:$('#third-party-container').toggleClass('hidden');void(0);" class="btn btn-default">{{ __('Show') }} »</a>
<a href="#third-party-container" data-toggle="collapse" class="btn btn-default">{{ __('Show') }} »</a>
</div>
<div class="row-container margin-top hidden" id="third-party-container">
<div class="row-container margin-top collapse" id="third-party-container">
@foreach ($third_party_modules as $module)
@include('modules/partials/module_card')
@endforeach

View File

@ -11,7 +11,7 @@
{!! Minify::javascript(['/js/flatpickr/flatpickr.min.js', '/js/flatpickr/l10n/'.strtolower(Config::get('app.locale')).'.js']) !!}
{{-- Default config should be defined here because if "javascript" section is used when new datepicker is loaded it redefined the default config. --}}
@if (\Helper::isTimeFormat24())
<script type="text/javascript">
<script type="text/javascript" {!! \Helper::cspNonceAttr() !!}>
flatpickr.defaultConfig.time_24hr = true;
</script>
@endif

View File

@ -73,7 +73,7 @@
@endsection
@section('javascripts')
<script src="{{ asset('js/datatables/datatables.min.js') }}"></script>
<script src="{{ asset('js/datatables/datatables.min.js') }}" {!! \Helper::cspNonceAttr() !!}></script>
@endsection
@section('javascript')

View File

@ -85,7 +85,7 @@
<label for="users" class="col-sm-4 control-label">{{ __('Which mailboxes will user use?') }}</label>
<div class="col-sm-6 control-padded">
<div><a href="javascript:void(0)" class="sel-all">{{ __('all') }}</a> / <a href="javascript:void(0)" class="sel-none">{{ __('none') }}</a></div>
<div><a href="#" class="sel-all">{{ __('all') }}</a> / <a href="#" class="sel-none">{{ __('none') }}</a></div>
<fieldset id="permissions-fields">
@foreach ($mailboxes as $mailbox)

View File

@ -27,7 +27,7 @@
</div>
<div class="col-xs-12">
<p><a href="javascript:void(0)" class="sel-all">{{ __('all') }}</a> / <a href="javascript:void(0)" class="sel-none">{{ __('none') }}</a></p>
<p><a href="#" class="sel-all">{{ __('all') }}</a> / <a href="#" class="sel-none">{{ __('none') }}</a></p>
<fieldset id="permissions-fields">
@foreach ($mailboxes as $mailbox)

View File

@ -190,46 +190,17 @@
</div>
</div>
</div>
<!-- jQuery for Bootstrap -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
<!-- FontAwesome -->
<script defer src="https://use.fontawesome.com/releases/v5.0.6/js/all.js"></script>
<!-- Datatables -->
<script type="text/javascript" src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/1.10.16/js/dataTables.bootstrap4.min.js"></script>
<script>
$(document).ready(function () {
$('.table-container tr').on('click', function () {
$('#' + $(this).data('display')).toggle();
});
$('#table-log').DataTable({
"order": [$('#table-log').data('orderingIndex'), 'desc'],
"stateSave": true,
"stateSaveCallback": function (settings, data) {
window.localStorage.setItem("datatable", JSON.stringify(data));
},
"stateLoadCallback": function (settings) {
var data = JSON.parse(window.localStorage.getItem("datatable"));
if (data) data.start = 0;
return data;
}
});
$('#delete-log, #clean-log, #delete-all-log').click(function () {
return confirm('Are you sure?');
});
});
</script>
@endsection
@section('stylesheets')
<link href="{{ asset('js/datatables/datatables.min.css') }}" rel="stylesheet">
@endsection
@section('javascript')
@parent
initLogsTable();
@endsection
@section('javascripts')
<script src="{{ asset('js/datatables/datatables.min.js') }}"></script>
<script src="{{ asset('js/datatables/datatables.min.js') }}" {!! \Helper::cspNonceAttr() !!}></script>
@endsection

View File

@ -0,0 +1,5 @@
# The list should be in sync with /config/app.php and nginx config.
<FilesMatch "\.((?!(jpg|jpeg|jfif|pjpeg|pjp|apng|bmp|gif|ico|cur|png|tif|tiff|webp|pdf|txt|diff|patch|json|mp3|wav|ogg|wma)).)*$">
ForceType application/octet-stream
Header set Content-Disposition attachment
</FilesMatch>

0
tests/Feature/.gitkeep Normal file
View File

View File

@ -23,7 +23,7 @@ class ConfigTest extends TestCase
$this->assertTrue(true);
}
protected function setUp()
protected function setUp() : void
{
$this->original_app_key = getenv("APP_KEY");
$this->original_app_key_file = getenv("APP_KEY_FILE");
@ -33,7 +33,7 @@ class ConfigTest extends TestCase
parent::setUp();
}
public function tearDown() {
public function tearDown() :void {
# This is to ensure tests don't influence each other
putenv("APP_KEY=" . $this->original_app_key);
putenv("APP_KEY_FILE=" . $this->original_app_key_file);

View File

@ -156,6 +156,11 @@ sudo echo 'server {
access_log off;
add_header Cache-Control "public, must-revalidate";
}
# The list should be in sync with /storage/app/public/uploads/.htaccess and /config/app.php
location ~* ^/storage/.*\.((?!(jpg|jpeg|jfif|pjpeg|pjp|apng|bmp|gif|ico|cur|png|tif|tiff|webp|pdf|txt|diff|patch|json|mp3|wav|ogg|wma)).)*$ {
add_header Content-disposition "attachment; filename=$1";
default_type application/octet-stream;
}
location ~* ^/(?:css|fonts|img|installer|js|modules|[^\\\]+\..*)$ {
expires 1M;
access_log off;

View File

@ -214,11 +214,6 @@ fi
php artisan queue:restart
if [ -f "${TOOLS_DIR}/post_update.sh" ]; then
echo "Including post_update.sh"
source "${TOOLS_DIR}/post_update.sh";
fi
printf "\nWould you like to update modules? (Y/n) [n]:"
if [ $yes = true ]; then
confirm_modules='Y';
@ -230,4 +225,9 @@ if [ $confirm_modules != "Y" ]; then
exit;
fi
php artisan freescout:module-update
php artisan freescout:module-update
if [ -f "${TOOLS_DIR}/post_update.sh" ]; then
echo "Including post_update.sh"
source "${TOOLS_DIR}/post_update.sh";
fi