Add freescout source 1.8.114
This commit is contained in:
parent
fa1bc382f9
commit
d0cb1eee59
12
freescout-dist/.editorconfig
Normal file
12
freescout-dist/.editorconfig
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
insert_final_newline = false
|
47
freescout-dist/.env.example
Normal file
47
freescout-dist/.env.example
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
####################################################################################################
|
||||||
|
## If you want to use web installer **DO NOT** create `.env` file manually.
|
||||||
|
## If `.env` file exists in the root of your app, web installer won't run.
|
||||||
|
##
|
||||||
|
## Every time you are making changes in .env file, in order changes to take an effect you need to run:
|
||||||
|
## php artisan freescout:clear-cache
|
||||||
|
#####################################################################################################
|
||||||
|
|
||||||
|
# Application URL
|
||||||
|
APP_URL=https://example.com
|
||||||
|
|
||||||
|
# If you are using HTTPS, feel free to uncomment this line to improve security
|
||||||
|
#SESSION_SECURE_COOKIE=true
|
||||||
|
|
||||||
|
# Enter your proxy address here if freescout.net is not available from your server
|
||||||
|
# (access to freescout.net is required to obtain official modules)
|
||||||
|
#APP_PROXY=
|
||||||
|
|
||||||
|
# Custom headers to add to all outgoing emails.
|
||||||
|
#APP_CUSTOM_MAIL_HEADERS="IsTransactional:True;X-Custom-Header:value"
|
||||||
|
|
||||||
|
# Uncomment if you have many folders and you are experiencing performance issues
|
||||||
|
#APP_UPDATE_FOLDER_COUNTERS_IN_BACKGROUND=true
|
||||||
|
|
||||||
|
# Timezones: https://github.com/freescout-helpdesk/freescout/wiki/PHP-Timezones
|
||||||
|
# Comment it to use default timezone from php.ini
|
||||||
|
#APP_TIMEZONE=Europe/London
|
||||||
|
|
||||||
|
# Comma separated list of trusted proxies for proper IP detection in FreeScout.
|
||||||
|
# To trust all proxies that connect to your server use single asterisk: *
|
||||||
|
# To trust ALL proxies, including those that are in a chain of forwarding use double asterisk: **
|
||||||
|
#APP_TRUSTED_PROXIES=192.168.1.1,192.168.1.2,192.168.1.3
|
||||||
|
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=
|
||||||
|
DB_USERNAME=
|
||||||
|
# Maximum password length is 50 characters
|
||||||
|
DB_PASSWORD=
|
||||||
|
|
||||||
|
# Run the following console command to generate the key: php artisan key:generate
|
||||||
|
# Otherwise application will show the following error: "Whoops, looks like something went wrong"
|
||||||
|
APP_KEY=
|
||||||
|
|
||||||
|
# Uncomment to see errors in your browser, don't forget to comment it back when debugging finished
|
||||||
|
#APP_DEBUG=true
|
10
freescout-dist/.env.travis
Normal file
10
freescout-dist/.env.travis
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
APP_ENV=testing
|
||||||
|
APP_KEY=SomeRandomString7
|
||||||
|
|
||||||
|
DB_CONNECTION=testing
|
||||||
|
DB_TEST_USERNAME=root
|
||||||
|
DB_TEST_PASSWORD=
|
||||||
|
|
||||||
|
CACHE_DRIVER=array
|
||||||
|
SESSION_DRIVER=array
|
||||||
|
QUEUE_DRIVER=sync
|
5
freescout-dist/.gitattributes
vendored
Normal file
5
freescout-dist/.gitattributes
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
* text=auto
|
||||||
|
*.css linguist-vendored
|
||||||
|
*.scss linguist-vendored
|
||||||
|
*.js linguist-vendored
|
||||||
|
CHANGELOG.md export-ignore
|
1
freescout-dist/.gitcommit
Normal file
1
freescout-dist/.gitcommit
Normal file
@ -0,0 +1 @@
|
|||||||
|
28e2d659db742540723b7d6cea7f0261cfe34bf1
|
23
freescout-dist/.github/ISSUE_TEMPLATE/general_help_request.md
vendored
Normal file
23
freescout-dist/.github/ISSUE_TEMPLATE/general_help_request.md
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
name: General Help Request
|
||||||
|
about: Create a general help request
|
||||||
|
|
||||||
|
---
|
||||||
|
<!--
|
||||||
|
* If you have some error the first thing you should do is to check "Manage » Logs » App Logs"! If there are any errors there - make sure to provide them in the issue.
|
||||||
|
* Have you read FAQ? https://github.com/freescout-helpdesk/freescout/wiki/FAQ
|
||||||
|
* If you have "Whoops something went wrong" error see: https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#11-troubleshooting
|
||||||
|
* Feature requests for modules should be posted here: https://freescout.net/request-feature/
|
||||||
|
* Docker questions should go here: https://github.com/tiredofit/docker-freescout/issues
|
||||||
|
* Create separate issues for different topics - no need to mix all your problems into one issue.
|
||||||
|
* Questions on issues with payments for modules should be sent by email: https://freescout.net/contact-us/
|
||||||
|
* Don't forget that you are not the project manager. Try to be humble, polite, friendly and positive. Otherwise your account may be banned and it can't be undone.
|
||||||
|
* Mentioning how much you've spent on modules is the way to be banned.
|
||||||
|
|
||||||
|
Still here? Well clean this out and go ahead :)
|
||||||
|
-->
|
||||||
|
|
||||||
|
PHP version:
|
||||||
|
FreeScout version:
|
||||||
|
Database: MySQL / PostgreSQL
|
||||||
|
Are you using CloudFlare: Yes / No
|
3
freescout-dist/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
3
freescout-dist/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Keep in mind that pull requests should be sent to the `master` branch! See https://github.com/freescout-helpdesk/freescout/wiki/Development-Guide#github-workflow
|
||||||
|
|
||||||
|
Now you can delete this text and type the description of your pull request...
|
20
freescout-dist/.github/workflows/lint-php.yml
vendored
Normal file
20
freescout-dist/.github/workflows/lint-php.yml
vendored
Normal 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
freescout-dist/.github/workflows/test-pgsql.yml
vendored
Normal file
59
freescout-dist/.github/workflows/test-pgsql.yml
vendored
Normal 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] }}
|
45
freescout-dist/.github/workflows/test.yml
vendored
Normal file
45
freescout-dist/.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
name: Test App (MySQL)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test App (MySQL)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
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: 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
|
36
freescout-dist/.gitignore
vendored
Normal file
36
freescout-dist/.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/node_modules
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
# We are committing /vendor directory to make installation process super easy, even on a shared hosting:
|
||||||
|
# - https://www.codeenigma.com/build/blog/do-you-really-need-composer-production
|
||||||
|
# - https://getcomposer.org/doc/faqs/should-i-commit-the-dependencies-in-my-vendor-directory.md
|
||||||
|
/vendor/**/.git
|
||||||
|
#/vendor
|
||||||
|
/.idea
|
||||||
|
/.vagrant
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
.env
|
||||||
|
app/Console/Commands/Test*
|
||||||
|
/bootstrap/compiled.php
|
||||||
|
composer.phar
|
||||||
|
#composer.lock
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
#/.htaccess
|
||||||
|
/public/css/builds/
|
||||||
|
/public/js/builds/
|
||||||
|
/public/.well-known
|
||||||
|
/Modules
|
||||||
|
/Modules/**/.git
|
||||||
|
/public/modules
|
||||||
|
/public/docs
|
||||||
|
/storage/.ignore_locales
|
||||||
|
/storage/.installed
|
||||||
|
/tools
|
||||||
|
.well-known
|
||||||
|
/resources/lang/module.*
|
||||||
|
.phpunit.result.cache
|
10
freescout-dist/.htaccess
Normal file
10
freescout-dist/.htaccess
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# On some hostings it is impossible to change web root directory
|
||||||
|
# so we rewrite all web requests into /public folder
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine on
|
||||||
|
# On some hostings it does not work as "public/$1", but works as "/public/$1".
|
||||||
|
# But we can't write it as "/public/$1" because in this case it does not work
|
||||||
|
# when application is installed in subdirectory.
|
||||||
|
RewriteCond %{REQUEST_URI} !/\.well\-known/?.*
|
||||||
|
RewriteRule (.*) public/$1 [L]
|
||||||
|
</IfModule>
|
15
freescout-dist/.travis.yml
Normal file
15
freescout-dist/.travis.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
language: php
|
||||||
|
|
||||||
|
php:
|
||||||
|
- 7.0
|
||||||
|
- 7.2
|
||||||
|
- 7.4
|
||||||
|
- 8.0
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- cp .env.travis .env
|
||||||
|
- mysql -e 'create database homestead_test;'
|
||||||
|
|
||||||
|
# phpunit testing is disabled as dist vendor is cleared by removing unneeded files
|
||||||
|
#script:
|
||||||
|
# - vendor/bin/phpunit
|
661
freescout-dist/LICENSE
Normal file
661
freescout-dist/LICENSE
Normal file
@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defences to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<http://www.gnu.org/licenses/>.
|
150
freescout-dist/README.md
Normal file
150
freescout-dist/README.md
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# Free Self-Hosted Zendesk & Help Scout Alternative
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/freescout-helpdesk/freescout/master/public/img/logo-300.png" width="180" height="180" />
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
[![PHP version](https://freescout-helpdesk.github.io/img/badges/PHP-7.1%2B-blue.svg)](https://github.com/freescout-helpdesk/freescout#requirements) [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Ffreescout-helpdesk%2Ffreescout&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
**FreeScout** is the super lightweight and powerful free open source help desk and shared inbox built with PHP (Laravel framework). Now you can enjoy free Zendesk & Help Scout without giving up privacy or locking yourself into a service you don't control. FreeScout has been developed from scratch and is not using any copyrighted Help Scout or Zendesk materials.
|
||||||
|
|
||||||
|
If you want to support the project feel free to **star this repository**. It helps to increase the visibility of the project and let people know that it is valuable. Thanks for your support!
|
||||||
|
|
||||||
|
![FreeScout](https://freescout-helpdesk.github.io/img/screenshots/screenshot.png)
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
* [Demo](#demo)
|
||||||
|
* [Features](#features)
|
||||||
|
* [Mobile Apps](#mobile-apps)
|
||||||
|
* [Requirements](#requirements)
|
||||||
|
* [Installation](#installation)
|
||||||
|
* [Cloud Hosted](#cloud-hosted)
|
||||||
|
* [Modules](#modules)
|
||||||
|
* [Tools & Integrations](#tools--integrations)
|
||||||
|
* [News & Updates](#news--updates)
|
||||||
|
* [Contributing](#contributing)
|
||||||
|
* [Screenshots](#screenshots)
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
**[Live Demo](https://demo.freescout.net)**
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* No limitations on the number of users, tickets, mailboxes, etc.
|
||||||
|
* 100% Mobile-friendly.
|
||||||
|
* Multilingual: English, Chinese, Croatian, Czech, Danish, Dutch, Finnish, French, German, Italian, Japanese, Korean, Norwegian, Persian, Polish, Portuguese, Russian, Spanish, Slovak, Swedish.
|
||||||
|
* Seamless email integration.
|
||||||
|
* Supports modern Microsoft Exchange authentication.
|
||||||
|
* Web installer & updater.
|
||||||
|
* Starred conversations.
|
||||||
|
* Forwarding conversations.
|
||||||
|
* Merging conversations.
|
||||||
|
* Moving conversations between mailboxes.
|
||||||
|
* Phone conversations.
|
||||||
|
* Sending new conversations to multiple recipients at once.
|
||||||
|
* Collision detection – notice is shown when two agents open the same conversation.
|
||||||
|
* Push notifications.
|
||||||
|
* Following a conversation.
|
||||||
|
* Auto reply.
|
||||||
|
* Internal notes.
|
||||||
|
* Automatic refreshing of the conversations list without the need to reload the page.
|
||||||
|
* Pasting screenshots from the clipboard into the reply area.
|
||||||
|
* Configuring notifications on a per user basis.
|
||||||
|
* Open tracking.
|
||||||
|
* Editing threads.
|
||||||
|
* Search.
|
||||||
|
* And more…
|
||||||
|
|
||||||
|
Need anything else? Suggest features [here](https://freescout.net/request-feature/).
|
||||||
|
|
||||||
|
## Mobile Apps
|
||||||
|
|
||||||
|
Mobile apps support the same functionality and modules as the web version of your FreeScout installation. Both support agents and administrators can use mobile apps.
|
||||||
|
|
||||||
|
<a href="https://freescout.net/android-app/" target="_blank" rel="nofollow"><img alt="Android App" src="https://freescout-helpdesk.github.io/img/apps/android.png" width="200px" /></a> <a href="https://freescout.net/ios-app/" target="_blank" rel="nofollow"><img alt="iOS App" src="https://freescout-helpdesk.github.io/img/apps/ios.png?v=1" width="200px" /></a>
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
FreeScout is a pure PHP/MySQL application, so it can be easily deployed even on a shared hosting.
|
||||||
|
|
||||||
|
* Nginx / Apache / IIS
|
||||||
|
* PHP 7.1 - 8.2
|
||||||
|
* MySQL 5.0+ / MariaDB 5.0+ / PostgreSQL
|
||||||
|
|
||||||
|
There is no minimum system requirements (CPU / RAM) – FreeScout will run on any system.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
[Installation Guide](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide)
|
||||||
|
|
||||||
|
Images & one-click installs:
|
||||||
|
|
||||||
|
* [Docker Image](http://freescout.net/docker/)
|
||||||
|
* [Softaculous](http://www.softaculous.com/apps/customersupport/FreeScout) (cPanel, Plesk, ISPmanager, H-Sphere, DirectAdmin, InterWorx)
|
||||||
|
* [Fantastico](http://ff3.netenberg.com/visitors/scripts/freescout/view) (cPanel, DirectAdmin, ISP Manager, ISP Config)
|
||||||
|
* [Cloudron](https://cloudron.io/store/net.freescout.cloudronapp.html)
|
||||||
|
* [Ubuntu](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#interactive-installation-bash-script-ubuntu) (bash script)
|
||||||
|
|
||||||
|
## Cloud Hosted
|
||||||
|
|
||||||
|
[Cloud Hosted FreeScout](https://freescout.net/cloud-hosted/)
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
* [Official Modules](https://freescout.net/modules/)
|
||||||
|
* [Community Modules](https://freescout.net/community-modules/)
|
||||||
|
|
||||||
|
## Tools & Integrations
|
||||||
|
|
||||||
|
* [API](https://api-docs.freescout.net/)
|
||||||
|
* [Migrate to FreeScout](http://freescout.net/migrate/) (from any help desk)
|
||||||
|
* [Zapier](https://freescout.net/zapier/)
|
||||||
|
* [Make](https://freescout.net/make-integration/) (Integromat)
|
||||||
|
|
||||||
|
## News & Updates
|
||||||
|
|
||||||
|
Don't miss news, updates and new modules!
|
||||||
|
|
||||||
|
[Email Newsletter](https://freescout.net/subscribe/) | [Facebook](https://freescout.net/facebook/) | [Twitter](https://freescout.net/twitter/) | [YouTube](https://freescout.net/youtube/) | [Telegram](https://freescout.net/telegram/) | [RSS](https://freescout.net/feed/)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
* [Support the project by leaving a feedback](https://github.com/freescout-helpdesk/freescout/issues/288)
|
||||||
|
* [Development Guide](https://github.com/freescout-helpdesk/freescout/wiki/Development-Guide)
|
||||||
|
* [Todo list](https://github.com/freescout-helpdesk/freescout/labels/help%20wanted)
|
||||||
|
* [Translate](https://github.com/freescout-helpdesk/freescout/wiki/Translate)
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
Dashboard:
|
||||||
|
|
||||||
|
![Dashboard](https://freescout-helpdesk.github.io/img/screenshots/dashboard.png)
|
||||||
|
|
||||||
|
Conversation:
|
||||||
|
|
||||||
|
![Conversation](https://freescout-helpdesk.github.io/img/screenshots/conversation.png)
|
||||||
|
|
||||||
|
|
||||||
|
Mailbox connection settings page:
|
||||||
|
|
||||||
|
![Mailbox connection settings page](https://freescout-helpdesk.github.io/img/screenshots/mailbox-connection.png)
|
||||||
|
|
||||||
|
Notifications:
|
||||||
|
|
||||||
|
![Notifications](https://freescout-helpdesk.github.io/img/screenshots/notifications.png)
|
||||||
|
|
||||||
|
Push notification:
|
||||||
|
|
||||||
|
![Push notification](https://freescout-helpdesk.github.io/img/screenshots/push.png)
|
||||||
|
|
||||||
|
Web installer:
|
||||||
|
|
||||||
|
![Web installer](https://freescout-helpdesk.github.io/img/screenshots/installer.png)
|
||||||
|
|
||||||
|
Login page:
|
||||||
|
|
||||||
|
![Login page](https://freescout-helpdesk.github.io/img/screenshots/freescout-login.png)
|
5
freescout-dist/SECURITY.md
Normal file
5
freescout-dist/SECURITY.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Email at support@freescout.net
|
135
freescout-dist/app/ActivityLog.php
Normal file
135
freescout-dist/app/ActivityLog.php
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Spatie\Activitylog\Models\Activity;
|
||||||
|
|
||||||
|
class ActivityLog extends Activity
|
||||||
|
{
|
||||||
|
const NAME_USER = 'users';
|
||||||
|
const NAME_OUT_EMAILS = 'out_emails'; // used to display send_log in Logs
|
||||||
|
const NAME_EMAILS_SENDING = 'send_errors';
|
||||||
|
const NAME_EMAILS_FETCHING = 'fetch_errors';
|
||||||
|
const NAME_SYSTEM = 'system';
|
||||||
|
const NAME_APP_LOGS = 'app';
|
||||||
|
|
||||||
|
public static $available_logs = [
|
||||||
|
self::NAME_USER,
|
||||||
|
self::NAME_OUT_EMAILS,
|
||||||
|
self::NAME_EMAILS_SENDING,
|
||||||
|
self::NAME_EMAILS_FETCHING,
|
||||||
|
self::NAME_SYSTEM,
|
||||||
|
self::NAME_APP_LOGS,
|
||||||
|
];
|
||||||
|
|
||||||
|
const DESCRIPTION_USER_LOGIN = 'login';
|
||||||
|
const DESCRIPTION_USER_LOGOUT = 'logout';
|
||||||
|
const DESCRIPTION_USER_REGISTER = 'register';
|
||||||
|
const DESCRIPTION_USER_LOCKED = 'locked';
|
||||||
|
const DESCRIPTION_USER_LOGIN_FAILED = 'login_failed';
|
||||||
|
const DESCRIPTION_USER_PASSWORD_RESET = 'password_reset';
|
||||||
|
const DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER = 'error_sending_email_to_customer';
|
||||||
|
const DESCRIPTION_EMAILS_SENDING_ERROR_TO_USER = 'error_sending_email_to_user';
|
||||||
|
const DESCRIPTION_EMAILS_SENDING_ERROR_INVITE = 'error_sending_invite_to_user';
|
||||||
|
const DESCRIPTION_EMAILS_SENDING_ERROR_PASSWORD_CHANGED = 'error_sending_password_changed';
|
||||||
|
const DESCRIPTION_EMAILS_SENDING_ERROR_ALERT = 'error_sending_alert';
|
||||||
|
const DESCRIPTION_EMAILS_SENDING_WRONG_EMAIL = 'error_sending_wrong_email';
|
||||||
|
const DESCRIPTION_EMAILS_FETCHING_ERROR = 'error_fetching_email';
|
||||||
|
const DESCRIPTION_SYSTEM_ERROR = 'system_error';
|
||||||
|
const DESCRIPTION_USER_DELETED = 'user_deleted';
|
||||||
|
|
||||||
|
public function getEventDescription()
|
||||||
|
{
|
||||||
|
switch ($this->description) {
|
||||||
|
case self::DESCRIPTION_USER_LOGIN:
|
||||||
|
return __('Logged in');
|
||||||
|
case self::DESCRIPTION_USER_LOGOUT:
|
||||||
|
return __('Logged out');
|
||||||
|
case self::DESCRIPTION_USER_REGISTER:
|
||||||
|
return __('Registered');
|
||||||
|
case self::DESCRIPTION_USER_LOCKED:
|
||||||
|
return __('Locked out');
|
||||||
|
case self::DESCRIPTION_USER_LOGIN_FAILED:
|
||||||
|
return __('Failed login');
|
||||||
|
case self::DESCRIPTION_USER_PASSWORD_RESET:
|
||||||
|
return __('Reset password');
|
||||||
|
case self::DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER:
|
||||||
|
return __('Error sending email to customer');
|
||||||
|
case self::DESCRIPTION_EMAILS_SENDING_ERROR_TO_USER:
|
||||||
|
return __('Error sending email to user');
|
||||||
|
case self::DESCRIPTION_EMAILS_SENDING_ERROR_INVITE:
|
||||||
|
return __('Error sending invitation email to user');
|
||||||
|
case self::DESCRIPTION_EMAILS_SENDING_ERROR_PASSWORD_CHANGED:
|
||||||
|
return __('Error sending password changed notification to user');
|
||||||
|
case self::DESCRIPTION_EMAILS_SENDING_ERROR_ALERT:
|
||||||
|
return __('Error sending alert');
|
||||||
|
case self::DESCRIPTION_EMAILS_SENDING_WRONG_EMAIL:
|
||||||
|
return __('Error sending email to the user who replied to notification from wrong email');
|
||||||
|
case self::DESCRIPTION_EMAILS_FETCHING_ERROR:
|
||||||
|
return __('Error fetching email');
|
||||||
|
case self::DESCRIPTION_SYSTEM_ERROR:
|
||||||
|
return __('System error');
|
||||||
|
case self::DESCRIPTION_USER_DELETED:
|
||||||
|
return __('Deleted user');
|
||||||
|
default:
|
||||||
|
return $this->description;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get title for the log record.
|
||||||
|
*/
|
||||||
|
public static function getLogTitle($log_name)
|
||||||
|
{
|
||||||
|
switch ($log_name) {
|
||||||
|
case self::NAME_USER:
|
||||||
|
return __('Users');
|
||||||
|
case self::NAME_OUT_EMAILS:
|
||||||
|
return __('Outgoing Emails');
|
||||||
|
case self::NAME_EMAILS_SENDING:
|
||||||
|
return __('Send Errors');
|
||||||
|
case self::NAME_EMAILS_FETCHING:
|
||||||
|
return __('Fetch Errors');
|
||||||
|
case self::NAME_SYSTEM:
|
||||||
|
return __('System');
|
||||||
|
case self::NAME_APP_LOGS:
|
||||||
|
return __('App Logs');
|
||||||
|
default:
|
||||||
|
return ucwords(str_replace('_', ' ', $log_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function formatColTitle($col)
|
||||||
|
{
|
||||||
|
$col = str_replace('_', ' ', $col);
|
||||||
|
$col = ucfirst($col);
|
||||||
|
|
||||||
|
return $col;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get log names.
|
||||||
|
*
|
||||||
|
* @return [type] [description]
|
||||||
|
*/
|
||||||
|
public static function getLogNames()
|
||||||
|
{
|
||||||
|
return self::select('log_name')->distinct()->pluck('log_name')->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available log names.
|
||||||
|
*
|
||||||
|
* @return [type] [description]
|
||||||
|
*/
|
||||||
|
public static function getAvailableLogs($check_existing = true)
|
||||||
|
{
|
||||||
|
$available_logs = self::$available_logs;
|
||||||
|
if ($check_existing) {
|
||||||
|
$available_logs = array_merge($available_logs, self::getLogNames());
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_unique(\Eventy::filter('activity_log.available_logs', self::$available_logs));
|
||||||
|
}
|
||||||
|
}
|
450
freescout-dist/app/Attachment.php
Normal file
450
freescout-dist/app/Attachment.php
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class Attachment extends Model
|
||||||
|
{
|
||||||
|
const TYPE_TEXT = 0;
|
||||||
|
const TYPE_MULTIPART = 1;
|
||||||
|
const TYPE_MESSAGE = 2;
|
||||||
|
const TYPE_APPLICATION = 3;
|
||||||
|
const TYPE_AUDIO = 4;
|
||||||
|
const TYPE_IMAGE = 5;
|
||||||
|
const TYPE_VIDEO = 6;
|
||||||
|
const TYPE_MODEL = 7;
|
||||||
|
const TYPE_OTHER = 8;
|
||||||
|
|
||||||
|
const DIRECTORY = 'attachment';
|
||||||
|
|
||||||
|
CONST DISK = 'private';
|
||||||
|
|
||||||
|
CONST MIME_TYPE_MAX_LENGTH = 127;
|
||||||
|
|
||||||
|
// https://github.com/Webklex/laravel-imap/blob/master/src/IMAP/Attachment.php
|
||||||
|
public static $types = [
|
||||||
|
'message' => self::TYPE_MESSAGE,
|
||||||
|
'application' => self::TYPE_APPLICATION,
|
||||||
|
'audio' => self::TYPE_AUDIO,
|
||||||
|
'image' => self::TYPE_IMAGE,
|
||||||
|
'video' => self::TYPE_VIDEO,
|
||||||
|
'model' => self::TYPE_MODEL,
|
||||||
|
'text' => self::TYPE_TEXT,
|
||||||
|
'multipart' => self::TYPE_MULTIPART,
|
||||||
|
'other' => self::TYPE_OTHER,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static $type_extensions = [
|
||||||
|
self::TYPE_VIDEO => ['flv', 'mp4', 'm3u8', 'ts', '3gp', 'mov', 'avi', 'wmv']
|
||||||
|
];
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get thread.
|
||||||
|
*/
|
||||||
|
public function thread()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Thread');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save attachment to file and database.
|
||||||
|
*/
|
||||||
|
public static function create($file_name, $mime_type, $type, $content, $uploaded_file, $embedded = false, $thread_id = null, $user_id = null)
|
||||||
|
{
|
||||||
|
if (!$content && !$uploaded_file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize mime type.
|
||||||
|
// https://github.com/freescout-helpdesk/freescout/issues/3048
|
||||||
|
$mime_duplicate = strpos($mime_type, "application/vnd.openxmlformats", 1);
|
||||||
|
if ($mime_duplicate) {
|
||||||
|
$mime_type = substr($mime_type, $mime_duplicate);
|
||||||
|
}
|
||||||
|
$mime_type = substr($mime_type, 0, self::MIME_TYPE_MAX_LENGTH);
|
||||||
|
|
||||||
|
$orig_extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||||
|
|
||||||
|
// Add underscore to the extension if file has restricted extension.
|
||||||
|
$file_name = \Helper::sanitizeUploadedFileName($file_name, $uploaded_file, $content);
|
||||||
|
|
||||||
|
// Replace some symbols in file name.
|
||||||
|
// Gmail can not load image if it contains spaces.
|
||||||
|
$file_name = preg_replace('/[ #\/]/', '_', $file_name);
|
||||||
|
// Replace soft hyphens.
|
||||||
|
$file_name = str_replace(html_entity_decode('­'), '_', $file_name);
|
||||||
|
|
||||||
|
if (!$file_name) {
|
||||||
|
if (!$orig_extension) {
|
||||||
|
preg_match("/.*\/([^\/]+)$/", $mime_type, $m);
|
||||||
|
if (!empty($m[1])) {
|
||||||
|
$orig_extension = $m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$file_name = uniqid();
|
||||||
|
if ($orig_extension) {
|
||||||
|
$file_name .= '.'.$orig_extension;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/freescout-helpdesk/freescout/issues/2385
|
||||||
|
// Fix for webklex/php-imap.
|
||||||
|
if ($file_name == 'undefined' && $mime_type == 'message/rfc822') {
|
||||||
|
$file_name = 'RFC822.eml';
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/freescout-helpdesk/freescout/issues/1412#issuecomment-1658881493
|
||||||
|
if ($file_name == 'undefined' && $mime_type == 'text/calendar') {
|
||||||
|
$file_name = 'calendar.ics';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($file_name) > 255) {
|
||||||
|
$without_ext = pathinfo($file_name, PATHINFO_FILENAME);
|
||||||
|
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
|
||||||
|
// 125 because file name may have unicode symbols.
|
||||||
|
$file_name = \Helper::substrUnicode($without_ext, 0, 125-strlen($extension)-1);
|
||||||
|
$file_name .= '.'.$extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$type) {
|
||||||
|
$type = self::detectType($mime_type, $orig_extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachment = new self();
|
||||||
|
$attachment->thread_id = $thread_id;
|
||||||
|
$attachment->user_id = $user_id;
|
||||||
|
$attachment->file_name = $file_name;
|
||||||
|
$attachment->mime_type = $mime_type;
|
||||||
|
$attachment->type = $type;
|
||||||
|
$attachment->embedded = $embedded;
|
||||||
|
$attachment->save();
|
||||||
|
|
||||||
|
$file_info = self::saveFileToDisk($attachment, $file_name, $content, $uploaded_file);
|
||||||
|
|
||||||
|
$attachment->file_dir = $file_info['file_dir'];
|
||||||
|
$attachment->size = Storage::disk(self::DISK)->size($file_info['file_path']);
|
||||||
|
$attachment->save();
|
||||||
|
|
||||||
|
return $attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save file to the disk and return file_dir.
|
||||||
|
*/
|
||||||
|
public static function saveFileToDisk($attachment, $file_name, $content, $uploaded_file)
|
||||||
|
{
|
||||||
|
// Save file from content or copy file.
|
||||||
|
// We have to keep file name as is, so if file exists we create extra folder.
|
||||||
|
// Examples: 1/2/3
|
||||||
|
$file_dir = self::generatePath($attachment->id);
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
do {
|
||||||
|
$i++;
|
||||||
|
$file_path = self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir.$i.DIRECTORY_SEPARATOR.$file_name;
|
||||||
|
} while (Storage::disk(self::DISK)->exists($file_path));
|
||||||
|
|
||||||
|
$file_dir .= $i.DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
|
if ($uploaded_file) {
|
||||||
|
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, ['disk' => self::DISK]);
|
||||||
|
} else {
|
||||||
|
Storage::disk(self::DISK)->put($file_path, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
\Helper::sanitizeUploadedFileData($file_path, \Helper::getPrivateStorage(), $content);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'file_dir' => $file_dir,
|
||||||
|
'file_path' => $file_path,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file path.
|
||||||
|
* Examples: 1/2, 1/3.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function generatePath($id)
|
||||||
|
{
|
||||||
|
$hash = md5($id);
|
||||||
|
|
||||||
|
$first = -1;
|
||||||
|
$second = 0;
|
||||||
|
|
||||||
|
for ($i = 0; $i < strlen($hash); $i++) {
|
||||||
|
if (is_numeric($hash[$i])) {
|
||||||
|
if ($first == -1) {
|
||||||
|
$first = $hash[$i];
|
||||||
|
} else {
|
||||||
|
$second = $hash[$i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($first == -1) {
|
||||||
|
$first = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $first.DIRECTORY_SEPARATOR.$second.DIRECTORY_SEPARATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect attachment type by it's mime type.
|
||||||
|
*
|
||||||
|
* @param string $mime_type
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function detectType($mime_type, $extension = '')
|
||||||
|
{
|
||||||
|
if (preg_match("/^text\//", $mime_type)) {
|
||||||
|
return self::TYPE_TEXT;
|
||||||
|
} elseif (preg_match("/^message\//", $mime_type)) {
|
||||||
|
return self::TYPE_MESSAGE;
|
||||||
|
} elseif (preg_match("/^application\//", $mime_type)) {
|
||||||
|
// This is tricky mime type.
|
||||||
|
// For .mp4 mime type can be application/octet-stream
|
||||||
|
if (!empty($extension) && in_array(strtolower($extension), self::$type_extensions[self::TYPE_VIDEO])) {
|
||||||
|
return self::TYPE_VIDEO;
|
||||||
|
}
|
||||||
|
return self::TYPE_APPLICATION;
|
||||||
|
} elseif (preg_match("/^audio\//", $mime_type)) {
|
||||||
|
return self::TYPE_AUDIO;
|
||||||
|
} elseif (preg_match("/^image\//", $mime_type)) {
|
||||||
|
return self::TYPE_IMAGE;
|
||||||
|
} elseif (preg_match("/^video\//", $mime_type)) {
|
||||||
|
return self::TYPE_VIDEO;
|
||||||
|
} elseif (preg_match("/^model\//", $mime_type)) {
|
||||||
|
return self::TYPE_MODEL;
|
||||||
|
} else {
|
||||||
|
return self::TYPE_OTHER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert type name to integer.
|
||||||
|
*/
|
||||||
|
public static function typeNameToInt($type_name)
|
||||||
|
{
|
||||||
|
if (!empty(self::$types[$type_name])) {
|
||||||
|
return self::$types[$type_name];
|
||||||
|
} else {
|
||||||
|
return self::TYPE_OTHER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get attachment full public URL.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function url()
|
||||||
|
{
|
||||||
|
$file_url = Storage::url($this->getStorageFilePath());
|
||||||
|
|
||||||
|
// Fix percents.
|
||||||
|
// https://github.com/freescout-helpdesk/freescout/issues/3530
|
||||||
|
$file_url = str_replace('%', '%25', $file_url);
|
||||||
|
|
||||||
|
return $file_url.'?id='.$this->id.'&token='.$this->getToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hashed security token for the attachment.
|
||||||
|
*/
|
||||||
|
public function getToken()
|
||||||
|
{
|
||||||
|
// \Hash::make() may contain . and / symbols which may cause problems.
|
||||||
|
return md5(config('app.key').$this->id.$this->size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs the current Attachment as download
|
||||||
|
*/
|
||||||
|
public function download($view = false)
|
||||||
|
{
|
||||||
|
$headers = [];
|
||||||
|
// #533
|
||||||
|
//return $this->getDisk()->download($this->getStorageFilePath(), \Str::ascii($this->file_name));
|
||||||
|
if ($view) {
|
||||||
|
$headers['Content-Disposition'] = '';
|
||||||
|
}
|
||||||
|
$file_name = $this->file_name;
|
||||||
|
|
||||||
|
if ($file_name == "RFC822"){
|
||||||
|
$file_name = $file_name.'.eml';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getDisk()->download($this->getStorageFilePath(), $file_name, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDisk() {
|
||||||
|
return Storage::disk(self::DISK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert size into human readable format.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getSizeName()
|
||||||
|
{
|
||||||
|
return self::formatBytes($this->size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* attachment/...
|
||||||
|
*/
|
||||||
|
public function getStorageFilePath()
|
||||||
|
{
|
||||||
|
return self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /var/html/storage/app/attachment/...
|
||||||
|
*/
|
||||||
|
public function getLocalFilePath($full = true)
|
||||||
|
{
|
||||||
|
if ($full) {
|
||||||
|
return $this->getDisk()->path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||||
|
} else {
|
||||||
|
return DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the attachment file actually exists on the disk.
|
||||||
|
*/
|
||||||
|
public function fileExists()
|
||||||
|
{
|
||||||
|
return $this->getDisk()->exists(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function formatBytes($size, $precision = 0)
|
||||||
|
{
|
||||||
|
$size = (int) $size;
|
||||||
|
if ($size > 0) {
|
||||||
|
$base = log($size) / log(1024);
|
||||||
|
$suffixes = [' b', ' KB', ' MB', ' GB', ' TB'];
|
||||||
|
|
||||||
|
return round(pow(1024, $base - floor($base)), $precision).$suffixes[floor($base)];
|
||||||
|
} else {
|
||||||
|
return $size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete attachments from disk and DB.
|
||||||
|
* Embeds are not taken into account.
|
||||||
|
*
|
||||||
|
* @param array $attachments
|
||||||
|
*/
|
||||||
|
public static function deleteByIds($attachment_ids)
|
||||||
|
{
|
||||||
|
if (!count($attachment_ids)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$attachments = self::whereIn('id', $attachment_ids)->get();
|
||||||
|
|
||||||
|
// Delete from disk
|
||||||
|
self::deleteForever($attachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete attachments by thread IDs.
|
||||||
|
*/
|
||||||
|
public static function deleteByThreadIds($thread_ids)
|
||||||
|
{
|
||||||
|
if (!count($thread_ids)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$attachments = self::whereIn('thread_id', $thread_ids)->get();
|
||||||
|
|
||||||
|
// Delete from disk
|
||||||
|
self::deleteForever($attachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function deleteForever($attachments)
|
||||||
|
{
|
||||||
|
// Delete from disk
|
||||||
|
foreach ($attachments as $attachment) {
|
||||||
|
$attachment->getDisk()->delete($attachment->getStorageFilePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete from DB
|
||||||
|
self::whereIn('id', $attachments->pluck('id')->toArray())->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete attachments and update Thread & Conversation.
|
||||||
|
*/
|
||||||
|
public static function deleteAttachments($attachments)
|
||||||
|
{
|
||||||
|
if (!$attachments instanceof \Illuminate\Support\Collection) {
|
||||||
|
$attachments = collect($attachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($attachments as $attachment) {
|
||||||
|
if ($attachment->thread_id && $attachment->thread
|
||||||
|
&& count($attachment->thread->attachments) <= 1
|
||||||
|
) {
|
||||||
|
$attachment->thread->has_attachments = false;
|
||||||
|
$attachment->thread->save();
|
||||||
|
// Update conversation.
|
||||||
|
$conversation = $attachment->thread->conversation;
|
||||||
|
foreach ($conversation->threads as $thread) {
|
||||||
|
if ($thread->has_attachments) {
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$conversation->has_attachments = false;
|
||||||
|
$conversation->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Attachment::deleteForever($attachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a copy of the attachment and it's file.
|
||||||
|
*/
|
||||||
|
public function duplicate($thread_id = null)
|
||||||
|
{
|
||||||
|
$new_attachment = $this->replicate();
|
||||||
|
if ($thread_id) {
|
||||||
|
$new_attachment->thread_id = $thread_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$new_attachment->save();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$attachment_file = new \Illuminate\Http\UploadedFile(
|
||||||
|
$this->getLocalFilePath(), $this->file_name,
|
||||||
|
null, null, true
|
||||||
|
);
|
||||||
|
|
||||||
|
$file_info = Attachment::saveFileToDisk($new_attachment, $new_attachment->file_name, '', $attachment_file);
|
||||||
|
|
||||||
|
if (!empty($file_info['file_dir'])) {
|
||||||
|
$new_attachment->file_dir = $file_info['file_dir'];
|
||||||
|
$new_attachment->save();
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Helper::logException($e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $new_attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFileContents()
|
||||||
|
{
|
||||||
|
return $this->getDisk()->get($this->getStorageFilePath());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Created by PhpStorm.
|
||||||
|
* User: leemason
|
||||||
|
* Date: 31/10/15
|
||||||
|
* Time: 00:20.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Broadcasting\Broadcasters;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Broadcasting\Broadcasters\Broadcaster;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
|
||||||
|
class PolycastBroadcaster extends Broadcaster
|
||||||
|
{
|
||||||
|
//private $db = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete old events after 2 minutes.
|
||||||
|
*/
|
||||||
|
private $delete_old = 2;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//$this->db = $app['db'];
|
||||||
|
if (\Config::get('broadcasting.connections.polycast.delete_old')) {
|
||||||
|
$this->delete_old = \Config::get('broadcasting.connections.polycast.delete_old');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast is called when the queued job is processed.
|
||||||
|
*/
|
||||||
|
public function broadcast(array $channels, $event, array $payload = [])
|
||||||
|
{
|
||||||
|
// delete events older than two minutes
|
||||||
|
\DB::table('polycast_events')->where('created_at', '<', Carbon::now()->subMinutes($this->delete_old)->toDateTimeString())->delete();
|
||||||
|
|
||||||
|
// insert the new event
|
||||||
|
\DB::table('polycast_events')->insert([
|
||||||
|
'channels' => json_encode($channels),
|
||||||
|
'event' => $event,
|
||||||
|
'payload' => json_encode($payload),
|
||||||
|
'created_at' => Carbon::now()->toDateTimeString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate the incoming request for a given channel.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function auth($request)
|
||||||
|
{
|
||||||
|
// For connect request
|
||||||
|
if (empty($request->channels)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all channels
|
||||||
|
foreach ($request->channels as $channel_name => $channel_info) {
|
||||||
|
// Copied from Illuminate\Broadcasting\Broadcasters\PusherBroadcaster
|
||||||
|
if (Str::startsWith($channel_name, ['private-', 'presence-']) &&
|
||||||
|
!$request->user()) {
|
||||||
|
|
||||||
|
throw new AccessDeniedHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Str::startsWith($channel_name, ['private-', 'presence-'])) {
|
||||||
|
$channelName = Str::startsWith($channel_name, 'private-')
|
||||||
|
? Str::replaceFirst('private-', '', $channel_name)
|
||||||
|
: Str::replaceFirst('presence-', '', $channel_name);
|
||||||
|
// This throws an exception if needed.
|
||||||
|
parent::verifyUserCanAccessChannel(
|
||||||
|
$request, $channelName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the valid authentication response.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param mixed $result
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function validAuthenticationResponse($request, $result)
|
||||||
|
{
|
||||||
|
// By some reason this is never called
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Copied from Illuminate\Broadcasting\Broadcasters\RedisBroadcaster
|
||||||
|
// if (is_bool($result)) {
|
||||||
|
// return json_encode($result);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return json_encode(['channel_data' => [
|
||||||
|
// 'user_id' => $request->user()->getAuthIdentifier(),
|
||||||
|
// 'user_info' => $result,
|
||||||
|
// ]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDeferred()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Created as there was an error:
|
||||||
|
* "Call to undefined method App\Broadcasting\Broadcasters\PolycastBroadcaster::channel()"
|
||||||
|
*
|
||||||
|
* It is called from routes/channels.php
|
||||||
|
*/
|
||||||
|
// public function channel($channel, callable $callback)
|
||||||
|
// {
|
||||||
|
// return true;
|
||||||
|
// //return (int) $user->id === (int) $id;
|
||||||
|
// }
|
||||||
|
}
|
29
freescout-dist/app/Channels/RealtimeBroadcastChannel.php
Normal file
29
freescout-dist/app/Channels/RealtimeBroadcastChannel.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Channels;
|
||||||
|
|
||||||
|
use App\Events\RealtimeBroadcastNotificationCreated;
|
||||||
|
use Illuminate\Notifications\Channels\BroadcastChannel;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class RealtimeBroadcastChannel extends BroadcastChannel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Send the given notification immediately using non-quable event.
|
||||||
|
*
|
||||||
|
* @param mixed $notifiable
|
||||||
|
* @param \Illuminate\Notifications\Notification $notification
|
||||||
|
*
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function send($notifiable, Notification $notification)
|
||||||
|
{
|
||||||
|
$message = $this->getData($notifiable, $notification);
|
||||||
|
|
||||||
|
$event = new RealtimeBroadcastNotificationCreated(
|
||||||
|
$notifiable, $notification, is_array($message) ? $message : $message->data
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->events->dispatch($event);
|
||||||
|
}
|
||||||
|
}
|
44
freescout-dist/app/Console/Commands/AfterAppUpdate.php
Normal file
44
freescout-dist/app/Console/Commands/AfterAppUpdate.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class AfterAppUpdate extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:after-app-update';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Run commands after application has been updated';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->call('freescout:clear-cache');
|
||||||
|
$this->call('migrate', ['--force' => true]);
|
||||||
|
$this->call('queue:restart');
|
||||||
|
}
|
||||||
|
}
|
43
freescout-dist/app/Console/Commands/Build.php
Normal file
43
freescout-dist/app/Console/Commands/Build.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class Build extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:build';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Run commands building application assets';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->call('freescout:generate-vars');
|
||||||
|
$this->call('laroute:generate');
|
||||||
|
}
|
||||||
|
}
|
117
freescout-dist/app/Console/Commands/CheckConvViewers.php
Normal file
117
freescout-dist/app/Console/Commands/CheckConvViewers.php
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class CheckConvViewers extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:check-conv-viewers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Check if user finished viewing conversation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
// Check dates in cache.
|
||||||
|
$cache_key = 'conv_view';
|
||||||
|
$cache_data = \Cache::get($cache_key);
|
||||||
|
|
||||||
|
if (empty($cache_data) || !is_array($cache_data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = Carbon::now();
|
||||||
|
$need_update = false;
|
||||||
|
foreach ($cache_data as $conversation_id => $conv_data) {
|
||||||
|
if (empty($conv_data) || !is_array($conv_data)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach ($conv_data as $user_id => $data) {
|
||||||
|
|
||||||
|
if (!isset($data['t']) || !isset($data['r'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$view_date = Carbon::createFromFormat('Y-m-d H:i:s', $data['t']);
|
||||||
|
|
||||||
|
if ($view_date && $now->diffInSeconds($view_date) > 25) {
|
||||||
|
// Remove user from viewers.
|
||||||
|
unset($cache_data[$conversation_id][$user_id]);
|
||||||
|
if (empty($cache_data[$conversation_id])) {
|
||||||
|
unset($cache_data[$conversation_id]);
|
||||||
|
}
|
||||||
|
$need_update = true;
|
||||||
|
|
||||||
|
\Cache::forget('conv_view_'.$user_id.'_'.$conversation_id);
|
||||||
|
|
||||||
|
// Create event to let other users know that user finished viewing conversation.
|
||||||
|
$notification_data = [
|
||||||
|
'conversation_id' => $conversation_id,
|
||||||
|
'user_id' => $user_id,
|
||||||
|
];
|
||||||
|
event(new \App\Events\RealtimeConvViewFinish($notification_data));
|
||||||
|
|
||||||
|
\Eventy::action('conversation.view.finish', $conversation_id, $user_id, $now->diffInSeconds($view_date));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($need_update) {
|
||||||
|
// Update conversation cache data.
|
||||||
|
\Cache::put($cache_key, $cache_data, 20 /*minutes*/);
|
||||||
|
}
|
||||||
|
/*$cache_key = 'conv_view_'.$this->user_id.'_'.$this->conversation_id;
|
||||||
|
$cache_data = \Cache::get($cache_key);
|
||||||
|
|
||||||
|
if (!isset($cache_data['t']) || !isset($cache_data['r'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$view_date = Carbon::createFromFormat('Y-m-d H:i:s', $cache_data['t']);
|
||||||
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
if ($view_date && $now->diffInSeconds($view_date) > 30) {
|
||||||
|
$cache_key = 'conv_view';
|
||||||
|
if (!empty($cache_data[$this->conversation_id]) && !empty($cache_data[$this->conversation_id][$this->user_id])) {
|
||||||
|
// Remove user from viewers.
|
||||||
|
unset($cache_data[$this->conversation_id][$this->user_id]);
|
||||||
|
|
||||||
|
// Update conversation cache data.
|
||||||
|
\Cache::put($cache_key, $cache_data, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create event to let other users know that user finished viewing conversation.
|
||||||
|
$notification_data = [
|
||||||
|
'conversation_id' => $conversation->id,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
];
|
||||||
|
event(new \App\Events\RealtimeConvViewFinish($notification_data));
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
63
freescout-dist/app/Console/Commands/CheckRequirements.php
Normal file
63
freescout-dist/app/Console/Commands/CheckRequirements.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class CheckRequirements extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:check-requirements';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Check console version of PHP';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
// PHP extensions.
|
||||||
|
$php_extensions = \Helper::checkRequiredExtensions();
|
||||||
|
|
||||||
|
$this->comment("PHP Version");
|
||||||
|
$this->line(' '.str_pad(phpversion(), 30, '.'). ' '.(version_compare(phpversion(), config('installer.core.minPhpVersion'), '>=') ? '<fg=green>OK</>' : '<fg=red>NOT FOUND</>'), false);
|
||||||
|
|
||||||
|
$this->comment("PHP Extensions");
|
||||||
|
$this->output($php_extensions);
|
||||||
|
|
||||||
|
// Functions.
|
||||||
|
$functions = \Helper::checkRequiredFunctions();
|
||||||
|
|
||||||
|
$this->comment("Functions");
|
||||||
|
$this->output($functions);
|
||||||
|
$this->line('');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function output($items)
|
||||||
|
{
|
||||||
|
foreach ($items as $item => $status) {
|
||||||
|
$this->line(' '.str_pad($item, 30, '.'). ' '.($status ? '<fg=green>OK</>' : '<fg=red>NOT FOUND</>'), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class CleanNotificationsTable extends Command
|
||||||
|
{
|
||||||
|
const PERIOD = '-6 months';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:clean-notifications-table';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Delete old read records from notifications table.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
\DB::table('notifications')->where('created_at', '<', \Carbon\Carbon::now()->modify(self::PERIOD))
|
||||||
|
->whereNotNull('read_at')
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
$this->info('['.date('Y-m-d H:i:s').'] Deleted old read notifications for: '.self::PERIOD);
|
||||||
|
}
|
||||||
|
}
|
46
freescout-dist/app/Console/Commands/CleanSendLog.php
Normal file
46
freescout-dist/app/Console/Commands/CleanSendLog.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class CleanSendLog extends Command
|
||||||
|
{
|
||||||
|
const PERIOD = '-6 months';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:clean-send-log';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Delete old records from send log.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$logs = \App\SendLog::where('created_at', '<', \Carbon\Carbon::now()->modify(self::PERIOD))->delete();
|
||||||
|
|
||||||
|
$this->info('['.date('Y-m-d H:i:s').'] Deleted send logs: '.self::PERIOD);
|
||||||
|
}
|
||||||
|
}
|
46
freescout-dist/app/Console/Commands/CleanTmp.php
Normal file
46
freescout-dist/app/Console/Commands/CleanTmp.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
// When processing attachments FreeScout may create files in /tmp folder.
|
||||||
|
// So it's good to clean this folder periodically.
|
||||||
|
class CleanTmp extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:clean-tmp';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Remove from system temp folder FreeScout files older than 1 week to avoid "No space left on device"';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
shell_exec('find '.\Helper::getTempDir().' -mtime +7 -type f -name '.\Helper::getTempFilePrefix().'* -exec rm -r -f {} \;');
|
||||||
|
|
||||||
|
$this->comment("Done");
|
||||||
|
}
|
||||||
|
}
|
62
freescout-dist/app/Console/Commands/ClearCache.php
Normal file
62
freescout-dist/app/Console/Commands/ClearCache.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class ClearCache extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:clear-cache {--doNotCacheConfig} {--doNotGenerateVars}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Clear application cache and cache config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$this->call('clear-compiled');
|
||||||
|
$this->call('cache:clear');
|
||||||
|
$this->call('view:clear');
|
||||||
|
if ($this->option('doNotCacheConfig')) {
|
||||||
|
$this->call('config:clear');
|
||||||
|
} else {
|
||||||
|
$this->call('config:cache');
|
||||||
|
// Laravel users `require` function to include config.php
|
||||||
|
// If opcache is being used for few seconds config.php is being cached.
|
||||||
|
if (function_exists('opcache_invalidate')) {
|
||||||
|
opcache_invalidate(app()->getCachedConfigPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Regenerate vars to get new data from .env
|
||||||
|
if (!$this->option('doNotGenerateVars')) {
|
||||||
|
$this->call('freescout:generate-vars');
|
||||||
|
}
|
||||||
|
// This should not be done during installation.
|
||||||
|
if (\Helper::isInstalled()) {
|
||||||
|
\Helper::queueWorkerRestart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
102
freescout-dist/app/Console/Commands/CreateUser.php
Normal file
102
freescout-dist/app/Console/Commands/CreateUser.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class CreateUser extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:create-user {--role=} {--firstName=} {--lastName=} {--email=} {--password=}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Create a new user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$class = config(
|
||||||
|
'auth.providers.'.config(
|
||||||
|
'auth.guards.'.config(
|
||||||
|
'auth.defaults.guard'
|
||||||
|
).'.provider'
|
||||||
|
).'.model'
|
||||||
|
);
|
||||||
|
$user = new $class();
|
||||||
|
|
||||||
|
$user->role = $this->option('role');
|
||||||
|
if ($user->role ) {
|
||||||
|
if (!in_array($user->role , User::$roles)) {
|
||||||
|
$this->error('Invalid role');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$user->role = $this->ask('User role (admin/user)', 'admin');
|
||||||
|
while (!in_array($user->role, User::$roles)) {
|
||||||
|
$this->error('Invalid role');
|
||||||
|
$user->role = $this->ask('Please enter valid role');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$user->role = array_flip(User::$roles)[$user->role];
|
||||||
|
|
||||||
|
$user->first_name = $this->option('firstName') ? $this->option('firstName') : $this->ask('User first name');
|
||||||
|
$user->last_name = $this->option('lastName') ? $this->option('lastName') : $this->ask('User last name');
|
||||||
|
|
||||||
|
$user->email = $this->option('email');
|
||||||
|
if ($user->email) {
|
||||||
|
if (!filter_var($user->email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$this->error('Invalid email address');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$user->email = $this->ask('User email address');
|
||||||
|
while (!filter_var($user->email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$this->error('Incorrect email address');
|
||||||
|
$user->email = $this->ask('Please enter valid email address');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->password = \Hash::make($this->option('password') ? $this->option('password') : $this->secret('User password'));
|
||||||
|
|
||||||
|
if ($this->confirm('Do you want to create the user?', true)) {
|
||||||
|
if ($user->isAdmin()) {
|
||||||
|
$user->invite_state = User::INVITE_STATE_ACTIVATED;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$user->save();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->line($e->getMessage());
|
||||||
|
$this->error('User already exists.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('User created with id: '.$user->id);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
1503
freescout-dist/app/Console/Commands/FetchEmails.php
Normal file
1503
freescout-dist/app/Console/Commands/FetchEmails.php
Normal file
File diff suppressed because it is too large
Load Diff
73
freescout-dist/app/Console/Commands/FetchMonitor.php
Normal file
73
freescout-dist/app/Console/Commands/FetchMonitor.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class FetchMonitor extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:fetch-monitor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Check emails fetching and send alert if fething is not working or recovered';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
$options = \Option::getOptions([
|
||||||
|
'alert_fetch_period',
|
||||||
|
'fetch_emails_last_successful_run',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$last_successful_run = $options['fetch_emails_last_successful_run'];
|
||||||
|
if ($last_successful_run && $last_successful_run < $now - ((config('app.fetch_schedule') * 60) + ($options['alert_fetch_period'] * 60))) {
|
||||||
|
$mins_ago = floor(($now - $last_successful_run) / 60);
|
||||||
|
|
||||||
|
$text = 'There are some problems fetching emails: last time emails were successfully fetched <strong>'.$mins_ago.' minutes ago</strong>. Please check <a href="'.route('logs', ['name' => 'fetch_errors']).'">fetching logs</a> and <a href="'.route('system').'#cron">make sure</a> that the following cron task is running: <code>php artisan schedule:run</code>';
|
||||||
|
|
||||||
|
if (\Option::get('alert_fetch') && !\Option::get('alert_fetch_sent')) {
|
||||||
|
// We send alert only once
|
||||||
|
\Option::set('alert_fetch_sent', true);
|
||||||
|
\MailHelper::sendAlertMail($text, 'Fetching Problems');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->error('['.date('Y-m-d H:i:s').'] '.$text);
|
||||||
|
} elseif (!$last_successful_run) {
|
||||||
|
$this->line('['.date('Y-m-d H:i:s').'] Fetching has not been configured yet');
|
||||||
|
} else {
|
||||||
|
if (\Option::get('alert_fetch_sent')) {
|
||||||
|
$text = 'Previously there were some problems fetching emails. Fetching recovered and functioning now!';
|
||||||
|
|
||||||
|
\MailHelper::sendAlertMail($text, 'Fetching Recovered');
|
||||||
|
}
|
||||||
|
\Option::set('alert_fetch_sent', false);
|
||||||
|
|
||||||
|
$this->info('['.date('Y-m-d H:i:s').'] Fetching is working');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
freescout-dist/app/Console/Commands/GenerateVars.php
Normal file
77
freescout-dist/app/Console/Commands/GenerateVars.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Comman generates vars.js file with variables and translated strings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
|
||||||
|
class GenerateVars extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:generate-vars';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Generates vars.js file with variables and translated string';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$params = [
|
||||||
|
'locales' => \Helper::getAllLocales(),
|
||||||
|
];
|
||||||
|
|
||||||
|
//$filesystem = new Filesystem();
|
||||||
|
|
||||||
|
//$file_path = public_path('js/vars.js');
|
||||||
|
$file_path = storage_path('app/public/js/vars.js');
|
||||||
|
|
||||||
|
$content = view('js/vars', $params)->render();
|
||||||
|
|
||||||
|
//$filesystem->put($file_path, $content);
|
||||||
|
// Save vars only if content changed
|
||||||
|
try {
|
||||||
|
if (\Storage::exists('js/vars.js')) {
|
||||||
|
$old_content = \Storage::get('js/vars.js');
|
||||||
|
if ($content != $old_content) {
|
||||||
|
\Storage::put('js/vars.js', $content);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
\Storage::put('js/vars.js', $content);
|
||||||
|
}
|
||||||
|
$this->info("Created: ".substr($file_path, strlen(base_path())+1));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$msg = "Error occurred saving /storage/app/public/js/vars.js. ".\Helper::formatException($e);
|
||||||
|
\Log::error($msg);
|
||||||
|
$this->error($msg);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
freescout-dist/app/Console/Commands/LogoutUsers.php
Normal file
61
freescout-dist/app/Console/Commands/LogoutUsers.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class LogoutUsers extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:logout-users';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Logout all users';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Remove files from storage/frameworks/sessions
|
||||||
|
$files = \File::files(storage_path('framework/sessions'));
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
try {
|
||||||
|
$deleted = \File::delete($file->getPathname());
|
||||||
|
if ($deleted) {
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->line('Deleted sessions: '.$count);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
96
freescout-dist/app/Console/Commands/LogsMonitor.php
Normal file
96
freescout-dist/app/Console/Commands/LogsMonitor.php
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class LogsMonitor extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:logs-monitor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Send new log records by email';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$now = \Carbon\Carbon::now();
|
||||||
|
|
||||||
|
$options = \Option::getOptions([
|
||||||
|
'alert_logs_names',
|
||||||
|
'alert_logs_period',
|
||||||
|
], [
|
||||||
|
'alert_logs_period' => config('app.alert_logs_period'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$options['alert_logs_names']) {
|
||||||
|
$this->error('['.date('Y-m-d H:i:s').'] No logs to monitor selected');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!$options['alert_logs_period']) {
|
||||||
|
$this->error('['.date('Y-m-d H:i:s').'] No logs monitoring period set');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = \App\ActivityLog::whereIn('log_name', $options['alert_logs_names'])
|
||||||
|
->where('created_at', '>=', \Carbon\Carbon::now()->modify('-1 '.$options['alert_logs_period'])->toDateTimeString())
|
||||||
|
->where('created_at', '<', $now->toDateTimeString())
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if (!count($logs)) {
|
||||||
|
$this->line('['.date('Y-m-d H:i:s').'] No new log records found for the last '.$options['alert_logs_period']);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$names = $logs->pluck('log_name')->unique()->toArray();
|
||||||
|
$text = 'Logs having new records for the last '.$options['alert_logs_period'].':<ul>';
|
||||||
|
foreach ($names as $name) {
|
||||||
|
$text .= '<li>';
|
||||||
|
$text .= '<strong>'.\App\ActivityLog::getLogTitle($name).'</strong>';
|
||||||
|
$text .= '</li>';
|
||||||
|
}
|
||||||
|
$text .= '</ul>';
|
||||||
|
|
||||||
|
foreach ($names as $name) {
|
||||||
|
$text .= '<br/><strong>'.\App\ActivityLog::getLogTitle($name).'</strong><br/>';
|
||||||
|
foreach ($logs as $log) {
|
||||||
|
if ($log->log_name != $name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$text .= '● ['.$log->created_at.'] '.$log->getEventDescription().' <code>'.$log->properties.'</code><br/>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Send alert.
|
||||||
|
\MailHelper::sendAlertMail($text, 'Logs Monitoring');
|
||||||
|
|
||||||
|
$this->line($text);
|
||||||
|
|
||||||
|
$this->info('['.date('Y-m-d H:i:s').'] Monitoring finished');
|
||||||
|
}
|
||||||
|
}
|
118
freescout-dist/app/Console/Commands/ModuleBuild.php
Normal file
118
freescout-dist/app/Console/Commands/ModuleBuild.php
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Filesystem\Filesystem;
|
||||||
|
|
||||||
|
class ModuleBuild extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:module-build {module_alias?}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Build module or all modules (if module_alias is empty)';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$all = false;
|
||||||
|
$modules = [];
|
||||||
|
|
||||||
|
// Create a symlink for the module (or all modules)
|
||||||
|
$module_alias = $this->argument('module_alias');
|
||||||
|
if (!$module_alias) {
|
||||||
|
$modules = \Module::all();
|
||||||
|
|
||||||
|
$modules_aliases = [];
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
$modules_aliases[] = $module->name;
|
||||||
|
}
|
||||||
|
if (!$modules_aliases) {
|
||||||
|
$this->error('No modules found');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$all = true;
|
||||||
|
// $all = $this->confirm('You have not specified a module alias, would you like to build all available modules ('.implode(', ', $modules_aliases).')?');
|
||||||
|
// if (!$all) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($all) {
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
$this->buildModule($module);
|
||||||
|
$this->call('freescout:module-laroute', ['module_alias' => $module->getAlias()]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$module = \Module::findByAlias($module_alias);
|
||||||
|
if (!$module) {
|
||||||
|
$this->error('Module with the specified alias not found: '.$module_alias);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->buildModule($module);
|
||||||
|
$this->call('freescout:module-laroute');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildModule($module)
|
||||||
|
{
|
||||||
|
$this->line('Module: '.$module->getName());
|
||||||
|
|
||||||
|
$public_symlink = public_path('modules').DIRECTORY_SEPARATOR.$module->alias;
|
||||||
|
if (!file_exists($public_symlink)) {
|
||||||
|
$this->error('Public symlink ['.$public_symlink.'] not found. Run module installation command first: php artisan freescout:module-install');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->buildVars($module);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildVars($module)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$params = [
|
||||||
|
'locales' => config('app.locales'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$filesystem = new Filesystem();
|
||||||
|
|
||||||
|
$file_path = public_path('modules/'.$module->alias.'/js/vars.js');
|
||||||
|
|
||||||
|
$compiled = view($module->alias.'::js/vars', $params)->render();
|
||||||
|
|
||||||
|
if ($compiled) {
|
||||||
|
$filesystem->put($file_path, $compiled);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Created: {$file_path}");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
102
freescout-dist/app/Console/Commands/ModuleCheckLicenses.php
Normal file
102
freescout-dist/app/Console/Commands/ModuleCheckLicenses.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Misc\WpApi;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class ModuleCheckLicenses extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:module-check-licenses';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Check licenses for modules';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
// Get active official modules and check validity of their licenses
|
||||||
|
$modules = \Module::getActive();
|
||||||
|
|
||||||
|
$this->info('Active modules found: '.count($modules));
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'url' => \App\Module::getAppUrl(),
|
||||||
|
'data' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
$license = $module->getLicense();
|
||||||
|
|
||||||
|
if (!$module->isOfficial() || !$license) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['license'] = $license;
|
||||||
|
$data['module_alias'] = $module->getAlias();
|
||||||
|
|
||||||
|
$params['data'][] = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = WpApi::checkLicenses($params);
|
||||||
|
|
||||||
|
if (!empty($result['statuses'])) {
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
$module_alias = $module->getAlias();
|
||||||
|
|
||||||
|
foreach ($result['statuses'] as $result_module_alias => $status) {
|
||||||
|
if ($result_module_alias != $module_alias) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!empty($status) && $status != 'valid') {
|
||||||
|
$msg = 'Module '.$module->getName().' has been deactivated due to invalid license: '.$status;
|
||||||
|
|
||||||
|
$this->error($module->getName().': '.$msg);
|
||||||
|
|
||||||
|
// Deactive module
|
||||||
|
\App\Module::deactiveModule($module->getAlias(), true);
|
||||||
|
|
||||||
|
// Inform admin
|
||||||
|
\Log::error($msg);
|
||||||
|
activity()
|
||||||
|
->withProperties([
|
||||||
|
'error' => $msg,
|
||||||
|
])
|
||||||
|
->useLog(\App\ActivityLog::NAME_SYSTEM)
|
||||||
|
->log(\App\ActivityLog::DESCRIPTION_SYSTEM_ERROR);
|
||||||
|
} else {
|
||||||
|
$this->info($module->getName().': OK');
|
||||||
|
}
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info($module->getName().': Unknown status');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Checking licenses finished');
|
||||||
|
}
|
||||||
|
}
|
140
freescout-dist/app/Console/Commands/ModuleInstall.php
Normal file
140
freescout-dist/app/Console/Commands/ModuleInstall.php
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* php artisan freescout:module-install modulealias.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class ModuleInstall extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:module-install {module_alias?}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Install module or all modules (if module_alias is empty): run migrations and create a symlink';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$install_all = false;
|
||||||
|
$modules = [];
|
||||||
|
|
||||||
|
// We have to clear modules cache first to update modules cache
|
||||||
|
$this->call('cache:clear');
|
||||||
|
|
||||||
|
// Create a symlink for the module (or all modules)
|
||||||
|
$module_alias = $this->argument('module_alias');
|
||||||
|
if (!$module_alias) {
|
||||||
|
$modules = \Module::all();
|
||||||
|
|
||||||
|
$modules_aliases = [];
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
$modules_aliases[] = $module->name;
|
||||||
|
}
|
||||||
|
if (!$modules_aliases) {
|
||||||
|
$this->error('No modules found');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$install_all = $this->confirm('You have not specified a module alias, would you like to install all available modules ('.implode(', ', $modules_aliases).')?');
|
||||||
|
if (!$install_all) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($install_all) {
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
$this->line('Module: '.$module->getName());
|
||||||
|
$this->call('module:migrate', ['module' => $module->getName()]);
|
||||||
|
$this->createModulePublicSymlink($module);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$module = \Module::findByAlias($module_alias);
|
||||||
|
if (!$module) {
|
||||||
|
$this->error('Module with the specified alias not found: '.$module_alias);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->call('module:migrate', ['module' => $module->getName(), '--force' => true]);
|
||||||
|
$this->createModulePublicSymlink($module);
|
||||||
|
}
|
||||||
|
$this->line('Clearing cache...');
|
||||||
|
$this->call('freescout:clear-cache');
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is similar function in \App\Module.
|
||||||
|
public function createModulePublicSymlink($module)
|
||||||
|
{
|
||||||
|
$from = public_path('modules').DIRECTORY_SEPARATOR.$module->alias;
|
||||||
|
$to = $module->getExtraPath('Public');
|
||||||
|
|
||||||
|
// file_exists() may throw "open_basedir restriction in effect".
|
||||||
|
try {
|
||||||
|
// If module's Public is symlink.
|
||||||
|
if (is_link($to)) {
|
||||||
|
@unlink($to);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symlimk may exist but lead to the module folder in a wrong case.
|
||||||
|
// So we need first try to remove it.
|
||||||
|
if (!file_exists($from) || !is_link($from)) {
|
||||||
|
if (is_dir($from)) {
|
||||||
|
@rename($from, $from.'_'.date('YmdHis'));
|
||||||
|
} else {
|
||||||
|
@unlink($from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists($from)) {
|
||||||
|
return $this->info('Public symlink already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check target.
|
||||||
|
if (!file_exists($to)) {
|
||||||
|
// Try to create Public folder.
|
||||||
|
try {
|
||||||
|
\File::makeDirectory($to, \Helper::DIR_PERMISSIONS);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// If it's a broken symlink.
|
||||||
|
if (is_link($to)) {
|
||||||
|
@unlink($to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
symlink($to, $from);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error('Error occurred creating ['.$from.' » '.$to.'] symlink: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error('Error occurred creating ['.$from.' » '.$to.'] symlink: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('The ['.$from.'] symlink has been created');
|
||||||
|
}
|
||||||
|
}
|
171
freescout-dist/app/Console/Commands/ModuleLaroute.php
Normal file
171
freescout-dist/app/Console/Commands/ModuleLaroute.php
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Axn\Laroute\Routes\Collection as Routes;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class ModuleLaroute extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:module-laroute {module_alias?}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Generate a laravel routes JS-file for a module or all modules (if module_alias is empty)';
|
||||||
|
|
||||||
|
public $routes;
|
||||||
|
|
||||||
|
public $config;
|
||||||
|
|
||||||
|
public $generator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$app = app();
|
||||||
|
|
||||||
|
$this->config = $app['config'];
|
||||||
|
$this->generator = $app->make('Lord\Laroute\Generators\GeneratorInterface');
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$all = false;
|
||||||
|
$modules = [];
|
||||||
|
|
||||||
|
// Create a symlink for the module (or all modules)
|
||||||
|
$module_alias = $this->argument('module_alias');
|
||||||
|
if (!$module_alias) {
|
||||||
|
$modules = \Module::all();
|
||||||
|
|
||||||
|
$modules_aliases = [];
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
$modules_aliases[] = $module->name;
|
||||||
|
}
|
||||||
|
if (!$modules_aliases) {
|
||||||
|
$this->error('No modules found');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$all = true;
|
||||||
|
// $all = $this->confirm('You have not specified a module alias, would you like to generate routes for all available modules ('.implode(', ', $modules_aliases).')?');
|
||||||
|
// if (!$all) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($all) {
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
$this->generateModuleRoutes($module);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$module = \Module::findByAlias($module_alias);
|
||||||
|
if (!$module) {
|
||||||
|
$this->error('Module with the specified alias not found: '.$module_alias);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->generateModuleRoutes($module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateModuleRoutes($module)
|
||||||
|
{
|
||||||
|
$this->line('Module: '.$module->getName());
|
||||||
|
|
||||||
|
$public_symlink = public_path('modules').DIRECTORY_SEPARATOR.$module->getAlias();
|
||||||
|
if (!file_exists($public_symlink)) {
|
||||||
|
$this->error('Public symlink ['.$public_symlink.'] not found. Run module installation command first: php artisan freescout:module-install');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->routes = new Routes(app()['router']->getRoutes(), $this->config->get('laroute.filter', 'all'), $this->config->get('laroute.action_namespace', ''), $module->getAlias());
|
||||||
|
|
||||||
|
try {
|
||||||
|
$filePath = $this->generator->compile(
|
||||||
|
$this->getTemplatePath(),
|
||||||
|
$this->getTemplateData(),
|
||||||
|
$this->getFileGenerationPath($module->getAlias())
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->info("Created: {$filePath}");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get path to the template file.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getTemplatePath()
|
||||||
|
{
|
||||||
|
return 'resources/assets/js/laroute_module.js';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data for the template.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getTemplateData()
|
||||||
|
{
|
||||||
|
$namespace = $this->getOptionOrConfig('namespace');
|
||||||
|
$routes = $this->routes->toJSON();
|
||||||
|
$absolute = $this->config->get('laroute.absolute', false);
|
||||||
|
$rootUrl = $this->config->get('app.url', '');
|
||||||
|
$prefix = $this->config->get('laroute.prefix', '');
|
||||||
|
|
||||||
|
return compact('namespace', 'routes', 'absolute', 'rootUrl', 'prefix');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path where the file will be generated.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getFileGenerationPath($module_alias)
|
||||||
|
{
|
||||||
|
$path = 'public/modules/'.$module_alias.'/js';
|
||||||
|
$filename = 'laroute'; //$this->getOptionOrConfig('filename');
|
||||||
|
|
||||||
|
return "{$path}/{$filename}.js";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an option value either from console input, or the config files.
|
||||||
|
*
|
||||||
|
* @param $key
|
||||||
|
*
|
||||||
|
* @return array|mixed|string
|
||||||
|
*/
|
||||||
|
protected function getOptionOrConfig($key)
|
||||||
|
{
|
||||||
|
// if ($option = $this->option($key)) {
|
||||||
|
// return $option;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return $this->config->get("laroute.{$key}");
|
||||||
|
}
|
||||||
|
}
|
105
freescout-dist/app/Console/Commands/ModuleUpdate.php
Normal file
105
freescout-dist/app/Console/Commands/ModuleUpdate.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* php artisan freescout:module-install modulealias.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class ModuleUpdate extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:module-update {module_alias?}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Update all modules or a single module (if module_alias is set)';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$install_all = false;
|
||||||
|
$modules = [];
|
||||||
|
|
||||||
|
// We have to clear modules cache first to update modules cache
|
||||||
|
\Artisan::call('cache:clear');
|
||||||
|
|
||||||
|
// Create a symlink for the module (or all modules)
|
||||||
|
$module_alias = $this->argument('module_alias');
|
||||||
|
|
||||||
|
$modules_directory = \WpApi::getModules();
|
||||||
|
if (\WpApi::$lastError) {
|
||||||
|
$this->error(__('Error occurred').': '.$lastError['message'].' ('.$lastError['code'].')');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$installed_modules = \Module::all();
|
||||||
|
|
||||||
|
$counter = 0;
|
||||||
|
$found = false;
|
||||||
|
foreach ($modules_directory as $dir_module) {
|
||||||
|
// Update single module.
|
||||||
|
if ($module_alias && $dir_module['alias'] != $module_alias) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$found = true;
|
||||||
|
|
||||||
|
// Detect if new version is available.
|
||||||
|
foreach ($installed_modules as $module) {
|
||||||
|
if ($module->getAlias() != $dir_module['alias'] || !$module->active()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!empty($dir_module['version']) && version_compare($dir_module['version'], $module->get('version'), '>')) {
|
||||||
|
$update_result = \App\Module::updateModule($dir_module['alias']);
|
||||||
|
|
||||||
|
$this->info('['.$update_result['module_name'].' Module'.']');
|
||||||
|
if ($update_result['status'] == 'success') {
|
||||||
|
$this->line($update_result['msg_success']);
|
||||||
|
} else {
|
||||||
|
$msg = $update_result['msg'];
|
||||||
|
if ($update_result['download_msg']) {
|
||||||
|
$msg .= ' ('.$update_result['download_msg'].')';
|
||||||
|
}
|
||||||
|
$this->error('ERROR: '.$msg);
|
||||||
|
}
|
||||||
|
if (trim($update_result['output'])) {
|
||||||
|
$this->line(preg_replace("#\n#", "\n> ", '> '.trim($update_result['output'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
$counter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($module_alias && !$found) {
|
||||||
|
$this->error('Module with the following alias not found: '.$module_alias);
|
||||||
|
} elseif (!$counter) {
|
||||||
|
$this->line('All modules are up-to-date');
|
||||||
|
}
|
||||||
|
|
||||||
|
\Artisan::call('freescout:clear-cache');
|
||||||
|
}
|
||||||
|
}
|
66
freescout-dist/app/Console/Commands/SendMonitor.php
Normal file
66
freescout-dist/app/Console/Commands/SendMonitor.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Job;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SendMonitor extends Command
|
||||||
|
{
|
||||||
|
const CHECK_PERIOD = 12 * 3600; // 12 hours
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:send-monitor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Check if queue:work is processing emails queue and show an alert in the web interface if needed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
// Get SendReplyToCustomer jobs.
|
||||||
|
$pending_jobs = \App\Job::where('queue', 'emails')
|
||||||
|
->where('payload', 'like', '{"displayName":"App\\\\\\\\Jobs\\\\\\\\SendReplyToCustomer"%')
|
||||||
|
->where('available_at', '<', time() - self::CHECK_PERIOD)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
// Check failed_jobs.
|
||||||
|
// No need - it can be done via Manage > Alerts > Logs Monitoring
|
||||||
|
// if (!$pending_jobs) {
|
||||||
|
// $pending_jobs = \App\FailedJob::where('queue', 'emails')
|
||||||
|
// ->where('payload', 'like', '{"displayName":"App\\\\\\\\Jobs\\\\\\\\SendReplyToCustomer"%')
|
||||||
|
// ->where('created_at', '<', time() - self::CHECK_PERIOD)
|
||||||
|
// ->exists();
|
||||||
|
// }
|
||||||
|
|
||||||
|
if ($pending_jobs) {
|
||||||
|
\Option::set('send_emails_problem', '1');
|
||||||
|
$this->error('['.date('Y-m-d H:i:s').'] There are problems with emails queue processing');
|
||||||
|
} else {
|
||||||
|
\Option::remove('send_emails_problem');
|
||||||
|
$this->info('['.date('Y-m-d H:i:s').'] Emails queue processing is working');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
freescout-dist/app/Console/Commands/Update.php
Normal file
64
freescout-dist/app/Console/Commands/Update.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Console\ConfirmableTrait;
|
||||||
|
|
||||||
|
class Update extends Command
|
||||||
|
{
|
||||||
|
use ConfirmableTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:update {--force : Force the operation to run when in production.}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Update application to the latest version from GitHub';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
if (!$this->confirmToProceed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ini_set('memory_limit', '128M');
|
||||||
|
|
||||||
|
if (\Updater::isNewVersionAvailable(config('app.version'))) {
|
||||||
|
$this->info('Updating... This may take several minutes');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Script may fail here and stop with the error:
|
||||||
|
// PHP Fatal error: Allowed memory size of 94371840 bytes exhausted
|
||||||
|
\Updater::update();
|
||||||
|
$this->call('freescout:after-app-update');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error('Error occurred: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info('You have the latest version installed: '.config('app.version'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
freescout-dist/app/Console/Commands/UpdateFolderCounters.php
Normal file
46
freescout-dist/app/Console/Commands/UpdateFolderCounters.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class UpdateFolderCounters extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'freescout:update-folder-counters';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Update counters for all folders';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
foreach (\App\Folder::get() as $folder) {
|
||||||
|
$folder->updateCounters();
|
||||||
|
$this->line('Updated counters for folder: '.$folder->id);
|
||||||
|
}
|
||||||
|
$this->info('Updating finished');
|
||||||
|
}
|
||||||
|
}
|
273
freescout-dist/app/Console/Kernel.php
Normal file
273
freescout-dist/app/Console/Kernel.php
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
|
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||||
|
use App\Misc\Mail;
|
||||||
|
use App\Option;
|
||||||
|
|
||||||
|
class Kernel extends ConsoleKernel
|
||||||
|
{
|
||||||
|
const FETCH_MAX_EXECUTION_TIME = 30; // minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Artisan commands provided by your application.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $commands = [
|
||||||
|
// It is not clear what for this array
|
||||||
|
//\App\Console\Commands\CreateUser::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the application's command schedule.
|
||||||
|
* If --no-interaction flag is set the script will not run 'queue:work' daemon.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function schedule(Schedule $schedule)
|
||||||
|
{
|
||||||
|
// Keep in mind that this function is also called on clearing cache.
|
||||||
|
|
||||||
|
// Remove failed jobs
|
||||||
|
$schedule->command('queue:flush')
|
||||||
|
->weekly();
|
||||||
|
|
||||||
|
// Restart processing queued jobs (just in case)
|
||||||
|
$schedule->command('queue:restart')
|
||||||
|
->hourly();
|
||||||
|
|
||||||
|
$schedule->command('freescout:fetch-monitor')
|
||||||
|
->everyMinute()
|
||||||
|
->withoutOverlapping();
|
||||||
|
|
||||||
|
$schedule->command('freescout:send-monitor')
|
||||||
|
// Every 10 minutes.
|
||||||
|
->cron('*/10 * * * *')
|
||||||
|
->withoutOverlapping();
|
||||||
|
|
||||||
|
$schedule->command('freescout:update-folder-counters')
|
||||||
|
->hourly();
|
||||||
|
|
||||||
|
$app_key = config('app.key');
|
||||||
|
if ($app_key) {
|
||||||
|
$crc = crc32($app_key);
|
||||||
|
$schedule->command('freescout:module-check-licenses')
|
||||||
|
->cron((int)($crc % 59).' '.(int)($crc % 23).' * * *');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user finished viewing conversation.
|
||||||
|
$schedule->command('freescout:check-conv-viewers')
|
||||||
|
->everyMinute()
|
||||||
|
->withoutOverlapping();
|
||||||
|
|
||||||
|
$schedule->command('freescout:clean-send-log')
|
||||||
|
->monthly();
|
||||||
|
|
||||||
|
$schedule->command('freescout:clean-notifications-table')
|
||||||
|
->weekly();
|
||||||
|
|
||||||
|
$schedule->command('freescout:clean-tmp')
|
||||||
|
->daily();
|
||||||
|
|
||||||
|
// Logs monitoring.
|
||||||
|
$alert_logs_period = config('app.alert_logs_period');
|
||||||
|
if (config('app.alert_logs') && $alert_logs_period) {
|
||||||
|
$logs_cron = '';
|
||||||
|
switch ($alert_logs_period) {
|
||||||
|
case 'hour':
|
||||||
|
$logs_cron = '0 * * * *';
|
||||||
|
break;
|
||||||
|
case 'day':
|
||||||
|
$logs_cron = '0 0 * * *';
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
$logs_cron = '0 0 * * 0';
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
$logs_cron = '0 0 1 * *';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($logs_cron) {
|
||||||
|
$schedule->command('freescout:logs-monitor')
|
||||||
|
->cron($logs_cron)
|
||||||
|
->withoutOverlapping();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fetch_command_identifier = \Helper::getWorkerIdentifier('freescout:fetch-emails');
|
||||||
|
$fetch_command_name = 'freescout:fetch-emails --identifier='.$fetch_command_identifier;
|
||||||
|
|
||||||
|
// Kill fetch commands running for too long.
|
||||||
|
// In shedule:run this code is executed every time $schedule->command() in this function is executed.
|
||||||
|
if ($this->isScheduleRun() && function_exists('shell_exec')) {
|
||||||
|
$fetch_command_pids = \Helper::getRunningProcesses($fetch_command_identifier);
|
||||||
|
|
||||||
|
// The name of the command here must be exactly the same as below!
|
||||||
|
// Otherwise long fetching will be killed and won't run longer than 1 mintue.
|
||||||
|
$mutex_name = $schedule->command($fetch_command_name)
|
||||||
|
->skip(function () {
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
->mutexName();
|
||||||
|
|
||||||
|
// If there is no cache mutext but there are running fetch commands
|
||||||
|
// it means the mutex had expired after self::FETCH_MAX_EXECUTION_TIME
|
||||||
|
// and the existing command(s) is running longer than self::FETCH_MAX_EXECUTION_TIME.
|
||||||
|
if (count($fetch_command_pids) > 0 && !\Cache::get($mutex_name)) {
|
||||||
|
// Kill freescout:fetch-emails commands running for too long
|
||||||
|
shell_exec('kill '.implode(' | kill ', $fetch_command_pids));
|
||||||
|
} elseif (count($fetch_command_pids) == 0) {
|
||||||
|
// Make sure 'ps' command actually works.
|
||||||
|
$ps_works = \Helper::getRunningProcesses('schedule:run');
|
||||||
|
|
||||||
|
if (count($ps_works)) {
|
||||||
|
// Previous freescout:fetch-emails may have been killed or errored and did not remove the mutex.
|
||||||
|
// So here we are forcefully removing the mutex. Otherwise mutex will live for 24 hours.
|
||||||
|
if (\Cache::has($mutex_name)) {
|
||||||
|
\Cache::forget($mutex_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch emails from mailboxes
|
||||||
|
$fetch_command = $schedule->command($fetch_command_name)
|
||||||
|
// withoutOverlapping() option creates a mutex in the cache
|
||||||
|
// which by default expires in 24 hours.
|
||||||
|
// So we are passing an 'expiresAt' parameter to withoutOverlapping() to
|
||||||
|
// prevent fetching from not being executed when fetching command by some reason
|
||||||
|
// does not remove the mutex from the cache.
|
||||||
|
->withoutOverlapping($expiresAt = self::FETCH_MAX_EXECUTION_TIME /* minutes */)
|
||||||
|
->sendOutputTo(storage_path().'/logs/fetch-emails.log');
|
||||||
|
|
||||||
|
switch (config('app.fetch_schedule')) {
|
||||||
|
case Mail::FETCH_SCHEDULE_EVERY_FIVE_MINUTES:
|
||||||
|
$fetch_command->everyFiveMinutes();
|
||||||
|
break;
|
||||||
|
case Mail::FETCH_SCHEDULE_EVERY_TEN_MINUTES:
|
||||||
|
$fetch_command->everyTenMinutes();
|
||||||
|
break;
|
||||||
|
case Mail::FETCH_SCHEDULE_EVERY_FIFTEEN_MINUTES:
|
||||||
|
$fetch_command->everyFifteenMinutes();
|
||||||
|
break;
|
||||||
|
case Mail::FETCH_SCHEDULE_EVERY_THIRTY_MINUTES:
|
||||||
|
$fetch_command->everyThirtyMinutes();
|
||||||
|
break;
|
||||||
|
case Mail::FETCH_SCHEDULE_HOURLY:
|
||||||
|
$fetch_command->Hourly();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$fetch_command->everyMinute();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schedule = \Eventy::filter('schedule', $schedule);
|
||||||
|
|
||||||
|
// If --no-daemonize flag is passed - do not run 'queue:work' daemon.
|
||||||
|
foreach ($_SERVER['argv'] ?? [] as $arg) {
|
||||||
|
if ($arg == '--no-interaction') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command runs as subprocess and sets cache mutex. If schedule:run command is killed
|
||||||
|
// subprocess does not clear the mutex and it stays in the cache until cache:clear is executed.
|
||||||
|
// By default, the lock will expire after 24 hours.
|
||||||
|
|
||||||
|
$queue_work_params = Config('app.queue_work_params');
|
||||||
|
// Add identifier to avoid conflicts with other FreeScout instances on the same server.
|
||||||
|
$queue_work_params['--queue'] .= ','.\Helper::getWorkerIdentifier();
|
||||||
|
|
||||||
|
// $schedule->command('queue:work') command below has withoutOverlapping() option,
|
||||||
|
// which works via special mutex stored in the cache preventing several 'queue:work' to work at the same time.
|
||||||
|
// So when the cache is cleared the mutex indicating that the 'queue:work' is running is removed,
|
||||||
|
// and the second 'queue:work' command is launched by cron. When `artisan schedule:run` is executed it sees
|
||||||
|
// that there are two 'queue:work' processes running and kills them.
|
||||||
|
// After one minute 'queue:work' is executed by cron via `artisan schedule:run` and works in the background.
|
||||||
|
if ($this->isScheduleRun() && function_exists('shell_exec')) {
|
||||||
|
$running_commands = \Helper::getRunningProcesses();
|
||||||
|
|
||||||
|
if (count($running_commands) > 1) {
|
||||||
|
// Stop all queue:work processes.
|
||||||
|
// queue:work command is stopped by settings a cache key
|
||||||
|
\Helper::queueWorkerRestart();
|
||||||
|
// Sometimes processes stuck and just continue running, so we need to kill them.
|
||||||
|
// Sleep to let processes stop.
|
||||||
|
sleep(1);
|
||||||
|
// Check processes again.
|
||||||
|
$worker_pids = \Helper::getRunningProcesses();
|
||||||
|
|
||||||
|
if (count($worker_pids) > 1) {
|
||||||
|
// Current process also has to be killed, as otherwise it "stucks"
|
||||||
|
// $current_pid = getmypid();
|
||||||
|
// foreach ($worker_pids as $i => $pid) {
|
||||||
|
// if ($pid == $current_pid) {
|
||||||
|
// unset($worker_pids[$i]);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
shell_exec('kill '.implode(' | kill ', $worker_pids));
|
||||||
|
}
|
||||||
|
} elseif (count($running_commands) == 0) {
|
||||||
|
// Make sure 'ps' command actually works.
|
||||||
|
$ps_works = \Helper::getRunningProcesses('schedule:run');
|
||||||
|
|
||||||
|
if (count($ps_works)) {
|
||||||
|
// Previous queue:work may have been killed or errored and did not remove the mutex.
|
||||||
|
// So here we are forcefully removing the mutex.
|
||||||
|
$mutex_name = $schedule->command('queue:work', $queue_work_params)
|
||||||
|
->skip(function () {
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
->mutexName();
|
||||||
|
if (\Cache::get($mutex_name)) {
|
||||||
|
\Cache::forget($mutex_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$schedule->command('queue:work', $queue_work_params)
|
||||||
|
->everyMinute()
|
||||||
|
->withoutOverlapping()
|
||||||
|
->sendOutputTo(storage_path().'/logs/queue-jobs.log');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is needed because every time $schedule->command() is executed
|
||||||
|
* the schedule() is executed also.
|
||||||
|
*/
|
||||||
|
public function isScheduleRun()
|
||||||
|
{
|
||||||
|
if (!\Helper::isConsole()) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return !empty($_SERVER['argv']) && in_array('schedule:run', $_SERVER['argv']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the commands for the application.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function commands()
|
||||||
|
{
|
||||||
|
$this->load(__DIR__.'/Commands');
|
||||||
|
|
||||||
|
// Swiftmailer uses $_SERVER['SERVER_NAME'] in transport_deps.php
|
||||||
|
// to set the host for EHLO command, if it is empty it uses [127.0.0.1].
|
||||||
|
// G Suite sometimes rejects emails with EHLO [127.0.0.1].
|
||||||
|
if (empty($_SERVER['SERVER_NAME'])) {
|
||||||
|
$_SERVER['SERVER_NAME'] = parse_url(config('app.url'), PHP_URL_HOST);
|
||||||
|
}
|
||||||
|
|
||||||
|
require base_path('routes/console.php');
|
||||||
|
}
|
||||||
|
}
|
2424
freescout-dist/app/Conversation.php
Normal file
2424
freescout-dist/app/Conversation.php
Normal file
File diff suppressed because it is too large
Load Diff
17
freescout-dist/app/ConversationFolder.php
Normal file
17
freescout-dist/app/ConversationFolder.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ConversationFolder extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The table associated with the model.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $table = 'conversation_folder';
|
||||||
|
|
||||||
|
protected $fillable = ['folder_id', 'conversation_id'];
|
||||||
|
}
|
1533
freescout-dist/app/Customer.php
Normal file
1533
freescout-dist/app/Customer.php
Normal file
File diff suppressed because it is too large
Load Diff
58
freescout-dist/app/CustomerChannel.php
Normal file
58
freescout-dist/app/CustomerChannel.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class CustomerChannel extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The table associated with the model.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $table = 'customer_channel';
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached communication channels.
|
||||||
|
*/
|
||||||
|
public static $channels = null;
|
||||||
|
|
||||||
|
public function customer()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Customer');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create($customer_id, $channel, $channel_id)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$customer_channel = new self();
|
||||||
|
$customer_channel->customer_id = $customer_id;
|
||||||
|
$customer_channel->channel = $channel;
|
||||||
|
$customer_channel->channel_id = $channel_id;
|
||||||
|
$customer_channel->save();
|
||||||
|
|
||||||
|
return $customer_channel;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Already exists.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChannelName()
|
||||||
|
{
|
||||||
|
return \Eventy::filter('channel.name', '', $this->channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getChannels()
|
||||||
|
{
|
||||||
|
if (self::$channels !== null) {
|
||||||
|
return self::$channels;
|
||||||
|
} else {
|
||||||
|
self::$channels = \Eventy::filter('channels.list', []);
|
||||||
|
return self::$channels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
87
freescout-dist/app/Email.php
Normal file
87
freescout-dist/app/Email.php
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Watson\Rememberable\Rememberable;
|
||||||
|
|
||||||
|
class Email extends Model
|
||||||
|
{
|
||||||
|
use Rememberable;
|
||||||
|
// This is obligatory.
|
||||||
|
public $rememberCacheDriver = 'array';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email types.
|
||||||
|
*/
|
||||||
|
const TYPE_WORK = 1;
|
||||||
|
const TYPE_HOME = 2;
|
||||||
|
const TYPE_OTHER = 3;
|
||||||
|
|
||||||
|
public static $types = [
|
||||||
|
self::TYPE_WORK => 'work',
|
||||||
|
self::TYPE_HOME => 'home',
|
||||||
|
self::TYPE_OTHER => 'other',
|
||||||
|
];
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attributes which are not fillable using fill() method.
|
||||||
|
*/
|
||||||
|
protected $guarded = ['id', 'customer_id'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get email's customer.
|
||||||
|
*/
|
||||||
|
public function customer()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Customer');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize email address.
|
||||||
|
*
|
||||||
|
* @param string $email
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function sanitizeEmail($email)
|
||||||
|
{
|
||||||
|
// FILTER_VALIDATE_EMAIL does not work with long emails for example
|
||||||
|
// Email validation is not recommended:
|
||||||
|
// http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address/201378#201378
|
||||||
|
// So we just check for @
|
||||||
|
if (!preg_match('/^.+@.+$/', $email ?? '')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$email = filter_var($email, FILTER_SANITIZE_EMAIL);
|
||||||
|
$email = mb_strtolower($email, 'UTF-8');
|
||||||
|
// Remove trailing dots.
|
||||||
|
$email = preg_replace("/\.+$/", '', $email);
|
||||||
|
// Remove dot before @
|
||||||
|
$email = preg_replace("/\.+@/", '@', $email);
|
||||||
|
|
||||||
|
return $email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNameFromEmail()
|
||||||
|
{
|
||||||
|
return explode('@', $this->email)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create($email, $customer_id, $type = self::TYPE_WORK)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$email_obj = new Email();
|
||||||
|
$email_obj->email = $email;
|
||||||
|
$email_obj->type = array_key_exists($type, self::$types) ? $type : self::TYPE_WORK;
|
||||||
|
$email_obj->customer_id = $customer_id;
|
||||||
|
$email_obj->save();
|
||||||
|
|
||||||
|
return $email_obj;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
freescout-dist/app/Events/ConversationCustomerChanged.php
Normal file
28
freescout-dist/app/Events/ConversationCustomerChanged.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
|
||||||
|
class ConversationCustomerChanged
|
||||||
|
{
|
||||||
|
public $conversation;
|
||||||
|
public $prev_customer_id;
|
||||||
|
public $prev_customer_email;
|
||||||
|
public $by_user;
|
||||||
|
public $by_customer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Conversation $conversation, $prev_customer_id, $prev_customer_email, $by_user, $by_customer)
|
||||||
|
{
|
||||||
|
$this->conversation = $conversation;
|
||||||
|
$this->prev_customer_id = $prev_customer_id;
|
||||||
|
$this->prev_customer_email = $prev_customer_email;
|
||||||
|
$this->by_user = $by_user;
|
||||||
|
$this->by_customer = $by_customer;
|
||||||
|
}
|
||||||
|
}
|
20
freescout-dist/app/Events/ConversationStatusChanged.php
Normal file
20
freescout-dist/app/Events/ConversationStatusChanged.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
|
||||||
|
class ConversationStatusChanged
|
||||||
|
{
|
||||||
|
public $conversation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Conversation $conversation)
|
||||||
|
{
|
||||||
|
$this->conversation = $conversation;
|
||||||
|
}
|
||||||
|
}
|
23
freescout-dist/app/Events/ConversationUserChanged.php
Normal file
23
freescout-dist/app/Events/ConversationUserChanged.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\User;
|
||||||
|
|
||||||
|
class ConversationUserChanged
|
||||||
|
{
|
||||||
|
public $conversation;
|
||||||
|
public $user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Conversation $conversation, User $user)
|
||||||
|
{
|
||||||
|
$this->conversation = $conversation;
|
||||||
|
$this->user = $user;
|
||||||
|
}
|
||||||
|
}
|
23
freescout-dist/app/Events/CustomerCreatedConversation.php
Normal file
23
freescout-dist/app/Events/CustomerCreatedConversation.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Thread;
|
||||||
|
|
||||||
|
class CustomerCreatedConversation
|
||||||
|
{
|
||||||
|
public $conversation;
|
||||||
|
public $last_thread;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Conversation $conversation, Thread $last_thread)
|
||||||
|
{
|
||||||
|
$this->conversation = $conversation;
|
||||||
|
$this->last_thread = $last_thread;
|
||||||
|
}
|
||||||
|
}
|
23
freescout-dist/app/Events/CustomerReplied.php
Normal file
23
freescout-dist/app/Events/CustomerReplied.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Thread;
|
||||||
|
|
||||||
|
class CustomerReplied
|
||||||
|
{
|
||||||
|
public $conversation;
|
||||||
|
public $thread;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Conversation $conversation, Thread $thread)
|
||||||
|
{
|
||||||
|
$this->conversation = $conversation;
|
||||||
|
$this->thread = $thread;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\Channel;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class RealtimeBroadcastNotificationCreated implements ShouldBroadcastNow
|
||||||
|
{
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The notifiable entity who received the notification.
|
||||||
|
*
|
||||||
|
* @var mixed
|
||||||
|
*/
|
||||||
|
public $notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The notification instance.
|
||||||
|
*
|
||||||
|
* @var \Illuminate\Notifications\Notification
|
||||||
|
*/
|
||||||
|
public $notification;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The notification data.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $data = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($notifiable, $notification, $data)
|
||||||
|
{
|
||||||
|
$this->data = $data;
|
||||||
|
$this->notifiable = $notifiable;
|
||||||
|
$this->notification = $notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the channels the event should broadcast on.
|
||||||
|
*
|
||||||
|
* @return Channel|array
|
||||||
|
*/
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
$channels = $this->notification->broadcastOn();
|
||||||
|
|
||||||
|
if (!empty($channels)) {
|
||||||
|
return $channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [new PrivateChannel($this->channelName())];
|
||||||
|
|
||||||
|
//return new PrivateChannel('App.User.'.$this->receiver_user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data to broadcast.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function broadcastWith()
|
||||||
|
{
|
||||||
|
return array_merge($this->data, [
|
||||||
|
'id' => $this->notification->id,
|
||||||
|
'type' => get_class($this->notification),
|
||||||
|
]);
|
||||||
|
// return [
|
||||||
|
// 'thread_id' => $this->thread->id
|
||||||
|
// ];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the broadcast channel name for the event.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function channelName()
|
||||||
|
{
|
||||||
|
if (method_exists($this->notifiable, 'receivesBroadcastNotificationsOn')) {
|
||||||
|
return $this->notifiable->receivesBroadcastNotificationsOn($this->notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
$class = str_replace('\\', '.', get_class($this->notifiable));
|
||||||
|
|
||||||
|
return $class.'.'.$this->notifiable->getKey();
|
||||||
|
}
|
||||||
|
}
|
102
freescout-dist/app/Events/RealtimeChat.php
Normal file
102
freescout-dist/app/Events/RealtimeChat.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Fefresh chats list when new thread created in mailbox.
|
||||||
|
*/
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Mailbox;
|
||||||
|
use Illuminate\Broadcasting\Channel;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class RealtimeChat implements ShouldBroadcastNow
|
||||||
|
{
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The notification data.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $data = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($data)
|
||||||
|
{
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the channels the event should broadcast on.
|
||||||
|
*
|
||||||
|
* @return Channel|array
|
||||||
|
*/
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
return new \Illuminate\Broadcasting\Channel($this->channelName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data to broadcast.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function broadcastWith()
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the broadcast channel name for the event.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function channelName()
|
||||||
|
{
|
||||||
|
if (!empty($this->data['mailbox_id'])) {
|
||||||
|
return 'chat.'.$this->data['mailbox_id'];
|
||||||
|
} else {
|
||||||
|
return 'chat.0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper funciton.
|
||||||
|
*/
|
||||||
|
public static function dispatchSelf($mailbox_id)
|
||||||
|
{
|
||||||
|
if (!\Helper::isChatModeAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$notification_data = [
|
||||||
|
'mailbox_id' => $mailbox_id
|
||||||
|
];
|
||||||
|
event(new \App\Events\RealtimeChat($notification_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function processPayload($payload)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$mailbox = Mailbox::rememberForever()->find($payload->mailbox_id);
|
||||||
|
|
||||||
|
// Check if user can listen to this event.
|
||||||
|
if (!$user || !$mailbox || !$user->can('viewCached', $mailbox)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chats are retrieved in the template.
|
||||||
|
$template_data = [
|
||||||
|
'mailbox' => $mailbox,
|
||||||
|
];
|
||||||
|
|
||||||
|
$payload->chats_html = \View::make('mailboxes/partials/chat_list')->with($template_data)->render();
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
}
|
118
freescout-dist/app/Events/RealtimeConvNewThread.php
Normal file
118
freescout-dist/app/Events/RealtimeConvNewThread.php
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* New thread created in conversation.
|
||||||
|
*/
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Mailbox;
|
||||||
|
use App\Thread;
|
||||||
|
use Illuminate\Broadcasting\Channel;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class RealtimeConvNewThread implements ShouldBroadcastNow
|
||||||
|
{
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The notification data.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $data = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($data)
|
||||||
|
{
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the channels the event should broadcast on.
|
||||||
|
*
|
||||||
|
* @return Channel|array
|
||||||
|
*/
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
return new \Illuminate\Broadcasting\Channel($this->channelName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data to broadcast.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function broadcastWith()
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the broadcast channel name for the event.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function channelName()
|
||||||
|
{
|
||||||
|
if (!empty($this->data['conversation_id'])) {
|
||||||
|
return 'conv.'.$this->data['conversation_id'];
|
||||||
|
} else {
|
||||||
|
return 'conv.0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper funciton.
|
||||||
|
*/
|
||||||
|
public static function dispatchSelf($thread)
|
||||||
|
{
|
||||||
|
if ($thread->state != Thread::STATE_PUBLISHED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$notification_data = [
|
||||||
|
'thread_id' => $thread->id,
|
||||||
|
'conversation_id' => $thread->conversation_id,
|
||||||
|
// conversation is prefetched in ThreadObserver.
|
||||||
|
'mailbox_id' => $thread->conversation->mailbox_id,
|
||||||
|
//'user_id' => $thread->created_by_user_id,
|
||||||
|
];
|
||||||
|
event(new \App\Events\RealtimeConvNewThread($notification_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function processPayload($payload)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$mailbox = Mailbox::rememberForever()->find($payload->mailbox_id);
|
||||||
|
|
||||||
|
// Check if user can listen to this event.
|
||||||
|
if (!$user || !$mailbox || !$user->can('viewCached', $mailbox)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$thread = Thread::find($payload->thread_id);
|
||||||
|
if (!$thread) {
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add thread html to the payload.
|
||||||
|
$template_data = [
|
||||||
|
'conversation' => $thread->conversation,
|
||||||
|
'mailbox' => $thread->conversation->mailbox,
|
||||||
|
'threads' => [$thread],
|
||||||
|
];
|
||||||
|
|
||||||
|
$payload->thread_html = \View::make('conversations/partials/threads')->with($template_data)->render();
|
||||||
|
$payload->conversation_user_id = $thread->conversation->user_id;
|
||||||
|
$payload->conversation_status = $thread->conversation->status;
|
||||||
|
$payload->conversation_status_class = Conversation::$status_classes[$thread->conversation->status];
|
||||||
|
$payload->conversation_status_icon = Conversation::$status_icons[$thread->conversation->status];
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
}
|
113
freescout-dist/app/Events/RealtimeConvView.php
Normal file
113
freescout-dist/app/Events/RealtimeConvView.php
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\Channel;
|
||||||
|
//use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class RealtimeConvView implements ShouldBroadcastNow
|
||||||
|
{
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The notifiable entity who received the notification.
|
||||||
|
*
|
||||||
|
* @var mixed
|
||||||
|
*/
|
||||||
|
//public $notifiable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The notification instance.
|
||||||
|
*
|
||||||
|
* @var \Illuminate\Notifications\Notification
|
||||||
|
*/
|
||||||
|
//public $notification;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The notification data.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $data = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(/*$notifiable, $notification,*/ $data)
|
||||||
|
{
|
||||||
|
$this->data = $data;
|
||||||
|
// $this->notifiable = $notifiable;
|
||||||
|
// $this->notification = $notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the channels the event should broadcast on.
|
||||||
|
*
|
||||||
|
* @return Channel|array
|
||||||
|
*/
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
// $channels = $this->notification->broadcastOn();
|
||||||
|
|
||||||
|
// if (!empty($channels)) {
|
||||||
|
// return $channels;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return new \Illuminate\Broadcasting\Channel($this->channelName());
|
||||||
|
//return [new PrivateChannel()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data to broadcast.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function broadcastWith()
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
|
||||||
|
/*return array_merge($this->data, [
|
||||||
|
'id' => $this->notification->id,
|
||||||
|
'type' => get_class($this->notification),
|
||||||
|
]);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the broadcast channel name for the event.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function channelName()
|
||||||
|
{
|
||||||
|
// if (method_exists($this->notifiable, 'receivesBroadcastNotificationsOn')) {
|
||||||
|
// return $this->notifiable->receivesBroadcastNotificationsOn($this->notification);
|
||||||
|
// }
|
||||||
|
|
||||||
|
return 'conv';
|
||||||
|
|
||||||
|
// $class = str_replace('\\', '.', get_class($this->notifiable));
|
||||||
|
|
||||||
|
// return $class.'.'.$this->notifiable->getKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper funciton.
|
||||||
|
*/
|
||||||
|
public static function dispatchSelf($conversation_id, $user, $replying = false)
|
||||||
|
{
|
||||||
|
$notification_data = [
|
||||||
|
'conversation_id' => $conversation_id,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'user_photo_url' => $user->getPhotoUrl(false),
|
||||||
|
// These has to be encoded to avoid "Unable to JSON encode payload. Error code: 5"
|
||||||
|
'user_initials' => htmlentities($user->getInitials()),
|
||||||
|
'user_name' => htmlentities($user->getFullName()),
|
||||||
|
'replying' => (int)$replying,
|
||||||
|
];
|
||||||
|
event(new \App\Events\RealtimeConvView($notification_data));
|
||||||
|
}
|
||||||
|
}
|
62
freescout-dist/app/Events/RealtimeConvViewFinish.php
Normal file
62
freescout-dist/app/Events/RealtimeConvViewFinish.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* User finished viewing conversatin.
|
||||||
|
*/
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\Channel;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class RealtimeConvViewFinish implements ShouldBroadcastNow
|
||||||
|
{
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The notification data.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $data = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($data)
|
||||||
|
{
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the channels the event should broadcast on.
|
||||||
|
*
|
||||||
|
* @return Channel|array
|
||||||
|
*/
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
return new \Illuminate\Broadcasting\Channel($this->channelName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data to broadcast.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function broadcastWith()
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the broadcast channel name for the event.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function channelName()
|
||||||
|
{
|
||||||
|
return 'conv';
|
||||||
|
}
|
||||||
|
}
|
110
freescout-dist/app/Events/RealtimeMailboxNewThread.php
Normal file
110
freescout-dist/app/Events/RealtimeMailboxNewThread.php
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* New thread created in mailbox.
|
||||||
|
*/
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Mailbox;
|
||||||
|
use App\Folder;
|
||||||
|
use Illuminate\Broadcasting\Channel;
|
||||||
|
use Illuminate\Broadcasting\PrivateChannel;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class RealtimeMailboxNewThread implements ShouldBroadcastNow
|
||||||
|
{
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The notification data.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
public $data = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($data)
|
||||||
|
{
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the channels the event should broadcast on.
|
||||||
|
*
|
||||||
|
* @return Channel|array
|
||||||
|
*/
|
||||||
|
public function broadcastOn()
|
||||||
|
{
|
||||||
|
return new \Illuminate\Broadcasting\Channel($this->channelName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data to broadcast.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function broadcastWith()
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the broadcast channel name for the event.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function channelName()
|
||||||
|
{
|
||||||
|
if (!empty($this->data['mailbox_id'])) {
|
||||||
|
return 'mailbox.'.$this->data['mailbox_id'];
|
||||||
|
} else {
|
||||||
|
return 'mailbox.0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper funciton.
|
||||||
|
*/
|
||||||
|
public static function dispatchSelf($mailbox_id)
|
||||||
|
{
|
||||||
|
$notification_data = [
|
||||||
|
'mailbox_id' => $mailbox_id
|
||||||
|
];
|
||||||
|
event(new \App\Events\RealtimeMailboxNewThread($notification_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function processPayload($payload)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$mailbox = Mailbox::rememberForever()->find($payload->mailbox_id);
|
||||||
|
|
||||||
|
// Check if user can listen to this event.
|
||||||
|
if (!$user || !$mailbox || !$user->can('viewCached', $mailbox)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$folder = null;
|
||||||
|
$foler_id = Conversation::getFolderParam();
|
||||||
|
if ($foler_id) {
|
||||||
|
$folder = Folder::find($foler_id);
|
||||||
|
}
|
||||||
|
// Just in case.
|
||||||
|
if (!$folder) {
|
||||||
|
$folder = new Folder();
|
||||||
|
}
|
||||||
|
$template_data = [
|
||||||
|
'folders' => $mailbox->getAssesibleFolders(),
|
||||||
|
'folder' => $folder,
|
||||||
|
'mailbox' => $mailbox,
|
||||||
|
];
|
||||||
|
|
||||||
|
$payload->folders_html = \View::make('mailboxes/partials/folders')->with($template_data)->render();
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
}
|
23
freescout-dist/app/Events/UserAddedNote.php
Normal file
23
freescout-dist/app/Events/UserAddedNote.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Thread;
|
||||||
|
|
||||||
|
class UserAddedNote
|
||||||
|
{
|
||||||
|
public $conversation;
|
||||||
|
public $thread;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Conversation $conversation, Thread $thread)
|
||||||
|
{
|
||||||
|
$this->conversation = $conversation;
|
||||||
|
$this->thread = $thread;
|
||||||
|
}
|
||||||
|
}
|
23
freescout-dist/app/Events/UserCreatedConversation.php
Normal file
23
freescout-dist/app/Events/UserCreatedConversation.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Thread;
|
||||||
|
|
||||||
|
class UserCreatedConversation
|
||||||
|
{
|
||||||
|
public $conversation;
|
||||||
|
public $last_thread;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Conversation $conversation, Thread $last_thread)
|
||||||
|
{
|
||||||
|
$this->conversation = $conversation;
|
||||||
|
$this->last_thread = $last_thread;
|
||||||
|
}
|
||||||
|
}
|
23
freescout-dist/app/Events/UserCreatedConversationDraft.php
Normal file
23
freescout-dist/app/Events/UserCreatedConversationDraft.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Thread;
|
||||||
|
|
||||||
|
class UserCreatedConversationDraft
|
||||||
|
{
|
||||||
|
public $conversation;
|
||||||
|
public $thread;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Conversation $conversation, Thread $thread)
|
||||||
|
{
|
||||||
|
$this->conversation = $conversation;
|
||||||
|
$this->thread = $thread;
|
||||||
|
}
|
||||||
|
}
|
23
freescout-dist/app/Events/UserCreatedThreadDraft.php
Normal file
23
freescout-dist/app/Events/UserCreatedThreadDraft.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Thread;
|
||||||
|
|
||||||
|
class UserCreatedThreadDraft
|
||||||
|
{
|
||||||
|
public $conversation;
|
||||||
|
public $thread;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Conversation $conversation, Thread $thread)
|
||||||
|
{
|
||||||
|
$this->conversation = $conversation;
|
||||||
|
$this->thread = $thread;
|
||||||
|
}
|
||||||
|
}
|
24
freescout-dist/app/Events/UserDeleted.php
Normal file
24
freescout-dist/app/Events/UserDeleted.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\User;
|
||||||
|
|
||||||
|
class UserDeleted
|
||||||
|
{
|
||||||
|
public $by_user;
|
||||||
|
public $deleted_user;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(User $deleted_user, User $by_user)
|
||||||
|
{
|
||||||
|
$this->by_user = $by_user;
|
||||||
|
$this->deleted_user = $deleted_user;
|
||||||
|
|
||||||
|
\Eventy::action('user.deleted', $deleted_user, $by_user);
|
||||||
|
}
|
||||||
|
}
|
23
freescout-dist/app/Events/UserReplied.php
Normal file
23
freescout-dist/app/Events/UserReplied.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Thread;
|
||||||
|
|
||||||
|
class UserReplied
|
||||||
|
{
|
||||||
|
public $conversation;
|
||||||
|
public $thread;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Conversation $conversation, Thread $thread)
|
||||||
|
{
|
||||||
|
$this->conversation = $conversation;
|
||||||
|
$this->thread = $thread;
|
||||||
|
}
|
||||||
|
}
|
55
freescout-dist/app/Exceptions/Handler.php
Normal file
55
freescout-dist/app/Exceptions/Handler.php
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
|
|
||||||
|
class Handler extends ExceptionHandler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* A list of the exception types that are not reported.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $dontReport = [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of the inputs that are never flashed for validation exceptions.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $dontFlash = [
|
||||||
|
'password',
|
||||||
|
'password_confirmation',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report or log an exception.
|
||||||
|
*
|
||||||
|
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
|
||||||
|
*
|
||||||
|
* @param \Exception $exception
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function report(Exception $exception)
|
||||||
|
{
|
||||||
|
parent::report($exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an exception into an HTTP response.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Exception $exception
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function render($request, Exception $exception)
|
||||||
|
{
|
||||||
|
return parent::render($request, $exception);
|
||||||
|
}
|
||||||
|
}
|
36
freescout-dist/app/FailedJob.php
Normal file
36
freescout-dist/app/FailedJob.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class FailedJob extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Automatically converted into Carbon dates.
|
||||||
|
*/
|
||||||
|
protected $dates = ['failed_at'];
|
||||||
|
|
||||||
|
public $payload_decoded = null;
|
||||||
|
|
||||||
|
public function getPayloadDecoded()
|
||||||
|
{
|
||||||
|
if ($this->payload_decoded !== null) {
|
||||||
|
return $this->payload_decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->payload_decoded = json_decode($this->payload, true);
|
||||||
|
|
||||||
|
return $this->payload_decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCommand()
|
||||||
|
{
|
||||||
|
return \App\Job::getPayloadCommand($this->getPayloadDecoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function retry($job_id)
|
||||||
|
{
|
||||||
|
\Artisan::call('queue:retry', ['id' => $job_id]);
|
||||||
|
}
|
||||||
|
}
|
456
freescout-dist/app/Folder.php
Normal file
456
freescout-dist/app/Folder.php
Normal file
@ -0,0 +1,456 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Folder extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Folders types (ids from HelpScout interface).
|
||||||
|
*/
|
||||||
|
const TYPE_UNASSIGNED = 1;
|
||||||
|
// User specific
|
||||||
|
const TYPE_MINE = 20;
|
||||||
|
// User specific
|
||||||
|
const TYPE_STARRED = 25;
|
||||||
|
const TYPE_DRAFTS = 30;
|
||||||
|
const TYPE_ASSIGNED = 40;
|
||||||
|
const TYPE_CLOSED = 60;
|
||||||
|
const TYPE_DELETED = 70;
|
||||||
|
const TYPE_SPAM = 80;
|
||||||
|
|
||||||
|
public static $types = [
|
||||||
|
self::TYPE_UNASSIGNED => 'Unassigned',
|
||||||
|
self::TYPE_MINE => 'Mine',
|
||||||
|
self::TYPE_STARRED => 'Starred',
|
||||||
|
self::TYPE_DRAFTS => 'Drafts',
|
||||||
|
self::TYPE_ASSIGNED => 'Assigned',
|
||||||
|
self::TYPE_CLOSED => 'Closed',
|
||||||
|
self::TYPE_SPAM => 'Spam',
|
||||||
|
self::TYPE_DELETED => 'Deleted',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://glyphicons.bootstrapcheatsheets.com/.
|
||||||
|
*/
|
||||||
|
public static $type_icons = [
|
||||||
|
self::TYPE_UNASSIGNED => 'folder-open',
|
||||||
|
self::TYPE_MINE => 'hand-right',
|
||||||
|
self::TYPE_DRAFTS => 'duplicate',
|
||||||
|
self::TYPE_ASSIGNED => 'user',
|
||||||
|
self::TYPE_CLOSED => 'lock', // lock
|
||||||
|
self::TYPE_SPAM => 'ban-circle',
|
||||||
|
self::TYPE_DELETED => 'trash',
|
||||||
|
self::TYPE_STARRED => 'star',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Public non-user specific mailbox types
|
||||||
|
public static $public_types = [
|
||||||
|
self::TYPE_UNASSIGNED,
|
||||||
|
self::TYPE_DRAFTS,
|
||||||
|
self::TYPE_ASSIGNED,
|
||||||
|
self::TYPE_CLOSED,
|
||||||
|
self::TYPE_SPAM,
|
||||||
|
self::TYPE_DELETED,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Folder types which belong to specific user.
|
||||||
|
// These folders has user_id specified.
|
||||||
|
public static $personal_types = [
|
||||||
|
self::TYPE_MINE,
|
||||||
|
self::TYPE_STARRED,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Folder types to which conversations are added via conversation_folder table.
|
||||||
|
public static $indirect_types = [
|
||||||
|
self::TYPE_DRAFTS,
|
||||||
|
self::TYPE_STARRED,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Counter mode.
|
||||||
|
const COUNTER_ACTIVE = 1;
|
||||||
|
const COUNTER_TOTAL = 2;
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'meta' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mailbox to which folder belongs.
|
||||||
|
*/
|
||||||
|
public function mailbox()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\Mailbox');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user to which folder belongs.
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo('App\User');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get starred conversations.
|
||||||
|
*/
|
||||||
|
public function conversations()
|
||||||
|
{
|
||||||
|
return $this->hasMany('App\Conversation');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTypeName()
|
||||||
|
{
|
||||||
|
// To make name translatable.
|
||||||
|
switch ($this->type) {
|
||||||
|
case self::TYPE_UNASSIGNED:
|
||||||
|
return __('Unassigned');
|
||||||
|
case self::TYPE_MINE:
|
||||||
|
return __('Mine');
|
||||||
|
case self::TYPE_DRAFTS:
|
||||||
|
return __('Drafts');
|
||||||
|
case self::TYPE_ASSIGNED:
|
||||||
|
return __('Assigned');
|
||||||
|
case self::TYPE_CLOSED:
|
||||||
|
return __('Closed');
|
||||||
|
case self::TYPE_SPAM:
|
||||||
|
return __('Spam');
|
||||||
|
case self::TYPE_DELETED:
|
||||||
|
return __('Deleted');
|
||||||
|
case self::TYPE_STARRED:
|
||||||
|
return __('Starred');
|
||||||
|
default:
|
||||||
|
return __(\Eventy::filter('folder.type_name', self::$types[$this->type] ?? '', $this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTypeIcon()
|
||||||
|
{
|
||||||
|
return \Eventy::filter('folder.type_icon', self::$type_icons[$this->type] ?? '', $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get order by array.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getOrderByArray()
|
||||||
|
{
|
||||||
|
$order_by = [];
|
||||||
|
|
||||||
|
switch ($this->type) {
|
||||||
|
case self::TYPE_UNASSIGNED:
|
||||||
|
case self::TYPE_MINE:
|
||||||
|
case self::TYPE_ASSIGNED:
|
||||||
|
$order_by[] = ['status' => 'asc'];
|
||||||
|
$order_by[] = ['last_reply_at' => 'desc'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::TYPE_STARRED:
|
||||||
|
$order_by[] = ['status' => 'asc'];
|
||||||
|
$order_by[] = ['last_reply_at' => 'desc'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::TYPE_DRAFTS:
|
||||||
|
$order_by = [['updated_at' => 'desc']];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::TYPE_CLOSED:
|
||||||
|
$order_by = [['closed_at' => 'desc']];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::TYPE_SPAM:
|
||||||
|
$order_by = [['last_reply_at' => 'desc']];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::TYPE_DELETED:
|
||||||
|
$order_by = [['user_updated_at' => 'desc']];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$order_by = \Eventy::filter('folder.conversations_order_by', $order_by, $this->type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process columns sorting.
|
||||||
|
$sorting = Conversation::getConvTableSorting();
|
||||||
|
if ($sorting['sort_by'] == 'date') {
|
||||||
|
if ($sorting['order'] != 'desc') {
|
||||||
|
foreach ($order_by as $block_i => $block) {
|
||||||
|
foreach ($block as $field => $order) {
|
||||||
|
if ($field == 'status') {
|
||||||
|
unset($order_by[$block_i][$field]);
|
||||||
|
} else {
|
||||||
|
if ($order == 'desc') {
|
||||||
|
$order_by[$block_i][$field] = 'asc';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$order_by = [[$sorting['sort_by'] => $sorting['order']]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $order_by;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add order by to the query.
|
||||||
|
*/
|
||||||
|
public function queryAddOrderBy($query)
|
||||||
|
{
|
||||||
|
$order_bys = $this->getOrderByArray();
|
||||||
|
foreach ($order_bys as $order_by) {
|
||||||
|
foreach ($order_by as $field => $sort_order) {
|
||||||
|
$query->orderBy($field, $sort_order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this folder accumulates conversations via conversation_folder table.
|
||||||
|
*/
|
||||||
|
public function isIndirect()
|
||||||
|
{
|
||||||
|
return in_array($this->type, self::$indirect_types);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateCounters()
|
||||||
|
{
|
||||||
|
if (config('app.update_folder_counters_in_background')) {
|
||||||
|
\App\Jobs\UpdateFolderCounters::dispatch($this);
|
||||||
|
} else {
|
||||||
|
$this->updateCountersNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateCountersNow()
|
||||||
|
{
|
||||||
|
if (\Eventy::filter('folder.update_counters', false, $this)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($this->type == self::TYPE_MINE && $this->user_id) {
|
||||||
|
$this->active_count = Conversation::where('user_id', $this->user_id)
|
||||||
|
->where('mailbox_id', $this->mailbox_id)
|
||||||
|
->where('state', Conversation::STATE_PUBLISHED)
|
||||||
|
->where('status', Conversation::STATUS_ACTIVE)
|
||||||
|
->count();
|
||||||
|
$this->total_count = Conversation::where('user_id', $this->user_id)
|
||||||
|
->where('mailbox_id', $this->mailbox_id)
|
||||||
|
->where('state', Conversation::STATE_PUBLISHED)
|
||||||
|
->count();
|
||||||
|
} elseif ($this->type == self::TYPE_STARRED) {
|
||||||
|
$this->active_count = count(Conversation::getUserStarredConversationIds($this->mailbox_id, $this->user_id));
|
||||||
|
$this->total_count = $this->active_count;
|
||||||
|
} elseif ($this->type == self::TYPE_DELETED) {
|
||||||
|
$this->active_count = $this->conversations()->where('state', Conversation::STATE_DELETED)
|
||||||
|
->count();
|
||||||
|
$this->total_count = $this->active_count;
|
||||||
|
} elseif ($this->isIndirect()) {
|
||||||
|
// Conversation are connected to folder via conversation_folder table.
|
||||||
|
// Drafts.
|
||||||
|
$this->active_count = ConversationFolder::where('conversation_folder.folder_id', $this->id)
|
||||||
|
->join('conversations', 'conversations.id', '=', 'conversation_folder.conversation_id')
|
||||||
|
//->where('state', Conversation::STATE_PUBLISHED)
|
||||||
|
->count();
|
||||||
|
$this->total_count = $this->active_count;
|
||||||
|
} else {
|
||||||
|
$this->active_count = $this->conversations()
|
||||||
|
->where('state', Conversation::STATE_PUBLISHED)
|
||||||
|
->where('status', Conversation::STATUS_ACTIVE)
|
||||||
|
->count();
|
||||||
|
$this->total_count = $this->conversations()
|
||||||
|
->where('state', Conversation::STATE_PUBLISHED)
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count to display in folders list.
|
||||||
|
*
|
||||||
|
* @param array $folders [description]
|
||||||
|
*
|
||||||
|
* @return [type] [description]
|
||||||
|
*/
|
||||||
|
public function getCount($folders = [])
|
||||||
|
{
|
||||||
|
$counter = \Eventy::filter('folder.counter', self::COUNTER_ACTIVE, $this, $folders);
|
||||||
|
|
||||||
|
$count = \Eventy::filter('folder.count', false, $this, $counter, $folders);
|
||||||
|
if ($count !== false) {
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($counter == self::COUNTER_TOTAL || $this->type == self::TYPE_STARRED || $this->type == self::TYPE_DRAFTS) {
|
||||||
|
return $this->total_count;
|
||||||
|
} else {
|
||||||
|
return $this->getActiveCount($folders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get calculated number of active conversations.
|
||||||
|
*/
|
||||||
|
public function getActiveCount($folders = [])
|
||||||
|
{
|
||||||
|
$active_count = $this->active_count;
|
||||||
|
if ($this->type == self::TYPE_ASSIGNED) {
|
||||||
|
$mine_folder = \Eventy::filter('folder.active_count_mine_folder', null, $this, $folders);
|
||||||
|
|
||||||
|
if (!$mine_folder) {
|
||||||
|
if ($folders) {
|
||||||
|
$mine_folder = $folders->firstWhere('type', self::TYPE_MINE);
|
||||||
|
} elseif ($this->mailbox_id) {
|
||||||
|
$mine_folder = $this->mailbox->folders()->where('type', self::TYPE_MINE)->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mine_folder) {
|
||||||
|
$active_count = $active_count - $mine_folder->active_count;
|
||||||
|
if ($active_count < 0) {
|
||||||
|
$active_count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $active_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query for waiting since.
|
||||||
|
*/
|
||||||
|
public function getWaitingSinceQuery()
|
||||||
|
{
|
||||||
|
$query = null;
|
||||||
|
|
||||||
|
if ($this->type == self::TYPE_MINE) {
|
||||||
|
// Assigned to user.
|
||||||
|
$query = Conversation::where('user_id', $this->user_id)
|
||||||
|
->where('mailbox_id', $this->mailbox_id);
|
||||||
|
} elseif ($this->isIndirect()) {
|
||||||
|
// Via intermediate table.
|
||||||
|
$query = Conversation::join('conversation_folder', 'conversations.id', '=', 'conversation_folder.conversation_id')
|
||||||
|
->where('conversation_folder.folder_id', $this->id);
|
||||||
|
} else {
|
||||||
|
// All other conversations.
|
||||||
|
$query = $this->conversations();
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Eventy::filter('folder.waiting_since_query', $query, $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Works for main folder only for now.
|
||||||
|
*
|
||||||
|
* @return [type] [description]
|
||||||
|
*/
|
||||||
|
public function getWaitingSince()
|
||||||
|
{
|
||||||
|
// Get oldest active conversation.
|
||||||
|
$conversation = $this->getWaitingSinceQuery()
|
||||||
|
->where('state', Conversation::STATE_PUBLISHED)
|
||||||
|
->where('status', Conversation::STATUS_ACTIVE)
|
||||||
|
->orderBy($this->getWaitingSinceField(), 'asc')
|
||||||
|
->first();
|
||||||
|
if ($conversation) {
|
||||||
|
return $conversation->getWaitingSince($this);
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get conversation field used to detect waiting since time.
|
||||||
|
*
|
||||||
|
* @return [type] [description]
|
||||||
|
*/
|
||||||
|
public function getWaitingSinceField()
|
||||||
|
{
|
||||||
|
if ($this->type == \App\Folder::TYPE_CLOSED) {
|
||||||
|
return 'closed_at';
|
||||||
|
} elseif ($this->type == \App\Folder::TYPE_DRAFTS) {
|
||||||
|
return 'updated_at';
|
||||||
|
} elseif ($this->type == \App\Folder::TYPE_DELETED) {
|
||||||
|
return 'user_updated_at';
|
||||||
|
} else {
|
||||||
|
return'last_reply_at';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function url($mailbox_id)
|
||||||
|
{
|
||||||
|
return \Eventy::filter('folder.url', route('mailboxes.view.folder', ['id'=>$mailbox_id, 'folder_id'=>$this->id]), $mailbox_id, $this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create($data, $unique_per_user = true, $save = true)
|
||||||
|
{
|
||||||
|
if (!isset($data['mailbox_id']) || !isset($data['type'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$folder = new Folder ();
|
||||||
|
$folder->mailbox_id = $data['mailbox_id'];
|
||||||
|
$folder->type = $data['type'];
|
||||||
|
|
||||||
|
if (!empty($data['user_id'])) {
|
||||||
|
if ($unique_per_user) {
|
||||||
|
$user_folder = Folder::where('mailbox_id', $data['mailbox_id'])
|
||||||
|
->where('user_id', $data['user_id'])
|
||||||
|
->where('type', $data['type'])
|
||||||
|
->first();
|
||||||
|
// User folder already exists.
|
||||||
|
if ($user_folder) {
|
||||||
|
return $user_folder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$folder->user_id = $data['user_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($save) {
|
||||||
|
$folder->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get meta value.
|
||||||
|
*/
|
||||||
|
public function getMeta($key, $default = null)
|
||||||
|
{
|
||||||
|
$metas = $this->meta;
|
||||||
|
if (isset($metas[$key])) {
|
||||||
|
return $metas[$key];
|
||||||
|
} else {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set meta value.
|
||||||
|
*/
|
||||||
|
public function setMeta($key, $value)
|
||||||
|
{
|
||||||
|
$metas = $this->meta;
|
||||||
|
$metas[$key] = $value;
|
||||||
|
$this->meta = $metas;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unset thread meta value.
|
||||||
|
*/
|
||||||
|
public function unsetMeta($key)
|
||||||
|
{
|
||||||
|
$metas = $this->meta;
|
||||||
|
if (isset($metas[$key])) {
|
||||||
|
unset($metas[$key]);
|
||||||
|
$this->meta = $metas;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
freescout-dist/app/Follower.php
Normal file
10
freescout-dist/app/Follower.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Follower extends Model
|
||||||
|
{
|
||||||
|
public $timestamps = false;
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
||||||
|
|
||||||
|
class ForgotPasswordController extends Controller
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Reset Controller
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This controller is responsible for handling password reset emails and
|
||||||
|
| includes a trait which assists in sending these notifications from
|
||||||
|
| your application to your users. Feel free to explore this trait.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
use SendsPasswordResetEmails;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('guest');
|
||||||
|
}
|
||||||
|
}
|
79
freescout-dist/app/Http/Controllers/Auth/LoginController.php
Normal file
79
freescout-dist/app/Http/Controllers/Auth/LoginController.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class LoginController extends Controller
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Login Controller
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This controller handles authenticating users for the application and
|
||||||
|
| redirecting them to your home screen. The controller uses a trait
|
||||||
|
| to conveniently provide its functionality to your applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
use AuthenticatesUsers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where to redirect users after login.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $redirectTo = '/home';
|
||||||
|
|
||||||
|
public $decayMinutes = 10; // minutes to lockout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('guest')->except('logout');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a login request to the application.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function login(Request $request)
|
||||||
|
{
|
||||||
|
$this->validateLogin($request);
|
||||||
|
|
||||||
|
// If the class is using the ThrottlesLogins trait, we can automatically throttle
|
||||||
|
// the login attempts for this application. We'll key this by the username and
|
||||||
|
// the IP address of the client making these requests into this application.
|
||||||
|
if ($this->hasTooManyLoginAttempts($request)) {
|
||||||
|
$this->fireLockoutEvent($request);
|
||||||
|
|
||||||
|
return $this->sendLockoutResponse($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$custom_errors = \Eventy::filter('login.custom_check', [], $request);
|
||||||
|
if ($custom_errors) {
|
||||||
|
throw ValidationException::withMessages($custom_errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->attemptLogin($request)) {
|
||||||
|
return $this->sendLoginResponse($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the login attempt was unsuccessful we will increment the number of attempts
|
||||||
|
// to login and redirect the user back to the login form. Of course, when this
|
||||||
|
// user surpasses their maximum number of attempts they will get locked out.
|
||||||
|
$this->incrementLoginAttempts($request);
|
||||||
|
|
||||||
|
return $this->sendFailedLoginResponse($request);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class RegisterController extends Controller
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Register Controller
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This controller handles the registration of new users as well as their
|
||||||
|
| validation and creation. By default this controller uses a trait to
|
||||||
|
| provide this functionality without requiring any additional code.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
use RegistersUsers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where to redirect users after registration.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $redirectTo = '/home';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('guest');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a validator for an incoming registration request.
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Contracts\Validation\Validator
|
||||||
|
*/
|
||||||
|
protected function validator(array $data)
|
||||||
|
{
|
||||||
|
return Validator::make($data, [
|
||||||
|
'first_name' => 'required|string|max:255',
|
||||||
|
'last_name' => 'required|string|max:255',
|
||||||
|
'email' => 'required|string|email|max:191|unique:users',
|
||||||
|
'password' => 'required|string|min:8|confirmed',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new user instance after a valid registration.
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
*
|
||||||
|
* @return \App\User
|
||||||
|
*/
|
||||||
|
protected function create(array $data)
|
||||||
|
{
|
||||||
|
return User::create([
|
||||||
|
'first_name' => $data['first_name'],
|
||||||
|
'last_name' => $data['last_name'],
|
||||||
|
'email' => $data['email'],
|
||||||
|
'password' => bcrypt($data['password']),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration is disabled.
|
||||||
|
*/
|
||||||
|
public function showRegistrationForm()
|
||||||
|
{
|
||||||
|
return redirect('login');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Auth\Events\PasswordReset;
|
||||||
|
|
||||||
|
class ResetPasswordController extends Controller
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Reset Controller
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This controller is responsible for handling password reset requests
|
||||||
|
| and uses a simple trait to include this behavior. You're free to
|
||||||
|
| explore this trait and override any methods you wish to tweak.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
use ResetsPasswords;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where to redirect users after resetting their password.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $redirectTo = '/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('guest');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the given user's password.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Contracts\Auth\CanResetPassword $user
|
||||||
|
* @param string $password
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function resetPassword($user, $password)
|
||||||
|
{
|
||||||
|
$user->password = Hash::make($password);
|
||||||
|
|
||||||
|
$user->setRememberToken(Str::random(60));
|
||||||
|
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
event(new PasswordReset($user));
|
||||||
|
|
||||||
|
//$this->guard()->login($user);
|
||||||
|
}
|
||||||
|
}
|
13
freescout-dist/app/Http/Controllers/Controller.php
Normal file
13
freescout-dist/app/Http/Controllers/Controller.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||||
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||||
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
|
|
||||||
|
class Controller extends BaseController
|
||||||
|
{
|
||||||
|
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
|
||||||
|
}
|
3231
freescout-dist/app/Http/Controllers/ConversationsController.php
Executable file
3231
freescout-dist/app/Http/Controllers/ConversationsController.php
Executable file
File diff suppressed because it is too large
Load Diff
414
freescout-dist/app/Http/Controllers/CustomersController.php
Normal file
414
freescout-dist/app/Http/Controllers/CustomersController.php
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Customer;
|
||||||
|
use App\Email;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Validator;
|
||||||
|
|
||||||
|
class CustomersController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit customer.
|
||||||
|
*/
|
||||||
|
public function update($id)
|
||||||
|
{
|
||||||
|
$customer = Customer::findOrFail($id);
|
||||||
|
|
||||||
|
$customer_emails = $customer->emails;
|
||||||
|
if (count($customer_emails)) {
|
||||||
|
foreach ($customer_emails as $row) {
|
||||||
|
$emails[] = $row->email;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$emails = [''];
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('customers/update', ['customer' => $customer, 'emails' => $emails]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save customer.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function updateSave($id, Request $request)
|
||||||
|
{
|
||||||
|
function mb_ucfirst($string)
|
||||||
|
{
|
||||||
|
return mb_strtoupper(mb_substr($string, 0, 1)).mb_strtolower(mb_substr($string, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer = Customer::findOrFail($id);
|
||||||
|
$flash_message = '';
|
||||||
|
|
||||||
|
// First name or email must be specified
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'first_name' => 'nullable|string|max:255|required_without:emails.0',
|
||||||
|
'last_name' => 'nullable|string|max:255',
|
||||||
|
'city' => 'nullable|string|max:255',
|
||||||
|
'state' => 'nullable|string|max:255',
|
||||||
|
'zip' => 'nullable|string|max:12',
|
||||||
|
'country' => 'nullable|string|max:2',
|
||||||
|
//'emails' => 'array|required_without:first_name',
|
||||||
|
//'emails.1' => 'nullable|email|required_without:first_name',
|
||||||
|
'emails.*' => 'nullable|email|distinct|required_without:first_name',
|
||||||
|
'photo_url' => 'nullable|image|mimes:jpeg,png,jpg,gif',
|
||||||
|
]);
|
||||||
|
$validator->setAttributeNames([
|
||||||
|
'photo_url' => __('Photo'),
|
||||||
|
'emails.*' => __('Email'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Photo
|
||||||
|
$validator->after(function ($validator) use ($customer, $request) {
|
||||||
|
if ($request->hasFile('photo_url')) {
|
||||||
|
$path_url = $customer->savePhoto($request->file('photo_url')->getRealPath(), $request->file('photo_url')->getMimeType());
|
||||||
|
|
||||||
|
if ($path_url) {
|
||||||
|
$customer->photo_url = $path_url;
|
||||||
|
} else {
|
||||||
|
$validator->errors()->add('photo_url', __('Error occurred processing the image. Make sure that PHP GD extension is enabled.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('customers.update', ['id' => $id])
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$new_emails = [];
|
||||||
|
$new_emails_change_customer = [];
|
||||||
|
$removed_emails = [];
|
||||||
|
|
||||||
|
// Detect new emails added
|
||||||
|
$customer_emails = $customer->emails()->pluck('email')->toArray();
|
||||||
|
foreach ($request->emails as $email) {
|
||||||
|
if (!in_array($email, $customer_emails)) {
|
||||||
|
$new_emails[] = $email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If new email belongs to another customer, let user know about it in the flash message
|
||||||
|
foreach ($new_emails as $new_email) {
|
||||||
|
$email = Email::where('email', $new_email)->first();
|
||||||
|
if ($email && $email->customer) {
|
||||||
|
// If customer whose email is removed does not have first name and other emails
|
||||||
|
// we have to create first name for this customer
|
||||||
|
if (!$email->customer->first_name && count($email->customer->emails) == 1) {
|
||||||
|
if ($request->first_name) {
|
||||||
|
$email->customer->first_name = $request->first_name;
|
||||||
|
} elseif ($customer->first_name) {
|
||||||
|
$email->customer->first_name = $customer->first_name;
|
||||||
|
} else {
|
||||||
|
$email->customer->first_name = mb_ucfirst($email->getNameFromEmail());
|
||||||
|
}
|
||||||
|
$email->customer->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$flash_message .= __('Email :tag_email_begin:email:tag_email_end has been moved from another customer: :a_begin:customer:a_end.', [
|
||||||
|
'email' => $email->email,
|
||||||
|
'tag_email_begin' => '<strong>',
|
||||||
|
'tag_email_end' => '</strong>',
|
||||||
|
'customer' => $email->customer->getFullName(),
|
||||||
|
'a_begin' => '<strong><a href="'.$email->customer->url().'" target="_blank">',
|
||||||
|
'a_end' => '</a></strong>',
|
||||||
|
]).' ';
|
||||||
|
|
||||||
|
$new_emails_change_customer[] = $email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect removed emails
|
||||||
|
foreach ($customer_emails as $email) {
|
||||||
|
if (!in_array($email, $request->emails)) {
|
||||||
|
$removed_emails[] = $email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$request_data = $request->all();
|
||||||
|
|
||||||
|
if (isset($request_data['photo_url'])) {
|
||||||
|
unset($request_data['photo_url']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer->setData($request_data);
|
||||||
|
// Websites
|
||||||
|
// if (!empty($request->websites)) {
|
||||||
|
// $customer->setWebsites($request->websites);
|
||||||
|
// }
|
||||||
|
$customer->save();
|
||||||
|
|
||||||
|
$customer->syncEmails($request->emails);
|
||||||
|
|
||||||
|
// Update customer_id in all conversations added to the current customer.
|
||||||
|
foreach ($new_emails_change_customer as $new_email) {
|
||||||
|
if ($new_email->customer_id) {
|
||||||
|
$conversations_to_change_customer = Conversation::where('customer_id', $new_email->customer_id)->get();
|
||||||
|
} else {
|
||||||
|
// This does not work for phone conversations.
|
||||||
|
$conversations_to_change_customer = Conversation::where('customer_email', $new_email->email)->get();
|
||||||
|
}
|
||||||
|
foreach ($conversations_to_change_customer as $conversation) {
|
||||||
|
// We have to pass user to create line item and let others know that customer has changed.
|
||||||
|
// Conversation may be even in other mailbox where user does not have an access.
|
||||||
|
$conversation->changeCustomer($new_email->email, $customer, auth()->user());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update customer in conversations for emails removed from current customer.
|
||||||
|
foreach ($removed_emails as $removed_email) {
|
||||||
|
$email = Email::where('email', $removed_email)->first();
|
||||||
|
if ($email) {
|
||||||
|
$conversations = Conversation::where('customer_email', $email->email)->get();
|
||||||
|
foreach ($conversations as $conversation) {
|
||||||
|
$conversation->changeCustomer($email->email, $email->customer, auth()->user());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
\Eventy::action('customer.updated', $customer);
|
||||||
|
|
||||||
|
$flash_message = __('Customer saved successfully.').' '.$flash_message;
|
||||||
|
\Session::flash('flash_success_unescaped', $flash_message);
|
||||||
|
|
||||||
|
\Session::flash('customer.updated', 1);
|
||||||
|
|
||||||
|
return redirect()->route('customers.update', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User mailboxes.
|
||||||
|
*/
|
||||||
|
public function permissions($id)
|
||||||
|
{
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
$this->authorize('update', $user);
|
||||||
|
|
||||||
|
$mailboxes = Mailbox::all();
|
||||||
|
|
||||||
|
return view('users/permissions', ['user' => $user, 'mailboxes' => $mailboxes, 'user_mailboxes' => $user->mailboxes]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save user permissions.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*/
|
||||||
|
public function permissionsSave($id, Request $request)
|
||||||
|
{
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
$this->authorize('update', $user);
|
||||||
|
|
||||||
|
$user->mailboxes()->sync($request->mailboxes ?: []);
|
||||||
|
|
||||||
|
\Session::flash('flash_success', __('Permissions saved successfully'));
|
||||||
|
|
||||||
|
return redirect()->route('users.permissions', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View customer conversations.
|
||||||
|
*
|
||||||
|
* @param intg $id
|
||||||
|
*/
|
||||||
|
public function conversations($id)
|
||||||
|
{
|
||||||
|
$customer = Customer::findOrFail($id);
|
||||||
|
|
||||||
|
$conversations = $customer->conversations()
|
||||||
|
->where('customer_id', $customer->id)
|
||||||
|
->whereIn('mailbox_id', auth()->user()->mailboxesIdsCanView())
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->paginate(Conversation::DEFAULT_LIST_SIZE);
|
||||||
|
|
||||||
|
return view('customers/conversations', [
|
||||||
|
'customer' => $customer,
|
||||||
|
'conversations' => $conversations,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customers ajax search.
|
||||||
|
*/
|
||||||
|
public function ajaxSearch(Request $request)
|
||||||
|
{
|
||||||
|
$response = [
|
||||||
|
'results' => [],
|
||||||
|
'pagination' => ['more' => false],
|
||||||
|
];
|
||||||
|
|
||||||
|
$q = $request->q;
|
||||||
|
|
||||||
|
$join_emails = false;
|
||||||
|
if ($request->search_by == 'all' || $request->search_by == 'email' || $request->exclude_email) {
|
||||||
|
$join_emails = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$select_list = ['customers.id', 'first_name', 'last_name'];
|
||||||
|
if ($join_emails) {
|
||||||
|
$select_list[] = 'emails.email';
|
||||||
|
}
|
||||||
|
if ($request->show_fields == 'phone') {
|
||||||
|
$select_list[] = 'phones';
|
||||||
|
}
|
||||||
|
$customers_query = Customer::select($select_list);
|
||||||
|
|
||||||
|
if ($join_emails) {
|
||||||
|
if ($request->allow_non_emails) {
|
||||||
|
$customers_query->leftJoin('emails', 'customers.id', '=', 'emails.customer_id');
|
||||||
|
} else {
|
||||||
|
$customers_query->join('emails', 'customers.id', '=', 'emails.customer_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->search_by == 'all' || $request->search_by == 'email') {
|
||||||
|
$customers_query->where('emails.email', 'like', '%'.$q.'%');
|
||||||
|
}
|
||||||
|
if ($request->exclude_email) {
|
||||||
|
$customers_query->where('emails.email', '<>', $request->exclude_email);
|
||||||
|
}
|
||||||
|
if ($request->search_by == 'all' || $request->search_by == 'name') {
|
||||||
|
$customers_query->orWhere('first_name', 'like', '%'.$q.'%')
|
||||||
|
->orWhere('last_name', 'like', '%'.$q.'%');
|
||||||
|
}
|
||||||
|
if ($request->search_by == 'phone') {
|
||||||
|
$customers_query->where('customers.phones', 'like', '%'.$q.'%');
|
||||||
|
}
|
||||||
|
|
||||||
|
$customers = $customers_query->paginate(20);
|
||||||
|
|
||||||
|
foreach ($customers as $customer) {
|
||||||
|
$id = '';
|
||||||
|
$text = '';
|
||||||
|
|
||||||
|
if ($request->show_fields != 'all') {
|
||||||
|
switch ($request->show_fields) {
|
||||||
|
case 'email':
|
||||||
|
$text = $customer->email;
|
||||||
|
break;
|
||||||
|
case 'name':
|
||||||
|
$text = $customer->getFullName();
|
||||||
|
break;
|
||||||
|
case 'phone':
|
||||||
|
// Get phone which matches
|
||||||
|
$phones = $customer->getPhones();
|
||||||
|
foreach ($phones as $phone) {
|
||||||
|
if (strstr($phone['value'], $q)) {
|
||||||
|
$text = $phone['value'];
|
||||||
|
if ($customer->getFullName()) {
|
||||||
|
$text .= ' — '.$customer->getFullName();
|
||||||
|
}
|
||||||
|
$id = $phone['value'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$text = $customer->getNameAndEmail();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$text = $customer->getNameAndEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
if (!empty($request->use_id)) {
|
||||||
|
$id = $customer->id;
|
||||||
|
} else {
|
||||||
|
$id = $customer->email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$response['results'][] = [
|
||||||
|
'id' => $id,
|
||||||
|
'text' => $text,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['pagination']['more'] = $customers->hasMorePages();
|
||||||
|
|
||||||
|
return \Response::json($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajax controller.
|
||||||
|
*/
|
||||||
|
public function ajax(Request $request)
|
||||||
|
{
|
||||||
|
$response = [
|
||||||
|
'status' => 'error',
|
||||||
|
'msg' => '', // this is error message
|
||||||
|
];
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
switch ($request->action) {
|
||||||
|
|
||||||
|
// Change conversation user
|
||||||
|
case 'create':
|
||||||
|
// First name or email must be specified
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'first_name' => 'required|string|max:255',
|
||||||
|
'last_name' => 'nullable|string|max:255',
|
||||||
|
'email' => 'required|email|unique:emails,email',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
foreach ($validator->errors()->getMessages()as $errors) {
|
||||||
|
foreach ($errors as $field => $message) {
|
||||||
|
$response['msg'] .= $message.' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
|
||||||
|
$customer = Customer::create($request->email, $request->all());
|
||||||
|
if ($customer) {
|
||||||
|
$response['email'] = $request->email;
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Conversations navigation
|
||||||
|
case 'customers_pagination':
|
||||||
|
|
||||||
|
$customers = app('App\Http\Controllers\ConversationsController')->searchCustomers($request, $user);
|
||||||
|
|
||||||
|
$response['status'] = 'success';
|
||||||
|
|
||||||
|
$response['html'] = view('customers/partials/customers_table', [
|
||||||
|
'customers' => $customers,
|
||||||
|
])->render();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['msg'] = 'Unknown action';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||||
|
$response['msg'] = 'Unknown error occurred';
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Response::json($response);
|
||||||
|
}
|
||||||
|
}
|
918
freescout-dist/app/Http/Controllers/MailboxesController.php
Executable file
918
freescout-dist/app/Http/Controllers/MailboxesController.php
Executable file
@ -0,0 +1,918 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Folder;
|
||||||
|
use App\Mailbox;
|
||||||
|
use App\Thread;
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Validator;
|
||||||
|
|
||||||
|
class MailboxesController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mailboxes list.
|
||||||
|
*/
|
||||||
|
public function mailboxes()
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$mailboxes = $user->mailboxesCanView();
|
||||||
|
|
||||||
|
if (!\Eventy::filter('user.can_view_mailbox_menu', false, $user)) {
|
||||||
|
foreach ($mailboxes as $i => $mailbox) {
|
||||||
|
if (!$user->canManageMailbox($mailbox->id)) {
|
||||||
|
$mailboxes->forget($i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('mailboxes/mailboxes', ['mailboxes' => $mailboxes]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New mailbox.
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$this->authorize('create', 'App\Mailbox');
|
||||||
|
|
||||||
|
$users = User::nonDeleted()->where('role', '!=', User::ROLE_ADMIN)->get();
|
||||||
|
|
||||||
|
return view('mailboxes/create', ['users' => $users]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new mailbox.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*/
|
||||||
|
public function createSave(Request $request)
|
||||||
|
{
|
||||||
|
$invalid = false;
|
||||||
|
|
||||||
|
$this->authorize('create', 'App\Mailbox');
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'email' => 'required|string|email|max:128|unique:mailboxes',
|
||||||
|
'name' => 'required|string|max:40',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// //event(new Registered($user = $this->create($request->all())));
|
||||||
|
|
||||||
|
if (Mailbox::userEmailExists($request->email)) {
|
||||||
|
$invalid = true;
|
||||||
|
$validator->errors()->add('email', __('There is a user with such email. Users and mailboxes can not have the same email addresses.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($invalid || $validator->fails()) {
|
||||||
|
return redirect()->route('mailboxes.create')
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$mailbox = new Mailbox();
|
||||||
|
$mailbox->fill($request->all());
|
||||||
|
|
||||||
|
$mailbox->save();
|
||||||
|
|
||||||
|
$mailbox->users()->sync($request->users ?: []);
|
||||||
|
$mailbox->syncPersonalFolders($request->users);
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Mailbox created successfully'));
|
||||||
|
|
||||||
|
return redirect()->route('mailboxes.update', ['id' => $mailbox->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit mailbox.
|
||||||
|
*/
|
||||||
|
public function update($id)
|
||||||
|
{
|
||||||
|
$mailbox = Mailbox::findOrFail($id);
|
||||||
|
$user = auth()->user();
|
||||||
|
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
|
||||||
|
$accessible_route = '';
|
||||||
|
|
||||||
|
$mailbox_settings = $user->mailboxSettings($mailbox->id);
|
||||||
|
|
||||||
|
if (!is_array($mailbox_settings->access)) {
|
||||||
|
$access_permissions = json_decode($mailbox_settings->access ?? '');
|
||||||
|
} else {
|
||||||
|
$access_permissions = $mailbox_settings->access;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($access_permissions && is_array($access_permissions)) {
|
||||||
|
foreach ($access_permissions as $perm) {
|
||||||
|
$accessible_route = Mailbox::getAccessPermissionRoute($perm);
|
||||||
|
if ($accessible_route) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$accessible_route) {
|
||||||
|
$accessible_route = \Eventy::filter('mailbox.accessible_settings_route', '', auth()->user(), $mailbox);
|
||||||
|
}
|
||||||
|
if ($accessible_route) {
|
||||||
|
return redirect()->route($accessible_route, ['id' => $mailbox->id]);
|
||||||
|
} else {
|
||||||
|
\Helper::denyAccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||||
|
if (!$mailbox_user && $user->isAdmin()) {
|
||||||
|
// Admin may not be connected to the mailbox yet
|
||||||
|
$user->mailboxes()->attach($id);
|
||||||
|
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
//$mailboxes = Mailbox::all()->except($id);
|
||||||
|
|
||||||
|
return view('mailboxes/update', ['mailbox' => $mailbox, 'mailbox_user' => $mailbox_user, 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save mailbox.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*/
|
||||||
|
public function updateSave($id, Request $request)
|
||||||
|
{
|
||||||
|
$invalid = false;
|
||||||
|
$mailbox = Mailbox::findOrFail($id);
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
|
||||||
|
\Helper::denyAccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->can('updateSettings', $mailbox)) {
|
||||||
|
|
||||||
|
// Checkboxes
|
||||||
|
$request->merge([
|
||||||
|
'aliases_reply' => ($request->filled('aliases_reply') ?? false),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// if not admin, the text only fields don't pass so spike them into the request.
|
||||||
|
if (!auth()->user()->isAdmin()) {
|
||||||
|
$request->merge([
|
||||||
|
'name' => $mailbox->name,
|
||||||
|
'email' => $mailbox->email
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'name' => 'required|string|max:40',
|
||||||
|
'email' => 'required|string|email|max:128|unique:mailboxes,email,'.$id,
|
||||||
|
'aliases' => 'nullable|string|max:255',
|
||||||
|
'from_name' => 'required|integer',
|
||||||
|
'from_name_custom' => 'nullable|string|max:128',
|
||||||
|
'ticket_status' => 'required|integer',
|
||||||
|
'template' => 'required|integer',
|
||||||
|
'ticket_assignee' => 'required|integer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
//event(new Registered($user = $this->create($request->all())));
|
||||||
|
if (Mailbox::userEmailExists($request->email)) {
|
||||||
|
$invalid = true;
|
||||||
|
$validator->errors()->add('email', __('There is a user with such email. Users and mailboxes can not have the same email addresses.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$validator = \Eventy::filter('mailbox.settings_validator', $validator, $mailbox, $request);
|
||||||
|
|
||||||
|
if ($invalid || count($validator->errors()) || $validator->fails()) {
|
||||||
|
return redirect()->route('mailboxes.update', ['id' => $id])
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->can('updateEmailSignature', $mailbox)) {
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'signature' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('mailboxes.email_signature', ['id' => $id])
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
\Eventy::action('mailbox.settings_before_save', $mailbox, $request);
|
||||||
|
|
||||||
|
$mailbox->fill($request->all());
|
||||||
|
|
||||||
|
$mailbox->save();
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Mailbox settings saved'));
|
||||||
|
|
||||||
|
return redirect()->route('mailboxes.update', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mailbox permissions.
|
||||||
|
*/
|
||||||
|
public function permissions($id)
|
||||||
|
{
|
||||||
|
$mailbox = Mailbox::findOrFail($id);
|
||||||
|
|
||||||
|
$this->authorize('updatePermissions', $mailbox);
|
||||||
|
|
||||||
|
$users = User::nonDeleted()->where('role', '!=', User::ROLE_ADMIN)->get();
|
||||||
|
$users = User::sortUsers($users);
|
||||||
|
|
||||||
|
$managers = User::nonDeleted()
|
||||||
|
->select(['users.*', 'mailbox_user.hide', 'mailbox_user.access'])
|
||||||
|
->leftJoin('mailbox_user', function ($join) use ($mailbox) {
|
||||||
|
$join->on('mailbox_user.user_id', '=', 'users.id');
|
||||||
|
$join->where('mailbox_user.mailbox_id', $mailbox->id);
|
||||||
|
})->get();
|
||||||
|
$managers = User::sortUsers($managers);
|
||||||
|
|
||||||
|
return view('mailboxes/permissions', [
|
||||||
|
'mailbox' => $mailbox,
|
||||||
|
'users' => $users,
|
||||||
|
'managers' => $managers,
|
||||||
|
'mailbox_users' => $mailbox->users,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save mailbox permissions.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*/
|
||||||
|
public function permissionsSave($id, Request $request)
|
||||||
|
{
|
||||||
|
$mailbox = Mailbox::findOrFail($id);
|
||||||
|
$this->authorize('updatePermissions', $mailbox);
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$mailbox->users()->sync(\Eventy::filter('mailbox.permission_users', $request->users, $id) ?: []);
|
||||||
|
$mailbox->syncPersonalFolders($request->users);
|
||||||
|
|
||||||
|
// Save admins settings.
|
||||||
|
$admins = User::nonDeleted()->where('role', User::ROLE_ADMIN)->get();
|
||||||
|
foreach ($admins as $admin) {
|
||||||
|
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||||
|
if (!$mailbox_user) {
|
||||||
|
// Admin may not be connected to the mailbox yet
|
||||||
|
$admin->mailboxes()->attach($id);
|
||||||
|
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||||
|
}
|
||||||
|
$mailbox_user->settings->hide = (isset($request->managers[$admin->id]['hide']) ? (int)$request->managers[$admin->id]['hide'] : false);
|
||||||
|
$mailbox_user->settings->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the mailbox_user.access array
|
||||||
|
$mailbox_users = $mailbox->users;
|
||||||
|
foreach ($mailbox_users as $mailbox_user) {
|
||||||
|
$access = [];
|
||||||
|
$mailbox_with_settings = $mailbox_user->mailboxesWithSettings()->where('mailbox_id', $id)->first();
|
||||||
|
|
||||||
|
foreach (Mailbox::$access_permissions as $perm) {
|
||||||
|
if (!empty($request->managers[$mailbox_user->id]['access'][$perm])) {
|
||||||
|
$access[] = $request->managers[$mailbox_user->id]['access'][$perm];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->id == $mailbox_user->id && !$user->isAdmin()) {
|
||||||
|
// User with Permission priv's can't edit their own additional priv's.
|
||||||
|
} else {
|
||||||
|
$mailbox_with_settings->settings->access = json_encode($access);
|
||||||
|
}
|
||||||
|
$mailbox_with_settings->settings->hide = (isset($request->managers[$mailbox_user->id]['hide']) ? (int)$request->managers[$mailbox_user->id]['hide'] : false);
|
||||||
|
$mailbox_with_settings->settings->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Mailbox permissions saved!'));
|
||||||
|
|
||||||
|
return redirect()->route('mailboxes.permissions', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mailbox connection settings.
|
||||||
|
*/
|
||||||
|
public function connectionOutgoing($id)
|
||||||
|
{
|
||||||
|
$mailbox = Mailbox::findOrFail($id);
|
||||||
|
$this->authorize('admin', $mailbox);
|
||||||
|
|
||||||
|
return view('mailboxes/connection', ['mailbox' => $mailbox, 'sendmail_path' => ini_get('sendmail_path'), 'flashes' => $this->mailboxActiveWarning($mailbox)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save mailbox connection settings.
|
||||||
|
*/
|
||||||
|
public function connectionOutgoingSave($id, Request $request)
|
||||||
|
{
|
||||||
|
$mailbox = Mailbox::findOrFail($id);
|
||||||
|
$this->authorize('admin', $mailbox);
|
||||||
|
|
||||||
|
if ($request->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'out_server' => 'required|string|max:255',
|
||||||
|
'out_port' => 'required|integer',
|
||||||
|
'out_username' => 'nullable|string|max:100',
|
||||||
|
'out_password' => 'nullable|string|max:255',
|
||||||
|
'out_encryption' => 'required|integer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('mailboxes.connection', ['id' => $id])
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not save dummy password.
|
||||||
|
if (preg_match("/^\*+$/", $request->out_password ?? '')) {
|
||||||
|
$params = $request->except(['out_password']);
|
||||||
|
} else {
|
||||||
|
$params = $request->all();
|
||||||
|
}
|
||||||
|
$mailbox->fill($params);
|
||||||
|
$mailbox->save();
|
||||||
|
|
||||||
|
if (!empty($request->send_test_to)) {
|
||||||
|
\Option::set('send_test_to', $request->send_test_to);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sometimes background job continues to use old connection settings.
|
||||||
|
\Helper::queueWorkerRestart();
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Connection settings saved!'));
|
||||||
|
|
||||||
|
return redirect()->route('mailboxes.connection', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mailbox incoming settings.
|
||||||
|
*/
|
||||||
|
public function connectionIncoming($id, Request $request)
|
||||||
|
{
|
||||||
|
$mailbox = Mailbox::findOrFail($id);
|
||||||
|
$this->authorize('admin', $mailbox);
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'in_server' => $mailbox->in_server ?? '',
|
||||||
|
'in_port' => $mailbox->in_port ?? '',
|
||||||
|
'in_username' => $mailbox->in_username ?? '',
|
||||||
|
'in_password' => $mailbox->in_password ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$validator = Validator::make($fields, [
|
||||||
|
'in_server' => 'required',
|
||||||
|
'in_port' => 'required',
|
||||||
|
'in_username' => 'required',
|
||||||
|
'in_password' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('mailboxes/connection_incoming', ['mailbox' => $mailbox, 'flashes' => $this->mailboxActiveWarning($mailbox)])->withErrors($validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save mailbox connection settings.
|
||||||
|
*/
|
||||||
|
public function connectionIncomingSave($id, Request $request)
|
||||||
|
{
|
||||||
|
$mailbox = Mailbox::findOrFail($id);
|
||||||
|
$this->authorize('admin', $mailbox);
|
||||||
|
|
||||||
|
// $validator = Validator::make($request->all(), [
|
||||||
|
// 'in_server' => 'nullable|string|max:255',
|
||||||
|
// 'in_port' => 'nullable|integer',
|
||||||
|
// 'in_username' => 'nullable|string|max:100',
|
||||||
|
// 'in_password' => 'nullable|string|max:255',
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// if ($validator->fails()) {
|
||||||
|
// return redirect()->route('mailboxes.connection.incoming', ['id' => $id])
|
||||||
|
// ->withErrors($validator)
|
||||||
|
// ->withInput();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Checkboxes
|
||||||
|
$request->merge([
|
||||||
|
'in_validate_cert' => ($request->filled('in_validate_cert') ?? false),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Do not save dummy password.
|
||||||
|
if (preg_match("/^\*+$/", $request->in_password ?? '')) {
|
||||||
|
$params = $request->except(['in_password']);
|
||||||
|
} else {
|
||||||
|
$params = $request->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
\Eventy::action('mailbox.incoming_settings_before_save', $mailbox, $request);
|
||||||
|
|
||||||
|
$mailbox->fill($params);
|
||||||
|
|
||||||
|
// Save IMAP Folders.
|
||||||
|
// Save all custom folders except INBOX.
|
||||||
|
$in_imap_folders = [];
|
||||||
|
if (is_array($request->in_imap_folders)) {
|
||||||
|
foreach ($request->in_imap_folders as $imap_folder) {
|
||||||
|
$in_imap_folders[] = $imap_folder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$mailbox->setInImapFolders($in_imap_folders);
|
||||||
|
|
||||||
|
$mailbox->save();
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Connection settings saved!'));
|
||||||
|
|
||||||
|
return redirect()->route('mailboxes.connection.incoming', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View mailbox.
|
||||||
|
*/
|
||||||
|
public function view(Request $request, $id, $folder_id = null)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$mailbox = Mailbox::findOrFailWithSettings($id, $user->id);
|
||||||
|
$this->authorize('viewCached', $mailbox);
|
||||||
|
|
||||||
|
$folders = $mailbox->getAssesibleFolders();
|
||||||
|
|
||||||
|
$folder = null;
|
||||||
|
if (!empty($folder_id)) {
|
||||||
|
$folder = $folders->filter(function ($item) use ($folder_id) {
|
||||||
|
return $item->id == $folder_id;
|
||||||
|
})->first();
|
||||||
|
}
|
||||||
|
// By default we display Unassigned folder
|
||||||
|
if (empty($folder)) {
|
||||||
|
$folder = $folders->filter(function ($item) {
|
||||||
|
return $item->type == Folder::TYPE_UNASSIGNED;
|
||||||
|
})->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->authorize('view', $folder);
|
||||||
|
|
||||||
|
$query_conversations = Conversation::getQueryByFolder($folder, $user->id);
|
||||||
|
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(
|
||||||
|
Conversation::DEFAULT_LIST_SIZE, ['*'], 'page', $request->get('page')
|
||||||
|
);
|
||||||
|
|
||||||
|
return view('mailboxes/view', [
|
||||||
|
'mailbox' => $mailbox,
|
||||||
|
'folders' => $folders,
|
||||||
|
'folder' => $folder,
|
||||||
|
'conversations' => $conversations,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mailboxActiveWarning($mailbox)
|
||||||
|
{
|
||||||
|
$flashes = [];
|
||||||
|
|
||||||
|
if ($mailbox && \Auth::user()->can('admin', $mailbox)) {
|
||||||
|
if (Route::currentRouteName() != 'mailboxes.connection' && !$mailbox->isOutActive()) {
|
||||||
|
$flashes[] = [
|
||||||
|
'type' => 'warning',
|
||||||
|
'text' => __('Sending emails need to be configured for the mailbox in order to send emails to customers and support agents').' ('.__('Connection Settings').' » <a href="'.route('mailboxes.connection', ['id' => $mailbox->id]).'">'.__('Sending Emails').'</a>)',
|
||||||
|
'unescaped' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (Route::currentRouteName() != 'mailboxes.connection.incoming' && !$mailbox->isInActive()) {
|
||||||
|
$flashes[] = [
|
||||||
|
'type' => 'warning',
|
||||||
|
'text' => __('Receiving emails need to be configured for the mailbox in order to fetch emails from your support email address').' ('.__('Connection Settings').' » <a href="'.route('mailboxes.connection.incoming', ['id' => $mailbox->id]).'">'.__('Receiving Emails').'</a>)',
|
||||||
|
'unescaped' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $flashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto reply settings.
|
||||||
|
*/
|
||||||
|
public function autoReply($id)
|
||||||
|
{
|
||||||
|
$mailbox = Mailbox::findOrFail($id);
|
||||||
|
$this->authorize('updateAutoReply', $mailbox);
|
||||||
|
|
||||||
|
if (!$mailbox->auto_reply_subject) {
|
||||||
|
$mailbox->auto_reply_subject = 'Re: {%subject%}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('mailboxes/auto_reply', [
|
||||||
|
'mailbox' => $mailbox,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save auto reply settings.
|
||||||
|
*/
|
||||||
|
public function autoReplySave($id, Request $request)
|
||||||
|
{
|
||||||
|
$mailbox = Mailbox::findOrFail($id);
|
||||||
|
|
||||||
|
// $this->authorize('update', $mailbox);
|
||||||
|
$this->authorize('updateAutoReply', $mailbox);
|
||||||
|
|
||||||
|
$request->merge([
|
||||||
|
'auto_reply_enabled' => ($request->filled('auto_reply_enabled') ?? false),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->auto_reply_enabled) {
|
||||||
|
$post = $request->all();
|
||||||
|
$post['auto_reply_message'] = strip_tags($post['auto_reply_message']);
|
||||||
|
$validator = Validator::make($post, [
|
||||||
|
'auto_reply_subject' => 'required|string|max:128',
|
||||||
|
'auto_reply_message' => 'required|string',
|
||||||
|
]);
|
||||||
|
$validator->setAttributeNames([
|
||||||
|
'auto_reply_subject' => __('Subject'),
|
||||||
|
'auto_reply_message' => __('Message'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('mailboxes.auto_reply', ['id' => $id])
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mailbox->fill($request->all());
|
||||||
|
|
||||||
|
$mailbox->save();
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Auto Reply status saved'));
|
||||||
|
|
||||||
|
return redirect()->route('mailboxes.auto_reply', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto reply settings.
|
||||||
|
*/
|
||||||
|
public function emailSignature($id)
|
||||||
|
{
|
||||||
|
$mailbox = Mailbox::findOrFail($id);
|
||||||
|
$this->authorize('updateAutoReply', $mailbox);
|
||||||
|
|
||||||
|
if (!$mailbox->auto_reply_subject) {
|
||||||
|
$mailbox->auto_reply_subject = 'Re: {%subject%}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('mailboxes/email_signature', [
|
||||||
|
'mailbox' => $mailbox,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Users ajax controller.
|
||||||
|
*/
|
||||||
|
public function ajax(Request $request)
|
||||||
|
{
|
||||||
|
$response = [
|
||||||
|
'status' => 'error',
|
||||||
|
'msg' => '', // this is error message
|
||||||
|
];
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
switch ($request->action) {
|
||||||
|
|
||||||
|
// Test sending emails from mailbox
|
||||||
|
case 'send_test':
|
||||||
|
$mailbox = Mailbox::find($request->mailbox_id);
|
||||||
|
|
||||||
|
if (!$mailbox) {
|
||||||
|
$response['msg'] = __('Mailbox not found');
|
||||||
|
} elseif (!$user->can('admin', $mailbox)) {
|
||||||
|
$response['msg'] = __('Not enough permissions');
|
||||||
|
} elseif (empty($request->to)) {
|
||||||
|
$response['msg'] = __('Please specify recipient of the test email');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if outgoing port is open.
|
||||||
|
if (!$response['msg'] && $mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
|
||||||
|
$test_result = \Helper::checkPort($mailbox->out_server, $mailbox->out_port);
|
||||||
|
if (!$test_result) {
|
||||||
|
$response['msg'] = __(':host is not available on :port port. Make sure that :host address is correct and that outgoing port :port on YOUR server is open.', ['host' => '<strong>'.$mailbox->out_server.'</strong>', 'port' => '<strong>'.$mailbox->out_port.'</strong>']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$test_result = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$test_result = \App\Misc\Mail::sendTestMail($request->to, $mailbox);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response['msg'] = $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$test_result && !$response['msg']) {
|
||||||
|
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember email address
|
||||||
|
if (!empty($request->to)) {
|
||||||
|
\App\Option::set('send_test_to', $request->to);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Test sending emails from mailbox
|
||||||
|
case 'fetch_test':
|
||||||
|
$mailbox = Mailbox::find($request->mailbox_id);
|
||||||
|
|
||||||
|
if (!$mailbox) {
|
||||||
|
$response['msg'] = __('Mailbox not found');
|
||||||
|
} elseif (!$user->can('admin', $mailbox)) {
|
||||||
|
$response['msg'] = __('Not enough permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = \Eventy::filter('mailbox.fetch_test', $response, $mailbox);
|
||||||
|
|
||||||
|
$tested = (isset($response['tested']) && $response['tested'] === true);
|
||||||
|
|
||||||
|
// Check if outgoing port is open.
|
||||||
|
if (!$response['msg'] && !$tested) {
|
||||||
|
$test_result = \Helper::checkPort($mailbox->in_server, $mailbox->in_port);
|
||||||
|
if (!$test_result) {
|
||||||
|
$response['msg'] = __(':host is not available on :port port. Make sure that :host address is correct and that outgoing port :port on YOUR server is open.', ['host' => '<strong>'.$mailbox->in_server.'</strong>', 'port' => '<strong>'.$mailbox->in_port.'</strong>']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg'] && !$tested) {
|
||||||
|
$test_result = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$test_result = \MailHelper::fetchTest($mailbox);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response['msg'] = $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$test_result && !$response['msg']) {
|
||||||
|
$response['msg'] = __('Error occurred connecting to the server');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg'] && !$tested) {
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Retrieve a list of available IMAP folders from server.
|
||||||
|
case 'imap_folders':
|
||||||
|
$mailbox = Mailbox::find($request->mailbox_id);
|
||||||
|
|
||||||
|
if (!$mailbox) {
|
||||||
|
$response['msg'] = __('Mailbox not found');
|
||||||
|
} elseif (!$user->can('admin', $mailbox)) {
|
||||||
|
$response['msg'] = __('Not enough permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['folders'] = [];
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
$client = \MailHelper::getMailboxClient($mailbox);
|
||||||
|
$client->connect();
|
||||||
|
|
||||||
|
$imap_folders = $client->getFolders();
|
||||||
|
|
||||||
|
if (count($imap_folders)) {
|
||||||
|
foreach ($imap_folders as $imap_folder) {
|
||||||
|
if (!empty($imap_folder->name)) {
|
||||||
|
$response['folders'][] = $imap_folder->name;
|
||||||
|
}
|
||||||
|
// Maybe we need a recursion here.
|
||||||
|
if (!empty($imap_folder->children)) {
|
||||||
|
foreach ($imap_folder->children as $child_imap_folder) {
|
||||||
|
// Old library.
|
||||||
|
if (!empty($child_imap_folder->fullName)) {
|
||||||
|
$response['folders'][] = $child_imap_folder->fullName;
|
||||||
|
}
|
||||||
|
// New library.
|
||||||
|
if (!empty($child_imap_folder->full_name)) {
|
||||||
|
$response['folders'][] = $child_imap_folder->full_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($response['folders'])) {
|
||||||
|
$response['msg_success'] = __('IMAP folders retrieved: '.implode(', ', $response['folders']));
|
||||||
|
} else {
|
||||||
|
$response['msg_success'] = __('Connected, but no IMAP folders found');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response['msg'] = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Delete mailbox
|
||||||
|
case 'delete_mailbox':
|
||||||
|
$mailbox = Mailbox::find($request->mailbox_id);
|
||||||
|
|
||||||
|
if (!$mailbox) {
|
||||||
|
$response['msg'] = __('Mailbox not found');
|
||||||
|
} elseif (!$user->can('admin', $mailbox)) {
|
||||||
|
$response['msg'] = __('Not enough permissions');
|
||||||
|
} elseif (!$user->isDummyPassword() && !Hash::check($request->password ?? '', $user->password)) {
|
||||||
|
$response['msg'] = __('Please double check your password, and try again');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
|
||||||
|
// Remove threads and conversations.
|
||||||
|
$conversation_ids = $mailbox->conversations()->pluck('id')->toArray();
|
||||||
|
|
||||||
|
for ($i=0; $i < ceil(count($conversation_ids) / \Helper::IN_LIMIT); $i++) {
|
||||||
|
$slice_ids = array_slice($conversation_ids, $i*\Helper::IN_LIMIT, \Helper::IN_LIMIT);
|
||||||
|
Thread::whereIn('conversation_id', $slice_ids)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$mailbox->conversations()->delete();
|
||||||
|
$mailbox->users()->sync([]);
|
||||||
|
$mailbox->folders()->delete();
|
||||||
|
// Maybe remove notifications on events in this mailbox?
|
||||||
|
|
||||||
|
$mailbox->delete();
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Mailbox deleted'));
|
||||||
|
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Mute notifications
|
||||||
|
case 'mute':
|
||||||
|
$mailbox = Mailbox::find($request->mailbox_id);
|
||||||
|
|
||||||
|
if (!$mailbox) {
|
||||||
|
$response['msg'] = __('Mailbox not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
|
||||||
|
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
|
||||||
|
if (!$mailbox_user) {
|
||||||
|
// User may not be connected to the mailbox yet
|
||||||
|
$user->mailboxes()->attach($mailbox->id);
|
||||||
|
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
|
||||||
|
}
|
||||||
|
$mailbox_user->settings->mute = (bool)$request->mute;
|
||||||
|
$mailbox_user->settings->save();
|
||||||
|
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['msg'] = 'Unknown action';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||||
|
$response['msg'] = 'Unknown error occurred';
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Response::json($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function oauth(Request $request)
|
||||||
|
{
|
||||||
|
$mailbox_id = $request->id ?? '';
|
||||||
|
$provider = $request->provider ?? '';
|
||||||
|
|
||||||
|
$state_data = [];
|
||||||
|
if (!empty($request->state)) {
|
||||||
|
$state_data = json_decode($request->state, true);
|
||||||
|
if (!empty($state_data['mailbox_id'])) {
|
||||||
|
$mailbox_id = $state_data['mailbox_id'];
|
||||||
|
}
|
||||||
|
if (!empty($state_data['provider'])) {
|
||||||
|
$provider = $state_data['provider'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MS Exchange.
|
||||||
|
if (!empty($request->error) && $request->error == 'invalid_request' && !empty($request->error_description)) {
|
||||||
|
return htmlspecialchars($request->error_description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($provider)) {
|
||||||
|
return 'Invalid oAuth Provider';
|
||||||
|
}
|
||||||
|
|
||||||
|
$mailbox = Mailbox::findOrFail($mailbox_id);
|
||||||
|
$this->authorize('admin', $mailbox);
|
||||||
|
|
||||||
|
if (empty($mailbox)) {
|
||||||
|
return __('Mailbox not found').': '.$mailbox_id;
|
||||||
|
}
|
||||||
|
if (empty($mailbox->in_username)) {
|
||||||
|
return 'Enter oAuth Client ID as Username and save mailbox settings';
|
||||||
|
}
|
||||||
|
if (empty($mailbox->in_password)) {
|
||||||
|
return 'Enter oAuth Client Secret as Password and save mailbox settings';
|
||||||
|
}
|
||||||
|
|
||||||
|
$session_data = [];
|
||||||
|
if (\Session::get('mailbox_oauth_'.$provider.'_'.$mailbox_id)) {
|
||||||
|
$session_data = \Session::get('mailbox_oauth_'.$provider.'_'.$mailbox_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($request->code)) {
|
||||||
|
$state = [
|
||||||
|
'provider' => $provider,
|
||||||
|
'mailbox_id' => $mailbox_id,
|
||||||
|
'state' => crc32($mailbox->in_username.$mailbox->in_password),
|
||||||
|
];
|
||||||
|
$url = \MailHelper::oauthGetAuthorizationUrl(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||||
|
'state' => json_encode($state),
|
||||||
|
'client_id' => $mailbox->in_username,
|
||||||
|
]);
|
||||||
|
if ($url) {
|
||||||
|
\Session::put('mailbox_oauth_'.$provider.'_'.$mailbox_id, $state);
|
||||||
|
// [
|
||||||
|
// 'provider' => $request->provider,
|
||||||
|
// 'mailbox_id' => $request->mailbox_id,
|
||||||
|
// 'state' => $provider->getState(),
|
||||||
|
// ]);
|
||||||
|
return redirect()->away($url);
|
||||||
|
} else {
|
||||||
|
return 'Could not generate authorization URL: check Client ID (Username) and Client Secret (Password)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check given state against previously stored one to mitigate CSRF attack
|
||||||
|
} elseif (empty($request->state) || ($state_data['state'] ?? '') !== ($session_data['state'] ?? '')) {
|
||||||
|
|
||||||
|
\Session::forget('mailbox_oauth_'.$provider.'_'.$mailbox_id);
|
||||||
|
return 'Invalid oAuth state';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Try to get an access token (using the authorization code grant)
|
||||||
|
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
|
||||||
|
'client_id' => $mailbox->in_username,
|
||||||
|
'client_secret' => $mailbox->in_password,
|
||||||
|
'code' => $request->code,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!empty($token_data['a_token'])) {
|
||||||
|
$mailbox->setMetaParam('oauth', $token_data, true);
|
||||||
|
} elseif (!empty($token_data['error'])) {
|
||||||
|
return __('Error occurred').': '.htmlspecialchars($token_data['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('mailboxes.connection.incoming', ['id' => $mailbox_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function oauthDisconnect(Request $request)
|
||||||
|
{
|
||||||
|
$mailbox_id = $request->id ?? '';
|
||||||
|
$provider = $request->provider ?? '';
|
||||||
|
|
||||||
|
$mailbox = Mailbox::findOrFail($mailbox_id);
|
||||||
|
$this->authorize('admin', $mailbox);
|
||||||
|
|
||||||
|
// oAuth Disconnect.
|
||||||
|
$mailbox->removeMetaParam('oauth', true);
|
||||||
|
return \MailHelper::oauthDisconnect($provider, route('mailboxes.connection.incoming', ['id' => $mailbox_id]));
|
||||||
|
}
|
||||||
|
}
|
570
freescout-dist/app/Http/Controllers/ModulesController.php
Normal file
570
freescout-dist/app/Http/Controllers/ModulesController.php
Normal file
@ -0,0 +1,570 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Misc\WpApi;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
//use Nwidart\Modules\Traits\CanClearModulesCache;
|
||||||
|
use Symfony\Component\Console\Output\BufferedOutput;
|
||||||
|
|
||||||
|
class ModulesController extends Controller
|
||||||
|
{
|
||||||
|
//use CanClearModulesCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modules.
|
||||||
|
*/
|
||||||
|
public function modules(Request $request)
|
||||||
|
{
|
||||||
|
$installed_modules = [];
|
||||||
|
$modules_directory = [];
|
||||||
|
$third_party_modules = [];
|
||||||
|
$all_modules = [];
|
||||||
|
$flashes = [];
|
||||||
|
$updates_available = false;
|
||||||
|
|
||||||
|
$flash = \Cache::get('modules_flash');
|
||||||
|
if ($flash) {
|
||||||
|
if (is_array($flash) && !isset($flash['text'])) {
|
||||||
|
$flashes = $flash;
|
||||||
|
} else {
|
||||||
|
$flashes[] = $flash;
|
||||||
|
}
|
||||||
|
\Cache::forget('modules_flash');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get available modules and cache them
|
||||||
|
if (\Cache::has('modules_directory')) {
|
||||||
|
$modules_directory = \Cache::get('modules_directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$modules_directory) {
|
||||||
|
$modules_directory = WpApi::getModules();
|
||||||
|
if ($modules_directory && is_array($modules_directory) && count($modules_directory)) {
|
||||||
|
\Cache::put('modules_directory', $modules_directory, now()->addMinutes(15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get installed modules
|
||||||
|
\Module::clearCache();
|
||||||
|
$modules = \Module::all();
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
$module_data = [
|
||||||
|
'alias' => $module->getAlias(),
|
||||||
|
'name' => $module->getName(),
|
||||||
|
'description' => $module->getDescription(),
|
||||||
|
'version' => $module->get('version'),
|
||||||
|
'detailsUrl' => $module->get('detailsUrl'),
|
||||||
|
'author' => $module->get('author'),
|
||||||
|
'authorUrl' => $module->get('authorUrl'),
|
||||||
|
'requiredAppVersion' => $module->get('requiredAppVersion'),
|
||||||
|
'requiredPhpExtensions' => $module->get('requiredPhpExtensions'),
|
||||||
|
'requiredPhpExtensionsMissing' => \App\Module::getMissingExtensions($module->get('requiredPhpExtensions')),
|
||||||
|
'requiredModulesMissing' => \App\Module::getMissingModules($module->get('requiredModules'), $modules),
|
||||||
|
'img' => $module->get('img'),
|
||||||
|
'active' => $module->active(), //\App\Module::isActive($module->getAlias()),
|
||||||
|
'installed' => true,
|
||||||
|
'activated' => \App\Module::isLicenseActivated($module->getAlias(), $module->get('authorUrl')),
|
||||||
|
'license' => \App\Module::getLicense($module->getAlias()),
|
||||||
|
// Determined later
|
||||||
|
'new_version' => '',
|
||||||
|
];
|
||||||
|
$module_data = \App\Module::formatModuleData($module_data);
|
||||||
|
$installed_modules[] = $module_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need, as we update modules list on each page load
|
||||||
|
// Clear modules cache if any module has been added or removed
|
||||||
|
// if (count($modules) != count(Module::getCached())) {
|
||||||
|
// $this->clearCache();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Prepare directory modules
|
||||||
|
if (is_array($modules_directory)) {
|
||||||
|
foreach ($modules_directory as $i_dir => $dir_module) {
|
||||||
|
|
||||||
|
$modules_directory[$i_dir] = \App\Module::formatModuleData($dir_module);
|
||||||
|
|
||||||
|
// Remove modules without aliases
|
||||||
|
if (empty($dir_module['alias'])) {
|
||||||
|
unset($modules_directory[$i_dir]);
|
||||||
|
}
|
||||||
|
$all_modules[$dir_module['alias']] = $dir_module['name'];
|
||||||
|
foreach ($installed_modules as $i_installed => $module) {
|
||||||
|
if ($dir_module['alias'] == $module['alias']) {
|
||||||
|
// Set image from director
|
||||||
|
$installed_modules[$i_installed]['img'] = $dir_module['img'];
|
||||||
|
// Remove installed modules from modules directory.
|
||||||
|
unset($modules_directory[$i_dir]);
|
||||||
|
|
||||||
|
// Detect if new version is available
|
||||||
|
if (!empty($dir_module['version']) && version_compare($dir_module['version'], $module['version'], '>')) {
|
||||||
|
$installed_modules[$i_installed]['new_version'] = $dir_module['version'];
|
||||||
|
$updates_available = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($dir_module['authorUrl']) || !\App\Module::isOfficial($dir_module['authorUrl'])) {
|
||||||
|
unset($modules_directory[$i_dir]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($dir_module['requiredPhpExtensions'])) {
|
||||||
|
$modules_directory[$i_dir]['requiredPhpExtensionsMissing'] = \App\Module::getMissingExtensions($dir_module['requiredPhpExtensions']);
|
||||||
|
}
|
||||||
|
$modules_directory[$i_dir]['active'] = \App\Module::isActive($dir_module['alias']);
|
||||||
|
$modules_directory[$i_dir]['activated'] = false;
|
||||||
|
|
||||||
|
// Do not show third-party modules in Modules Derectory.
|
||||||
|
if (\App\Module::isThirdParty($dir_module)) {
|
||||||
|
$third_party_modules[] = $modules_directory[$i_dir];
|
||||||
|
unset($modules_directory[$i_dir]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$modules_directory = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check modules symlinks. Somestimes instead of symlinks folders with files appear.
|
||||||
|
|
||||||
|
$invalid_symlinks = \App\Module::checkSymlinks(
|
||||||
|
collect($installed_modules)->where('active', true)->pluck('alias')->toArray()
|
||||||
|
);
|
||||||
|
|
||||||
|
return view('modules/modules', [
|
||||||
|
'installed_modules' => $installed_modules,
|
||||||
|
'modules_directory' => $modules_directory,
|
||||||
|
'third_party_modules' => $third_party_modules,
|
||||||
|
'flashes' => $flashes,
|
||||||
|
'updates_available' => $updates_available,
|
||||||
|
'all_modules' => $all_modules,
|
||||||
|
'invalid_symlinks' => $invalid_symlinks,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajax.
|
||||||
|
*/
|
||||||
|
public function ajax(Request $request)
|
||||||
|
{
|
||||||
|
$response = [
|
||||||
|
'status' => 'error',
|
||||||
|
'msg' => '', // this is error message
|
||||||
|
];
|
||||||
|
|
||||||
|
switch ($request->action) {
|
||||||
|
|
||||||
|
case 'install':
|
||||||
|
case 'activate_license':
|
||||||
|
$license = $request->license;
|
||||||
|
$alias = $request->alias;
|
||||||
|
|
||||||
|
if (!$license) {
|
||||||
|
$response['msg'] = __('Empty license key');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$params = [
|
||||||
|
'license' => $license,
|
||||||
|
'module_alias' => $alias,
|
||||||
|
'url' => \App\Module::getAppUrl(),
|
||||||
|
];
|
||||||
|
$result = WpApi::activateLicense($params);
|
||||||
|
|
||||||
|
if (WpApi::$lastError) {
|
||||||
|
$response['msg'] = WpApi::$lastError['message'];
|
||||||
|
} elseif (!empty($result['code']) && !empty($result['message'])) {
|
||||||
|
$response['msg'] = $result['message'];
|
||||||
|
} else {
|
||||||
|
if (!empty($result['status']) && $result['status'] == 'valid') {
|
||||||
|
if ($request->action == 'install') {
|
||||||
|
// Download and install module
|
||||||
|
$license_details = WpApi::getVersion($params);
|
||||||
|
|
||||||
|
if (WpApi::$lastError) {
|
||||||
|
$response['msg'] = WpApi::$lastError['message'];
|
||||||
|
} elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
|
||||||
|
$response['msg'] = $license_details['message'];
|
||||||
|
} elseif (!empty($license_details['download_link'])) {
|
||||||
|
// Download module
|
||||||
|
$module_archive = \Module::getPath().DIRECTORY_SEPARATOR.$alias.'.zip';
|
||||||
|
|
||||||
|
try {
|
||||||
|
\Helper::downloadRemoteFile($license_details['download_link'], $module_archive);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response['msg'] = $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
$download_error = false;
|
||||||
|
if (!file_exists($module_archive)) {
|
||||||
|
$download_error = true;
|
||||||
|
} else {
|
||||||
|
// Extract
|
||||||
|
try {
|
||||||
|
\Helper::unzip($module_archive, \Module::getPath());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response['msg'] = $e->getMessage();
|
||||||
|
}
|
||||||
|
// Check if extracted module exists
|
||||||
|
\Module::clearCache();
|
||||||
|
$module = \Module::findByAlias($alias);
|
||||||
|
if (!$module) {
|
||||||
|
$download_error = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove archive
|
||||||
|
if (file_exists($module_archive)) {
|
||||||
|
\File::delete($module_archive);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg'] && !$download_error) {
|
||||||
|
// Activate license
|
||||||
|
\App\Module::activateLicense($alias, $license);
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Module successfully installed!'));
|
||||||
|
$response['status'] = 'success';
|
||||||
|
} elseif ($download_error) {
|
||||||
|
$response['reload'] = true;
|
||||||
|
|
||||||
|
if ($response['msg']) {
|
||||||
|
\Session::flash('flash_error_floating', $response['msg']);
|
||||||
|
}
|
||||||
|
|
||||||
|
\Session::flash('flash_error_unescaped', __('Error occurred downloading the module. Please :%a_being%download:%a_end% module manually and extract into :folder', ['%a_being%' => '<a href="'.$license_details['download_link'].'" target="_blank">', '%a_end%' => '</a>', 'folder' => '<strong>'.\Module::getPath().'</strong>']));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['msg'] = __('Error occurred. Please try again later.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Just activate license
|
||||||
|
\App\Module::activateLicense($alias, $license);
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('License successfully activated!'));
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
} elseif (!empty($result['error'])) {
|
||||||
|
$response['msg'] = \App\Module::getErrorMessage($result['error'], $result);
|
||||||
|
} else {
|
||||||
|
$response['msg'] = __('Error occurred. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'activate':
|
||||||
|
$alias = $request->alias;
|
||||||
|
$module = \Module::findByAlias($alias);
|
||||||
|
|
||||||
|
if (!$module) {
|
||||||
|
$response['msg'] = __('Module not found').': '.$alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check license
|
||||||
|
if (!$response['msg']) {
|
||||||
|
if (!empty($module->get('authorUrl')) && $module->isOfficial()) {
|
||||||
|
$params = [
|
||||||
|
'license' => $module->getLicense(),
|
||||||
|
'module_alias' => $alias,
|
||||||
|
'url' => \App\Module::getAppUrl(),
|
||||||
|
];
|
||||||
|
$license_result = WpApi::checkLicense($params);
|
||||||
|
|
||||||
|
if (!empty($license_result['code']) && !empty($license_result['message'])) {
|
||||||
|
// Remove remembered license key and deactivate license in DB
|
||||||
|
\App\Module::deactivateLicense($alias, '');
|
||||||
|
|
||||||
|
$response['msg'] = $license_result['message'];
|
||||||
|
} elseif (!empty($license_result['status']) && $license_result['status'] != 'valid' && $license_result['status'] != 'inactive') {
|
||||||
|
// Remove remembered license key and deactivate license in DB
|
||||||
|
\App\Module::deactivateLicense($alias, '');
|
||||||
|
|
||||||
|
switch ($license_result['status']) {
|
||||||
|
case 'expired':
|
||||||
|
$response['msg'] = __('License key has expired');
|
||||||
|
break;
|
||||||
|
case 'disabled':
|
||||||
|
$response['msg'] = __('License key has been revoked');
|
||||||
|
break;
|
||||||
|
case 'inactive':
|
||||||
|
$response['msg'] = __('License key has not been activated yet');
|
||||||
|
case 'site_inactive':
|
||||||
|
$response['msg'] = __('No activations left for this license key').' ('.__("Use 'Deactivate License' link above to transfer license key from another domain").')';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} elseif (!empty($license_result['status']) && $license_result['status'] == 'inactive') {
|
||||||
|
// Activate the license.
|
||||||
|
$result = WpApi::activateLicense($params);
|
||||||
|
if (WpApi::$lastError) {
|
||||||
|
$response['msg'] = WpApi::$lastError['message'];
|
||||||
|
} elseif (!empty($result['code']) && !empty($result['message'])) {
|
||||||
|
$response['msg'] = $result['message'];
|
||||||
|
} else {
|
||||||
|
if (!empty($result['status']) && $result['status'] == 'valid') {
|
||||||
|
// Success.
|
||||||
|
} elseif (!empty($result['error'])) {
|
||||||
|
$response['msg'] = \App\Module::getErrorMessage($result['error'], $result);
|
||||||
|
} else {
|
||||||
|
// Some unknown error. Do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
\App\Module::setActive($alias, true);
|
||||||
|
|
||||||
|
$outputLog = new BufferedOutput();
|
||||||
|
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $outputLog);
|
||||||
|
$output = $outputLog->fetch();
|
||||||
|
|
||||||
|
// Get module name
|
||||||
|
$name = '?';
|
||||||
|
if ($module) {
|
||||||
|
$name = $module->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = 'danger';
|
||||||
|
$msg = __('Error occurred activating ":name" module', ['name' => $name]);
|
||||||
|
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
|
||||||
|
// If there was any error, module has been deactivated via modules.register_error filter
|
||||||
|
$msg = '';
|
||||||
|
foreach (session('flashes_floating') as $flash) {
|
||||||
|
$msg .= $flash['text'].' ';
|
||||||
|
}
|
||||||
|
} elseif (strstr($output, 'Configuration cached successfully')) {
|
||||||
|
$type = 'success';
|
||||||
|
$msg = __('":name" module successfully activated!', ['name' => $name]);
|
||||||
|
} else {
|
||||||
|
// Deactivate the module.
|
||||||
|
\App\Module::setActive($alias, false);
|
||||||
|
\Artisan::call('freescout:clear-cache');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check public folder.
|
||||||
|
if ($module && file_exists($module->getPath().DIRECTORY_SEPARATOR.'Public')) {
|
||||||
|
$symlink_path = public_path().\Module::getPublicPath($alias);
|
||||||
|
if (!file_exists($symlink_path)) {
|
||||||
|
$type = 'danger';
|
||||||
|
$msg = 'Error occurred creating a module symlink ('.$symlink_path.'). Please check folder permissions.';
|
||||||
|
\App\Module::setActive($alias, false);
|
||||||
|
\Artisan::call('freescout:clear-cache');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type == 'success') {
|
||||||
|
// Migrate again, in case migration did not work in the moment the module was activated.
|
||||||
|
\Artisan::call('migrate', ['--force' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// \Session::flash does not work after BufferedOutput
|
||||||
|
$flash = [
|
||||||
|
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$output.'</pre>',
|
||||||
|
'unescaped' => true,
|
||||||
|
'type' => $type,
|
||||||
|
];
|
||||||
|
\Cache::forever('modules_flash', $flash);
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deactivate':
|
||||||
|
$alias = $request->alias;
|
||||||
|
\App\Module::setActive($alias, false);
|
||||||
|
|
||||||
|
$outputLog = new BufferedOutput();
|
||||||
|
\Artisan::call('freescout:clear-cache', [], $outputLog);
|
||||||
|
$output = $outputLog->fetch();
|
||||||
|
|
||||||
|
// Get module name
|
||||||
|
$module = \Module::findByAlias($alias);
|
||||||
|
$name = '?';
|
||||||
|
if ($module) {
|
||||||
|
$name = $module->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = 'danger';
|
||||||
|
$msg = __('Error occurred deactivating :name module', ['name' => $name]);
|
||||||
|
if (strstr($output, 'Configuration cached successfully')) {
|
||||||
|
$type = 'success';
|
||||||
|
$msg = __('":name" module successfully Deactivated!', ['name' => $name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// \Session::flash does not work after BufferedOutput
|
||||||
|
$flash = [
|
||||||
|
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$output.'</pre>',
|
||||||
|
'unescaped' => true,
|
||||||
|
'type' => $type,
|
||||||
|
];
|
||||||
|
\Cache::forever('modules_flash', $flash);
|
||||||
|
$response['status'] = 'success';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deactivate_license':
|
||||||
|
$license = $request->license;
|
||||||
|
$alias = $request->alias;
|
||||||
|
|
||||||
|
if (!$license) {
|
||||||
|
$response['msg'] = __('Empty license key');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$params = [
|
||||||
|
'license' => $license,
|
||||||
|
'module_alias' => $alias,
|
||||||
|
'url' => (!empty($request->any_url) ? '*' : \App\Module::getAppUrl()),
|
||||||
|
];
|
||||||
|
$result = WpApi::deactivateLicense($params);
|
||||||
|
|
||||||
|
if (WpApi::$lastError) {
|
||||||
|
$response['msg'] = WpApi::$lastError['message'];
|
||||||
|
} elseif (!empty($result['code']) && !empty($result['message'])) {
|
||||||
|
$response['msg'] = $result['message'];
|
||||||
|
} else {
|
||||||
|
if (!empty($result['status']) && $result['status'] == 'success') {
|
||||||
|
$db_module = \App\Module::getByAlias($alias);
|
||||||
|
if ($db_module && trim($db_module->license ?? '') == trim($license ?? '')) {
|
||||||
|
// Remove remembered license key and deactivate license in DB
|
||||||
|
\App\Module::deactivateLicense($alias, '');
|
||||||
|
|
||||||
|
// Deactivate module
|
||||||
|
\App\Module::setActive($alias, false);
|
||||||
|
\Artisan::call('freescout:clear-cache', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flash does not work here.
|
||||||
|
$flash = [
|
||||||
|
'text' => '<strong>'.__('License successfully Deactivated!').'</strong>',
|
||||||
|
'unescaped' => true,
|
||||||
|
'type' => 'success',
|
||||||
|
];
|
||||||
|
\Cache::forever('modules_flash', $flash);
|
||||||
|
|
||||||
|
$response['status'] = 'success';
|
||||||
|
} elseif (!empty($result['error'])) {
|
||||||
|
$response['msg'] = \App\Module::getErrorMessage($result['error'], $result);
|
||||||
|
} else {
|
||||||
|
$response['msg'] = __('Error occurred. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete':
|
||||||
|
$alias = $request->alias;
|
||||||
|
|
||||||
|
$module = \Module::findByAlias($alias);
|
||||||
|
|
||||||
|
if ($module) {
|
||||||
|
|
||||||
|
//\App\Module::deactivateLicense($alias, $license);
|
||||||
|
|
||||||
|
$module->delete();
|
||||||
|
\Session::flash('flash_success_floating', __('Module deleted'));
|
||||||
|
} else {
|
||||||
|
$response['msg'] = __('Module not found').': '.$alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response['status'] = 'success';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update':
|
||||||
|
$update_result = \App\Module::updateModule($request->alias);
|
||||||
|
|
||||||
|
if ($update_result['download_error']) {
|
||||||
|
$response['reload'] = true;
|
||||||
|
|
||||||
|
if ($update_result['msg']) {
|
||||||
|
\Session::flash('flash_error_floating', $update_result['msg']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($update_result['download_msg']) {
|
||||||
|
\Session::flash('flash_error_unescaped', $update_result['download_msg']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install updated module.
|
||||||
|
if ($update_result['output'] || $update_result['status']) {
|
||||||
|
|
||||||
|
$type = 'danger';
|
||||||
|
$msg = $update_result['msg'];
|
||||||
|
|
||||||
|
if ($update_result['status'] == 'success') {
|
||||||
|
$type = 'success';
|
||||||
|
$msg = $update_result['msg_success'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// \Session::flash does not work after BufferedOutput
|
||||||
|
$flash = [
|
||||||
|
'text' => '<strong>'.$msg.'</strong><pre class="margin-top">'.$update_result['output'].'</pre>',
|
||||||
|
'unescaped' => true,
|
||||||
|
'type' => $type,
|
||||||
|
];
|
||||||
|
\Cache::forever('modules_flash', $flash);
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update_all':
|
||||||
|
$update_all_flashes = [];
|
||||||
|
|
||||||
|
foreach ($request->aliases as $alias) {
|
||||||
|
$update_result = \App\Module::updateModule($alias);
|
||||||
|
|
||||||
|
$type = 'danger';
|
||||||
|
$msg = $update_result['msg'];
|
||||||
|
|
||||||
|
if ($update_result['status'] == 'success') {
|
||||||
|
$type = 'success';
|
||||||
|
$msg = $update_result['msg_success'];
|
||||||
|
} elseif ($update_result['download_msg']) {
|
||||||
|
$msg .= '<br/>'.$update_result['download_msg'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$text = '<strong>'.$update_result['module_name'].':</strong> '.$msg;
|
||||||
|
if (trim($update_result['output'])) {
|
||||||
|
$text .= '<pre class="margin-top">'.$update_result['output'].'</pre>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// \Session::flash does not work after BufferedOutput
|
||||||
|
$update_all_flashes[] = [
|
||||||
|
'text' => $text,
|
||||||
|
'unescaped' => true,
|
||||||
|
'type' => $type,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($update_all_flashes) {
|
||||||
|
\Cache::forever('modules_flash', $update_all_flashes);
|
||||||
|
}
|
||||||
|
$response['status'] = 'success';
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['msg'] = 'Unknown action';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||||
|
$response['msg'] = 'Unknown error occurred';
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Response::json($response);
|
||||||
|
}
|
||||||
|
}
|
228
freescout-dist/app/Http/Controllers/OpenController.php
Normal file
228
freescout-dist/app/Http/Controllers/OpenController.php
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Attachment;
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Option;
|
||||||
|
use App\Thread;
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Validator;
|
||||||
|
|
||||||
|
class OpenController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup user from invitation.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function userSetup($hash)
|
||||||
|
{
|
||||||
|
if (auth()->user()) {
|
||||||
|
return redirect()->route('dashboard');
|
||||||
|
}
|
||||||
|
$user = User::where('invite_hash', $hash)->first();
|
||||||
|
|
||||||
|
if ($user && $user->locale) {
|
||||||
|
\Helper::setLocale($user->locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('open/user_setup', ['user' => $user]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save user from invitation.
|
||||||
|
*/
|
||||||
|
public function userSetupSave($hash, Request $request)
|
||||||
|
{
|
||||||
|
if (auth()->user()) {
|
||||||
|
return redirect()->route('dashboard');
|
||||||
|
}
|
||||||
|
$user = User::where('invite_hash', $hash)->first();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'email' => 'required|string|email|max:100|unique:users,email,'.$user->id,
|
||||||
|
'password' => 'required|string|min:8|confirmed',
|
||||||
|
'job_title' => 'max:100',
|
||||||
|
'phone' => 'max:60',
|
||||||
|
'timezone' => 'required|string|max:255',
|
||||||
|
'time_format' => 'required',
|
||||||
|
'photo_url' => 'nullable|image|mimes:jpeg,png,jpg,gif',
|
||||||
|
]);
|
||||||
|
$validator->setAttributeNames([
|
||||||
|
'photo_url' => __('Photo'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Photo
|
||||||
|
$validator->after(function ($validator) use ($user, $request) {
|
||||||
|
if ($request->hasFile('photo_url')) {
|
||||||
|
$path_url = $user->savePhoto($request->file('photo_url'));
|
||||||
|
|
||||||
|
if ($path_url) {
|
||||||
|
$user->photo_url = $path_url;
|
||||||
|
} else {
|
||||||
|
$validator->errors()->add('photo_url', __('Error occurred processing the image. Make sure that PHP GD extension is enabled.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('user_setup', ['hash' => $hash])
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$request_data = $request->all();
|
||||||
|
// Do not allow user to set his role
|
||||||
|
if (isset($request_data['role'])) {
|
||||||
|
unset($request_data['role']);
|
||||||
|
}
|
||||||
|
if (isset($request_data['photo_url'])) {
|
||||||
|
unset($request_data['photo_url']);
|
||||||
|
}
|
||||||
|
$user->fill($request_data);
|
||||||
|
|
||||||
|
$user->password = bcrypt($request->password);
|
||||||
|
|
||||||
|
$user->invite_state = User::INVITE_STATE_ACTIVATED;
|
||||||
|
$user->invite_hash = '';
|
||||||
|
|
||||||
|
$user = \Eventy::filter('user.setup_save', $user, $request);
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
// Login user
|
||||||
|
Auth::guard()->login($user);
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Welcome to :company_name!', ['company_name' => Option::getCompanyName()]));
|
||||||
|
|
||||||
|
return redirect()->route('dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set a thread as read by customer
|
||||||
|
*/
|
||||||
|
public function setThreadAsRead($conversation_id, $thread_id)
|
||||||
|
{
|
||||||
|
$conversation = Conversation::findOrFail($conversation_id);
|
||||||
|
$thread = Thread::findOrFail($thread_id);
|
||||||
|
|
||||||
|
// We only track the first opening
|
||||||
|
if (empty($thread->opened_at)) {
|
||||||
|
$thread->opened_at = date('Y-m-d H:i:s');
|
||||||
|
$thread->save();
|
||||||
|
\Eventy::action('thread.opened', $thread, $conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a 1x1 ttransparent pixel and return it
|
||||||
|
$pixel = sprintf('%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c', 71, 73, 70, 56, 57, 97, 1, 0, 1, 0, 128, 255, 0, 192, 192, 192, 0, 0, 0, 33, 249, 4, 1, 0, 0, 0, 0, 44, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 68, 1, 0, 59);
|
||||||
|
$response = \Response::make($pixel, 200);
|
||||||
|
$response->header('Content-type', 'image/gif');
|
||||||
|
$response->header('Content-Length', 42);
|
||||||
|
$response->header('Cache-Control', 'private, no-cache, no-cache=Set-Cookie, proxy-revalidate');
|
||||||
|
$response->header('Expires', 'Wed, 11 Jan 2000 12:59:00 GMT');
|
||||||
|
$response->header('Last-Modified', 'Wed, 11 Jan 2006 12:59:00 GMT');
|
||||||
|
$response->header('Pragma', 'no-cache');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download an attachment.
|
||||||
|
*/
|
||||||
|
public function downloadAttachment($dir_1, $dir_2, $dir_3, $file_name, Request $request)
|
||||||
|
{
|
||||||
|
$id = $request->query('id', '');
|
||||||
|
$token = $request->query('token', '');
|
||||||
|
$attachment = null;
|
||||||
|
|
||||||
|
// Old attachments can not be requested by id.
|
||||||
|
if (!$token && $id) {
|
||||||
|
return \Helper::denyAccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get attachment by id.
|
||||||
|
if ($id) {
|
||||||
|
$attachment = Attachment::findOrFail($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$attachment) {
|
||||||
|
$attachment = Attachment::where('file_dir', $dir_1.DIRECTORY_SEPARATOR.$dir_2.DIRECTORY_SEPARATOR.$dir_3.DIRECTORY_SEPARATOR)
|
||||||
|
->where('file_name', $file_name)
|
||||||
|
->firstOrFail();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow download if the attachment is public or if the token matches the hash of the contents
|
||||||
|
if ($token != $attachment->getToken() && (bool)$attachment->public !== true) {
|
||||||
|
return \Helper::denyAccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
$view_attachment = false;
|
||||||
|
$file_ext = strtolower(pathinfo($attachment->file_name, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
// Some file type should be viewed in the browser instead of downloading.
|
||||||
|
if (in_array($file_ext, config('app.viewable_attachments'))) {
|
||||||
|
$view_attachment = true;
|
||||||
|
}
|
||||||
|
// If HTML file is renamed into .txt for example it will be shown by the browser as HTML.
|
||||||
|
if ($view_attachment && $attachment->mime_type) {
|
||||||
|
$allowed_mime_type = false;
|
||||||
|
|
||||||
|
foreach (config('app.viewable_mime_types') as $mime_type) {
|
||||||
|
if (preg_match('#'.$mime_type.'#', $attachment->mime_type)) {
|
||||||
|
$allowed_mime_type = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$allowed_mime_type) {
|
||||||
|
$view_attachment = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config('app.download_attachments_via') == 'apache') {
|
||||||
|
// Send using Apache mod_xsendfile.
|
||||||
|
$response = response(null)
|
||||||
|
->header('Content-Type' , $attachment->mime_type)
|
||||||
|
->header('X-Sendfile', $attachment->getLocalFilePath());
|
||||||
|
|
||||||
|
if (!$view_attachment) {
|
||||||
|
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
|
||||||
|
}
|
||||||
|
} elseif (config('app.download_attachments_via') == 'nginx') {
|
||||||
|
// Send using Nginx.
|
||||||
|
$response = response(null)
|
||||||
|
->header('Content-Type' , $attachment->mime_type)
|
||||||
|
->header('X-Accel-Redirect', $attachment->getLocalFilePath(false));
|
||||||
|
|
||||||
|
if (!$view_attachment) {
|
||||||
|
$response->header('Content-Disposition', 'attachment; filename="'.$attachment->file_name.'"');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response = $attachment->download($view_attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Needed for the mobile app.
|
||||||
|
*/
|
||||||
|
// public function mobilePing()
|
||||||
|
// {
|
||||||
|
// echo file_get_contents(public_path('installer/css/fontawesome.css'));
|
||||||
|
// }
|
||||||
|
}
|
236
freescout-dist/app/Http/Controllers/SecureController.php
Normal file
236
freescout-dist/app/Http/Controllers/SecureController.php
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\ActivityLog;
|
||||||
|
use App\Misc\Helper;
|
||||||
|
use App\SendLog;
|
||||||
|
use App\Thread;
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SecureController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the application dashboard.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function dashboard()
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
if (!$user->isAdmin()) {
|
||||||
|
$mailboxes = $user->mailboxesCanView();
|
||||||
|
} else {
|
||||||
|
$mailboxes = $user->mailboxesCanViewWithSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by name.
|
||||||
|
$mailboxes = \Eventy::filter('dashboard.mailboxes', $mailboxes->sortBy('name'));
|
||||||
|
|
||||||
|
return view('secure/dashboard', ['mailboxes' => $mailboxes]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function logs(Request $request)
|
||||||
|
{
|
||||||
|
function addCol($cols, $col)
|
||||||
|
{
|
||||||
|
if (!in_array($col, $cols)) {
|
||||||
|
$cols[] = $col;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to check permissions here, as they are checked in routing
|
||||||
|
|
||||||
|
$names = ActivityLog::select('log_name')->distinct()->pluck('log_name')->toArray();
|
||||||
|
|
||||||
|
$activities = [];
|
||||||
|
$cols = [];
|
||||||
|
$page_size = 20;
|
||||||
|
$name = '';
|
||||||
|
|
||||||
|
if (!empty($request->name)) {
|
||||||
|
$activities = ActivityLog::inLog($request->name)->orderBy('created_at', 'desc')->paginate($page_size);
|
||||||
|
$name = $request->name;
|
||||||
|
} elseif (count($names)) {
|
||||||
|
$name = ActivityLog::NAME_OUT_EMAILS;
|
||||||
|
// $activities = ActivityLog::inLog($names[0])->orderBy('created_at', 'desc')->paginate($page_size);
|
||||||
|
// $name = $names[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($name != ActivityLog::NAME_OUT_EMAILS) {
|
||||||
|
$logs = [];
|
||||||
|
$cols = ['date'];
|
||||||
|
foreach ($activities as $activity) {
|
||||||
|
$log = [];
|
||||||
|
$log['date'] = $activity->created_at;
|
||||||
|
if ($activity->causer) {
|
||||||
|
if ($activity->causer_type == 'App\User') {
|
||||||
|
$cols = addCol($cols, 'user');
|
||||||
|
$log['user'] = $activity->causer;
|
||||||
|
} else {
|
||||||
|
$cols = addCol($cols, 'customer');
|
||||||
|
$log['customer'] = $activity->causer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$log['event'] = $activity->getEventDescription();
|
||||||
|
|
||||||
|
$cols = addCol($cols, 'event');
|
||||||
|
|
||||||
|
foreach ($activity->properties as $property_name => $property_value) {
|
||||||
|
if (!is_string($property_value)) {
|
||||||
|
$property_value = json_encode($property_value);
|
||||||
|
}
|
||||||
|
$log[$property_name] = $property_value;
|
||||||
|
$cols = addCol($cols, $property_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs[] = $log;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Outgoing emails are displayed from send log
|
||||||
|
$logs = [];
|
||||||
|
$cols = [
|
||||||
|
'date',
|
||||||
|
'type',
|
||||||
|
'email',
|
||||||
|
'status',
|
||||||
|
'message',
|
||||||
|
'user',
|
||||||
|
'customer',
|
||||||
|
];
|
||||||
|
|
||||||
|
$activities_query = SendLog::orderBy('created_at', 'desc');
|
||||||
|
if ($request->get('thread_id')) {
|
||||||
|
$activities_query->where('thread_id', $request->get('thread_id'));
|
||||||
|
}
|
||||||
|
$activities = $activities_query->paginate($page_size);
|
||||||
|
|
||||||
|
foreach ($activities as $record) {
|
||||||
|
$conversation = '';
|
||||||
|
if ($record->thread_id) {
|
||||||
|
$conversation = Thread::find($record->thread_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = $record->getStatusName();
|
||||||
|
if ($record->status_message) {
|
||||||
|
$status .= '. '.$record->status_message;
|
||||||
|
if ($record->status == SendLog::STATUS_SEND_ERROR) {
|
||||||
|
$status .= '. Message-ID: '.$record->message_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($record->smtp_queue_id) {
|
||||||
|
$status .= '. SMTP ID: '.$record->smtp_queue_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs[] = [
|
||||||
|
'date' => $record->created_at,
|
||||||
|
'type' => $record->getMailTypeName(),
|
||||||
|
'email' => $record->email,
|
||||||
|
'status' => $status,
|
||||||
|
'message' => $conversation,
|
||||||
|
'user' => $record->user,
|
||||||
|
'customer' => $record->customer,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
array_unshift($names, ActivityLog::NAME_OUT_EMAILS);
|
||||||
|
array_push($names, ActivityLog::NAME_APP_LOGS);
|
||||||
|
|
||||||
|
if (!in_array($name, $names)) {
|
||||||
|
$names[] = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('secure/logs', [
|
||||||
|
'logs' => $logs,
|
||||||
|
'names' => $names,
|
||||||
|
'current_name' => $name,
|
||||||
|
'cols' => $cols,
|
||||||
|
'activities' => $activities,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs page submitted.
|
||||||
|
*/
|
||||||
|
public function logsSubmit(Request $request)
|
||||||
|
{
|
||||||
|
// No need to check permissions here, as they are checked in routing
|
||||||
|
|
||||||
|
$name = '';
|
||||||
|
if (!empty($request->name)) {
|
||||||
|
//$activities = ActivityLog::inLog($request->name)->orderBy('created_at', 'desc')->get();
|
||||||
|
$name = $request->name;
|
||||||
|
} elseif (count($names = ActivityLog::select('log_name')->distinct()->get()->pluck('log_name'))) {
|
||||||
|
$name = ActivityLog::NAME_OUT_EMAILS;
|
||||||
|
// $activities = ActivityLog::inLog($names[0])->orderBy('created_at', 'desc')->get();
|
||||||
|
// $name = $names[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($request->action) {
|
||||||
|
case 'clean':
|
||||||
|
if ($name && $name != ActivityLog::NAME_OUT_EMAILS) {
|
||||||
|
ActivityLog::where('log_name', $name)->delete();
|
||||||
|
\Session::flash('flash_success_floating', __('Log successfully cleared'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('logs', ['name' => $name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload files and images.
|
||||||
|
*/
|
||||||
|
public function upload(Request $request, $allowed_exts = [])
|
||||||
|
{
|
||||||
|
// 'jpg','gif','png'
|
||||||
|
$response = [
|
||||||
|
'status' => 'error',
|
||||||
|
'msg' => '', // this is error message
|
||||||
|
];
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
$response['msg'] = __('Please login to upload file');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$request->hasFile('file') || !$request->file('file')->isValid() || !$request->file) {
|
||||||
|
$response['msg'] = __('Error occurred uploading file');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
|
||||||
|
$upload = Helper::uploadFile($request->file, $allowed_exts);
|
||||||
|
$filename = basename($upload);
|
||||||
|
|
||||||
|
if ($upload) {
|
||||||
|
$response['status'] = 'success';
|
||||||
|
$response['url'] = Helper::uploadedFileUrl($filename);
|
||||||
|
} else {
|
||||||
|
$response['msg'] = __('Error occurred uploading file');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Response::json($response);
|
||||||
|
}
|
||||||
|
}
|
431
freescout-dist/app/Http/Controllers/SettingsController.php
Normal file
431
freescout-dist/app/Http/Controllers/SettingsController.php
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Conversation;
|
||||||
|
use App\Option;
|
||||||
|
use App\Subscription;
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Validator;
|
||||||
|
|
||||||
|
class SettingsController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General settings.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function view($section = 'general')
|
||||||
|
{
|
||||||
|
$settings = $this->getSectionSettings($section);
|
||||||
|
|
||||||
|
if (!$settings) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sections = $this->getSections();
|
||||||
|
|
||||||
|
$template_vars = [
|
||||||
|
'settings' => $settings,
|
||||||
|
'section' => $section,
|
||||||
|
'sections' => $this->getSections(),
|
||||||
|
'section_name' => $sections[$section]['title'],
|
||||||
|
];
|
||||||
|
$template_vars = $this->getTemplateVars($section, $template_vars);
|
||||||
|
|
||||||
|
return view('settings/view', $template_vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getValidator($section)
|
||||||
|
{
|
||||||
|
$rules = $this->getSectionParams($section, 'validator_rules');
|
||||||
|
|
||||||
|
if (!empty($rules)) {
|
||||||
|
return Validator::make(request()->all(), $rules);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTemplateVars($section, $template_vars)
|
||||||
|
{
|
||||||
|
$section_vars = $this->getSectionParams($section, 'template_vars');
|
||||||
|
|
||||||
|
if ($section_vars && is_array($section_vars)) {
|
||||||
|
return array_merge($template_vars, $section_vars);
|
||||||
|
} else {
|
||||||
|
return $template_vars;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters of the sections settings.
|
||||||
|
*
|
||||||
|
* If in settings parameter `env` is set, option will be saved into .env file
|
||||||
|
* instead of DB.
|
||||||
|
*
|
||||||
|
* @param [type] $section [description]
|
||||||
|
* @param string $param [description]
|
||||||
|
*
|
||||||
|
* @return [type] [description]
|
||||||
|
*/
|
||||||
|
public function getSectionParams($section, $param = '')
|
||||||
|
{
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
switch ($section) {
|
||||||
|
case 'emails':
|
||||||
|
$params = [
|
||||||
|
'template_vars' => [
|
||||||
|
'sendmail_path' => ini_get('sendmail_path'),
|
||||||
|
'mail_drivers' => [
|
||||||
|
'mail' => __("PHP's mail() function"),
|
||||||
|
'sendmail' => __('Sendmail'),
|
||||||
|
'smtp' => 'SMTP',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'validator_rules' => [
|
||||||
|
'settings.mail_from' => 'required|email',
|
||||||
|
],
|
||||||
|
'settings' => [
|
||||||
|
'fetch_schedule' => [
|
||||||
|
'env' => 'APP_FETCH_SCHEDULE',
|
||||||
|
],
|
||||||
|
'mail_password' => [
|
||||||
|
'safe_password' => true,
|
||||||
|
'encrypt' => true,
|
||||||
|
],
|
||||||
|
// 'use_mail_date_on_fetching' => [
|
||||||
|
// 'env' => 'APP_USE_MAIL_DATE_ON_FETCHING',
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'general':
|
||||||
|
$params = [
|
||||||
|
'settings' => [
|
||||||
|
'custom_number' => [
|
||||||
|
'env' => 'APP_CUSTOM_NUMBER',
|
||||||
|
],
|
||||||
|
'max_message_size' => [
|
||||||
|
'env' => 'APP_MAX_MESSAGE_SIZE',
|
||||||
|
],
|
||||||
|
'email_conv_history' => [
|
||||||
|
'env' => 'APP_EMAIL_CONV_HISTORY',
|
||||||
|
],
|
||||||
|
'email_user_history' => [
|
||||||
|
'env' => 'APP_EMAIL_USER_HISTORY',
|
||||||
|
],
|
||||||
|
'locale' => [
|
||||||
|
'env' => 'APP_LOCALE',
|
||||||
|
],
|
||||||
|
'timezone' => [
|
||||||
|
'env' => 'APP_TIMEZONE',
|
||||||
|
],
|
||||||
|
'user_permissions' => [
|
||||||
|
'env' => 'APP_USER_PERMISSIONS',
|
||||||
|
'env_encode' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'alerts':
|
||||||
|
$subscriptions_defaults = Subscription::getDefaultSubscriptions();
|
||||||
|
$subscriptions = array();
|
||||||
|
foreach ($subscriptions_defaults as $medium => $subscriptions_for_medium) {
|
||||||
|
foreach ($subscriptions_defaults[$medium] as $subscription) {
|
||||||
|
$subscriptions[] = (object) array("medium" => $medium, "event" => $subscription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$params = [
|
||||||
|
'template_vars' => [
|
||||||
|
'logs' => \App\ActivityLog::getAvailableLogs(),
|
||||||
|
'person' => null,
|
||||||
|
'subscriptions' => $subscriptions,
|
||||||
|
'mobile_available' => \Eventy::filter('notifications.mobile_available', false),
|
||||||
|
],
|
||||||
|
'settings' => [
|
||||||
|
'alert_logs' => [
|
||||||
|
'env' => 'APP_ALERT_LOGS',
|
||||||
|
],
|
||||||
|
'alert_logs_period' => [
|
||||||
|
'env' => 'APP_ALERT_LOGS_PERIOD',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// todo: monitor App Logs
|
||||||
|
foreach ($params['template_vars']['logs'] as $i => $log) {
|
||||||
|
if ($log == \App\ActivityLog::NAME_APP_LOGS || $log == \App\ActivityLog::NAME_OUT_EMAILS) {
|
||||||
|
unset($params['template_vars']['logs'][$i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$params = \Eventy::filter('settings.section_params', $params, $section);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = \Eventy::filter('settings.alter_section_params', $params, $section);
|
||||||
|
|
||||||
|
if ($param) {
|
||||||
|
if (isset($params[$param])) {
|
||||||
|
return $params[$param];
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSectionSettings($section)
|
||||||
|
{
|
||||||
|
$settings = [];
|
||||||
|
|
||||||
|
switch ($section) {
|
||||||
|
case 'general':
|
||||||
|
$settings = [
|
||||||
|
'company_name' => Option::get('company_name', \Config::get('app.name')),
|
||||||
|
'next_ticket' => (Option::get('next_ticket') >= Conversation::max('number') + 1) ? Option::get('next_ticket') : Conversation::max('number') + 1,
|
||||||
|
'custom_number' => (int)config('app.custom_number'),
|
||||||
|
'user_permissions' => User::getGlobalUserPermissions(),
|
||||||
|
'email_branding' => Option::get('email_branding'),
|
||||||
|
'open_tracking' => Option::get('open_tracking'),
|
||||||
|
'email_conv_history' => config('app.email_conv_history'),
|
||||||
|
'max_message_size' => config('app.max_message_size'),
|
||||||
|
'email_user_history' => config('app.email_user_history'),
|
||||||
|
'enrich_customer_data' => Option::get('enrich_customer_data'),
|
||||||
|
'time_format' => Option::get('time_format', User::TIME_FORMAT_24),
|
||||||
|
'locale' => \Helper::getRealAppLocale(),
|
||||||
|
'timezone' => config('app.timezone'),
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'emails':
|
||||||
|
$settings = [
|
||||||
|
'mail_from' => \App\Misc\Mail::getSystemMailFrom(),
|
||||||
|
'mail_driver' => Option::get('mail_driver', \Config::get('mail.driver')),
|
||||||
|
'mail_host' => Option::get('mail_host', \Config::get('mail.host')),
|
||||||
|
'mail_port' => Option::get('mail_port', \Config::get('mail.port')),
|
||||||
|
'mail_username' => Option::get('mail_username', \Config::get('mail.username')),
|
||||||
|
'mail_password' => \Helper::decrypt(Option::get('mail_password', \Config::get('mail.password'))),
|
||||||
|
'mail_encryption' => Option::get('mail_encryption', \Config::get('mail.encryption')),
|
||||||
|
'fetch_schedule' => config('app.fetch_schedule'),
|
||||||
|
//'use_mail_date_on_fetching' => config('app.use_mail_date_on_fetching'),
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'alerts':
|
||||||
|
$settings = Option::getOptions([
|
||||||
|
'alert_recipients',
|
||||||
|
'alert_fetch',
|
||||||
|
'alert_fetch_period',
|
||||||
|
'alert_logs',
|
||||||
|
'alert_logs_names',
|
||||||
|
'alert_logs_period',
|
||||||
|
'subscription_defaults',
|
||||||
|
], [
|
||||||
|
'alert_logs_names' => [],
|
||||||
|
'alert_logs' => config('app.alert_logs'),
|
||||||
|
'alert_logs_period' => config('app.alert_logs_period'),
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$settings = \Eventy::filter('settings.section_settings', $settings, $section);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = \Eventy::filter('settings.alter_section_settings', $settings, $section);
|
||||||
|
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSections()
|
||||||
|
{
|
||||||
|
$sections = [
|
||||||
|
// todo: order
|
||||||
|
'general' => ['title' => __('General'), 'icon' => 'cog', 'order' => 100],
|
||||||
|
'emails' => ['title' => __('Mail Settings'), 'icon' => 'transfer', 'order' => 200],
|
||||||
|
'alerts' => ['title' => __('Alerts'), 'icon' => 'bell', 'order' => 300],
|
||||||
|
];
|
||||||
|
$sections = \Eventy::filter('settings.sections', $sections);
|
||||||
|
|
||||||
|
return $sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save general settings.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*/
|
||||||
|
public function save($section = 'general')
|
||||||
|
{
|
||||||
|
$settings = $this->getSectionSettings($section);
|
||||||
|
|
||||||
|
if (!$settings) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->processSave($section, array_keys($settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processSave($section, $settings)
|
||||||
|
{
|
||||||
|
// Validate
|
||||||
|
$validator = $this->getValidator($section);
|
||||||
|
|
||||||
|
if ($validator && $validator->fails()) {
|
||||||
|
return redirect()->route('settings', ['section' => $section])
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = request();
|
||||||
|
|
||||||
|
$request = \Eventy::filter('settings.before_save', $request, $section, $settings);
|
||||||
|
|
||||||
|
$cc_required = false;
|
||||||
|
$settings_params = $this->getSectionParams($section, 'settings');
|
||||||
|
foreach ($settings as $i => $option_name) {
|
||||||
|
// Do not save dummy passwords.
|
||||||
|
if (!empty($settings_params[$option_name])
|
||||||
|
&& !empty($settings_params[$option_name]['safe_password'])
|
||||||
|
&& $request->settings[$option_name]
|
||||||
|
&& preg_match("/^\*+$/", $request->settings[$option_name])
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option has to be saved to .env file.
|
||||||
|
if (!empty($settings_params[$option_name]) && !empty($settings_params[$option_name]['env'])) {
|
||||||
|
$env_value = $request->settings[$option_name] ?? '';
|
||||||
|
|
||||||
|
if (is_array($env_value)) {
|
||||||
|
$env_value = json_encode($env_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($settings_params[$option_name]['encrypt'])) {
|
||||||
|
$env_value = encrypt($env_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($settings_params[$option_name]['env_encode'])) {
|
||||||
|
$env_value = base64_encode($env_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
\Helper::setEnvFileVar($settings_params[$option_name]['env'], $env_value);
|
||||||
|
|
||||||
|
config($option_name, $env_value);
|
||||||
|
$cc_required = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// By some reason isset() does not work for empty elements.
|
||||||
|
if (isset($request->settings) && array_key_exists($option_name, $request->settings)) {
|
||||||
|
$option_value = $request->settings[$option_name];
|
||||||
|
|
||||||
|
if (!empty($settings_params[$option_name]['encrypt'])) {
|
||||||
|
$option_value = encrypt($option_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Option::set($option_name, $option_value);
|
||||||
|
} else {
|
||||||
|
// If option does not exist, default will be used,
|
||||||
|
// so we can not just remove bool settings.
|
||||||
|
if (isset($settings_params[$option_name]['default'])) {
|
||||||
|
$default = $settings_params[$option_name]['default'];
|
||||||
|
} else {
|
||||||
|
$default = \Option::getDefault($option_name, null);
|
||||||
|
}
|
||||||
|
if ($default === true) {
|
||||||
|
Option::set($option_name, false);
|
||||||
|
} elseif (is_array(\Option::getDefault($option_name, -1))) {
|
||||||
|
Option::set($option_name, []);
|
||||||
|
} else {
|
||||||
|
Option::remove($option_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cache if some options have been saved to .env file.
|
||||||
|
// Clearing the cache also restarts queue:work as it also
|
||||||
|
// needs to get new .env parameters.
|
||||||
|
if ($cc_required) {
|
||||||
|
\Helper::clearCache(['--doNotGenerateVars' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// \Helper::clearCache prevents \Session::flash() from displaying.
|
||||||
|
$request->session()->flash('flash_success_floating', __('Settings updated'));
|
||||||
|
|
||||||
|
$response = redirect()->route('settings', ['section' => $section]);
|
||||||
|
|
||||||
|
$response = \Eventy::filter('settings.after_save', $response, $request, $section, $settings);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Users ajax controller.
|
||||||
|
*/
|
||||||
|
public function ajax(Request $request)
|
||||||
|
{
|
||||||
|
$response = [
|
||||||
|
'status' => 'error',
|
||||||
|
'msg' => '', // this is error message
|
||||||
|
];
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
switch ($request->action) {
|
||||||
|
|
||||||
|
// Test sending emails from mailbox
|
||||||
|
case 'send_test':
|
||||||
|
|
||||||
|
if (empty($request->to)) {
|
||||||
|
$response['msg'] = __('Please specify recipient of the test email');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$test_result = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$test_result = \MailHelper::sendTestMail($request->to);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response['msg'] = $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$test_result && !$response['msg']) {
|
||||||
|
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember email address
|
||||||
|
if (!empty($request->to)) {
|
||||||
|
\App\Option::set('send_test_to', $request->to);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['msg'] = 'Unknown action';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||||
|
$response['msg'] = 'Unknown error occurred';
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Response::json($response);
|
||||||
|
}
|
||||||
|
}
|
416
freescout-dist/app/Http/Controllers/SystemController.php
Normal file
416
freescout-dist/app/Http/Controllers/SystemController.php
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Option;
|
||||||
|
use App\User;
|
||||||
|
use App\FailedJob;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\Console\Output\BufferedOutput;
|
||||||
|
|
||||||
|
class SystemController extends Controller
|
||||||
|
{
|
||||||
|
public static $latest_version_error = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth', ['except' => [
|
||||||
|
'cron'
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System status.
|
||||||
|
*/
|
||||||
|
public function status(Request $request)
|
||||||
|
{
|
||||||
|
// PHP extensions.
|
||||||
|
$php_extensions = \Helper::checkRequiredExtensions();
|
||||||
|
|
||||||
|
// Functions.
|
||||||
|
$functions = \Helper::checkRequiredFunctions();
|
||||||
|
|
||||||
|
// Permissions.
|
||||||
|
$permissions = [];
|
||||||
|
foreach (config('installer.permissions') as $perm_path => $perm_value) {
|
||||||
|
$path = base_path($perm_path);
|
||||||
|
$value = '';
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$value = substr(sprintf('%o', fileperms($path)), -4);
|
||||||
|
}
|
||||||
|
$permissions[$perm_path] = [
|
||||||
|
'status' => \Helper::isFolderWritable($path),
|
||||||
|
'value' => $value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if cache files are writable.
|
||||||
|
$non_writable_cache_file = '';
|
||||||
|
if (function_exists('shell_exec')) {
|
||||||
|
$non_writable_cache_file = shell_exec('find '.base_path('storage/framework/cache/data/').' -type f | xargs -I {} sh -c \'[ ! -w "{}" ] && echo {}\' 2>&1 | head -n 1');
|
||||||
|
$non_writable_cache_file = trim($non_writable_cache_file ?? '');
|
||||||
|
// Leave only one line (in case head -n 1 does not work)
|
||||||
|
$non_writable_cache_file = preg_replace("#[\r\n].+#m", '', $non_writable_cache_file);
|
||||||
|
if (!strstr($non_writable_cache_file, base_path('storage/framework/cache/data/'))) {
|
||||||
|
$non_writable_cache_file = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Check if public symlink exists, if not, try to create.
|
||||||
|
$public_symlink_exists = true;
|
||||||
|
$public_path = public_path('storage');
|
||||||
|
$public_test = $public_path.DIRECTORY_SEPARATOR.'.gitignore';
|
||||||
|
|
||||||
|
if (!file_exists($public_test) || !file_get_contents($public_test)) {
|
||||||
|
\File::delete($public_path);
|
||||||
|
\Artisan::call('storage:link');
|
||||||
|
if (!file_exists($public_test) || !file_get_contents($public_test)) {
|
||||||
|
$public_symlink_exists = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if .env is writable.
|
||||||
|
$env_is_writable = is_writable(base_path('.env'));
|
||||||
|
|
||||||
|
// Jobs
|
||||||
|
$queued_jobs = \App\Job::orderBy('created_at', 'desc')->get();
|
||||||
|
$failed_jobs = \App\FailedJob::orderBy('failed_at', 'desc')->get();
|
||||||
|
$failed_queues = $failed_jobs->pluck('queue')->unique();
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
$commands_list = [
|
||||||
|
'freescout:fetch-emails' => 'freescout:fetch-emails',
|
||||||
|
\Helper::getWorkerIdentifier() => 'queue:work'
|
||||||
|
];
|
||||||
|
foreach ($commands_list as $command_identifier => $command_name) {
|
||||||
|
$status_texts = [];
|
||||||
|
|
||||||
|
// Check if command is running now
|
||||||
|
if (function_exists('shell_exec')) {
|
||||||
|
$running_commands = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '{$command_identifier}'"));
|
||||||
|
$pids = [];
|
||||||
|
foreach ($processes as $process) {
|
||||||
|
$process = trim($process);
|
||||||
|
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
|
||||||
|
if (empty($m)) {
|
||||||
|
// Another format (used in Docker image).
|
||||||
|
// 1713 nginx 0:00 /usr/bin/php82...
|
||||||
|
preg_match("/^([\d]+)\s+[\S]+\s+/", $process, $m);
|
||||||
|
}
|
||||||
|
if (!preg_match("/(sh \-c|grep )/", $process) && !empty($m[1])) {
|
||||||
|
$running_commands++;
|
||||||
|
$pids[] = $m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
if ($running_commands == 1) {
|
||||||
|
$commands[] = [
|
||||||
|
'name' => $command_name,
|
||||||
|
'status' => 'success',
|
||||||
|
'status_text' => __('Running'),
|
||||||
|
];
|
||||||
|
continue;
|
||||||
|
} elseif ($running_commands > 1) {
|
||||||
|
// queue:work command is stopped by settings a cache key
|
||||||
|
if ($command_name == 'queue:work') {
|
||||||
|
\Helper::queueWorkerRestart();
|
||||||
|
$commands[] = [
|
||||||
|
'name' => $command_name,
|
||||||
|
'status' => 'error',
|
||||||
|
'status_text' => __(':number commands were running at the same time. Commands have been restarted', ['number' => $running_commands]),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
unset($pids[0]);
|
||||||
|
$commands[] = [
|
||||||
|
'name' => $command_name,
|
||||||
|
'status' => 'error',
|
||||||
|
'status_text' => __(':number commands are running at the same time. Please stop extra commands by executing the following console command:', ['number' => $running_commands]).' kill '.implode(' | kill ', $pids),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check last run
|
||||||
|
$option_name = str_replace('freescout_', '', preg_replace('/[^a-zA-Z0-9]/', '_', $command_name));
|
||||||
|
|
||||||
|
$date_text = '?';
|
||||||
|
$last_run = Option::get($option_name.'_last_run');
|
||||||
|
if ($last_run) {
|
||||||
|
$date = Carbon::createFromTimestamp($last_run);
|
||||||
|
$date_text = User::dateFormat($date);
|
||||||
|
}
|
||||||
|
$status_texts[] = __('Last run:').' '.$date_text;
|
||||||
|
|
||||||
|
$date_text = '?';
|
||||||
|
$last_successful_run = Option::get($option_name.'_last_successful_run');
|
||||||
|
if ($last_successful_run) {
|
||||||
|
$date_ = Carbon::createFromTimestamp($last_successful_run);
|
||||||
|
$date_text = User::dateFormat($date);
|
||||||
|
}
|
||||||
|
$status_texts[] = __('Last successful run:').' '.$date_text;
|
||||||
|
|
||||||
|
$status = 'error';
|
||||||
|
if ($last_successful_run && $last_run && (int) $last_successful_run >= (int) $last_run) {
|
||||||
|
unset($status_texts[0]);
|
||||||
|
$status = 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If queue:work is not running, clear cache to let it start if something is wrong with the mutex
|
||||||
|
if ($command_name == 'queue:work' && !$last_successful_run) {
|
||||||
|
$status_texts[] = __('Try to :%a_start%clear cache:%a_end% to force command to start.', ['%a_start%' => '<a href="'.route('system.tools').'" target="_blank">', '%a_end%' => '</a>']);
|
||||||
|
// This sometimes makes Status page open as non logged in user.
|
||||||
|
//\Artisan::call('freescout:clear-cache', ['--doNotGenerateVars' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$commands[] = [
|
||||||
|
'name' => $command_name,
|
||||||
|
'status' => $status,
|
||||||
|
'status_text' => implode(' ', $status_texts),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check new version if enabled
|
||||||
|
$new_version_available = false;
|
||||||
|
if (!\Config::get('app.disable_updating')) {
|
||||||
|
$latest_version = \Cache::remember('latest_version', 15, function () {
|
||||||
|
try {
|
||||||
|
return \Updater::getVersionAvailable();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
SystemController::$latest_version_error = $e->getMessage();
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($latest_version && version_compare($latest_version, \Config::get('app.version'), '>')) {
|
||||||
|
$new_version_available = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$latest_version = \Config::get('app.version');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect missing migrations.
|
||||||
|
$migrations_output = \Helper::runCommand('migrate:status');
|
||||||
|
preg_match_all("#\| N \| ([^\|]+)\|#", $migrations_output, $migrations_m);
|
||||||
|
$missing_migrations = $migrations_m[1] ?? [];
|
||||||
|
|
||||||
|
return view('system/status', [
|
||||||
|
'commands' => $commands,
|
||||||
|
'queued_jobs' => $queued_jobs,
|
||||||
|
'failed_jobs' => $failed_jobs,
|
||||||
|
'failed_queues' => $failed_queues,
|
||||||
|
'php_extensions' => $php_extensions,
|
||||||
|
'functions' => $functions,
|
||||||
|
'permissions' => $permissions,
|
||||||
|
'new_version_available' => $new_version_available,
|
||||||
|
'latest_version' => $latest_version,
|
||||||
|
'latest_version_error' => SystemController::$latest_version_error,
|
||||||
|
'public_symlink_exists' => $public_symlink_exists,
|
||||||
|
'env_is_writable' => $env_is_writable,
|
||||||
|
'non_writable_cache_file' => $non_writable_cache_file,
|
||||||
|
'missing_migrations' => $missing_migrations,
|
||||||
|
'invalid_symlinks' => \App\Module::checkSymlinks(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function action(Request $request)
|
||||||
|
{
|
||||||
|
switch ($request->action) {
|
||||||
|
case 'cancel_job':
|
||||||
|
\App\Job::where('id', $request->job_id)->delete();
|
||||||
|
\Session::flash('flash_success_floating', __('Done'));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'retry_job':
|
||||||
|
\App\Job::where('id', $request->job_id)->update(['available_at' => time()]);
|
||||||
|
sleep(1);
|
||||||
|
\Session::flash('flash_success_floating', __('Done'));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete_failed_jobs':
|
||||||
|
\App\FailedJob::where('queue', $request->failed_queue)->delete();
|
||||||
|
\Session::flash('flash_success_floating', __('Failed jobs deleted'));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'retry_failed_jobs':
|
||||||
|
$jobs = \App\FailedJob::where('queue', $request->failed_queue)->get();
|
||||||
|
foreach ($jobs as $job) {
|
||||||
|
\Artisan::call('queue:retry', ['id' => $job->id]);
|
||||||
|
}
|
||||||
|
\Session::flash('flash_success_floating', __('Failed jobs restarted'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('system');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System tools.
|
||||||
|
*/
|
||||||
|
public function tools(Request $request)
|
||||||
|
{
|
||||||
|
$output = \Cache::get('tools_execute_output');
|
||||||
|
if ($output) {
|
||||||
|
\Cache::forget('tools_execute_output');
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('system/tools', [
|
||||||
|
'output' => $output,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute tools action.
|
||||||
|
*
|
||||||
|
* @param Request $request [description]
|
||||||
|
*
|
||||||
|
* @return [type] [description]
|
||||||
|
*/
|
||||||
|
public function toolsExecute(Request $request)
|
||||||
|
{
|
||||||
|
$outputLog = new BufferedOutput();
|
||||||
|
|
||||||
|
switch ($request->action) {
|
||||||
|
case 'clear_cache':
|
||||||
|
\Artisan::call('freescout:clear-cache', [], $outputLog);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'fetch_emails':
|
||||||
|
$params = [];
|
||||||
|
$params['--days'] = (int)$request->days;
|
||||||
|
$params['--unseen'] = (int)$request->unseen;
|
||||||
|
\Artisan::call('freescout:fetch-emails', $params, $outputLog);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'migrate_db':
|
||||||
|
\Artisan::call('migrate', ['--force' => true], $outputLog);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'logout_users':
|
||||||
|
\Artisan::call('freescout:logout-users', [], $outputLog);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = $outputLog->fetch();
|
||||||
|
unset($outputLog);
|
||||||
|
|
||||||
|
if ($output) {
|
||||||
|
// \Session::flash does not work after BufferedOutput
|
||||||
|
\Cache::forever('tools_execute_output', $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('system.tools')->withInput($request->input());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajax.
|
||||||
|
*/
|
||||||
|
public function ajax(Request $request)
|
||||||
|
{
|
||||||
|
$response = [
|
||||||
|
'status' => 'error',
|
||||||
|
'msg' => '', // this is error message
|
||||||
|
];
|
||||||
|
|
||||||
|
switch ($request->action) {
|
||||||
|
|
||||||
|
case 'update':
|
||||||
|
try {
|
||||||
|
$status = \Updater::update();
|
||||||
|
|
||||||
|
// Artisan::output()
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response['msg'] = __('Error occurred. Please try again or try another :%a_start%update method:%a_end%', ['%a_start%' => '<a href="'.config('app.freescout_url').'/docs/update/" target="_blank">', '%a_end%' => '</a>']);
|
||||||
|
$response['msg'] .= '<br/><br/>'.$e->getMessage();
|
||||||
|
|
||||||
|
\Helper::logException($e);
|
||||||
|
}
|
||||||
|
if (!$response['msg'] && $status) {
|
||||||
|
// Adding session flash is useless as cache is cleared
|
||||||
|
$response['msg_success'] = __('Application successfully updated');
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'check_updates':
|
||||||
|
if (!\Config::get('app.disable_updating')) {
|
||||||
|
try {
|
||||||
|
$response['new_version_available'] = \Updater::isNewVersionAvailable(config('app.version'));
|
||||||
|
$response['status'] = 'success';
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$response['msg'] = __('Error occurred').': '.$e->getMessage();
|
||||||
|
}
|
||||||
|
if (!$response['msg'] && !$response['new_version_available']) {
|
||||||
|
// Adding session flash is useless as cache is cleated
|
||||||
|
$response['msg_success'] = __('You have the latest version installed');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['msg_success'] = __('You have the latest version installed');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['msg'] = 'Unknown action';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||||
|
$response['msg'] = 'Unknown error occurred';
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Response::json($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web Cron.
|
||||||
|
*/
|
||||||
|
public function cron(Request $request)
|
||||||
|
{
|
||||||
|
if (empty($request->hash) || $request->hash != \Helper::getWebCronHash()) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
$outputLog = new BufferedOutput();
|
||||||
|
\Artisan::call('schedule:run', [], $outputLog);
|
||||||
|
$output = $outputLog->fetch();
|
||||||
|
|
||||||
|
return response($output, 200)->header('Content-Type', 'text/plain');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajax HTML.
|
||||||
|
*/
|
||||||
|
public function ajaxHtml(Request $request)
|
||||||
|
{
|
||||||
|
switch ($request->action) {
|
||||||
|
case 'job_details':
|
||||||
|
$job = \App\FailedJob::find($request->param);
|
||||||
|
if (!$job) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = '';
|
||||||
|
$payload = json_decode($job->payload, true);
|
||||||
|
|
||||||
|
if (!empty($payload['data']['command'])) {
|
||||||
|
$html .= '<pre>'.print_r(unserialize($payload['data']['command']), 1).'</pre>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html .= '<pre>'.$job->exception.'</pre>';
|
||||||
|
|
||||||
|
return response($html);
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
}
|
94
freescout-dist/app/Http/Controllers/TranslateController.php
Normal file
94
freescout-dist/app/Http/Controllers/TranslateController.php
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use \Barryvdh\TranslationManager\Models\Translation;
|
||||||
|
use Barryvdh\TranslationManager\Controller as BaseController;
|
||||||
|
|
||||||
|
class TranslateController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Send translations to FreeScout team.
|
||||||
|
*
|
||||||
|
* @return [type] [description]
|
||||||
|
*/
|
||||||
|
public function postSend()
|
||||||
|
{
|
||||||
|
$result = false;
|
||||||
|
|
||||||
|
// Count changed translations.
|
||||||
|
$changed_data = Translation::select(['locale', 'group', \DB::raw('count(*) as changed')])
|
||||||
|
->where('status', Translation::STATUS_CHANGED)
|
||||||
|
->groupBy(['locale', 'group'])
|
||||||
|
->get()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$this->manager->exportTranslations('*', false);
|
||||||
|
|
||||||
|
// Archive langs folder
|
||||||
|
try {
|
||||||
|
$archive_path = \Helper::createZipArchive(base_path().DIRECTORY_SEPARATOR.'resources/lang', 'lang.zip', 'lang');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [
|
||||||
|
'status' => 'error',
|
||||||
|
'error_msg' => $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($archive_path) {
|
||||||
|
$attachments[] = $archive_path;
|
||||||
|
|
||||||
|
// Send archive to developers
|
||||||
|
$result = \MailHelper::sendEmailToDevs('Translations', json_encode($changed_data), $attachments, auth()->user());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
return ['status' => 'ok'];
|
||||||
|
} else {
|
||||||
|
abort(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all translations which has not been published yet.
|
||||||
|
*
|
||||||
|
* @return [type] [description]
|
||||||
|
*/
|
||||||
|
public function postRemoveUnpublished()
|
||||||
|
{
|
||||||
|
\Barryvdh\TranslationManager\Models\Translation::truncate();
|
||||||
|
|
||||||
|
return ['status' => 'ok'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download as ZIP.
|
||||||
|
*
|
||||||
|
* @return [type] [description]
|
||||||
|
*/
|
||||||
|
public function postDownload()
|
||||||
|
{
|
||||||
|
$this->manager->exportTranslations('*', false);
|
||||||
|
$file_name = 'lang.zip';
|
||||||
|
// Archive langs folder
|
||||||
|
$archive_path = \Helper::createZipArchive(base_path().DIRECTORY_SEPARATOR.'resources/lang', $file_name, 'lang');
|
||||||
|
$public_path = storage_path('app/public/'.$file_name);
|
||||||
|
|
||||||
|
\File::copy($archive_path, $public_path);
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Content-Type: application/zip',
|
||||||
|
];
|
||||||
|
|
||||||
|
return \Response::download($public_path, $file_name, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of strings to translate.
|
||||||
|
*/
|
||||||
|
public function stringsToTranslate()
|
||||||
|
{
|
||||||
|
__(':field is required');
|
||||||
|
__('The following modules have to be installed and activated: :modules');
|
||||||
|
}
|
||||||
|
}
|
660
freescout-dist/app/Http/Controllers/UsersController.php
Normal file
660
freescout-dist/app/Http/Controllers/UsersController.php
Normal file
@ -0,0 +1,660 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Events\UserDeleted;
|
||||||
|
use App\Folder;
|
||||||
|
use App\Mailbox;
|
||||||
|
use App\Subscription;
|
||||||
|
use App\Thread;
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Validator;
|
||||||
|
|
||||||
|
class UsersController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new controller instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Users list.
|
||||||
|
*/
|
||||||
|
public function users()
|
||||||
|
{
|
||||||
|
$this->authorize('create', 'App\User');
|
||||||
|
|
||||||
|
$users = User::nonDeleted()->get();
|
||||||
|
$users = User::sortUsers($users);
|
||||||
|
|
||||||
|
return view('users/users', ['users' => $users]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New user.
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$this->authorize('create', 'App\User');
|
||||||
|
$mailboxes = Mailbox::all();
|
||||||
|
|
||||||
|
return view('users/create', ['mailboxes' => $mailboxes]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new user.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*/
|
||||||
|
public function createSave(Request $request)
|
||||||
|
{
|
||||||
|
$invalid = false;
|
||||||
|
$this->authorize('create', 'App\User');
|
||||||
|
$auth_user = auth()->user();
|
||||||
|
|
||||||
|
$rules = [
|
||||||
|
'first_name' => 'required|string|max:20',
|
||||||
|
'last_name' => 'required|string|max:30',
|
||||||
|
'email' => 'required|string|email|max:100|unique:users',
|
||||||
|
//'role' => ['required', Rule::in(array_keys(User::$roles))],
|
||||||
|
];
|
||||||
|
if ($auth_user->isAdmin()) {
|
||||||
|
$rules['role'] = ['required', Rule::in(array_keys(User::$roles))];
|
||||||
|
}
|
||||||
|
if (empty($request->send_invite)) {
|
||||||
|
$rules['password'] = 'required|string|max:255';
|
||||||
|
}
|
||||||
|
$validator = Validator::make($request->all(), $rules);
|
||||||
|
|
||||||
|
if (User::mailboxEmailExists($request->email)) {
|
||||||
|
$invalid = true;
|
||||||
|
$validator->errors()->add('email', __('There is a mailbox with such email. Users and mailboxes can not have the same email addresses.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($invalid || $validator->fails()) {
|
||||||
|
return redirect()->route('users.create')
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$user->fill($request->all());
|
||||||
|
if (!$auth_user->can('changeRole', $user)) {
|
||||||
|
$user->role = User::ROLE_USER;
|
||||||
|
}
|
||||||
|
if (empty($request->send_invite)) {
|
||||||
|
// Set password from request
|
||||||
|
$user->password = Hash::make($request->password);
|
||||||
|
} else {
|
||||||
|
// Set some random password before sending invite
|
||||||
|
$user->password = Hash::make($user->generateRandomPassword());
|
||||||
|
}
|
||||||
|
// Set system timezone.
|
||||||
|
$user->timezone = config('app.timezone') ?: User::DEFAULT_TIMEZONE;
|
||||||
|
$user = \Eventy::filter('user.create_save', $user, $request);
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$user->mailboxes()->sync($request->mailboxes ?: []);
|
||||||
|
$user->syncPersonalFolders($request->mailboxes);
|
||||||
|
|
||||||
|
// Send invite
|
||||||
|
if (!empty($request->send_invite)) {
|
||||||
|
try {
|
||||||
|
$user->sendInvite(true);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Admin is allowed to see exceptions
|
||||||
|
\Session::flash('flash_error_floating', $e->getMessage().' — '.__('Check mail settings in "Manage » Settings » Mail Settings"'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('User created successfully'));
|
||||||
|
|
||||||
|
return redirect()->route('users.profile', ['id' => $user->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User profile.
|
||||||
|
*/
|
||||||
|
public function profile($id)
|
||||||
|
{
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
if ($user->isDeleted()) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->authorize('update', $user);
|
||||||
|
|
||||||
|
$users = $this->getUsersForSidebar($id);
|
||||||
|
|
||||||
|
return view('users/profile', ['user' => $user, 'users' => $users]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUsersForSidebar($except_id)
|
||||||
|
{
|
||||||
|
if (auth()->user()->isAdmin()) {
|
||||||
|
return User::sortUsers(User::nonDeleted()->get());/*->except($except_id)*/;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a registration request for the application.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function profileSave($id, Request $request)
|
||||||
|
{
|
||||||
|
$invalid = false;
|
||||||
|
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
$this->authorize('update', $user);
|
||||||
|
|
||||||
|
// This is also present in PublicController::userSetup
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'first_name' => 'required|string|max:20',
|
||||||
|
'last_name' => 'required|string|max:30',
|
||||||
|
'email' => 'required|string|email|max:100|unique:users,email,'.$id,
|
||||||
|
//'emails' => 'max:100',
|
||||||
|
'job_title' => 'max:100',
|
||||||
|
'phone' => 'max:60',
|
||||||
|
'timezone' => 'required|string|max:255',
|
||||||
|
'time_format' => 'required',
|
||||||
|
'role' => ['nullable', Rule::in(array_keys(User::$roles))],
|
||||||
|
'photo_url' => 'nullable|image|mimes:jpeg,png,jpg,gif',
|
||||||
|
]);
|
||||||
|
$validator->setAttributeNames([
|
||||||
|
'photo_url' => __('Photo'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Photo
|
||||||
|
$validator->after(function ($validator) use ($user, $request) {
|
||||||
|
if ($request->hasFile('photo_url')) {
|
||||||
|
$path_url = $user->savePhoto($request->file('photo_url'));
|
||||||
|
|
||||||
|
if ($path_url) {
|
||||||
|
$user->photo_url = $path_url;
|
||||||
|
} else {
|
||||||
|
$invalid = true;
|
||||||
|
$validator->errors()->add('photo_url', __('Error occurred processing the image. Make sure that PHP GD extension is enabled.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not allow to remove last administrator
|
||||||
|
if ($user->isAdmin() && isset($request->role) && $request->role != User::ROLE_ADMIN) {
|
||||||
|
$admins_count = User::where('role', User::ROLE_ADMIN)->count();
|
||||||
|
if ($admins_count < 2) {
|
||||||
|
$invalid = true;
|
||||||
|
$validator->errors()->add('role', __('Role of the only one administrator can not be changed.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (User::mailboxEmailExists($request->email)) {
|
||||||
|
$invalid = true;
|
||||||
|
$validator->errors()->add('email', __('There is a mailbox with such email. Users and mailboxes can not have the same email addresses.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($invalid || $validator->fails()) {
|
||||||
|
return redirect()->route('users.profile', ['id' => $id])
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save language into session.
|
||||||
|
if (auth()->user()->id == $id && $request->locale) {
|
||||||
|
session()->put('user_locale', $request->locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request_data = $request->all();
|
||||||
|
|
||||||
|
if (isset($request_data['photo_url'])) {
|
||||||
|
unset($request_data['photo_url']);
|
||||||
|
}
|
||||||
|
if (!auth()->user()->can('changeRole', $user)) {
|
||||||
|
unset($request_data['role']);
|
||||||
|
}
|
||||||
|
if ($user->status != User::STATUS_DELETED) {
|
||||||
|
if (!empty($request_data['disabled'])) {
|
||||||
|
$request_data['status'] = User::STATUS_DISABLED;
|
||||||
|
} else {
|
||||||
|
$request_data['status'] = User::STATUS_ACTIVE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$user->setData($request_data);
|
||||||
|
|
||||||
|
if (empty($request->input('enable_kb_shortcuts'))) {
|
||||||
|
$user->enable_kb_shortcuts = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = \Eventy::filter('user.save_profile', $user, $request);
|
||||||
|
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Profile saved successfully'));
|
||||||
|
|
||||||
|
return redirect()->route('users.profile', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User permissions.
|
||||||
|
*/
|
||||||
|
public function permissions($id)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
if (!$user->isAdmin()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
|
||||||
|
$mailboxes = Mailbox::all();
|
||||||
|
|
||||||
|
$users = $this->getUsersForSidebar($id);
|
||||||
|
|
||||||
|
return view('users/permissions', [
|
||||||
|
'user' => $user,
|
||||||
|
'mailboxes' => $mailboxes,
|
||||||
|
'user_mailboxes' => $user->mailboxes,
|
||||||
|
'users' => $users,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save user permissions.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*/
|
||||||
|
public function permissionsSave($id, Request $request)
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
if (!$user->isAdmin()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
|
||||||
|
$user->mailboxes()->sync($request->mailboxes ?: []);
|
||||||
|
$user->syncPersonalFolders($request->mailboxes);
|
||||||
|
|
||||||
|
// Save permissions.
|
||||||
|
$user_permissions = $request->user_permissions ?? [];
|
||||||
|
$permissions = [];
|
||||||
|
|
||||||
|
foreach (User::getUserPermissionsList() as $permission_id) {
|
||||||
|
$new_has_permission = in_array($permission_id, $user_permissions);
|
||||||
|
|
||||||
|
if ($user->hasPermission($permission_id, false) != $new_has_permission) {
|
||||||
|
$permissions[$permission_id] = (int)(bool)$new_has_permission;
|
||||||
|
$save_user = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$user->permissions = $permissions;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Permissions saved successfully'));
|
||||||
|
|
||||||
|
return redirect()->route('users.permissions', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User notifications settings.
|
||||||
|
*/
|
||||||
|
public function notifications($id)
|
||||||
|
{
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
$this->authorize('update', $user);
|
||||||
|
|
||||||
|
$subscriptions = $user->subscriptions()->select('medium', 'event')->get();
|
||||||
|
|
||||||
|
$person = '';
|
||||||
|
if ($id != auth()->user()->id) {
|
||||||
|
$person = $user->getFirstName(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = $this->getUsersForSidebar($id);
|
||||||
|
|
||||||
|
return view('users/notifications', [
|
||||||
|
'user' => $user,
|
||||||
|
'subscriptions' => $subscriptions,
|
||||||
|
'person' => $person,
|
||||||
|
'users' => $users,
|
||||||
|
'mobile_available' => \Eventy::filter('notifications.mobile_available', false),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save user notifications settings.
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*/
|
||||||
|
public function notificationsSave($id, Request $request)
|
||||||
|
{
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
$this->authorize('update', $user);
|
||||||
|
|
||||||
|
Subscription::saveFromArray($request->subscriptions, $user->id);
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Notifications saved successfully'));
|
||||||
|
|
||||||
|
return redirect()->route('users.notifications', ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Users ajax controller.
|
||||||
|
*/
|
||||||
|
public function ajax(Request $request)
|
||||||
|
{
|
||||||
|
$response = [
|
||||||
|
'status' => 'error',
|
||||||
|
'msg' => '', // this is error message
|
||||||
|
];
|
||||||
|
|
||||||
|
$auth_user = auth()->user();
|
||||||
|
|
||||||
|
switch ($request->action) {
|
||||||
|
|
||||||
|
// Both send and resend
|
||||||
|
case 'send_invite':
|
||||||
|
if (!$auth_user->isAdmin()) {
|
||||||
|
$response['msg'] = __('Not enough permissions');
|
||||||
|
}
|
||||||
|
if (empty($request->user_id)) {
|
||||||
|
$response['msg'] = __('Incorrect user');
|
||||||
|
}
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$user = User::find($request->user_id);
|
||||||
|
if (!$user) {
|
||||||
|
$response['msg'] = __('User not found');
|
||||||
|
} elseif ($user->invite_state == User::INVITE_STATE_ACTIVATED) {
|
||||||
|
$response['msg'] = __('User already accepted invitation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
try {
|
||||||
|
$user->sendInvite(true);
|
||||||
|
|
||||||
|
$response['status'] = 'success';
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Admin is allowed to see exceptions.
|
||||||
|
$response['msg'] = $e->getMessage().' — '.__('Check mail settings in "Manage » Settings » Mail Settings"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Reset password
|
||||||
|
case 'reset_password':
|
||||||
|
if (!auth()->user()->isAdmin()) {
|
||||||
|
$response['msg'] = __('Not enough permissions');
|
||||||
|
}
|
||||||
|
if (empty($request->user_id)) {
|
||||||
|
$response['msg'] = __('Incorrect user');
|
||||||
|
}
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$user = User::find($request->user_id);
|
||||||
|
if (!$user) {
|
||||||
|
$response['msg'] = __('User not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$reset_result = Password::broker()->sendResetLink(
|
||||||
|
//['id' => $request->user_id]
|
||||||
|
['id' => $request->user_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($reset_result == Password::RESET_LINK_SENT) {
|
||||||
|
$response['status'] = 'success';
|
||||||
|
$response['msg_success'] = __('Password reset email has been sent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Load website notifications
|
||||||
|
case 'web_notifications':
|
||||||
|
if (!$auth_user) {
|
||||||
|
$response['msg'] = __('You are not logged in');
|
||||||
|
}
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$web_notifications_info = $auth_user->getWebsiteNotificationsInfo(false);
|
||||||
|
$response['html'] = view('users/partials/web_notifications', [
|
||||||
|
'web_notifications_info_data' => $web_notifications_info['data'],
|
||||||
|
])->render();
|
||||||
|
|
||||||
|
$response['has_more_pages'] = (int) $web_notifications_info['notifications']->hasMorePages();
|
||||||
|
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Mark all user website notifications as read
|
||||||
|
case 'mark_notifications_as_read':
|
||||||
|
if (!$auth_user) {
|
||||||
|
$response['msg'] = __('You are not logged in');
|
||||||
|
}
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$auth_user->unreadNotifications()->update(['read_at' => now()]);
|
||||||
|
$auth_user->clearWebsiteNotificationsCache();
|
||||||
|
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Delete user photo
|
||||||
|
case 'delete_photo':
|
||||||
|
$user = User::find($request->user_id);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
$response['msg'] = __('User not found');
|
||||||
|
} elseif (!$auth_user->can('update', $user)) {
|
||||||
|
$response['msg'] = __('Not enough permissions');
|
||||||
|
}
|
||||||
|
if (!$response['msg']) {
|
||||||
|
$user->removePhoto();
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Delete user
|
||||||
|
case 'delete_user':
|
||||||
|
$user = User::find($request->user_id);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
$response['msg'] = __('User not found');
|
||||||
|
} elseif (!$auth_user->can('delete', $user)) {
|
||||||
|
$response['msg'] = __('Not enough permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user is the only one admin
|
||||||
|
if (!$response['msg'] && $user->isAdmin()) {
|
||||||
|
$admins_count = User::where('role', User::ROLE_ADMIN)->count();
|
||||||
|
if ($admins_count < 2) {
|
||||||
|
$response['msg'] = __('Administrator can not be deleted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response['msg']) {
|
||||||
|
|
||||||
|
// We have to process conversations one by one to move them to Unassigned folder,
|
||||||
|
// as conversations may be in different mailboxes
|
||||||
|
// $user->conversations()->update(['user_id' => null, 'folder_id' => ]);
|
||||||
|
$mailbox_unassigned_folders = [];
|
||||||
|
|
||||||
|
$user->conversations->each(function ($conversation) use ($auth_user, $request) {
|
||||||
|
// We don't fire ConversationUserChanged event to avoid sending notifications to users
|
||||||
|
if (!empty($request->assign_user)
|
||||||
|
&& !empty($request->assign_user[$conversation->mailbox_id])
|
||||||
|
&& (int) $request->assign_user[$conversation->mailbox_id] != -1
|
||||||
|
) {
|
||||||
|
// Set assignee.
|
||||||
|
// In this case conversation stays assigned, just assignee changes.
|
||||||
|
$conversation->user_id = $request->assign_user[$conversation->mailbox_id];
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Make convesation Unassigned.
|
||||||
|
|
||||||
|
// Unset assignee.
|
||||||
|
// Maybe use changeUser() here.
|
||||||
|
$conversation->user_id = null;
|
||||||
|
|
||||||
|
if ($conversation->isPublished()
|
||||||
|
&& ($conversation->isActive() || $conversation->isPending())
|
||||||
|
) {
|
||||||
|
// Change conversation folder to UNASSIGNED.
|
||||||
|
$folder_id = null;
|
||||||
|
if (!empty($mailbox_unassigned_folders[$conversation->mailbox_id])) {
|
||||||
|
$folder_id = $mailbox_unassigned_folders[$conversation->mailbox_id];
|
||||||
|
} else {
|
||||||
|
$folder = $conversation->mailbox->folders()
|
||||||
|
->where('type', Folder::TYPE_UNASSIGNED)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($folder) {
|
||||||
|
$folder_id = $folder->id;
|
||||||
|
$mailbox_unassigned_folders[$conversation->mailbox_id] = $folder_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($folder_id) {
|
||||||
|
$conversation->folder_id = $folder_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$conversation->save();
|
||||||
|
|
||||||
|
// Create lineitem thread
|
||||||
|
$thread = new Thread();
|
||||||
|
$thread->conversation_id = $conversation->id;
|
||||||
|
$thread->user_id = $conversation->user_id;
|
||||||
|
$thread->type = Thread::TYPE_LINEITEM;
|
||||||
|
$thread->state = Thread::STATE_PUBLISHED;
|
||||||
|
$thread->status = Thread::STATUS_NOCHANGE;
|
||||||
|
$thread->action_type = Thread::ACTION_TYPE_USER_CHANGED;
|
||||||
|
$thread->source_via = Thread::PERSON_USER;
|
||||||
|
$thread->source_type = Thread::SOURCE_TYPE_WEB;
|
||||||
|
$thread->customer_id = $conversation->customer_id;
|
||||||
|
$thread->created_by_user_id = $auth_user->id;
|
||||||
|
$thread->save();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recalculate counters for folders
|
||||||
|
//if ($user->isAdmin()) {
|
||||||
|
// Admin has access to all mailboxes
|
||||||
|
Mailbox::all()->each(function ($mailbox) {
|
||||||
|
$mailbox->updateFoldersCounters();
|
||||||
|
});
|
||||||
|
// } else {
|
||||||
|
// $user->mailboxes->each(function ($mailbox) {
|
||||||
|
// $mailbox->updateFoldersCounters();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Disconnect user from mailboxes.
|
||||||
|
$user->mailboxes()->sync([]);
|
||||||
|
$user->folders()->delete();
|
||||||
|
|
||||||
|
$user->status = \App\User::STATUS_DELETED;
|
||||||
|
// Update email.
|
||||||
|
$email_suffix = User::EMAIL_DELETED_SUFFIX.date('YmdHis');
|
||||||
|
// We have to truncate email to avoid "Data too long" error.
|
||||||
|
$user->email = mb_substr($user->email, 0, User::EMAIL_MAX_LENGTH - mb_strlen($email_suffix)).$email_suffix;
|
||||||
|
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
event(new UserDeleted($user, $auth_user));
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('User deleted').': '.$user->getFullName());
|
||||||
|
|
||||||
|
$response['status'] = 'success';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['msg'] = 'Unknown action';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response['status'] == 'error' && empty($response['msg'])) {
|
||||||
|
$response['msg'] = 'Unknown error occurred';
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Response::json($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change user password.
|
||||||
|
*/
|
||||||
|
public function password($id)
|
||||||
|
{
|
||||||
|
$user = User::findOrFail($id);
|
||||||
|
$this->authorize('update', $user);
|
||||||
|
|
||||||
|
$users = User::all()->except($id);
|
||||||
|
|
||||||
|
return view('users/password', ['user' => $user, 'users' => $users]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save changed user password.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function passwordSave($id, Request $request)
|
||||||
|
{
|
||||||
|
// It is allowed to edit only your own password
|
||||||
|
$user = auth()->user();
|
||||||
|
if ($user->id != $id) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is also present in PublicController::userSetup
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'password_current' => 'required|string',
|
||||||
|
'password' => 'required|string|min:8|confirmed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validator->after(function ($validator) use ($user, $request) {
|
||||||
|
// Check current password
|
||||||
|
if (!Hash::check($request->password_current, $user->password)) {
|
||||||
|
$validator->errors()->add('password_current', __('This password is incorrect.'));
|
||||||
|
} elseif (Hash::check($request->password, $user->password)) {
|
||||||
|
// Check new password
|
||||||
|
$validator->errors()->add('password', __('The new password is the same as the old password.'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('users.password', ['id' => $id])
|
||||||
|
->withErrors($validator)
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->password = bcrypt($request->password);
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$user->sendPasswordChanged();
|
||||||
|
|
||||||
|
\Session::flash('flash_success_floating', __('Password saved successfully!'));
|
||||||
|
|
||||||
|
return redirect()->route('users.profile', ['id' => $id]);
|
||||||
|
}
|
||||||
|
}
|
70
freescout-dist/app/Http/Kernel.php
Normal file
70
freescout-dist/app/Http/Kernel.php
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||||
|
|
||||||
|
class Kernel extends HttpKernel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The application's global HTTP middleware stack.
|
||||||
|
*
|
||||||
|
* These middleware are run during every request to your application.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $middleware = [
|
||||||
|
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
||||||
|
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||||
|
\App\Http\Middleware\TrimStrings::class,
|
||||||
|
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||||
|
\App\Http\Middleware\TrustProxies::class,
|
||||||
|
\App\Http\Middleware\ResponseHeaders::class,
|
||||||
|
\App\Http\Middleware\TerminateHandler::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The application's route middleware groups.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $middlewareGroups = [
|
||||||
|
'web' => [
|
||||||
|
\App\Http\Middleware\EncryptCookies::class,
|
||||||
|
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||||
|
\Illuminate\Session\Middleware\StartSession::class,
|
||||||
|
\App\Http\Middleware\TokenAuth::class,
|
||||||
|
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||||
|
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||||
|
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||||
|
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
|
\App\Http\Middleware\HttpsRedirect::class,
|
||||||
|
\App\Http\Middleware\Localize::class,
|
||||||
|
\App\Http\Middleware\LogoutIfDeleted::class,
|
||||||
|
\App\Http\Middleware\FrameGuard::class,
|
||||||
|
\App\Http\Middleware\CustomHandle::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
// 'api' => [
|
||||||
|
// 'throttle:60,1',
|
||||||
|
// 'bindings',
|
||||||
|
// ],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The application's route middleware.
|
||||||
|
*
|
||||||
|
* These middleware may be assigned to groups or used individually.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $routeMiddleware = [
|
||||||
|
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
|
||||||
|
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||||
|
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
|
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||||
|
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||||
|
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||||
|
'roles' => \App\Http\Middleware\CheckRole::class,
|
||||||
|
];
|
||||||
|
}
|
43
freescout-dist/app/Http/Middleware/CheckRole.php
Normal file
43
freescout-dist/app/Http/Middleware/CheckRole.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\User;
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class CheckRole
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
// Get the required roles from the route
|
||||||
|
$roles = $this->getRequiredRoleForRoute($request->route());
|
||||||
|
|
||||||
|
// Check if a role is required for the route, and
|
||||||
|
// if so, ensure that the user has that role.
|
||||||
|
if (!$roles || in_array($request->user()->getRoleName(), $roles)) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
abort(403, __('You are not authorized to access this resource.'));
|
||||||
|
// return response([
|
||||||
|
// 'error' => [
|
||||||
|
// 'code' => 'INSUFFICIENT_ROLE',
|
||||||
|
// 'description' =>
|
||||||
|
// ]
|
||||||
|
// ], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getRequiredRoleForRoute($route)
|
||||||
|
{
|
||||||
|
$actions = $route->getAction();
|
||||||
|
|
||||||
|
return isset($actions['roles']) ? $actions['roles'] : null;
|
||||||
|
}
|
||||||
|
}
|
29
freescout-dist/app/Http/Middleware/CustomHandle.php
Normal file
29
freescout-dist/app/Http/Middleware/CustomHandle.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class CustomHandle
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
// Enable/disable chat mode
|
||||||
|
if ($request->exists('chat_mode')) {
|
||||||
|
\Helper::setChatMode((int)$request->chat_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook.
|
||||||
|
\Eventy::action('middleware.web.custom_handle', $request);
|
||||||
|
|
||||||
|
return \Eventy::filter('middleware.web.custom_handle.response', $next($request), $request, $next);
|
||||||
|
}
|
||||||
|
}
|
17
freescout-dist/app/Http/Middleware/EncryptCookies.php
Normal file
17
freescout-dist/app/Http/Middleware/EncryptCookies.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
|
||||||
|
|
||||||
|
class EncryptCookies extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The names of the cookies that should not be encrypted.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $except = [
|
||||||
|
'in_app'
|
||||||
|
];
|
||||||
|
}
|
32
freescout-dist/app/Http/Middleware/FrameGuard.php
Normal file
32
freescout-dist/app/Http/Middleware/FrameGuard.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class FrameGuard
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the given request and get the response.
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
$response = $next($request);
|
||||||
|
|
||||||
|
$x_frame_options = config('app.x_frame_options');
|
||||||
|
|
||||||
|
if (false !== $x_frame_options
|
||||||
|
&& $x_frame_options !== 'false'
|
||||||
|
&& $x_frame_options !== '0'
|
||||||
|
) {
|
||||||
|
$value = 'SAMEORIGIN';
|
||||||
|
|
||||||
|
if (is_string($x_frame_options) && preg_match("#(DENY|SAMEORIGIN|ALLOW-FROM)#i", $x_frame_options)) {
|
||||||
|
$value = $x_frame_options;
|
||||||
|
}
|
||||||
|
$response->headers->set('X-Frame-Options', $value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
48
freescout-dist/app/Http/Middleware/HttpsRedirect.php
Normal file
48
freescout-dist/app/Http/Middleware/HttpsRedirect.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to HTTPS if force_redirect is enabled.
|
||||||
|
*
|
||||||
|
* https://stackoverflow.com/questions/28402726/laravel-5-redirect-to-https
|
||||||
|
*/
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class HttpsRedirect {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current proxy header mappings.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
// protected $headers = [
|
||||||
|
// Request::HEADER_FORWARDED => 'FORWARDED',
|
||||||
|
// Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
|
||||||
|
// Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
|
||||||
|
// Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
|
||||||
|
// Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
|
||||||
|
// ];
|
||||||
|
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
if (\Helper::isHttps()) {
|
||||||
|
//$request->setTrustedProxies( [ $request->getClientIp() ], array_keys($this->headers));
|
||||||
|
//!$request->secure()
|
||||||
|
if (!\Helper::isCurrentUrlHttps()) {
|
||||||
|
return redirect()->secure($request->getRequestUri());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct protocol in $_SERVER
|
||||||
|
if (\Helper::isHttps()
|
||||||
|
//&& !$request->secure()
|
||||||
|
&& strtolower($_SERVER['HTTPS'] ?? '') != 'on'
|
||||||
|
) {
|
||||||
|
$_SERVER['HTTPS'] = 'on';
|
||||||
|
}
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
29
freescout-dist/app/Http/Middleware/Localize.php
Normal file
29
freescout-dist/app/Http/Middleware/Localize.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class Localize
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
// Interface language is set automatically, as locale is stored in .env file.
|
||||||
|
|
||||||
|
// Set user language if user logged in.
|
||||||
|
$user_locale = \Eventy::filter('locale', session('user_locale'));
|
||||||
|
if ($user_locale) {
|
||||||
|
\Helper::setLocale($user_locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
30
freescout-dist/app/Http/Middleware/LogoutIfDeleted.php
Normal file
30
freescout-dist/app/Http/Middleware/LogoutIfDeleted.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Redirect;
|
||||||
|
|
||||||
|
class LogoutIfDeleted
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @param string|null $guard
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next, $guard = null)
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
if ($user && ($user->isDeleted() || $user->isDisabled())) {
|
||||||
|
Auth::logout();
|
||||||
|
return Redirect::route('login');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class RedirectIfAuthenticated
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @param string|null $guard
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next, $guard = null)
|
||||||
|
{
|
||||||
|
if (Auth::guard($guard)->check()) {
|
||||||
|
return redirect('/home');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
21
freescout-dist/app/Http/Middleware/ResponseHeaders.php
Normal file
21
freescout-dist/app/Http/Middleware/ResponseHeaders.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class ResponseHeaders
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
$response = $next($request);
|
||||||
|
|
||||||
|
// Disable caching
|
||||||
|
if (method_exists($response, 'header')) {
|
||||||
|
$response->header('Pragma', 'no-cache');
|
||||||
|
$response->header('Cache-Control', 'no-cache, max-age=0, must-revalidate, no-store');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
20
freescout-dist/app/Http/Middleware/TerminateHandler.php
Normal file
20
freescout-dist/app/Http/Middleware/TerminateHandler.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Subscription;
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class TerminateHandler
|
||||||
|
{
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function terminate($request, $response)
|
||||||
|
{
|
||||||
|
// Process events which occurred
|
||||||
|
Subscription::processEvents();
|
||||||
|
}
|
||||||
|
}
|
34
freescout-dist/app/Http/Middleware/TokenAuth.php
Normal file
34
freescout-dist/app/Http/Middleware/TokenAuth.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\User;
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class TokenAuth
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
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, created_at, "'.config('app.key').'"))'), $request->auth_token)
|
||||||
|
->first();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Helper::logException($e, '[TokenAuth]');
|
||||||
|
}
|
||||||
|
if (!empty($user)) {
|
||||||
|
\Auth::login($user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
18
freescout-dist/app/Http/Middleware/TrimStrings.php
Normal file
18
freescout-dist/app/Http/Middleware/TrimStrings.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
|
||||||
|
|
||||||
|
class TrimStrings extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The names of the attributes that should not be trimmed.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $except = [
|
||||||
|
'password',
|
||||||
|
'password_confirmation',
|
||||||
|
];
|
||||||
|
}
|
29
freescout-dist/app/Http/Middleware/TrustProxies.php
Normal file
29
freescout-dist/app/Http/Middleware/TrustProxies.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Fideloper\Proxy\TrustProxies as Middleware;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class TrustProxies extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The trusted proxies for this application.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $proxies;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current proxy header mappings.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $headers = [
|
||||||
|
Request::HEADER_FORWARDED => 'FORWARDED',
|
||||||
|
Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
|
||||||
|
Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
|
||||||
|
Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
|
||||||
|
Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
|
||||||
|
];
|
||||||
|
}
|
17
freescout-dist/app/Http/Middleware/VerifyCsrfToken.php
Normal file
17
freescout-dist/app/Http/Middleware/VerifyCsrfToken.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||||
|
|
||||||
|
class VerifyCsrfToken extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The URIs that should be excluded from CSRF verification.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $except = [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
58
freescout-dist/app/Job.php
Normal file
58
freescout-dist/app/Job.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use App\Thread;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Job extends Model
|
||||||
|
{
|
||||||
|
const UPDATED_AT = null;
|
||||||
|
|
||||||
|
public $payload_decoded = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically converted into Carbon dates.
|
||||||
|
*/
|
||||||
|
protected $dates = ['created_at', 'available_at', 'reserved_at'];
|
||||||
|
|
||||||
|
public function getPayloadDecoded()
|
||||||
|
{
|
||||||
|
if ($this->payload_decoded !== null) {
|
||||||
|
return $this->payload_decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->payload_decoded = json_decode($this->payload, true);
|
||||||
|
|
||||||
|
return $this->payload_decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCommand()
|
||||||
|
{
|
||||||
|
return self::getPayloadCommand($this->getPayloadDecoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCommandLastThread()
|
||||||
|
{
|
||||||
|
$command = $this->getCommand();
|
||||||
|
if ($command && !empty($command->threads)) {
|
||||||
|
return Thread::getLastThread($command->threads);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPayloadCommand($payload)
|
||||||
|
{
|
||||||
|
if (empty($payload['data']) || empty($payload['data']['command'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// If some record has been deleted from DB, there will be an error:
|
||||||
|
// No query results for model [App\Conversation].
|
||||||
|
return unserialize($payload['data']['command']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user