Merge branch 'release/4.7.8'

This commit is contained in:
James Cole
2018-10-28 19:44:34 +01:00
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_PORT=${FF_DB_PORT}
DB_DATABASE=${FF_DB_NAME} DB_DATABASE=${FF_DB_NAME}
DB_USERNAME=${FF_DB_USER} 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. # If you're looking for performance improvements, you could install memcached.
CACHE_DRIVER=file CACHE_DRIVER=file
SESSION_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 settings. Should not be necessary to change these.
COOKIE_PATH="/" COOKIE_PATH="/"
COOKIE_DOMAIN= COOKIE_DOMAIN=
@@ -61,7 +74,7 @@ MAIL_HOST=${MAIL_HOST}
MAIL_PORT=${MAIL_PORT} MAIL_PORT=${MAIL_PORT}
MAIL_FROM=${MAIL_FROM} MAIL_FROM=${MAIL_FROM}
MAIL_USERNAME=${MAIL_USERNAME} MAIL_USERNAME=${MAIL_USERNAME}
MAIL_PASSWORD=${MAIL_PASSWORD} MAIL_PASSWORD="${MAIL_PASSWORD}"
MAIL_ENCRYPTION=${MAIL_ENCRYPTION} MAIL_ENCRYPTION=${MAIL_ENCRYPTION}
# Other mail drivers: # Other mail drivers:
@@ -74,6 +87,9 @@ SPARKPOST_SECRET=${SPARKPOST_SECRET}
SEND_REGISTRATION_MAIL=true SEND_REGISTRATION_MAIL=true
SEND_ERROR_MESSAGE=false 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. # 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} 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. # This makes it easier to migrate your database. Not that some fields will never be decrypted.
USE_ENCRYPTION=true 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. # Leave the following configuration vars as is.
# Unless you like to tinker and know what you're doing. # Unless you like to tinker and know what you're doing.
APP_NAME=FireflyIII APP_NAME=FireflyIII
ADLDAP_CONNECTION=default
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
QUEUE_DRIVER=sync QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
@@ -108,5 +166,3 @@ IS_DOCKER=true
IS_SANDSTORM=false IS_SANDSTORM=false
IS_HEROKU=false IS_HEROKU=false
BUNQ_USE_SANDBOX=false BUNQ_USE_SANDBOX=false
MAILGUN_DOMAIN=
MAILGUN_SECRET=

View File

@@ -49,6 +49,19 @@ DB_PASSWORD=secret
CACHE_DRIVER=file CACHE_DRIVER=file
SESSION_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 settings. Should not be necessary to change these.
COOKIE_PATH="/" COOKIE_PATH="/"
COOKIE_DOMAIN= COOKIE_DOMAIN=
@@ -74,6 +87,9 @@ SPARKPOST_SECRET=
SEND_REGISTRATION_MAIL=true SEND_REGISTRATION_MAIL=true
SEND_ERROR_MESSAGE=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. # 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,52 @@ ANALYTICS_ID=
# This makes it easier to migrate your database. Not that some fields will never be decrypted. # This makes it easier to migrate your database. Not that some fields will never be decrypted.
USE_ENCRYPTION=true 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. # Leave the following configuration vars as is.
# Unless you like to tinker and know what you're doing. # Unless you like to tinker and know what you're doing.
APP_NAME=FireflyIII APP_NAME=FireflyIII
ADLDAP_CONNECTION=default
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
QUEUE_DRIVER=sync QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
@@ -108,5 +167,3 @@ IS_DOCKER=false
IS_SANDSTORM=false IS_SANDSTORM=false
IS_HEROKU=false IS_HEROKU=false
BUNQ_USE_SANDBOX=false BUNQ_USE_SANDBOX=false
MAILGUN_DOMAIN=
MAILGUN_SECRET=

View File

@@ -49,6 +49,19 @@ DB_CONNECTION=pgsql
CACHE_DRIVER=file CACHE_DRIVER=file
SESSION_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 settings. Should not be necessary to change these.
COOKIE_PATH="/" COOKIE_PATH="/"
COOKIE_DOMAIN= COOKIE_DOMAIN=
@@ -74,6 +87,9 @@ SPARKPOST_SECRET=
SEND_REGISTRATION_MAIL=true SEND_REGISTRATION_MAIL=true
SEND_ERROR_MESSAGE=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. # 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,52 @@ ANALYTICS_ID=
# This makes it easier to migrate your database. Not that some fields will never be decrypted. # This makes it easier to migrate your database. Not that some fields will never be decrypted.
USE_ENCRYPTION=true 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. # Leave the following configuration vars as is.
# Unless you like to tinker and know what you're doing. # Unless you like to tinker and know what you're doing.
APP_NAME=FireflyIII APP_NAME=FireflyIII
ADLDAP_CONNECTION=default
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
QUEUE_DRIVER=sync QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
@@ -108,5 +167,3 @@ IS_DOCKER=false
IS_SANDSTORM=false IS_SANDSTORM=false
IS_HEROKU=true IS_HEROKU=true
BUNQ_USE_SANDBOX=false BUNQ_USE_SANDBOX=false
MAILGUN_DOMAIN=
MAILGUN_SECRET=

View File

@@ -49,6 +49,19 @@ DB_PASSWORD=firefly
CACHE_DRIVER=file CACHE_DRIVER=file
SESSION_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 settings. Should not be necessary to change these.
COOKIE_PATH="/" COOKIE_PATH="/"
COOKIE_DOMAIN= COOKIE_DOMAIN=
@@ -74,6 +87,9 @@ SPARKPOST_SECRET=
SEND_REGISTRATION_MAIL=true SEND_REGISTRATION_MAIL=true
SEND_ERROR_MESSAGE=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. # 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,52 @@ ANALYTICS_ID=
# This makes it easier to migrate your database. Not that some fields will never be decrypted. # This makes it easier to migrate your database. Not that some fields will never be decrypted.
USE_ENCRYPTION=true 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. # Leave the following configuration vars as is.
# Unless you like to tinker and know what you're doing. # Unless you like to tinker and know what you're doing.
APP_NAME=FireflyIII APP_NAME=FireflyIII
ADLDAP_CONNECTION=default
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
QUEUE_DRIVER=sync QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
@@ -108,5 +167,3 @@ IS_DOCKER=false
IS_SANDSTORM=true IS_SANDSTORM=true
IS_HEROKU=false IS_HEROKU=false
BUNQ_USE_SANDBOX=false BUNQ_USE_SANDBOX=false
MAILGUN_DOMAIN=
MAILGUN_SECRET=

View File

@@ -49,6 +49,19 @@ DB_CONNECTION=sqlite
CACHE_DRIVER=file CACHE_DRIVER=file
SESSION_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 settings. Should not be necessary to change these.
COOKIE_PATH="/" COOKIE_PATH="/"
COOKIE_DOMAIN= COOKIE_DOMAIN=
@@ -74,6 +87,9 @@ SPARKPOST_SECRET=
SEND_REGISTRATION_MAIL=true SEND_REGISTRATION_MAIL=true
SEND_ERROR_MESSAGE=false 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. # 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,52 @@ ANALYTICS_ID=
# This makes it easier to migrate your database. Not that some fields will never be decrypted. # This makes it easier to migrate your database. Not that some fields will never be decrypted.
USE_ENCRYPTION=false 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. # Leave the following configuration vars as is.
# Unless you like to tinker and know what you're doing. # Unless you like to tinker and know what you're doing.
APP_NAME=FireflyIII APP_NAME=FireflyIII
ADLDAP_CONNECTION=default
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
QUEUE_DRIVER=sync QUEUE_DRIVER=sync
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
@@ -108,5 +167,3 @@ IS_DOCKER=false
IS_SANDSTORM=false IS_SANDSTORM=false
IS_HEROKU=false IS_HEROKU=false
BUNQ_USE_SANDBOX=true 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 # 4.7.7
- [Issue 954](https://github.com/firefly-iii/firefly-iii/issues/954) Some additional view chart ranges - [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)) - [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/BunqJobConfiguration.php
opt/app/app/Import/JobConfiguration/FakeJobConfiguration.php opt/app/app/Import/JobConfiguration/FakeJobConfiguration.php
opt/app/app/Import/JobConfiguration/FileJobConfiguration.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/JobConfigurationInterface.php
opt/app/app/Import/JobConfiguration/SpectreJobConfiguration.php opt/app/app/Import/JobConfiguration/SpectreJobConfiguration.php
opt/app/app/Import/JobConfiguration/YnabJobConfiguration.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/BunqRoutine.php
opt/app/app/Import/Routine/FakeRoutine.php opt/app/app/Import/Routine/FakeRoutine.php
opt/app/app/Import/Routine/FileRoutine.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/RoutineInterface.php
opt/app/app/Import/Routine/SpectreRoutine.php opt/app/app/Import/Routine/SpectreRoutine.php
opt/app/app/Import/Routine/YnabRoutine.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/Navigation.php
opt/app/app/Support/Facades/Preferences.php opt/app/app/Support/Facades/Preferences.php
opt/app/app/Support/Facades/Steam.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/FireflyConfig.php
opt/app/app/Support/Http/Controllers/AugumentData.php opt/app/app/Support/Http/Controllers/AugumentData.php
opt/app/app/Support/Http/Controllers/BasicDataSupport.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/ConfigureUploadHandler.php
opt/app/app/Support/Import/JobConfiguration/File/FileConfigurationInterface.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/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/AuthenticatedHandler.php
opt/app/app/Support/Import/JobConfiguration/Spectre/ChooseAccountsHandler.php opt/app/app/Support/Import/JobConfiguration/Spectre/ChooseAccountsHandler.php
opt/app/app/Support/Import/JobConfiguration/Spectre/ChooseLoginHandler.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/MappingConverger.php
opt/app/app/Support/Import/Routine/File/OFXProcessor.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/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/StageAuthenticatedHandler.php
opt/app/app/Support/Import/Routine/Spectre/StageImportDataHandler.php opt/app/app/Support/Import/Routine/Spectre/StageImportDataHandler.php
opt/app/app/Support/Import/Routine/Spectre/StageNewHandler.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/csv.png
opt/app/public/images/logos/fake.png opt/app/public/images/logos/fake.png
opt/app/public/images/logos/file.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/plaid.png
opt/app/public/images/logos/quovo.png opt/app/public/images/logos/quovo.png
opt/app/public/images/logos/spectre.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/map.twig
opt/app/resources/views/import/file/new.twig opt/app/resources/views/import/file/new.twig
opt/app/resources/views/import/file/roles.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/index.twig
opt/app/resources/views/import/spectre/accounts.twig opt/app/resources/views/import/spectre/accounts.twig
opt/app/resources/views/import/spectre/choose-login.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/MessageProvider.php
opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Support/Renderable.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/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/Loader.php
opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Translation/Translator.php opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Translation/Translator.php
opt/app/vendor/laravel/framework/src/Illuminate/Contracts/Validation/Factory.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/RelationNotFoundException.php
opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/BelongsTo.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/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/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/Concerns/SupportsDefaultModels.php
opt/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/HasMany.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/PipelineServiceProvider.php
opt/app/vendor/laravel/framework/src/Illuminate/Pipeline/composer.json 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/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/CallQueuedHandler.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/Capsule/Manager.php opt/app/vendor/laravel/framework/src/Illuminate/Queue/Capsule/Manager.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/Connectors/BeanstalkdConnector.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/QueueServiceProvider.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/README.md 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/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/SerializesAndRestoresModelIdentifiers.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/SerializesModels.php opt/app/vendor/laravel/framework/src/Illuminate/Queue/SerializesModels.php
opt/app/vendor/laravel/framework/src/Illuminate/Queue/SqsQueue.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/PsrLogCompatTest.php
opt/app/vendor/monolog/monolog/tests/Monolog/RegistryTest.php opt/app/vendor/monolog/monolog/tests/Monolog/RegistryTest.php
opt/app/vendor/monolog/monolog/tests/Monolog/TestCase.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/LICENSE
opt/app/vendor/nesbot/carbon/composer.json opt/app/vendor/nesbot/carbon/composer.json
opt/app/vendor/nesbot/carbon/readme.md 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/Laravel/ServiceProvider.php
opt/app/vendor/nesbot/carbon/src/Carbon/Translator.php opt/app/vendor/nesbot/carbon/src/Carbon/Translator.php
opt/app/vendor/nesbot/carbon/src/JsonSerializable.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/LICENSE.txt
opt/app/vendor/paragonie/constant_time_encoding/README.md opt/app/vendor/paragonie/constant_time_encoding/README.md
opt/app/vendor/paragonie/constant_time_encoding/composer.json 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/ExtendedFinalMethod.php
opt/app/vendor/symfony/debug/Tests/Fixtures/FinalClass.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/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/InternalClass.php
opt/app/vendor/symfony/debug/Tests/Fixtures/InternalInterface.php opt/app/vendor/symfony/debug/Tests/Fixtures/InternalInterface.php
opt/app/vendor/symfony/debug/Tests/Fixtures/InternalTrait.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/PEARClass.php
opt/app/vendor/symfony/debug/Tests/Fixtures/Throwing.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/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/casemismatch.php
opt/app/vendor/symfony/debug/Tests/Fixtures/notPsr0Bis.php opt/app/vendor/symfony/debug/Tests/Fixtures/notPsr0Bis.php
opt/app/vendor/symfony/debug/Tests/Fixtures/psr4/Psr4CaseMismatch.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/DataTest.php
opt/app/vendor/symfony/var-dumper/Tests/Cloner/VarClonerTest.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/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/HtmlDumperTest.php
opt/app/vendor/symfony/var-dumper/Tests/Dumper/ServerDumperTest.php opt/app/vendor/symfony/var-dumper/Tests/Dumper/ServerDumperTest.php
opt/app/vendor/symfony/var-dumper/Tests/Fixtures/FooInterface.php opt/app/vendor/symfony/var-dumper/Tests/Fixtures/FooInterface.php

View File

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

View File

@@ -8,9 +8,9 @@ ENV CORES ${CORES:-1}
ENV FIREFLY_PATH /var/www/firefly-iii/ ENV FIREFLY_PATH /var/www/firefly-iii/
ENV CURL_VERSION 7.60.0 ENV CURL_VERSION 7.60.0
ENV OPENSSL_VERSION 1.1.1-pre6 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 # install packages
RUN apt-get update -y && \ RUN apt-get update -y && \
@@ -20,6 +20,7 @@ RUN apt-get update -y && \
wget \ wget \
libpng-dev \ libpng-dev \
libicu-dev \ libicu-dev \
libldap2-dev \
libedit-dev \ libedit-dev \
libtidy-dev \ libtidy-dev \
libxml2-dev \ libxml2-dev \
@@ -35,6 +36,8 @@ RUN apt-get update -y && \
locales && \ locales && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* 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 # Install latest curl
RUN cd /tmp && \ 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 cron job supervisor conf file.
COPY ./.deploy/docker/cronjob.conf /etc/supervisor/conf.d/cronjob.conf 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 # test crons added via crontab
RUN echo "0 3 * * * /usr/local/bin/php /var/www/firefly-iii/artisan firefly:cron" | 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 - #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 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 # Run composer
ENV COMPOSER_ALLOW_SUPERUSER 1
RUN composer install --prefer-dist --no-dev --no-scripts --no-suggest RUN composer install --prefer-dist --no-dev --no-scripts --no-suggest
# Expose port 80 # Expose port 80

View File

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

View File

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

View File

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

View File

@@ -75,8 +75,10 @@ class RuleRequest extends Request
$validTriggers = array_keys(config('firefly.rule-triggers')); $validTriggers = array_keys(config('firefly.rule-triggers'));
$validActions = array_keys(config('firefly.rule-actions')); $validActions = array_keys(config('firefly.rule-actions'));
// some actions require text: // some triggers and actions require text:
$contextActions = implode(',', config('firefly.rule-actions-text')); $contextTriggers = implode(',', config('firefly.context-rule-triggers'));
$contextActions = implode(',', config('firefly.context-rule-actions'));
$rules = [ $rules = [
'title' => 'required|between:1,100|uniqueObjectForUser:rules,title', 'title' => 'required|between:1,100|uniqueObjectForUser:rules,title',
@@ -86,7 +88,7 @@ class RuleRequest extends Request
'trigger' => 'required|in:store-journal,update-journal', 'trigger' => 'required|in:store-journal,update-journal',
'rule_triggers.*.name' => 'required|in:' . implode(',', $validTriggers), 'rule_triggers.*.name' => 'required|in:' . implode(',', $validTriggers),
'rule_triggers.*.stop_processing' => 'boolean', '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.*.name' => 'required|in:' . implode(',', $validActions),
'rule_actions.*.value' => 'required_if:rule_actions.*.type,' . $contextActions . '|ruleActionValue', 'rule_actions.*.value' => 'required_if:rule_actions.*.type,' . $contextActions . '|ruleActionValue',
'rule_actions.*.stop_processing' => 'boolean', '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\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\Repositories\User\UserRepositoryInterface;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Storage; use Illuminate\Support\Facades\Storage;
/** /**
* Class CreateExport. * Class CreateExport.
@@ -136,10 +136,14 @@ class CreateExport extends Command
$processor->createZipFile(); $processor->createZipFile();
$disk = Storage::disk('export'); $disk = Storage::disk('export');
$fileName = sprintf('export-%s.zip', date('Y-m-d_H-i-s')); $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:'); // "move" from local to export disk
$this->line(storage_path(sprintf('export/%s', $fileName))); $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; return 0;
} }

View File

@@ -39,7 +39,7 @@ class EncryptFile extends Command
* *
* @var string * @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. * The name and signature of the console command.

View File

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

View File

@@ -23,17 +23,37 @@ declare(strict_types=1);
namespace FireflyIII\Console\Commands; namespace FireflyIII\Console\Commands;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\User;
use Log; use Log;
/** /**
* Trait VerifiesAccessToken. * Trait VerifiesAccessToken.
* *
* Verifies user access token for sensitive commands. * Verifies user access token for sensitive commands.
*
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
trait VerifiesAccessToken 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". * 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 FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Log; use Log;
use Storage; use Illuminate\Support\Facades\Storage;
use ZipArchive; 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 * @return bool
* *
@@ -195,9 +195,9 @@ class ExpandedProcessor implements ProcessorInterface
{ {
$zip = new ZipArchive; $zip = new ZipArchive;
$file = $this->job->key . '.zip'; $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.'); throw new FireflyException('Cannot store zip file.');
} }
// for each file in the collection, add it to the 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 FireflyIII\Export\Entry\Entry;
use League\Csv\Writer; use League\Csv\Writer;
use Storage; use Illuminate\Support\Facades\Storage;
/** /**
* Class CsvExporter. * Class CsvExporter.
@@ -57,15 +57,11 @@ class CsvExporter extends BasicExporter implements ExporterInterface
*/ */
public function run(): bool public function run(): bool
{ {
// create temporary file: // choose file name:
$this->tempFile(); $this->fileName = $this->job->key . '-records.csv';
// necessary for CSV writer:
$fullPath = storage_path('export') . DIRECTORY_SEPARATOR . $this->fileName;
//we create the CSV into memory //we create the CSV into memory
$writer = Writer::createFromPath($fullPath); $writer = Writer::createFromString('');
$rows = []; $rows = [];
// get field names for header row: // get field names for header row:
@@ -86,18 +82,9 @@ class CsvExporter extends BasicExporter implements ExporterInterface
$rows[] = $line; $rows[] = $line;
} }
$writer->insertAll($rows); $writer->insertAll($rows);
$disk = Storage::disk('export');
$disk->put($this->fileName, $writer->getContent());
return true; 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)); Log::debug(sprintf('Expect source destination to be of type "%s"', $destinationType));
// find source and destination account: // find source and destination account:
$sourceAccount = $this->findAccount($sourceType, $data['source_id'], $data['source_name']); $sourceAccount = $this->findAccount($sourceType, (int)$data['source_id'], $data['source_name']);
$destinationAccount = $this->findAccount($destinationType, $data['destination_id'], $data['destination_name']); $destinationAccount = $this->findAccount($destinationType, (int)$data['destination_id'], $data['destination_name']);
if (null === $sourceAccount || null === $destinationAccount) { if (null === $sourceAccount || null === $destinationAccount) {
$debugData = $data; $debugData = $data;

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\MessageBag; use Illuminate\Support\MessageBag;
use Log; use Log;
use Storage; use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\File\UploadedFile; 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 * @param Attachment $attachment
* *
@@ -102,8 +102,7 @@ class AttachmentHelper implements AttachmentHelperInterface
*/ */
public function getAttachmentLocation(Attachment $attachment): string 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; return $path;
} }
@@ -192,7 +191,7 @@ class AttachmentHelper implements AttachmentHelperInterface
public function saveAttachmentsForModel(object $model, ?array $files): bool public function saveAttachmentsForModel(object $model, ?array $files): bool
{ {
if(!($model instanceof Model)) { if(!($model instanceof Model)) {
return false; return false; // @codeCoverageIgnore
} }
Log::debug(sprintf('Now in saveAttachmentsForModel for model %s', \get_class($model))); Log::debug(sprintf('Now in saveAttachmentsForModel for model %s', \get_class($model)));
if (\is_array($files)) { if (\is_array($files)) {
@@ -270,7 +269,7 @@ class AttachmentHelper implements AttachmentHelperInterface
$fileObject->rewind(); $fileObject->rewind();
if(0 === $file->getSize()) { 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()); $content = $fileObject->fread($file->getSize());

View File

@@ -28,6 +28,7 @@ use Carbon\Carbon;
use DB; use DB;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Filter\CountAttachmentsFilter; use FireflyIII\Helpers\Filter\CountAttachmentsFilter;
use FireflyIII\Helpers\Filter\DoubleTransactionFilter;
use FireflyIII\Helpers\Filter\FilterInterface; use FireflyIII\Helpers\Filter\FilterInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter; use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Helpers\Filter\NegativeAmountFilter; use FireflyIII\Helpers\Filter\NegativeAmountFilter;
@@ -57,22 +58,10 @@ use Log;
*/ */
class TransactionCollector implements TransactionCollectorInterface 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 */ /** @var array */
private $accountIds = []; private $accountIds = [];
/** @var int */ /** @var int */
private $count = 0; private $count = 0;
/** @var array */ /** @var array */
private $fields private $fields
= [ = [
@@ -139,6 +128,16 @@ class TransactionCollector implements TransactionCollectorInterface
/** @var User */ /** @var User */
private $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 * @param string $filter
* *
@@ -253,6 +252,30 @@ class TransactionCollector implements TransactionCollectorInterface
return $this->count; 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 * @return Collection
*/ */
@@ -309,30 +332,6 @@ class TransactionCollector implements TransactionCollectorInterface
return $set; 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 * @return TransactionCollectorInterface
*/ */
@@ -784,14 +783,15 @@ class TransactionCollector implements TransactionCollectorInterface
{ {
// create all possible filters: // create all possible filters:
$filters = [ $filters = [
InternalTransferFilter::class => new InternalTransferFilter($this->accountIds), InternalTransferFilter::class => new InternalTransferFilter($this->accountIds),
OpposingAccountFilter::class => new OpposingAccountFilter($this->accountIds), OpposingAccountFilter::class => new OpposingAccountFilter($this->accountIds),
TransferFilter::class => new TransferFilter, TransferFilter::class => new TransferFilter,
PositiveAmountFilter::class => new PositiveAmountFilter, PositiveAmountFilter::class => new PositiveAmountFilter,
NegativeAmountFilter::class => new NegativeAmountFilter, NegativeAmountFilter::class => new NegativeAmountFilter,
SplitIndicatorFilter::class => new SplitIndicatorFilter, SplitIndicatorFilter::class => new SplitIndicatorFilter,
CountAttachmentsFilter::class => new CountAttachmentsFilter, CountAttachmentsFilter::class => new CountAttachmentsFilter,
TransactionViewFilter::class => new TransactionViewFilter, TransactionViewFilter::class => new TransactionViewFilter,
DoubleTransactionFilter::class => new DoubleTransactionFilter,
]; ];
Log::debug(sprintf('Will run %d filters on the set.', \count($this->filters))); Log::debug(sprintf('Will run %d filters on the set.', \count($this->filters)));
foreach ($this->filters as $enabled) { 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; $key = $journalId . '-' . implode(',', $transactionIds) . '-' . implode(',', $accountIds) . '-' . $amount;
Log::debug(sprintf('Current transaction key is "%s"', $key)); Log::debug(sprintf('Current transaction key is "%s"', $key));
if (!isset($count[$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: // not yet counted? add to new set and count it:
$new->push($transaction); $new->push($transaction);
$count[$key] = 1; $count[$key] = 1;
} }
if (isset($count[$key])) {
Log::debug(sprintf('Second instance of transaction #%d, do NOT add it.', $transaction->id));
}
} }
return $new; 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 * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
* @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
* @throws FireflyException
*/ */
public function reconcile(Account $account, Carbon $start = null, Carbon $end = null) 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\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
use Log;
/** /**
* Class ForgotPasswordController * Class ForgotPasswordController
@@ -58,6 +59,13 @@ class ForgotPasswordController extends Controller
*/ */
public function sendResetLinkEmail(Request $request, UserRepositoryInterface $repository) 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); $this->validateEmail($request);
// verify if the user is not a demo user. If so, we give him back an error. // 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() 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? // is allowed to?
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data; $singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$userCount = User::count(); $userCount = User::count();

View File

@@ -129,8 +129,9 @@ class LoginController extends Controller
*/ */
public function showLoginForm(Request $request) public function showLoginForm(Request $request)
{ {
$count = DB::table('users')->count(); $count = DB::table('users')->count();
if (0 === $count) { $loginProvider = envNonEmpty('LOGIN_PROVIDER','eloquent');
if (0 === $count && 'eloquent' === $loginProvider) {
return redirect(route('register')); // @codeCoverageIgnore 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; $singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$userCount = User::count(); $userCount = User::count();
$allowRegistration = true; $allowRegistration = true;
$allowReset = true;
if (true === $singleUserMode && $userCount > 0) { if (true === $singleUserMode && $userCount > 0) {
$allowRegistration = false; $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'); $email = $request->old('email');
$remember = $request->old('remember'); $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) public function register(Request $request)
{ {
// is allowed to? // is allowed to?
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data; $allowRegistration = true;
$userCount = User::count(); $loginProvider = envNonEmpty('LOGIN_PROVIDER','eloquent');
if (true === $singleUserMode && $userCount > 0) { $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.'; $message = 'Registration is currently not available.';
return view('error', compact('message')); return view('error', compact('message'));
@@ -102,13 +112,25 @@ class RegisterController extends Controller
*/ */
public function showRegistrationForm(Request $request) public function showRegistrationForm(Request $request)
{ {
// is demo site? $allowRegistration = true;
$isDemoSite = FireflyConfig::get('is_demo_site', config('firefly.configuration.is_demo_site'))->data; $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? if (true === $isDemoSite) {
$singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data; $allowRegistration = false;
$userCount = User::count(); }
if (true === $singleUserMode && $userCount > 0) {
if (true === $singleUserMode && $userCount > 0 && 'eloquent' === $loginProvider) {
$allowRegistration = false;
}
if ('eloquent' !== $loginProvider) {
$allowRegistration = false;
}
if (false === $allowRegistration) {
$message = 'Registration is currently not available.'; $message = 'Registration is currently not available.';
return view('error', compact('message')); return view('error', compact('message'));

View File

@@ -28,6 +28,7 @@ use FireflyIII\Http\Controllers\Controller;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
/** /**
* Class ResetPasswordController * Class ResetPasswordController
@@ -70,7 +71,15 @@ class ResetPasswordController extends Controller
*/ */
public function showResetForm(Request $request, $token = null) 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; $singleUserMode = FireflyConfig::get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
$userCount = User::count(); $userCount = User::count();
$allowRegistration = true; $allowRegistration = true;
@@ -83,4 +92,42 @@ class ResetPasswordController extends Controller
['token' => $token, 'email' => $request->email, 'allowRegistration' => $allowRegistration] ['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\Http\Controllers\Controller;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Support\Http\Controllers\DateCalculation; use FireflyIII\Support\Http\Controllers\DateCalculation;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
@@ -63,7 +64,6 @@ class IndexController extends Controller
); );
} }
/** /**
* Show all budgets. * 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', 'firefly.without_budget_between',
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] ['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
); );
$periods = $this->getBudgetPeriodOverview($end); $periods = $this->getNoBudgetPeriodOverview($end);
$page = (int)$request->get('page'); $page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data; $pageSize = (int)app('preferences')->get('listPageSize', 50)->data;

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Json; namespace FireflyIII\Http\Controllers\Json;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
@@ -37,58 +38,16 @@ use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Support\CacheProperties; use FireflyIII\Support\CacheProperties;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; 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. * Class AutoCompleteController.
* *
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/ */
class AutoCompleteController extends Controller 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. * List of all journals.
* *
@@ -106,7 +65,7 @@ class AutoCompleteController extends Controller
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key); $cache->addProperty($key);
if ($cache->has()) { if ($cache->has()) {
return response()->json($cache->get()); return response()->json($cache->get()); // @codeCoverageIgnore
} }
// find everything: // find everything:
$collector->setLimit(250)->setPage(1); $collector->setLimit(250)->setPage(1);
@@ -129,251 +88,79 @@ class AutoCompleteController extends Controller
} }
/** /**
* List of revenue accounts. * @param Request $request
* * @param string $subject
* @param Request $request
* @param AccountRepositoryInterface $repository
* *
* @throws FireflyException
* @return JsonResponse * @return JsonResponse
*/ */
public function assetAccounts(Request $request, AccountRepositoryInterface $repository): JsonResponse public function autoComplete(Request $request, string $subject): JsonResponse
{ {
$search = (string)$request->get('search'); $search = (string)$request->get('search');
$cache = new CacheProperties; $unfiltered = null;
$cache->addProperty('ac-asset-accounts'); $filtered = null;
// 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;
}
return false; // @codeCoverageIgnore // search for all accounts.
} if ('all-accounts' === $subject) {
); $unfiltered = $this->getAccounts(
$return = array_values(array_unique($filtered->pluck('name')->toArray())); [AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET, AccountType::LOAN,
AccountType::DEBT, AccountType::MORTGAGE]
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); // search for expense accounts.
} if ('expense-accounts' === $subject) {
$unfiltered = $this->getAccounts([AccountType::EXPENSE, AccountType::BENEFICIARY]);
/**
* 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());
} }
// find everything:
$return = array_unique($repository->getActiveBills()->pluck('name')->toArray());
if ('' !== $search) { // search for revenue accounts.
$return = array_values( if ('revenue-accounts' === $subject) {
array_unique( $unfiltered = $this->getAccounts([AccountType::REVENUE]);
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
)
);
} }
$cache->store($return);
return response()->json($return); // search for asset accounts.
} if ('asset-accounts' === $subject) {
$unfiltered = $this->getAccounts([AccountType::ASSET, AccountType::DEFAULT]);
/**
* 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());
} }
// find everything:
$return = array_unique($repository->getBudgets()->pluck('name')->toArray());
if ('' !== $search) { // search for categories.
$return = array_values( if ('categories' === $subject) {
array_unique( $unfiltered = $this->getCategories();
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
)
);
} }
$cache->store($return);
return response()->json($return); // search for budgets.
} if ('budgets' === $subject) {
$unfiltered = $this->getBudgets();
/**
* 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());
} }
// find everything:
$return = array_unique($repository->getCategories()->pluck('name')->toArray()); // search for tags
if ('' !== $search) { if ('tags' === $subject) {
$return = array_values( $unfiltered = $this->getTags();
array_filter(
$return, function (string $value) use ($search) {
return !(false === stripos($value, $search));
}, ARRAY_FILTER_USE_BOTH
)
);
} }
$cache->store($return);
return response()->json($return); // search for bills
} if ('bills' === $subject) {
$unfiltered = $this->getBills();
/**
* 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());
} }
// find everything: // search for currency names.
$return = $repository->get()->pluck('name')->toArray(); if ('currency-names' === $subject) {
sort($return); $unfiltered = $this->getCurrencyNames();
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); if ('transaction_types' === $subject) {
$unfiltered = $this->getTransactionTypes();
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());
} }
// find everything: if ('transaction-types' === $subject) {
$set = $repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]); $unfiltered = $this->getTransactionTypes();
$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
)
)
);
} }
$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; $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key); $cache->addProperty($key);
if ($cache->has()) { if ($cache->has()) {
return response()->json($cache->get()); return response()->json($cache->get()); // @codeCoverageIgnore
} }
// find everything: // find everything:
$collector->setLimit(400)->setPage(1); $collector->setLimit(400)->setPage(1);
@@ -413,105 +200,14 @@ class AutoCompleteController extends Controller
sort($return); sort($return);
if ('' !== $search) { if ('' !== $search) {
$return = array_values( $return = array_filter(
array_unique( $return, function (array $array) use ($search) {
array_filter( $haystack = $array['name'];
$return, function (array $array) use ($search) { $result = stripos($haystack, $search);
$value = $array['name']; return !(false === $result);
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_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); $cache->store($return);
@@ -536,7 +232,7 @@ class AutoCompleteController extends Controller
$key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search; $key = '' === $search ? 'skjf0893j89fj2398hd89dh289h2398hr7isd8900828u209ujnxs88929282u' : $search;
$cache->addProperty($key); $cache->addProperty($key);
if ($cache->has()) { if ($cache->has()) {
return response()->json($cache->get()); return response()->json($cache->get()); // @codeCoverageIgnore
} }
// find everything: // find everything:
$type = config('firefly.transactionTypesByWhat.' . $what); $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 * @return array|null
* @param JournalRepositoryInterface $repository
*
* @return JsonResponse
*/ */
public function transactionTypes(Request $request, JournalRepositoryInterface $repository): JsonResponse private function filterResult(?array $unfiltered, string $query): ?array
{ {
$search = (string)$request->get('search'); if (null === $unfiltered) {
$cache = new CacheProperties; return null; // @codeCoverageIgnore
$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: if ('' === $query) {
$return = array_unique($repository->getTransactionTypes()->pluck('type')->toArray()); sort($unfiltered);
sort($return);
if ('' !== $search) { return $unfiltered;
}
$return = [];
if ('' !== $query) {
$return = array_values( $return = array_values(
array_unique( array_filter(
array_filter( $unfiltered, function (string $value) use ($query) {
$return, function (string $value) use ($search) { return !(false === stripos($value, $query));
return !(false === stripos($value, $search)); }, ARRAY_FILTER_USE_BOTH
}, 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); $cleared = $this->repository->getTransactionsById($clearedIds);
$countCleared = 0; $countCleared = 0;
Log::debug('Start transaction loop');
/** @var Transaction $transaction */ /** @var Transaction $transaction */
foreach ($transactions as $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 */ /** @var Transaction $transaction */
foreach ($cleared as $transaction) { foreach ($cleared as $transaction) {
if ($transaction->transactionJournal->date <= $end) { if ($transaction->date <= $end) {
$clearedAmount = bcadd($clearedAmount, $transaction->amount); // @codeCoverageIgnore $clearedAmount = bcadd($clearedAmount, $transaction->transaction_amount); // @codeCoverageIgnore
++$countCleared; ++$countCleared;
} }
} }
@@ -201,6 +235,7 @@ class ReconcileController extends Controller
Log::debug(sprintf('Could not render: %s', $e->getMessage())); Log::debug(sprintf('Could not render: %s', $e->getMessage()));
$html = 'Could not render accounts.reconcile.transactions'; $html = 'Could not render accounts.reconcile.transactions';
} }
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
return response()->json(['html' => $html, 'startBalance' => $startBalance, 'endBalance' => $endBalance]); return response()->json(['html' => $html, 'startBalance' => $startBalance, 'endBalance' => $endBalance]);

View File

@@ -41,6 +41,7 @@ use FireflyIII\User;
use Google2FA; use Google2FA;
use Hash; use Hash;
use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\Request;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Laravel\Passport\ClientRepository; use Laravel\Passport\ClientRepository;
use Log; use Log;
@@ -71,6 +72,7 @@ class ProfileController extends Controller
return $next($request); return $next($request);
} }
); );
$this->middleware(IsDemoUser::class)->except(['index']); $this->middleware(IsDemoUser::class)->except(['index']);
$this->middleware(IsSandStormUser::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 * @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; $title = auth()->user()->email;
$email = auth()->user()->email; $email = auth()->user()->email;
$subTitle = (string)trans('firefly.change_your_email'); $subTitle = (string)trans('firefly.change_your_email');
@@ -95,8 +104,15 @@ class ProfileController extends Controller
* *
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View * @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; $title = auth()->user()->email;
$subTitle = (string)trans('firefly.change_your_password'); $subTitle = (string)trans('firefly.change_your_password');
$subTitleIcon = 'fa-key'; $subTitleIcon = 'fa-key';
@@ -132,6 +148,10 @@ class ProfileController extends Controller
*/ */
public function confirmEmailChange(UserRepositoryInterface $repository, string $token) 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. // find preference with this token value.
/** @var Collection $set */ /** @var Collection $set */
$set = app('preferences')->findByName('email_change_confirm_token'); $set = app('preferences')->findByName('email_change_confirm_token');
@@ -163,8 +183,12 @@ class ProfileController extends Controller
* *
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View * @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; $title = auth()->user()->email;
$subTitle = (string)trans('firefly.delete_account'); $subTitle = (string)trans('firefly.delete_account');
$subTitleIcon = 'fa-trash'; $subTitleIcon = 'fa-trash';
@@ -216,6 +240,7 @@ class ProfileController extends Controller
*/ */
public function index() public function index()
{ {
$loginProvider = config('firefly.login_provider');
// check if client token thing exists (default one) // check if client token thing exists (default one)
$count = DB::table('oauth_clients') $count = DB::table('oauth_clients')
->where('personal_access_client', 1) ->where('personal_access_client', 1)
@@ -241,7 +266,7 @@ class ProfileController extends Controller
$accessToken = app('preferences')->set('access_token', $token); $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) 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 */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
$newEmail = $request->string('email'); $newEmail = $request->string('email');
@@ -299,6 +331,13 @@ class ProfileController extends Controller
*/ */
public function postChangePassword(ProfileFormRequest $request, UserRepositoryInterface $repository) 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. // the request has already validated both new passwords must be equal.
$current = $request->get('current_password'); $current = $request->get('current_password');
$new = $request->get('new_password'); $new = $request->get('new_password');
@@ -396,6 +435,11 @@ class ProfileController extends Controller
*/ */
public function undoEmailChange(UserRepositoryInterface $repository, string $token, string $hash) 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. // find preference with this token value.
$set = app('preferences')->findByName('email_change_undo_token'); $set = app('preferences')->findByName('email_change_undo_token');
$user = null; $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), 'firefly.journals_in_period_for_tag', ['tag' => $tag->tag, 'start' => $start->formatLocalized($this->monthAndDayFormat),
'end' => $end->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')]); $path = route('tags.show', [$tag->id, $start->format('Y-m-d'), $end->format('Y-m-d')]);
/** @var TransactionCollectorInterface $collector */ /** @var TransactionCollectorInterface $collector */

View File

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

View File

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

View File

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

View File

@@ -168,8 +168,10 @@ class SplitJournalFormRequest extends Request
/** @var array $array */ /** @var array $array */
foreach ($transactions as $array) { foreach ($transactions as $array) {
if (null !== $array['destination_id'] && null !== $array['source_id'] && $array['destination_id'] === $array['source_id']) { 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_source_id', (string)trans('validation.source_equals_destination'));
$validator->errors()->add('journal_destination_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(); $handler->run();
$transactions = $handler->getTransactions(); $transactions = $handler->getTransactions();
// could be that more transactions will arrive in a second run. // 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.'); Log::debug('Handler indicates that it is still working.');
$this->repository->setStatus($this->importJob, 'ready_to_run'); $this->repository->setStatus($this->importJob, 'ready_to_run');
$this->repository->setStage($this->importJob, 'go-for-import'); $this->repository->setStage($this->importJob, 'go-for-import');
} }
$this->repository->appendTransactions($this->importJob, $transactions); $this->repository->appendTransactions($this->importJob, $transactions);
if (false === $handler->stillRunning) { if (false === $handler->isStillRunning()) {
Log::info('Handler indicates that its done!'); Log::info('Handler indicates that its done!');
$this->repository->setStatus($this->importJob, 'provider_finished'); $this->repository->setStatus($this->importJob, 'provider_finished');
$this->repository->setStage($this->importJob, 'final'); $this->repository->setStage($this->importJob, 'final');
@@ -98,6 +98,8 @@ class BunqRoutine implements RoutineInterface
} }
/** /**
* Set the import job. * 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; use Log;
/** /**
* Creates new transactions based upon arrays. Will first check the array for duplicates. * Creates new transactions based on arrays.
* *
* Class ImportArrayStorage * Class ImportArrayStorage
* *
@@ -58,11 +58,11 @@ class ImportArrayStorage
private $checkForTransfers = false; private $checkForTransfers = false;
/** @var ImportJob The import job */ /** @var ImportJob The import job */
private $importJob; private $importJob;
/** @var JournalRepositoryInterface */ /** @var JournalRepositoryInterface Journal repository for storage. */
private $journalRepos; private $journalRepos;
/** @var ImportJobRepositoryInterface Import job repository */ /** @var ImportJobRepositoryInterface Import job repository */
private $repository; private $repository;
/** @var Collection The transfers. */ /** @var Collection The transfers the user already has. */
private $transfers; private $transfers;
/** /**
@@ -72,12 +72,12 @@ class ImportArrayStorage
*/ */
public function setImportJob(ImportJob $importJob): void public function setImportJob(ImportJob $importJob): void
{ {
$this->importJob = $importJob; $this->importJob = $importJob;
$this->countTransfers();
$this->repository = app(ImportJobRepositoryInterface::class); $this->repository = app(ImportJobRepositoryInterface::class);
$this->repository->setUser($importJob->user); $this->repository->setUser($importJob->user);
$this->countTransfers();
$this->journalRepos = app(JournalRepositoryInterface::class); $this->journalRepos = app(JournalRepositoryInterface::class);
$this->journalRepos->setUser($importJob->user); $this->journalRepos->setUser($importJob->user);
@@ -116,7 +116,7 @@ class ImportArrayStorage
app('preferences')->mark(); app('preferences')->mark();
// email about this: // email about this:
event(new RequestedReportOnJournals($this->importJob->user_id, $collection)); event(new RequestedReportOnJournals((int)$this->importJob->user_id, $collection));
return $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. * 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 private function countTransfers(): void
{ {
Log::debug('Now in count transfers.'); Log::debug('Now in countTransfers()');
/** @var array $array */ /** @var array $array */
$array = $this->importJob->transactions; $array = $this->repository->getTransactions($this->importJob);
$count = 0; $count = 0;
foreach ($array as $index => $transaction) { foreach ($array as $index => $transaction) {
if (strtolower(TransactionType::TRANSFER) === strtolower($transaction['type'])) { 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)); Log::debug(sprintf('Row #%d is a transfer, increase count to %d', $index + 1, $count));
} }
} }
if (0 === $count) { Log::debug(sprintf('Count of transfers in import array is %d.', $count));
Log::debug('Count is zero, will not check for duplicate transfers.');
}
if ($count > 0) { if ($count > 0) {
Log::debug(sprintf('Count is %d, will check for duplicate transfers.', $count));
$this->checkForTransfers = true; $this->checkForTransfers = true;
Log::debug('Will check for duplicate transfers.');
// get users transfers. Needed for comparison. // get users transfers. Needed for comparison.
$this->getTransfers(); $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']); unset($transaction['importHashV2'], $transaction['original-source']);
$json = json_encode($transaction); $json = json_encode($transaction);
if (false === $json) { if (false === $json) {
// @codeCoverageIgnoreStart
/** @noinspection ForgottenDebugOutputInspection */ /** @noinspection ForgottenDebugOutputInspection */
Log::error('Could not encode import array.', print_r($transaction, true)); 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); $hash = hash('sha256', $json, false);
Log::debug(sprintf('The hash is: %s', $hash)); Log::debug(sprintf('The hash is: %s', $hash));
@@ -391,38 +420,21 @@ class ImportArrayStorage
private function storeArray(): Collection private function storeArray(): Collection
{ {
/** @var array $array */ /** @var array $array */
$array = $this->importJob->transactions; $array = $this->repository->getTransactions($this->importJob);
$count = \count($array); $count = \count($array);
$toStore = []; $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) { foreach ($array as $index => $transaction) {
Log::debug(sprintf('Now at item %d out of %d', $index + 1, $count)); Log::debug(sprintf('Now at item %d out of %d', $index + 1, $count));
$hash = $this->getHash($transaction); if ($this->duplicateDetected($index, $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']
)
);
continue; continue;
} }
if ($this->checkForTransfers && $this->transferExists($transaction)) { $transaction['importHashV2'] = $this->getHash($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;
$toStore[] = $transaction; $toStore[] = $transaction;
} }
$count = \count($toStore); $count = \count($toStore);
@@ -436,31 +448,8 @@ class ImportArrayStorage
// now actually store them: // now actually store them:
$collection = new Collection; $collection = new Collection;
foreach ($toStore as $index => $store) { foreach ($toStore as $index => $store) {
// do duplicate detection again! // do duplicate detection again!
$hash = $this->getHash($store); if ($this->duplicateDetected($index, $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']
)
);
continue; continue;
} }
@@ -554,8 +543,9 @@ class ImportArrayStorage
Log::debug(sprintf('Comparison is a hit! (%s)', $hits)); Log::debug(sprintf('Comparison is a hit! (%s)', $hits));
// compare description: // compare description:
Log::debug(sprintf('Comparing "%s" to "%s"', $description, $transfer->description)); $comparison = '(empty description)' === $transfer->description ? '' : $transfer->description;
if ($description !== $transfer->description) { Log::debug(sprintf('Comparing "%s" to "%s" (original: "%s")', $description, $transfer->description, $comparison));
if ($description !== $comparison) {
continue; // @codeCoverageIgnore continue; // @codeCoverageIgnore
} }
++$hits; ++$hits;

View File

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

View File

@@ -179,10 +179,6 @@ class ExecuteRuleOnExistingTransactions extends Job implements ShouldQueue
++$misses; ++$misses;
} }
Log::info(sprintf('Current progress: %d Transactions. Hits: %d, misses: %d', $total, $hits, $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)); 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-read string $email
* @property bool encrypted * @property bool encrypted
* @property Collection budgetlimits * @property Collection budgetlimits
* @property int $order
*/ */
class Budget extends Model class Budget extends Model
{ {
@@ -61,7 +62,7 @@ class Budget extends Model
'encrypted' => 'boolean', 'encrypted' => 'boolean',
]; ];
/** @var array Fields that can be filled */ /** @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 */ /** @var array Hidden from view */
protected $hidden = ['encrypted']; protected $hidden = ['encrypted'];

View File

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

View File

@@ -71,6 +71,7 @@ use FireflyIII\Validation\FireflyValidator;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Twig; use Twig;
use Twig_Extension_Debug;
use TwigBridge\Extension\Loader\Functions; use TwigBridge\Extension\Loader\Functions;
use Validator; use Validator;
@@ -105,6 +106,7 @@ class FireflyServiceProvider extends ServiceProvider
Twig::addExtension(new Transaction); Twig::addExtension(new Transaction);
Twig::addExtension(new Rule); Twig::addExtension(new Rule);
Twig::addExtension(new AmountFormat); 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\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Log; use Log;
use Storage; use Illuminate\Support\Facades\Storage;
/** /**
* Class AttachmentRepository. * Class AttachmentRepository.
@@ -66,9 +66,9 @@ class AttachmentRepository implements AttachmentRepositoryInterface
/** @var AttachmentHelperInterface $helper */ /** @var AttachmentHelperInterface $helper */
$helper = app(AttachmentHelperInterface::class); $helper = app(AttachmentHelperInterface::class);
$file = $helper->getAttachmentLocation($attachment); $path = $helper->getAttachmentLocation($attachment);
try { try {
unlink($file); Storage::disk('upload')->delete($path);
} catch (Exception $e) { } catch (Exception $e) {
Log::error(sprintf('Could not delete file for attachment %d: %s', $attachment->id, $e->getMessage())); 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 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; return $dates;
} }

View File

@@ -107,6 +107,7 @@ class BudgetRepository implements BudgetRepositoryInterface
} catch (Exception $e) { } catch (Exception $e) {
Log::debug(sprintf('Could not delete budget limit: %s', $e->getMessage())); 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. // 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']); $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 public function getActiveBudgets(): Collection
{ {
/** @var Collection $set */ /** @var Collection $set */
$set = $this->user->budgets()->where('active', 1)->get(); $set = $this->user->budgets()->where('active', 1)
->get();
$set = $set->sortBy( $set = $set->sortBy(
function (Budget $budget) { 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( $set = $set->sortBy(
function (Budget $budget) { 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( $set = $set->sortBy(
function (Budget $budget) { 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; 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 * @param User $user
*/ */
@@ -660,7 +680,6 @@ class BudgetRepository implements BudgetRepositoryInterface
$this->user = $user; $this->user = $user;
} }
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param Collection $budgets * @param Collection $budgets
* @param Collection $accounts * @param Collection $accounts
@@ -825,6 +844,8 @@ class BudgetRepository implements BudgetRepositoryInterface
} }
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param BudgetLimit $budgetLimit * @param BudgetLimit $budgetLimit
* @param array $data * @param array $data
@@ -848,7 +869,6 @@ class BudgetRepository implements BudgetRepositoryInterface
return $budgetLimit; return $budgetLimit;
} }
/** @noinspection MoreThanThreeArgumentsInspection */
/** /**
* @param Budget $budget * @param Budget $budget
* @param Carbon $start * @param Carbon $start

View File

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

View File

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

View File

@@ -84,26 +84,36 @@ class ImportJobRepository implements ImportJobRepositoryInterface
* @param array $transactions * @param array $transactions
* *
* @return ImportJob * @return ImportJob
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/ */
public function appendTransactions(ImportJob $job, array $transactions): ImportJob public function appendTransactions(ImportJob $job, array $transactions): ImportJob
{ {
Log::debug(sprintf('Now in appendTransactions(%s)', $job->key)); Log::debug(sprintf('Now in appendTransactions(%s)', $job->key));
$existingTransactions = $job->transactions; $existingTransactions = $this->getTransactions($job);
if (!\is_array($existingTransactions)) { $new = array_merge($existingTransactions, $transactions);
$existingTransactions = [];
}
$new = array_merge($existingTransactions, $transactions);
Log::debug(sprintf('Old transaction count: %d', \count($existingTransactions))); Log::debug(sprintf('Old transaction count: %d', \count($existingTransactions)));
Log::debug(sprintf('To be added transaction count: %d', \count($transactions))); Log::debug(sprintf('To be added transaction count: %d', \count($transactions)));
Log::debug(sprintf('New count: %d', \count($new))); Log::debug(sprintf('New count: %d', \count($new)));
$job->transactions = $new; $this->setTransactions($job, $new);
$job->save();
return $job; 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 * @param string $importProvider
* *
@@ -201,6 +211,31 @@ class ImportJobRepository implements ImportJobRepositoryInterface
return []; 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 ImportJob $job
* @param array $configuration * @param array $configuration
@@ -272,8 +307,17 @@ class ImportJobRepository implements ImportJobRepositoryInterface
*/ */
public function setTransactions(ImportJob $job, array $transactions): ImportJob 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(); $job->save();
// store file.
$disk->put($filename, $json);
return $job; return $job;
} }
@@ -377,7 +421,7 @@ class ImportJobRepository implements ImportJobRepositoryInterface
$fileObject->rewind(); $fileObject->rewind();
if(0 === $file->getSize()) { if (0 === $file->getSize()) {
throw new FireflyException('Cannot upload empty or non-existent file.'); throw new FireflyException('Cannot upload empty or non-existent file.');
} }
@@ -390,7 +434,6 @@ class ImportJobRepository implements ImportJobRepositoryInterface
return new MessageBag; return new MessageBag;
} }
/** /**
* @codeCoverageIgnore * @codeCoverageIgnore
* *

View File

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

View File

@@ -27,6 +27,9 @@ use Exception;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\TransactionJournalFactory; use FireflyIII\Factory\TransactionJournalFactory;
use FireflyIII\Factory\TransactionJournalMetaFactory; 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\Account;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Models\PiggyBankEvent; use FireflyIII\Models\PiggyBankEvent;
@@ -581,14 +584,23 @@ class JournalRepository implements JournalRepositoryInterface
*/ */
public function getTransactionsById(array $transactionIds): Collection public function getTransactionsById(array $transactionIds): Collection
{ {
$set = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') $journalIds = Transaction::whereIn('id', $transactionIds)->get(['transaction_journal_id'])->pluck('transaction_journal_id')->toArray();
->whereIn('transactions.id', $transactionIds) $journals = new Collection;
->where('transaction_journals.user_id', $this->user->id) foreach($journalIds as $journalId) {
->whereNull('transaction_journals.deleted_at') $result = $this->findNull((int)$journalId);
->whereNull('transactions.deleted_at') if(null !== $result) {
->get(['transactions.*']); $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'); 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 * @param string $tag
* *
@@ -151,6 +168,23 @@ class TagRepository implements TagRepositoryInterface
return $tags; 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 * @param Tag $tag
* *
@@ -315,6 +349,23 @@ class TagRepository implements TagRepositoryInterface
return $return; 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 Tag $tag
* @param array $data * @param array $data

View File

@@ -55,6 +55,15 @@ interface TagRepositoryInterface
*/ */
public function earnedInPeriod(Tag $tag, Carbon $start, Carbon $end): string; 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 * @param string $tag
* *
@@ -83,6 +92,15 @@ interface TagRepositoryInterface
*/ */
public function get(): Collection; 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 * @param Tag $tag
* *
@@ -147,6 +165,15 @@ interface TagRepositoryInterface
*/ */
public function tagCloud(?int $year): array; 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. * 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 DB;
use Exception; use Exception;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\RecurrenceTransaction; use FireflyIII\Models\RecurrenceTransaction;
use FireflyIII\Models\Transaction; use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
@@ -97,6 +98,9 @@ class AccountDestroyService
} }
} }
// delete piggy banks:
PiggyBank::where('account_id', $account->id)->delete();
try { try {
$account->delete(); $account->delete();
} catch (Exception $e) { // @codeCoverageIgnore } catch (Exception $e) { // @codeCoverageIgnore

View File

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

View File

@@ -217,13 +217,13 @@ trait AccountServiceTrait
*/ */
public function storeOpposingAccount(User $user, string $name): Account 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.'); Log::debug('Going to create an opening balance opposing account.');
/** @var AccountFactory $factory */ /** @var AccountFactory $factory */
$factory = app(AccountFactory::class); $factory = app(AccountFactory::class);
$factory->setUser($user); $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) { foreach ($users as $user) {
$accessToken = app('preferences')->getForUser($user, 'access_token', null); $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)); Log::info(sprintf('Recognized user #%d (%s) from his acccess token.', $user->id, $user->email));
return $value; 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 * and for each period, the amount of money spent and earned. This is a complex operation which is cached for
* performance reasons. * performance reasons.
* *
* @param Account $account the account involved * The method has been refactored recently for better performance.
* @param Carbon $date *
* @param Account $account The account involved
* @param Carbon $date The start date.
* *
* @return Collection * @return Collection
*/ */
protected function getAccountPeriodOverview(Account $account, Carbon $date): Collection // period overview protected function getAccountPeriodOverview(Account $account, Carbon $date): Collection
{ {
/** @var AccountRepositoryInterface $repository */ /** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class); $repository = app(AccountRepositoryInterface::class);
@@ -95,15 +97,15 @@ trait PeriodOverview
$collector = app(TransactionCollectorInterface::class); $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setRange($currentDate['start'], $currentDate['end'])->setTypes([TransactionType::DEPOSIT]) $collector->setAccounts(new Collection([$account]))->setRange($currentDate['start'], $currentDate['end'])->setTypes([TransactionType::DEPOSIT])
->withOpposingAccount(); ->withOpposingAccount();
$set = $collector->getTransactions(); $earnedSet = $collector->getTransactions();
$earned = $this->groupByCurrency($set); $earned = $this->groupByCurrency($earnedSet);
/** @var TransactionCollectorInterface $collector */ /** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setRange($currentDate['start'], $currentDate['end'])->setTypes([TransactionType::WITHDRAWAL]) $collector->setAccounts(new Collection([$account]))->setRange($currentDate['start'], $currentDate['end'])->setTypes([TransactionType::WITHDRAWAL])
->withOpposingAccount(); ->withOpposingAccount();
$set = $collector->getTransactions(); $spentSet = $collector->getTransactions();
$spent = $this->groupByCurrency($set); $spent = $this->groupByCurrency($spentSet);
$title = app('navigation')->periodShow($currentDate['start'], $currentDate['period']); $title = app('navigation')->periodShow($currentDate['start'], $currentDate['period']);
/** @noinspection PhpUndefinedMethodInspection */ /** @noinspection PhpUndefinedMethodInspection */
@@ -115,7 +117,6 @@ trait PeriodOverview
'earned' => $earned, 'earned' => $earned,
'transferred' => '0', 'transferred' => '0',
'route' => route('accounts.show', [$account->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]), '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 * @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 */ /** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class); $repository = app(JournalRepositoryInterface::class);
@@ -138,8 +212,12 @@ trait PeriodOverview
$end = null === $first ? new Carbon : $first->date; $end = null === $first ? new Carbon : $first->date;
$start = clone $date; $start = clone $date;
$range = app('preferences')->get('viewRange', '1M')->data; $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($start);
$cache->addProperty($end); $cache->addProperty($end);
$cache->addProperty('no-budget-period-entries'); $cache->addProperty('no-budget-period-entries');
@@ -149,7 +227,8 @@ trait PeriodOverview
} }
/** @var array $dates */ /** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $end, $range); $dates = app('navigation')->blockPeriods($start, $end, $range);
$entries = new Collection;
foreach ($dates as $currentDate) { foreach ($dates as $currentDate) {
/** @var TransactionCollectorInterface $collector */ /** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(TransactionCollectorInterface::class);
@@ -162,12 +241,12 @@ trait PeriodOverview
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']); $title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
$entries->push( $entries->push(
[ [
'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
'transactions' => $count, 'transactions' => $count,
'title' => $title, 'title' => $title,
'spent' => $spent, 'spent' => $spent,
'earned' => '0', 'earned' => '0',
'transferred' => '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. * This shows a period overview for a tag. It goes back in time and lists all relevant transactions and sums.
*
* 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.
* *
* @param Tag $tag * @param Tag $tag
* *
* @return Collection * @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 */ /** @var TagRepositoryInterface $repository */
$repository = app(TagRepositoryInterface::class); $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 */ /** @var Carbon $end */
$end = app('navigation')->endOfX($repository->lastUseDate($tag) ?? new Carbon, $range, null); $start = clone $date;
$start = $repository->firstUseDate($tag) ?? new Carbon; $end = $repository->firstUseDate($tag) ?? new Carbon;
if ($end < $start) {
[$start, $end] = [$end, $start]; // @codeCoverageIgnore
}
// properties for entries with their amounts. // properties for entries with their amounts.
$cache = new CacheProperties; $cache = new CacheProperties;
$cache->addProperty($start); $cache->addProperty($start);
$cache->addProperty($end); $cache->addProperty($end);
$cache->addProperty('tag.entries'); $cache->addProperty('tag-period-entries');
$cache->addProperty($tag->id); $cache->addProperty($tag->id);
if ($cache->has()) { if ($cache->has()) {
return $cache->get(); // @codeCoverageIgnore return $cache->get(); // @codeCoverageIgnore
} }
$collection = new Collection; /** @var array $dates */
$currentEnd = clone $end; $dates = app('navigation')->blockPeriods($start, $end, $range);
$entries = new Collection;
// while end larger or equal to start // while end larger or equal to start
while ($currentEnd >= $start) { foreach ($dates as $currentDate) {
$currentStart = app('navigation')->startOfPeriod($currentEnd, $range);
// get expenses and what-not in this period and this tag. $spentSet = $repository->expenseInPeriod($tag, $currentDate['start'], $currentDate['end']);
$arr = [ $spent = $this->groupByCurrency($spentSet);
'string' => $end->format('Y-m-d'), $earnedSet = $repository->incomeInPeriod($tag, $currentDate['start'], $currentDate['end']);
'name' => app('navigation')->periodShow($currentEnd, $range), $earned = $this->groupByCurrency($earnedSet);
'start' => clone $currentStart, $transferredSet = $repository->transferredInPeriod($tag, $currentDate['start'], $currentDate['end']);
'end' => clone $currentEnd, $transferred = $this->groupByCurrency($transferredSet);
'date' => clone $end, $title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
'spent' => $repository->spentInPeriod($tag, $currentStart, $currentEnd),
'earned' => $repository->earnedInPeriod($tag, $currentStart, $currentEnd), $entries->push(
]; [
$collection->push($arr); '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. * This list shows the overview of a type of transaction, for the period blocks on the list of transactions.
*
* TODO refactor me.
* *
* @param string $what * @param string $what
* @param Carbon $date * @param Carbon $date
@@ -315,42 +331,60 @@ trait PeriodOverview
/** @var JournalRepositoryInterface $repository */ /** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class); $repository = app(JournalRepositoryInterface::class);
$range = app('preferences')->get('viewRange', '1M')->data; $range = app('preferences')->get('viewRange', '1M')->data;
$first = $repository->firstNull(); $endJournal = $repository->firstNull();
$start = Carbon::now()->subYear(); $end = null === $endJournal ? new Carbon : $endJournal->date;
$start = clone $date;
$types = config('firefly.transactionTypesByWhat.' . $what); $types = config('firefly.transactionTypesByWhat.' . $what);
$entries = new Collection;
if (null !== $first) {
$start = $first->date; if ($end < $start) {
} [$start, $end] = [$end, $start]; // @codeCoverageIgnore
if ($date < $start) {
[$start, $date] = [$date, $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 */ /** @var array $dates */
$dates = app('navigation')->blockPeriods($start, $date, $range); $dates = app('navigation')->blockPeriods($start, $end, $range);
$entries = new Collection;
foreach ($dates as $currentDate) { foreach ($dates as $currentDate) {
// get all expenses, income or transfers:
/** @var TransactionCollectorInterface $collector */ /** @var TransactionCollectorInterface $collector */
$collector = app(TransactionCollectorInterface::class); $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($currentDate['start'], $currentDate['end'])->withOpposingAccount()->setTypes($types); $collector->setAllAssetAccounts()->setRange($currentDate['start'], $currentDate['end'])->withOpposingAccount()->setTypes($types);
$collector->removeFilter(InternalTransferFilter::class); $collector->removeFilter(InternalTransferFilter::class);
$transactions = $collector->getTransactions(); $transactions = $collector->getTransactions();
$title = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
if ($transactions->count() > 0) { $grouped = $this->groupByCurrency($transactions);
$sums = $this->sumPerCurrency($transactions); $spent = [];
$dateName = app('navigation')->periodShow($currentDate['start'], $currentDate['period']); $earned = [];
$sum = $transactions->sum('transaction_amount'); $transferred = [];
/** @noinspection PhpUndefinedMethodInspection */ if ('expenses' === $what || 'withdrawal' === $what) {
$entries->push( $spent = $grouped;
[
'name' => $dateName,
'sums' => $sums,
'sum' => $sum,
'start' => $currentDate['start']->format('Y-m-d'),
'end' => $currentDate['end']->format('Y-m-d'),
]
);
} }
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; return $entries;

View File

@@ -28,6 +28,7 @@ use FireflyIII\Models\AccountType;
use FireflyIII\Models\Transaction; use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Support\ViewErrorBag;
use Log; use Log;
use URL; use URL;
@@ -141,6 +142,10 @@ trait UserNavigation
*/ */
protected function rememberPreviousUri(string $identifier): void 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 $this->transactions;
} }
/**
* @return bool
*/
public function isStillRunning(): bool
{
return $this->stillRunning;
}
/** /**
* *
* @throws FireflyException * @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); $name = app('steam')->tryDecrypt($transaction->account_name);
$iban = $transaction->account_iban;
$transactionId = (int)$transaction->account_id; $transactionId = (int)$transaction->account_id;
$type = $transaction->account_type; $type = $transaction->account_type;
@@ -233,6 +234,7 @@ class Transaction extends Twig_Extension
$name = $transaction->opposing_account_name; $name = $transaction->opposing_account_name;
$transactionId = (int)$transaction->opposing_account_id; $transactionId = (int)$transaction->opposing_account_id;
$type = $transaction->opposing_account_type; $type = $transaction->opposing_account_type;
$iban = $transaction->opposing_account_iban;
} }
// Find the opposing account and use that one: // Find the opposing account and use that one:
@@ -264,7 +266,7 @@ class Transaction extends Twig_Extension
return $txt; 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; return $txt;
} }
@@ -385,12 +387,14 @@ class Transaction extends Twig_Extension
$name = app('steam')->tryDecrypt($transaction->account_name); $name = app('steam')->tryDecrypt($transaction->account_name);
$transactionId = (int)$transaction->account_id; $transactionId = (int)$transaction->account_id;
$type = $transaction->account_type; $type = $transaction->account_type;
$iban = $transaction->account_iban;
// name is present in object, use that one: // name is present in object, use that one:
if (null !== $transaction->opposing_account_id && 1 === bccomp($transaction->transaction_amount, '0')) { if (null !== $transaction->opposing_account_id && 1 === bccomp($transaction->transaction_amount, '0')) {
$name = $transaction->opposing_account_name; $name = $transaction->opposing_account_name;
$transactionId = (int)$transaction->opposing_account_id; $transactionId = (int)$transaction->opposing_account_id;
$type = $transaction->opposing_account_type; $type = $transaction->opposing_account_type;
$iban = $transaction->opposing_account_iban;
} }
// Find the opposing account and use that one: // Find the opposing account and use that one:
if (null === $transaction->opposing_account_id && 1 === bccomp($transaction->transaction_amount, '0')) { if (null === $transaction->opposing_account_id && 1 === bccomp($transaction->transaction_amount, '0')) {
@@ -415,7 +419,7 @@ class Transaction extends Twig_Extension
return $txt; 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; return $txt;
} }

View File

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

View File

@@ -90,7 +90,7 @@ final class ToAccountIs extends AbstractTrigger implements TriggerInterface
return true; 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; return false;
} }

View File

@@ -313,10 +313,8 @@ class FireflyValidator extends Validator
* *
* @return bool * @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: // first, get the index from this string:
$parts = explode('.', $attribute); $parts = explode('.', $attribute);
$index = (int)($parts[1] ?? '0'); $index = (int)($parts[1] ?? '0');

View File

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

View File

@@ -2,6 +2,46 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). 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 ## [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. 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-intl": "*",
"ext-xml": "*", "ext-xml": "*",
"ext-zip": "*", "ext-zip": "*",
"ext-json": "*",
"ext-ldap": "*",
"adldap2/adldap2-laravel": "^4.0",
"bacon/bacon-qr-code": "1.*", "bacon/bacon-qr-code": "1.*",
"bunq/sdk_php": "dev-master#8c1faefc111d9b970168a1837ca165d854954941", "bunq/sdk_php": "dev-master#8c1faefc111d9b970168a1837ca165d854954941",
"danhunsaker/laravel-flysystem-others": "^1.3",
"davejamesmiller/laravel-breadcrumbs": "5.*", "davejamesmiller/laravel-breadcrumbs": "5.*",
"doctrine/dbal": "2.*", "doctrine/dbal": "2.*",
"fideloper/proxy": "4.*", "fideloper/proxy": "4.*",
@@ -63,7 +67,11 @@
"laravelcollective/html": "5.7.*", "laravelcollective/html": "5.7.*",
"league/commonmark": "0.*", "league/commonmark": "0.*",
"league/csv": "9.*", "league/csv": "9.*",
"league/flysystem-replicate-adapter": "^1.0",
"league/flysystem-sftp": "^1.0",
"league/fractal": "^0.17.0", "league/fractal": "^0.17.0",
"litipk/flysystem-fallback-adapter": "0.1.2",
"mschindler83/fints-hbci-php": "^1.0",
"pragmarx/google2fa": "3.*", "pragmarx/google2fa": "3.*",
"pragmarx/google2fa-laravel": "0.*", "pragmarx/google2fa-laravel": "0.*",
"rcrowe/twigbridge": "0.9.*", "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', 'driver' => 'session',
'provider' => 'users', 'provider' => 'users',
], ],
'api' => [ 'api' => [
'driver' => 'passport', 'driver' => 'passport',
'provider' => 'users', 'provider' => 'users',
@@ -88,14 +87,9 @@ return [
'providers' => [ 'providers' => [
'users' => [ 'users' => [
'driver' => 'eloquent', 'driver' => envNonEmpty('LOGIN_PROVIDER', 'eloquent'),//'adldap',
'model' => FireflyIII\User::class, 'model' => FireflyIII\User::class,
], ],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
], ],
/* /*

View File

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

View File

@@ -21,6 +21,21 @@
declare(strict_types=1); 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 [ return [
@@ -59,25 +74,68 @@ return [
| may even configure multiple disks of the same driver. Defaults have | may even configure multiple disks of the same driver. Defaults have
| been setup for each driver as an example of the required options. | 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' => [ 'disks' => [
'local' => [ 'local' => [
'driver' => 'local', 'driver' => 'local',
'root' => storage_path('app'), 'root' => storage_path('app'),
], ],
'upload' => [ // local storage configuration for upload and export:
'local-upload' => [
'driver' => 'local', 'driver' => 'local',
'root' => storage_path('upload'), 'root' => storage_path('upload'),
], ],
'export' => [ 'local-export' => [
'driver' => 'local', 'driver' => 'local',
'root' => storage_path('export'), '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' => [ 'database' => [
'driver' => 'local', 'driver' => 'local',
'root' => storage_path('database'), 'root' => storage_path('database'),
@@ -98,6 +156,9 @@ return [
'visibility' => 'public', 'visibility' => 'public',
], ],
// unused storage backends.
/*
's3' => [ 's3' => [
'driver' => 's3', 'driver' => 's3',
'key' => env('AWS_KEY'), 'key' => env('AWS_KEY'),
@@ -106,6 +167,340 @@ return [
'bucket' => env('AWS_BUCKET'), '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, 'is_demo_site' => false,
], ],
'encryption' => null === env('USE_ENCRYPTION') || env('USE_ENCRYPTION') === true, 'encryption' => null === env('USE_ENCRYPTION') || env('USE_ENCRYPTION') === true,
'version' => '4.7.7', 'version' => '4.7.8',
'api_version' => '0.8', 'api_version' => '0.81',
'db_version' => 5, 'db_version' => 6,
'maxUploadSize' => 15242880, 'maxUploadSize' => 15242880,
'login_provider' => env('LOGIN_PROVIDER', 'eloquent'),
'allowedMimes' => [ 'allowedMimes' => [
/* plain files */ /* plain files */
'text/plain', 'text/plain',
@@ -238,16 +239,25 @@ return [
'languages' => [ 'languages' => [
// completed languages // completed languages
'en_US' => ['name_locale' => 'English', 'name_english' => 'English'], 'en_US' => ['name_locale' => 'English', 'name_english' => 'English'],
'es_ES' => ['name_locale' => 'Español', 'name_english' => 'Spanish'], 'es_ES' => ['name_locale' => 'Español', 'name_english' => 'Spanish'], // 2018-10-26: 96%
'de_DE' => ['name_locale' => 'Deutsch', 'name_english' => 'German'], 'de_DE' => ['name_locale' => 'Deutsch', 'name_english' => 'German'], // 2018-10-26: 100%
'fr_FR' => ['name_locale' => 'Français', 'name_english' => 'French'], 'fr_FR' => ['name_locale' => 'Français', 'name_english' => 'French'], // 2018-10-26: 100%
//'id_ID' => ['name_locale' => 'Bahasa Indonesia', 'name_english' => 'Indonesian'], //'id_ID' => ['name_locale' => 'Bahasa Indonesia', 'name_english' => 'Indonesian'], // 2018-10-26: 61% :(
'it_IT' => ['name_locale' => 'Italiano', 'name_english' => 'Italian'], 'it_IT' => ['name_locale' => 'Italiano', 'name_english' => 'Italian'], // 2018-10-26: 100%
'nl_NL' => ['name_locale' => 'Nederlands', 'name_english' => 'Dutch'], 'nl_NL' => ['name_locale' => 'Nederlands', 'name_english' => 'Dutch'], // 2018-10-26: 100%
'pl_PL' => ['name_locale' => 'Polski', 'name_english' => 'Polish '], 'pl_PL' => ['name_locale' => 'Polski', 'name_english' => 'Polish '], // 2018-10-26: 76%
//'pt_BR' => ['name_locale' => 'Português do Brasil', 'name_english' => 'Portuguese (Brazil)'], 'pt_BR' => ['name_locale' => 'Português do Brasil', 'name_english' => 'Portuguese (Brazil)'], // 2018-10-26: 77%
'ru_RU' => ['name_locale' => 'Русский', 'name_english' => 'Russian'], 'ru_RU' => ['name_locale' => 'Русский', 'name_english' => 'Russian'], // 2018-10-26: 80%
//'tr_TR' => ['name_locale' => 'Türkçe', 'name_english' => 'Turkish'], //'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' => [ 'transactionTypesByWhat' => [
'expenses' => ['Withdrawal'], 'expenses' => ['Withdrawal'],
@@ -374,23 +384,59 @@ return [
'convert_deposit' => ConvertToDeposit::class, 'convert_deposit' => ConvertToDeposit::class,
'convert_transfer' => ConvertToTransfer::class, 'convert_transfer' => ConvertToTransfer::class,
], ],
'rule-actions-text' => [ 'context-rule-actions' => [
'set_category', 'set_category',
'set_budget', 'set_budget',
'add_tag', 'add_tag',
'remove_tag', 'remove_tag',
'link_to_bill',
'set_description', 'set_description',
'append_description', 'append_description',
'prepend_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, 'limit' => 10,
'range' => 200, 'range' => 200,
], ],
'default_currency' => 'EUR', 'default_currency' => 'EUR',
'default_language' => 'en_US', 'default_language' => 'en_US',
'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category', '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'], 'budget', 'bill', 'type', 'date', 'date_before', 'date_after', 'on', 'before', 'after'],
// tag notes has_attachments // tag notes has_attachments
]; ];

View File

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

View File

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

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