Merge branch 'release/4.7.8'

This commit is contained in:
James Cole 2018-10-28 19:44:34 +01:00
commit cb9aefc489
191 changed files with 11008 additions and 2957 deletions

View File

@ -1,12 +0,0 @@
---
exclude_patterns:
- public/lib/
- public/js/lib/
- public/fonts/
- public/css/jquery-ui/
- public/css/bootstrap-multiselect.css
- public/css/bootstrap-sortable.css
- public/css/bootstrap-tagsinput.css
- public/css/daterangepicker.css
- public/css/google-fonts.css
- .sandstorm/

3314
.deploy/docker/cacert.pem Normal file

File diff suppressed because it is too large Load Diff

View File

@ -43,12 +43,25 @@ DB_HOST=${FF_DB_HOST}
DB_PORT=${FF_DB_PORT}
DB_DATABASE=${FF_DB_NAME}
DB_USERNAME=${FF_DB_USER}
DB_PASSWORD=${FF_DB_PASSWORD}
DB_PASSWORD="${FF_DB_PASSWORD}"
# If you're looking for performance improvements, you could install memcached.
CACHE_DRIVER=file
SESSION_DRIVER=file
# You can configure another file storage backend if you cannot use the local storage option.
# To set this up, fill in the following variables. The upload path is used to store uploaded
# files and the export path is to store exported data (before download).
SFTP_HOST=${SFTP_HOST}
SFTP_PORT=${SFTP_PORT}
SFTP_UPLOAD_PATH=${SFTP_UPLOAD_PATH}
SFTP_EXPORT_PATH=${SFTP_EXPORT_PATH}
# SFTP uses either the username/password combination or the private key to authenticate.
SFTP_USERNAME=${SFTP_USERNAME}
SFTP_PASSWORD="${SFTP_PASSWORD}"
SFTP_PRIV_KEY=${SFTP_PRIV_KEY}
# Cookie settings. Should not be necessary to change these.
COOKIE_PATH="/"
COOKIE_DOMAIN=
@ -61,7 +74,7 @@ MAIL_HOST=${MAIL_HOST}
MAIL_PORT=${MAIL_PORT}
MAIL_FROM=${MAIL_FROM}
MAIL_USERNAME=${MAIL_USERNAME}
MAIL_PASSWORD=${MAIL_PASSWORD}
MAIL_PASSWORD="${MAIL_PASSWORD}"
MAIL_ENCRYPTION=${MAIL_ENCRYPTION}
# Other mail drivers:
@ -74,6 +87,9 @@ SPARKPOST_SECRET=${SPARKPOST_SECRET}
SEND_REGISTRATION_MAIL=true
SEND_ERROR_MESSAGE=false
# These messages contain (sensitive) transaction information:
SEND_REPORT_JOURNALS=${SEND_REPORT_JOURNALS}
# Set a Mapbox API key here (see mapbox.com) so there might be a map available at various places.
MAPBOX_API_KEY=${MAPBOX_API_KEY}
@ -89,9 +105,51 @@ ANALYTICS_ID=${ANALYTICS_ID}
# This makes it easier to migrate your database. Not that some fields will never be decrypted.
USE_ENCRYPTION=true
# Firefly III has two options for user authentication. "eloquent" is the default,
# and "adldap" for LDAP servers.
# For full instructions on these settings please visit:
# https://firefly-iii.readthedocs.io/en/latest/installation/authentication.html
LOGIN_PROVIDER=${LOGIN_PROVIDER}
# LDAP connection configuration
ADLDAP_CONNECTION_SCHEME=${ADLDAP_CONNECTION_SCHEME}
ADLDAP_AUTO_CONNECT=${ADLDAP_AUTO_CONNECT}
# LDAP connection settings
ADLDAP_CONTROLLERS=${ADLDAP_CONTROLLERS}
ADLDAP_PORT=${ADLDAP_PORT}
ADLDAP_TIMEOUT=${ADLDAP_TIMEOUT}
ADLDAP_BASEDN="${ADLDAP_BASEDN}"
ADLDAP_FOLLOW_REFFERALS=${ADLDAP_FOLLOW_REFFERALS}
ADLDAP_USE_SSL=${ADLDAP_USE_SSL}
ADLDAP_USE_TLS=${ADLDAP_USE_TLS}
ADLDAP_ADMIN_USERNAME=${ADLDAP_ADMIN_USERNAME}
ADLDAP_ADMIN_PASSWORD="${ADLDAP_ADMIN_PASSWORD}"
ADLDAP_ACCOUNT_PREFIX="${ADLDAP_ACCOUNT_PREFIX}"
ADLDAP_ACCOUNT_SUFFIX="${ADLDAP_ACCOUNT_SUFFIX}"
ADLDAP_ADMIN_ACCOUNT_PREFIX="${ADLDAP_ADMIN_ACCOUNT_PREFIX}"
ADLDAP_ADMIN_ACCOUNT_SUFFIX="${ADLDAP_ADMIN_ACCOUNT_SUFFIX}"
# LDAP authentication settings.
ADLDAP_PASSWORD_SYNC=${ADLDAP_PASSWORD_SYNC}
ADLDAP_LOGIN_FALLBACK=${ADLDAP_LOGIN_FALLBACK}
ADLDAP_DISCOVER_FIELD=${ADLDAP_DISCOVER_FIELD}
ADLDAP_AUTH_FIELD=${ADLDAP_AUTH_FIELD}
# Will allow SSO if your server provides an AUTH_USER field.
WINDOWS_SSO_DISCOVER=${WINDOWS_SSO_DISCOVER}
WINDOWS_SSO_KEY=${WINDOWS_SSO_KEY}
# field to sync as local username.
ADLDAP_SYNC_FIELD=${ADLDAP_SYNC_FIELD}
# Leave the following configuration vars as is.
# Unless you like to tinker and know what you're doing.
APP_NAME=FireflyIII
ADLDAP_CONNECTION=default
BROADCAST_DRIVER=log
QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1
@ -108,5 +166,3 @@ IS_DOCKER=true
IS_SANDSTORM=false
IS_HEROKU=false
BUNQ_USE_SANDBOX=false
MAILGUN_DOMAIN=
MAILGUN_SECRET=

View File

@ -49,6 +49,19 @@ DB_PASSWORD=secret
CACHE_DRIVER=file
SESSION_DRIVER=file
# You can configure another file storage backend if you cannot use the local storage option.
# To set this up, fill in the following variables. The upload path is used to store uploaded
# files and the export path is to store exported data (before download).
SFTP_HOST=
SFTP_PORT=
SFTP_UPLOAD_PATH=
SFTP_EXPORT_PATH=
# SFTP uses either the username/password combination or the private key to authenticate.
SFTP_USERNAME=
SFTP_PASSWORD=
SFTP_PRIV_KEY=
# Cookie settings. Should not be necessary to change these.
COOKIE_PATH="/"
COOKIE_DOMAIN=
@ -74,6 +87,9 @@ SPARKPOST_SECRET=
SEND_REGISTRATION_MAIL=true
SEND_ERROR_MESSAGE=true
# These messages contain (sensitive) transaction information:
SEND_REPORT_JOURNALS=true
# Set a Mapbox API key here (see mapbox.com) so there might be a map available at various places.
MAPBOX_API_KEY=
@ -89,9 +105,52 @@ ANALYTICS_ID=
# This makes it easier to migrate your database. Not that some fields will never be decrypted.
USE_ENCRYPTION=true
# Firefly III has two options for user authentication. "eloquent" is the default,
# and "adldap" for LDAP servers.
# For full instructions on these settings please visit:
# https://firefly-iii.readthedocs.io/en/latest/installation/authentication.html
LOGIN_PROVIDER=eloquent
# LDAP connection configuration
# OpenLDAP, FreeIPA or ActiveDirectory
ADLDAP_CONNECTION_SCHEME=OpenLDAP
ADLDAP_AUTO_CONNECT=true
# LDAP connection settings
ADLDAP_CONTROLLERS=
ADLDAP_PORT=389
ADLDAP_TIMEOUT=5
ADLDAP_BASEDN=""
ADLDAP_FOLLOW_REFFERALS=false
ADLDAP_USE_SSL=false
ADLDAP_USE_TLS=false
ADLDAP_ADMIN_USERNAME=
ADLDAP_ADMIN_PASSWORD=
ADLDAP_ACCOUNT_PREFIX=
ADLDAP_ACCOUNT_SUFFIX=
ADLDAP_ADMIN_ACCOUNT_PREFIX=
ADLDAP_ADMIN_ACCOUNT_SUFFIX=
# LDAP authentication settings.
ADLDAP_PASSWORD_SYNC=false
ADLDAP_LOGIN_FALLBACK=false
ADLDAP_DISCOVER_FIELD=distinguishedname
ADLDAP_AUTH_FIELD=distinguishedname
# Will allow SSO if your server provides an AUTH_USER field.
WINDOWS_SSO_DISCOVER=samaccountname
WINDOWS_SSO_KEY=AUTH_USER
# field to sync as local username.
ADLDAP_SYNC_FIELD=userprincipalname
# Leave the following configuration vars as is.
# Unless you like to tinker and know what you're doing.
APP_NAME=FireflyIII
ADLDAP_CONNECTION=default
BROADCAST_DRIVER=log
QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1
@ -108,5 +167,3 @@ IS_DOCKER=false
IS_SANDSTORM=false
IS_HEROKU=false
BUNQ_USE_SANDBOX=false
MAILGUN_DOMAIN=
MAILGUN_SECRET=

View File

@ -49,6 +49,19 @@ DB_CONNECTION=pgsql
CACHE_DRIVER=file
SESSION_DRIVER=file
# You can configure another file storage backend if you cannot use the local storage option.
# To set this up, fill in the following variables. The upload path is used to store uploaded
# files and the export path is to store exported data (before download).
SFTP_HOST=
SFTP_PORT=
SFTP_UPLOAD_PATH=
SFTP_EXPORT_PATH=
# SFTP uses either the username/password combination or the private key to authenticate.
SFTP_USERNAME=
SFTP_PASSWORD=
SFTP_PRIV_KEY=
# Cookie settings. Should not be necessary to change these.
COOKIE_PATH="/"
COOKIE_DOMAIN=
@ -74,6 +87,9 @@ SPARKPOST_SECRET=
SEND_REGISTRATION_MAIL=true
SEND_ERROR_MESSAGE=true
# These messages contain (sensitive) transaction information:
SEND_REPORT_JOURNALS=true
# Set a Mapbox API key here (see mapbox.com) so there might be a map available at various places.
MAPBOX_API_KEY=
@ -89,9 +105,52 @@ ANALYTICS_ID=
# This makes it easier to migrate your database. Not that some fields will never be decrypted.
USE_ENCRYPTION=true
# Firefly III has two options for user authentication. "eloquent" is the default,
# and "adldap" for LDAP servers.
# For full instructions on these settings please visit:
# https://firefly-iii.readthedocs.io/en/latest/installation/authentication.html
LOGIN_PROVIDER=eloquent
# LDAP connection configuration
# OpenLDAP, FreeIPA or ActiveDirectory
ADLDAP_CONNECTION_SCHEME=OpenLDAP
ADLDAP_AUTO_CONNECT=true
# LDAP connection settings
ADLDAP_CONTROLLERS=
ADLDAP_PORT=389
ADLDAP_TIMEOUT=5
ADLDAP_BASEDN=""
ADLDAP_FOLLOW_REFFERALS=false
ADLDAP_USE_SSL=false
ADLDAP_USE_TLS=false
ADLDAP_ADMIN_USERNAME=
ADLDAP_ADMIN_PASSWORD=
ADLDAP_ACCOUNT_PREFIX=
ADLDAP_ACCOUNT_SUFFIX=
ADLDAP_ADMIN_ACCOUNT_PREFIX=
ADLDAP_ADMIN_ACCOUNT_SUFFIX=
# LDAP authentication settings.
ADLDAP_PASSWORD_SYNC=false
ADLDAP_LOGIN_FALLBACK=false
ADLDAP_DISCOVER_FIELD=distinguishedname
ADLDAP_AUTH_FIELD=distinguishedname
# Will allow SSO if your server provides an AUTH_USER field.
WINDOWS_SSO_DISCOVER=samaccountname
WINDOWS_SSO_KEY=AUTH_USER
# field to sync as local username.
ADLDAP_SYNC_FIELD=userprincipalname
# Leave the following configuration vars as is.
# Unless you like to tinker and know what you're doing.
APP_NAME=FireflyIII
ADLDAP_CONNECTION=default
BROADCAST_DRIVER=log
QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1
@ -108,5 +167,3 @@ IS_DOCKER=false
IS_SANDSTORM=false
IS_HEROKU=true
BUNQ_USE_SANDBOX=false
MAILGUN_DOMAIN=
MAILGUN_SECRET=

View File

@ -49,6 +49,19 @@ DB_PASSWORD=firefly
CACHE_DRIVER=file
SESSION_DRIVER=file
# You can configure another file storage backend if you cannot use the local storage option.
# To set this up, fill in the following variables. The upload path is used to store uploaded
# files and the export path is to store exported data (before download).
SFTP_HOST=
SFTP_PORT=
SFTP_UPLOAD_PATH=
SFTP_EXPORT_PATH=
# SFTP uses either the username/password combination or the private key to authenticate.
SFTP_USERNAME=
SFTP_PASSWORD=
SFTP_PRIV_KEY=
# Cookie settings. Should not be necessary to change these.
COOKIE_PATH="/"
COOKIE_DOMAIN=
@ -74,6 +87,9 @@ SPARKPOST_SECRET=
SEND_REGISTRATION_MAIL=true
SEND_ERROR_MESSAGE=true
# These messages contain (sensitive) transaction information:
SEND_REPORT_JOURNALS=true
# Set a Mapbox API key here (see mapbox.com) so there might be a map available at various places.
MAPBOX_API_KEY=
@ -89,9 +105,52 @@ ANALYTICS_ID=
# This makes it easier to migrate your database. Not that some fields will never be decrypted.
USE_ENCRYPTION=true
# Firefly III has two options for user authentication. "eloquent" is the default,
# and "adldap" for LDAP servers.
# For full instructions on these settings please visit:
# https://firefly-iii.readthedocs.io/en/latest/installation/authentication.html
LOGIN_PROVIDER=eloquent
# LDAP connection configuration
# or FreeIPA or ActiveDirectory
ADLDAP_CONNECTION_SCHEME=OpenLDAP
ADLDAP_AUTO_CONNECT=true
# LDAP connection settings
ADLDAP_CONTROLLERS=
ADLDAP_PORT=389
ADLDAP_TIMEOUT=5
ADLDAP_BASEDN=""
ADLDAP_FOLLOW_REFFERALS=false
ADLDAP_USE_SSL=false
ADLDAP_USE_TLS=false
ADLDAP_ADMIN_USERNAME=
ADLDAP_ADMIN_PASSWORD=
ADLDAP_ACCOUNT_PREFIX=
ADLDAP_ACCOUNT_SUFFIX=
ADLDAP_ADMIN_ACCOUNT_PREFIX=
ADLDAP_ADMIN_ACCOUNT_SUFFIX=
# LDAP authentication settings.
ADLDAP_PASSWORD_SYNC=false
ADLDAP_LOGIN_FALLBACK=false
ADLDAP_DISCOVER_FIELD=distinguishedname
ADLDAP_AUTH_FIELD=distinguishedname
# Will allow SSO if your server provides an AUTH_USER field.
WINDOWS_SSO_DISCOVER=samaccountname
WINDOWS_SSO_KEY=AUTH_USER
# field to sync as local username.
ADLDAP_SYNC_FIELD=userprincipalname
# Leave the following configuration vars as is.
# Unless you like to tinker and know what you're doing.
APP_NAME=FireflyIII
ADLDAP_CONNECTION=default
BROADCAST_DRIVER=log
QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1
@ -108,5 +167,3 @@ IS_DOCKER=false
IS_SANDSTORM=true
IS_HEROKU=false
BUNQ_USE_SANDBOX=false
MAILGUN_DOMAIN=
MAILGUN_SECRET=

View File

@ -49,6 +49,19 @@ DB_CONNECTION=sqlite
CACHE_DRIVER=file
SESSION_DRIVER=file
# You can configure another file storage backend if you cannot use the local storage option.
# To set this up, fill in the following variables. The upload path is used to store uploaded
# files and the export path is to store exported data (before download).
SFTP_HOST=
SFTP_PORT=
SFTP_UPLOAD_PATH=
SFTP_EXPORT_PATH=
# SFTP uses either the username/password combination or the private key to authenticate.
SFTP_USERNAME=
SFTP_PASSWORD=
SFTP_PRIV_KEY=
# Cookie settings. Should not be necessary to change these.
COOKIE_PATH="/"
COOKIE_DOMAIN=
@ -74,6 +87,9 @@ SPARKPOST_SECRET=
SEND_REGISTRATION_MAIL=true
SEND_ERROR_MESSAGE=false
# These messages contain (sensitive) transaction information:
SEND_REPORT_JOURNALS=true
# Set a Mapbox API key here (see mapbox.com) so there might be a map available at various places.
MAPBOX_API_KEY=
@ -89,9 +105,52 @@ ANALYTICS_ID=
# This makes it easier to migrate your database. Not that some fields will never be decrypted.
USE_ENCRYPTION=false
# Firefly III has two options for user authentication. "eloquent" is the default,
# and "adldap" for LDAP servers.
# For full instructions on these settings please visit:
# https://firefly-iii.readthedocs.io/en/latest/installation/authentication.html
LOGIN_PROVIDER=eloquent
# LDAP connection configuration
# or FreeIPA or ActiveDirectory
ADLDAP_CONNECTION_SCHEME=OpenLDAP
ADLDAP_AUTO_CONNECT=true
# LDAP connection settings
ADLDAP_CONTROLLERS=
ADLDAP_PORT=389
ADLDAP_TIMEOUT=5
ADLDAP_BASEDN=""
ADLDAP_FOLLOW_REFFERALS=false
ADLDAP_USE_SSL=false
ADLDAP_USE_TLS=false
ADLDAP_ADMIN_USERNAME=
ADLDAP_ADMIN_PASSWORD=
ADLDAP_ACCOUNT_PREFIX=
ADLDAP_ACCOUNT_SUFFIX=
ADLDAP_ADMIN_ACCOUNT_PREFIX=
ADLDAP_ADMIN_ACCOUNT_SUFFIX=
# LDAP authentication settings.
ADLDAP_PASSWORD_SYNC=false
ADLDAP_LOGIN_FALLBACK=false
ADLDAP_DISCOVER_FIELD=distinguishedname
ADLDAP_AUTH_FIELD=distinguishedname
# Will allow SSO if your server provides an AUTH_USER field.
WINDOWS_SSO_DISCOVER=samaccountname
WINDOWS_SSO_KEY=AUTH_USER
# field to sync as local username.
ADLDAP_SYNC_FIELD=userprincipalname
# Leave the following configuration vars as is.
# Unless you like to tinker and know what you're doing.
APP_NAME=FireflyIII
ADLDAP_CONNECTION=default
BROADCAST_DRIVER=log
QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1
@ -108,5 +167,3 @@ IS_DOCKER=false
IS_SANDSTORM=false
IS_HEROKU=false
BUNQ_USE_SANDBOX=true
MAILGUN_DOMAIN=
MAILGUN_SECRET=

9
.locales Normal file
View File

@ -0,0 +1,9 @@
en_US
de_DE
fr_FR
it_IT
nl_NL
pl_PL
pt_BR
ru_RU
tr_TR

View File

@ -1,3 +1,37 @@
$ 4.7.8
- [Issue 1005](https://github.com/firefly-iii/firefly-iii/issues/1005) You can now configure Firefly III to use LDAP.
- [Issue 1071](https://github.com/firefly-iii/firefly-iii/issues/1071) You can execute transaction rules using the command line (so you can cronjob it)
- [Issue 1108](https://github.com/firefly-iii/firefly-iii/issues/1108) You can now reorder budgets.
- [Issue 1159](https://github.com/firefly-iii/firefly-iii/issues/1159) The ability to import transactions from FinTS-enabled banks.
- [Issue 1727](https://github.com/firefly-iii/firefly-iii/issues/1727) You can now use SFTP as storage for uploads and exports.
- [Issue 1733](https://github.com/firefly-iii/firefly-iii/issues/1733) You can configure Firefly III not to send emails with transaction information in them.
- [Issue 1040](https://github.com/firefly-iii/firefly-iii/issues/1040) Fixed various things that would not scale properly in the past.
- [Issue 1771](https://github.com/firefly-iii/firefly-iii/issues/1771) A link to the transaction that fits the bill.
- [Issue 1800](https://github.com/firefly-iii/firefly-iii/issues/1800) Icon updated to match others.
- MySQL database connection now forces the InnoDB to be used.
- [Issue 1583](https://github.com/firefly-iii/firefly-iii/issues/1583) Some times recurring transactions would not fire.
- [Issue 1607](https://github.com/firefly-iii/firefly-iii/issues/1607) Problems with the bunq API, finally solved?! (I feel like a clickbait YouTube video now)
- [Issue 1698](https://github.com/firefly-iii/firefly-iii/issues/1698) Certificate problems in the Docker container
- [Issue 1751](https://github.com/firefly-iii/firefly-iii/issues/1751) Bug in autocomplete
- [Issue 1760](https://github.com/firefly-iii/firefly-iii/issues/1760) Tag report bad math
- [Issue 1765](https://github.com/firefly-iii/firefly-iii/issues/1765) API inconsistencies for piggy banks.
- [Issue 1774](https://github.com/firefly-iii/firefly-iii/issues/1774) Integer exception in SQLite databases
- [Issue 1775](https://github.com/firefly-iii/firefly-iii/issues/1775) Heroku now supports all locales
- [Issue 1778](https://github.com/firefly-iii/firefly-iii/issues/1778) More autocomplete problems fixed
- [Issue 1747](https://github.com/firefly-iii/firefly-iii/issues/1747) Rules now stop at the right moment.
- [Issue 1781](https://github.com/firefly-iii/firefly-iii/issues/1781) Problems when creating new rules.
- [Issue 1784](https://github.com/firefly-iii/firefly-iii/issues/1784) Can now create a liability with an empty balance.
- [Issue 1785](https://github.com/firefly-iii/firefly-iii/issues/1785) Redirect error
- [Issue 1790](https://github.com/firefly-iii/firefly-iii/issues/1790) Show attachments for bills.
- [Issue 1792](https://github.com/firefly-iii/firefly-iii/issues/1792) Mention excluded accounts.
- [Issue 1798](https://github.com/firefly-iii/firefly-iii/issues/1798) Could not recreate deleted piggy banks
- [Issue 1805](https://github.com/firefly-iii/firefly-iii/issues/1805) Fixes when handling foreign currencies
- [Issue 1807](https://github.com/firefly-iii/firefly-iii/issues/1807) Also decrypt deleted records.
- [Issue 1812](https://github.com/firefly-iii/firefly-iii/issues/1812) Fix in transactions API
- [Issue 1815](https://github.com/firefly-iii/firefly-iii/issues/1815) Opening balance account name can now be translated.
- [Issue 1830](https://github.com/firefly-iii/firefly-iii/issues/1830) Multi-user in a single browser could leak autocomplete data.
# 4.7.7
- [Issue 954](https://github.com/firefly-iii/firefly-iii/issues/954) Some additional view chart ranges
- [Issue 1710](https://github.com/firefly-iii/firefly-iii/issues/1710) Added a new currency ([hamuz](https://github.com/hamuz))

View File

@ -553,6 +553,8 @@ opt/app/app/Import/Converter/RabobankDebitCredit.php
opt/app/app/Import/JobConfiguration/BunqJobConfiguration.php
opt/app/app/Import/JobConfiguration/FakeJobConfiguration.php
opt/app/app/Import/JobConfiguration/FileJobConfiguration.php
opt/app/app/Import/JobConfiguration/FinTSConfigurationSteps.php
opt/app/app/Import/JobConfiguration/FinTSJobConfiguration.php
opt/app/app/Import/JobConfiguration/JobConfigurationInterface.php
opt/app/app/Import/JobConfiguration/SpectreJobConfiguration.php
opt/app/app/Import/JobConfiguration/YnabJobConfiguration.php
@ -578,6 +580,7 @@ opt/app/app/Import/Prerequisites/YnabPrerequisites.php
opt/app/app/Import/Routine/BunqRoutine.php
opt/app/app/Import/Routine/FakeRoutine.php
opt/app/app/Import/Routine/FileRoutine.php
opt/app/app/Import/Routine/FinTSRoutine.php
opt/app/app/Import/Routine/RoutineInterface.php
opt/app/app/Import/Routine/SpectreRoutine.php
opt/app/app/Import/Routine/YnabRoutine.php
@ -782,6 +785,7 @@ opt/app/app/Support/Facades/FireflyConfig.php
opt/app/app/Support/Facades/Navigation.php
opt/app/app/Support/Facades/Preferences.php
opt/app/app/Support/Facades/Steam.php
opt/app/app/Support/FinTS/FinTS.php
opt/app/app/Support/FireflyConfig.php
opt/app/app/Support/Http/Controllers/AugumentData.php
opt/app/app/Support/Http/Controllers/BasicDataSupport.php
@ -805,6 +809,9 @@ opt/app/app/Support/Import/JobConfiguration/File/ConfigureRolesHandler.php
opt/app/app/Support/Import/JobConfiguration/File/ConfigureUploadHandler.php
opt/app/app/Support/Import/JobConfiguration/File/FileConfigurationInterface.php
opt/app/app/Support/Import/JobConfiguration/File/NewFileJobHandler.php
opt/app/app/Support/Import/JobConfiguration/FinTS/ChooseAccountHandler.php
opt/app/app/Support/Import/JobConfiguration/FinTS/FinTSConfigurationInterface.php
opt/app/app/Support/Import/JobConfiguration/FinTS/NewFinTSJobHandler.php
opt/app/app/Support/Import/JobConfiguration/Spectre/AuthenticatedHandler.php
opt/app/app/Support/Import/JobConfiguration/Spectre/ChooseAccountsHandler.php
opt/app/app/Support/Import/JobConfiguration/Spectre/ChooseLoginHandler.php
@ -833,6 +840,7 @@ opt/app/app/Support/Import/Routine/File/MappedValuesValidator.php
opt/app/app/Support/Import/Routine/File/MappingConverger.php
opt/app/app/Support/Import/Routine/File/OFXProcessor.php
opt/app/app/Support/Import/Routine/File/OpposingAccountMapper.php
opt/app/app/Support/Import/Routine/FinTS/StageImportDataHandler.php
opt/app/app/Support/Import/Routine/Spectre/StageAuthenticatedHandler.php
opt/app/app/Support/Import/Routine/Spectre/StageImportDataHandler.php
opt/app/app/Support/Import/Routine/Spectre/StageNewHandler.php
@ -1201,6 +1209,7 @@ opt/app/public/images/logos/bunq.png
opt/app/public/images/logos/csv.png
opt/app/public/images/logos/fake.png
opt/app/public/images/logos/file.png
opt/app/public/images/logos/fints.png
opt/app/public/images/logos/plaid.png
opt/app/public/images/logos/quovo.png
opt/app/public/images/logos/spectre.png
@ -1718,6 +1727,8 @@ opt/app/resources/views/import/file/configure-upload.twig
opt/app/resources/views/import/file/map.twig
opt/app/resources/views/import/file/new.twig
opt/app/resources/views/import/file/roles.twig
opt/app/resources/views/import/fints/choose_account.twig
opt/app/resources/views/import/fints/new.twig
opt/app/resources/views/import/index.twig
opt/app/resources/views/import/spectre/accounts.twig
opt/app/resources/views/import/spectre/choose-login.twig
@ -3030,6 +3041,7 @@ opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Support/MessageBag.php
opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Support/MessageProvider.php
opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Support/Renderable.php
opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Support/Responsable.php
opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Translation/HasLocalePreference.php
opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Translation/Loader.php
opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Translation/Translator.php
opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Validation/Factory.php
@ -3099,6 +3111,7 @@ opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/QueueEntityRes
opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/RelationNotFoundException.php
opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsTo.php
opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php
opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/Concerns/AsPivot.php
opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php
opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/Concerns/SupportsDefaultModels.php
opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/HasMany.php
@ -3485,6 +3498,7 @@ opt/app/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php
opt/app/vendor/laravel/framework/src/Illuminate/Pipeline/PipelineServiceProvider.php
opt/app/vendor/laravel/framework/src/Illuminate/Pipeline/composer.json
opt/app/vendor/laravel/framework/src/Illuminate/Queue/BeanstalkdQueue.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedClosure.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/Capsule/Manager.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/Connectors/BeanstalkdConnector.php
@ -3537,6 +3551,7 @@ opt/app/vendor/laravel/framework/src/Illuminate/Queue/QueueManager.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/QueueServiceProvider.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/README.md
opt/app/vendor/laravel/framework/src/Illuminate/Queue/RedisQueue.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/SerializableClosure.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/SerializesModels.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/SqsQueue.php
@ -4373,6 +4388,94 @@ opt/app/vendor/monolog/monolog/tests/Monolog/Processor/WebProcessorTest.php
opt/app/vendor/monolog/monolog/tests/Monolog/PsrLogCompatTest.php
opt/app/vendor/monolog/monolog/tests/Monolog/RegistryTest.php
opt/app/vendor/monolog/monolog/tests/Monolog/TestCase.php
opt/app/vendor/mschindler83/fints-hbci-php/COMPATIBILITY.md
opt/app/vendor/mschindler83/fints-hbci-php/LICENSE
opt/app/vendor/mschindler83/fints-hbci-php/README.md
opt/app/vendor/mschindler83/fints-hbci-php/Samples/saldo.php
opt/app/vendor/mschindler83/fints-hbci-php/Samples/statement_of_account.php
opt/app/vendor/mschindler83/fints-hbci-php/composer.json
opt/app/vendor/mschindler83/fints-hbci-php/composer.lock
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Adapter/AdapterInterface.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Adapter/Curl.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Adapter/Debug.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Adapter/Exception/AdapterException.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Adapter/Exception/CurlException.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Connection.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/DataElementGroups/EncryptionAlgorithm.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/DataElementGroups/HashAlgorithm.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/DataElementGroups/KeyName.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/DataElementGroups/SecurityDateTime.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/DataElementGroups/SecurityIdentificationDetails.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/DataElementGroups/SecurityProfile.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/DataElementGroups/SignatureAlgorithm.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/DataTypes/Bin.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/DataTypes/Dat.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/DataTypes/Kik.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/DataTypes/Kti.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/DataTypes/Ktv.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Deg.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Dialog/Dialog.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Dialog/Exception/FailedRequestException.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/FinTs.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Message/AbstractMessage.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Message/Message.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Model/Account.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Model/SEPAAccount.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Model/Saldo.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Model/StatementOfAccount/Statement.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Model/StatementOfAccount/StatementOfAccount.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Model/StatementOfAccount/Transaction.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Parser/Exception/MT940Exception.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Parser/MT940.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Response/GetAccounts.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Response/GetSEPAAccounts.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Response/GetSaldo.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Response/GetStatementOfAccount.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Response/Initialization.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Response/Response.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/AbstractSegment.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HKEND.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HKIDN.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HKKAZ.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HKSAL.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HKSPA.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HKSYN.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HKVVB.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HNHBK.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HNHBS.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HNSHA.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HNSHK.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HNVSD.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/HNVSK.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/NameMapping.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/Segment.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Fhp/Segment/SegmentInterface.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/ConnectionTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DataElementGroups/EncryptionAlgorithmTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DataElementGroups/HashAlgorithmTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DataElementGroups/KeyNameTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DataElementGroups/SecurityDateTimeTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DataElementGroups/SecurityIdentificationDetailsTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DataElementGroups/SecurityProfileTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DataElementGroups/SignatureAlgorithmTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DataTypes/BinTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DataTypes/DatTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DataTypes/KikTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DataTypes/KtiTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DataTypes/KtvTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/DegTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/FinTsTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/Message/MessageTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/Model/AccountTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/Model/SEPAAccountTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/Model/SaldoTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/Model/StatementOfAccount/StatementOfAccountTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/Model/StatementOfAccount/StatementTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/Model/StatementOfAccount/TransactionTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/Fhp/ResponseTest/ResponseTest.php
opt/app/vendor/mschindler83/fints-hbci-php/lib/Tests/TestInit.php
opt/app/vendor/mschindler83/fints-hbci-php/phplint.sh
opt/app/vendor/mschindler83/fints-hbci-php/phpunit.xml.dist
opt/app/vendor/nesbot/carbon/LICENSE
opt/app/vendor/nesbot/carbon/composer.json
opt/app/vendor/nesbot/carbon/readme.md
@ -4456,6 +4559,23 @@ opt/app/vendor/nesbot/carbon/src/Carbon/Lang/zh_TW.php
opt/app/vendor/nesbot/carbon/src/Carbon/Laravel/ServiceProvider.php
opt/app/vendor/nesbot/carbon/src/Carbon/Translator.php
opt/app/vendor/nesbot/carbon/src/JsonSerializable.php
opt/app/vendor/opis/closure/CHANGELOG.md
opt/app/vendor/opis/closure/LICENSE
opt/app/vendor/opis/closure/NOTICE
opt/app/vendor/opis/closure/README.md
opt/app/vendor/opis/closure/autoload.php
opt/app/vendor/opis/closure/composer.json
opt/app/vendor/opis/closure/functions.php
opt/app/vendor/opis/closure/src/Analyzer.php
opt/app/vendor/opis/closure/src/ClosureContext.php
opt/app/vendor/opis/closure/src/ClosureScope.php
opt/app/vendor/opis/closure/src/ClosureStream.php
opt/app/vendor/opis/closure/src/ISecurityProvider.php
opt/app/vendor/opis/closure/src/ReflectionClosure.php
opt/app/vendor/opis/closure/src/SecurityException.php
opt/app/vendor/opis/closure/src/SecurityProvider.php
opt/app/vendor/opis/closure/src/SelfReference.php
opt/app/vendor/opis/closure/src/SerializableClosure.php
opt/app/vendor/paragonie/constant_time_encoding/LICENSE.txt
opt/app/vendor/paragonie/constant_time_encoding/README.md
opt/app/vendor/paragonie/constant_time_encoding/composer.json
@ -5458,6 +5578,7 @@ opt/app/vendor/symfony/debug/Tests/Fixtures/DeprecatedInterface.php
opt/app/vendor/symfony/debug/Tests/Fixtures/ExtendedFinalMethod.php
opt/app/vendor/symfony/debug/Tests/Fixtures/FinalClass.php
opt/app/vendor/symfony/debug/Tests/Fixtures/FinalMethod.php
opt/app/vendor/symfony/debug/Tests/Fixtures/FinalMethod2Trait.php
opt/app/vendor/symfony/debug/Tests/Fixtures/InternalClass.php
opt/app/vendor/symfony/debug/Tests/Fixtures/InternalInterface.php
opt/app/vendor/symfony/debug/Tests/Fixtures/InternalTrait.php
@ -5466,6 +5587,7 @@ opt/app/vendor/symfony/debug/Tests/Fixtures/NonDeprecatedInterface.php
opt/app/vendor/symfony/debug/Tests/Fixtures/PEARClass.php
opt/app/vendor/symfony/debug/Tests/Fixtures/Throwing.php
opt/app/vendor/symfony/debug/Tests/Fixtures/ToStringThrower.php
opt/app/vendor/symfony/debug/Tests/Fixtures/TraitWithInternalMethod.php
opt/app/vendor/symfony/debug/Tests/Fixtures/casemismatch.php
opt/app/vendor/symfony/debug/Tests/Fixtures/notPsr0Bis.php
opt/app/vendor/symfony/debug/Tests/Fixtures/psr4/Psr4CaseMismatch.php
@ -6539,6 +6661,7 @@ opt/app/vendor/symfony/var-dumper/Tests/Caster/XmlReaderCasterTest.php
opt/app/vendor/symfony/var-dumper/Tests/Cloner/DataTest.php
opt/app/vendor/symfony/var-dumper/Tests/Cloner/VarClonerTest.php
opt/app/vendor/symfony/var-dumper/Tests/Dumper/CliDumperTest.php
opt/app/vendor/symfony/var-dumper/Tests/Dumper/FunctionsTest.php
opt/app/vendor/symfony/var-dumper/Tests/Dumper/HtmlDumperTest.php
opt/app/vendor/symfony/var-dumper/Tests/Dumper/ServerDumperTest.php
opt/app/vendor/symfony/var-dumper/Tests/Fixtures/FooInterface.php

View File

@ -15,8 +15,8 @@ const pkgdef :Spk.PackageDefinition = (
manifest = (
appTitle = (defaultText = "Firefly III"),
appVersion = 17,
appMarketingVersion = (defaultText = "4.7.7"),
appVersion = 18,
appMarketingVersion = (defaultText = "4.7.8"),
actions = [
# Define your "new document" handlers here.

View File

@ -8,9 +8,9 @@ ENV CORES ${CORES:-1}
ENV FIREFLY_PATH /var/www/firefly-iii/
ENV CURL_VERSION 7.60.0
ENV OPENSSL_VERSION 1.1.1-pre6
ENV COMPOSER_ALLOW_SUPERUSER 1
LABEL version="1.0" maintainer="thegrumpydictator@gmail.com"
LABEL version="1.1" maintainer="thegrumpydictator@gmail.com"
# install packages
RUN apt-get update -y && \
@ -20,6 +20,7 @@ RUN apt-get update -y && \
wget \
libpng-dev \
libicu-dev \
libldap2-dev \
libedit-dev \
libtidy-dev \
libxml2-dev \
@ -35,6 +36,8 @@ RUN apt-get update -y && \
locales && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# LDAP install
RUN docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && docker-php-ext-install ldap
# Install latest curl
RUN cd /tmp && \
@ -66,6 +69,9 @@ COPY ./.deploy/docker/firefly-iii.conf /etc/supervisor/conf.d/firefly-iii.conf
# copy cron job supervisor conf file.
COPY ./.deploy/docker/cronjob.conf /etc/supervisor/conf.d/cronjob.conf
# copy ca certs to correct location
COPY ./.deploy/docker/cacert.pem /usr/local/ssl/cert.pem
# test crons added via crontab
RUN echo "0 3 * * * /usr/local/bin/php /var/www/firefly-iii/artisan firefly:cron" | crontab -
#RUN (crontab -l ; echo "*/1 * * * * free >> /var/www/firefly-iii/public/cron.html") 2>&1 | crontab -
@ -104,7 +110,6 @@ ADD . $FIREFLY_PATH
RUN rm -rf /usr/local/lib/libcurl.so.4 && ln -s /usr/lib/x86_64-linux-gnu/libcurl.so.4.4.0 /usr/local/lib/libcurl.so.4
# Run composer
ENV COMPOSER_ALLOW_SUPERUSER 1
RUN composer install --prefer-dist --no-dev --no-scripts --no-suggest
# Expose port 80

View File

@ -51,6 +51,9 @@
"buildpacks": [
{
"url": "heroku/php"
},
{
"url": "https://github.com/heroku/heroku-buildpack-locale"
}
],
"env": {

View File

@ -290,6 +290,7 @@ class TransactionController extends Controller
'withdrawal' => [TransactionType::WITHDRAWAL,],
'withdrawals' => [TransactionType::WITHDRAWAL,],
'expense' => [TransactionType::WITHDRAWAL,],
'expenses' => [TransactionType::WITHDRAWAL,],
'income' => [TransactionType::DEPOSIT,],
'deposit' => [TransactionType::DEPOSIT,],
'deposits' => [TransactionType::DEPOSIT,],

View File

@ -58,8 +58,8 @@ class PiggyBankRequest extends Request
'account_id' => $this->integer('account_id'),
'targetamount' => $this->string('target_amount'),
'current_amount' => $current,
'start_date' => $this->date('start_date'),
'target_date' => $this->date('target_date'),
'startdate' => $this->date('start_date'),
'targetdate' => $this->date('target_date'),
'notes' => $this->string('notes'),
];
}

View File

@ -75,8 +75,10 @@ class RuleRequest extends Request
$validTriggers = array_keys(config('firefly.rule-triggers'));
$validActions = array_keys(config('firefly.rule-actions'));
// some actions require text:
$contextActions = implode(',', config('firefly.rule-actions-text'));
// some triggers and actions require text:
$contextTriggers = implode(',', config('firefly.context-rule-triggers'));
$contextActions = implode(',', config('firefly.context-rule-actions'));
$rules = [
'title' => 'required|between:1,100|uniqueObjectForUser:rules,title',
@ -86,7 +88,7 @@ class RuleRequest extends Request
'trigger' => 'required|in:store-journal,update-journal',
'rule_triggers.*.name' => 'required|in:' . implode(',', $validTriggers),
'rule_triggers.*.stop_processing' => 'boolean',
'rule_triggers.*.value' => 'required|min:1|ruleTriggerValue',
'rule_triggers.*.value' => 'required_if:rule_actions.*.type,' . $contextTriggers . '|min:1|ruleTriggerValue',
'rule_actions.*.name' => 'required|in:' . implode(',', $validActions),
'rule_actions.*.value' => 'required_if:rule_actions.*.type,' . $contextActions . '|ruleActionValue',
'rule_actions.*.stop_processing' => 'boolean',

View File

@ -0,0 +1,392 @@
<?php
namespace FireflyIII\Console\Commands;
use Carbon\Carbon;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Rule;
use FireflyIII\Models\RuleGroup;
use FireflyIII\Models\Transaction;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\TransactionRules\Processor;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
/**
*
* Class ApplyRules
*/
class ApplyRules extends Command
{
use VerifiesAccessToken;
/**
* The console command description.
*
* @var string
*/
protected $description = 'This command will apply your rules and rule groups on a selection of your transactions.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature
= 'firefly:apply-rules
{--user=1 : The user ID that the import should import for.}
{--token= : The user\'s access token.}
{--accounts= : A comma-separated list of asset accounts or liabilities to apply your rules to.}
{--rule_groups= : A comma-separated list of rule groups to apply. Take the ID\'s of these rule groups from the Firefly III interface.}
{--rules= : A comma-separated list of rules to apply. Take the ID\'s of these rules from the Firefly III interface. Using this option overrules the option that selects rule groups.}
{--all_rules : If set, will overrule both settings and simply apply ALL of your rules.}
{--start_date= : The date of the earliest transaction to be included (inclusive). If omitted, will be your very first transaction ever. Format: YYYY-MM-DD}
{--end_date= : The date of the latest transaction to be included (inclusive). If omitted, will be your latest transaction ever. Format: YYYY-MM-DD}';
/** @var Collection */
private $accounts;
/** @var Carbon */
private $endDate;
/** @var Collection */
private $results;
/** @var Collection */
private $ruleGroups;
/** @var Collection */
private $rules;
/** @var Carbon */
private $startDate;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
$this->accounts = new Collection;
$this->rules = new Collection;
$this->ruleGroups = new Collection;
$this->results = new Collection;
}
/**
* Execute the console command.
*
* @return int
* @throws \FireflyIII\Exceptions\FireflyException
*/
public function handle(): int
{
if (!$this->verifyAccessToken()) {
$this->error('Invalid access token.');
return 1;
}
$result = $this->verifyInput();
if (false === $result) {
return 1;
}
// get transactions from asset accounts.
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class);
$collector->setUser($this->getUser());
$collector->setAccounts($this->accounts);
$collector->setRange($this->startDate, $this->endDate);
$transactions = $collector->getTransactions();
$count = $transactions->count();
// first run all rule groups:
/** @var RuleGroupRepositoryInterface $ruleGroupRepos */
$ruleGroupRepos = app(RuleGroupRepositoryInterface::class);
$ruleGroupRepos->setUser($this->getUser());
/** @var RuleGroup $ruleGroup */
foreach ($this->ruleGroups as $ruleGroup) {
$this->line(sprintf('Going to apply rule group "%s" to %d transaction(s).', $ruleGroup->title, $count));
$rules = $ruleGroupRepos->getActiveStoreRules($ruleGroup);
$this->applyRuleSelection($rules, $transactions, true);
}
// then run all rules (rule groups should be empty).
if ($this->rules->count() > 0) {
$this->line(sprintf('Will apply %d rule(s) to %d transaction(s)', $this->rules->count(), $transactions->count()));
$this->applyRuleSelection($this->rules, $transactions, false);
}
// filter results:
$this->results = $this->results->unique(
function (Transaction $transaction) {
return (int)$transaction->journal_id;
}
);
$this->line('');
if (0 === $this->results->count()) {
$this->line('The rules were fired but did not influence any transactions.');
}
if ($this->results->count() > 0) {
$this->line(sprintf('The rule(s) was/were fired, and influenced %d transaction(s).', $this->results->count()));
foreach ($this->results as $result) {
$this->line(
vsprintf(
'Transaction #%d: "%s" (%s %s)',
[
$result->journal_id,
$result->description,
$result->transaction_currency_code,
round($result->transaction_amount, $result->transaction_currency_dp),
]
)
);
}
}
return 0;
}
/**
* @param Collection $rules
* @param Collection $transactions
* @param bool $breakProcessing
*
* @throws \FireflyIII\Exceptions\FireflyException
*/
private function applyRuleSelection(Collection $rules, Collection $transactions, bool $breakProcessing): void
{
$bar = $this->output->createProgressBar($rules->count() * $transactions->count());
foreach ($rules as $rule) {
/** @var Processor $processor */
$processor = app(Processor::class);
$processor->make($rule, true);
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
/** @var Rule $rule */
$bar->advance();
$result = $processor->handleTransaction($transaction);
if (true === $result) {
$this->results->push($transaction);
}
}
if (true === $rule->stop_processing && true === $breakProcessing) {
$this->line('');
$this->line(sprintf('Rule #%d ("%s") says to stop processing.', $rule->id, $rule->title));
return;
}
}
$this->line('');
}
/**
*
*/
private function grabAllRules(): void
{
if (true === $this->option('all_rules')) {
/** @var RuleRepositoryInterface $ruleRepos */
$ruleRepos = app(RuleRepositoryInterface::class);
$ruleRepos->setUser($this->getUser());
$this->rules = $ruleRepos->getAll();
// reset rule groups.
$this->ruleGroups = new Collection;
}
}
/**
*
*/
private function parseDates(): void
{
// parse start date.
$startDate = Carbon::create()->startOfMonth();
$startString = $this->option('start_date');
if (null === $startString) {
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
$repository->setUser($this->getUser());
$first = $repository->firstNull();
if (null !== $first) {
$startDate = $first->date;
}
}
if (null !== $startString && '' !== $startString) {
$startDate = Carbon::createFromFormat('Y-m-d', $startString);
}
// parse end date
$endDate = Carbon::now();
$endString = $this->option('end_date');
if (null !== $endString && '' !== $endString) {
$endDate = Carbon::createFromFormat('Y-m-d', $endString);
}
if ($startDate > $endDate) {
[$endDate, $startDate] = [$startDate, $endDate];
}
$this->startDate = $startDate;
$this->endDate = $endDate;
}
/**
* @return bool
* @throws \FireflyIII\Exceptions\FireflyException
*/
private function verifyInput(): bool
{
// verify account.
$result = $this->verifyInputAccounts();
if (false === $result) {
return $result;
}
// verify rule groups.
$result = $this->verifyRuleGroups();
if (false === $result) {
return $result;
}
// verify rules.
$result = $this->verifyRules();
if (false === $result) {
return $result;
}
$this->grabAllRules();
$this->parseDates();
//$this->line('Number of rules found: ' . $this->rules->count());
$this->line('Start date is ' . $this->startDate->format('Y-m-d'));
$this->line('End date is ' . $this->endDate->format('Y-m-d'));
return true;
}
/**
* @return bool
* @throws \FireflyIII\Exceptions\FireflyException
*/
private function verifyInputAccounts(): bool
{
$accountString = $this->option('accounts');
if (null === $accountString || '' === $accountString) {
$this->error('Please use the --accounts to indicate the accounts to apply rules to.');
return false;
}
$finalList = new Collection;
$accountList = explode(',', $accountString);
if (0 === \count($accountList)) {
$this->error('Please use the --accounts to indicate the accounts to apply rules to.');
return false;
}
/** @var AccountRepositoryInterface $accountRepository */
$accountRepository = app(AccountRepositoryInterface::class);
$accountRepository->setUser($this->getUser());
foreach ($accountList as $accountId) {
$accountId = (int)$accountId;
$account = $accountRepository->findNull($accountId);
if (null !== $account
&& \in_array(
$account->accountType->type, [AccountType::DEFAULT, AccountType::DEBT, AccountType::ASSET, AccountType::LOAN, AccountType::MORTGAGE], true
)) {
$finalList->push($account);
}
}
if (0 === $finalList->count()) {
$this->error('Please make sure all accounts in --accounts are asset accounts or liabilities.');
return false;
}
$this->accounts = $finalList;
return true;
}
/**
* @return bool
* @throws \FireflyIII\Exceptions\FireflyException
*/
private function verifyRuleGroups(): bool
{
$ruleGroupString = $this->option('rule_groups');
if (null === $ruleGroupString || '' === $ruleGroupString) {
// can be empty.
return true;
}
$ruleGroupList = explode(',', $ruleGroupString);
if (0 === \count($ruleGroupList)) {
// can be empty.
return true;
}
/** @var RuleGroupRepositoryInterface $ruleGroupRepos */
$ruleGroupRepos = app(RuleGroupRepositoryInterface::class);
$ruleGroupRepos->setUser($this->getUser());
foreach ($ruleGroupList as $ruleGroupId) {
$ruleGroupId = (int)$ruleGroupId;
$ruleGroup = $ruleGroupRepos->find($ruleGroupId);
$this->ruleGroups->push($ruleGroup);
}
return true;
}
/**
* @return bool
* @throws \FireflyIII\Exceptions\FireflyException
*/
private function verifyRules(): bool
{
$ruleString = $this->option('rules');
if (null === $ruleString || '' === $ruleString) {
// can be empty.
return true;
}
$finalList = new Collection;
$ruleList = explode(',', $ruleString);
if (0 === \count($ruleList)) {
// can be empty.
return true;
}
/** @var RuleRepositoryInterface $ruleRepos */
$ruleRepos = app(RuleRepositoryInterface::class);
$ruleRepos->setUser($this->getUser());
foreach ($ruleList as $ruleId) {
$ruleId = (int)$ruleId;
$rule = $ruleRepos->find($ruleId);
if (null !== $rule) {
$finalList->push($rule);
}
}
if ($finalList->count() > 0) {
// reset rule groups.
$this->ruleGroups = new Collection;
$this->rules = $finalList;
}
return true;
}
}

View File

@ -33,7 +33,7 @@ use FireflyIII\Repositories\ExportJob\ExportJobRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use Illuminate\Console\Command;
use Storage;
use Illuminate\Support\Facades\Storage;
/**
* Class CreateExport.
@ -136,10 +136,14 @@ class CreateExport extends Command
$processor->createZipFile();
$disk = Storage::disk('export');
$fileName = sprintf('export-%s.zip', date('Y-m-d_H-i-s'));
$disk->move($job->key . '.zip', $fileName);
$localPath = storage_path('export') . '/' . $job->key . '.zip';
$this->line('The export has finished! You can find the ZIP file in this location:');
$this->line(storage_path(sprintf('export/%s', $fileName)));
// "move" from local to export disk
$disk->put($fileName, file_get_contents($localPath));
unlink($localPath);
$this->line('The export has finished! You can find the ZIP file in export disk with file name:');
$this->line($fileName);
return 0;
}

View File

@ -39,7 +39,7 @@ class EncryptFile extends Command
*
* @var string
*/
protected $description = 'Encrypts a file and places it in the storage/upload directory.';
protected $description = 'Encrypts a file and places it in the upload disk.';
/**
* The name and signature of the console command.

View File

@ -79,7 +79,7 @@ class UseEncryption extends Command
$fqn = sprintf('FireflyIII\Models\%s', $class);
$encrypt = true === config('firefly.encryption') ? 0 : 1;
/** @noinspection PhpUndefinedMethodInspection */
$set = $fqn::where($indicator, $encrypt)->get();
$set = $fqn::where($indicator, $encrypt)->withTrashed()->get();
foreach ($set as $entry) {
$newName = $entry->$field;

View File

@ -23,17 +23,37 @@ declare(strict_types=1);
namespace FireflyIII\Console\Commands;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\User;
use Log;
/**
* Trait VerifiesAccessToken.
*
* Verifies user access token for sensitive commands.
*
* @codeCoverageIgnore
*/
trait VerifiesAccessToken
{
/**
* @return User
* @throws FireflyException
*/
public function getUser(): User
{
$userId = (int)$this->option('user');
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);
$user = $repository->findNull($userId);
if (null === $user) {
throw new FireflyException('User is unexpectedly NULL');
}
return $user;
}
/**
* Abstract method to make sure trait knows about method "option".
*

View File

@ -42,7 +42,7 @@ use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use Illuminate\Support\Collection;
use Log;
use Storage;
use Illuminate\Support\Facades\Storage;
use ZipArchive;
/**
@ -184,7 +184,7 @@ class ExpandedProcessor implements ProcessorInterface
}
/**
* Create a ZIP file.
* Create a ZIP file locally (!) in storage_path('export').
*
* @return bool
*
@ -195,9 +195,9 @@ class ExpandedProcessor implements ProcessorInterface
{
$zip = new ZipArchive;
$file = $this->job->key . '.zip';
$fullPath = storage_path('export') . '/' . $file;
$localPath = storage_path('export') . '/' . $file;
if (true !== $zip->open($fullPath, ZipArchive::CREATE)) {
if (true !== $zip->open($localPath, ZipArchive::CREATE)) {
throw new FireflyException('Cannot store zip file.');
}
// for each file in the collection, add it to the zip file.

View File

@ -26,7 +26,7 @@ namespace FireflyIII\Export\Exporter;
use FireflyIII\Export\Entry\Entry;
use League\Csv\Writer;
use Storage;
use Illuminate\Support\Facades\Storage;
/**
* Class CsvExporter.
@ -57,15 +57,11 @@ class CsvExporter extends BasicExporter implements ExporterInterface
*/
public function run(): bool
{
// create temporary file:
$this->tempFile();
// necessary for CSV writer:
$fullPath = storage_path('export') . DIRECTORY_SEPARATOR . $this->fileName;
// choose file name:
$this->fileName = $this->job->key . '-records.csv';
//we create the CSV into memory
$writer = Writer::createFromPath($fullPath);
$writer = Writer::createFromString('');
$rows = [];
// get field names for header row:
@ -86,18 +82,9 @@ class CsvExporter extends BasicExporter implements ExporterInterface
$rows[] = $line;
}
$writer->insertAll($rows);
$disk = Storage::disk('export');
$disk->put($this->fileName, $writer->getContent());
return true;
}
/**
* Make a temp file.
*/
private function tempFile()
{
$this->fileName = $this->job->key . '-records.csv';
// touch file in export directory:
$disk = Storage::disk('export');
$disk->put($this->fileName, '');
}
}

View File

@ -120,8 +120,8 @@ class TransactionFactory
Log::debug(sprintf('Expect source destination to be of type "%s"', $destinationType));
// find source and destination account:
$sourceAccount = $this->findAccount($sourceType, $data['source_id'], $data['source_name']);
$destinationAccount = $this->findAccount($destinationType, $data['destination_id'], $data['destination_name']);
$sourceAccount = $this->findAccount($sourceType, (int)$data['source_id'], $data['source_name']);
$destinationAccount = $this->findAccount($destinationType, (int)$data['destination_id'], $data['destination_name']);
if (null === $sourceAccount || null === $destinationAccount) {
$debugData = $data;

View File

@ -29,6 +29,7 @@ use Carbon\Carbon;
use FireflyIII\Generator\Report\ReportGeneratorInterface;
use FireflyIII\Generator\Report\Support;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\DoubleTransactionFilter;
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
@ -216,9 +217,9 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
->setTags($this->tags)->withOpposingAccount();
$collector->removeFilter(TransferFilter::class);
$collector->addFilter(OpposingAccountFilter::class);
$collector->addFilter(PositiveAmountFilter::class);
$collector->addFilter(DoubleTransactionFilter::class);
$transactions = $collector->getTransactions();
$this->expenses = $transactions;

View File

@ -45,6 +45,12 @@ class AutomationHandler
*/
public function reportJournals(RequestedReportOnJournals $event): bool
{
$sendReport = envNonEmpty('SEND_REPORT_JOURNALS', true);
if (false === $sendReport) {
return true;
}
Log::debug('In reportJournals.');
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);

View File

@ -30,7 +30,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\MessageBag;
use Log;
use Storage;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
@ -94,7 +94,7 @@ class AttachmentHelper implements AttachmentHelperInterface
}
/**
* Returns the file location for an attachment,
* Returns the file path relative to upload disk for an attachment,
*
* @param Attachment $attachment
*
@ -102,8 +102,7 @@ class AttachmentHelper implements AttachmentHelperInterface
*/
public function getAttachmentLocation(Attachment $attachment): string
{
$path = sprintf('%s%sat-%d.data', storage_path('upload'), DIRECTORY_SEPARATOR, (int)$attachment->id);
$path = sprintf('%sat-%d.data', DIRECTORY_SEPARATOR, (int)$attachment->id);
return $path;
}
@ -192,7 +191,7 @@ class AttachmentHelper implements AttachmentHelperInterface
public function saveAttachmentsForModel(object $model, ?array $files): bool
{
if(!($model instanceof Model)) {
return false;
return false; // @codeCoverageIgnore
}
Log::debug(sprintf('Now in saveAttachmentsForModel for model %s', \get_class($model)));
if (\is_array($files)) {
@ -270,7 +269,7 @@ class AttachmentHelper implements AttachmentHelperInterface
$fileObject->rewind();
if(0 === $file->getSize()) {
throw new FireflyException('Cannot upload empty or non-existent file.');
throw new FireflyException('Cannot upload empty or non-existent file.'); // @codeCoverageIgnore
}
$content = $fileObject->fread($file->getSize());

View File

@ -28,6 +28,7 @@ use Carbon\Carbon;
use DB;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Filter\CountAttachmentsFilter;
use FireflyIII\Helpers\Filter\DoubleTransactionFilter;
use FireflyIII\Helpers\Filter\FilterInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
@ -53,26 +54,14 @@ use Log;
/**
* Class TransactionCollector
*
* @codeCoverageIgnore
* @codeCoverageIgnore
*/
class TransactionCollector implements TransactionCollectorInterface
{
/**
* Constructor.
*/
public function __construct()
{
if ('testing' === env('APP_ENV')) {
Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this)));
}
}
/** @var array */
private $accountIds = [];
/** @var int */
private $count = 0;
/** @var array */
private $fields
= [
@ -139,6 +128,16 @@ class TransactionCollector implements TransactionCollectorInterface
/** @var User */
private $user;
/**
* Constructor.
*/
public function __construct()
{
if ('testing' === env('APP_ENV')) {
Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this)));
}
}
/**
* @param string $filter
*
@ -253,6 +252,30 @@ class TransactionCollector implements TransactionCollectorInterface
return $this->count;
}
/**
* @return LengthAwarePaginator
* @throws FireflyException
*/
public function getPaginatedTransactions(): LengthAwarePaginator
{
if (true === $this->run) {
throw new FireflyException('Cannot getPaginatedTransactions after run in TransactionCollector.');
}
$this->count();
$set = $this->getTransactions();
$journals = new LengthAwarePaginator($set, $this->count, $this->limit, $this->page);
return $journals;
}
/**
* @return EloquentBuilder
*/
public function getQuery(): EloquentBuilder
{
return $this->query;
}
/**
* @return Collection
*/
@ -309,30 +332,6 @@ class TransactionCollector implements TransactionCollectorInterface
return $set;
}
/**
* @return LengthAwarePaginator
* @throws FireflyException
*/
public function getPaginatedTransactions(): LengthAwarePaginator
{
if (true === $this->run) {
throw new FireflyException('Cannot getPaginatedTransactions after run in TransactionCollector.');
}
$this->count();
$set = $this->getTransactions();
$journals = new LengthAwarePaginator($set, $this->count, $this->limit, $this->page);
return $journals;
}
/**
* @return EloquentBuilder
*/
public function getQuery(): EloquentBuilder
{
return $this->query;
}
/**
* @return TransactionCollectorInterface
*/
@ -784,14 +783,15 @@ class TransactionCollector implements TransactionCollectorInterface
{
// create all possible filters:
$filters = [
InternalTransferFilter::class => new InternalTransferFilter($this->accountIds),
OpposingAccountFilter::class => new OpposingAccountFilter($this->accountIds),
TransferFilter::class => new TransferFilter,
PositiveAmountFilter::class => new PositiveAmountFilter,
NegativeAmountFilter::class => new NegativeAmountFilter,
SplitIndicatorFilter::class => new SplitIndicatorFilter,
CountAttachmentsFilter::class => new CountAttachmentsFilter,
TransactionViewFilter::class => new TransactionViewFilter,
InternalTransferFilter::class => new InternalTransferFilter($this->accountIds),
OpposingAccountFilter::class => new OpposingAccountFilter($this->accountIds),
TransferFilter::class => new TransferFilter,
PositiveAmountFilter::class => new PositiveAmountFilter,
NegativeAmountFilter::class => new NegativeAmountFilter,
SplitIndicatorFilter::class => new SplitIndicatorFilter,
CountAttachmentsFilter::class => new CountAttachmentsFilter,
TransactionViewFilter::class => new TransactionViewFilter,
DoubleTransactionFilter::class => new DoubleTransactionFilter,
];
Log::debug(sprintf('Will run %d filters on the set.', \count($this->filters)));
foreach ($this->filters as $enabled) {

View File

@ -0,0 +1,60 @@
<?php
/**
* DoubleTransactionFilter.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Helpers\Filter;
use FireflyIII\Models\Transaction;
use Illuminate\Support\Collection;
/**
*
* Used when the final collection contains double transactions, which can happen when viewing the tag report.
* Class DoubleTransactionFilter
*/
class DoubleTransactionFilter implements FilterInterface
{
/**
* Apply the filter.
*
* @param Collection $set
*
* @return Collection
*/
public function filter(Collection $set): Collection
{
$count = [];
$result = new Collection;
/** @var Transaction $transaction */
foreach ($set as $transaction) {
$id = (int)$transaction->id;
$count[$id] = isset($count[$id]) ? $count[$id] + 1 : 1;
if (1 === $count[$id]) {
$result->push($transaction);
}
}
return $result;
}
}

View File

@ -63,10 +63,14 @@ class TransferFilter implements FilterInterface
$key = $journalId . '-' . implode(',', $transactionIds) . '-' . implode(',', $accountIds) . '-' . $amount;
Log::debug(sprintf('Current transaction key is "%s"', $key));
if (!isset($count[$key])) {
Log::debug(sprintf('First instance of transaction #%d, add it.', $transaction->id));
// not yet counted? add to new set and count it:
$new->push($transaction);
$count[$key] = 1;
}
if (isset($count[$key])) {
Log::debug(sprintf('Second instance of transaction #%d, do NOT add it.', $transaction->id));
}
}
return $new;

View File

@ -123,7 +123,6 @@ class ReconcileController extends Controller
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
* @throws FireflyException
*/
public function reconcile(Account $account, Carbon $start = null, Carbon $end = null)
{

View File

@ -30,6 +30,7 @@ use FireflyIII\User;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Log;
/**
* Class ForgotPasswordController
@ -58,6 +59,13 @@ class ForgotPasswordController extends Controller
*/
public function sendResetLinkEmail(Request $request, UserRepositoryInterface $repository)
{
$loginProvider = envNonEmpty('LOGIN_PROVIDER','eloquent');
if ('eloquent' !== $loginProvider) {
$message = sprintf('Cannot reset password when authenticating over "%s".', $loginProvider);
Log::error($message);
return view('error', compact('message'));
}
$this->validateEmail($request);
// verify if the user is not a demo user. If so, we give him back an error.
@ -90,6 +98,13 @@ class ForgotPasswordController extends Controller
*/
public function showLinkRequestForm()
{
$loginProvider = envNonEmpty('LOGIN_PROVIDER','eloquent');
if ('eloquent' !== $loginProvider) {
$message = sprintf('Cannot reset password when authenticating over "%s".', $loginProvider);
return view('error', compact('message'));
}
// is allowed to?
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$userCount = User::count();

View File

@ -129,8 +129,9 @@ class LoginController extends Controller
*/
public function showLoginForm(Request $request)
{
$count = DB::table('users')->count();
if (0 === $count) {
$count = DB::table('users')->count();
$loginProvider = envNonEmpty('LOGIN_PROVIDER','eloquent');
if (0 === $count && 'eloquent' === $loginProvider) {
return redirect(route('register')); // @codeCoverageIgnore
}
@ -141,13 +142,20 @@ class LoginController extends Controller
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$userCount = User::count();
$allowRegistration = true;
$allowReset = true;
if (true === $singleUserMode && $userCount > 0) {
$allowRegistration = false;
}
// single user mode is ignored when the user is not using eloquent:
if ('eloquent' !== $loginProvider) {
$allowRegistration = false;
$allowReset = false;
}
$email = $request->old('email');
$remember = $request->old('remember');
return view('auth.login', compact('allowRegistration', 'email', 'remember'));
return view('auth.login', compact('allowRegistration', 'email', 'remember','allowReset'));
}
}

View File

@ -71,9 +71,19 @@ class RegisterController extends Controller
public function register(Request $request)
{
// is allowed to?
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$userCount = User::count();
if (true === $singleUserMode && $userCount > 0) {
$allowRegistration = true;
$loginProvider = envNonEmpty('LOGIN_PROVIDER','eloquent');
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$userCount = User::count();
if (true === $singleUserMode && $userCount > 0 && 'eloquent' === $loginProvider) {
$allowRegistration = false;
}
if ('eloquent' !== $loginProvider) {
$allowRegistration = false;
}
if (false === $allowRegistration) {
$message = 'Registration is currently not available.';
return view('error', compact('message'));
@ -102,13 +112,25 @@ class RegisterController extends Controller
*/
public function showRegistrationForm(Request $request)
{
// is demo site?
$isDemoSite = FireflyConfig::get('is_demo_site', config('firefly.configuration.is_demo_site'))->data;
$allowRegistration = true;
$loginProvider = envNonEmpty('LOGIN_PROVIDER','eloquent');
$isDemoSite = FireflyConfig::get('is_demo_site', config('firefly.configuration.is_demo_site'))->data;
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$userCount = User::count();
// is allowed to?
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$userCount = User::count();
if (true === $singleUserMode && $userCount > 0) {
if (true === $isDemoSite) {
$allowRegistration = false;
}
if (true === $singleUserMode && $userCount > 0 && 'eloquent' === $loginProvider) {
$allowRegistration = false;
}
if ('eloquent' !== $loginProvider) {
$allowRegistration = false;
}
if (false === $allowRegistration) {
$message = 'Registration is currently not available.';
return view('error', compact('message'));

View File

@ -28,6 +28,7 @@ use FireflyIII\Http\Controllers\Controller;
use FireflyIII\User;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
/**
* Class ResetPasswordController
@ -70,7 +71,15 @@ class ResetPasswordController extends Controller
*/
public function showResetForm(Request $request, $token = null)
{
// is allowed to?
$loginProvider = envNonEmpty('LOGIN_PROVIDER','eloquent');
if ('eloquent' !== $loginProvider) {
$message = sprintf('Cannot reset password when authenticating over "%s".', $loginProvider);
return view('error', compact('message'));
}
// is allowed to register?
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$userCount = User::count();
$allowRegistration = true;
@ -83,4 +92,42 @@ class ResetPasswordController extends Controller
['token' => $token, 'email' => $request->email, 'allowRegistration' => $allowRegistration]
);
}
/**
* Reset the given user's password.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
* @throws \Illuminate\Validation\ValidationException
*/
public function reset(Request $request)
{
$loginProvider = envNonEmpty('LOGIN_PROVIDER','eloquent');
if ('eloquent' !== $loginProvider) {
$message = sprintf('Cannot reset password when authenticating over "%s".', $loginProvider);
return view('error', compact('message'));
}
$this->validate($request, $this->rules(), $this->validationErrorMessages());
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request), function ($user, $password) {
$this->resetPassword($user, $password);
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $response === Password::PASSWORD_RESET
? $this->sendResetResponse($request, $response)
: $this->sendResetFailedResponse($request, $response);
}
}

View File

@ -28,6 +28,7 @@ use Carbon\Carbon;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Support\Http\Controllers\DateCalculation;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
@ -63,7 +64,6 @@ class IndexController extends Controller
);
}
/**
* Show all budgets.
*
@ -134,5 +134,29 @@ class IndexController extends Controller
);
}
/**
* @param Request $request
*
* @return JsonResponse
*/
public function reorder(Request $request, BudgetRepositoryInterface $repository): JsonResponse
{
$budgetIds = $request->get('budgetIds');
$page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
$currentOrder = (($page - 1) * $pageSize) + 1;
foreach ($budgetIds as $budgetId) {
$budgetId = (int)$budgetId;
$budget = $repository->findNull($budgetId);
if (null !== $budget) {
$repository->setBudgetOrder($budget, $currentOrder);
}
$currentOrder++;
}
return response()->json(['OK']);
}
}

View File

@ -88,7 +88,7 @@ class ShowController extends Controller
'firefly.without_budget_between',
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
$periods = $this->getBudgetPeriodOverview($end);
$periods = $this->getNoBudgetPeriodOverview($end);
$page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;

View File

@ -375,7 +375,7 @@ class AccountController extends Controller
$cache->addProperty($end);
$cache->addProperty('chart.account.income-category');
if ($cache->has()) {
//return response()->json($cache->get()); // @codeCoverageIgnore
return response()->json($cache->get()); // @codeCoverageIgnore
}
// grab all journals:
@ -531,7 +531,7 @@ class AccountController extends Controller
$cache->addProperty($end);
$cache->addProperty('chart.account.revenue-accounts');
if ($cache->has()) {
//return response()->json($cache->get()); // @codeCoverageIgnore
return response()->json($cache->get()); // @codeCoverageIgnore
}
$start->subDay();

View File

@ -68,7 +68,7 @@ class BillController extends Controller
$cache->addProperty($end);
$cache->addProperty('chart.bill.frontpage');
if ($cache->has()) {
//return response()->json($cache->get()); // @codeCoverageIgnore
return response()->json($cache->get()); // @codeCoverageIgnore
}
/** @var CurrencyRepositoryInterface $currencyRepository */
$currencyRepository = app(CurrencyRepositoryInterface::class);

View File

@ -32,6 +32,7 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use FireflyIII\Support\Http\Controllers\DateCalculation;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
@ -40,6 +41,7 @@ use Illuminate\Support\Collection;
*/
class CategoryController extends Controller
{
use DateCalculation;
/** @var GeneratorInterface Chart generation methods. */
protected $generator;
@ -97,17 +99,36 @@ class CategoryController extends Controller
'entries' => [], 'type' => 'line', 'fill' => false,
],
];
while ($start <= $end) {
$currentEnd = app('navigation')->endOfPeriod($start, $range);
$spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $start, $currentEnd);
$earned = $repository->earnedInPeriod(new Collection([$category]), $accounts, $start, $currentEnd);
$sum = bcadd($spent, $earned);
$label = app('navigation')->periodShow($start, $range);
$chartData[0]['entries'][$label] = round(bcmul($spent, '-1'), 12);
$chartData[1]['entries'][$label] = round($earned, 12);
$chartData[2]['entries'][$label] = round($sum, 12);
$start = app('navigation')->addPeriod($start, $range, 0);
$step = $this->calculateStep($start, $end);
$current = clone $start;
switch ($step) {
case '1D':
while ($current <= $end) {
$spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $current, $current);
$earned = $repository->earnedInPeriod(new Collection([$category]), $accounts, $current, $current);
$sum = bcadd($spent, $earned);
$label = app('navigation')->periodShow($current, $step);
$chartData[0]['entries'][$label] = round(bcmul($spent, '-1'), 12);
$chartData[1]['entries'][$label] = round($earned, 12);
$chartData[2]['entries'][$label] = round($sum, 12);
$current->addDay();
}
break;
case '1W':
case '1M':
case '1Y':
while ($current <= $end) {
$currentEnd = app('navigation')->endOfPeriod($current, $range);
$spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $current, $currentEnd);
$earned = $repository->earnedInPeriod(new Collection([$category]), $accounts, $current, $currentEnd);
$sum = bcadd($spent, $earned);
$label = app('navigation')->periodShow($current, $step);
$chartData[0]['entries'][$label] = round(bcmul($spent, '-1'), 12);
$chartData[1]['entries'][$label] = round($earned, 12);
$chartData[2]['entries'][$label] = round($sum, 12);
$current= app('navigation')->addPeriod($current, $step, 0);
}
break;
}
$data = $this->generator->multiSet($chartData);
@ -135,7 +156,7 @@ class CategoryController extends Controller
$cache->addProperty($end);
$cache->addProperty('chart.category.frontpage');
if ($cache->has()) {
//return response()->json($cache->get()); // @codeCoverageIgnore
return response()->json($cache->get()); // @codeCoverageIgnore
}
// currency repos:
@ -168,7 +189,7 @@ class CategoryController extends Controller
$noCategory = $repository->spentInPeriodPcWoCategory(new Collection, $start, $end);
foreach ($noCategory as $currencyId => $spent) {
$currencies[$currencyId] = $currencies[$currencyId] ?? $currencyRepository->findNull($currencyId);
$tempData[] = [
$tempData[] = [
'name' => trans('firefly.no_category'),
'spent' => bcmul($spent, '-1'),
'spent_float' => (float)bcmul($spent, '-1'),

View File

@ -85,7 +85,7 @@ class JobStatusController extends Controller
*/
public function json(ImportJob $importJob): JsonResponse
{
$count = \count($importJob->transactions);
$count = $this->repository->countTransactions($importJob);
$json = [
'status' => $importJob->status,
'errors' => $importJob->errors,

View File

@ -48,7 +48,9 @@ class JavascriptController extends Controller
*/
public function accounts(AccountRepositoryInterface $repository, CurrencyRepositoryInterface $currencyRepository): Response
{
$accounts = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::DEBT,AccountType::LOAN,AccountType::MORTGAGE, AccountType::CREDITCARD]);
$accounts = $repository->getAccountsByType(
[AccountType::DEFAULT, AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD]
);
$preference = app('preferences')->get('currencyPreference', config('firefly.default_currency', 'EUR'));
/** @noinspection NullPointerExceptionInspection */
$default = $currencyRepository->findByCodeNull($preference->data);
@ -124,6 +126,7 @@ class JavascriptController extends Controller
/** @noinspection NullPointerExceptionInspection */
$lang = $pref->data;
$dateRange = $this->getDateRangeConfig();
$uid = substr(hash('sha256', auth()->user()->id . auth()->user()->email), 0, 12);
$data = [
'currencyCode' => $currency->code,
@ -133,6 +136,7 @@ class JavascriptController extends Controller
'language' => $lang,
'dateRangeTitle' => $dateRange['title'],
'dateRangeConfig' => $dateRange['configuration'],
'uid' => $uid,
];
$request->session()->keep(['two-factor-secret']);

View File

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Json;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Account;
@ -37,58 +38,16 @@ use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
/**
* TODO refactor so each auto-complete thing is a function call because lots of code duplication.
* Class AutoCompleteController.
*
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class AutoCompleteController extends Controller
{
/**
* Returns a JSON list of all accounts.
*
* @param Request $request
* @param AccountRepositoryInterface $repository
*
* @return JsonResponse
*/
public function allAccounts(Request $request, AccountRepositoryInterface $repository): JsonResponse
{
$search = (string)$request->get('search');
$cache = new CacheProperties;
$cache->addProperty('ac-all-accounts');
// very unlikely a user will actually search for this string.
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
}
// find everything:
$return = array_values(
array_unique(
$repository->getAccountsByType(
[AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET]
)->pluck('name')->toArray()
)
);
if ('' !== $search) {
$return = array_values(
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
);
}
$cache->store($return);
return response()->json($return);
}
/**
* List of all journals.
*
@ -106,7 +65,7 @@ class AutoCompleteController extends Controller
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
return response()->json($cache->get()); // @codeCoverageIgnore
}
// find everything:
$collector->setLimit(250)->setPage(1);
@ -129,251 +88,79 @@ class AutoCompleteController extends Controller
}
/**
* List of revenue accounts.
*
* @param Request $request
* @param AccountRepositoryInterface $repository
* @param Request $request
* @param string $subject
*
* @throws FireflyException
* @return JsonResponse
*/
public function assetAccounts(Request $request, AccountRepositoryInterface $repository): JsonResponse
public function autoComplete(Request $request, string $subject): JsonResponse
{
$search = (string)$request->get('search');
$cache = new CacheProperties;
$cache->addProperty('ac-asset-accounts');
// very unlikely a user will actually search for this string.
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
}
// find everything:
$set = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
$filtered = $set->filter(
function (Account $account) {
if (true === $account->active) {
return $account;
}
$search = (string)$request->get('search');
$unfiltered = null;
$filtered = null;
return false; // @codeCoverageIgnore
}
);
$return = array_values(array_unique($filtered->pluck('name')->toArray()));
if ('' !== $search) {
$return = array_values(
array_unique(
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
)
// search for all accounts.
if ('all-accounts' === $subject) {
$unfiltered = $this->getAccounts(
[AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET, AccountType::LOAN,
AccountType::DEBT, AccountType::MORTGAGE]
);
}
$cache->store($return);
return response()->json($return);
}
/**
* Returns a JSON list of all bills.
*
* @param Request $request
* @param BillRepositoryInterface $repository
*
* @return JsonResponse
*/
public function bills(Request $request, BillRepositoryInterface $repository): JsonResponse
{
$search = (string)$request->get('search');
$cache = new CacheProperties;
$cache->addProperty('ac-bills');
// very unlikely a user will actually search for this string.
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
// search for expense accounts.
if ('expense-accounts' === $subject) {
$unfiltered = $this->getAccounts([AccountType::EXPENSE, AccountType::BENEFICIARY]);
}
// find everything:
$return = array_unique($repository->getActiveBills()->pluck('name')->toArray());
if ('' !== $search) {
$return = array_values(
array_unique(
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
)
);
// search for revenue accounts.
if ('revenue-accounts' === $subject) {
$unfiltered = $this->getAccounts([AccountType::REVENUE]);
}
$cache->store($return);
return response()->json($return);
}
/**
* List of budgets.
*
* @param Request $request
* @param BudgetRepositoryInterface $repository
*
* @return JsonResponse
*/
public function budgets(Request $request, BudgetRepositoryInterface $repository): JsonResponse
{
$search = (string)$request->get('search');
$cache = new CacheProperties;
$cache->addProperty('ac-budgets');
// very unlikely a user will actually search for this string.
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
// search for asset accounts.
if ('asset-accounts' === $subject) {
$unfiltered = $this->getAccounts([AccountType::ASSET, AccountType::DEFAULT]);
}
// find everything:
$return = array_unique($repository->getBudgets()->pluck('name')->toArray());
if ('' !== $search) {
$return = array_values(
array_unique(
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
)
);
// search for categories.
if ('categories' === $subject) {
$unfiltered = $this->getCategories();
}
$cache->store($return);
return response()->json($return);
}
/**
* Returns a list of categories.
*
* @param Request $request
* @param CategoryRepositoryInterface $repository
*
* @return JsonResponse
*/
public function categories(Request $request, CategoryRepositoryInterface $repository): JsonResponse
{
$search = (string)$request->get('search');
$cache = new CacheProperties;
$cache->addProperty('ac-categories');
// very unlikely a user will actually search for this string.
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
// search for budgets.
if ('budgets' === $subject) {
$unfiltered = $this->getBudgets();
}
// find everything:
$return = array_unique($repository->getCategories()->pluck('name')->toArray());
if ('' !== $search) {
$return = array_values(
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
);
// search for tags
if ('tags' === $subject) {
$unfiltered = $this->getTags();
}
$cache->store($return);
return response()->json($return);
}
/**
* List of currency names.
*
* @param Request $request
* @param CurrencyRepositoryInterface $repository
*
* @return JsonResponse
*/
public function currencyNames(Request $request, CurrencyRepositoryInterface $repository): JsonResponse
{
$search = (string)$request->get('search');
$cache = new CacheProperties;
$cache->addProperty('ac-currency-names');
// very unlikely a user will actually search for this string.
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
// search for bills
if ('bills' === $subject) {
$unfiltered = $this->getBills();
}
// find everything:
$return = $repository->get()->pluck('name')->toArray();
sort($return);
if ('' !== $search) {
$return = array_values(
array_unique(
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
)
);
// search for currency names.
if ('currency-names' === $subject) {
$unfiltered = $this->getCurrencyNames();
}
$cache->store($return);
return response()->json($return);
}
/**
* Returns a JSON list of all beneficiaries.
*
* @param Request $request
* @param AccountRepositoryInterface $repository
*
* @return JsonResponse
*/
public function expenseAccounts(Request $request, AccountRepositoryInterface $repository): JsonResponse
{
$search = (string)$request->get('search');
$cache = new CacheProperties;
$cache->addProperty('ac-expense-accounts');
// very unlikely a user will actually search for this string.
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
if ('transaction_types' === $subject) {
$unfiltered = $this->getTransactionTypes();
}
// find everything:
$set = $repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]);
$filtered = $set->filter(
function (Account $account) {
if (true === $account->active) {
return $account;
}
return false;
}
);
$return = array_unique($filtered->pluck('name')->toArray());
sort($return);
if ('' !== $search) {
$return = array_values(
array_unique(
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
)
);
if ('transaction-types' === $subject) {
$unfiltered = $this->getTransactionTypes();
}
$cache->store($return);
return response()->json($return);
// filter results
$filtered = $this->filterResult($unfiltered, $search);
if (null === $filtered) {
throw new FireflyException(sprintf('Auto complete handler cannot handle "%s"', $subject)); // @codeCoverageIgnore
}
return response()->json($filtered);
}
/**
@ -394,7 +181,7 @@ class AutoCompleteController extends Controller
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
return response()->json($cache->get()); // @codeCoverageIgnore
}
// find everything:
$collector->setLimit(400)->setPage(1);
@ -413,105 +200,14 @@ class AutoCompleteController extends Controller
sort($return);
if ('' !== $search) {
$return = array_values(
array_unique(
array_filter(
$return, function (array $array) use ($search) {
$value = $array['name'];
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
)
);
}
$cache->store($return);
return response()->json($return);
}
/**
* List of revenue accounts.
*
* @param Request $request
* @param AccountRepositoryInterface $repository
*
* @return JsonResponse
*/
public function revenueAccounts(Request $request, AccountRepositoryInterface $repository): JsonResponse
{
$search = (string)$request->get('search');
$cache = new CacheProperties;
$cache->addProperty('ac-revenue-accounts');
// very unlikely a user will actually search for this string.
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
}
// find everything:
$set = $repository->getAccountsByType([AccountType::REVENUE]);
$filtered = $set->filter(
function (Account $account) {
if (true === $account->active) {
return $account;
}
return false;
$return = array_filter(
$return, function (array $array) use ($search) {
$haystack = $array['name'];
$result = stripos($haystack, $search);
return !(false === $result);
}
);
$return = array_unique($filtered->pluck('name')->toArray());
sort($return);
if ('' !== $search) {
$return = array_values(
array_unique(
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
)
);
}
$cache->store($return);
return response()->json($return);
}
/**
* Returns a JSON list of all beneficiaries.
*
* @param Request $request
* @param TagRepositoryInterface $tagRepository
*
* @return JsonResponse
*/
public function tags(Request $request, TagRepositoryInterface $tagRepository): JsonResponse
{
$search = (string)$request->get('search');
$cache = new CacheProperties;
$cache->addProperty('ac-revenue-accounts');
// very unlikely a user will actually search for this string.
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
}
// find everything:
$return = array_unique($tagRepository->get()->pluck('tag')->toArray());
sort($return);
if ('' !== $search) {
$return = array_values(
array_unique(
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
)
);
}
$cache->store($return);
@ -536,7 +232,7 @@ class AutoCompleteController extends Controller
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
return response()->json($cache->get()); // @codeCoverageIgnore
}
// find everything:
$type = config('firefly.transactionTypesByWhat.' . $what);
@ -563,43 +259,119 @@ class AutoCompleteController extends Controller
}
/**
* List if transaction types.
* @param array $unfiltered
* @param string $query
*
* @param Request $request
* @param JournalRepositoryInterface $repository
*
* @return JsonResponse
* @return array|null
*/
public function transactionTypes(Request $request, JournalRepositoryInterface $repository): JsonResponse
private function filterResult(?array $unfiltered, string $query): ?array
{
$search = (string)$request->get('search');
$cache = new CacheProperties;
$cache->addProperty('ac-revenue-accounts');
// very unlikely a user will actually search for this string.
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key);
if ($cache->has()) {
return response()->json($cache->get());
if (null === $unfiltered) {
return null; // @codeCoverageIgnore
}
// find everything:
$return = array_unique($repository->getTransactionTypes()->pluck('type')->toArray());
sort($return);
if ('' === $query) {
sort($unfiltered);
if ('' !== $search) {
return $unfiltered;
}
$return = [];
if ('' !== $query) {
$return = array_values(
array_unique(
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
array_filter(
$unfiltered, function (string $value) use ($query) {
return !(false === stripos($value, $query));
}, ARRAY_FILTER_USE_BOTH
)
);
}
$cache->store($return);
sort($return);
return response()->json($return);
return $return;
}
/**
* @param string $query
* @param array $types
*
* @return array
*/
private function getAccounts(array $types): array
{
$repository = app(AccountRepositoryInterface::class);
// find everything:
/** @var Collection $collection */
$collection = $repository->getAccountsByType($types);
$filtered = $collection->filter(
function (Account $account) {
return $account->active === true;
}
);
$return = array_values(array_unique($filtered->pluck('name')->toArray()));
return $return;
}
/**
* @return array
*/
private function getBills(): array
{
$repository = app(BillRepositoryInterface::class);
return array_unique($repository->getActiveBills()->pluck('name')->toArray());
}
/**
* @return array
*/
private function getBudgets(): array
{
$repository = app(BudgetRepositoryInterface::class);
return array_unique($repository->getBudgets()->pluck('name')->toArray());
}
/**
* @return array
*/
private function getCategories(): array
{
$repository = app(CategoryRepositoryInterface::class);
return array_unique($repository->getCategories()->pluck('name')->toArray());
}
/**
* @return array
*/
private function getCurrencyNames(): array
{
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
return $repository->get()->pluck('name')->toArray();
}
/**
* @return array
*/
private function getTags(): array
{
/** @var TagRepositoryInterface $repository */
$repository = app(TagRepositoryInterface::class);
return array_unique($repository->get()->pluck('tag')->toArray());
}
/**
* @return array
*/
private function getTransactionTypes(): array
{
$repository = app(JournalRepositoryInterface::class);
return array_unique($repository->getTransactionTypes()->pluck('type')->toArray());
}
}

View File

@ -111,15 +111,49 @@ class ReconcileController extends Controller
$cleared = $this->repository->getTransactionsById($clearedIds);
$countCleared = 0;
Log::debug('Start transaction loop');
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$amount = bcadd($amount, $transaction->amount);
}
// find the account and opposing account for this transaction
Log::debug(sprintf('Now at transaction #%d: %s', $transaction->journal_id, $transaction->description));
$srcAccount = $this->accountRepos->findNull((int)$transaction->account_id);
$dstAccount = $this->accountRepos->findNull((int)$transaction->opposing_account_id);
$srcCurrency = (int)$this->accountRepos->getMetaValue($srcAccount, 'currency_id');
$dstCurrency = (int)$this->accountRepos->getMetaValue($dstAccount, 'currency_id');
// is $account source or destination?
if ($account->id === $srcAccount->id) {
// source, and it matches the currency id or is 0
if ($srcCurrency === $transaction->transaction_currency_id || 0 === $srcCurrency) {
Log::debug(sprintf('Source matches currency: %s', $transaction->transaction_amount));
$amount = bcadd($amount, $transaction->transaction_amount);
}
// destination, and it matches the foreign currency ID.
if ($srcCurrency === $transaction->foreign_currency_id) {
Log::debug(sprintf('Source matches foreign currency: %s', $transaction->transaction_foreign_amount));
$amount = bcadd($amount, $transaction->transaction_foreign_amount);
}
}
if ($account->id === $dstAccount->id) {
// destination, and it matches the currency id or is 0
if ($dstCurrency === $transaction->transaction_currency_id || 0 === $dstCurrency) {
Log::debug(sprintf('Destination matches currency: %s', app('steam')->negative($transaction->transaction_amount)));
$amount = bcadd($amount, app('steam')->negative($transaction->transaction_amount));
}
// destination, and it matches the foreign currency ID.
if ($dstCurrency === $transaction->foreign_currency_id) {
Log::debug(sprintf('Destination matches foreign currency: %s', $transaction->transaction_foreign_amount));
$amount = bcadd($amount, $transaction->transaction_foreign_amount);
}
}
Log::debug(sprintf('Amount is now %s', $amount));
}
Log::debug('End transaction loop');
/** @var Transaction $transaction */
foreach ($cleared as $transaction) {
if ($transaction->transactionJournal->date <= $end) {
$clearedAmount = bcadd($clearedAmount, $transaction->amount); // @codeCoverageIgnore
if ($transaction->date <= $end) {
$clearedAmount = bcadd($clearedAmount, $transaction->transaction_amount); // @codeCoverageIgnore
++$countCleared;
}
}
@ -201,6 +235,7 @@ class ReconcileController extends Controller
Log::debug(sprintf('Could not render: %s', $e->getMessage()));
$html = 'Could not render accounts.reconcile.transactions';
}
// @codeCoverageIgnoreEnd
return response()->json(['html' => $html, 'startBalance' => $startBalance, 'endBalance' => $endBalance]);

View File

@ -41,6 +41,7 @@ use FireflyIII\User;
use Google2FA;
use Hash;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Laravel\Passport\ClientRepository;
use Log;
@ -71,6 +72,7 @@ class ProfileController extends Controller
return $next($request);
}
);
$this->middleware(IsDemoUser::class)->except(['index']);
$this->middleware(IsSandStormUser::class)->except('index');
}
@ -80,8 +82,15 @@ class ProfileController extends Controller
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function changeEmail()
public function changeEmail(Request $request)
{
$loginProvider = config('firefly.login_provider');
if ('eloquent' !== $loginProvider) {
$request->session()->flash('error', trans('firefly.login_provider_local_only', ['login_provider' => $loginProvider]));
return redirect(route('profile.index'));
}
$title = auth()->user()->email;
$email = auth()->user()->email;
$subTitle = (string)trans('firefly.change_your_email');
@ -95,8 +104,15 @@ class ProfileController extends Controller
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function changePassword()
public function changePassword(Request $request)
{
$loginProvider = config('firefly.login_provider');
if ('eloquent' !== $loginProvider) {
$request->session()->flash('error', trans('firefly.login_provider_local_only', ['login_provider' => $loginProvider]));
return redirect(route('profile.index'));
}
$title = auth()->user()->email;
$subTitle = (string)trans('firefly.change_your_password');
$subTitleIcon = 'fa-key';
@ -132,6 +148,10 @@ class ProfileController extends Controller
*/
public function confirmEmailChange(UserRepositoryInterface $repository, string $token)
{
$loginProvider = config('firefly.login_provider');
if ('eloquent' !== $loginProvider) {
throw new FireflyException('Cannot confirm email change when authentication provider is not local.');
}
// find preference with this token value.
/** @var Collection $set */
$set = app('preferences')->findByName('email_change_confirm_token');
@ -163,8 +183,12 @@ class ProfileController extends Controller
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function deleteAccount()
public function deleteAccount(Request $request)
{
$loginProvider = config('firefly.login_provider');
if ('eloquent' !== $loginProvider) {
$request->session()->flash('warning', trans('firefly.delete_local_info_only', ['login_provider' => $loginProvider]));
}
$title = auth()->user()->email;
$subTitle = (string)trans('firefly.delete_account');
$subTitleIcon = 'fa-trash';
@ -216,6 +240,7 @@ class ProfileController extends Controller
*/
public function index()
{
$loginProvider = config('firefly.login_provider');
// check if client token thing exists (default one)
$count = DB::table('oauth_clients')
->where('personal_access_client', 1)
@ -241,7 +266,7 @@ class ProfileController extends Controller
$accessToken = app('preferences')->set('access_token', $token);
}
return view('profile.index', compact('subTitle', 'userId', 'accessToken', 'enabled2FA'));
return view('profile.index', compact('subTitle', 'userId', 'accessToken', 'enabled2FA', 'loginProvider'));
}
/**
@ -254,6 +279,13 @@ class ProfileController extends Controller
*/
public function postChangeEmail(EmailFormRequest $request, UserRepositoryInterface $repository)
{
$loginProvider = config('firefly.login_provider');
if ('eloquent' !== $loginProvider) {
$request->session()->flash('error', trans('firefly.login_provider_local_only', ['login_provider' => $loginProvider]));
return redirect(route('profile.index'));
}
/** @var User $user */
$user = auth()->user();
$newEmail = $request->string('email');
@ -299,6 +331,13 @@ class ProfileController extends Controller
*/
public function postChangePassword(ProfileFormRequest $request, UserRepositoryInterface $repository)
{
$loginProvider = config('firefly.login_provider');
if ('eloquent' !== $loginProvider) {
$request->session()->flash('error', trans('firefly.login_provider_local_only', ['login_provider' => $loginProvider]));
return redirect(route('profile.index'));
}
// the request has already validated both new passwords must be equal.
$current = $request->get('current_password');
$new = $request->get('new_password');
@ -396,6 +435,11 @@ class ProfileController extends Controller
*/
public function undoEmailChange(UserRepositoryInterface $repository, string $token, string $hash)
{
$loginProvider = config('firefly.login_provider');
if ('eloquent' !== $loginProvider) {
throw new FireflyException('Cannot confirm email change when authentication provider is not local.');
}
// find preference with this token value.
$set = app('preferences')->findByName('email_change_undo_token');
$user = null;

View File

@ -192,7 +192,7 @@ class TagController extends Controller
'firefly.journals_in_period_for_tag', ['tag' => $tag->tag, 'start' => $start->formatLocalized($this->monthAndDayFormat),
'end' => $end->formatLocalized($this->monthAndDayFormat),]
);
$periods = $this->getTagPeriodOverview($tag);
$periods = $this->getTagPeriodOverview($tag, $start);
$path = route('tags.show', [$tag->id, $start->format('Y-m-d'), $end->format('Y-m-d')]);
/** @var TransactionCollectorInterface $collector */

View File

@ -37,7 +37,6 @@ class SecureHeaders
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
*
* @return mixed
*/
@ -51,6 +50,7 @@ class SecureHeaders
}
$csp = [
"default-src 'none'",
"object-src 'self'",
sprintf("script-src 'self' 'unsafe-eval' 'unsafe-inline' %s", $google),
"style-src 'self' 'unsafe-inline'",
"base-uri 'self'",

View File

@ -24,6 +24,7 @@ namespace FireflyIII\Http\Requests;
use FireflyIII\Models\Account;
use FireflyIII\Rules\UniqueIban;
use FireflyIII\Rules\ZeroOrMore;
/**
* Class AccountFormRequest.
@ -116,7 +117,7 @@ class AccountFormRequest extends Request
];
if ('liabilities' === $this->get('what')) {
$rules['openingBalance'] = 'numeric|required|more:0';
$rules['openingBalance'] = ['numeric', 'required', new ZeroOrMore];
$rules['openingBalanceDate'] = 'date|required';
}

View File

@ -75,32 +75,31 @@ class RuleFormRequest extends Request
$validTriggers = array_keys(config('firefly.rule-triggers'));
$validActions = array_keys(config('firefly.rule-actions'));
// some actions require text:
$contextActions = implode(',', config('firefly.rule-actions-text'));
// some actions require text (aka context):
$contextActions = implode(',', config('firefly.context-rule-actions'));
$titleRule = 'required|between:1,100|uniqueObjectForUser:rules,title';
/** @var Rule $rule */
$rule = $this->route()->parameter('rule');
// some triggers require text (aka context):
$contextTriggers = implode(',', config('firefly.context-rule-triggers'));
if (null !== $rule) {
$titleRule = 'required|between:1,100|uniqueObjectForUser:rules,title,' . $rule->id;
}
// initial set of rules:
$rules = [
'title' => $titleRule,
'title' => 'required|between:1,100|uniqueObjectForUser:rules,title',
'description' => 'between:1,5000|nullable',
'stop_processing' => 'boolean',
'rule_group_id' => 'required|belongsToUser:rule_groups',
'trigger' => 'required|in:store-journal,update-journal',
'rule_triggers.*.name' => 'required|in:' . implode(',', $validTriggers),
'rule_triggers.*.value' => 'required|min:1|ruleTriggerValue',
'rule_triggers.*.value' => sprintf('required_if:rule_triggers.*.name,%s|min:1|ruleTriggerValue', $contextTriggers),
'rule-actions.*.name' => 'required|in:' . implode(',', $validActions),
'rule_actions.*.value' => sprintf('required_if:rule_actions.*.name,%s|min:1|ruleActionValue', $contextActions),
'strict' => 'in:0,1',
];
// since Laravel does not support this stuff yet, here's a trick.
for ($i = 0; $i < 10; ++$i) {
$key = sprintf('rule_actions.%d.value', $i);
$rule = sprintf('required-if:rule_actions.%d.name,%s|ruleActionValue', $i, $contextActions);
$rules[$key] = $rule;
/** @var Rule $rule */
$rule = $this->route()->parameter('rule');
if (null !== $rule) {
$rules['title'] = 'required|between:1,100|uniqueObjectForUser:rules,title,' . $rule->id;
}
return $rules;

View File

@ -168,8 +168,10 @@ class SplitJournalFormRequest extends Request
/** @var array $array */
foreach ($transactions as $array) {
if (null !== $array['destination_id'] && null !== $array['source_id'] && $array['destination_id'] === $array['source_id']) {
// @codeCoverageIgnoreStart
$validator->errors()->add('journal_source_id', (string)trans('validation.source_equals_destination'));
$validator->errors()->add('journal_destination_id', (string)trans('validation.source_equals_destination'));
// @codeCoverageIgnoreEnd
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* FinTSConfigurationSteps.php
* Copyright (c) 2018 https://github.com/bnw
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Import\JobConfiguration;
/**
*
* Class FinTSConfigurationSteps
*/
abstract class FinTSConfigurationSteps
{
public const NEW = 'new';
public const CHOOSE_ACCOUNT = 'choose_account';
public const GO_FOR_IMPORT = 'go-for-import';
}

View File

@ -0,0 +1,134 @@
<?php
/**
* FinTSJobConfiguration.php
* Copyright (c) 2018 https://github.com/bnw
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Import\JobConfiguration;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\ImportJob;
use FireflyIII\Support\Import\JobConfiguration\FinTS\ChooseAccountHandler;
use FireflyIII\Support\Import\JobConfiguration\FinTS\FinTSConfigurationInterface;
use FireflyIII\Support\Import\JobConfiguration\FinTS\NewFinTSJobHandler;
use Illuminate\Support\MessageBag;
/**
*
* Class FinTSJobConfiguration
*/
class FinTSJobConfiguration implements JobConfigurationInterface
{
/** @var ImportJob */
private $importJob;
/**
* Returns true when the initial configuration for this job is complete.
*
* @return bool
*/
public function configurationComplete(): bool
{
return $this->importJob->stage === FinTSConfigurationSteps::GO_FOR_IMPORT;
}
/**
* Store any data from the $data array into the job. Anything in the message bag will be flashed
* as an error to the user, regardless of its content.
*
* @param array $data
*
* @return MessageBag
* @throws FireflyException
*/
public function configureJob(array $data): MessageBag
{
return $this->getConfigurationObject()->configureJob($data);
}
/**
* Return the data required for the next step in the job configuration.
*
* @return array
* @throws FireflyException
*/
public function getNextData(): array
{
return $this->getConfigurationObject()->getNextData();
}
/**
* Returns the view of the next step in the job configuration.
*
* @return string
* @throws FireflyException
*/
public function getNextView(): string
{
switch ($this->importJob->stage) {
case FinTSConfigurationSteps::NEW:
case FinTSConfigurationSteps::CHOOSE_ACCOUNT:
return 'import.fints.' . $this->importJob->stage;
break;
default:
// @codeCoverageIgnoreStart
throw new FireflyException(
sprintf('FinTSJobConfiguration::getNextView() cannot handle stage "%s"', $this->importJob->stage)
);
// @codeCoverageIgnoreEnd
}
}
/**
* @param ImportJob $importJob
*/
public function setImportJob(ImportJob $importJob): void
{
$this->importJob = $importJob;
}
/**
* Get the configuration handler for this specific stage.
*
* @return FinTSConfigurationInterface
* @throws FireflyException
*/
private function getConfigurationObject(): FinTSConfigurationInterface
{
$class = 'DoNotExist';
switch ($this->importJob->stage) {
case FinTSConfigurationSteps::NEW:
$class = NewFinTSJobHandler::class;
break;
case FinTSConfigurationSteps::CHOOSE_ACCOUNT:
$class = ChooseAccountHandler::class;
break;
}
if (!class_exists($class)) {
throw new FireflyException(sprintf('Class %s does not exist in getConfigurationClass().', $class)); // @codeCoverageIgnore
}
$configurator = app($class);
$configurator->setImportJob($this->importJob);
return $configurator;
}
}

View File

@ -78,13 +78,13 @@ class BunqRoutine implements RoutineInterface
$handler->run();
$transactions = $handler->getTransactions();
// could be that more transactions will arrive in a second run.
if (true === $handler->stillRunning) {
if (true === $handler->isStillRunning()) {
Log::debug('Handler indicates that it is still working.');
$this->repository->setStatus($this->importJob, 'ready_to_run');
$this->repository->setStage($this->importJob, 'go-for-import');
}
$this->repository->appendTransactions($this->importJob, $transactions);
if (false === $handler->stillRunning) {
if (false === $handler->isStillRunning()) {
Log::info('Handler indicates that its done!');
$this->repository->setStatus($this->importJob, 'provider_finished');
$this->repository->setStage($this->importJob, 'final');
@ -98,6 +98,8 @@ class BunqRoutine implements RoutineInterface
}
/**
* Set the import job.
*

View File

@ -0,0 +1,88 @@
<?php
/**
* FinTSRoutine.php
* Copyright (c) 2017 https://github.com/bnw
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Import\Routine;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Import\JobConfiguration\FinTSConfigurationSteps;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Support\Import\Routine\FinTS\StageImportDataHandler;
use Illuminate\Support\Facades\Log;
/**
*
* Class FinTSRoutine
*/
class FinTSRoutine implements RoutineInterface
{
/** @var ImportJob */
private $importJob;
/** @var ImportJobRepositoryInterface */
private $repository;
/**
* At the end of each run(), the import routine must set the job to the expected status.
*
* The final status of the routine must be "provider_finished".
*
* @throws FireflyException
*/
public function run(): void
{
Log::debug(sprintf('Now in FinTSRoutine::run() with status "%s" and stage "%s".', $this->importJob->status, $this->importJob->stage));
$valid = ['ready_to_run']; // should be only ready_to_run
if (\in_array($this->importJob->status, $valid, true)) {
switch ($this->importJob->stage) {
default:
throw new FireflyException(sprintf('FinTSRoutine cannot handle stage "%s".', $this->importJob->stage)); // @codeCoverageIgnore
case FinTSConfigurationSteps::GO_FOR_IMPORT:
$this->repository->setStatus($this->importJob, 'running');
/** @var StageImportDataHandler $handler */
$handler = app(StageImportDataHandler::class);
$handler->setImportJob($this->importJob);
$handler->run();
$transactions = $handler->getTransactions();
$this->repository->setTransactions($this->importJob, $transactions);
$this->repository->setStatus($this->importJob, 'provider_finished');
$this->repository->setStage($this->importJob, 'final');
return;
}
}
}
/**
* @param ImportJob $importJob
*
* @return void
*/
public function setImportJob(ImportJob $importJob): void
{
$this->importJob = $importJob;
$this->repository = app(ImportJobRepositoryInterface::class);
$this->repository->setUser($importJob->user);
}
}

View File

@ -46,7 +46,7 @@ use Illuminate\Support\Collection;
use Log;
/**
* Creates new transactions based upon arrays. Will first check the array for duplicates.
* Creates new transactions based on arrays.
*
* Class ImportArrayStorage
*
@ -58,11 +58,11 @@ class ImportArrayStorage
private $checkForTransfers = false;
/** @var ImportJob The import job */
private $importJob;
/** @var JournalRepositoryInterface */
/** @var JournalRepositoryInterface Journal repository for storage. */
private $journalRepos;
/** @var ImportJobRepositoryInterface Import job repository */
private $repository;
/** @var Collection The transfers. */
/** @var Collection The transfers the user already has. */
private $transfers;
/**
@ -72,12 +72,12 @@ class ImportArrayStorage
*/
public function setImportJob(ImportJob $importJob): void
{
$this->importJob = $importJob;
$this->countTransfers();
$this->importJob = $importJob;
$this->repository = app(ImportJobRepositoryInterface::class);
$this->repository->setUser($importJob->user);
$this->countTransfers();
$this->journalRepos = app(JournalRepositoryInterface::class);
$this->journalRepos->setUser($importJob->user);
@ -116,7 +116,7 @@ class ImportArrayStorage
app('preferences')->mark();
// email about this:
event(new RequestedReportOnJournals($this->importJob->user_id, $collection));
event(new RequestedReportOnJournals((int)$this->importJob->user_id, $collection));
return $collection;
}
@ -152,14 +152,14 @@ class ImportArrayStorage
/**
* Count the number of transfers in the array. If this is zero, don't bother checking for double transfers.
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private function countTransfers(): void
{
Log::debug('Now in count transfers.');
Log::debug('Now in countTransfers()');
/** @var array $array */
$array = $this->importJob->transactions;
$array = $this->repository->getTransactions($this->importJob);
$count = 0;
foreach ($array as $index => $transaction) {
if (strtolower(TransactionType::TRANSFER) === strtolower($transaction['type'])) {
@ -167,17 +167,44 @@ class ImportArrayStorage
Log::debug(sprintf('Row #%d is a transfer, increase count to %d', $index + 1, $count));
}
}
if (0 === $count) {
Log::debug('Count is zero, will not check for duplicate transfers.');
}
Log::debug(sprintf('Count of transfers in import array is %d.', $count));
if ($count > 0) {
Log::debug(sprintf('Count is %d, will check for duplicate transfers.', $count));
$this->checkForTransfers = true;
Log::debug('Will check for duplicate transfers.');
// get users transfers. Needed for comparison.
$this->getTransfers();
}
}
/**
* @param int $index
* @param array $transaction
*
* @return bool
* @throws FireflyException
*/
private function duplicateDetected(int $index, array $transaction): bool
{
$hash = $this->getHash($transaction);
$existingId = $this->hashExists($hash);
if (null !== $existingId) {
$message = sprintf('Row #%d ("%s") could not be imported. It already exists.', $index, $transaction['description']);
$this->logDuplicateObject($transaction, $existingId);
$this->repository->addErrorMessage($this->importJob, $message);
return true;
}
// do transfer detection:
if ($this->checkForTransfers && $this->transferExists($transaction)) {
$message = sprintf('Row #%d ("%s") could not be imported. Such a transfer already exists.', $index, $transaction['description']);
$this->logDuplicateTransfer($transaction);
$this->repository->addErrorMessage($this->importJob, $message);
return true;
}
return false;
}
/**
@ -193,9 +220,11 @@ class ImportArrayStorage
unset($transaction['importHashV2'], $transaction['original-source']);
$json = json_encode($transaction);
if (false === $json) {
// @codeCoverageIgnoreStart
/** @noinspection ForgottenDebugOutputInspection */
Log::error('Could not encode import array.', print_r($transaction, true));
throw new FireflyException('Could not encode import array. Please see the logs.'); // @codeCoverageIgnore
throw new FireflyException('Could not encode import array. Please see the logs.');
// @codeCoverageIgnoreEnd
}
$hash = hash('sha256', $json, false);
Log::debug(sprintf('The hash is: %s', $hash));
@ -391,38 +420,21 @@ class ImportArrayStorage
private function storeArray(): Collection
{
/** @var array $array */
$array = $this->importJob->transactions;
$array = $this->repository->getTransactions($this->importJob);
$count = \count($array);
$toStore = [];
Log::debug(sprintf('Now in store(). Count of items is %d', $count));
Log::debug(sprintf('Now in store(). Count of items is %d.', $count));
/*
* Detect duplicates in initial array:
*/
foreach ($array as $index => $transaction) {
Log::debug(sprintf('Now at item %d out of %d', $index + 1, $count));
$hash = $this->getHash($transaction);
$existingId = $this->hashExists($hash);
if (null !== $existingId) {
$this->logDuplicateObject($transaction, $existingId);
$this->repository->addErrorMessage(
$this->importJob, sprintf(
'Row #%d ("%s") could not be imported. It already exists.',
$index, $transaction['description']
)
);
if ($this->duplicateDetected($index, $transaction)) {
continue;
}
if ($this->checkForTransfers && $this->transferExists($transaction)) {
$this->logDuplicateTransfer($transaction);
$this->repository->addErrorMessage(
$this->importJob, sprintf(
'Row #%d ("%s") could not be imported. Such a transfer already exists.',
$index,
$transaction['description']
)
);
continue;
}
$transaction['importHashV2'] = $hash;
$transaction['importHashV2'] = $this->getHash($transaction);
$toStore[] = $transaction;
}
$count = \count($toStore);
@ -436,31 +448,8 @@ class ImportArrayStorage
// now actually store them:
$collection = new Collection;
foreach ($toStore as $index => $store) {
// do duplicate detection again!
$hash = $this->getHash($store);
$existingId = $this->hashExists($hash);
if (null !== $existingId) {
$this->logDuplicateObject($store, $existingId);
$this->repository->addErrorMessage(
$this->importJob, sprintf(
'Row #%d ("%s") could not be imported. It already exists.',
$index, $store['description']
)
);
continue;
}
// do transfer detection again!
if ($this->checkForTransfers && $this->transferExists($store)) {
$this->logDuplicateTransfer($store);
$this->repository->addErrorMessage(
$this->importJob, sprintf(
'Row #%d ("%s") could not be imported. Such a transfer already exists.',
$index,
$store['description']
)
);
if ($this->duplicateDetected($index, $store)) {
continue;
}
@ -554,8 +543,9 @@ class ImportArrayStorage
Log::debug(sprintf('Comparison is a hit! (%s)', $hits));
// compare description:
Log::debug(sprintf('Comparing "%s" to "%s"', $description, $transfer->description));
if ($description !== $transfer->description) {
$comparison = '(empty description)' === $transfer->description ? '' : $transfer->description;
Log::debug(sprintf('Comparing "%s" to "%s" (original: "%s")', $description, $transfer->description, $comparison));
if ($description !== $comparison) {
continue; // @codeCoverageIgnore
}
++$hits;

View File

@ -154,15 +154,15 @@ class ExecuteRuleGroupOnExistingTransactions extends Job implements ShouldQueue
$processors = $this->collectProcessors();
// Execute the rules for each transaction
foreach ($transactions as $transaction) {
/** @var Processor $processor */
foreach ($processors as $processor) {
foreach ($processors as $processor) {
foreach ($transactions as $transaction) {
/** @var Processor $processor */
$processor->handleTransaction($transaction);
// Stop processing this group if the rule specifies 'stop_processing'
if ($processor->getRule()->stop_processing) {
break;
}
}
// Stop processing this group if the rule specifies 'stop_processing'
if ($processor->getRule()->stop_processing) {
break;
}
}
}
@ -203,6 +203,7 @@ class ExecuteRuleGroupOnExistingTransactions extends Job implements ShouldQueue
/** @var Processor $processor */
$processor = app(Processor::class);
$processor->make($rule);
return $processor;
},
$rules->all()

View File

@ -179,10 +179,6 @@ class ExecuteRuleOnExistingTransactions extends Job implements ShouldQueue
++$misses;
}
Log::info(sprintf('Current progress: %d Transactions. Hits: %d, misses: %d', $total, $hits, $misses));
// Stop processing this group if the rule specifies 'stop_processing'
if ($processor->getRule()->stop_processing) {
break;
}
}
Log::info(sprintf('Total transactions: %d. Hits: %d, misses: %d', $total, $hits, $misses));
}

View File

@ -42,6 +42,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @property-read string $email
* @property bool encrypted
* @property Collection budgetlimits
* @property int $order
*/
class Budget extends Model
{
@ -61,7 +62,7 @@ class Budget extends Model
'encrypted' => 'boolean',
];
/** @var array Fields that can be filled */
protected $fillable = ['user_id', 'name', 'active'];
protected $fillable = ['user_id', 'name', 'active','order'];
/** @var array Hidden from view */
protected $hidden = ['encrypted'];

View File

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -45,6 +46,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @property array $errors
* @property array extended_status
* @property int id
* @property Carbon $created_at
*/
class ImportJob extends Model
{
@ -56,6 +58,7 @@ class ImportJob extends Model
*/
protected $casts
= [
'user_id' => 'int',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'configuration' => 'array',

View File

@ -71,6 +71,7 @@ use FireflyIII\Validation\FireflyValidator;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Twig;
use Twig_Extension_Debug;
use TwigBridge\Extension\Loader\Functions;
use Validator;
@ -105,6 +106,7 @@ class FireflyServiceProvider extends ServiceProvider
Twig::addExtension(new Transaction);
Twig::addExtension(new Rule);
Twig::addExtension(new AmountFormat);
Twig::addExtension(new Twig_Extension_Debug);
}
/**

View File

@ -34,7 +34,7 @@ use FireflyIII\User;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Collection;
use Log;
use Storage;
use Illuminate\Support\Facades\Storage;
/**
* Class AttachmentRepository.
@ -66,9 +66,9 @@ class AttachmentRepository implements AttachmentRepositoryInterface
/** @var AttachmentHelperInterface $helper */
$helper = app(AttachmentHelperInterface::class);
$file = $helper->getAttachmentLocation($attachment);
$path = $helper->getAttachmentLocation($attachment);
try {
unlink($file);
Storage::disk('upload')->delete($path);
} catch (Exception $e) {
Log::error(sprintf('Could not delete file for attachment %d: %s', $attachment->id, $e->getMessage()));
}

View File

@ -381,7 +381,9 @@ class BillRepository implements BillRepositoryInterface
*/
public function getPaidDatesInRange(Bill $bill, Carbon $start, Carbon $end): Collection
{
$dates = $bill->transactionJournals()->before($end)->after($start)->get(['transaction_journals.date'])->pluck('date');
$dates = $bill->transactionJournals()->before($end)->after($start)->get([
'transaction_journals.id','transaction_journals.date'
])->pluck('date', 'id');
return $dates;
}

View File

@ -107,6 +107,7 @@ class BudgetRepository implements BudgetRepositoryInterface
} catch (Exception $e) {
Log::debug(sprintf('Could not delete budget limit: %s', $e->getMessage()));
}
Budget::where('order',0)->update(['order' => 100]);
// do the clean up by hand because Sqlite can be tricky with this.
$budgetLimits = BudgetLimit::orderBy('created_at', 'DESC')->get(['id', 'budget_id', 'start_date', 'end_date']);
@ -289,11 +290,14 @@ class BudgetRepository implements BudgetRepositoryInterface
public function getActiveBudgets(): Collection
{
/** @var Collection $set */
$set = $this->user->budgets()->where('active', 1)->get();
$set = $this->user->budgets()->where('active', 1)
->get();
$set = $set->sortBy(
function (Budget $budget) {
return strtolower($budget->name);
$str = str_pad((string)$budget->order, 4, '0', STR_PAD_LEFT) . strtolower($budget->name);
return $str;
}
);
@ -554,7 +558,9 @@ class BudgetRepository implements BudgetRepositoryInterface
$set = $set->sortBy(
function (Budget $budget) {
return strtolower($budget->name);
$str = str_pad((string)$budget->order, 4, '0', STR_PAD_LEFT) . strtolower($budget->name);
return $str;
}
);
@ -583,7 +589,9 @@ class BudgetRepository implements BudgetRepositoryInterface
$set = $set->sortBy(
function (Budget $budget) {
return strtolower($budget->name);
$str = str_pad((string)$budget->order, 4, '0', STR_PAD_LEFT) . strtolower($budget->name);
return $str;
}
);
@ -652,6 +660,18 @@ class BudgetRepository implements BudgetRepositoryInterface
return $availableBudget;
}
/**
* @param Budget $budget
* @param int $order
*/
public function setBudgetOrder(Budget $budget, int $order): void
{
$budget->order = $order;
$budget->save();
}
/** @noinspection MoreThanThreeArgumentsInspection */
/**
* @param User $user
*/
@ -660,7 +680,6 @@ class BudgetRepository implements BudgetRepositoryInterface
$this->user = $user;
}
/** @noinspection MoreThanThreeArgumentsInspection */
/**
* @param Collection $budgets
* @param Collection $accounts
@ -825,6 +844,8 @@ class BudgetRepository implements BudgetRepositoryInterface
}
/** @noinspection MoreThanThreeArgumentsInspection */
/**
* @param BudgetLimit $budgetLimit
* @param array $data
@ -848,7 +869,6 @@ class BudgetRepository implements BudgetRepositoryInterface
return $budgetLimit;
}
/** @noinspection MoreThanThreeArgumentsInspection */
/**
* @param Budget $budget
* @param Carbon $start

View File

@ -156,7 +156,6 @@ interface BudgetRepositoryInterface
*/
public function getBudgetLimits(Budget $budget, Carbon $start = null, Carbon $end = null): Collection;
/** @noinspection MoreThanThreeArgumentsInspection */
/**
* @param Collection $budgets
* @param Collection $accounts
@ -167,6 +166,8 @@ interface BudgetRepositoryInterface
*/
public function getBudgetPeriodReport(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): array;
/** @noinspection MoreThanThreeArgumentsInspection */
/**
* @return Collection
*/
@ -195,7 +196,6 @@ interface BudgetRepositoryInterface
*/
public function getNoBudgetPeriodReport(Collection $accounts, Carbon $start, Carbon $end): array;
/** @noinspection MoreThanThreeArgumentsInspection */
/**
* @param TransactionCurrency $currency
* @param Carbon $start
@ -206,6 +206,14 @@ interface BudgetRepositoryInterface
*/
public function setAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end, string $amount): AvailableBudget;
/** @noinspection MoreThanThreeArgumentsInspection */
/**
* @param Budget $budget
* @param int $order
*/
public function setBudgetOrder(Budget $budget, int $order): void;
/**
* @param User $user
*/

View File

@ -28,8 +28,8 @@ use FireflyIII\Models\ExportJob;
use FireflyIII\User;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
use Log;
use Storage;
/**
* Class ExportJobRepository.
@ -74,15 +74,17 @@ class ExportJobRepository implements ExportJobRepositoryInterface
->whereIn('status', ['never_started', 'export_status_finished', 'export_downloaded'])
->get();
$disk = Storage::disk('export');
$files = $disk->files();
// loop set:
/** @var ExportJob $entry */
foreach ($set as $entry) {
$key = $entry->key;
$files = scandir(storage_path('export'), SCANDIR_SORT_NONE);
/** @var string $file */
/** @var array $file */
foreach ($files as $file) {
if (0 === strpos($file, $key)) {
unlink(storage_path('export') . DIRECTORY_SEPARATOR . $file);
if (0 === strpos($file['basename'], $key)) {
$disk->delete($file['path']);
}
}
try {

View File

@ -84,26 +84,36 @@ class ImportJobRepository implements ImportJobRepositoryInterface
* @param array $transactions
*
* @return ImportJob
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function appendTransactions(ImportJob $job, array $transactions): ImportJob
{
Log::debug(sprintf('Now in appendTransactions(%s)', $job->key));
$existingTransactions = $job->transactions;
if (!\is_array($existingTransactions)) {
$existingTransactions = [];
}
$new = array_merge($existingTransactions, $transactions);
$existingTransactions = $this->getTransactions($job);
$new = array_merge($existingTransactions, $transactions);
Log::debug(sprintf('Old transaction count: %d', \count($existingTransactions)));
Log::debug(sprintf('To be added transaction count: %d', \count($transactions)));
Log::debug(sprintf('New count: %d', \count($new)));
$job->transactions = $new;
$job->save();
$this->setTransactions($job, $new);
return $job;
}
/**
* @param ImportJob $job
*
* @return int
*/
public function countTransactions(ImportJob $job): int
{
$info = $job->transactions ?? [];
if (isset($info['count'])) {
return (int)$info['count'];
}
return 0;
}
/**
* @param string $importProvider
*
@ -201,6 +211,31 @@ class ImportJobRepository implements ImportJobRepositoryInterface
return [];
}
/**
* Return transactions from attachment.
*
* @param ImportJob $job
*
* @return array
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function getTransactions(ImportJob $job): array
{
// this will overwrite all transactions currently in the job.
$disk = Storage::disk('upload');
$filename = sprintf('%s-%s.crypt.json', $job->created_at->format('Ymd'), $job->key);
$array = [];
if ($disk->exists($filename)) {
$json = Crypt::decrypt($disk->get($filename));
$array = json_decode($json, true);
}
if (false === $array) {
$array = [];
}
return $array;
}
/**
* @param ImportJob $job
* @param array $configuration
@ -272,8 +307,17 @@ class ImportJobRepository implements ImportJobRepositoryInterface
*/
public function setTransactions(ImportJob $job, array $transactions): ImportJob
{
$job->transactions = $transactions;
// this will overwrite all transactions currently in the job.
$disk = Storage::disk('upload');
$filename = sprintf('%s-%s.crypt.json', $job->created_at->format('Ymd'), $job->key);
$json = Crypt::encrypt(json_encode($transactions));
// set count for easy access
$array = ['count' => \count($transactions)];
$job->transactions = $array;
$job->save();
// store file.
$disk->put($filename, $json);
return $job;
}
@ -377,7 +421,7 @@ class ImportJobRepository implements ImportJobRepositoryInterface
$fileObject->rewind();
if(0 === $file->getSize()) {
if (0 === $file->getSize()) {
throw new FireflyException('Cannot upload empty or non-existent file.');
}
@ -390,7 +434,6 @@ class ImportJobRepository implements ImportJobRepositoryInterface
return new MessageBag;
}
/**
* @codeCoverageIgnore
*

View File

@ -35,6 +35,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
*/
interface ImportJobRepositoryInterface
{
/**
* Add message to job.
*
@ -55,6 +56,13 @@ interface ImportJobRepositoryInterface
*/
public function appendTransactions(ImportJob $job, array $transactions): ImportJob;
/**
* @param ImportJob $job
*
* @return int
*/
public function countTransactions(ImportJob $job): int;
/**
* @param string $importProvider
*
@ -96,6 +104,15 @@ interface ImportJobRepositoryInterface
*/
public function getExtendedStatus(ImportJob $job): array;
/**
* Return transactions from attachment.
*
* @param ImportJob $job
*
* @return array
*/
public function getTransactions(ImportJob $job): array;
/**
* @param ImportJob $job
* @param array $configuration

View File

@ -27,6 +27,9 @@ use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\TransactionJournalFactory;
use FireflyIII\Factory\TransactionJournalMetaFactory;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Helpers\Filter\TransferFilter;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\PiggyBankEvent;
@ -581,14 +584,23 @@ class JournalRepository implements JournalRepositoryInterface
*/
public function getTransactionsById(array $transactionIds): Collection
{
$set = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->whereIn('transactions.id', $transactionIds)
->where('transaction_journals.user_id', $this->user->id)
->whereNull('transaction_journals.deleted_at')
->whereNull('transactions.deleted_at')
->get(['transactions.*']);
$journalIds = Transaction::whereIn('id', $transactionIds)->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray();
$journals = new Collection;
foreach($journalIds as $journalId) {
$result = $this->findNull((int)$journalId);
if(null !== $result) {
$journals->push($result);
}
}
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class);
$collector->setUser($this->user);
$collector->setAllAssetAccounts();
$collector->removeFilter(InternalTransferFilter::class);
//$collector->addFilter(TransferFilter::class);
return $set;
$collector->setJournals($journals)->withOpposingAccount();
return $collector->getTransactions();
}
/**

View File

@ -92,6 +92,23 @@ class TagRepository implements TagRepositoryInterface
return (string)$set->sum('transaction_amount');
}
/**
* @param Tag $tag
* @param Carbon $start
* @param Carbon $end
*
* @return Collection
*/
public function expenseInPeriod(Tag $tag, Carbon $start, Carbon $end): Collection
{
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class);
$collector->setUser($this->user);
$collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAllAssetAccounts()->setTag($tag);
return $collector->getTransactions();
}
/**
* @param string $tag
*
@ -151,6 +168,23 @@ class TagRepository implements TagRepositoryInterface
return $tags;
}
/**
* @param Tag $tag
* @param Carbon $start
* @param Carbon $end
*
* @return Collection
*/
public function incomeInPeriod(Tag $tag, Carbon $start, Carbon $end): Collection
{
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class);
$collector->setUser($this->user);
$collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAllAssetAccounts()->setTag($tag);
return $collector->getTransactions();
}
/**
* @param Tag $tag
*
@ -315,6 +349,23 @@ class TagRepository implements TagRepositoryInterface
return $return;
}
/**
* @param Tag $tag
* @param Carbon $start
* @param Carbon $end
*
* @return Collection
*/
public function transferredInPeriod(Tag $tag, Carbon $start, Carbon $end): Collection
{
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class);
$collector->setUser($this->user);
$collector->setRange($start, $end)->setTypes([TransactionType::TRANSFER])->setAllAssetAccounts()->setTag($tag);
return $collector->getTransactions();
}
/**
* @param Tag $tag
* @param array $data

View File

@ -55,6 +55,15 @@ interface TagRepositoryInterface
*/
public function earnedInPeriod(Tag $tag, Carbon $start, Carbon $end): string;
/**
* @param Tag $tag
* @param Carbon $start
* @param Carbon $end
*
* @return Collection
*/
public function expenseInPeriod(Tag $tag, Carbon $start, Carbon $end): Collection;
/**
* @param string $tag
*
@ -83,6 +92,15 @@ interface TagRepositoryInterface
*/
public function get(): Collection;
/**
* @param Tag $tag
* @param Carbon $start
* @param Carbon $end
*
* @return Collection
*/
public function incomeInPeriod(Tag $tag, Carbon $start, Carbon $end): Collection;
/**
* @param Tag $tag
*
@ -147,6 +165,15 @@ interface TagRepositoryInterface
*/
public function tagCloud(?int $year): array;
/**
* @param Tag $tag
* @param Carbon $start
* @param Carbon $end
*
* @return Collection
*/
public function transferredInPeriod(Tag $tag, Carbon $start, Carbon $end): Collection;
/**
* Update a tag.
*

45
app/Rules/ZeroOrMore.php Normal file
View File

@ -0,0 +1,45 @@
<?php
namespace FireflyIII\Rules;
use Illuminate\Contracts\Validation\Rule;
/**
*
* Class ZeroOrMore
*/
class ZeroOrMore implements Rule
{
/**
* Get the validation error message.
*
* @return string
*/
public function message(): string
{
return trans('validation.zero_or_more');
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
*
* @return bool
*/
public function passes($attribute, $value): bool
{
$value = (string)$value;
if ('' === $value) {
return true;
}
$res = bccomp('0', $value);
if ($res > 0) {
return false;
}
return true;
}
}

View File

@ -26,6 +26,7 @@ namespace FireflyIII\Services\Internal\Destroy;
use DB;
use Exception;
use FireflyIII\Models\Account;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\RecurrenceTransaction;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
@ -97,6 +98,9 @@ class AccountDestroyService
}
}
// delete piggy banks:
PiggyBank::where('account_id', $account->id)->delete();
try {
$account->delete();
} catch (Exception $e) { // @codeCoverageIgnore

View File

@ -26,6 +26,7 @@ namespace FireflyIII\Services\Internal\File;
use Crypt;
use FireflyIII\Exceptions\FireflyException;
use Illuminate\Contracts\Encryption\EncryptException;
use Illuminate\Support\Facades\Storage;
use Log;
/**
@ -63,9 +64,8 @@ class EncryptService
throw new FireflyException($message);
}
$newName = sprintf('%s.upload', $key);
$path = storage_path('upload') . '/' . $newName;
file_put_contents($path, $content);
$disk = Storage::disk('upload');
$disk->put($newName, $content);
}
}

View File

@ -217,13 +217,13 @@ trait AccountServiceTrait
*/
public function storeOpposingAccount(User $user, string $name): Account
{
$name .= ' initial balance';
$opposingAccountName = (string)trans('firefly.initial_balance_account', ['name' => $name]);
Log::debug('Going to create an opening balance opposing account.');
/** @var AccountFactory $factory */
$factory = app(AccountFactory::class);
$factory->setUser($user);
return $factory->findOrCreate($name, AccountType::INITIAL_BALANCE);
return $factory->findOrCreate($opposingAccountName, AccountType::INITIAL_BALANCE);
}
/**

View File

@ -51,7 +51,7 @@ class CLIToken implements BinderInterface
foreach ($users as $user) {
$accessToken = app('preferences')->getForUser($user, 'access_token', null);
if ($accessToken->data === $value) {
if(null !== $accessToken && $accessToken->data === $value) {
Log::info(sprintf('Recognized user #%d (%s) from his acccess token.', $user->id, $user->email));
return $value;

121
app/Support/FinTS/FinTS.php Normal file
View File

@ -0,0 +1,121 @@
<?php
/**
* FinTS.php
* Copyright (c) 2018 https://github.com/bnw
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\FinTS;
use Fhp\Model\SEPAAccount;
use FireflyIII\Exceptions\FireflyException;
use Illuminate\Support\Facades\Crypt;
/**
*
* Class FinTS
*/
class FinTS
{
/** @var \Fhp\FinTs */
private $finTS;
/**
* @param array $config
*
* @throws FireflyException
*/
public function __construct(array $config)
{
if (!isset($config['fints_url'], $config['fints_port'], $config['fints_bank_code'], $config['fints_username'], $config['fints_password'])) {
throw new FireflyException('Constructed FinTS with incomplete config.');
}
$this->finTS = new \Fhp\FinTs(
$config['fints_url'],
$config['fints_port'],
$config['fints_bank_code'],
$config['fints_username'],
Crypt::decrypt($config['fints_password'])
);
}
/**
* @return bool|string
*/
public function checkConnection()
{
try {
$this->finTS->getSEPAAccounts();
return true;
} catch (\Exception $exception) {
return $exception->getMessage();
}
}
/**
* @param string $accountNumber
*
* @return SEPAAccount
* @throws FireflyException
*/
public function getAccount(string $accountNumber)
{
$accounts = $this->getAccounts();
$filteredAccounts = array_filter(
$accounts, function (SEPAAccount $account) use ($accountNumber) {
return $account->getAccountNumber() === $accountNumber;
}
);
if (count($filteredAccounts) != 1) {
throw new FireflyException("Cannot find account with number " . $accountNumber);
}
return reset($filteredAccounts);
}
/**
* @return SEPAAccount[]
* @throws FireflyException
*/
public function getAccounts()
{
try {
return $this->finTS->getSEPAAccounts();
} catch (\Exception $exception) {
throw new FireflyException($exception->getMessage());
}
}
/**
* @param SEPAAccount $account
* @param \DateTime $from
* @param \DateTIme $to
*
* @return \Fhp\Model\StatementOfAccount\StatementOfAccount|null
* @throws FireflyException
*/
public function getStatementOfAccount(SEPAAccount $account, \DateTime $from, \DateTIme $to)
{
try {
return $this->finTS->getStatementOfAccount($account, $from, $to);
} catch (\Exception $exception) {
throw new FireflyException($exception->getMessage());
}
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* FinTS.php
* Copyright (c) 2018 https://github.com/bnw
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\FinTS;
use Fhp\Model\StatementOfAccount\Transaction as FinTSTransaction;
/**
*
* Class MetadataParser
*/
class MetadataParser
{
/**
* @param FinTSTransaction $transaction
*
* @return string
*/
public function getDescription(FinTSTransaction $transaction): string
{
//Given a description like 'EREF+AbcCRED+DE123SVWZ+DefABWA+Ghi' or 'EREF+AbcCRED+DE123SVWZ+Def' return 'Def'
$finTSDescription = $transaction->getDescription1();
$matches = [];
if (1 === preg_match('/SVWZ\+([^\+]*)([A-Z]{4}\+|$)/', $finTSDescription, $matches)) {
return $matches[1];
}
return $finTSDescription;
}
}

View File

@ -60,12 +60,14 @@ trait PeriodOverview
* and for each period, the amount of money spent and earned. This is a complex operation which is cached for
* performance reasons.
*
* @param Account $account the account involved
* @param Carbon $date
* The method has been refactored recently for better performance.
*
* @param Account $account The account involved
* @param Carbon $date The start date.
*
* @return Collection
*/
protected function getAccountPeriodOverview(Account $account, Carbon $date): Collection // period overview
protected function getAccountPeriodOverview(Account $account, Carbon $date): Collection
{
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
@ -95,15 +97,15 @@ trait PeriodOverview
$collector = app(TransactionCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setRange($currentDate['start'], $currentDate['end'])->setTypes([TransactionType::DEPOSIT])
->withOpposingAccount();
$set = $collector->getTransactions();
$earned = $this->groupByCurrency($set);
$earnedSet = $collector->getTransactions();
$earned = $this->groupByCurrency($earnedSet);
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setRange($currentDate['start'], $currentDate['end'])->setTypes([TransactionType::WITHDRAWAL])
->withOpposingAccount();
$set = $collector->getTransactions();
$spent = $this->groupByCurrency($set);
$spentSet = $collector->getTransactions();
$spent = $this->groupByCurrency($spentSet);
$title = app('navigation')->periodShow($currentDate['start'], $currentDate['period']);
/** @noinspection PhpUndefinedMethodInspection */
@ -115,7 +117,6 @@ trait PeriodOverview
'earned' => $earned,
'transferred' => '0',
'route' => route('accounts.show', [$account->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
]
);
}
@ -126,11 +127,84 @@ trait PeriodOverview
}
/**
* Gets period overview used for budgets.
* Overview for single category. Has been refactored recently.
*
* @param Category $category
* @param Carbon $date
*
* @return Collection
*/
protected function getBudgetPeriodOverview(Carbon $date): Collection
protected function getCategoryPeriodOverview(Category $category, Carbon $date): Collection
{
/** @var JournalRepositoryInterface $journalRepository */
$journalRepository = app(JournalRepositoryInterface::class);
$range = app('preferences')->get('viewRange', '1M')->data;
$first = $journalRepository->firstNull();
$end = null === $first ? new Carbon : $first->date;
$start = clone $date;
if ($end < $start) {
[$start, $end] = [$end, $start]; // @codeCoverageIgnore
}
// properties for entries with their amounts.
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($range);
$cache->addProperty('category-show-period-entries');
$cache->addProperty($category->id);
if ($cache->has()) {
return $cache->get(); // @codeCoverageIgnore
}
/** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $end, $range);
$entries = new Collection;
/** @var CategoryRepositoryInterface $categoryRepository */
$categoryRepository = app(CategoryRepositoryInterface::class);
foreach ($dates as $currentDate) {
$spent = $categoryRepository->spentInPeriodCollection(new Collection([$category]), new Collection, $currentDate['start'], $currentDate['end']);
$earned = $categoryRepository->earnedInPeriodCollection(new Collection([$category]), new Collection, $currentDate['start'], $currentDate['end']);
$spent = $this->groupByCurrency($spent);
$earned = $this->groupByCurrency($earned);
// amount transferred
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($currentDate['start'], $currentDate['end'])->setCategory($category)
->withOpposingAccount()->setTypes([TransactionType::TRANSFER]);
$collector->removeFilter(InternalTransferFilter::class);
$transferred = $this->groupByCurrency($collector->getTransactions());
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
$entries->push(
[
'transactions' => 0,
'title' => $title,
'spent' => $spent,
'earned' => $earned,
'transferred' => $transferred,
'route' => route('categories.show', [$category->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
]
);
}
$cache->store($entries);
return $entries;
}
/**
* Same as above, but for lists that involve transactions without a budget.
*
* This method has been refactored recently.
*
* @param Carbon $date
*
* @return Collection
*/
protected function getNoBudgetPeriodOverview(Carbon $date): Collection
{
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
@ -138,8 +212,12 @@ trait PeriodOverview
$end = null === $first ? new Carbon : $first->date;
$start = clone $date;
$range = app('preferences')->get('viewRange', '1M')->data;
$entries = new Collection;
$cache = new CacheProperties;
if ($end < $start) {
[$start, $end] = [$end, $start]; // @codeCoverageIgnore
}
$cache = new CacheProperties;
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('no-budget-period-entries');
@ -149,7 +227,8 @@ trait PeriodOverview
}
/** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $end, $range);
$dates = app('navigation')->blockPeriods($start, $end, $range);
$entries = new Collection;
foreach ($dates as $currentDate) {
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class);
@ -162,12 +241,12 @@ trait PeriodOverview
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
$entries->push(
[
'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'transactions' => $count,
'title' => $title,
'spent' => $spent,
'earned' => '0',
'transferred' => '0',
'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
]
);
}
@ -177,133 +256,70 @@ trait PeriodOverview
}
/**
* Get a period overview for category.
*
* TODO refactor me.
*
* @param Category $category
* @param Carbon $date
*
* @return Collection
*/
protected function getCategoryPeriodOverview(Category $category, Carbon $date): Collection // periodOverview method
{
/** @var JournalRepositoryInterface $journalRepository */
$journalRepository = app(JournalRepositoryInterface::class);
/** @var CategoryRepositoryInterface $categoryRepository */
$categoryRepository = app(CategoryRepositoryInterface::class);
$range = app('preferences')->get('viewRange', '1M')->data;
$first = $journalRepository->firstNull();
$end = null === $first ? new Carbon : $first->date;
$start = clone $date;
// properties for entries with their amounts.
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($range);
$cache->addProperty('categories.entries');
$cache->addProperty($category->id);
if ($cache->has()) {
//return $cache->get(); // @codeCoverageIgnore
}
/** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $end, $range);
$entries = new Collection;
foreach ($dates as $currentDate) {
$spent = $categoryRepository->spentInPeriodCollection(new Collection([$category]), new Collection, $currentDate['start'], $currentDate['end']);
$earned = $categoryRepository->earnedInPeriodCollection(new Collection([$category]), new Collection, $currentDate['start'], $currentDate['end']);
$spent = $this->groupByCurrency($spent);
$earned = $this->groupByCurrency($earned);
// amount transferred
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($currentDate['start'], $currentDate['end'])->setCategory($category)
->withOpposingAccount()->setTypes([TransactionType::TRANSFER]);
$collector->removeFilter(InternalTransferFilter::class);
$transferred = $this->groupByCurrency($collector->getTransactions());
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
$entries->push(
[
'route' => route('categories.show', [$category->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'title' => $title,
'spent' => $spent,
'earned' => $earned,
'transferred' => $transferred,
]
);
}
$cache->store($entries);
return $entries;
}
/**
* Get overview of periods for tag.
*
* TODO refactor this.
* This shows a period overview for a tag. It goes back in time and lists all relevant transactions and sums.
*
* @param Tag $tag
*
* @return Collection
*/
protected function getTagPeriodOverview(Tag $tag): Collection // period overview for tags.
protected function getTagPeriodOverview(Tag $tag, Carbon $date): Collection // period overview for tags.
{
/** @var TagRepositoryInterface $repository */
$repository = app(TagRepositoryInterface::class);
// get first and last tag date from tag:
$range = app('preferences')->get('viewRange', '1M')->data;
$range = app('preferences')->get('viewRange', '1M')->data;
/** @var Carbon $end */
$end = app('navigation')->endOfX($repository->lastUseDate($tag) ?? new Carbon, $range, null);
$start = $repository->firstUseDate($tag) ?? new Carbon;
$start = clone $date;
$end = $repository->firstUseDate($tag) ?? new Carbon;
if ($end < $start) {
[$start, $end] = [$end, $start]; // @codeCoverageIgnore
}
// properties for entries with their amounts.
$cache = new CacheProperties;
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('tag.entries');
$cache->addProperty('tag-period-entries');
$cache->addProperty($tag->id);
if ($cache->has()) {
return $cache->get(); // @codeCoverageIgnore
}
$collection = new Collection;
$currentEnd = clone $end;
/** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $end, $range);
$entries = new Collection;
// while end larger or equal to start
while ($currentEnd >= $start) {
$currentStart = app('navigation')->startOfPeriod($currentEnd, $range);
foreach ($dates as $currentDate) {
// get expenses and what-not in this period and this tag.
$arr = [
'string' => $end->format('Y-m-d'),
'name' => app('navigation')->periodShow($currentEnd, $range),
'start' => clone $currentStart,
'end' => clone $currentEnd,
'date' => clone $end,
'spent' => $repository->spentInPeriod($tag, $currentStart, $currentEnd),
'earned' => $repository->earnedInPeriod($tag, $currentStart, $currentEnd),
];
$collection->push($arr);
$spentSet = $repository->expenseInPeriod($tag, $currentDate['start'], $currentDate['end']);
$spent = $this->groupByCurrency($spentSet);
$earnedSet = $repository->incomeInPeriod($tag, $currentDate['start'], $currentDate['end']);
$earned = $this->groupByCurrency($earnedSet);
$transferredSet = $repository->transferredInPeriod($tag, $currentDate['start'], $currentDate['end']);
$transferred = $this->groupByCurrency($transferredSet);
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
$entries->push(
[
'transactions' => $spentSet->count() + $earnedSet->count() + $transferredSet->count(),
'title' => $title,
'spent' => $spent,
'earned' => $earned,
'transferred' => $transferred,
'route' => route('tags.show', [$tag->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
]
);
/** @var Carbon $currentEnd */
$currentEnd = clone $currentStart;
$currentEnd->subDay();
}
$cache->store($collection);
$cache->store($entries);
return $collection;
return $entries;
}
/**
* Get period overview for index.
*
* TODO refactor me.
* This list shows the overview of a type of transaction, for the period blocks on the list of transactions.
*
* @param string $what
* @param Carbon $date
@ -315,42 +331,60 @@ trait PeriodOverview
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
$range = app('preferences')->get('viewRange', '1M')->data;
$first = $repository->firstNull();
$start = Carbon::now()->subYear();
$endJournal = $repository->firstNull();
$end = null === $endJournal ? new Carbon : $endJournal->date;
$start = clone $date;
$types = config('firefly.transactionTypesByWhat.' . $what);
$entries = new Collection;
if (null !== $first) {
$start = $first->date;
}
if ($date < $start) {
[$start, $date] = [$date, $start]; // @codeCoverageIgnore
if ($end < $start) {
[$start, $end] = [$end, $start]; // @codeCoverageIgnore
}
// properties for entries with their amounts.
$cache = new CacheProperties;
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('transactions-period-entries');
$cache->addProperty($what);
/** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $date, $range);
$dates = app('navigation')->blockPeriods($start, $end, $range);
$entries = new Collection;
foreach ($dates as $currentDate) {
// get all expenses, income or transfers:
/** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($currentDate['start'], $currentDate['end'])->withOpposingAccount()->setTypes($types);
$collector->removeFilter(InternalTransferFilter::class);
$transactions = $collector->getTransactions();
if ($transactions->count() > 0) {
$sums = $this->sumPerCurrency($transactions);
$dateName = app('navigation')->periodShow($currentDate['start'], $currentDate['period']);
$sum = $transactions->sum('transaction_amount');
/** @noinspection PhpUndefinedMethodInspection */
$entries->push(
[
'name' => $dateName,
'sums' => $sums,
'sum' => $sum,
'start' => $currentDate['start']->format('Y-m-d'),
'end' => $currentDate['end']->format('Y-m-d'),
]
);
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
$grouped = $this->groupByCurrency($transactions);
$spent = [];
$earned = [];
$transferred = [];
if ('expenses' === $what || 'withdrawal' === $what) {
$spent = $grouped;
}
if ('revenue' === $what || 'deposit' === $what) {
$earned = $grouped;
}
if ('transfer' === $what || 'transfers' === $what) {
$transferred = $grouped;
}
$entries->push(
[
'transactions' => $transactions->count(),
'title' => $title,
'spent' => $spent,
'earned' => $earned,
'transferred' => $transferred,
'route' => route('transactions.index', [$what, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
]
);
}
return $entries;

View File

@ -28,6 +28,7 @@ use FireflyIII\Models\AccountType;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\ViewErrorBag;
use Log;
use URL;
@ -141,6 +142,10 @@ trait UserNavigation
*/
protected function rememberPreviousUri(string $identifier): void
{
session()->put($identifier, URL::previous());
/** @var ViewErrorBag $errors */
$errors = session()->get('errors');
if(null === $errors || (null !== $errors && 0=== $errors->count())) {
session()->put($identifier, URL::previous());
}
}
}

View File

@ -0,0 +1,123 @@
<?php
/**
* ChooseAccountHandler.php
* Copyright (c) 2018 https://github.com/bnw
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Import\JobConfiguration\FinTS;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Import\JobConfiguration\FinTSConfigurationSteps;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Support\FinTS\FinTS;
use Illuminate\Support\MessageBag;
/**
*
* Class ChooseAccountHandler
*/
class ChooseAccountHandler implements FinTSConfigurationInterface
{
/** @var AccountRepositoryInterface */
private $accountRepository;
/** @var ImportJob */
private $importJob;
/** @var ImportJobRepositoryInterface */
private $repository;
/**
* Store data associated with current stage.
*
* @param array $data
*
* @return MessageBag
*/
public function configureJob(array $data): MessageBag
{
$config = $this->repository->getConfiguration($this->importJob);
$config['fints_account'] = (string)($data['fints_account'] ?? '');
$config['local_account'] = (string)($data['local_account'] ?? '');
$config['from_date'] = (string)($data['from_date'] ?? '');
$config['to_date'] = (string)($data['to_date'] ?? '');
$this->repository->setConfiguration($this->importJob, $config);
try {
$finTS = app(FinTS::class, ['config' => $config]);
$finTS->getAccount($config['fints_account']);
} catch (FireflyException $e) {
return new MessageBag([$e->getMessage()]);
}
$this->repository->setStage($this->importJob, FinTSConfigurationSteps::GO_FOR_IMPORT);
return new MessageBag();
}
/**
* Get the data necessary to show the configuration screen.
*
* @return array
* @throws \FireflyIII\Exceptions\FireflyException
*/
public function getNextData(): array
{
$finTS = app(FinTS::class, ['config' => $this->importJob->configuration]);
$finTSAccounts = $finTS->getAccounts();
$finTSAccountsData = [];
foreach ($finTSAccounts as $account) {
$finTSAccountsData[$account->getAccountNumber()] = $account->getIban();
}
$localAccounts = [];
foreach ($this->accountRepository->getAccountsByType([AccountType::ASSET]) as $localAccount) {
$display_name = $localAccount->name;
if ($localAccount->iban) {
$display_name .= sprintf(' - %s', $localAccount->iban);
}
$localAccounts[$localAccount->id] = $display_name;
}
$data = [
'fints_accounts' => $finTSAccountsData,
'fints_account' => $this->importJob->configuration['fints_account'] ?? null,
'local_accounts' => $localAccounts,
'local_account' => $this->importJob->configuration['local_account'] ?? null,
'from_date' => $this->importJob->configuration['from_date'] ?? (new Carbon('now - 1 month'))->format('Y-m-d'),
'to_date' => $this->importJob->configuration['to_date'] ?? (new Carbon('now'))->format('Y-m-d'),
];
return $data;
}
/**
* @param ImportJob $importJob
*/
public function setImportJob(ImportJob $importJob): void
{
$this->importJob = $importJob;
$this->repository = app(ImportJobRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->repository->setUser($importJob->user);
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* FinTSConfigurationInterface.php
* Copyright (c) 2018 https://github.com/bnw
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Import\JobConfiguration\FinTS;
use FireflyIII\Models\ImportJob;
use Illuminate\Support\MessageBag;
/**
*
*/
interface FinTSConfigurationInterface
{
/**
* Store data associated with current stage.
*
* @param array $data
*
* @return MessageBag
*/
public function configureJob(array $data): MessageBag;
/**
* Get the data necessary to show the configuration screen.
*
* @return array
*/
public function getNextData(): array;
/**
* @param ImportJob $importJob
*/
public function setImportJob(ImportJob $importJob): void;
}

View File

@ -0,0 +1,110 @@
<?php
/**
* NewFinTSJobHandler.php
* Copyright (c) 2018 https://github.com/bnw
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Import\JobConfiguration\FinTS;
use FireflyIII\Import\JobConfiguration\FinTSConfigurationSteps;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Support\FinTS\FinTS;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\MessageBag;
/**
*
* Class NewFinTSJobHandler
*/
class NewFinTSJobHandler implements FinTSConfigurationInterface
{
/** @var ImportJob */
private $importJob;
/** @var ImportJobRepositoryInterface */
private $repository;
/**
* Store data associated with current stage.
*
* @param array $data
*
* @return MessageBag
* @throws \FireflyIII\Exceptions\FireflyException
*/
public function configureJob(array $data): MessageBag
{
$config = [];
$config['fints_url'] = trim($data['fints_url'] ?? '');
$config['fints_port'] = (int)($data['fints_port'] ?? '');
$config['fints_bank_code'] = (string)($data['fints_bank_code'] ?? '');
$config['fints_username'] = (string)($data['fints_username'] ?? '');
$config['fints_password'] = (string)(Crypt::encrypt($data['fints_password']) ?? '');
$this->repository->setConfiguration($this->importJob, $config);
$incomplete = false;
foreach ($config as $value) {
$incomplete = '' === $value or $incomplete;
}
if ($incomplete) {
return new MessageBag([trans('import.incomplete_fints_form')]);
}
$finTS = app(FinTS::class, ['config' => $this->importJob->configuration]);
if (true !== ($checkConnection = $finTS->checkConnection())) {
return new MessageBag([trans('import.fints_connection_failed', ['originalError' => $checkConnection])]);
}
$this->repository->setStage($this->importJob, FinTSConfigurationSteps::CHOOSE_ACCOUNT);
return new MessageBag();
}
/**
* Get the data necessary to show the configuration screen.
*
* @return array
*/
public function getNextData(): array
{
$config = $this->importJob->configuration;
return [
'fints_url' => $config['fints_url'] ?? '',
'fints_port' => $config['fints_port'] ?? '443',
'fints_bank_code' => $config['fints_bank_code'] ?? '',
'fints_username' => $config['fints_username'] ?? '',
];
}
/**
* @param ImportJob $importJob
*/
public function setImportJob(ImportJob $importJob): void
{
$this->importJob = $importJob;
$this->repository = app(ImportJobRepositoryInterface::class);
$this->repository->setUser($importJob->user);
}
}

View File

@ -80,6 +80,14 @@ class StageImportDataHandler
return $this->transactions;
}
/**
* @return bool
*/
public function isStillRunning(): bool
{
return $this->stillRunning;
}
/**
*
* @throws FireflyException

View File

@ -0,0 +1,189 @@
<?php
/**
* StageImportDataHandler.php
* Copyright (c) 2018 https://github.com/bnw
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Import\Routine\FinTS;
use Fhp\Model\StatementOfAccount\Transaction;
use Fhp\Model\StatementOfAccount\Transaction as FinTSTransaction;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account as LocalAccount;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\ImportJob;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Support\FinTS\FinTS;
use FireflyIII\Support\FinTS\MetadataParser;
use FireflyIII\Support\Import\Routine\File\OpposingAccountMapper;
use Illuminate\Support\Facades\Log;
/**
*
* Class StageImportDataHandler
*/
class StageImportDataHandler
{
/** @var AccountRepositoryInterface */
private $accountRepository;
/** @var ImportJob */
private $importJob;
/** @var OpposingAccountMapper */
private $mapper;
/** @var ImportJobRepositoryInterface */
private $repository;
/** @var array */
private $transactions;
/**
* @return array
*/
public function getTransactions(): array
{
return $this->transactions;
}
/**
* @throws FireflyException
*/
public function run()
{
Log::debug('Now in StageImportDataHandler::run()');
$localAccount = $this->accountRepository->findNull((int)$this->importJob->configuration['local_account']);
if (null === $localAccount) {
throw new FireflyException(sprintf('Cannot find Firefly account with id #%d ' , $this->importJob->configuration['local_account']));
}
$finTS = app(FinTS::class, ['config' => $this->importJob->configuration]);
$fintTSAccount = $finTS->getAccount($this->importJob->configuration['fints_account']);
$statementOfAccount = $finTS->getStatementOfAccount(
$fintTSAccount, new \DateTime($this->importJob->configuration['from_date']), new \DateTime($this->importJob->configuration['to_date'])
);
$collection = [];
foreach ($statementOfAccount->getStatements() as $statement) {
foreach ($statement->getTransactions() as $transaction) {
$collection[] = $this->convertTransaction($transaction, $localAccount);
}
}
$this->transactions = $collection;
}
/**
* @param ImportJob $importJob
*
* @return void
*/
public function setImportJob(ImportJob $importJob): void
{
$this->transactions = [];
$this->importJob = $importJob;
$this->repository = app(ImportJobRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$this->mapper = app(OpposingAccountMapper::class);
$this->mapper->setUser($importJob->user);
$this->repository->setUser($importJob->user);
$this->accountRepository->setUser($importJob->user);
}
/**
* @param FinTSTransaction $transaction
* @param LocalAccount $source
*
* @return array
*/
private function convertTransaction(FinTSTransaction $transaction, LocalAccount $source): array
{
Log::debug(sprintf('Start converting transaction %s', $transaction->getDescription1()));
$amount = (string)$transaction->getAmount();
$debitOrCredit = $transaction->getCreditDebit();
// assume deposit.
$type = TransactionType::DEPOSIT;
Log::debug(sprintf('Amount is %s', $amount));
// inverse if not.
if ($debitOrCredit !== Transaction::CD_CREDIT) {
$type = TransactionType::WITHDRAWAL;
$amount = bcmul($amount, '-1');
}
$destination = $this->mapper->map(
null,
$amount,
['iban' => $transaction->getAccountNumber(), 'name' => $transaction->getName()]
);
if ($debitOrCredit === Transaction::CD_CREDIT) {
[$source, $destination] = [$destination, $source];
}
if ($source->accountType->type === AccountType::ASSET && $destination->accountType->type === AccountType::ASSET) {
$type = TransactionType::TRANSFER;
Log::debug('Both are assets, will make transfer.');
}
$metadataParser = new MetadataParser();
$description = $metadataParser->getDescription($transaction);
$storeData = [
'user' => $this->importJob->user_id,
'type' => $type,
'date' => $transaction->getValutaDate()->format('Y-m-d'),
'description' => $description,
'piggy_bank_id' => null,
'piggy_bank_name' => null,
'bill_id' => null,
'bill_name' => null,
'tags' => [],
'internal_reference' => null,
'external_id' => null,
'notes' => null,
'bunq_payment_id' => null,
'original-source' => sprintf('fints-v%s', config('firefly.version')),
'transactions' => [
// single transaction:
[
'description' => null,
'amount' => $amount,
'currency_id' => null,
'currency_code' => 'EUR',
'foreign_amount' => null,
'foreign_currency_id' => null,
'foreign_currency_code' => null,
'budget_id' => null,
'budget_name' => null,
'category_id' => null,
'category_name' => null,
'source_id' => $source->id,
'source_name' => null,
'destination_id' => $destination->id,
'destination_name' => null,
'reconciled' => false,
'identifier' => 0,
],
],
];
return $storeData;
}
}

View File

@ -225,6 +225,7 @@ class Transaction extends Twig_Extension
}
$name = app('steam')->tryDecrypt($transaction->account_name);
$iban = $transaction->account_iban;
$transactionId = (int)$transaction->account_id;
$type = $transaction->account_type;
@ -233,6 +234,7 @@ class Transaction extends Twig_Extension
$name = $transaction->opposing_account_name;
$transactionId = (int)$transaction->opposing_account_id;
$type = $transaction->opposing_account_type;
$iban = $transaction->opposing_account_iban;
}
// Find the opposing account and use that one:
@ -264,7 +266,7 @@ class Transaction extends Twig_Extension
return $txt;
}
$txt = sprintf('<a title="%1$s" href="%2$s">%1$s</a>', e($name), route('accounts.show', [$transactionId]));
$txt = sprintf('<a title="%3$s" href="%2$s">%1$s</a>', e($name), route('accounts.show', [$transactionId]), $iban);
return $txt;
}
@ -385,12 +387,14 @@ class Transaction extends Twig_Extension
$name = app('steam')->tryDecrypt($transaction->account_name);
$transactionId = (int)$transaction->account_id;
$type = $transaction->account_type;
$iban = $transaction->account_iban;
// name is present in object, use that one:
if (null !== $transaction->opposing_account_id && 1 === bccomp($transaction->transaction_amount, '0')) {
$name = $transaction->opposing_account_name;
$transactionId = (int)$transaction->opposing_account_id;
$type = $transaction->opposing_account_type;
$iban = $transaction->opposing_account_iban;
}
// Find the opposing account and use that one:
if (null === $transaction->opposing_account_id && 1 === bccomp($transaction->transaction_amount, '0')) {
@ -415,7 +419,7 @@ class Transaction extends Twig_Extension
return $txt;
}
$txt = sprintf('<a title="%1$s" href="%2$s">%1$s</a>', e($name), route('accounts.show', [$transactionId]));
$txt = sprintf('<a title="%3$s" href="%2$s">%1$s</a>', e($name), route('accounts.show', [$transactionId]), $iban);
return $txt;
}

View File

@ -27,7 +27,7 @@ use FireflyIII\Models\TransactionJournal;
use Log;
/**
* Class AppendDescription.
* Class PrependDescription.
*/
class PrependDescription implements ActionInterface
{

View File

@ -90,7 +90,7 @@ final class ToAccountIs extends AbstractTrigger implements TriggerInterface
return true;
}
Log::debug(sprintf('RuleTrigger ToAccountIs for journal #%d: "%s" is NOT "%s", return true.', $journal->id, $toAccountName, $search));
Log::debug(sprintf('RuleTrigger ToAccountIs for journal #%d: "%s" is NOT "%s", return false.', $journal->id, $toAccountName, $search));
return false;
}

View File

@ -313,10 +313,8 @@ class FireflyValidator extends Validator
*
* @return bool
*/
public function validateRuleTriggerValue(string $attribute, string $value): bool
public function validateRuleTriggerValue(string $attribute, string $value = null): bool
{
//
// first, get the index from this string:
$parts = explode('.', $attribute);
$index = (int)($parts[1] ?? '0');

View File

@ -46,7 +46,7 @@ if (!function_exists('envNonEmpty')) {
function envNonEmpty(string $key, $default = null)
{
$result = env($key, $default);
if (is_string($result) && $result === '') {
if (is_string($result) && '' === $result) {
$result = $default;
}

View File

@ -2,6 +2,46 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [4.7.8] - 2018-10-28
### Added
- [Issue 1005](https://github.com/firefly-iii/firefly-iii/issues/1005) You can now configure Firefly III to use LDAP.
- [Issue 1071](https://github.com/firefly-iii/firefly-iii/issues/1071) You can execute transaction rules using the command line (so you can cronjob it)
- [Issue 1108](https://github.com/firefly-iii/firefly-iii/issues/1108) You can now reorder budgets.
- [Issue 1159](https://github.com/firefly-iii/firefly-iii/issues/1159) The ability to import transactions from FinTS-enabled banks.
- [Issue 1727](https://github.com/firefly-iii/firefly-iii/issues/1727) You can now use SFTP as storage for uploads and exports.
- [Issue 1733](https://github.com/firefly-iii/firefly-iii/issues/1733) You can configure Firefly III not to send emails with transaction information in them.
### Changed
- [Issue 1040](https://github.com/firefly-iii/firefly-iii/issues/1040) Fixed various things that would not scale properly in the past.
- [Issue 1771](https://github.com/firefly-iii/firefly-iii/issues/1771) A link to the transaction that fits the bill.
- [Issue 1800](https://github.com/firefly-iii/firefly-iii/issues/1800) Icon updated to match others.
- MySQL database connection now forces the InnoDB to be used.
### Fixed
- [Issue 1583](https://github.com/firefly-iii/firefly-iii/issues/1583) Some times recurring transactions would not fire.
- [Issue 1607](https://github.com/firefly-iii/firefly-iii/issues/1607) Problems with the bunq API, finally solved?! (I feel like a clickbait YouTube video now)
- [Issue 1698](https://github.com/firefly-iii/firefly-iii/issues/1698) Certificate problems in the Docker container
- [Issue 1751](https://github.com/firefly-iii/firefly-iii/issues/1751) Bug in autocomplete
- [Issue 1760](https://github.com/firefly-iii/firefly-iii/issues/1760) Tag report bad math
- [Issue 1765](https://github.com/firefly-iii/firefly-iii/issues/1765) API inconsistencies for piggy banks.
- [Issue 1774](https://github.com/firefly-iii/firefly-iii/issues/1774) Integer exception in SQLite databases
- [Issue 1775](https://github.com/firefly-iii/firefly-iii/issues/1775) Heroku now supports all locales
- [Issue 1778](https://github.com/firefly-iii/firefly-iii/issues/1778) More autocomplete problems fixed
- [Issue 1747](https://github.com/firefly-iii/firefly-iii/issues/1747) Rules now stop at the right moment.
- [Issue 1781](https://github.com/firefly-iii/firefly-iii/issues/1781) Problems when creating new rules.
- [Issue 1784](https://github.com/firefly-iii/firefly-iii/issues/1784) Can now create a liability with an empty balance.
- [Issue 1785](https://github.com/firefly-iii/firefly-iii/issues/1785) Redirect error
- [Issue 1790](https://github.com/firefly-iii/firefly-iii/issues/1790) Show attachments for bills.
- [Issue 1792](https://github.com/firefly-iii/firefly-iii/issues/1792) Mention excluded accounts.
- [Issue 1798](https://github.com/firefly-iii/firefly-iii/issues/1798) Could not recreate deleted piggy banks
- [Issue 1805](https://github.com/firefly-iii/firefly-iii/issues/1805) Fixes when handling foreign currencies
- [Issue 1807](https://github.com/firefly-iii/firefly-iii/issues/1807) Also decrypt deleted records.
- [Issue 1812](https://github.com/firefly-iii/firefly-iii/issues/1812) Fix in transactions API
- [Issue 1815](https://github.com/firefly-iii/firefly-iii/issues/1815) Opening balance account name can now be translated.
- [Issue 1830](https://github.com/firefly-iii/firefly-iii/issues/1830) Multi-user in a single browser could leak autocomplete data.
## [4.7.7] - 2018-10-01
This version of Firefly III requires PHP 7.2. I've already started using several features from 7.2. Please make sure you upgrade.

View File

@ -53,8 +53,12 @@
"ext-intl": "*",
"ext-xml": "*",
"ext-zip": "*",
"ext-json": "*",
"ext-ldap": "*",
"adldap2/adldap2-laravel": "^4.0",
"bacon/bacon-qr-code": "1.*",
"bunq/sdk_php": "dev-master#8c1faefc111d9b970168a1837ca165d854954941",
"danhunsaker/laravel-flysystem-others": "^1.3",
"davejamesmiller/laravel-breadcrumbs": "5.*",
"doctrine/dbal": "2.*",
"fideloper/proxy": "4.*",
@ -63,7 +67,11 @@
"laravelcollective/html": "5.7.*",
"league/commonmark": "0.*",
"league/csv": "9.*",
"league/flysystem-replicate-adapter": "^1.0",
"league/flysystem-sftp": "^1.0",
"league/fractal": "^0.17.0",
"litipk/flysystem-fallback-adapter": "0.1.2",
"mschindler83/fints-hbci-php": "^1.0",
"pragmarx/google2fa": "3.*",
"pragmarx/google2fa-laravel": "0.*",
"rcrowe/twigbridge": "0.9.*",

787
composer.lock generated

File diff suppressed because it is too large Load Diff

259
config/adldap.php Normal file
View File

@ -0,0 +1,259 @@
<?php
use Adldap\Schemas\ActiveDirectory;
use Adldap\Schemas\FreeIPA;
use Adldap\Schemas\OpenLDAP;
/*
* Get schema from .env file.
*/
$schema = OpenLDAP::class;
if ('FreeIPA' === envNonEmpty('ADLDAP_CONNECTION_SCHEME', 'OpenLDAP')) {
$schema = FreeIPA::class;
}
if ('ActiveDirectory' === envNonEmpty('ADLDAP_CONNECTION_SCHEME', 'OpenLDAP')) {
$schema = ActiveDirectory::class;
}
return [
/*
|--------------------------------------------------------------------------
| Connections
|--------------------------------------------------------------------------
|
| This array stores the connections that are added to Adldap. You can add
| as many connections as you like.
|
| The key is the name of the connection you wish to use and the value is
| an array of configuration settings.
|
*/
'connections' => [
'default' => [
/*
|--------------------------------------------------------------------------
| Auto Connect
|--------------------------------------------------------------------------
|
| If auto connect is true, Adldap will try to automatically connect to
| your LDAP server in your configuration. This allows you to assume
| connectivity rather than having to connect manually
| in your application.
|
| If this is set to false, you **must** connect manually before running
| LDAP operations.
|
*/
'auto_connect' => env('ADLDAP_AUTO_CONNECT', true),
/*
|--------------------------------------------------------------------------
| Connection
|--------------------------------------------------------------------------
|
| The connection class to use to run raw LDAP operations on.
|
| Custom connection classes must implement:
|
| Adldap\Connections\ConnectionInterface
|
*/
'connection' => Adldap\Connections\Ldap::class,
/*
|--------------------------------------------------------------------------
| Schema
|--------------------------------------------------------------------------
|
| The schema class to use for retrieving attributes and generating models.
|
| You can also set this option to `null` to use the default schema class.
|
| For OpenLDAP, you must use the schema:
|
| Adldap\Schemas\OpenLDAP::class
|
| For FreeIPA, you must use the schema:
|
| Adldap\Schemas\FreeIPA::class
|
| Custom schema classes must implement Adldap\Schemas\SchemaInterface
|
*/
'schema' => $schema,
/*
|--------------------------------------------------------------------------
| Connection Settings
|--------------------------------------------------------------------------
|
| This connection settings array is directly passed into the Adldap constructor.
|
| Feel free to add or remove settings you don't need.
|
*/
'connection_settings' => [
/*
|--------------------------------------------------------------------------
| Account Prefix
|--------------------------------------------------------------------------
|
| The account prefix option is the prefix of your user accounts in LDAP directory.
|
| This string is prepended to authenticating users usernames.
|
*/
'account_prefix' => env('ADLDAP_ACCOUNT_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Account Suffix
|--------------------------------------------------------------------------
|
| The account suffix option is the suffix of your user accounts in your LDAP directory.
|
| This string is appended to authenticating users usernames.
|
*/
'account_suffix' => env('ADLDAP_ACCOUNT_SUFFIX', ''),
/*
|--------------------------------------------------------------------------
| Domain Controllers
|--------------------------------------------------------------------------
|
| The domain controllers option is an array of servers located on your
| network that serve Active Directory. You can insert as many servers or
| as little as you'd like depending on your forest (with the
| minimum of one of course).
|
| These can be IP addresses of your server(s), or the host name.
|
*/
'domain_controllers' => explode(' ', env('ADLDAP_CONTROLLERS', '127.0.0.1')),
/*
|--------------------------------------------------------------------------
| Port
|--------------------------------------------------------------------------
|
| The port option is used for authenticating and binding to your LDAP server.
|
*/
'port' => env('ADLDAP_PORT', 389),
/*
|--------------------------------------------------------------------------
| Timeout
|--------------------------------------------------------------------------
|
| The timeout option allows you to configure the amount of time in
| seconds that your application waits until a response
| is received from your LDAP server.
|
*/
'timeout' => env('ADLDAP_TIMEOUT', 5),
/*
|--------------------------------------------------------------------------
| Base Distinguished Name
|--------------------------------------------------------------------------
|
| The base distinguished name is the base distinguished name you'd
| like to perform query operations on. An example base DN would be:
|
| dc=corp,dc=acme,dc=org
|
| A correct base DN is required for any query results to be returned.
|
*/
'base_dn' => env('ADLDAP_BASEDN', 'dc=temp'),
/*
|--------------------------------------------------------------------------
| Administrator Account Suffix / Prefix
|--------------------------------------------------------------------------
|
| This option allows you to set a different account prefix and suffix
| for your configured administrator account upon binding.
|
| If left empty or set to `null`, your `account_prefix` and
| `account_suffix` options above will be used.
|
*/
'admin_account_prefix' => env('ADLDAP_ADMIN_ACCOUNT_PREFIX', ''),
'admin_account_suffix' => env('ADLDAP_ADMIN_ACCOUNT_SUFFIX', ''),
/*
|--------------------------------------------------------------------------
| Administrator Username & Password
|--------------------------------------------------------------------------
|
| When connecting to your LDAP server, a username and password is required
| to be able to query and run operations on your server(s). You can
| use any user account that has these permissions. This account
| does not need to be a domain administrator unless you
| require changing and resetting user passwords.
|
*/
'admin_username' => env('ADLDAP_ADMIN_USERNAME', ''),
'admin_password' => env('ADLDAP_ADMIN_PASSWORD', ''),
/*
|--------------------------------------------------------------------------
| Follow Referrals
|--------------------------------------------------------------------------
|
| The follow referrals option is a boolean to tell active directory
| to follow a referral to another server on your network if the
| server queried knows the information your asking for exists,
| but does not yet contain a copy of it locally.
|
| This option is defaulted to false.
|
*/
'follow_referrals' => env('ADLDAP_FOLLOW_REFFERALS', false),
/*
|--------------------------------------------------------------------------
| SSL & TLS
|--------------------------------------------------------------------------
|
| If you need to be able to change user passwords on your server, then an
| SSL or TLS connection is required. All other operations are allowed
| on unsecured protocols.
|
| One of these options are definitely recommended if you
| have the ability to connect to your server securely.
|
*/
'use_ssl' => env('ADLDAP_USE_SSL', false),
'use_tls' => env('ADLDAP_USE_TLS', false),
],
],
],
];

317
config/adldap_auth.php Normal file
View File

@ -0,0 +1,317 @@
<?php
use Adldap\Laravel\Scopes\UidScope;
use Adldap\Laravel\Scopes\UpnScope;
// default OpenLDAP scopes.
$scopes = [
UidScope::class,
];
if ('FreeIPA' === env('ADLDAP_CONNECTION_SCHEME')) {
$scopes = [
UpnScope::class,
];
}
if ('ActiveDirectory' === env('ADLDAP_CONNECTION_SCHEME')) {
$scopes = [
UpnScope::class,
];
}
return [
/*
|--------------------------------------------------------------------------
| Connection
|--------------------------------------------------------------------------
|
| The LDAP connection to use for laravel authentication.
|
| You must specify connections in your `config/adldap.php` configuration file.
|
| This must be a string.
|
*/
'connection' => envNonEmpty('ADLDAP_CONNECTION', 'default'),
/*
|--------------------------------------------------------------------------
| Provider
|--------------------------------------------------------------------------
|
| The LDAP authentication provider to use depending
| if you require database synchronization.
|
| For synchronizing LDAP users to your local applications database, use the provider:
|
| Adldap\Laravel\Auth\DatabaseUserProvider::class
|
| Otherwise, if you just require LDAP authentication, use the provider:
|
| Adldap\Laravel\Auth\NoDatabaseUserProvider::class
|
*/
'provider' => Adldap\Laravel\Auth\DatabaseUserProvider::class,
//'provider' => Adldap\Laravel\Auth\NoDatabaseUserProvider::class,
/*
|--------------------------------------------------------------------------
| Rules
|--------------------------------------------------------------------------
|
| Rules allow you to control user authentication requests depending on scenarios.
|
| You can create your own rules and insert them here.
|
| All rules must extend from the following class:
|
| Adldap\Laravel\Validation\Rules\Rule
|
*/
'rules' => [
// Denys deleted users from authenticating.
Adldap\Laravel\Validation\Rules\DenyTrashed::class,
// Allows only manually imported users to authenticate.
// Adldap\Laravel\Validation\Rules\OnlyImported::class,
],
/*
|--------------------------------------------------------------------------
| Scopes
|--------------------------------------------------------------------------
|
| Scopes allow you to restrict the LDAP query that locates
| users upon import and authentication.
|
| All scopes must implement the following interface:
|
| Adldap\Laravel\Scopes\ScopeInterface
|[
// Only allows users with a user principal name to authenticate.
// Remove this if you're using OpenLDAP.
//Adldap\Laravel\Scopes\UpnScope::class,
// Only allows users with a uid to authenticate.
// Uncomment if you're using OpenLDAP.
Adldap\Laravel\Scopes\UidScope::class,
],
*/
'scopes' => $scopes,
'usernames' => [
/*
|--------------------------------------------------------------------------
| LDAP
|--------------------------------------------------------------------------
|
| Discover:
|
| The discover value is the users attribute you would
| like to locate LDAP users by in your directory.
|
| For example, using the default configuration below, if you're
| authenticating users with an email address, your LDAP server
| will be queried for a user with the a `userprincipalname`
| equal to the entered email address.
|
| Authenticate:
|
| The authenticate value is the users attribute you would
| like to use to bind to your LDAP server.
|
| For example, when a user is located by the above 'discover'
| attribute, the users attribute you specify below will
| be used as the username to bind to your LDAP server.
|
*/
'ldap' => [
'discover' => envNonEmpty('ADLDAP_DISCOVER_FIELD', 'userprincipalname'),
'authenticate' => envNonEmpty('ADLDAP_AUTH_FIELD', 'distinguishedname'),
],
/*
|--------------------------------------------------------------------------
| Eloquent
|--------------------------------------------------------------------------
|
| The value you enter is the database column name used for locating
| the local database record of the authenticating user.
|
| If you're using a `username` column instead, change this to `username`.
|
| This option is only applicable to the DatabaseUserProvider.
|
*/
'eloquent' => 'email',
/*
|--------------------------------------------------------------------------
| Windows Authentication Middleware (SSO)
|--------------------------------------------------------------------------
|
| Discover:
|
| The 'discover' value is the users attribute you would
| like to locate LDAP users by in your directory.
|
| For example, if 'samaccountname' is the value, then your LDAP server is
| queried for a user with the 'samaccountname' equal to the value of
| $_SERVER['AUTH_USER'].
|
| If a user is found, they are imported (if using the DatabaseUserProvider)
| into your local database, then logged in.
|
| Key:
|
| The 'key' value represents the 'key' of the $_SERVER
| array to pull the users account name from.
|
| For example, $_SERVER['AUTH_USER'].
|
*/
'windows' => [
'discover' => envNonEmpty('WINDOWS_SSO_DISCOVER', 'samaccountname'),
'key' => envNonEmpty('WINDOWS_SSO_KEY', 'AUTH_USER'),
],
],
'passwords' => [
/*
|--------------------------------------------------------------------------
| Password Sync
|--------------------------------------------------------------------------
|
| The password sync option allows you to automatically synchronize users
| LDAP passwords to your local database. These passwords are hashed
| natively by Laravel using the bcrypt() method.
|
| Enabling this option would also allow users to login to their accounts
| using the password last used when an LDAP connection was present.
|
| If this option is disabled, the local database account is applied a
| random 16 character hashed password upon every login, and will
| lose access to this account upon loss of LDAP connectivity.
|
| This option must be true or false and is only applicable
| to the DatabaseUserProvider.
|
*/
'sync' => env('ADLDAP_PASSWORD_SYNC', false),
/*
|--------------------------------------------------------------------------
| Column
|--------------------------------------------------------------------------
|
| This is the column of your users database table
| that is used to store passwords.
|
| Set this to `null` if you do not have a password column.
|
| This option is only applicable to the DatabaseUserProvider.
|
*/
'column' => 'password',
],
/*
|--------------------------------------------------------------------------
| Login Fallback
|--------------------------------------------------------------------------
|
| The login fallback option allows you to login as a user located on the
| local database if active directory authentication fails.
|
| Set this to true if you would like to enable it.
|
| This option must be true or false and is only
| applicable to the DatabaseUserProvider.
|
*/
'login_fallback' => env('ADLDAP_LOGIN_FALLBACK', false),
/*
|--------------------------------------------------------------------------
| Sync Attributes
|--------------------------------------------------------------------------
|
| Attributes specified here will be added / replaced on the user model
| upon login, automatically synchronizing and keeping the attributes
| up to date.
|
| The array key represents the users Laravel model key, and
| the value represents the users LDAP attribute.
|
| This option must be an array and is only applicable
| to the DatabaseUserProvider.
|
*/
'sync_attributes' => [
'email' => envNonEmpty('ADLDAP_SYNC_FIELD', 'userprincipalname'),
//'name' => 'cn',
],
/*
|--------------------------------------------------------------------------
| Logging
|--------------------------------------------------------------------------
|
| User authentication attempts will be logged using Laravel's
| default logger if this setting is enabled.
|
| No credentials are logged, only usernames.
|
| This is usually stored in the '/storage/logs' directory
| in the root of your application.
|
| This option is useful for debugging as well as auditing.
|
| You can freely remove any events you would not like to log below,
| as well as use your own listeners if you would prefer.
|
*/
'logging' => [
'enabled' => true,
'events' => [
\Adldap\Laravel\Events\Importing::class => \Adldap\Laravel\Listeners\LogImport::class,
\Adldap\Laravel\Events\Synchronized::class => \Adldap\Laravel\Listeners\LogSynchronized::class,
\Adldap\Laravel\Events\Synchronizing::class => \Adldap\Laravel\Listeners\LogSynchronizing::class,
\Adldap\Laravel\Events\Authenticated::class => \Adldap\Laravel\Listeners\LogAuthenticated::class,
\Adldap\Laravel\Events\Authenticating::class => \Adldap\Laravel\Listeners\LogAuthentication::class,
\Adldap\Laravel\Events\AuthenticationFailed::class => \Adldap\Laravel\Listeners\LogAuthenticationFailure::class,
\Adldap\Laravel\Events\AuthenticationRejected::class => \Adldap\Laravel\Listeners\LogAuthenticationRejection::class,
\Adldap\Laravel\Events\AuthenticationSuccessful::class => \Adldap\Laravel\Listeners\LogAuthenticationSuccess::class,
\Adldap\Laravel\Events\DiscoveredWithCredentials::class => \Adldap\Laravel\Listeners\LogDiscovery::class,
\Adldap\Laravel\Events\AuthenticatedWithWindows::class => \Adldap\Laravel\Listeners\LogWindowsAuth::class,
\Adldap\Laravel\Events\AuthenticatedModelTrashed::class => \Adldap\Laravel\Listeners\LogTrashedModel::class,
],
],
];

View File

@ -62,7 +62,6 @@ return [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
@ -88,14 +87,9 @@ return [
'providers' => [
'users' => [
'driver' => 'eloquent',
'driver' => envNonEmpty('LOGIN_PROVIDER', 'eloquent'),//'adldap',
'model' => FireflyIII\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*

View File

@ -28,7 +28,7 @@ $username = '';
$password = '';
$database = '';
if (!($databaseUrl === false)) {
if (!(false === $databaseUrl)) {
$options = parse_url($databaseUrl);
$host = $options['host'];
$username = $options['user'];
@ -57,7 +57,7 @@ return [
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
'engine' => 'InnoDB',
],
'pgsql' => [
'driver' => 'pgsql',

View File

@ -21,6 +21,21 @@
declare(strict_types=1);
$uploadDisk = [
'driver' => 'mirror',
'disks' => ['local-upload'],
];
$exportDisk = [
'driver' => 'mirror',
'disks' => ['local-export'],
];
// setting the SFTP host is enough to trigger the SFTP option.
if ('' !== env('SFTP_HOST', '')) {
array_push($uploadDisk['disks'], 'sftp-upload');
array_push($exportDisk['disks'], 'sftp-export');
}
return [
@ -59,25 +74,68 @@ return [
| may even configure multiple disks of the same driver. Defaults have
| been setup for each driver as an example of the required options.
|
| Supported Drivers: "local", "ftp", "s3", "rackspace"
| Supported: "local", "ftp", "s3", "rackspace", "null", "azure", "copy",
| "dropbox", "gridfs", "memory", "phpcr", "replicate", "sftp",
| "vfs", "webdav", "zip", "bos", "cloudinary", "eloquent",
| "fallback", "github", "gdrive", "google", "mirror", "onedrive",
| "oss", "qiniu", "redis", "runabove", "sae", "smb", "temp"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
],
'upload' => [
// local storage configuration for upload and export:
'local-upload' => [
'driver' => 'local',
'root' => storage_path('upload'),
],
'export' => [
'local-export' => [
'driver' => 'local',
'root' => storage_path('export'),
],
// SFTP storage configuration for upload and export:
'sftp-upload' => [
'driver' => 'sftp',
'host' => env('SFTP_HOST', '127.0.0.1'),
'port' => env('SFTP_PORT', 22),
'username' => env('SFTP_USERNAME', 'anonymous'),
'password' => env('SFTP_PASSWORD', ''),
'root' => env('SFTP_UPLOAD_PATH', ''),
'privateKey' => env('SFTP_PRIV_KEY'),
// Optional SFTP Settings
// 'timeout' => 30,
// 'directoryPerm' => 0755,
// 'permPublic' => 0644,
// 'permPrivate' => 0600,
],
'sftp-export' => [
'driver' => 'sftp',
'host' => env('SFTP_HOST', '127.0.0.1'),
'port' => env('SFTP_PORT', 22),
'username' => env('SFTP_USERNAME', 'anonymous'),
'password' => env('SFTP_PASSWORD', ''),
'root' => env('SFTP_EXPORT_PATH', ''),
'privateKey' => env('SFTP_PRIV_KEY'),
// Optional SFTP Settings
// 'timeout' => 30,
// 'directoryPerm' => 0755,
// 'permPublic' => 0644,
// 'permPrivate' => 0600,
],
// final configuration of upload disk and export disk.
'upload' => $uploadDisk,
'export' => $exportDisk,
// various other paths:
'database' => [
'driver' => 'local',
'root' => storage_path('database'),
@ -98,6 +156,9 @@ return [
'visibility' => 'public',
],
// unused storage backends.
/*
's3' => [
'driver' => 's3',
'key' => env('AWS_KEY'),
@ -106,6 +167,340 @@ return [
'bucket' => env('AWS_BUCKET'),
],
'sftp' => [
'driver' => 'sftp',
'host' => 'sftp.example.com',
'username' => 'username',
'password' => 'password',
// Optional SFTP Settings
// 'privateKey' => 'path/to/or/contents/of/privatekey',
// 'port' => 22,
// 'root' => '/path/to/root',
// 'timeout' => 30,
// 'directoryPerm' => 0755,
// 'permPublic' => 0644,
// 'permPrivate' => 0600,
],
'rackspace' => [
'driver' => 'rackspace',
'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/',
'username' => 'your-username',
'key' => 'your-key',
'region' => 'IAD',
'url_type' => 'publicURL',
'container' => 'your-container',
],
'null' => [
'driver' => 'null',
],
'azure' => [
'driver' => 'azure',
'accountName' => 'your-account-name',
'apiKey' => 'your-api-key',
'container' => 'your-container',
],
'gridfs' => [
'driver' => 'gridfs',
'server' => 'your-server',
'context' => 'your-context',
'dbName' => 'your-db-name',
// You can also provide other MongoDB connection options here
],
'memory' => [
'driver' => 'memory',
],
'phpcr-jackrabbit' => [
'driver' => 'phpcr',
'jackrabbit_url' => 'your-jackrabbit-url',
'workspace' => 'your-workspace',
'root' => 'your-root',
// Optional PHPCR Settings
// 'userId' => 'your-user-id',
// 'password' => 'your-password',
],
'phpcr-dbal' => [
'driver' => 'phpcr',
'database' => 'mysql',
'workspace' => 'your-workspace',
'root' => 'your-root',
// Optional PHPCR Settings
// 'userId' => 'your-user-id',
// 'password' => 'your-password',
],
'phpcr-prismic' => [
'driver' => 'phpcr',
'prismic_uri' => 'your-prismic-uri',
'workspace' => 'your-workspace',
'root' => 'your-root',
// Optional PHPCR Settings
// 'userId' => 'your-user-id',
// 'password' => 'your-password',
],
'replicate' => [
'driver' => 'replicate',
'master' => 'local',
'replica' => 's3',
],
'vfs' => [
'driver' => 'vfs',
],
'webdav' => [
'driver' => 'webdav',
'baseUri' => 'http://example.org/dav/',
// Optional WebDAV Settings
// 'userName' => 'user',
// 'password' => 'password',
// 'proxy' => 'locahost:8888',
// 'authType' => 'digest', // alternately 'ntlm' or 'basic'
// 'encoding' => 'all', // same as ['deflate', 'gzip', 'identity']
],
'zip' => [
'driver' => 'zip',
'path' => 'path/to/file.zip',
// Alternate value if twistor/flysystem-stream-wrapper is available
// 'path' => 'local://path/to/file.zip',
],
'backblaze' => [
'driver' => 'backblaze',
'account_id' => 'your-account-id',
'application_key' => 'your-app-key',
'bucket' => 'your-bucket',
],
'bos' => [
'driver' => 'bos',
'credentials' => [
'ak' => 'your-access-key-id',
'sk' => 'your-secret-access-key',
],
'bucket' => 'your-bucket',
// Optional BOS Settings
// 'endpoint' => 'http://bj.bcebos.com',
],
'clamav' => [
'driver' => 'clamav',
'server' => 'tcp://127.0.0.1:3310',
'drive' => 'local',
// Optional ClamAV Settings
// 'copy_scan' => false,
],
'cloudinary' => [
'driver' => 'cloudinary',
'api_key' => env('CLOUDINARY_API_KEY'),
'api_secret' => env('CLOUDINARY_API_SECRET'),
'cloud_name' => env('CLOUDINARY_CLOUD_NAME'),
],
'dropbox' => [
'driver' => 'dropbox',
'authToken' => 'your-auth-token',
],
'eloquent' => [
'driver' => 'eloquent',
// Optional Eloquent Settings
// 'model' => '\Rokde\Flysystem\Adapter\Model\FileModel',
],
'fallback' => [
'driver' => 'fallback',
'main' => 'local',
'fallback' => 's3',
],
'gdrive' => [
'driver' => 'gdrive',
'client_id' => 'your-client-id',
'secret' => 'your-secret',
'token' => 'your-token',
// Optional GDrive Settings
// 'root' => 'your-root-directory',
// 'paths_sheet' => 'your-paths-sheet',
// 'paths_cache_drive' => 'local',
],
'github' => [
'driver' => 'github',
'project' => 'yourname/project',
'token' => 'your-github-token',
],
'google' => [
'driver' => 'google',
'project_id' => 'your-project-id',
'bucket' => 'your-bucket',
// Optional Google Cloud Storage Settings
// 'prefix' => 'prefix/path/for/drive',
// 'url' => 'http://your.custom.cname/',
// 'key_file' => 'path/to/file.json',
//
// Alternate value if twistor/flysystem-stream-wrapper is available
// 'key_file' => 'local://path/to/file.json',
],
'http' => [
'driver' => 'http',
'root' => 'http://example.com',
// Optional HTTP Settings
// 'use_head' => true,
// 'context' => [],
],
'onedrive' => [
'driver' => 'onedrive',
'access_token' => 'your-access-token',
// Options only needed for ignited/flysystem-onedrive
// 'base_url' => 'https://api.onedrive.com/v1.0/',
// 'use_logger' => false,
// Option only used by nicolasbeauvais/flysystem-onedrive
// 'root' => 'root',
],
'openstack' => [
'driver' => 'openstack',
'auth_url' => 'your-auth-url',
'region' => 'your-region',
'user_id' => 'your-user-id',
'password' => 'your-password',
'project_id' => 'your-project-id',
'container' => 'your-container',
],
'oss' => [
'driver' => 'oss',
'access_id' => env('OSS_ACCESS_KEY_ID'),
'access_key' => env('OSS_ACCESS_KEY_SECRET'),
'endpoint' => env('OSS_ENDPOINT'),
'bucket' => env('OSS_BUCKET'),
// Optional OSS Settings
// 'prefix' => '',
// 'region' => '', // One of 'hangzhou', 'qingdao', 'beijing', 'hongkong',
// // 'shenzhen', 'shanghai', 'west-1' and 'southeast-1'
],
'pdo' => [
'driver' => 'pdo',
'database' => 'default',
],
'qcloud' => [
'driver' => 'qcloud',
'app_id' => 'your-app-id',
'secret_id' => 'your-secret-id',
'secret_key' => 'your-secret-key',
'bucket' => 'your-bucket-name',
'protocol' => 'https',
// Optional Tencent/Qcloud COS Settings
// 'domain' => 'your-domain',
// 'timeout' => 60,
// 'region' => 'gz',
],
'qiniu' => [
'driver' => 'qiniu',
'accessKey' => 'your-access-key',
'secretKey' => 'your-secret-key',
'bucket' => 'your-bucket',
'domain' => 'xxxx.qiniudn.com',
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
'runabove' => [
'driver' => 'runabove',
'username' => 'your-username',
'password' => 'your-password',
'tenantId' => 'your-tenantId',
// Optional Runabove Settings
// 'container' => 'container',
// 'region' => 'SBG1', // One of 'SBG1', 'BHS1', and 'GRA1'
],
'selectel' => [
'driver' => 'selectel',
'username' => 'your-username',
'password' => 'your-password',
'container' => 'your-container',
// Optional Selectel Settings
// 'domain' => '',
],
'sharefile' => [
'driver' => 'sharefile',
'hostname' => 'sharefile.example.com',
'client_id' => 'your-client-id',
'secret' => 'your-secret',
'username' => 'your-username',
'password' => 'your-password',
],
'smb' => [
'driver' => 'smb',
'host' => 'smb.example.com',
'username' => 'your-username',
'password' => 'your-password',
'path' => 'path/to/shared/directory/for/root',
],
'temp' => [
'driver' => 'temp',
// Optional TempDir Settings
// 'prefix' => '',
// 'tempdir' => '/tmp',
],
'upyun' => [
'driver' => 'upyun',
'bucket' => 'your-bucket',
'operator' => 'operator-name',
'password' => 'operator-password',
'protocol' => 'https',
'domain' => 'example.b0.upaiyun.com',
],
'yandex' => [
'driver' => 'yandex',
'access_token' => 'your-access-token',
// Optional Yandex Settings
// 'prefix' => 'app:/',
],
*/
],
/*
|--------------------------------------------------------------------------
| Automatically Register Stream Wrappers
|--------------------------------------------------------------------------
|
| This is a list of the filesystem "disks" to automatically register the
| stream wrappers for on application start. Any "disk" you don't want to
| register on every application load will have to be manually referenced
| before attempting stream access, as the stream wrapper is otherwise only
| registered when used.
|
*/
/*
// Disabled, pending "twistor/flysystem-stream-wrapper" dependency
'autowrap' => [
'local',
],
*/
];

View File

@ -91,10 +91,11 @@ return [
'is_demo_site' => false,
],
'encryption' => null === env('USE_ENCRYPTION') || env('USE_ENCRYPTION') === true,
'version' => '4.7.7',
'api_version' => '0.8',
'db_version' => 5,
'version' => '4.7.8',
'api_version' => '0.81',
'db_version' => 6,
'maxUploadSize' => 15242880,
'login_provider' => env('LOGIN_PROVIDER', 'eloquent'),
'allowedMimes' => [
/* plain files */
'text/plain',
@ -238,16 +239,25 @@ return [
'languages' => [
// completed languages
'en_US' => ['name_locale' => 'English', 'name_english' => 'English'],
'es_ES' => ['name_locale' => 'Español', 'name_english' => 'Spanish'],
'de_DE' => ['name_locale' => 'Deutsch', 'name_english' => 'German'],
'fr_FR' => ['name_locale' => 'Français', 'name_english' => 'French'],
//'id_ID' => ['name_locale' => 'Bahasa Indonesia', 'name_english' => 'Indonesian'],
'it_IT' => ['name_locale' => 'Italiano', 'name_english' => 'Italian'],
'nl_NL' => ['name_locale' => 'Nederlands', 'name_english' => 'Dutch'],
'pl_PL' => ['name_locale' => 'Polski', 'name_english' => 'Polish '],
//'pt_BR' => ['name_locale' => 'Português do Brasil', 'name_english' => 'Portuguese (Brazil)'],
'ru_RU' => ['name_locale' => 'Русский', 'name_english' => 'Russian'],
//'tr_TR' => ['name_locale' => 'Türkçe', 'name_english' => 'Turkish'],
'es_ES' => ['name_locale' => 'Español', 'name_english' => 'Spanish'], // 2018-10-26: 96%
'de_DE' => ['name_locale' => 'Deutsch', 'name_english' => 'German'], // 2018-10-26: 100%
'fr_FR' => ['name_locale' => 'Français', 'name_english' => 'French'], // 2018-10-26: 100%
//'id_ID' => ['name_locale' => 'Bahasa Indonesia', 'name_english' => 'Indonesian'], // 2018-10-26: 61% :(
'it_IT' => ['name_locale' => 'Italiano', 'name_english' => 'Italian'], // 2018-10-26: 100%
'nl_NL' => ['name_locale' => 'Nederlands', 'name_english' => 'Dutch'], // 2018-10-26: 100%
'pl_PL' => ['name_locale' => 'Polski', 'name_english' => 'Polish '], // 2018-10-26: 76%
'pt_BR' => ['name_locale' => 'Português do Brasil', 'name_english' => 'Portuguese (Brazil)'], // 2018-10-26: 77%
'ru_RU' => ['name_locale' => 'Русский', 'name_english' => 'Russian'], // 2018-10-26: 80%
//'tr_TR' => ['name_locale' => 'Türkçe', 'name_english' => 'Turkish'], // 2018-10-26: 71%
// very far away:
//'ca_ES' => ['name_locale' => 'Catalan', 'name_english' => 'Catalan'], // 2018-10-26: 0%
//'cs_CZ' => ['name_locale' => 'Czech', 'name_english' => 'Czech'], // 2018-10-26: 8%
//'he_IL' => ['name_locale' => 'Hebrew', 'name_english' => 'Hebrew'], // 2018-10-26: 3%
//'hu_HU' => ['name_locale' => 'Hungarian', 'name_english' => 'Hungarian'], // 2018-10-26: 40%
//'nb_NO' => ['name_locale' => 'Norwegian', 'name_english' => 'Norwegian'], // 2018-10-26: 54%
//'sl_SI' => ['name_locale' => 'Slovenian', 'name_english' => 'Slovenian'], // 2018-10-26: 10%
//'uk_UA' => ['name_locale' => 'Ukranian', 'name_english' => 'Ukranian'], // 2018-10-26: 3%
],
'transactionTypesByWhat' => [
'expenses' => ['Withdrawal'],
@ -374,23 +384,59 @@ return [
'convert_deposit' => ConvertToDeposit::class,
'convert_transfer' => ConvertToTransfer::class,
],
'rule-actions-text' => [
'context-rule-actions' => [
'set_category',
'set_budget',
'add_tag',
'remove_tag',
'link_to_bill',
'set_description',
'append_description',
'prepend_description',
'set_source_account',
'set_destination_account',
'set_notes',
'append_notes',
'prepend_notes',
'link_to_bill',
'convert_withdrawal',
'convert_deposit',
'convert_transfer',
],
'test-triggers' => [
'context-rule-triggers' => [
'from_account_starts',
'from_account_ends',
'from_account_is',
'from_account_contains',
'to_account_starts',
'to_account_ends',
'to_account_is',
'to_account_contains',
'amount_less',
'amount_exactly',
'amount_more',
'description_starts',
'description_ends',
'description_contains',
'description_is',
'transaction_type',
'category_is',
'budget_is',
'tag_is',
'currency_is',
'notes_contain',
'notes_start',
'notes_end',
'notes_are',
],
'test-triggers' => [
'limit' => 10,
'range' => 200,
],
'default_currency' => 'EUR',
'default_language' => 'en_US',
'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category',
'budget', 'bill', 'type', 'date', 'date_before', 'date_after', 'on', 'before', 'after'],
'default_currency' => 'EUR',
'default_language' => 'en_US',
'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category',
'budget', 'bill', 'type', 'date', 'date_before', 'date_after', 'on', 'before', 'after'],
// tag notes has_attachments
];

View File

@ -25,6 +25,7 @@ declare(strict_types=1);
use FireflyIII\Import\JobConfiguration\BunqJobConfiguration;
use FireflyIII\Import\JobConfiguration\FakeJobConfiguration;
use FireflyIII\Import\JobConfiguration\FileJobConfiguration;
use FireflyIII\Import\JobConfiguration\FinTSJobConfiguration;
use FireflyIII\Import\JobConfiguration\SpectreJobConfiguration;
use FireflyIII\Import\JobConfiguration\YnabJobConfiguration;
use FireflyIII\Import\Prerequisites\BunqPrerequisites;
@ -34,6 +35,7 @@ use FireflyIII\Import\Prerequisites\YnabPrerequisites;
use FireflyIII\Import\Routine\BunqRoutine;
use FireflyIII\Import\Routine\FakeRoutine;
use FireflyIII\Import\Routine\FileRoutine;
use FireflyIII\Import\Routine\FinTSRoutine;
use FireflyIII\Import\Routine\SpectreRoutine;
use FireflyIII\Import\Routine\YnabRoutine;
use FireflyIII\Support\Import\Routine\File\CSVProcessor;
@ -49,6 +51,7 @@ return [
'plaid' => false,
'quovo' => false,
'yodlee' => false,
'fints' => true,
'bad' => false, // always disabled
],
// demo user can use these import providers (when enabled):
@ -61,6 +64,7 @@ return [
'plaid' => false,
'quovo' => false,
'yodlee' => false,
'fints' => false,
],
// a normal user user can use these import providers (when enabled):
'allowed_for_user' => [
@ -72,6 +76,7 @@ return [
'plaid' => true,
'quovo' => true,
'yodlee' => true,
'fints' => true,
],
// some providers have pre-requisites.
'has_prereq' => [
@ -83,6 +88,7 @@ return [
'plaid' => true,
'quovo' => true,
'yodlee' => true,
'fints' => false,
],
// if so, there must be a class to handle them.
'prerequisites' => [
@ -94,6 +100,7 @@ return [
'plaid' => false,
'quovo' => false,
'yodlee' => false,
'fints' => false,
],
// some providers may need extra configuration per job
'has_job_config' => [
@ -105,6 +112,7 @@ return [
'plaid' => false,
'quovo' => false,
'yodlee' => false,
'fints' => true,
],
// if so, this is the class that handles it.
'configuration' => [
@ -116,6 +124,7 @@ return [
'plaid' => false,
'quovo' => false,
'yodlee' => false,
'fints' => FinTSJobConfiguration::class,
],
// this is the routine that runs the actual import.
'routine' => [
@ -127,6 +136,7 @@ return [
'plaid' => false,
'quovo' => false,
'yodlee' => false,
'fints' => FinTSRoutine::class,
],
'options' => [

View File

@ -14,7 +14,7 @@ services:
- FF_DB_CONNECTION=pgsql
- TZ=Europe/Amsterdam
- APP_LOG_LEVEL=debug
image: jc5x/firefly-iii:develop
image: jc5x/firefly-iii
links:
- firefly_iii_db
networks:
@ -34,7 +34,7 @@ services:
environment:
- POSTGRES_PASSWORD=firefly
- POSTGRES_USER=firefly
image: "postgres:latest"
image: "postgres:10"
networks:
- firefly_iii_net
volumes:

Some files were not shown because too many files have changed in this diff Show More