Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,3 +36,4 @@ Thumbs.db
|
||||
/resources/lang/module.*
|
||||
.phpunit.result.cache
|
||||
/.phpunit.cache
|
||||
.env.testing
|
||||
@@ -1845,10 +1845,14 @@ class ConversationsController extends Controller
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
$conversation->changeCustomer($customer_email, null, $user);
|
||||
if (!$response['msg']) {
|
||||
$result = $conversation->changeCustomer($customer_email, null, $user);
|
||||
|
||||
$response['status'] = 'success';
|
||||
\Session::flash('flash_success_floating', __('Customer changed'));
|
||||
if ($result) {
|
||||
$response['status'] = 'success';
|
||||
\Session::flash('flash_success_floating', __('Customer changed'));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -2133,7 +2137,7 @@ class ConversationsController extends Controller
|
||||
// Delete converations in a specific folder.
|
||||
case 'empty_folder':
|
||||
// At first, check if this user is able to delete conversations
|
||||
if (!auth()->user()->isAdmin() && !auth()->user()->hasPermission(\App\User::PERM_DELETE_CONVERSATIONS)) {
|
||||
if (!$user->isAdmin() && !$user->hasPermission(\App\User::PERM_DELETE_CONVERSATIONS)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
return \Response::json($response);
|
||||
}
|
||||
@@ -2150,6 +2154,10 @@ class ConversationsController extends Controller
|
||||
$response['msg'] = __('Folder not found');
|
||||
}
|
||||
|
||||
if (!$user->isAdmin() && $folder->mailbox && !$folder->mailbox->userHasAccess($user->id)) {
|
||||
$response['msg'] = __('Not enough permissions');
|
||||
}
|
||||
|
||||
if (!$response['msg']) {
|
||||
$conversation_ids = Conversation::where('folder_id', $folder->id)->pluck('id')->toArray();
|
||||
Conversation::deleteConversationsForever($conversation_ids);
|
||||
|
||||
@@ -489,6 +489,8 @@ class CustomersController extends Controller
|
||||
{
|
||||
$customer = Customer::findOrFail($id);
|
||||
|
||||
$this->checkLimitVisibility($customer);
|
||||
|
||||
// $customers = Customer::where('id', '!=', $id)
|
||||
// ->orderBy('first_name')
|
||||
// ->orderBy('last_name')
|
||||
@@ -512,6 +514,8 @@ class CustomersController extends Controller
|
||||
$customer = Customer::findOrFail($id);
|
||||
$customer2 = Customer::find($request->customer2_id);
|
||||
|
||||
$this->checkLimitVisibility($customer);
|
||||
|
||||
// Ensure customers are different
|
||||
if ($id === $customer2->id) {
|
||||
return redirect()->back()->with('error', __('Cannot merge the same customer'));
|
||||
|
||||
@@ -980,7 +980,7 @@ class MailboxesController extends Controller
|
||||
'in_out' => $in_out,
|
||||
'state' => crc32($username.$password),
|
||||
];
|
||||
$url = \MailHelper::oauthGetAuthorizationUrl(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
$url = \MailHelper::oauthGetAuthorizationUrl($provider, [
|
||||
'state' => json_encode($state),
|
||||
'client_id' => $username,
|
||||
]);
|
||||
@@ -1005,7 +1005,7 @@ class MailboxesController extends Controller
|
||||
} else {
|
||||
// state is set.
|
||||
// Try to get an access token (using the authorization code grant)
|
||||
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
$token_data = \MailHelper::oauthGetAccessToken($provider, [
|
||||
'client_id' => $username,
|
||||
'client_secret' => $password,
|
||||
'code' => $request->code,
|
||||
@@ -1015,7 +1015,7 @@ class MailboxesController extends Controller
|
||||
// Set username and password for the oppozite in_out.
|
||||
if ($in_out == 'in') {
|
||||
if (empty($mailbox->out_server)
|
||||
|| (trim($mailbox->out_server) == \MailHelper::OAUTH_MICROSOFT_SMTP
|
||||
|| ($mailbox->isOutServerOauth()
|
||||
&& (!$mailbox->out_username || $mailbox->out_username == $username))
|
||||
) {
|
||||
$mailbox->out_username = $username;
|
||||
@@ -1058,6 +1058,6 @@ class MailboxesController extends Controller
|
||||
$route = 'mailboxes.connection';
|
||||
}
|
||||
|
||||
return \MailHelper::oauthDisconnect($provider, route($route, ['id' => $mailbox_id]));
|
||||
return \MailHelper::oauthDisconnect($provider, route($route, ['id' => $mailbox_id]), $mailbox);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class HttpsRedirect {
|
||||
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if (\Helper::isHttps()) {
|
||||
if (\Helper::isHttps() && !\Helper::isConsole()) {
|
||||
//$request->setTrustedProxies( [ $request->getClientIp() ], array_keys($this->headers));
|
||||
//!$request->secure()
|
||||
if (!\Helper::isCurrentUrlHttps()) {
|
||||
|
||||
@@ -73,7 +73,12 @@ class SendReplyToCustomer
|
||||
|
||||
// The reply may be sent to some other customer from previous threads.
|
||||
// https://github.com/freescout-help-desk/freescout/pull/5199
|
||||
if ($thread && ($customer_email = $thread->getToArray()[0]) && $customer_email != $main_customer_email) {
|
||||
if ($thread
|
||||
&& ($to_array = $thread->getToArray())
|
||||
&& !empty($to_array[0])
|
||||
&& ($customer_email = $to_array[0])
|
||||
&& $customer_email != $main_customer_email
|
||||
) {
|
||||
$other_customer = Customer::getByEmail($customer_email);
|
||||
if ($other_customer) {
|
||||
$recipient_customer = $other_customer;
|
||||
|
||||
@@ -990,7 +990,8 @@ class Mailbox extends Model
|
||||
return $this->oauthEnabled()
|
||||
&& $this->out_username !== null
|
||||
&& $this->isOutUsernameOauth()
|
||||
&& $this->out_server !== null && trim($this->out_server) == \MailHelper::OAUTH_MICROSOFT_SMTP;
|
||||
&& $this->out_server !== null
|
||||
&& $this->isOutServerOauth();
|
||||
}
|
||||
|
||||
// For oAuth Username may have the following format:
|
||||
@@ -1037,6 +1038,16 @@ class Mailbox extends Model
|
||||
return (!strstr($this->out_username, '@') || preg_match("#.*@.*:.*#", $this->out_username));
|
||||
}
|
||||
|
||||
public function isOauthProvider($provider)
|
||||
{
|
||||
return ($this->oauthGetParam('provider') == $provider);
|
||||
}
|
||||
|
||||
public function isOutServerOauth()
|
||||
{
|
||||
return in_array(trim($this->out_server), [\MailHelper::OAUTH_MICROSOFT_SMTP, \MailHelper::OAUTH_GOOGLE_SMTP]);
|
||||
}
|
||||
|
||||
public function setEmailAttribute($value)
|
||||
{
|
||||
if ($value) {
|
||||
|
||||
@@ -1907,22 +1907,6 @@ class Helper
|
||||
|
||||
public static function sanitizeUploadedFileName($file_name, $uploaded_file = null, $contents = null)
|
||||
{
|
||||
// Check extension.
|
||||
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||
|
||||
if (preg_match('/('.implode('|', self::$restricted_extensions).')/', $ext) || mb_substr($file_name, 0, 1) == '.') {
|
||||
// Add underscore to the extension if file has restricted extension.
|
||||
$file_name = $file_name.'_';
|
||||
} elseif ($ext == 'pdf') {
|
||||
// Rename PDF to avoid running embedded JavaScript.
|
||||
if ($uploaded_file && !$contents) {
|
||||
$contents = file_get_contents($uploaded_file->getRealPath() ?: $uploaded_file->getPathname());
|
||||
}
|
||||
if ($contents && strstr($contents, '/JavaScript')) {
|
||||
$file_name = $file_name.'_';
|
||||
}
|
||||
}
|
||||
|
||||
// Remove illegal chars.
|
||||
$illegal_chars = [
|
||||
// Unix.
|
||||
@@ -1958,6 +1942,22 @@ class Helper
|
||||
// https://github.com/freescout-help-desk/freescout/issues/2123#issuecomment-2775392740
|
||||
$file_name = preg_replace("#\p{Cf}+#u", '', $file_name);
|
||||
|
||||
// Check extension.
|
||||
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
|
||||
|
||||
if (preg_match('/('.implode('|', self::$restricted_extensions).')/', $ext) || mb_substr($file_name, 0, 1) == '.') {
|
||||
// Add underscore to the extension if file has restricted extension.
|
||||
$file_name = $file_name.'_';
|
||||
} elseif ($ext == 'pdf') {
|
||||
// Rename PDF to avoid running embedded JavaScript.
|
||||
if ($uploaded_file && !$contents) {
|
||||
$contents = file_get_contents($uploaded_file->getRealPath() ?: $uploaded_file->getPathname());
|
||||
}
|
||||
if ($contents && strstr($contents, '/JavaScript')) {
|
||||
$file_name = $file_name.'_';
|
||||
}
|
||||
}
|
||||
|
||||
return $file_name;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ class Mail
|
||||
const OAUTH_PROVIDER_MICROSOFT = 'ms';
|
||||
const OAUTH_MICROSOFT_SMTP = 'smtp.office365.com';
|
||||
|
||||
// Google Workspace
|
||||
const OAUTH_PROVIDER_GOOGLE = 'gw';
|
||||
const OAUTH_GOOGLE_SMTP = 'smtp.gmail.com';
|
||||
|
||||
/**
|
||||
* If reply is not extracted properly from the incoming email, add here a new separator.
|
||||
* Order is not important.
|
||||
@@ -150,13 +154,17 @@ class Mail
|
||||
if ($oauth) {
|
||||
if ((strtotime($mailbox->oauthGetParam('issued_on')) + (int)$mailbox->oauthGetParam('expires_in')) < time()) {
|
||||
// Try to get an access token (using the authorization code grant)
|
||||
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
$token_data = \MailHelper::oauthGetAccessToken($mailbox->oauthGetParam('provider'), [
|
||||
'client_id' => $mailbox->getOutOauthClientId(),
|
||||
'client_secret' => $mailbox->out_password,
|
||||
'refresh_token' => $mailbox->oauthGetParam('r_token'),
|
||||
]);
|
||||
|
||||
if (!empty($token_data['a_token'])) {
|
||||
// In Google Workspace new refresh token is not returned after refreshing the token.
|
||||
if (empty($token_data['r_token'])) {
|
||||
$token_data['r_token'] = $mailbox->oauthGetParam('r_token');
|
||||
}
|
||||
$mailbox->setMetaParam('oauth', $token_data, true);
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
$error_message = 'Error occurred refreshing oAuth Access Token: '.$token_data['error'];
|
||||
@@ -822,13 +830,17 @@ class Mail
|
||||
if ($oauth) {
|
||||
if ((strtotime($mailbox->oauthGetParam('issued_on')) + (int)$mailbox->oauthGetParam('expires_in')) < time()) {
|
||||
// Try to get an access token (using the authorization code grant)
|
||||
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||
$token_data = \MailHelper::oauthGetAccessToken($mailbox->oauthGetParam('provider'), [
|
||||
'client_id' => $mailbox->getInOauthClientId(),
|
||||
'client_secret' => $mailbox->in_password,
|
||||
'refresh_token' => $mailbox->oauthGetParam('r_token'),
|
||||
]);
|
||||
|
||||
if (!empty($token_data['a_token'])) {
|
||||
// In Google Workspace new refresh token is not returned after refreshing the token.
|
||||
if (empty($token_data['r_token'])) {
|
||||
$token_data['r_token'] = $mailbox->oauthGetParam('r_token');
|
||||
}
|
||||
$mailbox->setMetaParam('oauth', $token_data, true);
|
||||
} elseif (!empty($token_data['error'])) {
|
||||
$error_message = 'Error occurred refreshing oAuth Access Token: '.$token_data['error'];
|
||||
@@ -947,6 +959,7 @@ class Mail
|
||||
$args = [];
|
||||
|
||||
switch ($provider_code) {
|
||||
|
||||
case self::OAUTH_PROVIDER_MICROSOFT:
|
||||
// https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
|
||||
$args = [
|
||||
@@ -954,10 +967,24 @@ class Mail
|
||||
'response_type' => 'code',
|
||||
'approval_prompt' => 'auto',
|
||||
'redirect_uri' => route('mailboxes.oauth_callback'),
|
||||
//'state' => // passed in $params
|
||||
];
|
||||
$args = array_merge($args, $params);
|
||||
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?'.http_build_query($args);
|
||||
break;
|
||||
|
||||
case self::OAUTH_PROVIDER_GOOGLE:
|
||||
$args = [
|
||||
'scope' => 'https://mail.google.com/',
|
||||
'response_type' => 'code',
|
||||
'prompt' => 'consent', // Without this there will be no refresh_token in response
|
||||
'redirect_uri' => route('mailboxes.oauth_callback'),
|
||||
'access_type' => 'offline', // Without this there will be no refresh_token in response
|
||||
//'state' => // passed in $params
|
||||
];
|
||||
$args = array_merge($args, $params);
|
||||
$url = 'https://accounts.google.com/o/oauth2/v2/auth?'.http_build_query($args);
|
||||
break;
|
||||
}
|
||||
|
||||
return $url;
|
||||
@@ -969,6 +996,7 @@ class Mail
|
||||
$post_params = [];
|
||||
|
||||
switch ($provider_code) {
|
||||
|
||||
case self::OAUTH_PROVIDER_MICROSOFT:
|
||||
$post_params = [
|
||||
'scope' => 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send',
|
||||
@@ -1031,20 +1059,103 @@ class Mail
|
||||
$token_data['error'] = 'Response code: '.curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
}
|
||||
}
|
||||
curl_close($curl);
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
curl_close($curl);
|
||||
}
|
||||
break;
|
||||
|
||||
case self::OAUTH_PROVIDER_GOOGLE:
|
||||
$post_params = [
|
||||
//'scope' => 'https://mail.google.com/', // Just in case
|
||||
"grant_type" => "authorization_code",
|
||||
'redirect_uri' => route('mailboxes.oauth_callback'),
|
||||
];
|
||||
|
||||
$post_params = array_merge($post_params, $params);
|
||||
|
||||
// Refreshing Access Token.
|
||||
if (!empty($post_params['refresh_token'])) {
|
||||
$post_params['grant_type'] = 'refresh_token';
|
||||
}
|
||||
|
||||
$full_url = "https://oauth2.googleapis.com/token";
|
||||
|
||||
$curl = curl_init($full_url);
|
||||
|
||||
curl_setopt($curl, CURLOPT_POST, true);
|
||||
//curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $post_params);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array("application/x-www-form-urlencoded"));
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
\Helper::setCurlDefaultOptions($curl);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 180);
|
||||
|
||||
$response = curl_exec($curl);
|
||||
|
||||
if ($response) {
|
||||
$result = json_decode($response, true);
|
||||
// {
|
||||
// "access_token": "...",
|
||||
// "refresh_token": "...",
|
||||
// "expires_in": 3598,
|
||||
// "scope": "https://mail.google.com/",
|
||||
// "token_type": "Bearer"
|
||||
// }
|
||||
if (!empty($result['access_token'])) {
|
||||
$token_data['provider'] = self::OAUTH_PROVIDER_GOOGLE;
|
||||
$token_data['a_token'] = $result['access_token'];
|
||||
if ($post_params['grant_type'] != 'refresh_token') {
|
||||
$token_data['r_token'] = $result['refresh_token'];
|
||||
}
|
||||
//$token_data['id_token'] = $result['id_token'];
|
||||
$token_data['issued_on'] = now()->toDateTimeString();
|
||||
$token_data['expires_in'] = $result['expires_in'];
|
||||
} elseif ($response) {
|
||||
$token_data['error'] = $response;
|
||||
} else {
|
||||
$token_data['error'] = 'Response code: '.curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
}
|
||||
}
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
curl_close($curl);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $token_data;
|
||||
}
|
||||
|
||||
public static function oauthDisconnect($provider_code, $redirect_uri)
|
||||
public static function oauthDisconnect($provider_code, $redirect_uri, $mailbox)
|
||||
{
|
||||
switch ($provider_code) {
|
||||
|
||||
case self::OAUTH_PROVIDER_MICROSOFT:
|
||||
return redirect()->away('https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri='.urlencode($redirect_uri));
|
||||
break;
|
||||
|
||||
case self::OAUTH_PROVIDER_GOOGLE:
|
||||
// This does not work.
|
||||
// $post_params = [
|
||||
// 'token' => $mailbox->oauthGetParam('a_token')
|
||||
// ];
|
||||
|
||||
// $full_url = "https://oauth2.googleapis.com/revoke";
|
||||
// $curl = curl_init($full_url);
|
||||
|
||||
// curl_setopt($curl, CURLOPT_POST, true);
|
||||
// curl_setopt($curl, CURLOPT_POSTFIELDS, $post_params);
|
||||
// curl_setopt($curl, CURLOPT_HTTPHEADER, array("application/x-www-form-urlencoded"));
|
||||
// curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
// \Helper::setCurlDefaultOptions($curl);
|
||||
// curl_setopt($curl, CURLOPT_TIMEOUT, 180);
|
||||
|
||||
// $response = curl_exec($curl);
|
||||
// $http_status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
|
||||
// if ($http_status != 200) {
|
||||
// \Log::error('[Google Workspace OAuth] Error revoking token on logout: HTTPS Status Code - '.$http_status.'; '.json_encode($response));
|
||||
// }
|
||||
return redirect()->away($redirect_uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,8 @@
|
||||
"classmap": [
|
||||
"database/seeds",
|
||||
"database/factories",
|
||||
"overrides/natxet/cssmin/src/"
|
||||
"overrides/natxet/cssmin/src/",
|
||||
"overrides/erusev/parsedown/"
|
||||
],
|
||||
"psr-0": {
|
||||
"Swift_": "overrides/swiftmailer/swiftmailer/lib/classes",
|
||||
@@ -198,6 +199,8 @@
|
||||
"Symfony\\Component\\Console\\Exception\\": "overrides/symfony/console/Exception/",
|
||||
"Symfony\\Component\\Console\\Question\\": "overrides/symfony/console/",
|
||||
"Symfony\\Component\\CssSelector\\XPath\\Extension\\": "overrides/symfony/css-selector/XPath/Extension/",
|
||||
"Symfony\\Component\\CssSelector\\XPath\\": "overrides/symfony/css-selector/XPath/",
|
||||
"Symfony\\Component\\CssSelector\\Parser\\": "overrides/symfony/css-selector/Parser/",
|
||||
"Symfony\\Component\\Translation\\Loader\\": "overrides/symfony/translation/Loader/",
|
||||
"Symfony\\Component\\Translation\\Formatter\\": "overrides/symfony/translation/Formatter/",
|
||||
"Symfony\\Component\\Translation\\": "overrides/symfony/translation/",
|
||||
@@ -215,7 +218,9 @@
|
||||
"Psy\\TimeitCommand\\": "overrides/psy/psysh/src/TimeitCommand/",
|
||||
"Psy\\Command\\ListCommand\\": "overrides/psy/psysh/src/Command/ListCommand/",
|
||||
"Psy\\Exception\\": "overrides/psy/psysh/src/ErrorException/",
|
||||
"Psy\\VarDumper\\": "overrides/psy/psysh/src/VarDumper/"
|
||||
"Psy\\VarDumper\\": "overrides/psy/psysh/src/VarDumper/",
|
||||
"TijsVerkoyen\\CssToInlineStyles\\": "overrides/tijsverkoyen/css-to-inline-styles/src/",
|
||||
"TijsVerkoyen\\CssToInlineStyles\\Css\\Property\\": "overrides/tijsverkoyen/css-to-inline-styles/src/Css/Property/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"Modules/",
|
||||
@@ -265,6 +270,7 @@
|
||||
"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/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Database/DetectsLostConnections.php",
|
||||
|
||||
"vendor/laravel/framework/src/Illuminate/Support/Collection.php",
|
||||
@@ -290,6 +296,7 @@
|
||||
"vendor/laravel/framework/src/Illuminate/Pagination/LengthAwarePaginator.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Session/FileSessionHandler.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Session/NullSessionHandler.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Session/CacheBasedSessionHandler.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Queue/Listener.php",
|
||||
@@ -397,6 +404,8 @@
|
||||
"vendor/symfony/console/Question/Question.php",
|
||||
"vendor/symfony/finder/Iterator/SortableIterator.php",
|
||||
"vendor/symfony/css-selector/XPath/Extension/NodeExtension.php",
|
||||
"vendor/symfony/css-selector/XPath/Translator.php",
|
||||
"vendor/symfony/css-selector/Parser/Parser.php",
|
||||
"vendor/symfony/translation/Loader/ArrayLoader.php",
|
||||
"vendor/symfony/translation/Formatter/MessageFormatter.php",
|
||||
"vendor/symfony/translation/Translator.php",
|
||||
@@ -497,7 +506,11 @@
|
||||
"vendor/psy/psysh/src/Exception/BreakException.php",
|
||||
"vendor/psy/psysh/src/VarDumper/Dumper.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Foundation/Application.php",
|
||||
"vendor/laravel/framework/src/Illuminate/Contracts/Container/Container.php"
|
||||
"vendor/laravel/framework/src/Illuminate/Contracts/Container/Container.php",
|
||||
"vendor/erusev/parsedown/Parsedown.php",
|
||||
"vendor/tijsverkoyen/css-to-inline-styles/src/CssToInlineStyles.php",
|
||||
"vendor/tijsverkoyen/css-to-inline-styles/src/Css/Property/Processor.php",
|
||||
"vendor/tijsverkoyen/css-to-inline-styles/src/Css/Property/Property.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
|
||||
@@ -18,7 +18,7 @@ return [
|
||||
| or any other location as required by the application or its packages.
|
||||
*/
|
||||
|
||||
'version' => '1.8.206',
|
||||
'version' => '1.8.207',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -55,15 +55,16 @@ return [
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(defined('Pdo\Mysql::ATTR_SSL_CA') ? Pdo\Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('DB_MYSQL_ATTR_SSL_CA'),
|
||||
(defined('Pdo\Mysql::ATTR_SSL_CERT') ? Pdo\Mysql::ATTR_SSL_CERT : PDO::MYSQL_ATTR_SSL_CERT) => env('DB_MYSQL_ATTR_SSL_CERT'),
|
||||
(defined('Pdo\Mysql::ATTR_SSL_VERIFY_SERVER_CERT') ? Pdo\Mysql::ATTR_SSL_VERIFY_SERVER_CERT : PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT) => env('DB_MYSQL_ATTR_SSL_VERIFY_SERVER_CERT', true),
|
||||
PDO::ATTR_PERSISTENT => env('DB_ATTR_PERSISTENT'),
|
||||
]) : [],
|
||||
], function($value) { return $value !== null; }) : [],
|
||||
],
|
||||
|
||||
'testing' => [
|
||||
'driver' => 'mysql',
|
||||
//'url' => env('DB_TEST_DATABASE_URL'),
|
||||
'host' => '127.0.0.1',
|
||||
'database' => 'freescout-test',
|
||||
'host' => env('DB_TEST_HOST', '127.0.0.1'),
|
||||
'database' => env('DB_TEST_DATABASE', 'freescout-test'),
|
||||
'username' => env('DB_TEST_USERNAME', 'freescout-test'),
|
||||
'password' => env('DB_TEST_PASSWORD', 'freescout-test'),
|
||||
//'port' => env('DB_TEST_PORT', '3306'),
|
||||
|
||||
1693
overrides/erusev/parsedown/Parsedown.php
Normal file
1693
overrides/erusev/parsedown/Parsedown.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Validates a font family list according to CSS spec
|
||||
*/
|
||||
class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
|
||||
{
|
||||
|
||||
protected $mask = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->mask = '_- ';
|
||||
for ($c = 'a'; $c <= 'z'; (function_exists('str_increment') ? $c = str_increment($c) : $c++)) {
|
||||
$this->mask .= $c;
|
||||
}
|
||||
for ($c = 'A'; $c <= 'Z'; (function_exists('str_increment') ? $c = str_increment($c) : $c++)) {
|
||||
$this->mask .= $c;
|
||||
}
|
||||
for ($c = '0'; $c <= '9'; $c++) {
|
||||
$this->mask .= $c;
|
||||
} // cast-y, but should be fine
|
||||
// special bytes used by UTF-8
|
||||
for ($i = 0x80; $i <= 0xFF; $i++) {
|
||||
// We don't bother excluding invalid bytes in this range,
|
||||
// because the our restriction of well-formed UTF-8 will
|
||||
// prevent these from ever occurring.
|
||||
$this->mask .= chr($i);
|
||||
}
|
||||
|
||||
/*
|
||||
PHP's internal strcspn implementation is
|
||||
O(length of string * length of mask), making it inefficient
|
||||
for large masks. However, it's still faster than
|
||||
preg_match 8)
|
||||
for (p = s1;;) {
|
||||
spanp = s2;
|
||||
do {
|
||||
if (*spanp == c || p == s1_end) {
|
||||
return p - s1;
|
||||
}
|
||||
} while (spanp++ < (s2_end - 1));
|
||||
c = *++p;
|
||||
}
|
||||
*/
|
||||
// possible optimization: invert the mask.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @param HTMLPurifier_Config $config
|
||||
* @param HTMLPurifier_Context $context
|
||||
* @return bool|string
|
||||
*/
|
||||
public function validate($string, $config, $context)
|
||||
{
|
||||
static $generic_names = array(
|
||||
'serif' => true,
|
||||
'sans-serif' => true,
|
||||
'monospace' => true,
|
||||
'fantasy' => true,
|
||||
'cursive' => true
|
||||
);
|
||||
$allowed_fonts = $config->get('CSS.AllowedFonts');
|
||||
|
||||
// assume that no font names contain commas in them
|
||||
$fonts = explode(',', $string);
|
||||
$final = '';
|
||||
foreach ($fonts as $font) {
|
||||
$font = trim($font);
|
||||
if ($font === '') {
|
||||
continue;
|
||||
}
|
||||
// match a generic name
|
||||
if (isset($generic_names[$font])) {
|
||||
if ($allowed_fonts === null || isset($allowed_fonts[$font])) {
|
||||
$final .= $font . ', ';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// match a quoted name
|
||||
if ($font[0] === '"' || $font[0] === "'") {
|
||||
$length = strlen($font);
|
||||
if ($length <= 2) {
|
||||
continue;
|
||||
}
|
||||
$quote = $font[0];
|
||||
if ($font[$length - 1] !== $quote) {
|
||||
continue;
|
||||
}
|
||||
$font = substr($font, 1, $length - 2);
|
||||
}
|
||||
|
||||
$font = $this->expandCSSEscape($font);
|
||||
|
||||
// $font is a pure representation of the font name
|
||||
|
||||
if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ctype_alnum($font) && $font !== '') {
|
||||
// very simple font, allow it in unharmed
|
||||
$final .= $font . ', ';
|
||||
continue;
|
||||
}
|
||||
|
||||
// bugger out on whitespace. form feed (0C) really
|
||||
// shouldn't show up regardless
|
||||
$font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font);
|
||||
|
||||
// Here, there are various classes of characters which need
|
||||
// to be treated differently:
|
||||
// - Alphanumeric characters are essentially safe. We
|
||||
// handled these above.
|
||||
// - Spaces require quoting, though most parsers will do
|
||||
// the right thing if there aren't any characters that
|
||||
// can be misinterpreted
|
||||
// - Dashes rarely occur, but they fairly unproblematic
|
||||
// for parsing/rendering purposes.
|
||||
// The above characters cover the majority of Western font
|
||||
// names.
|
||||
// - Arbitrary Unicode characters not in ASCII. Because
|
||||
// most parsers give little thought to Unicode, treatment
|
||||
// of these codepoints is basically uniform, even for
|
||||
// punctuation-like codepoints. These characters can
|
||||
// show up in non-Western pages and are supported by most
|
||||
// major browsers, for example: "MS 明朝" is a
|
||||
// legitimate font-name
|
||||
// <http://ja.wikipedia.org/wiki/MS_明朝>. See
|
||||
// the CSS3 spec for more examples:
|
||||
// <http://www.w3.org/TR/2011/WD-css3-fonts-20110324/localizedfamilynames.png>
|
||||
// You can see live samples of these on the Internet:
|
||||
// <http://www.google.co.jp/search?q=font-family+MS+明朝|ゴシック>
|
||||
// However, most of these fonts have ASCII equivalents:
|
||||
// for example, 'MS Mincho', and it's considered
|
||||
// professional to use ASCII font names instead of
|
||||
// Unicode font names. Thanks Takeshi Terada for
|
||||
// providing this information.
|
||||
// The following characters, to my knowledge, have not been
|
||||
// used to name font names.
|
||||
// - Single quote. While theoretically you might find a
|
||||
// font name that has a single quote in its name (serving
|
||||
// as an apostrophe, e.g. Dave's Scribble), I haven't
|
||||
// been able to find any actual examples of this.
|
||||
// Internet Explorer's cssText translation (which I
|
||||
// believe is invoked by innerHTML) normalizes any
|
||||
// quoting to single quotes, and fails to escape single
|
||||
// quotes. (Note that this is not IE's behavior for all
|
||||
// CSS properties, just some sort of special casing for
|
||||
// font-family). So a single quote *cannot* be used
|
||||
// safely in the font-family context if there will be an
|
||||
// innerHTML/cssText translation. Note that Firefox 3.x
|
||||
// does this too.
|
||||
// - Double quote. In IE, these get normalized to
|
||||
// single-quotes, no matter what the encoding. (Fun
|
||||
// fact, in IE8, the 'content' CSS property gained
|
||||
// support, where they special cased to preserve encoded
|
||||
// double quotes, but still translate unadorned double
|
||||
// quotes into single quotes.) So, because their
|
||||
// fixpoint behavior is identical to single quotes, they
|
||||
// cannot be allowed either. Firefox 3.x displays
|
||||
// single-quote style behavior.
|
||||
// - Backslashes are reduced by one (so \\ -> \) every
|
||||
// iteration, so they cannot be used safely. This shows
|
||||
// up in IE7, IE8 and FF3
|
||||
// - Semicolons, commas and backticks are handled properly.
|
||||
// - The rest of the ASCII punctuation is handled properly.
|
||||
// We haven't checked what browsers do to unadorned
|
||||
// versions, but this is not important as long as the
|
||||
// browser doesn't /remove/ surrounding quotes (as IE does
|
||||
// for HTML).
|
||||
//
|
||||
// With these results in hand, we conclude that there are
|
||||
// various levels of safety:
|
||||
// - Paranoid: alphanumeric, spaces and dashes(?)
|
||||
// - International: Paranoid + non-ASCII Unicode
|
||||
// - Edgy: Everything except quotes, backslashes
|
||||
// - NoJS: Standards compliance, e.g. sod IE. Note that
|
||||
// with some judicious character escaping (since certain
|
||||
// types of escaping doesn't work) this is theoretically
|
||||
// OK as long as innerHTML/cssText is not called.
|
||||
// We believe that international is a reasonable default
|
||||
// (that we will implement now), and once we do more
|
||||
// extensive research, we may feel comfortable with dropping
|
||||
// it down to edgy.
|
||||
|
||||
// Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of
|
||||
// str(c)spn assumes that the string was already well formed
|
||||
// Unicode (which of course it is).
|
||||
if (strspn($font, $this->mask) !== strlen($font)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Historical:
|
||||
// In the absence of innerHTML/cssText, these ugly
|
||||
// transforms don't pose a security risk (as \\ and \"
|
||||
// might--these escapes are not supported by most browsers).
|
||||
// We could try to be clever and use single-quote wrapping
|
||||
// when there is a double quote present, but I have choosen
|
||||
// not to implement that. (NOTE: you can reduce the amount
|
||||
// of escapes by one depending on what quoting style you use)
|
||||
// $font = str_replace('\\', '\\5C ', $font);
|
||||
// $font = str_replace('"', '\\22 ', $font);
|
||||
// $font = str_replace("'", '\\27 ', $font);
|
||||
|
||||
// font possibly with spaces, requires quoting
|
||||
$final .= "'$font', ";
|
||||
}
|
||||
$final = rtrim($final, ', ');
|
||||
if ($final === '') {
|
||||
return false;
|
||||
}
|
||||
return $final;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// vim: et sw=4 sts=4
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Illuminate\Session;
|
||||
|
||||
use SessionHandlerInterface;
|
||||
|
||||
class NullSessionHandler implements SessionHandlerInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function open($savePath, $sessionName): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function close(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function read($sessionId)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function write($sessionId, $data): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function destroy($sessionId): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function gc($lifetime)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
384
overrides/symfony/css-selector/Parser/Parser.php
Normal file
384
overrides/symfony/css-selector/Parser/Parser.php
Normal file
@@ -0,0 +1,384 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\Parser;
|
||||
|
||||
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
|
||||
use Symfony\Component\CssSelector\Node;
|
||||
use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer;
|
||||
|
||||
/**
|
||||
* CSS selector parser.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Parser implements ParserInterface
|
||||
{
|
||||
private $tokenizer;
|
||||
|
||||
public function __construct(?Tokenizer $tokenizer = null)
|
||||
{
|
||||
$this->tokenizer = $tokenizer ?: new Tokenizer();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function parse($source)
|
||||
{
|
||||
$reader = new Reader($source);
|
||||
$stream = $this->tokenizer->tokenize($reader);
|
||||
|
||||
return $this->parseSelectorList($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the arguments for ":nth-child()" and friends.
|
||||
*
|
||||
* @param Token[] $tokens
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws SyntaxErrorException
|
||||
*/
|
||||
public static function parseSeries(array $tokens)
|
||||
{
|
||||
foreach ($tokens as $token) {
|
||||
if ($token->isString()) {
|
||||
throw SyntaxErrorException::stringAsFunctionArgument();
|
||||
}
|
||||
}
|
||||
|
||||
$joined = trim(implode('', array_map(function (Token $token) {
|
||||
return $token->getValue();
|
||||
}, $tokens)));
|
||||
|
||||
$int = function ($string) {
|
||||
if (!is_numeric($string)) {
|
||||
throw SyntaxErrorException::stringAsFunctionArgument();
|
||||
}
|
||||
|
||||
return (int) $string;
|
||||
};
|
||||
|
||||
switch (true) {
|
||||
case 'odd' === $joined:
|
||||
return array(2, 1);
|
||||
case 'even' === $joined:
|
||||
return array(2, 0);
|
||||
case 'n' === $joined:
|
||||
return array(1, 0);
|
||||
case false === strpos($joined, 'n'):
|
||||
return array(0, $int($joined));
|
||||
}
|
||||
|
||||
$split = explode('n', $joined);
|
||||
$first = isset($split[0]) ? $split[0] : null;
|
||||
|
||||
return array(
|
||||
$first ? ('-' === $first || '+' === $first ? $int($first.'1') : $int($first)) : 1,
|
||||
isset($split[1]) && $split[1] ? $int($split[1]) : 0,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses selector nodes.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function parseSelectorList(TokenStream $stream)
|
||||
{
|
||||
$stream->skipWhitespace();
|
||||
$selectors = array();
|
||||
|
||||
while (true) {
|
||||
$selectors[] = $this->parserSelectorNode($stream);
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(array(','))) {
|
||||
$stream->getNext();
|
||||
$stream->skipWhitespace();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $selectors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next selector or combined node.
|
||||
*
|
||||
* @return Node\SelectorNode
|
||||
*
|
||||
* @throws SyntaxErrorException
|
||||
*/
|
||||
private function parserSelectorNode(TokenStream $stream)
|
||||
{
|
||||
list($result, $pseudoElement) = $this->parseSimpleSelector($stream);
|
||||
|
||||
while (true) {
|
||||
$stream->skipWhitespace();
|
||||
$peek = $stream->getPeek();
|
||||
|
||||
if ($peek->isFileEnd() || $peek->isDelimiter(array(','))) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (null !== $pseudoElement) {
|
||||
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
|
||||
}
|
||||
|
||||
if ($peek->isDelimiter(array('+', '>', '~'))) {
|
||||
$combinator = $stream->getNext()->getValue();
|
||||
$stream->skipWhitespace();
|
||||
} else {
|
||||
$combinator = ' ';
|
||||
}
|
||||
|
||||
list($nextSelector, $pseudoElement) = $this->parseSimpleSelector($stream);
|
||||
$result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector);
|
||||
}
|
||||
|
||||
return new Node\SelectorNode($result, $pseudoElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next simple node (hash, class, pseudo, negation).
|
||||
*
|
||||
* @param TokenStream $stream
|
||||
* @param bool $insideNegation
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws SyntaxErrorException
|
||||
*/
|
||||
private function parseSimpleSelector(TokenStream $stream, $insideNegation = false)
|
||||
{
|
||||
$stream->skipWhitespace();
|
||||
|
||||
$selectorStart = \count($stream->getUsed());
|
||||
$result = $this->parseElementNode($stream);
|
||||
$pseudoElement = null;
|
||||
|
||||
while (true) {
|
||||
$peek = $stream->getPeek();
|
||||
if ($peek->isWhitespace()
|
||||
|| $peek->isFileEnd()
|
||||
|| $peek->isDelimiter(array(',', '+', '>', '~'))
|
||||
|| ($insideNegation && $peek->isDelimiter(array(')')))
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (null !== $pseudoElement) {
|
||||
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
|
||||
}
|
||||
|
||||
if ($peek->isHash()) {
|
||||
$result = new Node\HashNode($result, $stream->getNext()->getValue());
|
||||
} elseif ($peek->isDelimiter(array('.'))) {
|
||||
$stream->getNext();
|
||||
$result = new Node\ClassNode($result, $stream->getNextIdentifier());
|
||||
} elseif ($peek->isDelimiter(array('['))) {
|
||||
$stream->getNext();
|
||||
$result = $this->parseAttributeNode($result, $stream);
|
||||
} elseif ($peek->isDelimiter(array(':'))) {
|
||||
$stream->getNext();
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(array(':'))) {
|
||||
$stream->getNext();
|
||||
$pseudoElement = $stream->getNextIdentifier();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$identifier = $stream->getNextIdentifier();
|
||||
if (\in_array(strtolower($identifier), array('first-line', 'first-letter', 'before', 'after'))) {
|
||||
// Special case: CSS 2.1 pseudo-elements can have a single ':'.
|
||||
// Any new pseudo-element must have two.
|
||||
$pseudoElement = $identifier;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$stream->getPeek()->isDelimiter(array('('))) {
|
||||
$result = new Node\PseudoNode($result, $identifier);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$stream->getNext();
|
||||
$stream->skipWhitespace();
|
||||
|
||||
if ('not' === strtolower($identifier)) {
|
||||
if ($insideNegation) {
|
||||
throw SyntaxErrorException::nestedNot();
|
||||
}
|
||||
|
||||
list($argument, $argumentPseudoElement) = $this->parseSimpleSelector($stream, true);
|
||||
$next = $stream->getNext();
|
||||
|
||||
if (null !== $argumentPseudoElement) {
|
||||
throw SyntaxErrorException::pseudoElementFound($argumentPseudoElement, 'inside ::not()');
|
||||
}
|
||||
|
||||
if (!$next->isDelimiter(array(')'))) {
|
||||
throw SyntaxErrorException::unexpectedToken('")"', $next);
|
||||
}
|
||||
|
||||
$result = new Node\NegationNode($result, $argument);
|
||||
} else {
|
||||
$arguments = array();
|
||||
$next = null;
|
||||
|
||||
while (true) {
|
||||
$stream->skipWhitespace();
|
||||
$next = $stream->getNext();
|
||||
|
||||
if ($next->isIdentifier()
|
||||
|| $next->isString()
|
||||
|| $next->isNumber()
|
||||
|| $next->isDelimiter(array('+', '-'))
|
||||
) {
|
||||
$arguments[] = $next;
|
||||
} elseif ($next->isDelimiter(array(')'))) {
|
||||
break;
|
||||
} else {
|
||||
throw SyntaxErrorException::unexpectedToken('an argument', $next);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($arguments)) {
|
||||
throw SyntaxErrorException::unexpectedToken('at least one argument', $next);
|
||||
}
|
||||
|
||||
$result = new Node\FunctionNode($result, $identifier, $arguments);
|
||||
}
|
||||
} else {
|
||||
throw SyntaxErrorException::unexpectedToken('selector', $peek);
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($stream->getUsed()) === $selectorStart) {
|
||||
throw SyntaxErrorException::unexpectedToken('selector', $stream->getPeek());
|
||||
}
|
||||
|
||||
return array($result, $pseudoElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next element node.
|
||||
*
|
||||
* @return Node\ElementNode
|
||||
*/
|
||||
private function parseElementNode(TokenStream $stream)
|
||||
{
|
||||
$peek = $stream->getPeek();
|
||||
|
||||
if ($peek->isIdentifier() || $peek->isDelimiter(array('*'))) {
|
||||
if ($peek->isIdentifier()) {
|
||||
$namespace = $stream->getNext()->getValue();
|
||||
} else {
|
||||
$stream->getNext();
|
||||
$namespace = null;
|
||||
}
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(array('|'))) {
|
||||
$stream->getNext();
|
||||
$element = $stream->getNextIdentifierOrStar();
|
||||
} else {
|
||||
$element = $namespace;
|
||||
$namespace = null;
|
||||
}
|
||||
} else {
|
||||
$element = $namespace = null;
|
||||
}
|
||||
|
||||
return new Node\ElementNode($namespace, $element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses next attribute node.
|
||||
*
|
||||
* @return Node\AttributeNode
|
||||
*
|
||||
* @throws SyntaxErrorException
|
||||
*/
|
||||
private function parseAttributeNode(Node\NodeInterface $selector, TokenStream $stream)
|
||||
{
|
||||
$stream->skipWhitespace();
|
||||
$attribute = $stream->getNextIdentifierOrStar();
|
||||
|
||||
if (null === $attribute && !$stream->getPeek()->isDelimiter(array('|'))) {
|
||||
throw SyntaxErrorException::unexpectedToken('"|"', $stream->getPeek());
|
||||
}
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(array('|'))) {
|
||||
$stream->getNext();
|
||||
|
||||
if ($stream->getPeek()->isDelimiter(array('='))) {
|
||||
$namespace = null;
|
||||
$stream->getNext();
|
||||
$operator = '|=';
|
||||
} else {
|
||||
$namespace = $attribute;
|
||||
$attribute = $stream->getNextIdentifier();
|
||||
$operator = null;
|
||||
}
|
||||
} else {
|
||||
$namespace = $operator = null;
|
||||
}
|
||||
|
||||
if (null === $operator) {
|
||||
$stream->skipWhitespace();
|
||||
$next = $stream->getNext();
|
||||
|
||||
if ($next->isDelimiter(array(']'))) {
|
||||
return new Node\AttributeNode($selector, $namespace, $attribute, 'exists', null);
|
||||
} elseif ($next->isDelimiter(array('='))) {
|
||||
$operator = '=';
|
||||
} elseif ($next->isDelimiter(array('^', '$', '*', '~', '|', '!'))
|
||||
&& $stream->getPeek()->isDelimiter(array('='))
|
||||
) {
|
||||
$operator = $next->getValue().'=';
|
||||
$stream->getNext();
|
||||
} else {
|
||||
throw SyntaxErrorException::unexpectedToken('operator', $next);
|
||||
}
|
||||
}
|
||||
|
||||
$stream->skipWhitespace();
|
||||
$value = $stream->getNext();
|
||||
|
||||
if ($value->isNumber()) {
|
||||
// if the value is a number, it's casted into a string
|
||||
$value = new Token(Token::TYPE_STRING, (string) $value->getValue(), $value->getPosition());
|
||||
}
|
||||
|
||||
if (!($value->isIdentifier() || $value->isString())) {
|
||||
throw SyntaxErrorException::unexpectedToken('string or identifier', $value);
|
||||
}
|
||||
|
||||
$stream->skipWhitespace();
|
||||
$next = $stream->getNext();
|
||||
|
||||
if (!$next->isDelimiter(array(']'))) {
|
||||
throw SyntaxErrorException::unexpectedToken('"]"', $next);
|
||||
}
|
||||
|
||||
return new Node\AttributeNode($selector, $namespace, $attribute, $operator, $value->getValue());
|
||||
}
|
||||
}
|
||||
267
overrides/symfony/css-selector/XPath/Translator.php
Normal file
267
overrides/symfony/css-selector/XPath/Translator.php
Normal file
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\CssSelector\XPath;
|
||||
|
||||
use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
|
||||
use Symfony\Component\CssSelector\Node\FunctionNode;
|
||||
use Symfony\Component\CssSelector\Node\NodeInterface;
|
||||
use Symfony\Component\CssSelector\Node\SelectorNode;
|
||||
use Symfony\Component\CssSelector\Parser\Parser;
|
||||
use Symfony\Component\CssSelector\Parser\ParserInterface;
|
||||
|
||||
/**
|
||||
* XPath expression translator interface.
|
||||
*
|
||||
* This component is a port of the Python cssselect library,
|
||||
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Translator implements TranslatorInterface
|
||||
{
|
||||
private $mainParser;
|
||||
|
||||
/**
|
||||
* @var ParserInterface[]
|
||||
*/
|
||||
private $shortcutParsers = array();
|
||||
|
||||
/**
|
||||
* @var Extension\ExtensionInterface[]
|
||||
*/
|
||||
private $extensions = array();
|
||||
|
||||
private $nodeTranslators = array();
|
||||
private $combinationTranslators = array();
|
||||
private $functionTranslators = array();
|
||||
private $pseudoClassTranslators = array();
|
||||
private $attributeMatchingTranslators = array();
|
||||
|
||||
public function __construct(?ParserInterface $parser = null)
|
||||
{
|
||||
$this->mainParser = $parser ?: new Parser();
|
||||
|
||||
$this
|
||||
->registerExtension(new Extension\NodeExtension())
|
||||
->registerExtension(new Extension\CombinationExtension())
|
||||
->registerExtension(new Extension\FunctionExtension())
|
||||
->registerExtension(new Extension\PseudoClassExtension())
|
||||
->registerExtension(new Extension\AttributeMatchingExtension())
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $element
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getXpathLiteral($element)
|
||||
{
|
||||
if (false === strpos($element, "'")) {
|
||||
return "'".$element."'";
|
||||
}
|
||||
|
||||
if (false === strpos($element, '"')) {
|
||||
return '"'.$element.'"';
|
||||
}
|
||||
|
||||
$string = $element;
|
||||
$parts = array();
|
||||
while (true) {
|
||||
if (false !== $pos = strpos($string, "'")) {
|
||||
$parts[] = sprintf("'%s'", substr($string, 0, $pos));
|
||||
$parts[] = "\"'\"";
|
||||
$string = substr($string, $pos + 1);
|
||||
} else {
|
||||
$parts[] = "'$string'";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf('concat(%s)', implode(', ', $parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cssToXPath($cssExpr, $prefix = 'descendant-or-self::')
|
||||
{
|
||||
$selectors = $this->parseSelectors($cssExpr);
|
||||
|
||||
/** @var SelectorNode $selector */
|
||||
foreach ($selectors as $index => $selector) {
|
||||
if (null !== $selector->getPseudoElement()) {
|
||||
throw new ExpressionErrorException('Pseudo-elements are not supported.');
|
||||
}
|
||||
|
||||
$selectors[$index] = $this->selectorToXPath($selector, $prefix);
|
||||
}
|
||||
|
||||
return implode(' | ', $selectors);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function selectorToXPath(SelectorNode $selector, $prefix = 'descendant-or-self::')
|
||||
{
|
||||
return ($prefix ?: '').$this->nodeToXPath($selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an extension.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function registerExtension(Extension\ExtensionInterface $extension)
|
||||
{
|
||||
$this->extensions[$extension->getName()] = $extension;
|
||||
|
||||
$this->nodeTranslators = array_merge($this->nodeTranslators, $extension->getNodeTranslators());
|
||||
$this->combinationTranslators = array_merge($this->combinationTranslators, $extension->getCombinationTranslators());
|
||||
$this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators());
|
||||
$this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators());
|
||||
$this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return Extension\ExtensionInterface
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function getExtension($name)
|
||||
{
|
||||
if (!isset($this->extensions[$name])) {
|
||||
throw new ExpressionErrorException(sprintf('Extension "%s" not registered.', $name));
|
||||
}
|
||||
|
||||
return $this->extensions[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a shortcut parser.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function registerParserShortcut(ParserInterface $shortcut)
|
||||
{
|
||||
$this->shortcutParsers[] = $shortcut;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function nodeToXPath(NodeInterface $node)
|
||||
{
|
||||
if (!isset($this->nodeTranslators[$node->getNodeName()])) {
|
||||
throw new ExpressionErrorException(sprintf('Node "%s" not supported.', $node->getNodeName()));
|
||||
}
|
||||
|
||||
return \call_user_func($this->nodeTranslators[$node->getNodeName()], $node, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $combiner
|
||||
* @param NodeInterface $xpath
|
||||
* @param NodeInterface $combinedXpath
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function addCombination($combiner, NodeInterface $xpath, NodeInterface $combinedXpath)
|
||||
{
|
||||
if (!isset($this->combinationTranslators[$combiner])) {
|
||||
throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner));
|
||||
}
|
||||
|
||||
return \call_user_func($this->combinationTranslators[$combiner], $this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function addFunction(XPathExpr $xpath, FunctionNode $function)
|
||||
{
|
||||
if (!isset($this->functionTranslators[$function->getName()])) {
|
||||
throw new ExpressionErrorException(sprintf('Function "%s" not supported.', $function->getName()));
|
||||
}
|
||||
|
||||
return \call_user_func($this->functionTranslators[$function->getName()], $xpath, $function);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param string $pseudoClass
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function addPseudoClass(XPathExpr $xpath, $pseudoClass)
|
||||
{
|
||||
if (!isset($this->pseudoClassTranslators[$pseudoClass])) {
|
||||
throw new ExpressionErrorException(sprintf('Pseudo-class "%s" not supported.', $pseudoClass));
|
||||
}
|
||||
|
||||
return \call_user_func($this->pseudoClassTranslators[$pseudoClass], $xpath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param XPathExpr $xpath
|
||||
* @param string $operator
|
||||
* @param string $attribute
|
||||
* @param string $value
|
||||
*
|
||||
* @return XPathExpr
|
||||
*
|
||||
* @throws ExpressionErrorException
|
||||
*/
|
||||
public function addAttributeMatching(XPathExpr $xpath, $operator, $attribute, $value)
|
||||
{
|
||||
if (!isset($this->attributeMatchingTranslators[$operator])) {
|
||||
throw new ExpressionErrorException(sprintf('Attribute matcher operator "%s" not supported.', $operator));
|
||||
}
|
||||
|
||||
return \call_user_func($this->attributeMatchingTranslators[$operator], $xpath, $attribute, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $css
|
||||
*
|
||||
* @return SelectorNode[]
|
||||
*/
|
||||
private function parseSelectors($css)
|
||||
{
|
||||
foreach ($this->shortcutParsers as $shortcut) {
|
||||
$tokens = $shortcut->parse($css);
|
||||
|
||||
if (!empty($tokens)) {
|
||||
return $tokens;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->mainParser->parse($css);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace TijsVerkoyen\CssToInlineStyles\Css\Property;
|
||||
|
||||
use Symfony\Component\CssSelector\Node\Specificity;
|
||||
|
||||
class Processor
|
||||
{
|
||||
/**
|
||||
* Split a string into separate properties
|
||||
*
|
||||
* @param string $propertiesString
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function splitIntoSeparateProperties($propertiesString)
|
||||
{
|
||||
$propertiesString = $this->cleanup($propertiesString);
|
||||
|
||||
$properties = (array) explode(';', $propertiesString);
|
||||
$keysToRemove = array();
|
||||
$numberOfProperties = count($properties);
|
||||
|
||||
for ($i = 0; $i < $numberOfProperties; $i++) {
|
||||
$properties[$i] = trim($properties[$i]);
|
||||
|
||||
// if the new property begins with base64 it is part of the current property
|
||||
if (isset($properties[$i + 1]) && strpos(trim($properties[$i + 1]), 'base64,') === 0) {
|
||||
$properties[$i] .= ';' . trim($properties[$i + 1]);
|
||||
$keysToRemove[] = $i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($keysToRemove)) {
|
||||
foreach ($keysToRemove as $key) {
|
||||
unset($properties[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function cleanup($string)
|
||||
{
|
||||
$string = str_replace(array("\r", "\n"), '', $string);
|
||||
$string = str_replace(array("\t"), ' ', $string);
|
||||
$string = str_replace('"', '\'', $string);
|
||||
$string = preg_replace('|/\*.*?\*/|', '', $string);
|
||||
$string = preg_replace('/\s\s+/', ' ', $string);
|
||||
|
||||
$string = trim($string);
|
||||
$string = rtrim($string, ';');
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a property-string into an object
|
||||
*
|
||||
* @param string $property
|
||||
*
|
||||
* @return Property|null
|
||||
*/
|
||||
public function convertToObject($property, ?Specificity $specificity = null)
|
||||
{
|
||||
if (strpos($property, ':') === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
list($name, $value) = explode(':', $property, 2);
|
||||
|
||||
$name = trim($name);
|
||||
$value = trim($value);
|
||||
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Property($name, $value, $specificity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of property-strings into objects
|
||||
*
|
||||
* @param string[] $properties
|
||||
*
|
||||
* @return Property[]
|
||||
*/
|
||||
public function convertArrayToObjects(array $properties, ?Specificity $specificity = null)
|
||||
{
|
||||
$objects = array();
|
||||
|
||||
foreach ($properties as $property) {
|
||||
$object = $this->convertToObject($property, $specificity);
|
||||
if ($object === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$objects[] = $object;
|
||||
}
|
||||
|
||||
return $objects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the property-string for multiple properties
|
||||
*
|
||||
* @param Property[] $properties
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buildPropertiesString(array $properties)
|
||||
{
|
||||
$chunks = array();
|
||||
|
||||
foreach ($properties as $property) {
|
||||
$chunks[] = $property->toString();
|
||||
}
|
||||
|
||||
return implode(' ', $chunks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace TijsVerkoyen\CssToInlineStyles\Css\Property;
|
||||
|
||||
use Symfony\Component\CssSelector\Node\Specificity;
|
||||
|
||||
final class Property
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* @var Specificity
|
||||
*/
|
||||
private $originalSpecificity;
|
||||
|
||||
/**
|
||||
* Property constructor.
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @param Specificity|null $specificity
|
||||
*/
|
||||
public function __construct($name, $value, ?Specificity $specificity = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->value = $value;
|
||||
$this->originalSpecificity = $specificity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get originalSpecificity
|
||||
*
|
||||
* @return Specificity
|
||||
*/
|
||||
public function getOriginalSpecificity()
|
||||
{
|
||||
return $this->originalSpecificity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this property important?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isImportant()
|
||||
{
|
||||
return (stripos($this->value, '!important') !== false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the textual representation of the property
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toString()
|
||||
{
|
||||
return sprintf(
|
||||
'%1$s: %2$s;',
|
||||
$this->name,
|
||||
$this->value
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace TijsVerkoyen\CssToInlineStyles;
|
||||
|
||||
use Symfony\Component\CssSelector\CssSelector;
|
||||
use Symfony\Component\CssSelector\CssSelectorConverter;
|
||||
use Symfony\Component\CssSelector\Exception\ExceptionInterface;
|
||||
use TijsVerkoyen\CssToInlineStyles\Css\Processor;
|
||||
use TijsVerkoyen\CssToInlineStyles\Css\Property\Processor as PropertyProcessor;
|
||||
use TijsVerkoyen\CssToInlineStyles\Css\Rule\Processor as RuleProcessor;
|
||||
|
||||
class CssToInlineStyles
|
||||
{
|
||||
private $cssConverter;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (class_exists('Symfony\Component\CssSelector\CssSelectorConverter')) {
|
||||
$this->cssConverter = new CssSelectorConverter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will inline the $css into the given $html
|
||||
*
|
||||
* Remark: if the html contains <style>-tags those will be used, the rules
|
||||
* in $css will be appended.
|
||||
*
|
||||
* @param string $html
|
||||
* @param string $css
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function convert($html, $css = null)
|
||||
{
|
||||
$document = $this->createDomDocumentFromHtml($html);
|
||||
$processor = new Processor();
|
||||
|
||||
// get all styles from the style-tags
|
||||
$rules = $processor->getRules(
|
||||
$processor->getCssFromStyleTags($html)
|
||||
);
|
||||
|
||||
if ($css !== null) {
|
||||
$rules = $processor->getRules($css, $rules);
|
||||
}
|
||||
|
||||
$document = $this->inline($document, $rules);
|
||||
|
||||
return $this->getHtmlFromDocument($document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline the given properties on an given DOMElement
|
||||
*
|
||||
* @param \DOMElement $element
|
||||
* @param Css\Property\Property[] $properties
|
||||
*
|
||||
* @return \DOMElement
|
||||
*/
|
||||
public function inlineCssOnElement(\DOMElement $element, array $properties)
|
||||
{
|
||||
if (empty($properties)) {
|
||||
return $element;
|
||||
}
|
||||
|
||||
$cssProperties = array();
|
||||
$inlineProperties = array();
|
||||
|
||||
foreach ($this->getInlineStyles($element) as $property) {
|
||||
$inlineProperties[$property->getName()] = $property;
|
||||
}
|
||||
|
||||
foreach ($properties as $property) {
|
||||
if (!isset($inlineProperties[$property->getName()])) {
|
||||
$cssProperties[$property->getName()] = $property;
|
||||
}
|
||||
}
|
||||
|
||||
$rules = array();
|
||||
foreach (array_merge($cssProperties, $inlineProperties) as $property) {
|
||||
$rules[] = $property->toString();
|
||||
}
|
||||
$element->setAttribute('style', implode(' ', $rules));
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current inline styles for a given DOMElement
|
||||
*
|
||||
* @param \DOMElement $element
|
||||
*
|
||||
* @return Css\Property\Property[]
|
||||
*/
|
||||
public function getInlineStyles(\DOMElement $element)
|
||||
{
|
||||
$processor = new PropertyProcessor();
|
||||
|
||||
return $processor->convertArrayToObjects(
|
||||
$processor->splitIntoSeparateProperties(
|
||||
$element->getAttribute('style')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
*
|
||||
* @return \DOMDocument
|
||||
*/
|
||||
protected function createDomDocumentFromHtml($html)
|
||||
{
|
||||
$document = new \DOMDocument('1.0', 'UTF-8');
|
||||
$internalErrors = libxml_use_internal_errors(true);
|
||||
$document->loadHTML(mb_encode_numericentity($html, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8'));
|
||||
libxml_use_internal_errors($internalErrors);
|
||||
$document->formatOutput = true;
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DOMDocument $document
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getHtmlFromDocument(\DOMDocument $document)
|
||||
{
|
||||
// retrieve the document element
|
||||
// we do it this way to preserve the utf-8 encoding
|
||||
$htmlElement = $document->documentElement;
|
||||
$html = $document->saveHTML($htmlElement);
|
||||
$html = trim($html);
|
||||
|
||||
// retrieve the doctype
|
||||
$document->removeChild($htmlElement);
|
||||
$doctype = $document->saveHTML();
|
||||
$doctype = trim($doctype);
|
||||
|
||||
// if it is the html5 doctype convert it to lowercase
|
||||
if ($doctype === '<!DOCTYPE html>') {
|
||||
$doctype = strtolower($doctype);
|
||||
}
|
||||
|
||||
return $doctype."\n".$html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DOMDocument $document
|
||||
* @param Css\Rule\Rule[] $rules
|
||||
*
|
||||
* @return \DOMDocument
|
||||
*/
|
||||
protected function inline(\DOMDocument $document, array $rules)
|
||||
{
|
||||
if (empty($rules)) {
|
||||
return $document;
|
||||
}
|
||||
|
||||
$propertyStorage = new \SplObjectStorage();
|
||||
|
||||
$xPath = new \DOMXPath($document);
|
||||
|
||||
usort($rules, array(RuleProcessor::class, 'sortOnSpecificity'));
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
try {
|
||||
if (null !== $this->cssConverter) {
|
||||
$expression = $this->cssConverter->toXPath($rule->getSelector());
|
||||
} else {
|
||||
// Compatibility layer for Symfony 2.7 and older
|
||||
$expression = CssSelector::toXPath($rule->getSelector());
|
||||
}
|
||||
} catch (ExceptionInterface $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$elements = $xPath->query($expression);
|
||||
|
||||
if ($elements === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($elements as $element) {
|
||||
$propertyStorage[$element] = $this->calculatePropertiesToBeApplied(
|
||||
$rule->getProperties(),
|
||||
$propertyStorage->offsetExists($element) ? $propertyStorage[$element] : array()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($propertyStorage as $element) {
|
||||
$this->inlineCssOnElement($element, $propertyStorage[$element]);
|
||||
}
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the CSS rules to determine the applied properties.
|
||||
*
|
||||
* @param Css\Property\Property[] $properties
|
||||
* @param Css\Property\Property[] $cssProperties existing applied properties indexed by name
|
||||
*
|
||||
* @return Css\Property\Property[] updated properties, indexed by name
|
||||
*/
|
||||
private function calculatePropertiesToBeApplied(array $properties, array $cssProperties)
|
||||
{
|
||||
if (empty($properties)) {
|
||||
return $cssProperties;
|
||||
}
|
||||
|
||||
foreach ($properties as $property) {
|
||||
if (isset($cssProperties[$property->getName()])) {
|
||||
$existingProperty = $cssProperties[$property->getName()];
|
||||
|
||||
//skip check to overrule if existing property is important and current is not
|
||||
if ($existingProperty->isImportant() && !$property->isImportant()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//overrule if current property is important and existing is not, else check specificity
|
||||
$overrule = !$existingProperty->isImportant() && $property->isImportant();
|
||||
if (!$overrule) {
|
||||
$overrule = $existingProperty->getOriginalSpecificity()->compareTo($property->getOriginalSpecificity()) <= 0;
|
||||
}
|
||||
|
||||
if ($overrule) {
|
||||
unset($cssProperties[$property->getName()]);
|
||||
$cssProperties[$property->getName()] = $property;
|
||||
}
|
||||
} else {
|
||||
$cssProperties[$property->getName()] = $property;
|
||||
}
|
||||
}
|
||||
|
||||
return $cssProperties;
|
||||
}
|
||||
}
|
||||
@@ -811,6 +811,11 @@ class Header {
|
||||
if (str_contains($value, ";") && str_contains($value, "=")) {
|
||||
$_attributes = $this->read_attribute($value);
|
||||
foreach($_attributes as $_key => $_value) {
|
||||
// Without this the following will not be parsed properly with ";" on the end:
|
||||
// Content-Type: text/html; charset="UTF-8";
|
||||
if ($_key === "") {
|
||||
continue;
|
||||
}
|
||||
if ($_value === "") {
|
||||
// Remove existing value.
|
||||
if (isset($this->attributes[$key])) {
|
||||
|
||||
11
phpcs.xml
11
phpcs.xml
@@ -39,7 +39,6 @@
|
||||
</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">
|
||||
@@ -67,7 +66,6 @@
|
||||
<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"/>
|
||||
@@ -114,7 +112,6 @@
|
||||
<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>
|
||||
@@ -126,17 +123,12 @@
|
||||
<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>
|
||||
<rule ref="PSR2.ControlStructures.ControlStructureSpacing"/>
|
||||
|
||||
<exclude-pattern>*/.phpstorm.meta.php</exclude-pattern>
|
||||
<exclude-pattern>*/_ide_helper.php</exclude-pattern>
|
||||
@@ -150,6 +142,7 @@
|
||||
<exclude-pattern>*/storage/*</exclude-pattern>
|
||||
<exclude-pattern>*/docs/*</exclude-pattern>
|
||||
<exclude-pattern>*/vendor/*</exclude-pattern>
|
||||
<exclude-pattern>*/overrides/*</exclude-pattern>
|
||||
<exclude-pattern>*/migrations/*</exclude-pattern>
|
||||
<exclude-pattern>*/config/*</exclude-pattern>
|
||||
<exclude-pattern>*/public/index.php</exclude-pattern>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.2/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.2/phpunit.xsd">
|
||||
<testsuites>
|
||||
<testsuite name="Feature">
|
||||
<directory suffix="Test.php">./tests/Feature</directory>
|
||||
@@ -16,9 +16,9 @@
|
||||
<env name="QUEUE_DRIVER" value="sync"/>
|
||||
<env name="APP_KEY" value="value_from_phpunit"/>
|
||||
</php>
|
||||
<source>
|
||||
<coverage>
|
||||
<include>
|
||||
<directory suffix=".php">./app</directory>
|
||||
</include>
|
||||
</source>
|
||||
</coverage>
|
||||
</phpunit>
|
||||
|
||||
@@ -102,17 +102,42 @@
|
||||
<div class="col-sm-6">
|
||||
<input id="out_password" type="password" class="form-control input-sized @if ($out_oauth_enabled) disabled @endif" name="out_password" value="{{ old('out_password', $mailbox->outPasswordSafe()) }}" maxlength="255" @if ($mailbox->out_method == App\Mailbox::OUT_METHOD_SMTP) @endif autofocus {{-- This added to prevent autocomplete in Chrome --}}autocomplete="new-password" @if ($out_oauth_enabled) readonly @endif>
|
||||
|
||||
<p class="form-help">
|
||||
<small @if ($mailbox->oauthGetParam('provider') == \MailHelper::OAUTH_PROVIDER_MICROSOFT && $out_oauth_enabled) class="text-success" @endif>Microsoft Exchange</small>
|
||||
@if (!$mailbox->oauthEnabled())
|
||||
@if ($mailbox->out_username && $mailbox->out_password && $mailbox->isOutUsernameOauth())
|
||||
– <a href="{{ route('mailboxes.oauth', ['id' => $mailbox->id, 'provider' => \MailHelper::OAUTH_PROVIDER_MICROSOFT, 'in_out' => 'out']) }}" target="_blank">{{ __('Connect') }}</a>
|
||||
@php
|
||||
$active_oauth_provider = '';
|
||||
if ($out_oauth_enabled) {
|
||||
$active_oauth_provider = $mailbox->oauthGetParam('provider');
|
||||
}
|
||||
@endphp
|
||||
|
||||
{{-- Microsoft Exchange --}}
|
||||
@if ($active_oauth_provider != \MailHelper::OAUTH_PROVIDER_GOOGLE)
|
||||
<p class="form-help">
|
||||
<small @if ($mailbox->isOauthProvider(\MailHelper::OAUTH_PROVIDER_MICROSOFT) && $out_oauth_enabled) class="text-success" @endif>Microsoft Exchange</small>
|
||||
@if (!$mailbox->oauthEnabled())
|
||||
@if ($mailbox->out_username && $mailbox->out_password && $mailbox->isOutUsernameOauth())
|
||||
– <a href="{{ route('mailboxes.oauth', ['id' => $mailbox->id, 'provider' => \MailHelper::OAUTH_PROVIDER_MICROSOFT, 'in_out' => 'out']) }}" target="_blank">{{ __('Connect') }}</a>
|
||||
@endif
|
||||
@elseif ($mailbox->isOauthProvider(\MailHelper::OAUTH_PROVIDER_MICROSOFT) && $out_oauth_enabled)
|
||||
– <a href="{{ route('mailboxes.oauth_disconnect', ['id' => $mailbox->id, 'provider' => \MailHelper::OAUTH_PROVIDER_MICROSOFT, 'in_out' => 'out']) }}">{{ __('Disconnect') }}</a>
|
||||
@endif
|
||||
@elseif ($mailbox->oauthGetParam('provider') == \MailHelper::OAUTH_PROVIDER_MICROSOFT && $out_oauth_enabled)
|
||||
– <a href="{{ route('mailboxes.oauth_disconnect', ['id' => $mailbox->id, 'provider' => \MailHelper::OAUTH_PROVIDER_MICROSOFT, 'in_out' => 'out']) }}">{{ __('Disconnect') }}</a>
|
||||
@endif
|
||||
<small>(<a href="{{ config('app.freescout_repo') }}/wiki/Connect-FreeScout-to-Microsoft-365-Exchange-via-oAuth" target="_blank">{{ __('Help') }}</a>)</small>
|
||||
</p>
|
||||
<small>(<a href="{{ config('app.freescout_repo') }}/wiki/Connect-FreeScout-to-Microsoft-365-Exchange-via-oAuth" target="_blank">{{ __('Help') }}</a>)</small>
|
||||
</p>
|
||||
@endif
|
||||
|
||||
{{-- Google Workspace --}}
|
||||
@if ($active_oauth_provider != \MailHelper::OAUTH_PROVIDER_MICROSOFT)
|
||||
<p class="form-help">
|
||||
<small @if ($mailbox->isOauthProvider(\MailHelper::OAUTH_PROVIDER_GOOGLE) && $out_oauth_enabled) class="text-success" @endif>Google Workspace</small>
|
||||
@if (!$mailbox->oauthEnabled())
|
||||
@if ($mailbox->out_username && $mailbox->out_password && $mailbox->isOutUsernameOauth())
|
||||
– <a href="{{ route('mailboxes.oauth', ['id' => $mailbox->id, 'provider' => \MailHelper::OAUTH_PROVIDER_GOOGLE, 'in_out' => 'out']) }}" target="_blank">{{ __('Connect') }}</a>
|
||||
@endif
|
||||
@elseif ($mailbox->isOauthProvider(\MailHelper::OAUTH_PROVIDER_GOOGLE) && $out_oauth_enabled)
|
||||
– <a href="{{ route('mailboxes.oauth_disconnect', ['id' => $mailbox->id, 'provider' => \MailHelper::OAUTH_PROVIDER_GOOGLE, 'in_out' => 'out']) }}">{{ __('Disconnect') }}</a>
|
||||
@endif
|
||||
<small>(<a href="{{ config('app.freescout_repo') }}/wiki/Connect-FreeScout-to-Google-Workspace" target="_blank">{{ __('Help') }}</a>)</small>
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group{{ $errors->has('out_encryption') ? ' has-error' : '' }}">
|
||||
|
||||
@@ -107,17 +107,42 @@
|
||||
<div class="col-sm-6">
|
||||
<input id="in_password" type="password" class="form-control input-sized @if ($in_oauth_enabled) disabled @endif" name="in_password" value="{{ old('in_password', $mailbox->inPasswordSafe()) }}" maxlength="255" {{-- This added to prevent autocomplete in Chrome --}}autocomplete="new-password" @if ($in_oauth_enabled) readonly @endif>
|
||||
|
||||
<p class="form-help">
|
||||
<small @if ($mailbox->oauthGetParam('provider') == \MailHelper::OAUTH_PROVIDER_MICROSOFT) class="text-success" @endif>Microsoft Exchange</small>
|
||||
@if (!$mailbox->oauthEnabled())
|
||||
@if ($mailbox->in_username && $mailbox->in_password && $mailbox->isInUsernameOauth())
|
||||
– <a href="{{ route('mailboxes.oauth', ['id' => $mailbox->id, 'provider' => \MailHelper::OAUTH_PROVIDER_MICROSOFT, 'in_out' => 'in']) }}" target="_blank">{{ __('Connect') }}</a>
|
||||
@php
|
||||
$active_oauth_provider = '';
|
||||
if ($in_oauth_enabled) {
|
||||
$active_oauth_provider = $mailbox->oauthGetParam('provider');
|
||||
}
|
||||
@endphp
|
||||
|
||||
{{-- Microsoft Exchange --}}
|
||||
@if ($active_oauth_provider != \MailHelper::OAUTH_PROVIDER_GOOGLE)
|
||||
<p class="form-help">
|
||||
<small @if ($mailbox->isOauthProvider(\MailHelper::OAUTH_PROVIDER_MICROSOFT)) class="text-success" @endif>Microsoft Exchange</small>
|
||||
@if (!$mailbox->oauthEnabled())
|
||||
@if ($mailbox->in_username && $mailbox->in_password && $mailbox->isInUsernameOauth())
|
||||
– <a href="{{ route('mailboxes.oauth', ['id' => $mailbox->id, 'provider' => \MailHelper::OAUTH_PROVIDER_MICROSOFT, 'in_out' => 'in']) }}" target="_blank">{{ __('Connect') }}</a>
|
||||
@endif
|
||||
@elseif ($mailbox->isOauthProvider(\MailHelper::OAUTH_PROVIDER_MICROSOFT) && $in_oauth_enabled)
|
||||
– <a href="{{ route('mailboxes.oauth_disconnect', ['id' => $mailbox->id, 'provider' => \MailHelper::OAUTH_PROVIDER_MICROSOFT, 'in_out' => 'in']) }}">{{ __('Disconnect') }}</a>
|
||||
@endif
|
||||
@elseif ($mailbox->oauthGetParam('provider') == \MailHelper::OAUTH_PROVIDER_MICROSOFT && $in_oauth_enabled)
|
||||
– <a href="{{ route('mailboxes.oauth_disconnect', ['id' => $mailbox->id, 'provider' => \MailHelper::OAUTH_PROVIDER_MICROSOFT, 'in_out' => 'in']) }}">{{ __('Disconnect') }}</a>
|
||||
@endif
|
||||
<small>(<a href="{{ config('app.freescout_repo') }}/wiki/Connect-FreeScout-to-Microsoft-365-Exchange-via-oAuth" target="_blank">{{ __('Help') }}</a>)</small>
|
||||
</p>
|
||||
<small>(<a href="{{ config('app.freescout_repo') }}/wiki/Connect-FreeScout-to-Microsoft-365-Exchange-via-oAuth" target="_blank">{{ __('Help') }}</a>)</small>
|
||||
</p>
|
||||
@endif
|
||||
|
||||
{{-- Google Workspace --}}
|
||||
@if ($active_oauth_provider != \MailHelper::OAUTH_PROVIDER_MICROSOFT)
|
||||
<p class="form-help">
|
||||
<small @if ($mailbox->isOauthProvider(\MailHelper::OAUTH_PROVIDER_GOOGLE)) class="text-success" @endif>Google Workspace</small>
|
||||
@if (!$mailbox->oauthEnabled())
|
||||
@if ($mailbox->in_username && $mailbox->in_password && $mailbox->isInUsernameOauth())
|
||||
– <a href="{{ route('mailboxes.oauth', ['id' => $mailbox->id, 'provider' => \MailHelper::OAUTH_PROVIDER_GOOGLE, 'in_out' => 'in']) }}" target="_blank">{{ __('Connect') }}</a>
|
||||
@endif
|
||||
@elseif ($mailbox->isOauthProvider(\MailHelper::OAUTH_PROVIDER_GOOGLE) && $in_oauth_enabled)
|
||||
– <a href="{{ route('mailboxes.oauth_disconnect', ['id' => $mailbox->id, 'provider' => \MailHelper::OAUTH_PROVIDER_GOOGLE, 'in_out' => 'in']) }}">{{ __('Disconnect') }}</a>
|
||||
@endif
|
||||
<small>(<a href="{{ config('app.freescout_repo') }}/wiki/Connect-FreeScout-to-Google-Workspace" target="_blank">{{ __('Help') }}</a>)</small>
|
||||
</p>
|
||||
@endif
|
||||
{{--@include('partials/field_error', ['field'=>'in_password'])--}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
132
tests/Feature/ConversationChangeCustomerTest.php
Normal file
132
tests/Feature/ConversationChangeCustomerTest.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Conversation;
|
||||
use App\Customer;
|
||||
use App\Email;
|
||||
use App\Mailbox;
|
||||
use App\Thread;
|
||||
use App\User;
|
||||
//use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ConversationChangeCustomerTest extends TestCase
|
||||
{
|
||||
// This cleans existing tables.
|
||||
//use RefreshDatabase;
|
||||
|
||||
private $admin;
|
||||
private $unprivUser;
|
||||
private $mailbox;
|
||||
private $conversation;
|
||||
private $originalCustomer;
|
||||
private $attackerCustomer;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
// Without it csrf_token() returns empty string.
|
||||
\Session::start();
|
||||
|
||||
// Create admin user
|
||||
$this->admin = factory(User::class)->create([
|
||||
'role' => User::ROLE_ADMIN,
|
||||
]);
|
||||
|
||||
// Create mailbox
|
||||
$this->mailbox = factory(Mailbox::class)->create();
|
||||
$this->mailbox->users()->sync([$this->admin->id]);
|
||||
|
||||
// Create unprivileged user with NO mailbox access
|
||||
$this->unprivUser = factory(User::class)->create([
|
||||
'role' => User::ROLE_USER,
|
||||
]);
|
||||
|
||||
// Create customers
|
||||
$this->originalCustomer = factory(Customer::class)->create([
|
||||
'first_name' => 'Original',
|
||||
'last_name' => 'Customer',
|
||||
]);
|
||||
$this->originalCustomer->syncEmails(['original.customer@example.org']);
|
||||
|
||||
$this->attackerCustomer = factory(Customer::class)->create([
|
||||
'first_name' => 'Attacker',
|
||||
'last_name' => 'Evil',
|
||||
]);
|
||||
$this->attackerCustomer->syncEmails(['attacker@example.org']);
|
||||
|
||||
// Create conversation belonging to original customer.
|
||||
$this->conversation = factory(Conversation::class)->create([
|
||||
'mailbox_id' => $this->mailbox->id,
|
||||
'customer_id' => $this->originalCustomer->id,
|
||||
'customer_email' => 'original.customer@example.org',
|
||||
'status' => Conversation::STATUS_ACTIVE,
|
||||
'state' => Conversation::STATE_PUBLISHED,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a user without mailbox access cannot change a conversation's customer.
|
||||
* This is the regression test for the authorization bypass where changeCustomer()
|
||||
* executed unconditionally even when permission checks set an error message.
|
||||
*/
|
||||
public function testUnauthorizedUserCannotChangeCustomer()
|
||||
{
|
||||
if (PHP_VERSION_ID < 80400) {
|
||||
$this->assertEquals('1', '1');
|
||||
return;
|
||||
}
|
||||
$response = $this->actingAs($this->unprivUser)
|
||||
->post('/conversation/ajax', [
|
||||
'action' => 'conversation_change_customer',
|
||||
'conversation_id' => $this->conversation->id,
|
||||
'customer_email' => 'attacker@example.org',
|
||||
'_token' => csrf_token(),
|
||||
], [
|
||||
'X-Requested-With' => 'XMLHttpRequest',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$json = $response->json();
|
||||
$this->assertEquals('Not enough permissions', $json['msg']);
|
||||
|
||||
// The customer must NOT have changed
|
||||
$this->conversation->refresh();
|
||||
$this->assertEquals('original.customer@example.org', $this->conversation->customer_email);
|
||||
$this->assertEquals($this->originalCustomer->id, $this->conversation->customer_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an authorized admin user CAN change a conversation's customer.
|
||||
* This ensures the fix does not break normal functionality.
|
||||
*/
|
||||
public function testAuthorizedUserCanChangeCustomer()
|
||||
{
|
||||
if (PHP_VERSION_ID < 80400) {
|
||||
$this->assertEquals('1', '1');
|
||||
return;
|
||||
}
|
||||
$response = $this->actingAs($this->admin)
|
||||
->post('/conversation/ajax', [
|
||||
'action' => 'conversation_change_customer',
|
||||
'conversation_id' => $this->conversation->id,
|
||||
'customer_email' => 'attacker@example.org',
|
||||
'_token' => csrf_token(),
|
||||
], [
|
||||
'X-Requested-With' => 'XMLHttpRequest',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$json = $response->json();
|
||||
$this->assertEquals('success', $json['status']);
|
||||
$this->assertEmpty($json['msg']);
|
||||
|
||||
// The customer SHOULD have changed.
|
||||
$this->conversation->refresh();
|
||||
$this->assertEquals('attacker@example.org', $this->conversation->customer_email);
|
||||
$this->assertEquals($this->attackerCustomer->id, $this->conversation->customer_id);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ Content-Type: text/plain; charset="UTF-8"
|
||||
1
|
||||
|
||||
--000000000000ff062c062e516ec3
|
||||
Content-Type: text/html; charset="UTF-8"
|
||||
Content-Type: text/html; charset="UTF-8";
|
||||
|
||||
<div dir="ltr"><div>1</div></div>
|
||||
|
||||
|
||||
@@ -129,6 +129,13 @@ class WebklexTest extends FixtureWebklexMessage {
|
||||
self::assertSame("------3f0eb27c226a6efc44713e1b8f40befd34d8d3c9199e2ad04dab9839cbbd3524", $message->header->getBoundary());
|
||||
}
|
||||
|
||||
/**
|
||||
* This test also makes sure that
|
||||
* headers of multipart message are parsed properly
|
||||
* when there is a ";" at the end:
|
||||
*
|
||||
* Content-Type: text/html; charset="UTF-8";
|
||||
*/
|
||||
public function testRegularEmail() {
|
||||
$message = $this->getFixture("message-3.eml");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user