mirror of
				https://github.com/firefly-iii/firefly-iii.git
				synced 2025-10-31 10:47:00 +00:00 
			
		
		
		
	Compare commits
	
		
			287 Commits
		
	
	
		
			develop-20
			...
			develop-20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 25c1ca2f5d | ||
|  | 6f0bb82f59 | ||
|  | 22a5184ebe | ||
|  | 17b0b1f43f | ||
|  | b61df5ec19 | ||
|  | 1ac7275f83 | ||
|  | cd10d04907 | ||
|  | f9b76fcb8b | ||
|  | 093fa067e6 | ||
|  | fa655f065b | ||
|  | c8f2244912 | ||
|  | f3a20e14a6 | ||
|  | 33ad47b115 | ||
|  | 775424d3b7 | ||
|  | c9c86bbd1d | ||
|  | f76a6ad85c | ||
|  | 2138b14d89 | ||
|  | 1bf61f57f5 | ||
|  | 07b55bd71f | ||
|  | 8d2d3d4002 | ||
|  | d182b4b4a6 | ||
|  | 60f6a91fe4 | ||
|  | ec89a2f956 | ||
|  | 87113d7181 | ||
|  | 59fae290e5 | ||
|  | 1a8ba2ce53 | ||
|  | dddaa25d86 | ||
|  | f28341587a | ||
|  | 5593bf3e08 | ||
|  | 92574a7a9d | ||
|  | e049266f5d | ||
|  | 5b3e6fcb07 | ||
|  | b0bfb556db | ||
|  | 484acbcb45 | ||
|  | cdc802cfb8 | ||
|  | 582671ca84 | ||
|  | 22498b5804 | ||
|  | 87f277a482 | ||
|  | ae0d74f57a | ||
|  | 0ae5593dde | ||
|  | 0d11769590 | ||
|  | b7d8daf013 | ||
|  | a9c0126b05 | ||
|  | 6bc5a57d10 | ||
|  | 2714ee96f1 | ||
|  | 524d382b7a | ||
|  | 2723e05d2a | ||
|  | 6dd9bda6b4 | ||
|  | 44449bc716 | ||
|  | b17d8edb50 | ||
|  | 578072238a | ||
|  | b4edd3dcc4 | ||
|  | 068094caac | ||
|  | deb58e617d | ||
|  | baca0c1120 | ||
|  | 02543438a4 | ||
|  | d507e59038 | ||
|  | 9d0fd7ef1b | ||
|  | dbef5e2143 | ||
|  | 04eca755d2 | ||
|  | 7883692196 | ||
|  | 8f64977cb9 | ||
|  | f94fdc4979 | ||
|  | a0a0e28447 | ||
|  | f6f7783b94 | ||
|  | d233cc1de8 | ||
|  | 37671499c8 | ||
|  | c83b79998d | ||
|  | ed842c2b42 | ||
|  | 8c5f114339 | ||
|  | 8b2f1d0b4f | ||
|  | 591a1b3050 | ||
|  | 42ec3fe02b | ||
|  | 370a398b5e | ||
|  | 554d89b6e9 | ||
|  | cb049f5dda | ||
|  | 0728668d41 | ||
|  | dfc187874e | ||
|  | 225588f3e7 | ||
|  | 06cc6c29aa | ||
|  | b2d4469908 | ||
|  | c398383905 | ||
|  | 7af9dce33b | ||
|  | 038790a5d6 | ||
|  | fb3295bde1 | ||
|  | 43a4fd2ecb | ||
|  | 899c72d068 | ||
|  | d118c0d886 | ||
|  | 6d4004d1ed | ||
|  | ae60cd5b28 | ||
|  | ab31a72199 | ||
|  | 2c1b9534f3 | ||
|  | 7028cb1546 | ||
|  | dc1ecf6a42 | ||
|  | 3a27f9d02c | ||
|  | 4b27ab38f8 | ||
|  | 40de147611 | ||
|  | df5756dc86 | ||
|  | bb4f90d730 | ||
|  | d89d46aaec | ||
|  | 304d720c4c | ||
|  | 7eff160190 | ||
|  | 8b2e18ed9d | ||
|  | 7001051833 | ||
|  | b4b9752c05 | ||
|  | acadc89eaa | ||
|  | 6ff84b8e90 | ||
|  | 7f3e3fc3bf | ||
|  | 02233fd7a4 | ||
|  | 50d3db0643 | ||
|  | 3751831779 | ||
|  | 14a24e47fb | ||
|  | b7e78cb0e6 | ||
|  | a8f65f42fc | ||
|  | d3385a116d | ||
|  | e0c446dd13 | ||
|  | 33d11b4780 | ||
|  | 07c49d1d04 | ||
|  | 9463285ac9 | ||
|  | b41fc43e64 | ||
|  | 562763c938 | ||
|  | ec60194110 | ||
|  | 1e472ee095 | ||
|  | 5597327448 | ||
|  | cdd5baf5be | ||
|  | 7b5978059b | ||
|  | da0b41e45c | ||
|  | d0be2afba5 | ||
|  | d99851231a | ||
|  | 7e02c141f9 | ||
|  | d03960e379 | ||
|  | 16d3984ffc | ||
|  | 856a194988 | ||
|  | 1bff966bfe | ||
|  | 1948b6118b | ||
|  | 20c25d3ca2 | ||
|  | a153735ac3 | ||
|  | 62509f7c18 | ||
|  | 9b48b67158 | ||
|  | cbd50634a4 | ||
|  | f475393bc1 | ||
|  | abcddb09bf | ||
|  | cf71a0fc55 | ||
|  | 78253f9e1e | ||
|  | ebd0848c7f | ||
|  | c8461eb0b5 | ||
|  | a4cbdeaeac | ||
|  | 3e1ce69d52 | ||
|  | 08a26b976e | ||
|  | 5fc55381a2 | ||
|  | dbf3d24ae7 | ||
|  | cc7c6e02c5 | ||
|  | b45aa85853 | ||
|  | e7526ac5e3 | ||
|  | 441ada70b8 | ||
|  | dedc06a46b | ||
|  | b0adf1b277 | ||
|  | 28f65e9f44 | ||
|  | a013af5f0d | ||
|  | 9552701662 | ||
|  | ef52f0aad1 | ||
|  | 0b6f04905a | ||
|  | cdb36357d4 | ||
|  | 8938622bd9 | ||
|  | b210294aa9 | ||
|  | 5b02f20775 | ||
|  | fac382a5df | ||
|  | 88d88bebc9 | ||
|  | 755fb9c29b | ||
|  | 51a835ab51 | ||
|  | c9895ab182 | ||
|  | e71d46a4e5 | ||
|  | 8d1d5f37c1 | ||
|  | 525a68682d | ||
|  | 715648d0d8 | ||
|  | 9452e93f22 | ||
|  | a6aa145471 | ||
|  | 25aa6dcb59 | ||
|  | bb2270b274 | ||
|  | d7f6b4143e | ||
|  | 0cf0e26fa8 | ||
|  | cc23197d60 | ||
|  | bc1721d95e | ||
|  | 0d19173da6 | ||
|  | 1983f07d3c | ||
|  | aad1b91cc2 | ||
|  | 8cb1057a33 | ||
|  | b178032985 | ||
|  | 561213e95d | ||
|  | 44fa7c4306 | ||
|  | e2169563e2 | ||
|  | 845344e003 | ||
|  | cdb48453e8 | ||
|  | 9669cef518 | ||
|  | f962f71ed7 | ||
|  | 94ed4021fb | ||
|  | 1765855c57 | ||
|  | 55cf3e7d44 | ||
|  | 9f1840dc05 | ||
|  | 78dab2e5f9 | ||
|  | 103b9d5005 | ||
|  | 1b75b778d8 | ||
|  | 7e665dbdfc | ||
|  | b6897ec3a9 | ||
|  | 660260174a | ||
|  | 78d32865b5 | ||
|  | edfa92c1aa | ||
|  | 63012f269c | ||
|  | 7d0e7f779f | ||
|  | b8e18f80f4 | ||
|  | 481b01e4f7 | ||
|  | edf2030251 | ||
|  | bd1cfffb61 | ||
|  | 629f70d27d | ||
|  | 57f5ebc0f9 | ||
|  | b4f51e7b47 | ||
|  | d78d254e86 | ||
|  | eb0c113699 | ||
|  | 23045ebd59 | ||
|  | 2e5931f304 | ||
|  | a620b07c00 | ||
|  | cb724145f2 | ||
|  | 8ef17f6686 | ||
|  | debfd9160c | ||
|  | f2482e4ace | ||
|  | d98d757f8b | ||
|  | 0c9a41a929 | ||
|  | e26f78bf50 | ||
|  | ed265f68ba | ||
|  | d2e9b64bf5 | ||
|  | 3811aff206 | ||
|  | 762d898fee | ||
|  | 5e6034fc86 | ||
|  | 9da10459d6 | ||
|  | ff80cedd6b | ||
|  | b213148ae8 | ||
|  | c8646e20cb | ||
|  | 76a41fec50 | ||
|  | 0e705bd038 | ||
|  | f33ffb98ff | ||
|  | faa0d59340 | ||
|  | 5af0219884 | ||
|  | dafd99f155 | ||
|  | 3560f0388c | ||
|  | b2954658d8 | ||
|  | 44581d9983 | ||
|  | 02dcfeb227 | ||
|  | d8bafb349d | ||
|  | 889598a4c8 | ||
|  | 7e37d10016 | ||
|  | ebaebb09d1 | ||
|  | 531a3a4b6c | ||
|  | b3e313821b | ||
|  | 51958af422 | ||
|  | e3b21ccdba | ||
|  | 31bb208835 | ||
|  | 8c97e805a2 | ||
|  | ac8a43bb37 | ||
|  | 2df4b40a28 | ||
|  | e06736c254 | ||
|  | ec367e94ce | ||
|  | 1515dea9fa | ||
|  | adedf9c17d | ||
|  | 0b52fb84f1 | ||
|  | 16e742ae73 | ||
|  | 1b4471dfae | ||
|  | ae152ce0a4 | ||
|  | 2aa023f140 | ||
|  | 6e2e4c6f08 | ||
|  | 1c8c038735 | ||
|  | 4d339a6da8 | ||
|  | b7edd4407a | ||
|  | a679a1e94a | ||
|  | 180451d32f | ||
|  | 7396f22bca | ||
|  | 058019aa84 | ||
|  | 695f83d1d8 | ||
|  | ac4dfb3baf | ||
|  | 427001b223 | ||
|  | 3117d8b30d | ||
|  | d19dd2a8b2 | ||
|  | de3dcc3fc2 | ||
|  | 077f3e095b | ||
|  | ad3b0bb320 | ||
|  | 8538741341 | ||
|  | a0aef5d579 | ||
|  | fdd93427aa | 
							
								
								
									
										286
									
								
								.ci/php-cs-fixer/composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										286
									
								
								.ci/php-cs-fixer/composer.lock
									
									
									
										generated
									
									
									
								
							| @@ -72,30 +72,38 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "composer/pcre", | ||||
|             "version": "3.1.4", | ||||
|             "version": "3.3.2", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/composer/pcre.git", | ||||
|                 "reference": "04229f163664973f68f38f6f73d917799168ef24" | ||||
|                 "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/composer/pcre/zipball/04229f163664973f68f38f6f73d917799168ef24", | ||||
|                 "reference": "04229f163664973f68f38f6f73d917799168ef24", | ||||
|                 "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", | ||||
|                 "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "php": "^7.4 || ^8.0" | ||||
|             }, | ||||
|             "conflict": { | ||||
|                 "phpstan/phpstan": "<1.11.10" | ||||
|             }, | ||||
|             "require-dev": { | ||||
|                 "phpstan/phpstan": "^1.3", | ||||
|                 "phpstan/phpstan-strict-rules": "^1.1", | ||||
|                 "symfony/phpunit-bridge": "^5" | ||||
|                 "phpstan/phpstan": "^1.12 || ^2", | ||||
|                 "phpstan/phpstan-strict-rules": "^1 || ^2", | ||||
|                 "phpunit/phpunit": "^8 || ^9" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
|                 "branch-alias": { | ||||
|                     "dev-main": "3.x-dev" | ||||
|                 }, | ||||
|                 "phpstan": { | ||||
|                     "includes": [ | ||||
|                         "extension.neon" | ||||
|                     ] | ||||
|                 } | ||||
|             }, | ||||
|             "autoload": { | ||||
| @@ -123,7 +131,7 @@ | ||||
|             ], | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/composer/pcre/issues", | ||||
|                 "source": "https://github.com/composer/pcre/tree/3.1.4" | ||||
|                 "source": "https://github.com/composer/pcre/tree/3.3.2" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -139,28 +147,28 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-05-27T13:40:54+00:00" | ||||
|             "time": "2024-11-12T16:29:46+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "composer/semver", | ||||
|             "version": "3.4.2", | ||||
|             "version": "3.4.3", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/composer/semver.git", | ||||
|                 "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6" | ||||
|                 "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/composer/semver/zipball/c51258e759afdb17f1fd1fe83bc12baaef6309d6", | ||||
|                 "reference": "c51258e759afdb17f1fd1fe83bc12baaef6309d6", | ||||
|                 "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", | ||||
|                 "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "php": "^5.3.2 || ^7.0 || ^8.0" | ||||
|             }, | ||||
|             "require-dev": { | ||||
|                 "phpstan/phpstan": "^1.4", | ||||
|                 "symfony/phpunit-bridge": "^4.2 || ^5" | ||||
|                 "phpstan/phpstan": "^1.11", | ||||
|                 "symfony/phpunit-bridge": "^3 || ^7" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
| @@ -204,7 +212,7 @@ | ||||
|             "support": { | ||||
|                 "irc": "ircs://irc.libera.chat:6697/composer", | ||||
|                 "issues": "https://github.com/composer/semver/issues", | ||||
|                 "source": "https://github.com/composer/semver/tree/3.4.2" | ||||
|                 "source": "https://github.com/composer/semver/tree/3.4.3" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -220,7 +228,7 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-07-12T11:35:52+00:00" | ||||
|             "time": "2024-09-19T14:15:21+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "composer/xdebug-handler", | ||||
| @@ -337,16 +345,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "fidry/cpu-core-counter", | ||||
|             "version": "1.1.0", | ||||
|             "version": "1.2.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/theofidry/cpu-core-counter.git", | ||||
|                 "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" | ||||
|                 "reference": "8520451a140d3f46ac33042715115e290cf5785f" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", | ||||
|                 "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", | ||||
|                 "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", | ||||
|                 "reference": "8520451a140d3f46ac33042715115e290cf5785f", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -386,7 +394,7 @@ | ||||
|             ], | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/theofidry/cpu-core-counter/issues", | ||||
|                 "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" | ||||
|                 "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -394,20 +402,20 @@ | ||||
|                     "type": "github" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-02-07T09:43:46+00:00" | ||||
|             "time": "2024-08-06T10:04:20+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "friendsofphp/php-cs-fixer", | ||||
|             "version": "v3.59.3", | ||||
|             "version": "v3.64.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", | ||||
|                 "reference": "30ba9ecc2b0e5205e578fe29973c15653d9bfd29" | ||||
|                 "reference": "58dd9c931c785a79739310aef5178928305ffa67" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/30ba9ecc2b0e5205e578fe29973c15653d9bfd29", | ||||
|                 "reference": "30ba9ecc2b0e5205e578fe29973c15653d9bfd29", | ||||
|                 "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/58dd9c931c785a79739310aef5178928305ffa67", | ||||
|                 "reference": "58dd9c931c785a79739310aef5178928305ffa67", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -489,7 +497,7 @@ | ||||
|             ], | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", | ||||
|                 "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.59.3" | ||||
|                 "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.64.0" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -497,7 +505,7 @@ | ||||
|                     "type": "github" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-06-16T14:17:03+00:00" | ||||
|             "time": "2024-08-30T23:09:38+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "psr/container", | ||||
| @@ -604,16 +612,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "psr/log", | ||||
|             "version": "3.0.0", | ||||
|             "version": "3.0.2", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/php-fig/log.git", | ||||
|                 "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" | ||||
|                 "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", | ||||
|                 "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", | ||||
|                 "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", | ||||
|                 "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -648,9 +656,9 @@ | ||||
|                 "psr-3" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "source": "https://github.com/php-fig/log/tree/3.0.0" | ||||
|                 "source": "https://github.com/php-fig/log/tree/3.0.2" | ||||
|             }, | ||||
|             "time": "2021-07-14T16:46:02+00:00" | ||||
|             "time": "2024-09-11T13:17:53+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "react/cache", | ||||
| @@ -1026,31 +1034,31 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "react/socket", | ||||
|             "version": "v1.15.0", | ||||
|             "version": "v1.16.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/reactphp/socket.git", | ||||
|                 "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038" | ||||
|                 "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/reactphp/socket/zipball/216d3aec0b87f04a40ca04f481e6af01bdd1d038", | ||||
|                 "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038", | ||||
|                 "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", | ||||
|                 "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "evenement/evenement": "^3.0 || ^2.0 || ^1.0", | ||||
|                 "php": ">=5.3.0", | ||||
|                 "react/dns": "^1.11", | ||||
|                 "react/dns": "^1.13", | ||||
|                 "react/event-loop": "^1.2", | ||||
|                 "react/promise": "^3 || ^2.6 || ^1.2.1", | ||||
|                 "react/stream": "^1.2" | ||||
|                 "react/promise": "^3.2 || ^2.6 || ^1.2.1", | ||||
|                 "react/stream": "^1.4" | ||||
|             }, | ||||
|             "require-dev": { | ||||
|                 "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", | ||||
|                 "react/async": "^4 || ^3 || ^2", | ||||
|                 "react/async": "^4.3 || ^3.3 || ^2", | ||||
|                 "react/promise-stream": "^1.4", | ||||
|                 "react/promise-timer": "^1.10" | ||||
|                 "react/promise-timer": "^1.11" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "autoload": { | ||||
| @@ -1094,7 +1102,7 @@ | ||||
|             ], | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/reactphp/socket/issues", | ||||
|                 "source": "https://github.com/reactphp/socket/tree/v1.15.0" | ||||
|                 "source": "https://github.com/reactphp/socket/tree/v1.16.0" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -1102,7 +1110,7 @@ | ||||
|                     "type": "open_collective" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2023-12-15T11:02:10+00:00" | ||||
|             "time": "2024-07-26T10:38:09+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "react/stream", | ||||
| @@ -1251,16 +1259,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/console", | ||||
|             "version": "v7.1.2", | ||||
|             "version": "v7.1.8", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/console.git", | ||||
|                 "reference": "0aa29ca177f432ab68533432db0de059f39c92ae" | ||||
|                 "reference": "ff04e5b5ba043d2badfb308197b9e6b42883fcd5" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/console/zipball/0aa29ca177f432ab68533432db0de059f39c92ae", | ||||
|                 "reference": "0aa29ca177f432ab68533432db0de059f39c92ae", | ||||
|                 "url": "https://api.github.com/repos/symfony/console/zipball/ff04e5b5ba043d2badfb308197b9e6b42883fcd5", | ||||
|                 "reference": "ff04e5b5ba043d2badfb308197b9e6b42883fcd5", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -1324,7 +1332,7 @@ | ||||
|                 "terminal" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/console/tree/v7.1.2" | ||||
|                 "source": "https://github.com/symfony/console/tree/v7.1.8" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -1340,7 +1348,7 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-06-28T10:03:55+00:00" | ||||
|             "time": "2024-11-06T14:23:19+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/deprecation-contracts", | ||||
| @@ -1411,16 +1419,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/event-dispatcher", | ||||
|             "version": "v7.1.1", | ||||
|             "version": "v7.1.6", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/event-dispatcher.git", | ||||
|                 "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7" | ||||
|                 "reference": "87254c78dd50721cfd015b62277a8281c5589702" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", | ||||
|                 "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", | ||||
|                 "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87254c78dd50721cfd015b62277a8281c5589702", | ||||
|                 "reference": "87254c78dd50721cfd015b62277a8281c5589702", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -1471,7 +1479,7 @@ | ||||
|             "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", | ||||
|             "homepage": "https://symfony.com", | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1" | ||||
|                 "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.6" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -1487,7 +1495,7 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-05-31T14:57:53+00:00" | ||||
|             "time": "2024-09-25T14:20:29+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/event-dispatcher-contracts", | ||||
| @@ -1567,16 +1575,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/filesystem", | ||||
|             "version": "v7.1.2", | ||||
|             "version": "v7.1.6", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/filesystem.git", | ||||
|                 "reference": "92a91985250c251de9b947a14bb2c9390b1a562c" | ||||
|                 "reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/filesystem/zipball/92a91985250c251de9b947a14bb2c9390b1a562c", | ||||
|                 "reference": "92a91985250c251de9b947a14bb2c9390b1a562c", | ||||
|                 "url": "https://api.github.com/repos/symfony/filesystem/zipball/c835867b3c62bb05c7fe3d637c871c7ae52024d4", | ||||
|                 "reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -1613,7 +1621,7 @@ | ||||
|             "description": "Provides basic utilities for the filesystem", | ||||
|             "homepage": "https://symfony.com", | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/filesystem/tree/v7.1.2" | ||||
|                 "source": "https://github.com/symfony/filesystem/tree/v7.1.6" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -1629,20 +1637,20 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-06-28T10:03:55+00:00" | ||||
|             "time": "2024-10-25T15:11:02+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/finder", | ||||
|             "version": "v7.1.1", | ||||
|             "version": "v7.1.6", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/finder.git", | ||||
|                 "reference": "fbb0ba67688b780efbc886c1a0a0948dcf7205d6" | ||||
|                 "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/finder/zipball/fbb0ba67688b780efbc886c1a0a0948dcf7205d6", | ||||
|                 "reference": "fbb0ba67688b780efbc886c1a0a0948dcf7205d6", | ||||
|                 "url": "https://api.github.com/repos/symfony/finder/zipball/2cb89664897be33f78c65d3d2845954c8d7a43b8", | ||||
|                 "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -1677,7 +1685,7 @@ | ||||
|             "description": "Finds files and directories via an intuitive fluent interface", | ||||
|             "homepage": "https://symfony.com", | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/finder/tree/v7.1.1" | ||||
|                 "source": "https://github.com/symfony/finder/tree/v7.1.6" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -1693,20 +1701,20 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-05-31T14:57:53+00:00" | ||||
|             "time": "2024-10-01T08:31:23+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/options-resolver", | ||||
|             "version": "v7.1.1", | ||||
|             "version": "v7.1.6", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/options-resolver.git", | ||||
|                 "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55" | ||||
|                 "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55", | ||||
|                 "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55", | ||||
|                 "url": "https://api.github.com/repos/symfony/options-resolver/zipball/85e95eeede2d41cd146146e98c9c81d9214cae85", | ||||
|                 "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -1744,7 +1752,7 @@ | ||||
|                 "options" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/options-resolver/tree/v7.1.1" | ||||
|                 "source": "https://github.com/symfony/options-resolver/tree/v7.1.6" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -1760,24 +1768,24 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-05-31T14:57:53+00:00" | ||||
|             "time": "2024-09-25T14:20:29+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/polyfill-ctype", | ||||
|             "version": "v1.30.0", | ||||
|             "version": "v1.31.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/polyfill-ctype.git", | ||||
|                 "reference": "0424dff1c58f028c451efff2045f5d92410bd540" | ||||
|                 "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", | ||||
|                 "reference": "0424dff1c58f028c451efff2045f5d92410bd540", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", | ||||
|                 "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "php": ">=7.1" | ||||
|                 "php": ">=7.2" | ||||
|             }, | ||||
|             "provide": { | ||||
|                 "ext-ctype": "*" | ||||
| @@ -1823,7 +1831,7 @@ | ||||
|                 "portable" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" | ||||
|                 "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -1839,24 +1847,24 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-05-31T15:07:36+00:00" | ||||
|             "time": "2024-09-09T11:45:10+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/polyfill-intl-grapheme", | ||||
|             "version": "v1.30.0", | ||||
|             "version": "v1.31.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/polyfill-intl-grapheme.git", | ||||
|                 "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" | ||||
|                 "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", | ||||
|                 "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", | ||||
|                 "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "php": ">=7.1" | ||||
|                 "php": ">=7.2" | ||||
|             }, | ||||
|             "suggest": { | ||||
|                 "ext-intl": "For best performance" | ||||
| @@ -1901,7 +1909,7 @@ | ||||
|                 "shim" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" | ||||
|                 "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -1917,24 +1925,24 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-05-31T15:07:36+00:00" | ||||
|             "time": "2024-09-09T11:45:10+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/polyfill-intl-normalizer", | ||||
|             "version": "v1.30.0", | ||||
|             "version": "v1.31.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/polyfill-intl-normalizer.git", | ||||
|                 "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" | ||||
|                 "reference": "3833d7255cc303546435cb650316bff708a1c75c" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", | ||||
|                 "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", | ||||
|                 "reference": "3833d7255cc303546435cb650316bff708a1c75c", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "php": ">=7.1" | ||||
|                 "php": ">=7.2" | ||||
|             }, | ||||
|             "suggest": { | ||||
|                 "ext-intl": "For best performance" | ||||
| @@ -1982,7 +1990,7 @@ | ||||
|                 "shim" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" | ||||
|                 "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -1998,24 +2006,24 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-05-31T15:07:36+00:00" | ||||
|             "time": "2024-09-09T11:45:10+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/polyfill-mbstring", | ||||
|             "version": "v1.30.0", | ||||
|             "version": "v1.31.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/polyfill-mbstring.git", | ||||
|                 "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" | ||||
|                 "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", | ||||
|                 "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", | ||||
|                 "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "php": ">=7.1" | ||||
|                 "php": ">=7.2" | ||||
|             }, | ||||
|             "provide": { | ||||
|                 "ext-mbstring": "*" | ||||
| @@ -2062,7 +2070,7 @@ | ||||
|                 "shim" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" | ||||
|                 "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -2078,24 +2086,24 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-06-19T12:30:46+00:00" | ||||
|             "time": "2024-09-09T11:45:10+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/polyfill-php80", | ||||
|             "version": "v1.30.0", | ||||
|             "version": "v1.31.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/polyfill-php80.git", | ||||
|                 "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" | ||||
|                 "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", | ||||
|                 "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", | ||||
|                 "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "php": ">=7.1" | ||||
|                 "php": ">=7.2" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
| @@ -2142,7 +2150,7 @@ | ||||
|                 "shim" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" | ||||
|                 "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -2158,24 +2166,24 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-05-31T15:07:36+00:00" | ||||
|             "time": "2024-09-09T11:45:10+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/polyfill-php81", | ||||
|             "version": "v1.30.0", | ||||
|             "version": "v1.31.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/polyfill-php81.git", | ||||
|                 "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af" | ||||
|                 "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/3fb075789fb91f9ad9af537c4012d523085bd5af", | ||||
|                 "reference": "3fb075789fb91f9ad9af537c4012d523085bd5af", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", | ||||
|                 "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "php": ">=7.1" | ||||
|                 "php": ">=7.2" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
| @@ -2218,7 +2226,7 @@ | ||||
|                 "shim" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/polyfill-php81/tree/v1.30.0" | ||||
|                 "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -2234,20 +2242,20 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-06-19T12:30:46+00:00" | ||||
|             "time": "2024-09-09T11:45:10+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/process", | ||||
|             "version": "v7.1.1", | ||||
|             "version": "v7.1.8", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/process.git", | ||||
|                 "reference": "febf90124323a093c7ee06fdb30e765ca3c20028" | ||||
|                 "reference": "42783370fda6e538771f7c7a36e9fa2ee3a84892" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/process/zipball/febf90124323a093c7ee06fdb30e765ca3c20028", | ||||
|                 "reference": "febf90124323a093c7ee06fdb30e765ca3c20028", | ||||
|                 "url": "https://api.github.com/repos/symfony/process/zipball/42783370fda6e538771f7c7a36e9fa2ee3a84892", | ||||
|                 "reference": "42783370fda6e538771f7c7a36e9fa2ee3a84892", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -2279,7 +2287,7 @@ | ||||
|             "description": "Executes commands in sub-processes", | ||||
|             "homepage": "https://symfony.com", | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/process/tree/v7.1.1" | ||||
|                 "source": "https://github.com/symfony/process/tree/v7.1.8" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -2295,7 +2303,7 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-05-31T14:57:53+00:00" | ||||
|             "time": "2024-11-06T14:23:19+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/service-contracts", | ||||
| @@ -2382,16 +2390,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/stopwatch", | ||||
|             "version": "v7.1.1", | ||||
|             "version": "v7.1.6", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/stopwatch.git", | ||||
|                 "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d" | ||||
|                 "reference": "8b4a434e6e7faf6adedffb48783a5c75409a1a05" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", | ||||
|                 "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", | ||||
|                 "url": "https://api.github.com/repos/symfony/stopwatch/zipball/8b4a434e6e7faf6adedffb48783a5c75409a1a05", | ||||
|                 "reference": "8b4a434e6e7faf6adedffb48783a5c75409a1a05", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -2424,7 +2432,7 @@ | ||||
|             "description": "Provides a way to profile code", | ||||
|             "homepage": "https://symfony.com", | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/stopwatch/tree/v7.1.1" | ||||
|                 "source": "https://github.com/symfony/stopwatch/tree/v7.1.6" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -2440,20 +2448,20 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-05-31T14:57:53+00:00" | ||||
|             "time": "2024-09-25T14:20:29+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/string", | ||||
|             "version": "v7.1.2", | ||||
|             "version": "v7.1.8", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/string.git", | ||||
|                 "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8" | ||||
|                 "reference": "591ebd41565f356fcd8b090fe64dbb5878f50281" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/string/zipball/14221089ac66cf82e3cf3d1c1da65de305587ff8", | ||||
|                 "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8", | ||||
|                 "url": "https://api.github.com/repos/symfony/string/zipball/591ebd41565f356fcd8b090fe64dbb5878f50281", | ||||
|                 "reference": "591ebd41565f356fcd8b090fe64dbb5878f50281", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -2511,7 +2519,7 @@ | ||||
|                 "utf8" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/string/tree/v7.1.2" | ||||
|                 "source": "https://github.com/symfony/string/tree/v7.1.8" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @@ -2527,16 +2535,16 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2024-06-28T09:27:18+00:00" | ||||
|             "time": "2024-11-13T13:31:21+00:00" | ||||
|         } | ||||
|     ], | ||||
|     "packages-dev": [], | ||||
|     "aliases": [], | ||||
|     "minimum-stability": "stable", | ||||
|     "stability-flags": [], | ||||
|     "stability-flags": {}, | ||||
|     "prefer-stable": false, | ||||
|     "prefer-lowest": false, | ||||
|     "platform": [], | ||||
|     "platform-dev": [], | ||||
|     "platform": {}, | ||||
|     "platform-dev": {}, | ||||
|     "plugin-api-version": "2.6.0" | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| parameters: | ||||
|   scanFiles: | ||||
|     - ../_ide_helper_models.php | ||||
|   universalObjectCratesClasses: | ||||
|     - Illuminate\Database\Eloquent\Model | ||||
|   # TODO: slowly remove these parameters and fix the issues found. | ||||
|   reportUnmatchedIgnoredErrors: false | ||||
|   checkGenericClassInNonGenericObjectType: false  # remove this rule when all other issues are solved. | ||||
|   ignoreErrors: | ||||
|   # TODO: slowly remove these exceptions and fix the issues found. | ||||
|     - '#Dynamic call to static method#' # all the Laravel ORM things depend on this. | ||||
| @@ -11,6 +12,7 @@ parameters: | ||||
|     - '#with no value type specified in iterable type array#' # remove this rule when all other issues are solved. | ||||
|     - '#has no value type specified in iterable type array#' # remove this rule when all other issues are solved. | ||||
|     - '#is not allowed to extend#' | ||||
|     - '#does not specify its types#' | ||||
|     - '#switch is forbidden to use#' | ||||
|     - '#is neither abstract nor final#' | ||||
|     - '#on left side of \?\?\= always exists and is not nullable#' | ||||
|   | ||||
| @@ -176,6 +176,7 @@ MAILGUN_ENDPOINT=api.mailgun.net | ||||
| # If you use Docker or similar, you can set these variables from a file by appending them with _FILE | ||||
| MANDRILL_SECRET= | ||||
| SPARKPOST_SECRET= | ||||
| MAILERSEND_API_KEY= | ||||
|  | ||||
| # Firefly III can send you the following messages. | ||||
| SEND_ERROR_MESSAGE=true | ||||
| @@ -312,6 +313,12 @@ PUSHER_ID= | ||||
| DEMO_USERNAME= | ||||
| DEMO_PASSWORD= | ||||
|  | ||||
| # | ||||
| # Disable or enable the running balance column data | ||||
| # Please disable this. It's a very experimental feature. | ||||
| # | ||||
| USE_RUNNING_BALANCE=false | ||||
|  | ||||
| # | ||||
| # The v2 layout is very experimental. If it breaks you get to keep both parts. | ||||
| # Be wary of data loss. | ||||
|   | ||||
							
								
								
									
										3
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -4,6 +4,7 @@ updates: | ||||
|   # Check for updates to GitHub Actions every week | ||||
|   - package-ecosystem: "github-actions" | ||||
|     directory: "/" | ||||
|     labels: [] | ||||
|     schedule: | ||||
|       interval: "weekly" | ||||
|  | ||||
| @@ -11,6 +12,7 @@ updates: | ||||
|   - package-ecosystem: "composer" | ||||
|     directory: "/" # Location of package manifests | ||||
|     target-branch: develop | ||||
|     labels: [] | ||||
|     versioning-strategy: increase | ||||
|     schedule: | ||||
|       interval: "weekly" | ||||
| @@ -18,6 +20,7 @@ updates: | ||||
|   # yarn / JS updates | ||||
|   - package-ecosystem: "npm" | ||||
|     directory: "/" | ||||
|     labels: [] | ||||
|     target-branch: develop | ||||
|     versioning-strategy: increase | ||||
|     schedule: | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/label-actions.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/label-actions.yml
									
									
									
									
										vendored
									
									
								
							| @@ -4,6 +4,7 @@ | ||||
| feature: | ||||
|   issues: | ||||
|     # Post a comment, `{issue-author}` is an optional placeholder | ||||
|     unlabel: feature | ||||
|     comment: | | ||||
|       Hi there!  | ||||
|  | ||||
| @@ -32,6 +33,7 @@ epic: | ||||
|       Thank you for your contributions. | ||||
|  | ||||
| enhancement: | ||||
|   unlabel: enhancement | ||||
|   issues: | ||||
|     # Post a comment, `{issue-author}` is an optional placeholder | ||||
|     comment: | | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							| @@ -6,6 +6,7 @@ Thank you for submitting new code to Firefly III, or any of the related projects | ||||
| - Please do not open PRs to "discuss" possible solutions or to "get feedback" on your code. I simply don't have time for that. | ||||
| - Pull requests for the MAIN branch will be closed. | ||||
| - DO NOT include translated strings in your PR. | ||||
| - PRs (or parts thereof) that only fix issues inside code comments will not be accepted. | ||||
| 
 | ||||
| If it feels necessary to open an issue first, please do so, before you open a PR. | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/close-duplicates.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/close-duplicates.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,7 +13,7 @@ jobs: | ||||
|   close_duplicates: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: github/command@v1.2.0 | ||||
|       - uses: github/command@v1.2.2 | ||||
|         id: command | ||||
|         with: | ||||
|           allowed_contexts: "issue" | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -12,6 +12,7 @@ jobs: | ||||
|     permissions: | ||||
|       issues: write  # for actions/stale to close stale issues | ||||
|       pull-requests: write  # for actions/stale to close stale PRs | ||||
|       actions: write | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v9 | ||||
| @@ -35,4 +36,5 @@ jobs: | ||||
|             Thank you for your contributions. | ||||
|           days-before-stale: 14 | ||||
|           days-before-close: 7 | ||||
|           exempt-issue-labels: 'enhancement,feature,bug,announcement,epic,triage' | ||||
|           exempt-all-milestones: true | ||||
|           exempt-issue-labels: 'triage' | ||||
|   | ||||
| @@ -4,6 +4,12 @@ Over time, many people have contributed to Firefly III. Their efforts are not al | ||||
| Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution. | ||||
| 
 | ||||
| ## 2024 | ||||
| - Antônio Franco | ||||
| - yparitcher | ||||
| - Jhon Pedroza | ||||
| - mzhubail | ||||
| - tasnim | ||||
| - withbest | ||||
| - Steve Wasiura | ||||
| - imlonghao | ||||
| - Rahman Yusuf | ||||
|   | ||||
| @@ -112,7 +112,12 @@ class StoreController extends Controller | ||||
| 
 | ||||
|             return response()->json([], 422); | ||||
|         } | ||||
|         $helper->saveAttachmentFromApi($attachment, $body); | ||||
|         $result = $helper->saveAttachmentFromApi($attachment, $body); | ||||
|         if (false === $result) { | ||||
|             app('log')->error('Could not save attachment from API.'); | ||||
| 
 | ||||
|             return response()->json([], 422); | ||||
|         } | ||||
| 
 | ||||
|         return response()->json([], 204); | ||||
|     } | ||||
|   | ||||
| @@ -69,7 +69,7 @@ class UpdateController extends Controller | ||||
|      */ | ||||
|     public function update(UpdateRequest $request, TransactionGroup $transactionGroup): JsonResponse | ||||
|     { | ||||
|         app('log')->debug('Now in update routine for transaction group!'); | ||||
|         app('log')->debug('Now in update routine for transaction group'); | ||||
|         $data             = $request->getAll(); | ||||
| 
 | ||||
|         // Fixes 8750.
 | ||||
|   | ||||
| @@ -34,6 +34,7 @@ use FireflyIII\Support\Http\Api\TransactionFilter; | ||||
| use FireflyIII\Transformers\CurrencyTransformer; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Http\JsonResponse; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use League\Fractal\Resource\Item; | ||||
| 
 | ||||
| /** | ||||
| @@ -164,6 +165,7 @@ class UpdateController extends Controller | ||||
|     public function update(UpdateRequest $request, TransactionCurrency $currency): JsonResponse | ||||
|     { | ||||
|         $data        = $request->getAll(); | ||||
|         Log::debug(__METHOD__, $data); | ||||
| 
 | ||||
|         /** @var User $user */ | ||||
|         $user        = auth()->user(); | ||||
| @@ -173,6 +175,11 @@ class UpdateController extends Controller | ||||
|         if (array_key_exists('enabled', $data) && false === $data['enabled'] && 1 === count($set) && $set->first()->id === $currency->id) { | ||||
|             return response()->json([], 409); | ||||
|         } | ||||
|         // second safety catch on currency disable.
 | ||||
|         if (array_key_exists('enabled', $data) && false === $data['enabled'] && $this->repository->currencyInUse($currency)) { | ||||
|             return response()->json([], 409); | ||||
|         } | ||||
| 
 | ||||
|         $currency    = $this->repository->update($currency, $data); | ||||
| 
 | ||||
|         app('preferences')->mark(); | ||||
|   | ||||
| @@ -52,7 +52,7 @@ class AboutController extends Controller | ||||
|         $data | ||||
|                        = [ | ||||
|                            'version'     => config('firefly.version'), | ||||
|                            'api_version' => config('firefly.api_version'), | ||||
|                            'api_version' => config('firefly.version'), | ||||
|                            'php_version' => $phpVersion, | ||||
|                            'os'          => $phpOs, | ||||
|                            'driver'      => $currentDriver, | ||||
|   | ||||
| @@ -102,6 +102,8 @@ class PreferencesController extends Controller | ||||
|      * TODO This endpoint is not documented. | ||||
|      * | ||||
|      * Return a single preference by name. | ||||
|      * | ||||
|      * @param Collection<int, Preference> $collection | ||||
|      */ | ||||
|     public function showList(Collection $collection): JsonResponse | ||||
|     { | ||||
|   | ||||
| @@ -46,6 +46,8 @@ class DateRequest extends FormRequest | ||||
|     { | ||||
|         $start = $this->getCarbonDate('start'); | ||||
|         $end   = $this->getCarbonDate('end'); | ||||
|         $start->startOfDay(); | ||||
|         $end->endOfDay(); | ||||
|         if ($start->diffInYears($end, true) > 5) { | ||||
|             throw new FireflyException('Date range out of range.'); | ||||
|         } | ||||
|   | ||||
| @@ -79,12 +79,12 @@ class StoreRequest extends FormRequest | ||||
|             'currency_id'    => 'numeric|exists:transaction_currencies,id', | ||||
|             'currency_code'  => 'min:3|max:51|exists:transaction_currencies,code', | ||||
|             'date'           => 'date|required', | ||||
|             'end_date'       => 'date|after:date', | ||||
|             'extension_date' => 'date|after:date', | ||||
|             'end_date'       => 'nullable|date|after:date', | ||||
|             'extension_date' => 'nullable|date|after:date', | ||||
|             'repeat_freq'    => 'in:weekly,monthly,quarterly,half-year,yearly|required', | ||||
|             'skip'           => 'min:0|max:31|numeric', | ||||
|             'active'         => [new IsBoolean()], | ||||
|             'notes'          => 'min:1|max:32768', | ||||
|             'notes'          => 'nullable|min:1|max:32768', | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|   | ||||
| @@ -138,7 +138,7 @@ class StoreRequest extends FormRequest | ||||
|                 // all custom fields:
 | ||||
|                 'internal_reference'    => $this->clearString((string)$object['internal_reference']), | ||||
|                 'external_id'           => $this->clearString((string)$object['external_id']), | ||||
|                 'original_source'       => sprintf('ff3-v%s|api-v%s', config('firefly.version'), config('firefly.api_version')), | ||||
|                 'original_source'       => sprintf('ff3-v%s', config('firefly.version')), | ||||
|                 'recurrence_id'         => $this->integerFromValue($object['recurrence_id']), | ||||
|                 'bunq_payment_id'       => $this->clearString((string)$object['bunq_payment_id']), | ||||
|                 'external_url'          => $this->clearString((string)$object['external_url']), | ||||
|   | ||||
| @@ -68,9 +68,9 @@ class AccountController extends Controller | ||||
|      */ | ||||
|     public function accounts(AutocompleteRequest $request): JsonResponse | ||||
|     { | ||||
|         $queryParameters = $request->getParameters(); | ||||
|         $result          = $this->repository->searchAccount($queryParameters['query'], $queryParameters['account_types'], $queryParameters['size']); | ||||
|         $return          = []; | ||||
|         $params = $request->getParameters(); | ||||
|         $result = $this->repository->searchAccount($params['query'], $params['account_types'], $params['page'], $params['size']); | ||||
|         $return = []; | ||||
| 
 | ||||
|         /** @var Account $account */ | ||||
|         foreach ($result as $account) { | ||||
| @@ -89,6 +89,7 @@ class AccountController extends Controller | ||||
|             'title' => $account->name, | ||||
|             'meta'  => [ | ||||
|                 'type'                    => $account->accountType->type, | ||||
|                 // TODO is multi currency property.
 | ||||
|                 'currency_id'             => null === $currency ? null : (string) $currency->id, | ||||
|                 'currency_code'           => $currency?->code, | ||||
|                 'currency_symbol'         => $currency?->symbol, | ||||
|   | ||||
| @@ -48,7 +48,7 @@ class BalanceController extends Controller | ||||
|     private AccountRepositoryInterface $repository; | ||||
|     private GroupCollectorInterface    $collector; | ||||
|     private ChartData                  $chartData; | ||||
|     private TransactionCurrency        $default; | ||||
|     // private TransactionCurrency        $default;
 | ||||
| 
 | ||||
|     public function __construct() | ||||
|     { | ||||
| @@ -61,7 +61,7 @@ class BalanceController extends Controller | ||||
|                 $this->repository->setUserGroup($userGroup); | ||||
|                 $this->collector->setUserGroup($userGroup); | ||||
|                 $this->chartData  = new ChartData(); | ||||
|                 $this->default    = app('amount')->getDefaultCurrency(); | ||||
|                 // $this->default    = app('amount')->getDefaultCurrency();
 | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
| @@ -84,10 +84,6 @@ class BalanceController extends Controller | ||||
|         $queryParameters = $request->getParameters(); | ||||
|         $accounts        = $this->getAccountList($queryParameters); | ||||
| 
 | ||||
|         // move date to end of day
 | ||||
|         $queryParameters['start']->startOfDay(); | ||||
|         $queryParameters['end']->endOfDay(); | ||||
| 
 | ||||
|         // prepare for currency conversion and data collection:
 | ||||
|         /** @var TransactionCurrency $default */ | ||||
|         $default         = app('amount')->getDefaultCurrency(); | ||||
|   | ||||
							
								
								
									
										111
									
								
								app/Api/V2/Controllers/JsonApi/AccountController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								app/Api/V2/Controllers/JsonApi/AccountController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| <?php | ||||
| /* | ||||
|  * AccountController.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Api\V2\Controllers\JsonApi; | ||||
| 
 | ||||
| use FireflyIII\Http\Controllers\Controller; | ||||
| use FireflyIII\JsonApi\V2\Accounts\AccountCollectionQuery; | ||||
| use FireflyIII\JsonApi\V2\Accounts\AccountSchema; | ||||
| use FireflyIII\JsonApi\V2\Accounts\AccountSingleQuery; | ||||
| use FireflyIII\Models\Account; | ||||
| use Illuminate\Contracts\Support\Responsable; | ||||
| use Illuminate\Http\Response; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use LaravelJsonApi\Core\Responses\DataResponse; | ||||
| use LaravelJsonApi\Laravel\Http\Controllers\Actions; | ||||
| 
 | ||||
| /** | ||||
|  * Class AccountController | ||||
|  * | ||||
|  * This class handles api/v2 requests for accounts. | ||||
|  * Most stuff is default stuff. | ||||
|  */ | ||||
| class AccountController extends Controller | ||||
| { | ||||
|     use Actions\AttachRelationship; | ||||
|     use Actions\Destroy; | ||||
|     use Actions\DetachRelationship; | ||||
| 
 | ||||
|     use Actions\FetchMany; | ||||
|     // use Actions\FetchOne;
 | ||||
|     use Actions\FetchRelated; | ||||
|     use Actions\FetchRelationship; | ||||
|     use Actions\Store; | ||||
|     use Actions\Update; | ||||
|     use Actions\UpdateRelationship; | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch zero to many JSON API resources. | ||||
|      * | ||||
|      * @return Responsable|Response | ||||
|      */ | ||||
|     public function index(AccountSchema $schema, AccountCollectionQuery $request) | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
|         $models = $schema | ||||
|             ->repository() | ||||
|             ->queryAll() | ||||
|             ->withRequest($request) | ||||
|             ->get() | ||||
|         ; | ||||
| 
 | ||||
|         // do something custom...
 | ||||
| 
 | ||||
|         return new DataResponse($models); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch zero to one JSON API resource by id. | ||||
|      * | ||||
|      * @return Responsable|Response | ||||
|      */ | ||||
|     public function show(AccountSchema $schema, AccountSingleQuery $request, Account $account) | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
|         $model = $schema->repository() | ||||
|             ->queryOne($account) | ||||
|             ->withRequest($request) | ||||
|             ->first() | ||||
|         ; | ||||
|         Log::debug(sprintf('%s again!', __METHOD__)); | ||||
| 
 | ||||
|         // do something custom...
 | ||||
| 
 | ||||
|         return new DataResponse($model); | ||||
|     } | ||||
| 
 | ||||
|     //    public function readAccountBalances(AnonymousQuery $query, AccountBalanceSchema $schema, Account $account): Responsable
 | ||||
|     //    {
 | ||||
|     //        $schema = JsonApi::server()->schemas()->schemaFor('account-balances');
 | ||||
|     //
 | ||||
|     //        $models = $schema
 | ||||
|     //            ->repository()
 | ||||
|     //            ->queryAll()
 | ||||
|     //            ->withRequest($query)
 | ||||
|     //            ->withAccount($account)
 | ||||
|     //            ->get()
 | ||||
|     //        ;
 | ||||
|     //
 | ||||
|     //        return DataResponse::make($models);
 | ||||
|     //    }
 | ||||
| } | ||||
| @@ -23,15 +23,12 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Api\V2\Request\Autocomplete; | ||||
| 
 | ||||
| use FireflyIII\JsonApi\Rules\IsValidFilter; | ||||
| use FireflyIII\JsonApi\Rules\IsValidPage; | ||||
| use FireflyIII\Models\AccountType; | ||||
| use FireflyIII\Support\Http\Api\AccountFilter; | ||||
| use FireflyIII\Support\Http\Api\ParsesQueryFilters; | ||||
| use FireflyIII\Support\Request\ChecksLogin; | ||||
| use FireflyIII\Support\Request\ConvertsDataTypes; | ||||
| use Illuminate\Foundation\Http\FormRequest; | ||||
| use LaravelJsonApi\Core\Query\QueryParameters; | ||||
| use LaravelJsonApi\Validation\Rule as JsonApiRule; | ||||
| 
 | ||||
| /** | ||||
|  * Class AutocompleteRequest | ||||
| @@ -51,35 +48,46 @@ class AutocompleteRequest extends FormRequest | ||||
|      */ | ||||
|     public function getParameters(): array | ||||
|     { | ||||
|         $queryParameters = QueryParameters::cast($this->all()); | ||||
| 
 | ||||
|         return [ | ||||
|             'date'          => $this->dateOrToday($queryParameters, 'date'), | ||||
|             'query'         => $this->arrayOfStrings($queryParameters, 'query'), | ||||
|             'size'          => $this->integerFromQueryParams($queryParameters, 'size', 50), | ||||
|             'account_types' => $this->getAccountTypeParameter($this->arrayOfStrings($queryParameters, 'account_types')), | ||||
|         $array                  = [ | ||||
|             'date'              => $this->convertDateTime('date'), | ||||
|             'query'             => $this->clearString((string) $this->get('query')), | ||||
|             'size'              => $this->integerFromValue('size'), | ||||
|             'page'              => $this->integerFromValue('page'), | ||||
|             'account_types'     => $this->arrayFromValue($this->get('account_types')), | ||||
|             'transaction_types' => $this->arrayFromValue($this->get('transaction_types')), | ||||
|         ]; | ||||
|         $array['size']          = $array['size'] < 1 || $array['size'] > 100 ? 15 : $array['size']; | ||||
|         $array['page']          = max($array['page'], 1); | ||||
|         if (null === $array['account_types']) { | ||||
|             $array['account_types'] = []; | ||||
|         } | ||||
|         if (null === $array['transaction_types']) { | ||||
|             $array['transaction_types'] = []; | ||||
|         } | ||||
| 
 | ||||
|         // remove 'initial balance' from allowed types. its internal
 | ||||
|         $array['account_types'] = array_diff($array['account_types'], [AccountType::INITIAL_BALANCE, AccountType::RECONCILIATION, AccountType::CREDITCARD]); | ||||
|         $array['account_types'] = $this->getAccountTypeParameter($array['account_types']); | ||||
| 
 | ||||
|         return $array; | ||||
|     } | ||||
| 
 | ||||
|     public function rules(): array | ||||
|     { | ||||
|         $valid = array_keys($this->types); | ||||
| 
 | ||||
|         return [ | ||||
|             'fields'  => JsonApiRule::notSupported(), | ||||
|             'filter'  => ['nullable', 'array', new IsValidFilter(['query', 'date', 'account_types'])], | ||||
|             'include' => JsonApiRule::notSupported(), | ||||
|             'page'    => ['nullable', 'array', new IsValidPage(['size'])], | ||||
|             'sort'    => JsonApiRule::notSupported(), | ||||
|             'date'              => 'nullable|date|after:1900-01-01|before:2100-01-01', | ||||
|             'query'             => 'nullable|string', | ||||
|             'size'              => 'nullable|integer|min:1|max:100', | ||||
|             'page'              => 'nullable|integer|min:1', | ||||
|             'account_types'     => sprintf('nullable|in:%s', implode(',', $valid)), | ||||
|             'transaction_types' => 'nullable|in:todo', | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     private function getAccountTypeParameter(mixed $types): array | ||||
|     private function getAccountTypeParameter(array $types): array | ||||
|     { | ||||
|         if (is_string($types) && str_contains($types, ',')) { | ||||
|             $types = explode(',', $types); | ||||
|         } | ||||
|         if (!is_iterable($types)) { | ||||
|             $types = [$types]; | ||||
|         } | ||||
|         $return = []; | ||||
|         foreach ($types as $type) { | ||||
|             $return = array_merge($return, $this->mapAccountTypes($type)); | ||||
|   | ||||
| @@ -24,8 +24,6 @@ declare(strict_types=1); | ||||
| namespace FireflyIII\Api\V2\Request\Chart; | ||||
| 
 | ||||
| use FireflyIII\Enums\UserRoleEnum; | ||||
| use FireflyIII\JsonApi\Rules\IsValidFilter; | ||||
| use FireflyIII\Rules\IsFilterValueIn; | ||||
| use FireflyIII\Support\Http\Api\ParsesQueryFilters; | ||||
| use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; | ||||
| use FireflyIII\Support\Request\ChecksLogin; | ||||
| @@ -33,8 +31,6 @@ use FireflyIII\Support\Request\ConvertsDataTypes; | ||||
| use Illuminate\Foundation\Http\FormRequest; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use Illuminate\Validation\Validator; | ||||
| use LaravelJsonApi\Core\Query\QueryParameters; | ||||
| use LaravelJsonApi\Validation\Rule as JsonApiRule; | ||||
| 
 | ||||
| /** | ||||
|  * Class ChartRequest | ||||
| @@ -50,50 +46,29 @@ class ChartRequest extends FormRequest | ||||
| 
 | ||||
|     public function getParameters(): array | ||||
|     { | ||||
|         $queryParameters = QueryParameters::cast($this->all()); | ||||
| 
 | ||||
|         // $queryParameters = QueryParameters::cast($this->all());
 | ||||
|         return [ | ||||
|             'start'       => $this->dateOrToday($queryParameters, 'start'), | ||||
|             'end'         => $this->dateOrToday($queryParameters, 'end'), | ||||
|             'preselected' => $this->stringFromQueryParams($queryParameters, 'preselected', 'empty'), | ||||
|             'period'      => $this->stringFromQueryParams($queryParameters, 'period', '1M'), | ||||
|             'accounts'    => $this->arrayOfStrings($queryParameters, 'accounts'), | ||||
|             // preselected heeft maar een paar toegestane waardes, dat moet ook goed gaan.
 | ||||
|             //            'query'         => $this->arrayOfStrings($queryParameters, 'query'),
 | ||||
|             //            'size'          => $this->integerFromQueryParams($queryParameters,'size', 50),
 | ||||
|             //            'account_types' => $this->getAccountTypeParameter($this->arrayOfStrings($queryParameters, 'account_types')),
 | ||||
|             'start'       => $this->convertDateTime('start')?->startOfDay(), | ||||
|             'end'         => $this->convertDateTime('end')?->endOfDay(), | ||||
|             'preselected' => $this->convertString('preselected', 'empty'), | ||||
|             'period'      => $this->convertString('period', '1M'), | ||||
|             'accounts'    => $this->arrayFromValue($this->get('accounts')), | ||||
|         ]; | ||||
|         // collect accounts based on this list?
 | ||||
|     } | ||||
| 
 | ||||
|     //        return [
 | ||||
|     //            'accounts'    => $this->getAccountList(),
 | ||||
|     //            'preselected' => $this->convertString('preselected'),
 | ||||
|     //        ];
 | ||||
|     //    }
 | ||||
| 
 | ||||
|     /** | ||||
|      * The rules that the incoming request must be matched against. | ||||
|      */ | ||||
|     public function rules(): array | ||||
|     { | ||||
|         return [ | ||||
|             'fields'  => JsonApiRule::notSupported(), | ||||
|             'filter'  => ['nullable', 'array', | ||||
|                 new IsValidFilter(['start', 'end', 'preselected', 'accounts']), | ||||
|                 new IsFilterValueIn('preselected', config('firefly.preselected_accounts')), | ||||
|             ], | ||||
|             'include' => JsonApiRule::notSupported(), | ||||
|             'page'    => JsonApiRule::notSupported(), | ||||
|             'sort'    => JsonApiRule::notSupported(), | ||||
|             'start'       => 'required|date|after:1900-01-01|before:2099-12-31|before_or_equal:end', | ||||
|             'end'         => 'required|date|after:1900-01-01|before:2099-12-31|after_or_equal:start', | ||||
|             'preselected' => sprintf('nullable|in:%s', implode(',', config('firefly.preselected_accounts'))), | ||||
|             'period'      => sprintf('nullable|in:%s', implode(',', config('firefly.valid_view_ranges'))), | ||||
|             'accounts.*'  => 'exists:accounts,id', | ||||
|         ]; | ||||
| 
 | ||||
|         //        return [
 | ||||
|         //            'start'       => 'required|date|after:1900-01-01|before:2099-12-31',
 | ||||
|         //            'end'         => 'required|date|after_or_equal:start|before:2099-12-31|after:1900-01-01',
 | ||||
|         //            'preselected' => sprintf('in:%s', implode(',', config('firefly.preselected_accounts'))),
 | ||||
|         //            'accounts.*'  => 'exists:accounts,id',
 | ||||
|         //        ];
 | ||||
|     } | ||||
| 
 | ||||
|     public function withValidator(Validator $validator): void | ||||
|   | ||||
| @@ -44,7 +44,7 @@ class DateRequest extends FormRequest | ||||
|     public function getAll(): array | ||||
|     { | ||||
|         return [ | ||||
|             'start' => $this->getCarbonDate('start'), | ||||
|             'start' => $this->getCarbonDate('start')->startOfDay(), | ||||
|             'end'   => $this->getCarbonDate('end')->endOfDay(), | ||||
|         ]; | ||||
|     } | ||||
|   | ||||
| @@ -147,7 +147,7 @@ class StoreRequest extends FormRequest | ||||
|                 // all custom fields:
 | ||||
|                 'internal_reference'    => $this->clearString((string)$object['internal_reference']), | ||||
|                 'external_id'           => $this->clearString((string)$object['external_id']), | ||||
|                 'original_source'       => sprintf('ff3-v%s|api-v%s', config('firefly.version'), config('firefly.api_version')), | ||||
|                 'original_source'       => sprintf('ff3-v%s', config('firefly.version')), | ||||
|                 'recurrence_id'         => $this->integerFromValue($object['recurrence_id']), | ||||
|                 'bunq_payment_id'       => $this->clearString((string)$object['bunq_payment_id']), | ||||
|                 'external_url'          => $this->clearString((string)$object['external_url']), | ||||
|   | ||||
| @@ -48,8 +48,19 @@ class StoreRequest extends FormRequest | ||||
| 
 | ||||
|     public function rules(): array | ||||
|     { | ||||
|         $roles  = []; | ||||
|         foreach (UserRoleEnum::cases() as $role) { | ||||
|             $roles[] = $role->value; | ||||
|         } | ||||
|         $string = implode(',', $roles); | ||||
| 
 | ||||
|         return [ | ||||
|             'title' => 'unique:user_groups,title|required|min:1|max:255', | ||||
|             'title'                => 'unique:user_groups,title|required|min:1|max:255', | ||||
|             'members'              => 'required|min:1', | ||||
|             'members.*.user_email' => 'email|missing_with:members.*.user_id', | ||||
|             'members.*.user_id'    => 'integer|exists:users,id|missing_with:members.*.user_email', | ||||
|             'members.*.roles'      => 'required|array|min:1', | ||||
|             'members.*.roles.*'    => sprintf('required|in:%s', $string), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										44
									
								
								app/Casts/SeparateTimezoneCaster.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/Casts/SeparateTimezoneCaster.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Casts; | ||||
| 
 | ||||
| use Carbon\Carbon; | ||||
| use Illuminate\Contracts\Database\Eloquent\CastsAttributes; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| 
 | ||||
| /** | ||||
|  * Class SeparateTimezoneCaster | ||||
|  * | ||||
|  * Checks if the object has a separate _tz value. If it does, it will use that timezone to parse the date. | ||||
|  * If it is NULL, it will use the system's timezone. | ||||
|  * | ||||
|  * At some point a user's database consists entirely of UTC dates, and we won't need this anymore. However, | ||||
|  * the completeness of this migration is not yet guaranteed. | ||||
|  */ | ||||
| class SeparateTimezoneCaster implements CastsAttributes | ||||
| { | ||||
|     /** | ||||
|      * @param array<string, mixed> $attributes | ||||
|      */ | ||||
|     public function get(Model $model, string $key, mixed $value, array $attributes): ?Carbon | ||||
|     { | ||||
|         if ('' === $value || null === $value) { | ||||
|             return null; | ||||
|         } | ||||
|         $timeZone = $attributes[sprintf('%s_tz', $key)] ?? config('app.timezone'); | ||||
| 
 | ||||
|         return Carbon::parse($value, $timeZone)->setTimezone(config('app.timezone')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepare the given value for storage. | ||||
|      * | ||||
|      * @param array<string, mixed> $attributes | ||||
|      */ | ||||
|     public function set(Model $model, string $key, mixed $value, array $attributes): mixed | ||||
|     { | ||||
|         return $value; | ||||
|     } | ||||
| } | ||||
| @@ -26,6 +26,8 @@ namespace FireflyIII\Console\Commands\Correction; | ||||
| use FireflyIII\Console\Commands\ShowsFriendlyMessages; | ||||
| use FireflyIII\Models\Transaction; | ||||
| use FireflyIII\Models\TransactionJournal; | ||||
| use FireflyIII\Models\TransactionType; | ||||
| use FireflyIII\Support\Models\AccountBalanceCalculator; | ||||
| use Illuminate\Console\Command; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| @@ -38,60 +40,21 @@ class FixUnevenAmount extends Command | ||||
| 
 | ||||
|     protected $description = 'Fix journals with uneven amounts.'; | ||||
|     protected $signature   = 'firefly-iii:fix-uneven-amount'; | ||||
|     private int $count; | ||||
| 
 | ||||
|     /** | ||||
|      * Execute the console command. | ||||
|      */ | ||||
|     public function handle(): int | ||||
|     { | ||||
|         $count    = 0; | ||||
|         $journals = \DB::table('transactions') | ||||
|             ->groupBy('transaction_journal_id') | ||||
|             ->whereNull('deleted_at') | ||||
|             ->get(['transaction_journal_id', \DB::raw('SUM(amount) AS the_sum')]) | ||||
|         ; | ||||
| 
 | ||||
|         /** @var \stdClass $entry */ | ||||
|         foreach ($journals as $entry) { | ||||
|             $sum = (string)$entry->the_sum; | ||||
|             if (!is_numeric($sum) | ||||
|                 || '' === $sum // @phpstan-ignore-line
 | ||||
|                 || str_contains($sum, 'e') | ||||
|                 || str_contains($sum, ',')) { | ||||
|                 $message = sprintf( | ||||
|                     'Journal #%d has an invalid sum ("%s"). No sure what to do.', | ||||
|                     $entry->transaction_journal_id, | ||||
|                     $entry->the_sum | ||||
|                 ); | ||||
|                 $this->friendlyWarning($message); | ||||
|                 app('log')->warning($message); | ||||
|                 ++$count; | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
|             $res = -1; | ||||
| 
 | ||||
|             try { | ||||
|                 $res = bccomp($sum, '0'); | ||||
|             } catch (\ValueError $e) { | ||||
|                 $this->friendlyError(sprintf('Could not bccomp("%s", "0").', $sum)); | ||||
|                 Log::error($e->getMessage()); | ||||
|                 Log::error($e->getTraceAsString()); | ||||
|             } | ||||
|             if (0 !== $res) { | ||||
|                 $message = sprintf( | ||||
|                     'Sum of journal #%d is %s instead of zero.', | ||||
|                     $entry->transaction_journal_id, | ||||
|                     $entry->the_sum | ||||
|                 ); | ||||
|                 $this->friendlyWarning($message); | ||||
|                 app('log')->warning($message); | ||||
|                 $this->fixJournal($entry->transaction_journal_id); | ||||
|                 ++$count; | ||||
|             } | ||||
|         } | ||||
|         if (0 === $count) { | ||||
|             $this->friendlyPositive('Database amount integrity is OK'); | ||||
|         $this->count = 0; | ||||
|         $this->convertOldStyleTransfers(); | ||||
|         $this->fixUnevenAmounts(); | ||||
|         $this->matchCurrencies(); | ||||
|         if (config('firefly.feature_flags.running_balance_column')) { | ||||
|             $this->friendlyInfo('Will recalculate transaction running balance columns. This may take a LONG time. Please be patient.'); | ||||
|             AccountBalanceCalculator::recalculateAll(true); | ||||
|             $this->friendlyInfo('Done recalculating transaction running balance columns.'); | ||||
|         } | ||||
| 
 | ||||
|         return 0; | ||||
| @@ -118,6 +81,7 @@ class FixUnevenAmount extends Command | ||||
|             ); | ||||
|             Transaction::where('transaction_journal_id', $journal->id ?? 0)->forceDelete(); | ||||
|             TransactionJournal::where('id', $journal->id ?? 0)->forceDelete(); | ||||
|             ++$this->count; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| @@ -139,14 +103,194 @@ class FixUnevenAmount extends Command | ||||
| 
 | ||||
|             Transaction::where('transaction_journal_id', $journal->id ?? 0)->forceDelete(); | ||||
|             TransactionJournal::where('id', $journal->id ?? 0)->forceDelete(); | ||||
|             ++$this->count; | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // may still be able to salvage this journal if it is a transfer with foreign currency info
 | ||||
|         if ($this->isForeignCurrencyTransfer($journal)) { | ||||
|             Log::debug(sprintf('Can skip foreign currency transfer #%d.', $journal->id)); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         $message             = sprintf('Sum of journal #%d is not zero, journal is broken and now fixed.', $journal->id); | ||||
| 
 | ||||
|         $this->friendlyWarning($message); | ||||
|         app('log')->warning($message); | ||||
| 
 | ||||
|         $destination->amount = $amount; | ||||
|         $destination->save(); | ||||
| 
 | ||||
|         $message             = sprintf('Corrected amount in transaction journal #%d', $param); | ||||
|         $this->friendlyInfo($message); | ||||
|         ++$this->count; | ||||
|     } | ||||
| 
 | ||||
|     private function fixUnevenAmounts(): void | ||||
|     { | ||||
|         $journals = \DB::table('transactions') | ||||
|             ->groupBy('transaction_journal_id') | ||||
|             ->whereNull('deleted_at') | ||||
|             ->get(['transaction_journal_id', \DB::raw('SUM(amount) AS the_sum')]) | ||||
|         ; | ||||
| 
 | ||||
|         /** @var \stdClass $entry */ | ||||
|         foreach ($journals as $entry) { | ||||
|             $sum = (string) $entry->the_sum; | ||||
|             if (!is_numeric($sum) | ||||
|                 || '' === $sum // @phpstan-ignore-line
 | ||||
|                 || str_contains($sum, 'e') | ||||
|                 || str_contains($sum, ',')) { | ||||
|                 $message = sprintf( | ||||
|                     'Journal #%d has an invalid sum ("%s"). No sure what to do.', | ||||
|                     $entry->transaction_journal_id, | ||||
|                     $entry->the_sum | ||||
|                 ); | ||||
|                 $this->friendlyWarning($message); | ||||
|                 app('log')->warning($message); | ||||
|                 ++$this->count; | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
|             $res = -1; | ||||
| 
 | ||||
|             try { | ||||
|                 $res = bccomp($sum, '0'); | ||||
|             } catch (\ValueError $e) { | ||||
|                 $this->friendlyError(sprintf('Could not bccomp("%s", "0").', $sum)); | ||||
|                 Log::error($e->getMessage()); | ||||
|                 Log::error($e->getTraceAsString()); | ||||
|             } | ||||
|             if (0 !== $res) { | ||||
|                 $this->fixJournal($entry->transaction_journal_id); | ||||
|             } | ||||
|         } | ||||
|         if (0 === $this->count) { | ||||
|             $this->friendlyPositive('Database amount integrity is OK'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private function matchCurrencies(): void | ||||
|     { | ||||
|         $journals = TransactionJournal::leftJoin('transactions', 'transaction_journals.id', 'transactions.transaction_journal_id') | ||||
|             ->where('transactions.transaction_currency_id', '!=', \DB::raw('transaction_journals.transaction_currency_id')) | ||||
|             ->get(['transaction_journals.*']) | ||||
|         ; | ||||
| 
 | ||||
|         $count    = 0; | ||||
| 
 | ||||
|         /** @var TransactionJournal $journal */ | ||||
|         foreach ($journals as $journal) { | ||||
|             if (!$this->isForeignCurrencyTransfer($journal)) { | ||||
|                 Transaction::where('transaction_journal_id', $journal->id)->update(['transaction_currency_id' => $journal->transaction_currency_id]); | ||||
|                 ++$count; | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
|             Log::debug(sprintf('Can skip foreign currency transfer #%d.', $journal->id)); | ||||
|         } | ||||
|         if (0 === $count) { | ||||
|             $this->friendlyPositive('Journal currency integrity is OK'); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         $this->friendlyPositive(sprintf('Fixed %d journal(s) with mismatched currencies.', $journals->count())); | ||||
|     } | ||||
| 
 | ||||
|     private function isForeignCurrencyTransfer(TransactionJournal $journal): bool | ||||
|     { | ||||
|         if (TransactionType::TRANSFER !== $journal->transactionType->type) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         /** @var Transaction $destination */ | ||||
|         $destination = $journal->transactions()->where('amount', '>', 0)->first(); | ||||
| 
 | ||||
|         /** @var Transaction $source */ | ||||
|         $source      = $journal->transactions()->where('amount', '<', 0)->first(); | ||||
| 
 | ||||
|         // safety catch on NULL should not be necessary, we just had that catch.
 | ||||
|         // source amount = dest foreign amount
 | ||||
|         // source currency = dest foreign currency
 | ||||
|         // dest amount = source foreign currency
 | ||||
|         // dest currency = source foreign currency
 | ||||
| 
 | ||||
|         //        Log::debug(sprintf('[a] %s', bccomp(app('steam')->positive($source->amount), app('steam')->positive($destination->foreign_amount))));
 | ||||
|         //        Log::debug(sprintf('[b] %s', bccomp(app('steam')->positive($destination->amount), app('steam')->positive($source->foreign_amount))));
 | ||||
|         //        Log::debug(sprintf('[c] %s', var_export($source->transaction_currency_id === $destination->foreign_currency_id,true)));
 | ||||
|         //        Log::debug(sprintf('[d] %s', var_export((int) $destination->transaction_currency_id ===(int)  $source->foreign_currency_id, true)));
 | ||||
| 
 | ||||
|         if (0 === bccomp(app('steam')->positive($source->amount), app('steam')->positive($destination->foreign_amount)) | ||||
|             && $source->transaction_currency_id === $destination->foreign_currency_id | ||||
|             && 0 === bccomp(app('steam')->positive($destination->amount), app('steam')->positive($source->foreign_amount)) | ||||
|             && (int) $destination->transaction_currency_id === (int) $source->foreign_currency_id | ||||
|         ) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     private function convertOldStyleTransfers(): void | ||||
|     { | ||||
|         Log::debug('convertOldStyleTransfers()'); | ||||
|         // select transactions with a foreign amount and a foreign currency. and it's a transfer. and they are different.
 | ||||
|         $transactions = Transaction::distinct() | ||||
|             ->whereNotNull('foreign_currency_id') | ||||
|             ->whereNotNull('foreign_amount')->get(['transactions.transaction_journal_id']) | ||||
|         ; | ||||
|         $count        = 0; | ||||
| 
 | ||||
|         Log::debug(sprintf('Found %d potential journal(s)', $transactions->count())); | ||||
| 
 | ||||
|         /** @var Transaction $transaction */ | ||||
|         foreach ($transactions as $transaction) { | ||||
|             /** @var null|TransactionJournal $journal */ | ||||
|             $journal     = TransactionJournal::find($transaction->transaction_journal_id); | ||||
|             if (null === $journal) { | ||||
|                 Log::debug('Found no journal, continue.'); | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
|             // needs to be a transfer.
 | ||||
|             if (TransactionType::TRANSFER !== $journal->transactionType->type) { | ||||
|                 Log::debug('Must be a transfer, continue.'); | ||||
| 
 | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             /** @var null|Transaction $destination */ | ||||
|             $destination = $journal->transactions()->where('amount', '>', 0)->first(); | ||||
| 
 | ||||
|             /** @var null|Transaction $source */ | ||||
|             $source      = $journal->transactions()->where('amount', '<', 0)->first(); | ||||
|             if (null === $destination || null === $source) { | ||||
|                 Log::debug('Source or destination transaction is NULL, continue.'); | ||||
| 
 | ||||
|                 // will be picked up later.
 | ||||
|                 continue; | ||||
|             } | ||||
|             if ($source->transaction_currency_id === $destination->transaction_currency_id) { | ||||
|                 Log::debug('Ready to swap data between transactions.'); | ||||
|                 $destination->foreign_currency_id     = $source->transaction_currency_id; | ||||
|                 $destination->foreign_amount          = app('steam')->positive($source->amount); | ||||
|                 $destination->transaction_currency_id = $source->foreign_currency_id; | ||||
|                 $destination->amount                  = app('steam')->positive($source->foreign_amount); | ||||
|                 $destination->balance_dirty           = true; | ||||
|                 $source->balance_dirty                = true; | ||||
|                 $destination->save(); | ||||
|                 $source->save(); | ||||
|                 $this->friendlyWarning(sprintf('Corrected foreign amounts of transfer #%d.', $journal->id)); | ||||
|                 ++$count; | ||||
|             } | ||||
|         } | ||||
|         if (0 === $count) { | ||||
|             $this->friendlyPositive('No "old style" foreign currency transfers.'); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										117
									
								
								app/Console/Commands/Integrity/AddTimezonesToDates.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								app/Console/Commands/Integrity/AddTimezonesToDates.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| /* | ||||
|  * AddTimezonesToDates.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| namespace FireflyIII\Console\Commands\Integrity; | ||||
| 
 | ||||
| use FireflyIII\Console\Commands\ShowsFriendlyMessages; | ||||
| use FireflyIII\Models\AccountBalance; | ||||
| use FireflyIII\Models\AvailableBudget; | ||||
| use FireflyIII\Models\Bill; | ||||
| use FireflyIII\Models\BudgetLimit; | ||||
| use FireflyIII\Models\CurrencyExchangeRate; | ||||
| use FireflyIII\Models\InvitedUser; | ||||
| use FireflyIII\Models\PiggyBank; | ||||
| use FireflyIII\Models\PiggyBankEvent; | ||||
| use FireflyIII\Models\PiggyBankRepetition; | ||||
| use FireflyIII\Models\Recurrence; | ||||
| use FireflyIII\Models\Tag; | ||||
| use FireflyIII\Models\TransactionJournal; | ||||
| use FireflyIII\Support\Facades\FireflyConfig; | ||||
| use Illuminate\Console\Command; | ||||
| use Illuminate\Database\QueryException; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| class AddTimezonesToDates extends Command | ||||
| { | ||||
|     use ShowsFriendlyMessages; | ||||
| 
 | ||||
|     /** | ||||
|      * The name and signature of the console command. | ||||
|      * | ||||
|      * @var string | ||||
|      */ | ||||
|     protected $signature        = 'firefly-iii:add-timezones-to-dates'; | ||||
| 
 | ||||
|     /** | ||||
|      * The console command description. | ||||
|      * | ||||
|      * @var string | ||||
|      */ | ||||
|     protected $description      = 'Make sure all dates have a timezone.'; | ||||
| 
 | ||||
|     public static array $models = [ | ||||
|         AccountBalance::class       => ['date'], // done
 | ||||
|         AvailableBudget::class      => ['start_date', 'end_date'], // done
 | ||||
|         Bill::class                 => ['date', 'end_date', 'extension_date'], // done
 | ||||
|         BudgetLimit::class          => ['start_date', 'end_date'], // done
 | ||||
|         CurrencyExchangeRate::class => ['date'], // done
 | ||||
|         InvitedUser::class          => ['expires'], | ||||
|         PiggyBankEvent::class       => ['date'], | ||||
|         PiggyBankRepetition::class  => ['startdate', 'targetdate'], | ||||
|         PiggyBank::class            => ['startdate', 'targetdate'], // done
 | ||||
|         Recurrence::class           => ['first_date', 'repeat_until', 'latest_date'], | ||||
|         Tag::class                  => ['date'], | ||||
|         TransactionJournal::class   => ['date'], | ||||
|     ]; | ||||
| 
 | ||||
|     /** | ||||
|      * Execute the console command. | ||||
|      */ | ||||
|     public function handle(): void | ||||
|     { | ||||
|         foreach (self::$models as $model => $fields) { | ||||
|             $this->addTimezoneToModel($model, $fields); | ||||
|         } | ||||
|         // not yet in UTC mode
 | ||||
|         FireflyConfig::set('utc', false); | ||||
|     } | ||||
| 
 | ||||
|     private function addTimezoneToModel(string $model, array $fields): void | ||||
|     { | ||||
|         foreach ($fields as $field) { | ||||
|             $this->addTimezoneToModelField($model, $field); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private function addTimezoneToModelField(string $model, string $field): void | ||||
|     { | ||||
|         $shortModel    = str_replace('FireflyIII\Models\\', '', $model); | ||||
|         $timezoneField = sprintf('%s_tz', $field); | ||||
|         $count         = 0; | ||||
| 
 | ||||
|         try { | ||||
|             $count = $model::whereNull($timezoneField)->count(); | ||||
|         } catch (QueryException $e) { | ||||
|             $this->friendlyError(sprintf('Cannot add timezone information to field "%s" of model "%s". Field does not exist.', $field, $shortModel)); | ||||
|             Log::error($e->getMessage()); | ||||
|         } | ||||
|         if (0 === $count) { | ||||
|             $this->friendlyPositive(sprintf('Timezone information is present in field "%s" of model "%s".', $field, $shortModel)); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
|         $this->friendlyInfo(sprintf('Adding timezone information to field "%s" of model "%s".', $field, $shortModel)); | ||||
| 
 | ||||
|         $model::whereNull($timezoneField)->update([$timezoneField => config('app.timezone')]); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										109
									
								
								app/Console/Commands/Integrity/ConvertDatesToUTC.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								app/Console/Commands/Integrity/ConvertDatesToUTC.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| /* | ||||
|  * ConvertDatesToUTC.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| namespace FireflyIII\Console\Commands\Integrity; | ||||
| 
 | ||||
| use Carbon\Carbon; | ||||
| use FireflyIII\Console\Commands\ShowsFriendlyMessages; | ||||
| use FireflyIII\Support\Facades\FireflyConfig; | ||||
| use Illuminate\Console\Command; | ||||
| use Illuminate\Database\QueryException; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| class ConvertDatesToUTC extends Command | ||||
| { | ||||
|     use ShowsFriendlyMessages; | ||||
| 
 | ||||
|     /** | ||||
|      * The name and signature of the console command. | ||||
|      * | ||||
|      * @var string | ||||
|      */ | ||||
|     protected $signature   = 'firefly-iii:migrate-to-utc'; | ||||
| 
 | ||||
|     /** | ||||
|      * The console command description. | ||||
|      * | ||||
|      * @var string | ||||
|      */ | ||||
|     protected $description = 'Convert stored dates to UTC.'; | ||||
| 
 | ||||
|     /** | ||||
|      * Execute the console command. | ||||
|      */ | ||||
|     public function handle(): int | ||||
|     { | ||||
|         /** | ||||
|          * @var string $model | ||||
|          * @var array  $fields | ||||
|          */ | ||||
|         foreach (AddTimezonesToDates::$models as $model => $fields) { | ||||
|             $this->ConvertModeltoUTC($model, $fields); | ||||
|         } | ||||
|         // tell the system we are now in UTC mode.
 | ||||
|         FireflyConfig::set('utc', true); | ||||
| 
 | ||||
|         return Command::SUCCESS; | ||||
|     } | ||||
| 
 | ||||
|     private function ConvertModeltoUTC(string $model, array $fields): void | ||||
|     { | ||||
|         /** @var string $field */ | ||||
|         foreach ($fields as $field) { | ||||
|             $this->convertFieldtoUTC($model, $field); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private function convertFieldtoUTC(string $model, string $field): void | ||||
|     { | ||||
|         $this->info(sprintf('Converting %s.%s to UTC', $model, $field)); | ||||
|         $shortModel    = str_replace('FireflyIII\Models\\', '', $model); | ||||
|         $timezoneField = sprintf('%s_tz', $field); | ||||
|         $items         = new Collection(); | ||||
|         $timeZone      = config('app.timezone'); | ||||
| 
 | ||||
|         try { | ||||
|             $items = $model::where($timezoneField, $timeZone)->get(); | ||||
|         } catch (QueryException $e) { | ||||
|             $this->friendlyError(sprintf('Cannot find timezone information to field "%s" of model "%s". Field does not exist.', $field, $shortModel)); | ||||
|             Log::error($e->getMessage()); | ||||
|         } | ||||
|         if (0 === $items->count()) { | ||||
|             $this->friendlyPositive(sprintf('All timezone information is UTC in field "%s" of model "%s".', $field, $shortModel)); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
|         $this->friendlyInfo(sprintf('Converting field "%s" of model "%s" to UTC.', $field, $shortModel)); | ||||
|         $items->each( | ||||
|             function ($item) use ($field, $timezoneField): void { | ||||
|                 /** @var Carbon $date */ | ||||
|                 $date                   = Carbon::parse($item->{$field}, $item->{$timezoneField}); | ||||
|                 $date->setTimezone('UTC'); | ||||
|                 $item->{$field}         = $date->format('Y-m-d H:i:s'); | ||||
|                 $item->{$timezoneField} = 'UTC'; | ||||
|                 $item->save(); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| @@ -47,6 +47,7 @@ class ReportIntegrity extends Command | ||||
|             return 1; | ||||
|         } | ||||
|         $commands = [ | ||||
|             'firefly-iii:add-timezones-to-dates', | ||||
|             'firefly-iii:create-group-memberships', | ||||
|             'firefly-iii:report-empty-objects', | ||||
|             'firefly-iii:report-sum', | ||||
|   | ||||
| @@ -58,7 +58,7 @@ class ReportSum extends Command | ||||
| 
 | ||||
|         /** @var User $user */ | ||||
|         foreach ($userRepository->all() as $user) { | ||||
|             $sum = (string)$user->transactions()->sum('amount'); | ||||
|             $sum = (string)$user->transactions()->selectRaw('SUM(amount) + SUM(foreign_amount) as total')->value('total'); | ||||
|             if (!is_numeric($sum)) { | ||||
|                 $message = sprintf('Error: Transactions for user #%d (%s) have an invalid sum ("%s").', $user->id, $user->email, $sum); | ||||
|                 $this->friendlyError($message); | ||||
|   | ||||
| @@ -80,7 +80,7 @@ class ForceMigration extends Command | ||||
|         sleep(2); | ||||
|         Schema::dropIfExists('migrations'); | ||||
|         $this->friendlyLine('Re-run all migrations...'); | ||||
|         Artisan::call('migrate', ['--seed' => true]); | ||||
|         Artisan::call('migrate', ['--seed' => true, '--force' => true]); | ||||
|         sleep(2); | ||||
|         $this->friendlyLine(''); | ||||
|         $this->friendlyWarning('There is a good chance you just saw a lot of error messages.'); | ||||
|   | ||||
| @@ -46,10 +46,15 @@ class Cron extends Command | ||||
|     protected $signature   = 'firefly-iii:cron | ||||
|         {--F|force : Force the cron job(s) to execute.} | ||||
|         {--date= : Set the date in YYYY-MM-DD to make Firefly III think that\'s the current date.} | ||||
|         {--download-cer : Download exchange rates. Other tasks will be skipped unless also requested.} | ||||
|         {--create-recurring : Create recurring transactions. Other tasks will be skipped unless also requested.} | ||||
|         {--create-auto-budgets : Create auto budgets. Other tasks will be skipped unless also requested.} | ||||
|         {--send-bill-warnings : Send bill warnings. Other tasks will be skipped unless also requested.} | ||||
|         '; | ||||
| 
 | ||||
|     public function handle(): int | ||||
|     { | ||||
|         $doAll = !$this->option('download-cer') && !$this->option('create-recurring') && !$this->option('create-auto-budgets') && !$this->option('send-bill-warnings'); | ||||
|         $date  = null; | ||||
| 
 | ||||
|         try { | ||||
| @@ -60,7 +65,7 @@ class Cron extends Command | ||||
|         $force = (bool)$this->option('force'); // @phpstan-ignore-line
 | ||||
| 
 | ||||
|         // Fire exchange rates cron job.
 | ||||
|         if (true === config('cer.download_enabled')) { | ||||
|         if (true === config('cer.download_enabled') && ($doAll || $this->option('download-cer'))) { | ||||
|             try { | ||||
|                 $this->exchangeRatesCronJob($force, $date); | ||||
|             } catch (FireflyException $e) { | ||||
| @@ -71,30 +76,36 @@ class Cron extends Command | ||||
|         } | ||||
| 
 | ||||
|         // Fire recurring transaction cron job.
 | ||||
|         try { | ||||
|             $this->recurringCronJob($force, $date); | ||||
|         } catch (FireflyException $e) { | ||||
|             app('log')->error($e->getMessage()); | ||||
|             app('log')->error($e->getTraceAsString()); | ||||
|             $this->friendlyError($e->getMessage()); | ||||
|         if ($doAll || $this->option('create-recurring')) { | ||||
|             try { | ||||
|                 $this->recurringCronJob($force, $date); | ||||
|             } catch (FireflyException $e) { | ||||
|                 app('log')->error($e->getMessage()); | ||||
|                 app('log')->error($e->getTraceAsString()); | ||||
|                 $this->friendlyError($e->getMessage()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Fire auto-budget cron job:
 | ||||
|         try { | ||||
|             $this->autoBudgetCronJob($force, $date); | ||||
|         } catch (FireflyException $e) { | ||||
|             app('log')->error($e->getMessage()); | ||||
|             app('log')->error($e->getTraceAsString()); | ||||
|             $this->friendlyError($e->getMessage()); | ||||
|         if ($doAll || $this->option('create-auto-budgets')) { | ||||
|             try { | ||||
|                 $this->autoBudgetCronJob($force, $date); | ||||
|             } catch (FireflyException $e) { | ||||
|                 app('log')->error($e->getMessage()); | ||||
|                 app('log')->error($e->getTraceAsString()); | ||||
|                 $this->friendlyError($e->getMessage()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Fire bill warning cron job
 | ||||
|         try { | ||||
|             $this->billWarningCronJob($force, $date); | ||||
|         } catch (FireflyException $e) { | ||||
|             app('log')->error($e->getMessage()); | ||||
|             app('log')->error($e->getTraceAsString()); | ||||
|             $this->friendlyError($e->getMessage()); | ||||
|         if ($doAll || $this->option('send-bill-warnings')) { | ||||
|             try { | ||||
|                 $this->billWarningCronJob($force, $date); | ||||
|             } catch (FireflyException $e) { | ||||
|                 app('log')->error($e->getMessage()); | ||||
|                 app('log')->error($e->getTraceAsString()); | ||||
|                 $this->friendlyError($e->getMessage()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         $this->friendlyInfo('More feedback on the cron jobs can be found in the log files.'); | ||||
|   | ||||
| @@ -33,6 +33,7 @@ use Illuminate\Console\Command; | ||||
| class CorrectAccountBalance extends Command | ||||
| { | ||||
|     use ShowsFriendlyMessages; | ||||
| 
 | ||||
|     public const string CONFIG_NAME = '610_correct_balances'; | ||||
|     protected $description          = 'Recalculate all account balance amounts'; | ||||
|     protected $signature            = 'firefly-iii:correct-account-balance {--F|force : Force the execution of this command.}'; | ||||
| @@ -44,22 +45,30 @@ class CorrectAccountBalance extends Command | ||||
| 
 | ||||
|             return 0; | ||||
|         } | ||||
|         $this->correctBalanceAmounts(); | ||||
|         $this->markAsExecuted(); | ||||
|         if (config('firefly.feature_flags.running_balance_column')) { | ||||
|             $this->friendlyInfo('Will recalculate account balances. This may take a LONG time. Please be patient.'); | ||||
|             $this->markAsExecuted(); | ||||
|             $this->correctBalanceAmounts(); | ||||
|             $this->friendlyInfo('Done recalculating account balances.'); | ||||
| 
 | ||||
|             return 0; | ||||
|         } | ||||
|         $this->friendlyWarning('This command has been disabled.'); | ||||
| 
 | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     private function correctBalanceAmounts(): void | ||||
|     { | ||||
|         AccountBalanceCalculator::recalculateAll(); | ||||
|         return; | ||||
|         AccountBalanceCalculator::recalculateAll(true); | ||||
|     } | ||||
| 
 | ||||
|     private function isExecuted(): bool | ||||
|     { | ||||
|         $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); | ||||
| 
 | ||||
|         return (bool)$configVar?->data; | ||||
|         return (bool) $configVar?->data; | ||||
|     } | ||||
| 
 | ||||
|     private function markAsExecuted(): void | ||||
|   | ||||
| @@ -47,7 +47,7 @@ class FixPostgresSequences extends Command | ||||
|             return 0; | ||||
|         } | ||||
|         $this->friendlyLine('Going to verify PostgreSQL table sequences.'); | ||||
|         $tablesToCheck = ['2fa_tokens', 'account_meta', 'account_types', 'accounts', 'attachments', 'auto_budgets', 'available_budgets', 'bills', 'budget_limits', 'budget_transaction', 'budget_transaction_journal', 'budgets', 'categories', 'category_transaction', 'category_transaction_journal', 'configuration', 'currency_exchange_rates', 'failed_jobs', 'group_journals', 'jobs', 'journal_links', 'journal_meta', 'limit_repetitions', 'link_types', 'locations', 'migrations', 'notes', 'oauth_clients', 'oauth_personal_access_clients', 'object_groups', 'permissions', 'piggy_bank_events', 'piggy_bank_repetitions', 'piggy_banks', 'preferences', 'recurrences', 'recurrences_meta', 'recurrences_repetitions', 'recurrences_transactions', 'roles', 'rt_meta', 'rule_actions', 'rule_groups', 'rule_triggers', 'rules', 'tag_transaction_journal', 'tags', 'transaction_currencies', 'transaction_groups', 'transaction_journals', 'transaction_types', 'transactions', 'users', 'webhook_attempts', 'webhook_messages', 'webhooks']; | ||||
|         $tablesToCheck = ['2fa_tokens', 'account_meta', 'account_types', 'accounts', 'attachments', 'auto_budgets', 'available_budgets', 'bills', 'budget_limits', 'budget_transaction', 'budget_transaction_journal', 'budgets', 'categories', 'category_transaction', 'category_transaction_journal', 'configuration', 'currency_exchange_rates', 'failed_jobs', 'group_journals', 'jobs', 'journal_links', 'journal_meta', 'link_types', 'locations', 'migrations', 'notes', 'oauth_clients', 'oauth_personal_access_clients', 'object_groups', 'permissions', 'piggy_bank_events', 'piggy_bank_repetitions', 'piggy_banks', 'preferences', 'recurrences', 'recurrences_meta', 'recurrences_repetitions', 'recurrences_transactions', 'roles', 'rt_meta', 'rule_actions', 'rule_groups', 'rule_triggers', 'rules', 'tag_transaction_journal', 'tags', 'transaction_currencies', 'transaction_groups', 'transaction_journals', 'transaction_types', 'transactions', 'users', 'webhook_attempts', 'webhook_messages', 'webhooks']; | ||||
| 
 | ||||
|         foreach ($tablesToCheck as $tableToCheck) { | ||||
|             $this->friendlyLine(sprintf('Checking the next id sequence for table "%s".', $tableToCheck)); | ||||
|   | ||||
| @@ -54,6 +54,7 @@ class MigrateRuleActions extends Command | ||||
|         } | ||||
|         $this->replaceEqualSign(); | ||||
|         $this->replaceObsoleteActions(); | ||||
|         $this->markAsExecuted(); | ||||
| 
 | ||||
|         return 0; | ||||
|     } | ||||
| @@ -179,4 +180,9 @@ class MigrateRuleActions extends Command | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private function markAsExecuted(): void | ||||
|     { | ||||
|         app('fireflyconfig')->set(self::CONFIG_NAME, true); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -90,7 +90,7 @@ class OtherCurrenciesCorrections extends Command | ||||
|     { | ||||
|         $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); | ||||
|         if (null !== $configVar) { | ||||
|             return (bool)$configVar->data; | ||||
|             return (bool) $configVar->data; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
| @@ -120,7 +120,7 @@ class OtherCurrenciesCorrections extends Command | ||||
|         $this->journalRepos->setUser($journal->user); | ||||
|         $this->cliRepos->setUser($journal->user); | ||||
| 
 | ||||
|         $leadTransaction                  = $this->getLeadTransaction($journal); | ||||
|         $leadTransaction = $this->getLeadTransaction($journal); | ||||
| 
 | ||||
|         if (null === $leadTransaction) { | ||||
|             $this->friendlyError(sprintf('Could not reliably determine which transaction is in the lead for transaction journal #%d.', $journal->id)); | ||||
| @@ -128,8 +128,9 @@ class OtherCurrenciesCorrections extends Command | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         $account                          = $leadTransaction->account; | ||||
|         $currency                         = $this->getCurrency($account); | ||||
|         $account         = $leadTransaction->account; | ||||
|         $currency        = $this->getCurrency($account); | ||||
|         $isMultiCurrency = $this->isMultiCurrency($account); | ||||
|         if (null === $currency) { | ||||
|             $this->friendlyError( | ||||
|                 sprintf( | ||||
| @@ -145,14 +146,14 @@ class OtherCurrenciesCorrections extends Command | ||||
|         } | ||||
|         // fix each transaction:
 | ||||
|         $journal->transactions->each( | ||||
|             static function (Transaction $transaction) use ($currency): void { | ||||
|             static function (Transaction $transaction) use ($currency, $isMultiCurrency): void { | ||||
|                 if (null === $transaction->transaction_currency_id) { | ||||
|                     $transaction->transaction_currency_id = $currency->id; | ||||
|                     $transaction->save(); | ||||
|                 } | ||||
| 
 | ||||
|                 // when mismatch in transaction:
 | ||||
|                 if ($transaction->transaction_currency_id !== $currency->id) { | ||||
|                 if ($transaction->transaction_currency_id !== $currency->id && !$isMultiCurrency) { | ||||
|                     $transaction->foreign_currency_id     = $transaction->transaction_currency_id; | ||||
|                     $transaction->foreign_amount          = $transaction->amount; | ||||
|                     $transaction->transaction_currency_id = $currency->id; | ||||
| @@ -161,7 +162,9 @@ class OtherCurrenciesCorrections extends Command | ||||
|             } | ||||
|         ); | ||||
|         // also update the journal, of course:
 | ||||
|         $journal->transaction_currency_id = $currency->id; | ||||
|         if (!$isMultiCurrency) { | ||||
|             $journal->transaction_currency_id = $currency->id; | ||||
|         } | ||||
|         ++$this->count; | ||||
|         $journal->save(); | ||||
|     } | ||||
| @@ -239,4 +242,14 @@ class OtherCurrenciesCorrections extends Command | ||||
|     { | ||||
|         app('fireflyconfig')->set(self::CONFIG_NAME, true); | ||||
|     } | ||||
| 
 | ||||
|     private function isMultiCurrency(Account $account): bool | ||||
|     { | ||||
|         $value = $this->accountRepos->getMetaValue($account, 'is_multi_currency', false); | ||||
|         if (false === $value || null === $value) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         return '1' === $value; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -154,7 +154,7 @@ class TransactionIdentifier extends Command | ||||
|             app('log')->error($e->getMessage()); | ||||
|             $this->friendlyError('Firefly III could not find the "identifier" field in the "transactions" table.'); | ||||
|             $this->friendlyError(sprintf('This field is required for Firefly III version %s to run.', config('firefly.version'))); | ||||
|             $this->friendlyError('Please run "php artisan migrate" to add this field to the table.'); | ||||
|             $this->friendlyError('Please run "php artisan migrate --force" to add this field to the table.'); | ||||
|             $this->friendlyError('Then, run "php artisan firefly:upgrade-database" to try again.'); | ||||
| 
 | ||||
|             return null; | ||||
|   | ||||
| @@ -67,6 +67,7 @@ class UpgradeDatabase extends Command | ||||
|             'firefly-iii:restore-oauth-keys', | ||||
|             'firefly-iii:correct-account-balance', | ||||
|             // also just in case, some integrity commands:
 | ||||
|             'firefly-iii:add-timezones-to-dates', | ||||
|             'firefly-iii:create-group-memberships', | ||||
|             'firefly-iii:upgrade-group-information', | ||||
|             'firefly-iii:upgrade-currency-preferences', | ||||
|   | ||||
| @@ -30,6 +30,7 @@ namespace FireflyIII\Enums; | ||||
| enum UserRoleEnum: string | ||||
| { | ||||
|     // most basic rights, cannot see other members, can see everything else.
 | ||||
|     // includes reading of metadata
 | ||||
|     case READ_ONLY            = 'ro'; | ||||
| 
 | ||||
|     // required to even USE the group properly (in this order)
 | ||||
| @@ -38,6 +39,15 @@ enum UserRoleEnum: string | ||||
|     // required to edit, add or change categories/tags/object-groups
 | ||||
|     case MANAGE_META          = 'mng_meta'; | ||||
| 
 | ||||
|     // read other objects and things.
 | ||||
|     case READ_BUDGETS         = 'read_budgets'; | ||||
|     case READ_PIGGY_BANKS     = 'read_piggies'; | ||||
|     case READ_SUBSCRIPTIONS   = 'read_subscriptions'; | ||||
|     case READ_RULES           = 'read_rules'; | ||||
|     case READ_RECURRING       = 'read_recurring'; | ||||
|     case READ_WEBHOOKS        = 'read_webhooks'; | ||||
|     case READ_CURRENCIES      = 'read_currencies'; | ||||
| 
 | ||||
|     // manage other financial objects:
 | ||||
|     case MANAGE_BUDGETS       = 'mng_budgets'; | ||||
|     case MANAGE_PIGGY_BANKS   = 'mng_piggies'; | ||||
|   | ||||
							
								
								
									
										43
									
								
								app/Events/Security/DisabledMFA.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/Events/Security/DisabledMFA.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| <?php | ||||
| /* | ||||
|  * EnabledMFA.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Events\Security; | ||||
| 
 | ||||
| use FireflyIII\Events\Event; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Contracts\Auth\Authenticatable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| 
 | ||||
| class DisabledMFA extends Event | ||||
| { | ||||
|     use SerializesModels; | ||||
| 
 | ||||
|     public User $user; | ||||
| 
 | ||||
|     public function __construct(null|Authenticatable|User $user) | ||||
|     { | ||||
|         if ($user instanceof User) { | ||||
|             $this->user = $user; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										43
									
								
								app/Events/Security/EnabledMFA.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/Events/Security/EnabledMFA.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| <?php | ||||
| /* | ||||
|  * EnabledMFA.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Events\Security; | ||||
| 
 | ||||
| use FireflyIII\Events\Event; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Contracts\Auth\Authenticatable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| 
 | ||||
| class EnabledMFA extends Event | ||||
| { | ||||
|     use SerializesModels; | ||||
| 
 | ||||
|     public User $user; | ||||
| 
 | ||||
|     public function __construct(null|Authenticatable|User $user) | ||||
|     { | ||||
|         if ($user instanceof User) { | ||||
|             $this->user = $user; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										45
									
								
								app/Events/Security/MFABackupFewLeft.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/Events/Security/MFABackupFewLeft.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| <?php | ||||
| /* | ||||
|  * EnabledMFA.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Events\Security; | ||||
| 
 | ||||
| use FireflyIII\Events\Event; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Contracts\Auth\Authenticatable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| 
 | ||||
| class MFABackupFewLeft extends Event | ||||
| { | ||||
|     use SerializesModels; | ||||
| 
 | ||||
|     public User $user; | ||||
|     public int $count; | ||||
| 
 | ||||
|     public function __construct(null|Authenticatable|User $user, int $count) | ||||
|     { | ||||
|         if ($user instanceof User) { | ||||
|             $this->user = $user; | ||||
|         } | ||||
|         $this->count = $count; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										43
									
								
								app/Events/Security/MFABackupNoLeft.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/Events/Security/MFABackupNoLeft.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| <?php | ||||
| /* | ||||
|  * EnabledMFA.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Events\Security; | ||||
| 
 | ||||
| use FireflyIII\Events\Event; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Contracts\Auth\Authenticatable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| 
 | ||||
| class MFABackupNoLeft extends Event | ||||
| { | ||||
|     use SerializesModels; | ||||
| 
 | ||||
|     public User $user; | ||||
| 
 | ||||
|     public function __construct(null|Authenticatable|User $user) | ||||
|     { | ||||
|         if ($user instanceof User) { | ||||
|             $this->user = $user; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										45
									
								
								app/Events/Security/MFAManyFailedAttempts.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/Events/Security/MFAManyFailedAttempts.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| <?php | ||||
| /* | ||||
|  * EnabledMFA.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Events\Security; | ||||
| 
 | ||||
| use FireflyIII\Events\Event; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Contracts\Auth\Authenticatable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| 
 | ||||
| class MFAManyFailedAttempts extends Event | ||||
| { | ||||
|     use SerializesModels; | ||||
| 
 | ||||
|     public User $user; | ||||
|     public int $count; | ||||
| 
 | ||||
|     public function __construct(null|Authenticatable|User $user, int $count) | ||||
|     { | ||||
|         if ($user instanceof User) { | ||||
|             $this->user = $user; | ||||
|         } | ||||
|         $this->count = $count; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										43
									
								
								app/Events/Security/MFANewBackupCodes.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/Events/Security/MFANewBackupCodes.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| <?php | ||||
| /* | ||||
|  * EnabledMFA.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Events\Security; | ||||
| 
 | ||||
| use FireflyIII\Events\Event; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Contracts\Auth\Authenticatable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| 
 | ||||
| class MFANewBackupCodes extends Event | ||||
| { | ||||
|     use SerializesModels; | ||||
| 
 | ||||
|     public User $user; | ||||
| 
 | ||||
|     public function __construct(null|Authenticatable|User $user) | ||||
|     { | ||||
|         if ($user instanceof User) { | ||||
|             $this->user = $user; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										43
									
								
								app/Events/Security/MFAUsedBackupCode.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/Events/Security/MFAUsedBackupCode.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| <?php | ||||
| /* | ||||
|  * EnabledMFA.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Events\Security; | ||||
| 
 | ||||
| use FireflyIII\Events\Event; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Contracts\Auth\Authenticatable; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| 
 | ||||
| class MFAUsedBackupCode extends Event | ||||
| { | ||||
|     use SerializesModels; | ||||
| 
 | ||||
|     public User $user; | ||||
| 
 | ||||
|     public function __construct(null|Authenticatable|User $user) | ||||
|     { | ||||
|         if ($user instanceof User) { | ||||
|             $this->user = $user; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -37,6 +37,7 @@ use Illuminate\Support\Arr; | ||||
| use Illuminate\Validation\ValidationException as LaravelValidationException; | ||||
| use Laravel\Passport\Exceptions\OAuthServerException as LaravelOAuthException; | ||||
| use LaravelJsonApi\Core\Exceptions\JsonApiException; | ||||
| use LaravelJsonApi\Exceptions\ExceptionParser; | ||||
| use League\OAuth2\Server\Exception\OAuthServerException; | ||||
| use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| @@ -67,6 +68,16 @@ class Handler extends ExceptionHandler | ||||
|             JsonApiException::class, | ||||
|         ]; | ||||
| 
 | ||||
|     /** | ||||
|      * Register the exception handling callbacks for the application. | ||||
|      */ | ||||
|     public function register(): void | ||||
|     { | ||||
|         $this->renderable( | ||||
|             ExceptionParser::make()->renderable() | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Render an exception into an HTTP response. It's complex but lucky for us, we never use it because | ||||
|      * Firefly III never crashes. | ||||
| @@ -81,15 +92,23 @@ class Handler extends ExceptionHandler | ||||
|     public function render($request, \Throwable $e): Response | ||||
|     { | ||||
|         $expectsJson = $request->expectsJson(); | ||||
|         // if the user requests anything /api/, assume the user wants to see JSON.
 | ||||
|         if (str_starts_with($request->getRequestUri(), '/api/')) { | ||||
|             app('log')->debug('API endpoint, always assume user wants JSON.'); | ||||
|             $expectsJson = true; | ||||
|         } | ||||
| 
 | ||||
|         app('log')->debug('Now in Handler::render()'); | ||||
| 
 | ||||
|         if ($e instanceof JsonApiException) { | ||||
|             // ignore it: controller will handle it.
 | ||||
| 
 | ||||
|             app('log')->debug(sprintf( | ||||
|                 'Return to parent to handle JsonApiException(%d)', | ||||
|                 $e->getCode() | ||||
|             )); | ||||
| 
 | ||||
|             return parent::render($request, $e); | ||||
|         } | ||||
| 
 | ||||
|         if ($e instanceof LaravelValidationException && $expectsJson) { | ||||
|             // ignore it: controller will handle it.
 | ||||
| 
 | ||||
|             app('log')->debug(sprintf('Return to parent to handle LaravelValidationException(%d)', $e->status)); | ||||
| 
 | ||||
|             return parent::render($request, $e); | ||||
|   | ||||
| @@ -58,20 +58,23 @@ class BillFactory | ||||
|             /** @var Bill $bill */ | ||||
|             $bill   = Bill::create( | ||||
|                 [ | ||||
|                     'name'                    => $data['name'], | ||||
|                     'match'                   => 'MIGRATED_TO_RULES', | ||||
|                     'amount_min'              => $data['amount_min'], | ||||
|                     'user_id'                 => $this->user->id, | ||||
|                     'user_group_id'           => $this->user->user_group_id, | ||||
|                     'transaction_currency_id' => $currency->id, | ||||
|                     'amount_max'              => $data['amount_max'], | ||||
|                     'date'                    => $data['date'], | ||||
|                     'end_date'                => $data['end_date'] ?? null, | ||||
|                     'extension_date'          => $data['extension_date'] ?? null, | ||||
|                     'repeat_freq'             => $data['repeat_freq'], | ||||
|                     'skip'                    => $skip, | ||||
|                     'automatch'               => true, | ||||
|                     'active'                  => $active, | ||||
|                     'name'                       => $data['name'], | ||||
|                     'match'                      => 'MIGRATED_TO_RULES', | ||||
|                     'amount_min'                 => $data['amount_min'], | ||||
|                     'user_id'                    => $this->user->id, | ||||
|                     'user_group_id'              => $this->user->user_group_id, | ||||
|                     'transaction_currency_id'    => $currency->id, | ||||
|                     'amount_max'                 => $data['amount_max'], | ||||
|                     'date'                       => $data['date'], | ||||
|                     'date_tz'                    => $data['date']->format('e'), | ||||
|                     'end_date'                   => $data['end_date'] ?? null, | ||||
|                     'end_date_tz'                => $data['end_date']?->format('e'), | ||||
|                     'extension_date'             => $data['extension_date'] ?? null, | ||||
|                     'extension_date_tz'          => $data['extension_date']?->format('e'), | ||||
|                     'repeat_freq'                => $data['repeat_freq'], | ||||
|                     'skip'                       => $skip, | ||||
|                     'automatch'                  => true, | ||||
|                     'active'                     => $active, | ||||
|                 ] | ||||
|             ); | ||||
|         } catch (QueryException $e) { | ||||
| @@ -126,7 +129,7 @@ class BillFactory | ||||
| 
 | ||||
|     public function findByName(string $name): ?Bill | ||||
|     { | ||||
|         return $this->user->bills()->where('name', 'LIKE', sprintf('%%%s%%', $name))->first(); | ||||
|         return $this->user->bills()->whereLike('name', sprintf('%%%s%%', $name))->first(); | ||||
|     } | ||||
| 
 | ||||
|     public function setUser(User $user): void | ||||
|   | ||||
| @@ -25,6 +25,7 @@ declare(strict_types=1); | ||||
| namespace FireflyIII\Factory; | ||||
| 
 | ||||
| use Carbon\Carbon; | ||||
| use FireflyIII\Enums\TransactionTypeEnum; | ||||
| use FireflyIII\Exceptions\DuplicateTransactionException; | ||||
| use FireflyIII\Exceptions\FireflyException; | ||||
| use FireflyIII\Models\Account; | ||||
| @@ -43,10 +44,12 @@ use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface; | ||||
| use FireflyIII\Repositories\UserGroups\Currency\CurrencyRepositoryInterface; | ||||
| use FireflyIII\Services\Internal\Destroy\JournalDestroyService; | ||||
| use FireflyIII\Services\Internal\Support\JournalServiceTrait; | ||||
| use FireflyIII\Support\Facades\FireflyConfig; | ||||
| use FireflyIII\Support\NullArrayObject; | ||||
| use FireflyIII\User; | ||||
| use FireflyIII\Validation\AccountValidator; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| /** | ||||
|  * Class TransactionJournalFactory | ||||
| @@ -156,7 +159,7 @@ class TransactionJournalFactory | ||||
| 
 | ||||
|         $this->errorIfDuplicate($row['import_hash_v2']); | ||||
| 
 | ||||
|         /** Some basic fields */ | ||||
|         // Some basic fields
 | ||||
|         $type                  = $this->typeRepository->findTransactionType(null, $row['type']); | ||||
|         $carbon                = $row['date'] ?? today(config('app.timezone')); | ||||
|         $order                 = $row['order'] ?? 0; | ||||
| @@ -169,6 +172,13 @@ class TransactionJournalFactory | ||||
|         // Manipulate basic fields
 | ||||
|         $carbon->setTimezone(config('app.timezone')); | ||||
| 
 | ||||
|         // 2024-11-19, overrule timezone with UTC and store it as UTC.
 | ||||
| 
 | ||||
|         if (FireflyConfig::get('utc', false)->data) { | ||||
|             $carbon->setTimezone('UTC'); | ||||
|         } | ||||
|         // $carbon->setTimezone('UTC');
 | ||||
| 
 | ||||
|         try { | ||||
|             // validate source and destination using a new Validator.
 | ||||
|             $this->validateAccounts($row); | ||||
| @@ -204,7 +214,7 @@ class TransactionJournalFactory | ||||
|         app('log')->debug('Done with getAccount(2x)'); | ||||
| 
 | ||||
|         // this is the moment for a reconciliation sanity check (again).
 | ||||
|         if (TransactionType::RECONCILIATION === $type->type) { | ||||
|         if (TransactionTypeEnum::RECONCILIATION->value === $type->type) { | ||||
|             [$sourceAccount, $destinationAccount] = $this->reconciliationSanityCheck($sourceAccount, $destinationAccount); | ||||
|         } | ||||
| 
 | ||||
| @@ -224,7 +234,8 @@ class TransactionJournalFactory | ||||
|                 'bill_id'                 => $billId, | ||||
|                 'transaction_currency_id' => $currency->id, | ||||
|                 'description'             => substr($description, 0, 1000), | ||||
|                 'date'                    => $carbon->format('Y-m-d H:i:s'), | ||||
|                 'date'                    => $carbon, | ||||
|                 'date_tz'                 => $carbon->format('e'), | ||||
|                 'order'                   => $order, | ||||
|                 'tag_count'               => 0, | ||||
|                 'completed'               => 0, | ||||
| @@ -261,8 +272,24 @@ class TransactionJournalFactory | ||||
|         $transactionFactory->setForeignCurrency($foreignCurrency); | ||||
|         $transactionFactory->setReconciled($row['reconciled'] ?? false); | ||||
| 
 | ||||
|         // if the foreign currency is set and is different, and the transaction type is a transfer,
 | ||||
|         // Firefly III will save the foreign currency information in such a way that both
 | ||||
|         // asset accounts can look at the "amount" and "transaction_currency_id" column and
 | ||||
|         // see the currency they expect to see.
 | ||||
|         $amount                = (string)$row['amount']; | ||||
|         $foreignAmount         = (string)$row['foreign_amount']; | ||||
|         if (null !== $foreignCurrency && $foreignCurrency->id !== $currency->id | ||||
|         && TransactionType::TRANSFER === $type->type | ||||
|         ) { | ||||
|             $transactionFactory->setCurrency($foreignCurrency); | ||||
|             $transactionFactory->setForeignCurrency($currency); | ||||
|             $amount        = (string)$row['foreign_amount']; | ||||
|             $foreignAmount = (string)$row['amount']; | ||||
|             Log::debug('Swap native/foreign amounts in transfer for new save method.'); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             $transactionFactory->createPositive((string)$row['amount'], (string)$row['foreign_amount']); | ||||
|             $transactionFactory->createPositive($amount, $foreignAmount); | ||||
|         } catch (FireflyException $e) { | ||||
|             app('log')->error(sprintf('Exception creating positive transaction: %s', $e->getMessage())); | ||||
|             $this->forceTrDelete($negative); | ||||
|   | ||||
| @@ -131,12 +131,14 @@ class BudgetLimitHandler | ||||
|                     app('log')->debug(sprintf('Will create AB for period %s to %s', $current->format('Y-m-d'), $currentEnd->format('Y-m-d'))); | ||||
|                     $availableBudget = new AvailableBudget( | ||||
|                         [ | ||||
|                             'user_id'                 => $budgetLimit->budget->user->id, | ||||
|                             'user_group_id'           => $budgetLimit->budget->user->user_group_id, | ||||
|                             'transaction_currency_id' => $budgetLimit->transaction_currency_id, | ||||
|                             'start_date'              => $current, | ||||
|                             'end_date'                => $currentEnd, | ||||
|                             'amount'                  => $amount, | ||||
|                             'user_id'                    => $budgetLimit->budget->user->id, | ||||
|                             'user_group_id'              => $budgetLimit->budget->user->user_group_id, | ||||
|                             'transaction_currency_id'    => $budgetLimit->transaction_currency_id, | ||||
|                             'start_date'                 => $current, | ||||
|                             'start_date_tz'              => $current->format('e'), | ||||
|                             'end_date'                   => $currentEnd, | ||||
|                             'end_date_tz'                => $currentEnd->format('e'), | ||||
|                             'amount'                     => $amount, | ||||
|                         ] | ||||
|                     ); | ||||
|                     $availableBudget->save(); | ||||
|   | ||||
| @@ -58,6 +58,7 @@ class PiggyBankEventHandler | ||||
|                 'piggy_bank_id'          => $event->piggyBank->id, | ||||
|                 'transaction_journal_id' => $journal?->id, | ||||
|                 'date'                   => $date->format('Y-m-d'), | ||||
|                 'date_tz'                => $date->format('e'), | ||||
|                 'amount'                 => $event->amount, | ||||
|             ] | ||||
|         ); | ||||
|   | ||||
							
								
								
									
										220
									
								
								app/Handlers/Events/Security/MFAHandler.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								app/Handlers/Events/Security/MFAHandler.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,220 @@ | ||||
| <?php | ||||
| /* | ||||
|  * MFAHandler.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Handlers\Events\Security; | ||||
| 
 | ||||
| use FireflyIII\Events\Security\DisabledMFA; | ||||
| use FireflyIII\Events\Security\EnabledMFA; | ||||
| use FireflyIII\Events\Security\MFABackupFewLeft; | ||||
| use FireflyIII\Events\Security\MFABackupNoLeft; | ||||
| use FireflyIII\Events\Security\MFAManyFailedAttempts; | ||||
| use FireflyIII\Events\Security\MFANewBackupCodes; | ||||
| use FireflyIII\Events\Security\MFAUsedBackupCode; | ||||
| use FireflyIII\Notifications\Security\DisabledMFANotification; | ||||
| use FireflyIII\Notifications\Security\EnabledMFANotification; | ||||
| use FireflyIII\Notifications\Security\MFABackupFewLeftNotification; | ||||
| use FireflyIII\Notifications\Security\MFABackupNoLeftNotification; | ||||
| use FireflyIII\Notifications\Security\MFAManyFailedAttemptsNotification; | ||||
| use FireflyIII\Notifications\Security\MFAUsedBackupCodeNotification; | ||||
| use FireflyIII\Notifications\Security\NewBackupCodesNotification; | ||||
| use Illuminate\Support\Facades\Notification; | ||||
| 
 | ||||
| class MFAHandler | ||||
| { | ||||
|     public function sendMFAEnabledMail(EnabledMFA $event): void | ||||
|     { | ||||
|         app('log')->debug(sprintf('Now in %s', __METHOD__)); | ||||
| 
 | ||||
|         $user = $event->user; | ||||
| 
 | ||||
|         try { | ||||
|             Notification::send($user, new EnabledMFANotification($user)); | ||||
|         } catch (\Exception $e) { // @phpstan-ignore-line
 | ||||
|             $message = $e->getMessage(); | ||||
|             if (str_contains($message, 'Bcc')) { | ||||
|                 app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             if (str_contains($message, 'RFC 2822')) { | ||||
|                 app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             app('log')->error($e->getMessage()); | ||||
|             app('log')->error($e->getTraceAsString()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public function sendNewMFABackupCodesMail(MFANewBackupCodes $event): void | ||||
|     { | ||||
|         app('log')->debug(sprintf('Now in %s', __METHOD__)); | ||||
| 
 | ||||
|         $user = $event->user; | ||||
| 
 | ||||
|         try { | ||||
|             Notification::send($user, new NewBackupCodesNotification($user)); | ||||
|         } catch (\Exception $e) { // @phpstan-ignore-line
 | ||||
|             $message = $e->getMessage(); | ||||
|             if (str_contains($message, 'Bcc')) { | ||||
|                 app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             if (str_contains($message, 'RFC 2822')) { | ||||
|                 app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             app('log')->error($e->getMessage()); | ||||
|             app('log')->error($e->getTraceAsString()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public function sendBackupFewLeftMail(MFABackupFewLeft $event): void | ||||
|     { | ||||
|         app('log')->debug(sprintf('Now in %s', __METHOD__)); | ||||
| 
 | ||||
|         $user  = $event->user; | ||||
|         $count = $event->count; | ||||
| 
 | ||||
|         try { | ||||
|             Notification::send($user, new MFABackupFewLeftNotification($user, $count)); | ||||
|         } catch (\Exception $e) { // @phpstan-ignore-line
 | ||||
|             $message = $e->getMessage(); | ||||
|             if (str_contains($message, 'Bcc')) { | ||||
|                 app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             if (str_contains($message, 'RFC 2822')) { | ||||
|                 app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             app('log')->error($e->getMessage()); | ||||
|             app('log')->error($e->getTraceAsString()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public function sendMFAFailedAttemptsMail(MFAManyFailedAttempts $event): void | ||||
|     { | ||||
|         app('log')->debug(sprintf('Now in %s', __METHOD__)); | ||||
| 
 | ||||
|         $user  = $event->user; | ||||
|         $count = $event->count; | ||||
| 
 | ||||
|         try { | ||||
|             Notification::send($user, new MFAManyFailedAttemptsNotification($user, $count)); | ||||
|         } catch (\Exception $e) { // @phpstan-ignore-line
 | ||||
|             $message = $e->getMessage(); | ||||
|             if (str_contains($message, 'Bcc')) { | ||||
|                 app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             if (str_contains($message, 'RFC 2822')) { | ||||
|                 app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             app('log')->error($e->getMessage()); | ||||
|             app('log')->error($e->getTraceAsString()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public function sendBackupNoLeftMail(MFABackupNoLeft $event): void | ||||
|     { | ||||
|         app('log')->debug(sprintf('Now in %s', __METHOD__)); | ||||
| 
 | ||||
|         $user = $event->user; | ||||
| 
 | ||||
|         try { | ||||
|             Notification::send($user, new MFABackupNoLeftNotification($user)); | ||||
|         } catch (\Exception $e) { // @phpstan-ignore-line
 | ||||
|             $message = $e->getMessage(); | ||||
|             if (str_contains($message, 'Bcc')) { | ||||
|                 app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             if (str_contains($message, 'RFC 2822')) { | ||||
|                 app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             app('log')->error($e->getMessage()); | ||||
|             app('log')->error($e->getTraceAsString()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public function sendUsedBackupCodeMail(MFAUsedBackupCode $event): void | ||||
|     { | ||||
|         app('log')->debug(sprintf('Now in %s', __METHOD__)); | ||||
| 
 | ||||
|         $user = $event->user; | ||||
| 
 | ||||
|         try { | ||||
|             Notification::send($user, new MFAUsedBackupCodeNotification($user)); | ||||
|         } catch (\Exception $e) { // @phpstan-ignore-line
 | ||||
|             $message = $e->getMessage(); | ||||
|             if (str_contains($message, 'Bcc')) { | ||||
|                 app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             if (str_contains($message, 'RFC 2822')) { | ||||
|                 app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             app('log')->error($e->getMessage()); | ||||
|             app('log')->error($e->getTraceAsString()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public function sendMFADisabledMail(DisabledMFA $event): void | ||||
|     { | ||||
|         app('log')->debug(sprintf('Now in %s', __METHOD__)); | ||||
| 
 | ||||
|         $user = $event->user; | ||||
| 
 | ||||
|         try { | ||||
|             Notification::send($user, new DisabledMFANotification($user)); | ||||
|         } catch (\Exception $e) { // @phpstan-ignore-line
 | ||||
|             $message = $e->getMessage(); | ||||
|             if (str_contains($message, 'Bcc')) { | ||||
|                 app('log')->warning('[Bcc] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             if (str_contains($message, 'RFC 2822')) { | ||||
|                 app('log')->warning('[RFC] Could not send notification. Please validate your email settings, use the .env.example file as a guide.'); | ||||
| 
 | ||||
|                 return; | ||||
|             } | ||||
|             app('log')->error($e->getMessage()); | ||||
|             app('log')->error($e->getTraceAsString()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -37,7 +37,9 @@ class PiggyBankObserver | ||||
|         $repetition                = new PiggyBankRepetition(); | ||||
|         $repetition->piggyBank()->associate($piggyBank); | ||||
|         $repetition->startdate     = $piggyBank->startdate; | ||||
|         $repetition->startdate_tz  = $piggyBank->startdate->format('e'); | ||||
|         $repetition->targetdate    = $piggyBank->targetdate; | ||||
|         $repetition->targetdate_tz = $piggyBank->targetdate?->format('e'); | ||||
|         $repetition->currentamount = '0'; | ||||
|         $repetition->save(); | ||||
|     } | ||||
|   | ||||
| @@ -25,6 +25,7 @@ namespace FireflyIII\Handlers\Observer; | ||||
| 
 | ||||
| use FireflyIII\Models\Transaction; | ||||
| use FireflyIII\Support\Models\AccountBalanceCalculator; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| /** | ||||
|  * Class TransactionObserver | ||||
| @@ -39,13 +40,19 @@ class TransactionObserver | ||||
| 
 | ||||
|     public function updated(Transaction $transaction): void | ||||
|     { | ||||
|         app('log')->debug('Observe "updated" of a transaction.'); | ||||
|         AccountBalanceCalculator::recalculateForJournal($transaction->transactionJournal); | ||||
|         Log::debug('Observe "updated" of a transaction.'); | ||||
|         if (1 === bccomp($transaction->amount, '0')) { | ||||
|             Log::debug('Trigger recalculateForJournal'); | ||||
|             AccountBalanceCalculator::recalculateForJournal($transaction->transactionJournal); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public function created(Transaction $transaction): void | ||||
|     { | ||||
|         app('log')->debug('Observe "created" of a transaction.'); | ||||
|         AccountBalanceCalculator::recalculateForJournal($transaction->transactionJournal); | ||||
|         Log::debug('Observe "created" of a transaction.'); | ||||
|         if (1 === bccomp($transaction->amount, '0')) { | ||||
|             Log::debug('Trigger recalculateForJournal'); | ||||
|             AccountBalanceCalculator::recalculateForJournal($transaction->transactionJournal); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -93,7 +93,7 @@ trait AttachmentCollection | ||||
|                 ->where( | ||||
|                     static function (EloquentBuilder $q1): void { // @phpstan-ignore-line
 | ||||
|                         $q1->where('attachments.attachable_type', TransactionJournal::class); | ||||
|                         $q1->where('attachments.uploaded', true); | ||||
|                         // $q1->where('attachments.uploaded', true);
 | ||||
|                         $q1->whereNull('attachments.deleted_at'); | ||||
|                         $q1->orWhereNull('attachments.attachable_type'); | ||||
|                     } | ||||
|   | ||||
| @@ -200,7 +200,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'internal_reference'); | ||||
|         $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $internalReference)); | ||||
|         $this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $internalReference)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -221,7 +221,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'external_id'); | ||||
|         $this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s%%', $externalId)); | ||||
|         $this->query->whereLike('journal_meta.data', sprintf('%%%s%%', $externalId)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -233,7 +233,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'external_id'); | ||||
|         $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $externalId)); | ||||
|         $this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $externalId)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -245,7 +245,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'external_id'); | ||||
|         $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s"', $externalId)); | ||||
|         $this->query->whereNotLike('journal_meta.data', sprintf('%%%s"', $externalId)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -257,7 +257,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'external_id'); | ||||
|         $this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $externalId)); | ||||
|         $this->query->whereLike('journal_meta.data', sprintf('"%s%%', $externalId)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -269,7 +269,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'external_id'); | ||||
|         $this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s"', $externalId)); | ||||
|         $this->query->whereLike('journal_meta.data', sprintf('%%%s"', $externalId)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -281,7 +281,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'external_id'); | ||||
|         $this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $externalId)); | ||||
|         $this->query->whereLike('journal_meta.data', sprintf('"%s%%', $externalId)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -292,7 +292,7 @@ trait MetaCollection | ||||
|         $url = (string)json_encode($url); | ||||
|         $url = str_replace('\\', '\\\\', trim($url, '"')); | ||||
|         $this->query->where('journal_meta.name', '=', 'external_url'); | ||||
|         $this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s%%', $url)); | ||||
|         $this->query->whereLike('journal_meta.data', sprintf('%%%s%%', $url)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -303,7 +303,7 @@ trait MetaCollection | ||||
|         $url = (string)json_encode($url); | ||||
|         $url = str_replace('\\', '\\\\', trim($url, '"')); | ||||
|         $this->query->where('journal_meta.name', '=', 'external_url'); | ||||
|         $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $url)); | ||||
|         $this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $url)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -314,7 +314,7 @@ trait MetaCollection | ||||
|         $url = (string)json_encode($url); | ||||
|         $url = str_replace('\\', '\\\\', ltrim($url, '"')); | ||||
|         $this->query->where('journal_meta.name', '=', 'external_url'); | ||||
|         $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s', $url)); | ||||
|         $this->query->whereNotLike('journal_meta.data', sprintf('%%%s', $url)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -327,7 +327,7 @@ trait MetaCollection | ||||
|         // var_dump($url);
 | ||||
| 
 | ||||
|         $this->query->where('journal_meta.name', '=', 'external_url'); | ||||
|         $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%s%%', $url)); | ||||
|         $this->query->whereNotLike('journal_meta.data', sprintf('%s%%', $url)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -338,7 +338,7 @@ trait MetaCollection | ||||
|         $url = (string)json_encode($url); | ||||
|         $url = str_replace('\\', '\\\\', ltrim($url, '"')); | ||||
|         $this->query->where('journal_meta.name', '=', 'external_url'); | ||||
|         $this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s', $url)); | ||||
|         $this->query->whereLike('journal_meta.data', sprintf('%%%s', $url)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -351,7 +351,7 @@ trait MetaCollection | ||||
|         // var_dump($url);
 | ||||
| 
 | ||||
|         $this->query->where('journal_meta.name', '=', 'external_url'); | ||||
|         $this->query->where('journal_meta.data', 'LIKE', sprintf('%s%%', $url)); | ||||
|         $this->query->whereLike('journal_meta.data', sprintf('%s%%', $url)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -404,7 +404,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'internal_reference'); | ||||
|         $this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s%%', $internalReference)); | ||||
|         $this->query->whereLike('journal_meta.data', sprintf('%%%s%%', $internalReference)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -416,7 +416,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'internal_reference'); | ||||
|         $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s%%', $internalReference)); | ||||
|         $this->query->whereNotLike('journal_meta.data', sprintf('%%%s%%', $internalReference)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -428,7 +428,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'internal_reference'); | ||||
|         $this->query->where('journal_meta.data', 'NOT LIKE', sprintf('%%%s"', $internalReference)); | ||||
|         $this->query->whereNotLike('journal_meta.data', sprintf('%%%s"', $internalReference)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -440,7 +440,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'internal_reference'); | ||||
|         $this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $internalReference)); | ||||
|         $this->query->whereLike('journal_meta.data', sprintf('"%s%%', $internalReference)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -452,7 +452,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'internal_reference'); | ||||
|         $this->query->where('journal_meta.data', 'LIKE', sprintf('%%%s"', $internalReference)); | ||||
|         $this->query->whereLike('journal_meta.data', sprintf('%%%s"', $internalReference)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -464,7 +464,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'internal_reference'); | ||||
|         $this->query->where('journal_meta.data', 'LIKE', sprintf('"%s%%', $internalReference)); | ||||
|         $this->query->whereLike('journal_meta.data', sprintf('"%s%%', $internalReference)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -472,7 +472,7 @@ trait MetaCollection | ||||
|     public function notesContain(string $value): GroupCollectorInterface | ||||
|     { | ||||
|         $this->withNotes(); | ||||
|         $this->query->where('notes.text', 'LIKE', sprintf('%%%s%%', $value)); | ||||
|         $this->query->whereLike('notes.text', sprintf('%%%s%%', $value)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -502,7 +502,7 @@ trait MetaCollection | ||||
|         $this->withNotes(); | ||||
|         $this->query->where(static function (Builder $q) use ($value): void { // @phpstan-ignore-line
 | ||||
|             $q->whereNull('notes.text'); | ||||
|             $q->orWhere('notes.text', 'NOT LIKE', sprintf('%%%s%%', $value)); | ||||
|             $q->orWhereNotLike('notes.text', sprintf('%%%s%%', $value)); | ||||
|         }); | ||||
| 
 | ||||
|         return $this; | ||||
| @@ -513,7 +513,7 @@ trait MetaCollection | ||||
|         $this->withNotes(); | ||||
|         $this->query->where(static function (Builder $q) use ($value): void { // @phpstan-ignore-line
 | ||||
|             $q->whereNull('notes.text'); | ||||
|             $q->orWhere('notes.text', 'NOT LIKE', sprintf('%%%s', $value)); | ||||
|             $q->orWhereNotLike('notes.text', sprintf('%%%s', $value)); | ||||
|         }); | ||||
| 
 | ||||
|         return $this; | ||||
| @@ -524,7 +524,7 @@ trait MetaCollection | ||||
|         $this->withNotes(); | ||||
|         $this->query->where(static function (Builder $q) use ($value): void { // @phpstan-ignore-line
 | ||||
|             $q->whereNull('notes.text'); | ||||
|             $q->orWhere('notes.text', 'NOT LIKE', sprintf('%s%%', $value)); | ||||
|             $q->orWhereNotLike('notes.text', sprintf('%s%%', $value)); | ||||
|         }); | ||||
| 
 | ||||
|         return $this; | ||||
| @@ -533,7 +533,7 @@ trait MetaCollection | ||||
|     public function notesEndWith(string $value): GroupCollectorInterface | ||||
|     { | ||||
|         $this->withNotes(); | ||||
|         $this->query->where('notes.text', 'LIKE', sprintf('%%%s', $value)); | ||||
|         $this->query->whereLike('notes.text', sprintf('%%%s', $value)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -560,7 +560,7 @@ trait MetaCollection | ||||
|     public function notesStartWith(string $value): GroupCollectorInterface | ||||
|     { | ||||
|         $this->withNotes(); | ||||
|         $this->query->where('notes.text', 'LIKE', sprintf('%s%%', $value)); | ||||
|         $this->query->whereLike('notes.text', sprintf('%s%%', $value)); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
| @@ -717,7 +717,7 @@ trait MetaCollection | ||||
| 
 | ||||
|         $this->joinMetaDataTables(); | ||||
|         $this->query->where('journal_meta.name', '=', 'internal_reference'); | ||||
|         $this->query->where('journal_meta.data', '=', $internalReference); | ||||
|         $this->query->where('journal_meta.data', '=', sprintf('%s', json_encode($internalReference))); | ||||
| 
 | ||||
|         return $this; | ||||
|     } | ||||
|   | ||||
| @@ -117,6 +117,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|             'transaction_journals.transaction_type_id', | ||||
|             'transaction_journals.description', | ||||
|             'transaction_journals.date', | ||||
|             'transaction_journals.date_tz', | ||||
|             'transaction_journals.order', | ||||
| 
 | ||||
|             // types
 | ||||
| @@ -156,7 +157,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     static function (EloquentBuilder $q1) use ($array): void { | ||||
|                         foreach ($array as $word) { | ||||
|                             $keyword = sprintf('%%%s', $word); | ||||
|                             $q1->where('transaction_journals.description', 'NOT LIKE', $keyword); | ||||
|                             $q1->whereNotLike('transaction_journals.description', $keyword); | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
| @@ -164,7 +165,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     static function (EloquentBuilder $q2) use ($array): void { | ||||
|                         foreach ($array as $word) { | ||||
|                             $keyword = sprintf('%%%s', $word); | ||||
|                             $q2->where('transaction_groups.title', 'NOT LIKE', $keyword); | ||||
|                             $q2->whereNotLike('transaction_groups.title', $keyword); | ||||
|                             $q2->orWhereNull('transaction_groups.title'); | ||||
|                         } | ||||
|                     } | ||||
| @@ -183,7 +184,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     static function (EloquentBuilder $q1) use ($array): void { | ||||
|                         foreach ($array as $word) { | ||||
|                             $keyword = sprintf('%s%%', $word); | ||||
|                             $q1->where('transaction_journals.description', 'NOT LIKE', $keyword); | ||||
|                             $q1->whereNotLike('transaction_journals.description', $keyword); | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
| @@ -191,7 +192,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     static function (EloquentBuilder $q2) use ($array): void { | ||||
|                         foreach ($array as $word) { | ||||
|                             $keyword = sprintf('%s%%', $word); | ||||
|                             $q2->where('transaction_groups.title', 'NOT LIKE', $keyword); | ||||
|                             $q2->whereNotLike('transaction_groups.title', $keyword); | ||||
|                             $q2->orWhereNull('transaction_groups.title'); | ||||
|                         } | ||||
|                     } | ||||
| @@ -210,7 +211,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     static function (EloquentBuilder $q1) use ($array): void { | ||||
|                         foreach ($array as $word) { | ||||
|                             $keyword = sprintf('%%%s', $word); | ||||
|                             $q1->where('transaction_journals.description', 'LIKE', $keyword); | ||||
|                             $q1->whereLike('transaction_journals.description', $keyword); | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
| @@ -218,7 +219,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     static function (EloquentBuilder $q2) use ($array): void { | ||||
|                         foreach ($array as $word) { | ||||
|                             $keyword = sprintf('%%%s', $word); | ||||
|                             $q2->where('transaction_groups.title', 'LIKE', $keyword); | ||||
|                             $q2->whereLike('transaction_groups.title', $keyword); | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
| @@ -265,7 +266,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     static function (EloquentBuilder $q1) use ($array): void { | ||||
|                         foreach ($array as $word) { | ||||
|                             $keyword = sprintf('%s%%', $word); | ||||
|                             $q1->where('transaction_journals.description', 'LIKE', $keyword); | ||||
|                             $q1->whereLike('transaction_journals.description', $keyword); | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
| @@ -273,7 +274,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     static function (EloquentBuilder $q2) use ($array): void { | ||||
|                         foreach ($array as $word) { | ||||
|                             $keyword = sprintf('%s%%', $word); | ||||
|                             $q2->where('transaction_groups.title', 'LIKE', $keyword); | ||||
|                             $q2->whereLike('transaction_groups.title', $keyword); | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
| @@ -379,7 +380,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     static function (EloquentBuilder $q1) use ($array): void { | ||||
|                         foreach ($array as $word) { | ||||
|                             $keyword = sprintf('%%%s%%', $word); | ||||
|                             $q1->where('transaction_journals.description', 'NOT LIKE', $keyword); | ||||
|                             $q1->whereNotLike('transaction_journals.description', $keyword); | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
| @@ -387,7 +388,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     static function (EloquentBuilder $q2) use ($array): void { | ||||
|                         foreach ($array as $word) { | ||||
|                             $keyword = sprintf('%%%s%%', $word); | ||||
|                             $q2->where('transaction_groups.title', 'NOT LIKE', $keyword); | ||||
|                             $q2->whereNotLike('transaction_groups.title', $keyword); | ||||
|                             $q2->orWhereNull('transaction_groups.title'); | ||||
|                         } | ||||
|                     } | ||||
| @@ -805,7 +806,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     return 'zzz'; | ||||
|                 } | ||||
| 
 | ||||
|                 exit('here we are'); | ||||
|                 exit('here we are 2'); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
| @@ -944,7 +945,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     static function (EloquentBuilder $q1) use ($array): void { | ||||
|                         foreach ($array as $word) { | ||||
|                             $keyword = sprintf('%%%s%%', $word); | ||||
|                             $q1->where('transaction_journals.description', 'LIKE', $keyword); | ||||
|                             $q1->whereLike('transaction_journals.description', $keyword); | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
| @@ -952,7 +953,7 @@ class GroupCollector implements GroupCollectorInterface | ||||
|                     static function (EloquentBuilder $q2) use ($array): void { | ||||
|                         foreach ($array as $word) { | ||||
|                             $keyword = sprintf('%%%s%%', $word); | ||||
|                             $q2->where('transaction_groups.title', 'LIKE', $keyword); | ||||
|                             $q2->whereLike('transaction_groups.title', $keyword); | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
|   | ||||
| @@ -103,7 +103,8 @@ class PopupReport implements PopupReportInterface | ||||
| 
 | ||||
|         /** @var GroupCollectorInterface $collector */ | ||||
|         $collector  = app(GroupCollectorInterface::class); | ||||
|         $collector->setAccounts($attributes['accounts']) | ||||
|         $collector | ||||
|             ->setAccounts($attributes['accounts']) | ||||
|             ->withAccountInformation() | ||||
|             ->withBudgetInformation() | ||||
|             ->withCategoryInformation() | ||||
| @@ -113,11 +114,10 @@ class PopupReport implements PopupReportInterface | ||||
|         if (null !== $currency) { | ||||
|             $collector->setCurrency($currency); | ||||
|         } | ||||
| 
 | ||||
|         if (null === $budget->id) { | ||||
|         if (null === $budget->id || 0 === $budget->id) { | ||||
|             $collector->setTypes([TransactionType::WITHDRAWAL])->withoutBudget(); | ||||
|         } | ||||
|         if (null !== $budget->id) { | ||||
|         if (null !== $budget->id && 0 !== $budget->id) { | ||||
|             $collector->setBudget($budget); | ||||
|         } | ||||
| 
 | ||||
|   | ||||
| @@ -1,43 +0,0 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Http\Controllers\Api\V3\Controllers; | ||||
| 
 | ||||
| use FireflyIII\Http\Controllers\Controller; | ||||
| use FireflyIII\JsonApi\V3\AccountBalances\AccountBalanceSchema; | ||||
| use FireflyIII\Models\Account; | ||||
| use Illuminate\Contracts\Support\Responsable; | ||||
| use LaravelJsonApi\Core\Facades\JsonApi; | ||||
| use LaravelJsonApi\Core\Responses\DataResponse; | ||||
| use LaravelJsonApi\Laravel\Http\Controllers\Actions; | ||||
| use LaravelJsonApi\Laravel\Http\Requests\AnonymousQuery; | ||||
| 
 | ||||
| class AccountController extends Controller | ||||
| { | ||||
|     use Actions\AttachRelationship; | ||||
|     use Actions\Destroy; | ||||
|     use Actions\DetachRelationship; | ||||
|     use Actions\FetchMany; | ||||
|     use Actions\FetchOne; | ||||
|     use Actions\FetchRelated; | ||||
|     use Actions\FetchRelationship; | ||||
|     use Actions\Store; | ||||
|     use Actions\Update; | ||||
|     use Actions\UpdateRelationship; | ||||
| 
 | ||||
|     public function readAccountBalances(AnonymousQuery $query, AccountBalanceSchema $schema, Account $account): Responsable | ||||
|     { | ||||
|         $schema = JsonApi::server()->schemas()->schemaFor('account-balances'); | ||||
| 
 | ||||
|         $models = $schema | ||||
|             ->repository() | ||||
|             ->queryAll() | ||||
|             ->withRequest($query) | ||||
|             ->withAccount($account) | ||||
|             ->get() | ||||
|         ; | ||||
| 
 | ||||
|         return DataResponse::make($models); | ||||
|     } | ||||
| } | ||||
| @@ -77,12 +77,16 @@ class LoginController extends Controller | ||||
|      */ | ||||
|     public function login(Request $request): JsonResponse|RedirectResponse | ||||
|     { | ||||
|         Log::channel('audit')->info(sprintf('User is trying to login using "%s"', $request->get($this->username()))); | ||||
|         $username = $request->get($this->username()); | ||||
|         Log::channel('audit')->info(sprintf('User is trying to login using "%s"', $username)); | ||||
|         app('log')->debug('User is trying to login.'); | ||||
| 
 | ||||
|         try { | ||||
|             $this->validateLogin($request); | ||||
|         } catch (ValidationException $e) { | ||||
|             // basic validation exception.
 | ||||
|             // report the failed login to the user if the count is 2 or 5.
 | ||||
|             // TODO here be warning.
 | ||||
|             return redirect(route('login')) | ||||
|                 ->withErrors( | ||||
|                     [ | ||||
|   | ||||
| @@ -23,6 +23,10 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Http\Controllers\Auth; | ||||
| 
 | ||||
| use FireflyIII\Events\Security\MFABackupFewLeft; | ||||
| use FireflyIII\Events\Security\MFABackupNoLeft; | ||||
| use FireflyIII\Events\Security\MFAManyFailedAttempts; | ||||
| use FireflyIII\Events\Security\MFAUsedBackupCode; | ||||
| use FireflyIII\Http\Controllers\Controller; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Contracts\View\Factory; | ||||
| @@ -30,6 +34,7 @@ use Illuminate\Contracts\View\View; | ||||
| use Illuminate\Http\RedirectResponse; | ||||
| use Illuminate\Http\Request; | ||||
| use Illuminate\Routing\Redirector; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use PragmaRX\Google2FALaravel\Support\Authenticator; | ||||
| 
 | ||||
| /** | ||||
| @@ -47,7 +52,7 @@ class TwoFactorController extends Controller | ||||
|         /** @var User $user */ | ||||
|         $user      = auth()->user(); | ||||
|         $siteOwner = config('firefly.site_owner'); | ||||
|         $title     = (string)trans('firefly.two_factor_forgot_title'); | ||||
|         $title     = (string) trans('firefly.two_factor_forgot_title'); | ||||
| 
 | ||||
|         return view('auth.lost-two-factor', compact('user', 'siteOwner', 'title')); | ||||
|     } | ||||
| @@ -59,7 +64,7 @@ class TwoFactorController extends Controller | ||||
|     { | ||||
|         /** @var array $mfaHistory */ | ||||
|         $mfaHistory    = app('preferences')->get('mfa_history', [])->data; | ||||
|         $mfaCode       = (string)$request->get('one_time_password'); | ||||
|         $mfaCode       = (string) $request->get('one_time_password'); | ||||
| 
 | ||||
|         // is in history? then refuse to use it.
 | ||||
|         if ($this->inMFAHistory($mfaCode, $mfaHistory)) { | ||||
| @@ -72,10 +77,26 @@ class TwoFactorController extends Controller | ||||
|         /** @var Authenticator $authenticator */ | ||||
|         $authenticator = app(Authenticator::class)->boot($request); | ||||
| 
 | ||||
|         // if not OK, save error.
 | ||||
|         if (!$authenticator->isAuthenticated()) { | ||||
|             $user    = auth()->user(); | ||||
|             $this->addToMFAFailureCounter(); | ||||
|             $counter = $this->getMFAFailureCounter(); | ||||
|             if (3 === $counter || 10 === $counter) { | ||||
|                 // do not reset MFA failure counter, but DO send a warning to the user.
 | ||||
|                 Log::channel('audit')->info(sprintf('User "%s" has had %d failed MFA attempts.', $user->email, $counter)); | ||||
|                 event(new MFAManyFailedAttempts($user, $counter)); | ||||
|             } | ||||
|             unset($user); | ||||
|         } | ||||
| 
 | ||||
|         if ($authenticator->isAuthenticated()) { | ||||
|             // save MFA in preferences
 | ||||
|             $this->addToMFAHistory($mfaCode); | ||||
| 
 | ||||
|             // reset failure count
 | ||||
|             $this->resetMFAFailureCounter(); | ||||
| 
 | ||||
|             // otp auth success!
 | ||||
|             return redirect(route('home')); | ||||
|         } | ||||
| @@ -85,7 +106,14 @@ class TwoFactorController extends Controller | ||||
|             $this->removeFromBackupCodes($mfaCode); | ||||
|             $authenticator->login(); | ||||
| 
 | ||||
|             // reset failure count
 | ||||
|             $this->resetMFAFailureCounter(); | ||||
| 
 | ||||
|             session()->flash('info', trans('firefly.mfa_backup_code')); | ||||
|             // send user notification.
 | ||||
|             $user = auth()->user(); | ||||
|             Log::channel('audit')->info(sprintf('User "%s" has used a backup code.', $user->email)); | ||||
|             event(new MFAUsedBackupCode($user)); | ||||
| 
 | ||||
|             return redirect(route('home')); | ||||
|         } | ||||
| @@ -175,6 +203,42 @@ class TwoFactorController extends Controller | ||||
|             $list = []; | ||||
|         } | ||||
|         $newList = array_values(array_diff($list, [$mfaCode])); | ||||
| 
 | ||||
|         // if the list is 3 or less, send a notification.
 | ||||
|         if (count($newList) <= 3 && count($newList) > 0) { | ||||
|             $user = auth()->user(); | ||||
|             Log::channel('audit')->info(sprintf('User "%s" has used a backup code. They have %d backup codes left.', $user->email, count($newList))); | ||||
|             event(new MFABackupFewLeft($user, count($newList))); | ||||
|         } | ||||
|         // if the list is empty, send notification
 | ||||
|         if (0 === count($newList)) { | ||||
|             $user = auth()->user(); | ||||
|             Log::channel('audit')->info(sprintf('User "%s" has used their last backup code.', $user->email)); | ||||
|             event(new MFABackupNoLeft($user)); | ||||
|         } | ||||
| 
 | ||||
|         app('preferences')->set('mfa_recovery', $newList); | ||||
|     } | ||||
| 
 | ||||
|     private function addToMFAFailureCounter(): void | ||||
|     { | ||||
|         $preference = (int) app('preferences')->get('mfa_failure_count', 0)->data; | ||||
|         ++$preference; | ||||
|         Log::channel('audit')->info(sprintf('MFA failure count is set to %d.', $preference)); | ||||
|         app('preferences')->set('mfa_failure_count', $preference); | ||||
|     } | ||||
| 
 | ||||
|     private function getMFAFailureCounter(): int | ||||
|     { | ||||
|         $value = (int) app('preferences')->get('mfa_failure_count', 0)->data; | ||||
|         Log::channel('audit')->info(sprintf('MFA failure count is %d.', $value)); | ||||
| 
 | ||||
|         return $value; | ||||
|     } | ||||
| 
 | ||||
|     private function resetMFAFailureCounter(): void | ||||
|     { | ||||
|         app('preferences')->set('mfa_failure_count', 0); | ||||
|         Log::channel('audit')->info('MFA failure count is set to zero.'); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -40,6 +40,7 @@ use FireflyIII\Support\Http\Controllers\ChartGeneration; | ||||
| use FireflyIII\Support\Http\Controllers\DateCalculation; | ||||
| use Illuminate\Http\JsonResponse; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| /** | ||||
|  * Class AccountController. | ||||
| @@ -300,13 +301,13 @@ class AccountController extends Controller | ||||
|         $start          = clone session('start', today(config('app.timezone'))->startOfMonth()); | ||||
|         $end            = clone session('end', today(config('app.timezone'))->endOfMonth()); | ||||
|         $defaultSet     = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])->pluck('id')->toArray(); | ||||
|         app('log')->debug('Default set is ', $defaultSet); | ||||
|         Log::debug('Default set is ', $defaultSet); | ||||
|         $frontpage      = app('preferences')->get('frontpageAccounts', $defaultSet); | ||||
|         $frontpageArray = !is_array($frontpage->data) ? [] : $frontpage->data; | ||||
|         app('log')->debug('Frontpage preference set is ', $frontpageArray); | ||||
|         Log::debug('Frontpage preference set is ', $frontpageArray); | ||||
|         if (0 === count($frontpageArray)) { | ||||
|             app('preferences')->set('frontpageAccounts', $defaultSet); | ||||
|             app('log')->debug('frontpage set is empty!'); | ||||
|             Log::debug('frontpage set is empty!'); | ||||
|         } | ||||
|         $accounts       = $repository->getAccountsById($frontpageArray); | ||||
| 
 | ||||
| @@ -414,7 +415,7 @@ class AccountController extends Controller | ||||
|      */ | ||||
|     private function periodByCurrency(Carbon $start, Carbon $end, Account $account, TransactionCurrency $currency): array | ||||
|     { | ||||
|         app('log')->debug(sprintf('Now in periodByCurrency("%s", "%s", %s, "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'), $account->id, $currency->code)); | ||||
|         Log::debug(sprintf('Now in periodByCurrency("%s", "%s", %s, "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'), $account->id, $currency->code)); | ||||
|         $locale            = app('steam')->getLocale(); | ||||
|         $step              = $this->calculateStep($start, $end); | ||||
|         $result            = [ | ||||
| @@ -424,13 +425,13 @@ class AccountController extends Controller | ||||
|         ]; | ||||
|         $entries           = []; | ||||
|         $current           = clone $start; | ||||
|         app('log')->debug(sprintf('Step is %s', $step)); | ||||
|         Log::debug(sprintf('Step is %s', $step)); | ||||
| 
 | ||||
|         // fix for issue https://github.com/firefly-iii/firefly-iii/issues/8041
 | ||||
|         // have to make sure this chart is always based on the balance at the END of the period.
 | ||||
|         // This period depends on the size of the chart
 | ||||
|         $current           = app('navigation')->endOfX($current, $step, null); | ||||
|         app('log')->debug(sprintf('$current date is %s', $current->format('Y-m-d'))); | ||||
|         Log::debug(sprintf('$current date is %s', $current->format('Y-m-d'))); | ||||
|         if ('1D' === $step) { | ||||
|             // per day the entire period, balance for every day.
 | ||||
|             $format   = (string)trans('config.month_and_day_js', [], $locale); | ||||
| @@ -447,7 +448,7 @@ class AccountController extends Controller | ||||
|         } | ||||
|         if ('1W' === $step || '1M' === $step || '1Y' === $step) { | ||||
|             while ($end >= $current) { | ||||
|                 app('log')->debug(sprintf('Current is: %s', $current->format('Y-m-d'))); | ||||
|                 Log::debug(sprintf('Current is: %s', $current->format('Y-m-d'))); | ||||
|                 $balance         = (float)app('steam')->balance($account, $current, $currency); | ||||
|                 $label           = app('navigation')->periodShow($current, $step); | ||||
|                 $entries[$label] = $balance; | ||||
|   | ||||
| @@ -151,6 +151,7 @@ class CategoryController extends Controller | ||||
|      */ | ||||
|     private function reportPeriodChart(Collection $accounts, Carbon $start, Carbon $end, ?Category $category): array | ||||
|     { | ||||
| 
 | ||||
|         $income     = []; | ||||
|         $expenses   = []; | ||||
|         $categoryId = 0; | ||||
| @@ -169,8 +170,8 @@ class CategoryController extends Controller | ||||
|             $categoryId    = $category->id; | ||||
|             // this gives us all currencies
 | ||||
|             $collection    = new Collection([$category]); | ||||
|             $expenses      = $opsRepository->listExpenses($start, $end, null, $collection); | ||||
|             $income        = $opsRepository->listIncome($start, $end, null, $collection); | ||||
|             $expenses      = $opsRepository->listExpenses($start, $end, $accounts, $collection); | ||||
|             $income        = $opsRepository->listIncome($start, $end, $accounts, $collection); | ||||
|         } | ||||
|         $currencies = array_unique(array_merge(array_keys($income), array_keys($expenses))); | ||||
|         $periods    = app('navigation')->listOfPeriods($start, $end); | ||||
|   | ||||
| @@ -122,6 +122,7 @@ class CategoryReportController extends Controller | ||||
| 
 | ||||
|     public function categoryIncome(Collection $accounts, Collection $categories, Carbon $start, Carbon $end): JsonResponse | ||||
|     { | ||||
| 
 | ||||
|         $result = []; | ||||
|         $earned = $this->opsRepository->listIncome($start, $end, $accounts, $categories); | ||||
| 
 | ||||
|   | ||||
| @@ -24,18 +24,19 @@ declare(strict_types=1); | ||||
| namespace FireflyIII\Http\Controllers\Chart; | ||||
| 
 | ||||
| use Carbon\Carbon; | ||||
| use FireflyIII\Enums\TransactionTypeEnum; | ||||
| use FireflyIII\Generator\Chart\Basic\GeneratorInterface; | ||||
| use FireflyIII\Helpers\Collector\GroupCollectorInterface; | ||||
| use FireflyIII\Helpers\Report\NetWorthInterface; | ||||
| use FireflyIII\Http\Controllers\Controller; | ||||
| use FireflyIII\Models\Account; | ||||
| use FireflyIII\Models\TransactionType; | ||||
| use FireflyIII\Repositories\Account\AccountRepositoryInterface; | ||||
| use FireflyIII\Support\CacheProperties; | ||||
| use FireflyIII\Support\Http\Controllers\BasicDataSupport; | ||||
| use FireflyIII\Support\Http\Controllers\ChartGeneration; | ||||
| use Illuminate\Http\JsonResponse; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| /** | ||||
|  * Class ReportController. | ||||
| @@ -88,7 +89,7 @@ class ReportController extends Controller | ||||
|                 $includeNetWorth = $accountRepository->getMetaValue($account, 'include_net_worth'); | ||||
|                 $result          = null === $includeNetWorth ? true : '1' === $includeNetWorth; | ||||
|                 if (false === $result) { | ||||
|                     app('log')->debug(sprintf('Will not include "%s" in net worth charts.', $account->name)); | ||||
|                     Log::debug(sprintf('Will not include "%s" in net worth charts.', $account->name)); | ||||
|                 } | ||||
| 
 | ||||
|                 return $result; | ||||
| @@ -136,6 +137,7 @@ class ReportController extends Controller | ||||
|      */ | ||||
|     public function operations(Collection $accounts, Carbon $start, Carbon $end): JsonResponse | ||||
|     { | ||||
|         $end->endOfDay(); | ||||
|         // chart properties for cache:
 | ||||
|         $cache          = new CacheProperties(); | ||||
|         $cache->addProperty('chart.report.operations'); | ||||
| @@ -146,7 +148,8 @@ class ReportController extends Controller | ||||
|             // return response()->json($cache->get());
 | ||||
|         } | ||||
| 
 | ||||
|         app('log')->debug('Going to do operations for accounts ', $accounts->pluck('id')->toArray()); | ||||
|         Log::debug('Going to do operations for accounts ', $accounts->pluck('id')->toArray()); | ||||
|         Log::debug(sprintf('Period: %s to %s', $start->toW3cString(), $end->toW3cString())); | ||||
|         $format         = app('navigation')->preferredCarbonFormat($start, $end); | ||||
|         $titleFormat    = app('navigation')->preferredCarbonLocalizedFormat($start, $end); | ||||
|         $preferredRange = app('navigation')->preferredRangeFormat($start, $end); | ||||
| @@ -158,7 +161,14 @@ class ReportController extends Controller | ||||
|         $collector      = app(GroupCollectorInterface::class); | ||||
|         $collector->setRange($start, $end)->withAccountInformation(); | ||||
|         $collector->setXorAccounts($accounts); | ||||
|         $collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::RECONCILIATION, TransactionType::TRANSFER]); | ||||
|         $collector->setTypes( | ||||
|             [ | ||||
|                 TransactionTypeEnum::WITHDRAWAL, | ||||
|                 TransactionTypeEnum::DEPOSIT, | ||||
|                 TransactionTypeEnum::RECONCILIATION, | ||||
|                 TransactionTypeEnum::TRANSFER, | ||||
|             ] | ||||
|         ); | ||||
|         $journals       = $collector->getExtractedJournals(); | ||||
| 
 | ||||
|         // loop. group by currency and by period.
 | ||||
| @@ -184,15 +194,25 @@ class ReportController extends Controller | ||||
| 
 | ||||
|             // deposit = incoming
 | ||||
|             // transfer or reconcile or opening balance, and these accounts are the destination.
 | ||||
|             if (TransactionType::DEPOSIT === $journal['transaction_type_type'] || ((TransactionType::TRANSFER === $journal['transaction_type_type'] || TransactionType::RECONCILIATION === $journal['transaction_type_type'] || TransactionType::OPENING_BALANCE === $journal['transaction_type_type']) && in_array($journal['destination_account_id'], $ids, true))) { | ||||
|             if ( | ||||
|                 TransactionTypeEnum::DEPOSIT->value === $journal['transaction_type_type'] | ||||
|                 || (( | ||||
|                     TransactionTypeEnum::TRANSFER->value === $journal['transaction_type_type'] | ||||
|                     || TransactionTypeEnum::RECONCILIATION->value === $journal['transaction_type_type'] | ||||
|                     || TransactionTypeEnum::OPENING_BALANCE->value === $journal['transaction_type_type'] | ||||
|                 ) | ||||
|                     && in_array($journal['destination_account_id'], $ids, true))) { | ||||
|                 $key = 'earned'; | ||||
|             } | ||||
|             $data[$currencyId][$period][$key] = bcadd($data[$currencyId][$period][$key], $amount); | ||||
|         } | ||||
| 
 | ||||
|         // loop this data, make chart bars for each currency:
 | ||||
|         Log::debug('Looping data'); | ||||
| 
 | ||||
|         /** @var array $currency */ | ||||
|         foreach ($data as $currency) { | ||||
|             Log::debug(sprintf('Now processing currency "%s"', $currency['currency_name'])); | ||||
|             $income       = [ | ||||
|                 'label'           => (string)trans('firefly.box_earned_in_currency', ['currency' => $currency['currency_name']]), | ||||
|                 'type'            => 'bar', | ||||
| @@ -214,12 +234,15 @@ class ReportController extends Controller | ||||
|             // loop all possible periods between $start and $end
 | ||||
|             $currentStart = clone $start; | ||||
|             $currentEnd   = clone $end; | ||||
|             Log::debug(sprintf('START current start and end: %s and %s', $currentStart->toW3cString(), $currentEnd->toW3cString())); | ||||
| 
 | ||||
|             // #8374. Sloppy fix for yearly charts. Not really interested in a better fix with v2 layout and all.
 | ||||
|             if ('1Y' === $preferredRange) { | ||||
|                 $currentEnd = app('navigation')->endOfPeriod($currentEnd, $preferredRange); | ||||
|             } | ||||
|             Log::debug('Start of sub-loop'); | ||||
|             while ($currentStart <= $currentEnd) { | ||||
|                 Log::debug(sprintf('Current start: %s', $currentStart->toW3cString())); | ||||
|                 $key          = $currentStart->format($format); | ||||
|                 $title        = $currentStart->isoFormat($titleFormat); | ||||
|                 // #8663 make sure the period exists in the data previously collected.
 | ||||
| @@ -227,12 +250,20 @@ class ReportController extends Controller | ||||
|                     $income['entries'][$title]  = app('steam')->bcround($currency[$key]['earned'] ?? '0', $currency['currency_decimal_places']); | ||||
|                     $expense['entries'][$title] = app('steam')->bcround($currency[$key]['spent'] ?? '0', $currency['currency_decimal_places']); | ||||
|                 } | ||||
|                 // #9477 if the period is not in the data, add it with zero values.
 | ||||
|                 if (!array_key_exists($key, $currency)) { | ||||
|                     $income['entries'][$title]  = '0'; | ||||
|                     $expense['entries'][$title] = '0'; | ||||
| 
 | ||||
|                 } | ||||
|                 $currentStart = app('navigation')->addPeriod($currentStart, $preferredRange, 0); | ||||
|             } | ||||
|             Log::debug('End of sub-loop'); | ||||
| 
 | ||||
|             $chartData[]  = $income; | ||||
|             $chartData[]  = $expense; | ||||
|         } | ||||
|         Log::debug('End of loop'); | ||||
| 
 | ||||
|         $data           = $this->generator->multiSet($chartData); | ||||
|         $cache->store($data); | ||||
|   | ||||
| @@ -30,6 +30,7 @@ use FireflyIII\Http\Middleware\IsDemoUser; | ||||
| use FireflyIII\Models\AccountType; | ||||
| use FireflyIII\Models\TransactionType; | ||||
| use FireflyIII\Support\Http\Controllers\GetConfigurationData; | ||||
| use FireflyIII\Support\Models\AccountBalanceCalculator; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Contracts\View\Factory; | ||||
| use Illuminate\Http\RedirectResponse; | ||||
| @@ -94,6 +95,7 @@ class DebugController extends Controller | ||||
| 
 | ||||
|         // also do some recalculations.
 | ||||
|         Artisan::call('firefly-iii:trigger-credit-recalculation'); | ||||
|         AccountBalanceCalculator::recalculateAll(true); | ||||
| 
 | ||||
|         try { | ||||
|             Artisan::call('twig:clean'); | ||||
|   | ||||
| @@ -107,7 +107,6 @@ class JavascriptController extends Controller | ||||
|         $lang                      = $pref->data; | ||||
|         $dateRange                 = $this->getDateRangeConfig(); | ||||
|         $uid                       = substr(hash('sha256', sprintf('%s-%s-%s', (string)config('app.key'), auth()->user()->id, auth()->user()->email)), 0, 12); | ||||
| 
 | ||||
|         $data                      = [ | ||||
|             'currencyCode'         => $currency->code, | ||||
|             'currencySymbol'       => $currency->symbol, | ||||
|   | ||||
| @@ -65,6 +65,16 @@ class FrontpageController extends Controller | ||||
|                 $info[] = $entry; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // sort by current percentage (lowest at the top)
 | ||||
|         uasort( | ||||
|             $info, | ||||
|             static function (array $a, array $b) { | ||||
|                 return $a['percentage'] <=> $b['percentage']; | ||||
|             } | ||||
|         ); | ||||
| 
 | ||||
| 
 | ||||
|         $html = ''; | ||||
|         if (0 !== count($info)) { | ||||
|             try { | ||||
|   | ||||
							
								
								
									
										342
									
								
								app/Http/Controllers/Profile/MfaController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										342
									
								
								app/Http/Controllers/Profile/MfaController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,342 @@ | ||||
| <?php | ||||
| /* | ||||
|  * MfaController.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Http\Controllers\Profile; | ||||
| 
 | ||||
| use FireflyIII\Events\Security\DisabledMFA; | ||||
| use FireflyIII\Events\Security\EnabledMFA; | ||||
| use FireflyIII\Events\Security\MFANewBackupCodes; | ||||
| use FireflyIII\Exceptions\FireflyException; | ||||
| use FireflyIII\Http\Controllers\Controller; | ||||
| use FireflyIII\Http\Middleware\IsDemoUser; | ||||
| use FireflyIII\Http\Requests\ExistingTokenFormRequest; | ||||
| use FireflyIII\Http\Requests\TokenFormRequest; | ||||
| use FireflyIII\Repositories\User\UserRepositoryInterface; | ||||
| use FireflyIII\User; | ||||
| use Illuminate\Contracts\View\Factory; | ||||
| use Illuminate\Http\RedirectResponse; | ||||
| use Illuminate\Http\Request; | ||||
| use Illuminate\Routing\Redirector; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use Illuminate\View\View; | ||||
| use PragmaRX\Recovery\Recovery; | ||||
| 
 | ||||
| /** | ||||
|  * Class MfaController | ||||
|  * | ||||
|  * Enable MFA Flow: | ||||
|  * | ||||
|  * Page 1 (GET): Show QR code and the manual code. Secret keeps rotating. | ||||
|  *  POST: store secret, store response, validate password. | ||||
|  * --- | ||||
|  * Page 3 (GET): Confirm 2FA status and show recovery codes. | ||||
|  *        Same page as page 1, but when secret is present. | ||||
|  */ | ||||
| class MfaController extends Controller | ||||
| { | ||||
|     protected bool $internalAuth; | ||||
| 
 | ||||
|     /** | ||||
|      * ProfileController constructor. | ||||
|      */ | ||||
|     public function __construct() | ||||
|     { | ||||
|         parent::__construct(); | ||||
| 
 | ||||
|         $this->middleware( | ||||
|             static function ($request, $next) { | ||||
|                 app('view')->share('title', (string) trans('firefly.profile')); | ||||
|                 app('view')->share('mainTitleIcon', 'fa-user'); | ||||
| 
 | ||||
|                 return $next($request); | ||||
|             } | ||||
|         ); | ||||
|         $authGuard          = config('firefly.authentication_guard'); | ||||
|         $this->internalAuth = 'web' === $authGuard; | ||||
|         app('log')->debug(sprintf('ProfileController::__construct(). Authentication guard is "%s"', $authGuard)); | ||||
| 
 | ||||
|         $this->middleware(IsDemoUser::class)->except(['index']); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public function index(): Factory|RedirectResponse|View | ||||
|     { | ||||
|         if (!$this->internalAuth) { | ||||
|             request()->session()->flash('error', trans('firefly.external_user_mgt_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
| 
 | ||||
|         $subTitle     = (string)trans('firefly.mfa_index_title'); | ||||
|         $subTitleIcon = 'fa-calculator'; | ||||
|         $enabledMFA   = null !== auth()->user()->mfa_secret; | ||||
| 
 | ||||
|         return view('profile.mfa.index')->with(compact('subTitle', 'subTitleIcon', 'enabledMFA')); | ||||
|     } | ||||
| 
 | ||||
|     public function disableMFA(Request $request): Factory|RedirectResponse|View | ||||
|     { | ||||
|         if (!$this->internalAuth) { | ||||
|             request()->session()->flash('error', trans('firefly.external_user_mgt_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
|         $enabledMFA   = null !== auth()->user()->mfa_secret; | ||||
|         if (false === $enabledMFA) { | ||||
|             request()->session()->flash('info', trans('firefly.mfa_already_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
|         $subTitle     = (string)trans('firefly.mfa_index_title'); | ||||
|         $subTitleIcon = 'fa-calculator'; | ||||
| 
 | ||||
|         return view('profile.mfa.disable-mfa')->with(compact('subTitle', 'subTitleIcon', 'enabledMFA')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete 2FA routine. | ||||
|      */ | ||||
|     public function disableMFAPost(ExistingTokenFormRequest $request): Redirector|RedirectResponse | ||||
|     { | ||||
|         if (!$this->internalAuth) { | ||||
|             $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
| 
 | ||||
|         /** @var UserRepositoryInterface $repository */ | ||||
|         $repository = app(UserRepositoryInterface::class); | ||||
| 
 | ||||
|         /** @var User $user */ | ||||
|         $user       = auth()->user(); | ||||
| 
 | ||||
|         app('preferences')->delete('temp-mfa-secret'); | ||||
|         app('preferences')->delete('temp-mfa-codes'); | ||||
|         $repository->setMFACode($user, null); | ||||
|         app('preferences')->mark(); | ||||
| 
 | ||||
|         session()->flash('success', (string) trans('firefly.pref_two_factor_auth_disabled')); | ||||
|         session()->flash('info', (string) trans('firefly.pref_two_factor_auth_remove_it')); | ||||
| 
 | ||||
|         // also logout current 2FA tokens.
 | ||||
|         $cookieName = config('google2fa.cookie_name', 'google2fa_token'); | ||||
|         \Cookie::forget($cookieName); | ||||
| 
 | ||||
|         // send user notification.
 | ||||
|         Log::channel('audit')->info(sprintf('User "%s" has disabled MFA', $user->email)); | ||||
|         event(new DisabledMFA($user)); | ||||
| 
 | ||||
|         return redirect(route('profile.index')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Enable 2FA screen. | ||||
|      */ | ||||
|     public function enableMFA(Request $request): Redirector|RedirectResponse|View | ||||
|     { | ||||
|         if (!$this->internalAuth) { | ||||
|             $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
| 
 | ||||
|         /** @var User $user */ | ||||
|         $user       = auth()->user(); | ||||
|         $enabledMFA = null !== $user->mfa_secret; | ||||
| 
 | ||||
|         // If FF3 already has a secret, just set the two-factor auth enabled to 1,
 | ||||
|         // and let the user continue with the existing secret.
 | ||||
|         if ($enabledMFA) { | ||||
|             session()->flash('info', (string) trans('firefly.2fa_already_enabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
| 
 | ||||
|         $domain     = $this->getDomain(); | ||||
|         $secret     = \Google2FA::generateSecretKey(); | ||||
|         $image      = \Google2FA::getQRCodeInline($domain, auth()->user()->email, (string) $secret); | ||||
| 
 | ||||
|         app('preferences')->set('temp-mfa-secret', $secret); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|         return view('profile.mfa.enable-mfa', compact('image', 'secret')); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public function backupCodesPost(ExistingTokenFormRequest $request): Redirector|RedirectResponse|View | ||||
|     { | ||||
|         if (!$this->internalAuth) { | ||||
|             $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
|         $enabledMFA    = null !== auth()->user()->mfa_secret; | ||||
|         if (false === $enabledMFA) { | ||||
|             request()->session()->flash('info', trans('firefly.mfa_not_enabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
|         // generate recovery codes:
 | ||||
|         $recovery      = app(Recovery::class); | ||||
|         $recoveryCodes = $recovery->lowercase() | ||||
|             ->setCount(8)     // Generate 8 codes
 | ||||
|             ->setBlocks(2)    // Every code must have 2 blocks
 | ||||
|             ->setChars(6)     // Each block must have 6 chars
 | ||||
|             ->toArray() | ||||
|         ; | ||||
|         $codes         = implode("\r\n", $recoveryCodes); | ||||
| 
 | ||||
|         app('preferences')->set('mfa_recovery', $recoveryCodes); | ||||
|         app('preferences')->mark(); | ||||
| 
 | ||||
|         // send user notification.
 | ||||
|         $user          = auth()->user(); | ||||
|         Log::channel('audit')->info(sprintf('User "%s" has generated new backup codes.', $user->email)); | ||||
|         event(new MFANewBackupCodes($user)); | ||||
| 
 | ||||
|         return view('profile.mfa.backup-codes-post')->with(compact('codes')); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @throws FireflyException | ||||
|      */ | ||||
|     public function backupCodes(Request $request): Factory|RedirectResponse|View | ||||
|     { | ||||
|         if (!$this->internalAuth) { | ||||
|             $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
|         $enabledMFA = null !== auth()->user()->mfa_secret; | ||||
|         if (false === $enabledMFA) { | ||||
|             request()->session()->flash('info', trans('firefly.mfa_not_enabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
| 
 | ||||
|         return view('profile.mfa.backup-codes-intro'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Submit 2FA for the first time. | ||||
|      * | ||||
|      * @return Redirector|RedirectResponse | ||||
|      * | ||||
|      * @throws FireflyException | ||||
|      */ | ||||
|     public function enableMFAPost(TokenFormRequest $request) | ||||
|     { | ||||
|         if (!$this->internalAuth) { | ||||
|             $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
| 
 | ||||
|         /** @var User $user */ | ||||
|         $user       = auth()->user(); | ||||
| 
 | ||||
|         // verify password.
 | ||||
|         $password   = $request->get('password'); | ||||
|         if (!auth()->validate(['email' => $user->email, 'password' => $password])) { | ||||
|             session()->flash('error', 'Bad user pw, no MFA for you!'); | ||||
| 
 | ||||
|             return redirect(route('profile.mfa.index')); | ||||
|         } | ||||
| 
 | ||||
|         /** @var UserRepositoryInterface $repository */ | ||||
|         $repository = app(UserRepositoryInterface::class); | ||||
|         $secret     = app('preferences')->get('temp-mfa-secret')?->data; | ||||
|         if (is_array($secret)) { | ||||
|             $secret = null; | ||||
|         } | ||||
|         $secret     = (string) $secret; | ||||
| 
 | ||||
|         $repository->setMFACode($user, $secret); | ||||
| 
 | ||||
|         app('preferences')->delete('temp-mfa-secret'); | ||||
| 
 | ||||
|         session()->flash('success', (string) trans('firefly.saved_preferences')); | ||||
|         app('preferences')->mark(); | ||||
| 
 | ||||
|         // also save the code so replay attack is prevented.
 | ||||
|         $mfaCode    = $request->get('code'); | ||||
|         $this->addToMFAHistory($mfaCode); | ||||
| 
 | ||||
|         // make sure MFA is logged out.
 | ||||
|         if ('testing' !== config('app.env')) { | ||||
|             \Google2FA::logout(); | ||||
|         } | ||||
| 
 | ||||
|         // drop all info from session:
 | ||||
|         session()->forget(['temp-mfa-secret', 'two-factor-secret', 'two-factor-codes']); | ||||
| 
 | ||||
|         // send user notification.
 | ||||
|         Log::channel('audit')->info(sprintf('User "%s" has enabled MFA', $user->email)); | ||||
|         event(new EnabledMFA($user)); | ||||
| 
 | ||||
|         return redirect(route('profile.mfa.backup-codes')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * TODO duplicate code. | ||||
|      * | ||||
|      * @throws FireflyException | ||||
|      */ | ||||
|     private function addToMFAHistory(string $mfaCode): void | ||||
|     { | ||||
|         /** @var array $mfaHistory */ | ||||
|         $mfaHistory   = app('preferences')->get('mfa_history', [])->data; | ||||
|         $entry        = [ | ||||
|             'time' => time(), | ||||
|             'code' => $mfaCode, | ||||
|         ]; | ||||
|         $mfaHistory[] = $entry; | ||||
| 
 | ||||
|         app('preferences')->set('mfa_history', $mfaHistory); | ||||
|         $this->filterMFAHistory(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Remove old entries from the preferences array. | ||||
|      */ | ||||
|     private function filterMFAHistory(): void | ||||
|     { | ||||
|         /** @var array $mfaHistory */ | ||||
|         $mfaHistory = app('preferences')->get('mfa_history', [])->data; | ||||
|         $newHistory = []; | ||||
|         $now        = time(); | ||||
|         foreach ($mfaHistory as $entry) { | ||||
|             $time = $entry['time']; | ||||
|             $code = $entry['code']; | ||||
|             if ($now - $time <= 300) { | ||||
|                 $newHistory[] = [ | ||||
|                     'time' => $time, | ||||
|                     'code' => $code, | ||||
|                 ]; | ||||
|             } | ||||
|         } | ||||
|         app('preferences')->set('mfa_history', $newHistory); | ||||
|     } | ||||
| } | ||||
| @@ -30,7 +30,6 @@ use FireflyIII\Http\Middleware\IsDemoUser; | ||||
| use FireflyIII\Http\Requests\DeleteAccountFormRequest; | ||||
| use FireflyIII\Http\Requests\EmailFormRequest; | ||||
| use FireflyIII\Http\Requests\ProfileFormRequest; | ||||
| use FireflyIII\Http\Requests\TokenFormRequest; | ||||
| use FireflyIII\Models\Preference; | ||||
| use FireflyIII\Repositories\User\UserRepositoryInterface; | ||||
| use FireflyIII\Support\Http\Controllers\CreateStuff; | ||||
| @@ -45,10 +44,6 @@ use Illuminate\Routing\Redirector; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\View\View; | ||||
| use Laravel\Passport\ClientRepository; | ||||
| use PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException; | ||||
| use PragmaRX\Google2FA\Exceptions\InvalidCharactersException; | ||||
| use PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException; | ||||
| use PragmaRX\Recovery\Recovery; | ||||
| 
 | ||||
| /** | ||||
|  * Class ProfileController. | ||||
| @@ -83,65 +78,6 @@ class ProfileController extends Controller | ||||
|         $this->middleware(IsDemoUser::class)->except(['index']); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * View that generates a 2FA code for the user. | ||||
|      * | ||||
|      * @throws IncompatibleWithGoogleAuthenticatorException | ||||
|      * @throws InvalidCharactersException | ||||
|      * @throws SecretKeyTooShortException | ||||
|      */ | ||||
|     public function code(Request $request): Factory|RedirectResponse|View | ||||
|     { | ||||
|         if (!$this->internalAuth) { | ||||
|             $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
|         $domain           = $this->getDomain(); | ||||
|         $secretPreference = app('preferences')->get('temp-mfa-secret'); | ||||
|         $codesPreference  = app('preferences')->get('temp-mfa-codes'); | ||||
| 
 | ||||
|         // generate secret if not in session
 | ||||
|         if (null === $secretPreference) { | ||||
|             // generate secret + store + flash
 | ||||
|             $secret = \Google2FA::generateSecretKey(); | ||||
|             app('preferences')->set('temp-mfa-secret', $secret); | ||||
|         } | ||||
| 
 | ||||
|         // re-use secret if in session
 | ||||
|         if (null !== $secretPreference) { | ||||
|             // get secret from session and flash
 | ||||
|             $secret = $secretPreference->data; | ||||
|         } | ||||
|         if (is_array($secret)) { | ||||
|             $secret = ''; | ||||
|         } | ||||
| 
 | ||||
|         // generate recovery codes if not in session:
 | ||||
|         $recoveryCodes    = ''; | ||||
| 
 | ||||
|         if (null === $codesPreference) { | ||||
|             // generate codes + store + flash:
 | ||||
|             $recovery      = app(Recovery::class); | ||||
|             $recoveryCodes = $recovery->lowercase()->setCount(8)->setBlocks(2)->setChars(6)->toArray(); | ||||
|             app('preferences')->set('temp-mfa-codes', $recoveryCodes); | ||||
|         } | ||||
| 
 | ||||
|         // get codes from session if present already:
 | ||||
|         if (null !== $codesPreference) { | ||||
|             $recoveryCodes = $codesPreference->data; | ||||
|         } | ||||
|         if (!is_array($recoveryCodes)) { | ||||
|             $recoveryCodes = []; | ||||
|         } | ||||
| 
 | ||||
|         $codes            = implode("\r\n", $recoveryCodes); | ||||
| 
 | ||||
|         $image            = \Google2FA::getQRCodeInline($domain, auth()->user()->email, (string)$secret); | ||||
| 
 | ||||
|         return view('profile.code', compact('image', 'secret', 'codes')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Screen to confirm email change. | ||||
|      * | ||||
| @@ -193,61 +129,6 @@ class ProfileController extends Controller | ||||
|         return view('profile.delete-account', compact('title', 'subTitle', 'subTitleIcon')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Delete 2FA routine. | ||||
|      */ | ||||
|     public function deleteCode(Request $request): Redirector|RedirectResponse | ||||
|     { | ||||
|         if (!$this->internalAuth) { | ||||
|             $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
| 
 | ||||
|         /** @var UserRepositoryInterface $repository */ | ||||
|         $repository = app(UserRepositoryInterface::class); | ||||
| 
 | ||||
|         /** @var User $user */ | ||||
|         $user       = auth()->user(); | ||||
| 
 | ||||
|         app('preferences')->delete('temp-mfa-secret'); | ||||
|         app('preferences')->delete('temp-mfa-codes'); | ||||
|         $repository->setMFACode($user, null); | ||||
|         app('preferences')->mark(); | ||||
| 
 | ||||
|         session()->flash('success', (string)trans('firefly.pref_two_factor_auth_disabled')); | ||||
|         session()->flash('info', (string)trans('firefly.pref_two_factor_auth_remove_it')); | ||||
| 
 | ||||
|         return redirect(route('profile.index')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Enable 2FA screen. | ||||
|      */ | ||||
|     public function enable2FA(Request $request): Redirector|RedirectResponse | ||||
|     { | ||||
|         if (!$this->internalAuth) { | ||||
|             $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
| 
 | ||||
|         /** @var User $user */ | ||||
|         $user       = auth()->user(); | ||||
|         $enabledMFA = null !== $user->mfa_secret; | ||||
| 
 | ||||
|         // if we don't have a valid secret yet, redirect to the code page to get one.
 | ||||
|         if (!$enabledMFA) { | ||||
|             return redirect(route('profile.code')); | ||||
|         } | ||||
| 
 | ||||
|         // If FF3 already has a secret, just set the two factor auth enabled to 1,
 | ||||
|         // and let the user continue with the existing secret.
 | ||||
|         session()->flash('info', (string)trans('firefly.2fa_already_enabled')); | ||||
| 
 | ||||
|         return redirect(route('profile.index')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Index for profile. | ||||
|      * | ||||
| @@ -298,33 +179,6 @@ class ProfileController extends Controller | ||||
|         return view('profile.logout-other-sessions'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @throws FireflyException | ||||
|      */ | ||||
|     public function newBackupCodes(Request $request): Factory|RedirectResponse|View | ||||
|     { | ||||
|         if (!$this->internalAuth) { | ||||
|             $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
| 
 | ||||
|         // generate recovery codes:
 | ||||
|         $recovery      = app(Recovery::class); | ||||
|         $recoveryCodes = $recovery->lowercase() | ||||
|             ->setCount(8)     // Generate 8 codes
 | ||||
|             ->setBlocks(2)    // Every code must have 7 blocks
 | ||||
|             ->setChars(6)     // Each block must have 16 chars
 | ||||
|             ->toArray() | ||||
|         ; | ||||
|         $codes         = implode("\r\n", $recoveryCodes); | ||||
| 
 | ||||
|         app('preferences')->set('mfa_recovery', $recoveryCodes); | ||||
|         app('preferences')->mark(); | ||||
| 
 | ||||
|         return view('profile.new-backup-codes', compact('codes')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Submit the change email form. | ||||
|      */ | ||||
| @@ -442,99 +296,6 @@ class ProfileController extends Controller | ||||
|         return view('profile.change-password', compact('title', 'subTitle', 'subTitleIcon')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Submit 2FA for the first time. | ||||
|      * | ||||
|      * @return Redirector|RedirectResponse | ||||
|      * | ||||
|      * @throws FireflyException | ||||
|      */ | ||||
|     public function postCode(TokenFormRequest $request) | ||||
|     { | ||||
|         if (!$this->internalAuth) { | ||||
|             $request->session()->flash('error', trans('firefly.external_user_mgt_disabled')); | ||||
| 
 | ||||
|             return redirect(route('profile.index')); | ||||
|         } | ||||
| 
 | ||||
|         /** @var User $user */ | ||||
|         $user       = auth()->user(); | ||||
| 
 | ||||
|         /** @var UserRepositoryInterface $repository */ | ||||
|         $repository = app(UserRepositoryInterface::class); | ||||
|         $secret     = app('preferences')->get('temp-mfa-secret')?->data; | ||||
|         if (is_array($secret)) { | ||||
|             $secret = null; | ||||
|         } | ||||
|         $secret     = (string)$secret; | ||||
| 
 | ||||
|         $repository->setMFACode($user, $secret); | ||||
| 
 | ||||
|         app('preferences')->delete('temp-mfa-secret'); | ||||
|         app('preferences')->delete('temp-mfa-codes'); | ||||
| 
 | ||||
|         session()->flash('success', (string)trans('firefly.saved_preferences')); | ||||
|         app('preferences')->mark(); | ||||
| 
 | ||||
|         // also save the code so replay attack is prevented.
 | ||||
|         $mfaCode    = $request->get('code'); | ||||
|         $this->addToMFAHistory($mfaCode); | ||||
| 
 | ||||
|         // save backup codes in preferences:
 | ||||
|         app('preferences')->set('mfa_recovery', session()->get('temp-mfa-codes')); | ||||
| 
 | ||||
|         // make sure MFA is logged out.
 | ||||
|         if ('testing' !== config('app.env')) { | ||||
|             \Google2FA::logout(); | ||||
|         } | ||||
| 
 | ||||
|         // drop all info from session:
 | ||||
|         session()->forget(['temp-mfa-secret', 'two-factor-secret', 'temp-mfa-codes', 'two-factor-codes']); | ||||
| 
 | ||||
|         return redirect(route('profile.index')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * TODO duplicate code. | ||||
|      * | ||||
|      * @throws FireflyException | ||||
|      */ | ||||
|     private function addToMFAHistory(string $mfaCode): void | ||||
|     { | ||||
|         /** @var array $mfaHistory */ | ||||
|         $mfaHistory   = app('preferences')->get('mfa_history', [])->data; | ||||
|         $entry        = [ | ||||
|             'time' => time(), | ||||
|             'code' => $mfaCode, | ||||
|         ]; | ||||
|         $mfaHistory[] = $entry; | ||||
| 
 | ||||
|         app('preferences')->set('mfa_history', $mfaHistory); | ||||
|         $this->filterMFAHistory(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Remove old entries from the preferences array. | ||||
|      */ | ||||
|     private function filterMFAHistory(): void | ||||
|     { | ||||
|         /** @var array $mfaHistory */ | ||||
|         $mfaHistory = app('preferences')->get('mfa_history', [])->data; | ||||
|         $newHistory = []; | ||||
|         $now        = time(); | ||||
|         foreach ($mfaHistory as $entry) { | ||||
|             $time = $entry['time']; | ||||
|             $code = $entry['code']; | ||||
|             if ($now - $time <= 300) { | ||||
|                 $newHistory[] = [ | ||||
|                     'time' => $time, | ||||
|                     'code' => $code, | ||||
|                 ]; | ||||
|             } | ||||
|         } | ||||
|         app('preferences')->set('mfa_history', $newHistory); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Submit delete account. | ||||
|      * | ||||
| @@ -664,7 +425,7 @@ class ProfileController extends Controller | ||||
|         $repository->changeEmail($user, $match); | ||||
|         $repository->unblockUser($user); | ||||
| 
 | ||||
|         // return to login.
 | ||||
|         // return to login page.
 | ||||
|         session()->flash('success', (string)trans('firefly.login_with_old_email')); | ||||
| 
 | ||||
|         return redirect(route('login')); | ||||
|   | ||||
| @@ -77,17 +77,18 @@ class ShowController extends Controller | ||||
|      */ | ||||
|     public function show(Recurrence $recurrence) | ||||
|     { | ||||
|         $repos                 = app(AttachmentRepositoryInterface::class); | ||||
|         $repos                  = app(AttachmentRepositoryInterface::class); | ||||
| 
 | ||||
|         /** @var RecurrenceTransformer $transformer */ | ||||
|         $transformer           = app(RecurrenceTransformer::class); | ||||
|         $transformer            = app(RecurrenceTransformer::class); | ||||
|         $transformer->setParameters(new ParameterBag()); | ||||
| 
 | ||||
|         $array                 = $transformer->transform($recurrence); | ||||
|         $array                  = $transformer->transform($recurrence); | ||||
| 
 | ||||
|         $groups                = $this->recurring->getTransactions($recurrence); | ||||
|         $today                 = today(config('app.timezone')); | ||||
|         $array['repeat_until'] = null !== $array['repeat_until'] ? new Carbon($array['repeat_until']) : null; | ||||
|         $groups                 = $this->recurring->getTransactions($recurrence); | ||||
|         $today                  = today(config('app.timezone')); | ||||
|         $array['repeat_until']  = null !== $array['repeat_until'] ? new Carbon($array['repeat_until']) : null; | ||||
|         $array['journal_count'] = $this->recurring->getJournalCount($recurrence); | ||||
| 
 | ||||
|         // transform dates back to Carbon objects and expand information
 | ||||
|         foreach ($array['repetitions'] as $index => $repetition) { | ||||
| @@ -103,9 +104,9 @@ class ShowController extends Controller | ||||
|         } | ||||
| 
 | ||||
|         // add attachments to the recurrence object.
 | ||||
|         $attachments           = $recurrence->attachments()->get(); | ||||
|         $array['attachments']  = []; | ||||
|         $attachmentTransformer = app(AttachmentTransformer::class); | ||||
|         $attachments            = $recurrence->attachments()->get(); | ||||
|         $array['attachments']   = []; | ||||
|         $attachmentTransformer  = app(AttachmentTransformer::class); | ||||
| 
 | ||||
|         /** @var Attachment $attachment */ | ||||
|         foreach ($attachments as $attachment) { | ||||
| @@ -114,7 +115,16 @@ class ShowController extends Controller | ||||
|             $array['attachments'][] = $item; | ||||
|         } | ||||
| 
 | ||||
|         $subTitle              = (string)trans('firefly.overview_for_recurrence', ['title' => $recurrence->title]); | ||||
|         if (null !== $array['nr_of_repetitions']) { | ||||
|             $left = $array['nr_of_repetitions'] - $array['journal_count']; | ||||
|             $left = max(0, $left); | ||||
|             // limit each repetition to X occurrences:
 | ||||
|             foreach ($array['repetitions'] as $index => $repetition) { | ||||
|                 $array['repetitions'][$index]['occurrences'] = array_slice($repetition['occurrences'], 0, $left); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         $subTitle               = (string)trans('firefly.overview_for_recurrence', ['title' => $recurrence->title]); | ||||
| 
 | ||||
|         return view('recurring.show', compact('recurrence', 'subTitle', 'array', 'groups', 'today')); | ||||
|     } | ||||
|   | ||||
| @@ -40,24 +40,24 @@ class TriggerController extends Controller | ||||
| { | ||||
|     public function trigger(Recurrence $recurrence, TriggerRecurrenceRequest $request): RedirectResponse | ||||
|     { | ||||
|         $all                     = $request->getAll(); | ||||
|         $date                    = $all['date']; | ||||
|         $all                        = $request->getAll(); | ||||
|         $date                       = $all['date']; | ||||
| 
 | ||||
|         // grab the date from the last time the recurrence fired:
 | ||||
|         $backupDate              = $recurrence->latest_date; | ||||
|         $backupDate                 = $recurrence->latest_date; | ||||
| 
 | ||||
|         // fire the recurring cron job on the given date, then post-date the created transaction.
 | ||||
|         app('log')->info(sprintf('Trigger: will now fire recurring cron job task for date "%s".', $date->format('Y-m-d H:i:s'))); | ||||
| 
 | ||||
|         /** @var CreateRecurringTransactions $job */ | ||||
|         $job                     = app(CreateRecurringTransactions::class); | ||||
|         $job                        = app(CreateRecurringTransactions::class); | ||||
|         $job->setRecurrences(new Collection([$recurrence])); | ||||
|         $job->setDate($date); | ||||
|         $job->setForce(false); | ||||
|         $job->handle(); | ||||
|         app('log')->debug('Done with recurrence.'); | ||||
| 
 | ||||
|         $groups                  = $job->getGroups(); | ||||
|         $groups                     = $job->getGroups(); | ||||
| 
 | ||||
|         /** @var TransactionGroup $group */ | ||||
|         foreach ($groups as $group) { | ||||
| @@ -68,7 +68,8 @@ class TriggerController extends Controller | ||||
|                 $journal->save(); | ||||
|             } | ||||
|         } | ||||
|         $recurrence->latest_date = $backupDate; | ||||
|         $recurrence->latest_date    = $backupDate; | ||||
|         $recurrence->latest_date_tz = $backupDate?->format('e'); | ||||
|         $recurrence->save(); | ||||
|         app('preferences')->mark(); | ||||
| 
 | ||||
|   | ||||
| @@ -29,6 +29,7 @@ use FireflyIII\Exceptions\FireflyException; | ||||
| use FireflyIII\Support\System\OAuthKeys; | ||||
| use Illuminate\Database\QueryException; | ||||
| use Illuminate\Http\Request; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| /** | ||||
|  * Class Installer | ||||
| @@ -46,7 +47,7 @@ class Installer | ||||
|      */ | ||||
|     public function handle($request, \Closure $next) | ||||
|     { | ||||
|         // app('log')->debug(sprintf('Installer middleware for URL %s', $request->url()));
 | ||||
|         // Log::debug(sprintf('Installer middleware for URL %s', $request->url()));
 | ||||
|         // ignore installer in test environment.
 | ||||
|         if ('testing' === config('app.env')) { | ||||
|             return $next($request); | ||||
| @@ -55,7 +56,7 @@ class Installer | ||||
|         $url    = $request->url(); | ||||
|         $strpos = stripos($url, '/install'); | ||||
|         if (false !== $strpos) { | ||||
|             // app('log')->debug(sprintf('URL is %s, will NOT run installer middleware', $url));
 | ||||
|             // Log::debug(sprintf('URL is %s, will NOT run installer middleware', $url));
 | ||||
| 
 | ||||
|             return $next($request); | ||||
|         } | ||||
| @@ -80,13 +81,13 @@ class Installer | ||||
|      */ | ||||
|     private function hasNoTables(): bool | ||||
|     { | ||||
|         // app('log')->debug('Now in routine hasNoTables()');
 | ||||
|         // Log::debug('Now in routine hasNoTables()');
 | ||||
| 
 | ||||
|         try { | ||||
|             \DB::table('users')->count(); | ||||
|         } catch (QueryException $e) { | ||||
|             $message = $e->getMessage(); | ||||
|             app('log')->error(sprintf('Error message trying to access users-table: %s', $message)); | ||||
|             Log::error(sprintf('Error message trying to access users-table: %s', $message)); | ||||
|             if ($this->isAccessDenied($message)) { | ||||
|                 throw new FireflyException( | ||||
|                     'It seems your database configuration is not correct. Please verify the username and password in your .env file.', | ||||
| @@ -96,7 +97,7 @@ class Installer | ||||
|             } | ||||
|             if ($this->noTablesExist($message)) { | ||||
|                 // redirect to UpdateController
 | ||||
|                 app('log')->warning('There are no Firefly III tables present. Redirect to migrate routine.'); | ||||
|                 Log::warning('There are no Firefly III tables present. Redirect to migrate routine.'); | ||||
| 
 | ||||
|                 return true; | ||||
|             } | ||||
| @@ -104,7 +105,7 @@ class Installer | ||||
|             throw new FireflyException(sprintf('Could not access the database: %s', $message), 0, $e); | ||||
|         } | ||||
| 
 | ||||
|         // app('log')->debug('Everything seems OK with the tables.');
 | ||||
|         // Log::debug('Everything seems OK with the tables.');
 | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| @@ -134,7 +135,7 @@ class Installer | ||||
|         $configVersion = (int)config('firefly.db_version'); | ||||
|         $dbVersion     = (int)app('fireflyconfig')->getFresh('db_version', 1)->data; | ||||
|         if ($configVersion > $dbVersion) { | ||||
|             app('log')->warning( | ||||
|             Log::warning( | ||||
|                 sprintf( | ||||
|                     'The current configured version (%d) is older than the required version (%d). Redirect to migrate routine.', | ||||
|                     $dbVersion, | ||||
| @@ -145,7 +146,7 @@ class Installer | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         // app('log')->info(sprintf('Configured DB version (%d) equals expected DB version (%d)', $dbVersion, $configVersion));
 | ||||
|         // Log::info(sprintf('Configured DB version (%d) equals expected DB version (%d)', $dbVersion, $configVersion));
 | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| @@ -158,8 +159,13 @@ class Installer | ||||
|         // version compare thing.
 | ||||
|         $configVersion = (string)config('firefly.version'); | ||||
|         $dbVersion     = (string)app('fireflyconfig')->getFresh('ff3_version', '1.0')->data; | ||||
|         if (str_starts_with($configVersion, 'develop')) { | ||||
|             Log::debug('Skipping version check for develop version.'); | ||||
| 
 | ||||
|             return false; | ||||
|         } | ||||
|         if (1 === version_compare($configVersion, $dbVersion)) { | ||||
|             app('log')->warning( | ||||
|             Log::warning( | ||||
|                 sprintf( | ||||
|                     'The current configured Firefly III version (%s) is older than the required version (%s). Redirect to migrate routine.', | ||||
|                     $dbVersion, | ||||
| @@ -170,7 +176,7 @@ class Installer | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         // app('log')->info(sprintf('Installed Firefly III version (%s) equals expected Firefly III version (%s)', $dbVersion, $configVersion));
 | ||||
|         // Log::info(sprintf('Installed Firefly III version (%s) equals expected Firefly III version (%s)', $dbVersion, $configVersion));
 | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
|   | ||||
							
								
								
									
										56
									
								
								app/Http/Requests/ExistingTokenFormRequest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								app/Http/Requests/ExistingTokenFormRequest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| <?php | ||||
| 
 | ||||
| /** | ||||
|  * TokenFormRequest.php | ||||
|  * Copyright (c) 2019 james@firefly-iii.org | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Http\Requests; | ||||
| 
 | ||||
| use FireflyIII\Support\Request\ChecksLogin; | ||||
| use Illuminate\Foundation\Http\FormRequest; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use Illuminate\Validation\Validator; | ||||
| 
 | ||||
| /** | ||||
|  * Class ExistingTokenFormRequest. | ||||
|  */ | ||||
| class ExistingTokenFormRequest extends FormRequest | ||||
| { | ||||
|     use ChecksLogin; | ||||
| 
 | ||||
|     /** | ||||
|      * Rules for this request. | ||||
|      */ | ||||
|     public function rules(): array | ||||
|     { | ||||
|         // fixed
 | ||||
|         return [ | ||||
|             'password' => 'required|currentPassword', | ||||
|             'code'     => 'required|existingMfaCode', | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     public function withValidator(Validator $validator): void | ||||
|     { | ||||
|         if ($validator->fails()) { | ||||
|             Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -42,7 +42,8 @@ class TokenFormRequest extends FormRequest | ||||
|     { | ||||
|         // fixed
 | ||||
|         return [ | ||||
|             'code' => 'required|2faCode', | ||||
|             'password' => 'required|currentPassword', | ||||
|             'code'     => 'required|2faCode', | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|   | ||||
| @@ -42,6 +42,7 @@ use Illuminate\Foundation\Bus\Dispatchable; | ||||
| use Illuminate\Queue\InteractsWithQueue; | ||||
| use Illuminate\Queue\SerializesModels; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| 
 | ||||
| /** | ||||
|  * Class CreateRecurringTransactions. | ||||
| @@ -88,7 +89,7 @@ class CreateRecurringTransactions implements ShouldQueue | ||||
|         $this->recurrences       = new Collection(); | ||||
|         $this->groups            = new Collection(); | ||||
| 
 | ||||
|         app('log')->debug(sprintf('Created new CreateRecurringTransactions("%s")', $this->date->format('Y-m-d'))); | ||||
|         Log::debug(sprintf('Created new CreateRecurringTransactions("%s")', $this->date->format('Y-m-d'))); | ||||
|     } | ||||
| 
 | ||||
|     public function getGroups(): Collection | ||||
| @@ -101,25 +102,25 @@ class CreateRecurringTransactions implements ShouldQueue | ||||
|      */ | ||||
|     public function handle(): void | ||||
|     { | ||||
|         app('log')->debug(sprintf('Now at start of CreateRecurringTransactions() job for %s.', $this->date->format('D d M Y'))); | ||||
|         Log::debug(sprintf('Now at start of CreateRecurringTransactions() job for %s.', $this->date->format('D d M Y'))); | ||||
| 
 | ||||
|         // only use recurrences from database if there is no collection submitted.
 | ||||
|         if (0 !== count($this->recurrences)) { | ||||
|             app('log')->debug('Using predetermined set of recurrences.'); | ||||
|             Log::debug('Using predetermined set of recurrences.'); | ||||
|         } | ||||
|         if (0 === count($this->recurrences)) { | ||||
|             app('log')->debug('Grab all recurrences from the database.'); | ||||
|             Log::debug('Grab all recurrences from the database.'); | ||||
|             $this->recurrences = $this->repository->getAll(); | ||||
|         } | ||||
| 
 | ||||
|         $result          = []; | ||||
|         $count           = $this->recurrences->count(); | ||||
|         $this->submitted = $count; | ||||
|         app('log')->debug(sprintf('Count of collection is %d', $count)); | ||||
|         Log::debug(sprintf('Count of collection is %d', $count)); | ||||
| 
 | ||||
|         // filter recurrences:
 | ||||
|         $filtered        = $this->filterRecurrences($this->recurrences); | ||||
|         app('log')->debug(sprintf('Left after filtering is %d', $filtered->count())); | ||||
|         Log::debug(sprintf('Left after filtering is %d', $filtered->count())); | ||||
| 
 | ||||
|         /** @var Recurrence $recurrence */ | ||||
|         foreach ($filtered as $recurrence) { | ||||
| @@ -133,20 +134,20 @@ class CreateRecurringTransactions implements ShouldQueue | ||||
|             // clear cache for user
 | ||||
|             app('preferences')->setForUser($recurrence->user, 'lastActivity', microtime()); | ||||
| 
 | ||||
|             app('log')->debug(sprintf('Now at recurrence #%d of user #%d', $recurrence->id, $recurrence->user_id)); | ||||
|             Log::debug(sprintf('Now at recurrence #%d of user #%d', $recurrence->id, $recurrence->user_id)); | ||||
|             $createdReps                  = $this->handleRepetitions($recurrence); | ||||
|             app('log')->debug(sprintf('Done with recurrence #%d', $recurrence->id)); | ||||
|             Log::debug(sprintf('Done with recurrence #%d', $recurrence->id)); | ||||
|             $result[$recurrence->user_id] = $result[$recurrence->user_id]->merge($createdReps); | ||||
|             ++$this->executed; | ||||
|         } | ||||
| 
 | ||||
|         app('log')->debug('Now running report thing.'); | ||||
|         Log::debug('Now running report thing.'); | ||||
|         // will now send email to users.
 | ||||
|         foreach ($result as $userId => $journals) { | ||||
|             event(new RequestedReportOnJournals($userId, $journals)); | ||||
|         } | ||||
| 
 | ||||
|         app('log')->debug('Done with handle()'); | ||||
|         Log::debug('Done with handle()'); | ||||
| 
 | ||||
|         // clear cache:
 | ||||
|         app('preferences')->mark(); | ||||
| @@ -166,10 +167,10 @@ class CreateRecurringTransactions implements ShouldQueue | ||||
|      */ | ||||
|     private function validRecurrence(Recurrence $recurrence): bool | ||||
|     { | ||||
|         app('log')->debug(sprintf('Now filtering recurrence #%d, owned by user #%d', $recurrence->id, $recurrence->user_id)); | ||||
|         Log::debug(sprintf('Now filtering recurrence #%d, owned by user #%d', $recurrence->id, $recurrence->user_id)); | ||||
|         // is not active.
 | ||||
|         if (!$this->active($recurrence)) { | ||||
|             app('log')->info(sprintf('Recurrence #%d is not active. Skipped.', $recurrence->id)); | ||||
|             Log::info(sprintf('Recurrence #%d is not active. Skipped.', $recurrence->id)); | ||||
| 
 | ||||
|             return false; | ||||
|         } | ||||
| @@ -177,14 +178,15 @@ class CreateRecurringTransactions implements ShouldQueue | ||||
|         // has repeated X times.
 | ||||
|         $journalCount = $this->repository->getJournalCount($recurrence); | ||||
|         if (0 !== $recurrence->repetitions && $journalCount >= $recurrence->repetitions && false === $this->force) { | ||||
|             app('log')->info(sprintf('Recurrence #%d has run %d times, so will run no longer.', $recurrence->id, $recurrence->repetitions)); | ||||
|             Log::info(sprintf('Recurrence #%d has run %d times, so will run no longer.', $recurrence->id, $journalCount)); | ||||
| 
 | ||||
|             return false; | ||||
|         } | ||||
|         Log::debug(sprintf('Recurrence #%d has run %d times, max is %d times.', $recurrence->id, $journalCount, $recurrence->repetitions)); | ||||
| 
 | ||||
|         // is no longer running
 | ||||
|         if ($this->repeatUntilHasPassed($recurrence)) { | ||||
|             app('log')->info( | ||||
|             Log::info( | ||||
|                 sprintf( | ||||
|                     'Recurrence #%d was set to run until %s, and today\'s date is %s. Skipped.', | ||||
|                     $recurrence->id, | ||||
| @@ -198,12 +200,12 @@ class CreateRecurringTransactions implements ShouldQueue | ||||
| 
 | ||||
|         // first_date is in the future
 | ||||
|         if ($this->hasNotStartedYet($recurrence)) { | ||||
|             app('log')->info( | ||||
|             Log::info( | ||||
|                 sprintf( | ||||
|                     'Recurrence #%d is set to run on %s, and today\'s date is %s. Skipped.', | ||||
|                     $recurrence->id, | ||||
|                     $recurrence->first_date->format('Y-m-d'), | ||||
|                     $this->date->format('Y-m-d') | ||||
|                     $recurrence->first_date->format('Y-m-d H:i:s'), | ||||
|                     $this->date->format('Y-m-d H:i:s') | ||||
|                 ) | ||||
|             ); | ||||
| 
 | ||||
| @@ -212,11 +214,11 @@ class CreateRecurringTransactions implements ShouldQueue | ||||
| 
 | ||||
|         // already fired today (with success):
 | ||||
|         if (false === $this->force && $this->hasFiredToday($recurrence)) { | ||||
|             app('log')->info(sprintf('Recurrence #%d has already fired today. Skipped.', $recurrence->id)); | ||||
|             Log::info(sprintf('Recurrence #%d has already fired today. Skipped.', $recurrence->id)); | ||||
| 
 | ||||
|             return false; | ||||
|         } | ||||
|         app('log')->debug('Will be included.'); | ||||
|         Log::debug('Will be included.'); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| @@ -244,7 +246,8 @@ class CreateRecurringTransactions implements ShouldQueue | ||||
|     private function hasNotStartedYet(Recurrence $recurrence): bool | ||||
|     { | ||||
|         $startDate = $this->getStartDate($recurrence); | ||||
|         app('log')->debug(sprintf('Start date is %s', $startDate->format('Y-m-d'))); | ||||
|         Log::debug(sprintf('Start date is %s', $startDate->toW3cString())); | ||||
|         Log::debug(sprintf('Ask   date is %s', $this->date->toW3cString())); | ||||
| 
 | ||||
|         return $startDate->gt($this->date); | ||||
|     } | ||||
| @@ -283,7 +286,7 @@ class CreateRecurringTransactions implements ShouldQueue | ||||
| 
 | ||||
|         /** @var RecurrenceRepetition $repetition */ | ||||
|         foreach ($recurrence->recurrenceRepetitions as $repetition) { | ||||
|             app('log')->debug( | ||||
|             Log::debug( | ||||
|                 sprintf( | ||||
|                     'Now repeating %s with value "%s", skips every %d time(s)', | ||||
|                     $repetition->repetition_type, | ||||
| @@ -338,63 +341,62 @@ class CreateRecurringTransactions implements ShouldQueue | ||||
|         if ($date->ne($this->date)) { | ||||
|             return null; | ||||
|         } | ||||
|         app('log')->debug(sprintf('%s IS today (%s)', $date->format('Y-m-d'), $this->date->format('Y-m-d'))); | ||||
|         Log::debug(sprintf('%s IS today (%s)', $date->format('Y-m-d'), $this->date->format('Y-m-d'))); | ||||
| 
 | ||||
|         // count created journals on THIS day.
 | ||||
|         $journalCount            = $this->repository->getJournalCount($recurrence, $date, $date); | ||||
|         $journalCount               = $this->repository->getJournalCount($recurrence, $date, $date); | ||||
|         if ($journalCount > 0 && false === $this->force) { | ||||
|             app('log')->info(sprintf('Already created %d journal(s) for date %s', $journalCount, $date->format('Y-m-d'))); | ||||
|             Log::info(sprintf('Already created %d journal(s) for date %s', $journalCount, $date->format('Y-m-d'))); | ||||
| 
 | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         if ($this->repository->createdPreviously($recurrence, $date) && false === $this->force) { | ||||
|             app('log')->info('There is a transaction already made for this date, so will not be created now'); | ||||
|             Log::info('There is a transaction already made for this date, so will not be created now'); | ||||
| 
 | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         if ($journalCount > 0 && true === $this->force) { | ||||
|             app('log')->warning(sprintf('Already created %d groups for date %s but FORCED to continue.', $journalCount, $date->format('Y-m-d'))); | ||||
|             Log::warning(sprintf('Already created %d groups for date %s but FORCED to continue.', $journalCount, $date->format('Y-m-d'))); | ||||
|         } | ||||
| 
 | ||||
|         // create transaction array and send to factory.
 | ||||
|         $groupTitle              = null; | ||||
|         $count                   = $recurrence->recurrenceTransactions->count(); | ||||
|         $groupTitle                 = null; | ||||
|         $count                      = $recurrence->recurrenceTransactions->count(); | ||||
|         // #8844, if there is one recurrence transaction, use the first title as the title.
 | ||||
|         if (1 === $count) { | ||||
|             /** @var RecurrenceTransaction $first */ | ||||
|             $first      = $recurrence->recurrenceTransactions()->first(); | ||||
|             $groupTitle = $first->description; | ||||
|         } | ||||
|         // #9305, if there is one recurrence transaction, group title must be NULL.
 | ||||
|         $groupTitle                 = null; | ||||
| 
 | ||||
|         // #8844, if there are more, use the recurrence transaction itself.
 | ||||
|         if ($count > 1) { | ||||
|             $groupTitle = $recurrence->title; | ||||
|         } | ||||
| 
 | ||||
|         if (0 === $count) { | ||||
|             app('log')->error('No transactions to be created in this recurrence. Cannot continue.'); | ||||
|             Log::error('No transactions to be created in this recurrence. Cannot continue.'); | ||||
| 
 | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         $array                   = [ | ||||
|         $array                      = [ | ||||
|             'user'         => $recurrence->user_id, | ||||
|             'group_title'  => $groupTitle, | ||||
|             'transactions' => $this->getTransactionData($recurrence, $repetition, $date), | ||||
|         ]; | ||||
| 
 | ||||
|         /** @var TransactionGroup $group */ | ||||
|         $group                   = $this->groupRepository->store($array); | ||||
|         $group                      = $this->groupRepository->store($array); | ||||
|         ++$this->created; | ||||
|         app('log')->info(sprintf('Created new transaction group #%d', $group->id)); | ||||
|         Log::info(sprintf('Created new transaction group #%d', $group->id)); | ||||
| 
 | ||||
|         // trigger event:
 | ||||
|         event(new StoredTransactionGroup($group, $recurrence->apply_rules, true)); | ||||
|         $this->groups->push($group); | ||||
| 
 | ||||
|         // update recurring thing:
 | ||||
|         $recurrence->latest_date = $date; | ||||
|         $recurrence->latest_date    = $date; | ||||
|         $recurrence->latest_date_tz = $date?->format('e'); | ||||
|         $recurrence->save(); | ||||
| 
 | ||||
|         return $group; | ||||
| @@ -459,6 +461,7 @@ class CreateRecurringTransactions implements ShouldQueue | ||||
|     { | ||||
|         $newDate    = clone $date; | ||||
|         $newDate->startOfDay(); | ||||
|         Log::debug(sprintf('Overruled date to "%s', $newDate->format('Y-m-d H:i:s'))); | ||||
|         $this->date = $newDate; | ||||
|     } | ||||
| 
 | ||||
|   | ||||
| @@ -21,7 +21,7 @@ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V3\AccountBalances; | ||||
| namespace FireflyIII\JsonApi\V2\AccountBalances; | ||||
| 
 | ||||
| use FireflyIII\Entities\AccountBalance; | ||||
| use LaravelJsonApi\Contracts\Store\QueriesAll; | ||||
| @@ -2,7 +2,7 @@ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V3\AccountBalances; | ||||
| namespace FireflyIII\JsonApi\V2\AccountBalances; | ||||
| 
 | ||||
| use Illuminate\Http\Request; | ||||
| use LaravelJsonApi\Core\Resources\JsonApiResource; | ||||
| @@ -2,7 +2,7 @@ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V3\AccountBalances; | ||||
| namespace FireflyIII\JsonApi\V2\AccountBalances; | ||||
| 
 | ||||
| use FireflyIII\Entities\AccountBalance; | ||||
| use LaravelJsonApi\Core\Schema\Schema; | ||||
| @@ -21,7 +21,7 @@ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V3\AccountBalances\Capabilities; | ||||
| namespace FireflyIII\JsonApi\V2\AccountBalances\Capabilities; | ||||
| 
 | ||||
| use FireflyIII\Entities\AccountBalance; | ||||
| use FireflyIII\Models\Account; | ||||
							
								
								
									
										77
									
								
								app/JsonApi/V2/Accounts/AccountCollectionQuery.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								app/JsonApi/V2/Accounts/AccountCollectionQuery.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V2\Accounts; | ||||
| 
 | ||||
| use FireflyIII\Models\Account; | ||||
| use FireflyIII\Rules\Account\IsValidAccountType; | ||||
| use FireflyIII\Rules\IsAllowedGroupAction; | ||||
| use FireflyIII\Rules\IsDateOrTime; | ||||
| use FireflyIII\Rules\IsValidDateRange; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use LaravelJsonApi\Laravel\Http\Requests\ResourceQuery; | ||||
| use LaravelJsonApi\Validation\Rule as JsonApiRule; | ||||
| 
 | ||||
| class AccountCollectionQuery extends ResourceQuery | ||||
| { | ||||
|     /** | ||||
|      * Get the validation rules that apply to the request query parameters. | ||||
|      */ | ||||
|     public function rules(): array | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
|         $validFilters = config('api.valid_api_filters')[Account::class]; | ||||
| 
 | ||||
|         return [ | ||||
|             'fields'        => [ | ||||
|                 'nullable', | ||||
|                 'array', | ||||
|                 JsonApiRule::fieldSets(), | ||||
|             ], | ||||
|             'userGroupId'   => [ | ||||
|                 'nullable', | ||||
|                 'integer', | ||||
|                 new IsAllowedGroupAction(Account::class, request()->method()), | ||||
|             ], | ||||
|             'startPeriod'   => [ | ||||
|                 'nullable', | ||||
|                 'date', | ||||
|                 new IsDateOrTime(), | ||||
|                 new IsValidDateRange(), | ||||
|             ], | ||||
|             'endPeriod'     => [ | ||||
|                 'nullable', | ||||
|                 'date', | ||||
|                 new IsDateOrTime(), | ||||
|                 new IsValidDateRange(), | ||||
|             ], | ||||
|             'filter'        => [ | ||||
|                 'nullable', | ||||
|                 'array', | ||||
|                 JsonApiRule::filter($validFilters), | ||||
|                 new IsValidAccountType(), | ||||
|             ], | ||||
|             'include'       => [ | ||||
|                 'nullable', | ||||
|                 'string', | ||||
|                 JsonApiRule::includePaths(), | ||||
|             ], | ||||
|             'page'          => [ | ||||
|                 'nullable', | ||||
|                 'array', | ||||
|                 JsonApiRule::page(), | ||||
|             ], | ||||
|             'sort'          => [ | ||||
|                 'nullable', | ||||
|                 'string', | ||||
|                 JsonApiRule::sort(), | ||||
|             ], | ||||
|             'withCount'     => [ | ||||
|                 'nullable', | ||||
|                 'string', | ||||
|                 JsonApiRule::countable(), | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										112
									
								
								app/JsonApi/V2/Accounts/AccountRepository.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								app/JsonApi/V2/Accounts/AccountRepository.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| <?php | ||||
| /* | ||||
|  * AccountRepository.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V2\Accounts; | ||||
| 
 | ||||
| use FireflyIII\Models\Account; | ||||
| use FireflyIII\Support\JsonApi\Concerns\UsergroupAware; | ||||
| use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use LaravelJsonApi\Contracts\Store\CreatesResources; | ||||
| use LaravelJsonApi\Contracts\Store\QueriesAll; | ||||
| use LaravelJsonApi\NonEloquent\AbstractRepository; | ||||
| use LaravelJsonApi\NonEloquent\Capabilities\CrudRelations; | ||||
| use LaravelJsonApi\NonEloquent\Concerns\HasCrudCapability; | ||||
| use LaravelJsonApi\NonEloquent\Concerns\HasRelationsCapability; | ||||
| 
 | ||||
| /** | ||||
|  * Class AccountRepository | ||||
|  * | ||||
|  * The repository collects a single or many (account) objects from the database and returns them to the | ||||
|  * account resource. The account resource links all account properties to the JSON properties. | ||||
|  * | ||||
|  * For the queryAll thing, a separate query is constructed that does the actual querying of the database. | ||||
|  * This is necessary because the user can't just query all accounts (it would return other user's data) | ||||
|  * and because we also need to collect all kinds of metadata, like the currency and user info. | ||||
|  */ | ||||
| class AccountRepository extends AbstractRepository implements QueriesAll, CreatesResources | ||||
| { | ||||
|     use HasCrudCapability; | ||||
|     use HasRelationsCapability; | ||||
|     use UsergroupAware; | ||||
| 
 | ||||
|     /** | ||||
|      * SiteRepository constructor. | ||||
|      */ | ||||
|     public function __construct() | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
|     } | ||||
| 
 | ||||
|     public function exists(string $resourceId): bool | ||||
|     { | ||||
|         $result = null !== Account::find((int) $resourceId); | ||||
|         Log::debug(sprintf('%s: %s', __METHOD__, var_export($result, true))); | ||||
| 
 | ||||
|         return $result; | ||||
|     } | ||||
| 
 | ||||
|     public function find(string $resourceId): ?object | ||||
|     { | ||||
|         exit(__METHOD__); | ||||
|         Log::debug(__METHOD__); | ||||
|         //        throw new \RuntimeException('trace me');
 | ||||
|         $account    = Account::find((int) $resourceId); | ||||
|         if (null === $account) { | ||||
|             return null; | ||||
|         } | ||||
|         // enrich the collected data
 | ||||
|         $enrichment = new AccountEnrichment(); | ||||
| 
 | ||||
|         return $enrichment->enrichSingle($account); | ||||
|     } | ||||
| 
 | ||||
|     public function queryAll(): Capabilities\AccountQuery | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
| 
 | ||||
|         return Capabilities\AccountQuery::make() | ||||
|             ->withUserGroup($this->userGroup) | ||||
|             ->withServer($this->server) | ||||
|             ->withSchema($this->schema) | ||||
|         ; | ||||
|     } | ||||
| 
 | ||||
|     protected function crud(): Capabilities\CrudAccount | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
| 
 | ||||
|         return Capabilities\CrudAccount::make(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * TODO piggy banks | ||||
|      * TODO transactions | ||||
|      */ | ||||
|     protected function relations(): CrudRelations | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
| 
 | ||||
|         return Capabilities\CrudAccountRelations::make(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										58
									
								
								app/JsonApi/V2/Accounts/AccountRequest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								app/JsonApi/V2/Accounts/AccountRequest.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V2\Accounts; | ||||
| 
 | ||||
| use FireflyIII\Rules\Account\IsUniqueAccount; | ||||
| use FireflyIII\Rules\IsBoolean; | ||||
| use FireflyIII\Rules\IsValidPositiveAmount; | ||||
| use FireflyIII\Rules\UniqueAccountNumber; | ||||
| use FireflyIII\Rules\UniqueIban; | ||||
| use FireflyIII\Support\Request\ConvertsDataTypes; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use LaravelJsonApi\Laravel\Http\Requests\ResourceRequest; | ||||
| 
 | ||||
| class AccountRequest extends ResourceRequest | ||||
| { | ||||
|     use ConvertsDataTypes; | ||||
| 
 | ||||
|     /** | ||||
|      * Get the validation rules for the resource. | ||||
|      */ | ||||
|     public function rules(): array | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
|         $accountRoles   = implode(',', config('firefly.accountRoles')); | ||||
|         $ccPaymentTypes = implode(',', array_keys(config('firefly.ccTypes'))); | ||||
|         $types          = implode(',', array_keys(config('firefly.subTitlesByIdentifier'))); | ||||
|         $type           = $this->convertString('type'); | ||||
|         // var_dump($types);exit;
 | ||||
| 
 | ||||
|         return [ | ||||
|             'name'                         => ['required', 'max:1024', 'min:1'], // , new IsUniqueAccount()
 | ||||
|             'account_type'                 => ['required', 'max:1024', 'min:1', sprintf('in:%s', $types)], | ||||
|             //            'iban'                 => ['iban', 'nullable', new UniqueIban(null, $type)],
 | ||||
|             //            'bic'                  => 'bic|nullable',
 | ||||
|             //            'account_number'       => ['min:1', 'max:255', 'nullable', new UniqueAccountNumber(null, $type)],
 | ||||
|             //            'opening_balance'      => 'numeric|required_with:opening_balance_date|nullable',
 | ||||
|             //            'opening_balance_date' => 'date|required_with:opening_balance|nullable',
 | ||||
|             //            'virtual_balance'      => 'numeric|nullable',
 | ||||
|             //            'order'                => 'numeric|nullable',
 | ||||
|             //            'currency_id'          => 'numeric|exists:transaction_currencies,id',
 | ||||
|             //            'currency_code'        => 'min:3|max:3|exists:transaction_currencies,code',
 | ||||
|             //            'active'               => [new IsBoolean()],
 | ||||
|             //            'include_net_worth'    => [new IsBoolean()],
 | ||||
|             //            'account_role'         => sprintf('nullable|in:%s|required_if:type,asset', $accountRoles),
 | ||||
|             //            'credit_card_type'     => sprintf('nullable|in:%s|required_if:account_role,ccAsset', $ccPaymentTypes),
 | ||||
|             //            'monthly_payment_date' => 'nullable|date|required_if:account_role,ccAsset|required_if:credit_card_type,monthlyFull',
 | ||||
|             //            'liability_type'       => 'nullable|required_if:type,liability|required_if:type,liabilities|in:loan,debt,mortgage',
 | ||||
|             //            'liability_amount'     => ['required_with:liability_start_date', new IsValidPositiveAmount()],
 | ||||
|             //            'liability_start_date' => 'required_with:liability_amount|date',
 | ||||
|             //            'liability_direction'  => 'nullable|required_if:type,liability|required_if:type,liabilities|in:credit,debit',
 | ||||
|             //            'interest'             => 'min:0|max:100|numeric',
 | ||||
|             //            'interest_period'      => sprintf('nullable|in:%s', implode(',', config('firefly.interest_periods'))),
 | ||||
|             //            'notes'                => 'min:0|max:32768',
 | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										83
									
								
								app/JsonApi/V2/Accounts/AccountResource.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								app/JsonApi/V2/Accounts/AccountResource.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V2\Accounts; | ||||
| 
 | ||||
| use FireflyIII\Models\Account; | ||||
| use Illuminate\Http\Request; | ||||
| use LaravelJsonApi\Core\Resources\JsonApiResource; | ||||
| 
 | ||||
| /** | ||||
|  * @property Account $resource | ||||
|  */ | ||||
| class AccountResource extends JsonApiResource | ||||
| { | ||||
|     /** | ||||
|      * Get the resource id. | ||||
|      */ | ||||
|     public function id(): string | ||||
|     { | ||||
|         return (string) $this->resource->id; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the resource's attributes. | ||||
|      * | ||||
|      * @param null|Request $request | ||||
|      */ | ||||
|     public function attributes($request): iterable | ||||
|     { | ||||
|         // Log::debug(__METHOD__);
 | ||||
| 
 | ||||
|         return [ | ||||
|             'created_at'              => $this->resource->created_at, | ||||
|             'updated_at'              => $this->resource->updated_at, | ||||
|             'name'                    => $this->resource->name, | ||||
|             'active'                  => $this->resource->active, | ||||
|             'order'                   => $this->resource->order, | ||||
|             'iban'                    => $this->resource->iban, | ||||
|             'account_type'            => $this->resource->account_type_string, | ||||
|             'account_role'            => $this->resource->account_role, | ||||
|             'account_number'          => '' === $this->resource->account_number ? null : $this->resource->account_number, | ||||
| 
 | ||||
|             // currency (if the account has a currency setting, otherwise NULL).
 | ||||
|             'currency_id'             => $this->resource->currency_id, | ||||
|             'currency_name'           => $this->resource->currency_name, | ||||
|             'currency_code'           => $this->resource->currency_code, | ||||
|             'currency_symbol'         => $this->resource->currency_symbol, | ||||
|             'currency_decimal_places' => $this->resource->currency_decimal_places, | ||||
|             'is_multi_currency'       => '1' === $this->resource->is_multi_currency, | ||||
| 
 | ||||
|             // balances
 | ||||
|             'balance'                 => $this->resource->balance, | ||||
|             'native_balance'          => $this->resource->native_balance, | ||||
| 
 | ||||
|             // liability things
 | ||||
|             'liability_direction'     => $this->resource->liability_direction, | ||||
|             'interest'                => $this->resource->interest, | ||||
|             'interest_period'         => $this->resource->interest_period, | ||||
|             'current_debt'            => $this->resource->current_debt, // TODO may be removed in the future.
 | ||||
| 
 | ||||
|             // other things
 | ||||
|             'last_activity'           => $this->resource->last_activity, | ||||
| 
 | ||||
|             // object group
 | ||||
|             'object_group_id'         => $this->resource->object_group_id, | ||||
|             'object_group_title'      => $this->resource->object_group_title, | ||||
|             'object_group_order'      => $this->resource->object_group_order, | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the resource's relationships. | ||||
|      * | ||||
|      * @param null|Request $request | ||||
|      */ | ||||
|     public function relationships($request): iterable | ||||
|     { | ||||
|         return [ | ||||
|             $this->relation('user')->withData($this->resource->user), | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -2,7 +2,7 @@ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V3\Accounts; | ||||
| namespace FireflyIII\JsonApi\V2\Accounts; | ||||
| 
 | ||||
| use FireflyIII\Models\Account; | ||||
| use Illuminate\Http\Request; | ||||
| @@ -10,8 +10,13 @@ use LaravelJsonApi\Core\Resources\JsonApiResource; | ||||
| 
 | ||||
| /** | ||||
|  * @property Account $resource | ||||
|  * | ||||
|  * This class collects the resources attributes, the account in this case. | ||||
|  * Generally speaking, each property here is directly related to a property on the account object itself. | ||||
|  * However, many properties are collected from other sources, like the user or the currency. | ||||
|  * As a result, the account repository is where it's at, which is where the collection takes place and is optimised. | ||||
|  */ | ||||
| class AccountResource extends JsonApiResource | ||||
| class AccountResourceOld extends JsonApiResource | ||||
| { | ||||
|     /** | ||||
|      * Get the resource's attributes. | ||||
| @@ -20,32 +25,23 @@ class AccountResource extends JsonApiResource | ||||
|      */ | ||||
|     public function attributes($request): iterable | ||||
|     { | ||||
|         // fields removed here have been migrated.
 | ||||
|         return [ | ||||
|             'created_at'      => $this->resource->created_at, | ||||
|             'updated_at'      => $this->resource->updated_at, | ||||
|             'name'            => $this->resource->name, | ||||
|             'iban'            => '' === $this->resource->iban ? null : $this->resource->iban, | ||||
|             'active'          => $this->resource->active, | ||||
|             'last_activity'   => $this->resource->last_activity, | ||||
|             'type'            => $this->resource->type, | ||||
|             'account_role'    => $this->resource->account_role, | ||||
|             'created_at'    => $this->resource->created_at, | ||||
|             'updated_at'    => $this->resource->updated_at, | ||||
|             'name'          => $this->resource->name, | ||||
| 
 | ||||
|             //            'virtual_balance' => $this->resource->virtual_balance,
 | ||||
|             //            'native_balance'  => $this->resource->native_balance,
 | ||||
|             // 'user' => $this->resource->user_array,
 | ||||
|             //            'balances' => []
 | ||||
|             //
 | ||||
|             // currency
 | ||||
|             //            'currency_id'             => $this->resource->currency_id,
 | ||||
|             //            'currency_code'           => $this->resource->currency_code,
 | ||||
|             //            'currency_symbol'         => $this->resource->currency_symbol,
 | ||||
|             //            'currency_decimal_places' => $this->resource->currency_decimal_places,
 | ||||
| 
 | ||||
|             // balance (in currency, on date)
 | ||||
|             //            'current_balance'         => $this->resource->current_balance,
 | ||||
| 
 | ||||
|             //            'current_balance'         => app('steam')->bcround(app('steam')->balance($account, $date), $decimalPlaces),
 | ||||
|             //            'current_balance_date'    => $date->toAtomString(),
 | ||||
| 
 | ||||
|             //            'notes'                   => $this->repository->getNoteText($account),
 | ||||
|             //            'monthly_payment_date'    => $monthlyPaymentDate,
 | ||||
|             //            'credit_card_type'        => $creditCardType,
 | ||||
| @@ -65,11 +61,6 @@ class AccountResource extends JsonApiResource | ||||
| 
 | ||||
|             //            'order'                          => $order,
 | ||||
| 
 | ||||
|             //            'currency_id'                    => (string) $currency->id,
 | ||||
|             //            'currency_code'                  => $currency->code,
 | ||||
|             //            'currency_symbol'                => $currency->symbol,
 | ||||
|             //            'currency_decimal_places'        => $currency->decimal_places,
 | ||||
|             //
 | ||||
|             //            'native_currency_id'             => (string) $this->default->id,
 | ||||
|             //            'native_currency_code'           => $this->default->code,
 | ||||
|             //            'native_currency_symbol'         => $this->default->symbol,
 | ||||
| @@ -86,15 +77,9 @@ class AccountResource extends JsonApiResource | ||||
|             //            'balance_difference_start'       => $diffStart,
 | ||||
|             //            'balance_difference_end'         => $diffEnd,
 | ||||
|             //
 | ||||
|             //            // more meta
 | ||||
|             //            'last_activity'                  => array_key_exists($id, $this->lastActivity) ? $this->lastActivity[$id]->toAtomString() : null,
 | ||||
|             //
 | ||||
|             //            // liability stuff
 | ||||
|             //            'liability_type'                 => $liabilityType,
 | ||||
|             //            'liability_direction'            => $liabilityDirection,
 | ||||
|             //            'interest'                       => $interest,
 | ||||
|             //            'interest_period'                => $interestPeriod,
 | ||||
|             //            'current_debt'                   => $currentDebt,
 | ||||
|             //
 | ||||
|             //            // object group
 | ||||
|             //            'object_group_id'                => null !== $objectGroupId ? (string) $objectGroupId : null,
 | ||||
| @@ -123,7 +108,8 @@ class AccountResource extends JsonApiResource | ||||
|     { | ||||
|         return [ | ||||
|             $this->relation('user')->withData($this->resource->user), | ||||
|             $this->relation('account_balances')->withData($this->resource->balances), | ||||
|             $this->relation('currency')->withData($this->resource->transactionCurrency), | ||||
|             // $this->relation('account_balances')->withData($this->resource->balances),
 | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										112
									
								
								app/JsonApi/V2/Accounts/AccountSchema.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								app/JsonApi/V2/Accounts/AccountSchema.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V2\Accounts; | ||||
| 
 | ||||
| use FireflyIII\Models\Account; | ||||
| use FireflyIII\Support\JsonApi\Concerns\UsergroupAware; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use LaravelJsonApi\Core\Schema\Schema; | ||||
| use LaravelJsonApi\Eloquent\Fields\Relations\HasOne; | ||||
| use LaravelJsonApi\NonEloquent\Fields\Attribute; | ||||
| use LaravelJsonApi\NonEloquent\Fields\ID; | ||||
| use LaravelJsonApi\NonEloquent\Filters\Filter; | ||||
| use LaravelJsonApi\NonEloquent\Pagination\EnumerablePagination; | ||||
| 
 | ||||
| class AccountSchema extends Schema | ||||
| { | ||||
|     use UsergroupAware; | ||||
| 
 | ||||
|     /** | ||||
|      * The model the schema corresponds to. | ||||
|      */ | ||||
|     public static string $model = Account::class; | ||||
| 
 | ||||
|     /** | ||||
|      * Get the resource fields. | ||||
|      */ | ||||
|     public function fields(): array | ||||
|     { | ||||
|         return [ | ||||
|             ID::make(), | ||||
|             Attribute::make('created_at'), | ||||
|             Attribute::make('updated_at'), | ||||
| 
 | ||||
|             // basic info and meta data
 | ||||
|             Attribute::make('name')->sortable(), | ||||
|             Attribute::make('active')->sortable(), | ||||
|             Attribute::make('order')->sortable(), | ||||
|             Attribute::make('iban')->sortable(), | ||||
|             Attribute::make('account_type'), | ||||
|             Attribute::make('account_role'), | ||||
|             Attribute::make('account_number')->sortable(), | ||||
| 
 | ||||
|             // currency
 | ||||
|             Attribute::make('currency_id'), | ||||
|             Attribute::make('currency_name'), | ||||
|             Attribute::make('currency_code'), | ||||
|             Attribute::make('currency_symbol'), | ||||
|             Attribute::make('currency_decimal_places'), | ||||
|             Attribute::make('is_multi_currency'), | ||||
| 
 | ||||
|             // balance
 | ||||
|             Attribute::make('balance')->sortable(), | ||||
|             Attribute::make('native_balance')->sortable(), | ||||
| 
 | ||||
|             // liability things
 | ||||
|             Attribute::make('liability_direction'), | ||||
|             Attribute::make('interest'), | ||||
|             Attribute::make('interest_period'), | ||||
|             // Attribute::make('current_debt')->sortable(),
 | ||||
| 
 | ||||
|             // TODO credit card fields.
 | ||||
| 
 | ||||
|             // dynamic data
 | ||||
|             Attribute::make('last_activity')->sortable(), | ||||
|             Attribute::make('balance_difference')->sortable(), // only used for sort.
 | ||||
| 
 | ||||
|             // group
 | ||||
|             Attribute::make('object_group_id'), | ||||
|             Attribute::make('object_group_title'), | ||||
|             Attribute::make('object_group_order'), | ||||
| 
 | ||||
|             // relations.
 | ||||
|             HasOne::make('user')->readOnly(), | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the resource filters. | ||||
|      */ | ||||
|     public function filters(): array | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
|         $array  = []; | ||||
|         $config = config('api.valid_api_filters')[Account::class]; | ||||
|         foreach ($config as $entry) { | ||||
|             $array[] = Filter::make($entry); | ||||
|         } | ||||
| 
 | ||||
|         return $array; | ||||
|     } | ||||
| 
 | ||||
|     public function repository(): AccountRepository | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
|         $this->setUserGroup($this->server->getUsergroup()); | ||||
| 
 | ||||
|         return AccountRepository::make() | ||||
|             ->withServer($this->server) | ||||
|             ->withSchema($this) | ||||
|             ->withUserGroup($this->userGroup) | ||||
|         ; | ||||
|     } | ||||
| 
 | ||||
|     public function pagination(): EnumerablePagination | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
| 
 | ||||
|         return EnumerablePagination::make(); | ||||
|     } | ||||
| } | ||||
| @@ -2,7 +2,7 @@ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V3\Accounts; | ||||
| namespace FireflyIII\JsonApi\V2\Accounts; | ||||
| 
 | ||||
| use FireflyIII\Models\Account; | ||||
| use LaravelJsonApi\Eloquent\Contracts\Paginator; | ||||
| @@ -17,7 +17,14 @@ use LaravelJsonApi\Eloquent\Filters\WhereIdIn; | ||||
| use LaravelJsonApi\Eloquent\Pagination\PagePagination; | ||||
| use LaravelJsonApi\Eloquent\Schema; | ||||
| 
 | ||||
| class AccountSchema extends Schema | ||||
| /** | ||||
|  * Class AccountSchema | ||||
|  * | ||||
|  * This is the schema of all fields that an account exposes to the world. | ||||
|  * Fields do not have to have a relation to the actual model. | ||||
|  * Fields mentioned here still need to be filled in by the AccountResource. | ||||
|  */ | ||||
| class AccountSchemaOld extends Schema | ||||
| { | ||||
|     /** | ||||
|      * The model the schema corresponds to. | ||||
| @@ -34,18 +41,19 @@ class AccountSchema extends Schema | ||||
|             DateTime::make('created_at')->sortable()->readOnly(), | ||||
|             DateTime::make('updated_at')->sortable()->readOnly(), | ||||
|             Str::make('name')->sortable(), | ||||
|             Str::make('account_type'), | ||||
|             Str::make('virtual_balance'), | ||||
|             Str::make('iban'), | ||||
|             Boolean::make('active'), | ||||
|             Number::make('order'), | ||||
|             HasOne::make('user'), | ||||
|             HasMany::make('account_balances'), | ||||
|             //            Str::make('account_type'),
 | ||||
|             //            Str::make('virtual_balance'),
 | ||||
|             //            Str::make('iban'),
 | ||||
|             //            Boolean::make('active'),
 | ||||
|             //            Number::make('order'),
 | ||||
|             HasOne::make('user')->readOnly(), | ||||
|             // HasMany::make('account_balances'),
 | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the resource filters. | ||||
|      * Filters mentioned here can be used to filter the results. | ||||
|      * TODO write down exactly how this works. | ||||
|      */ | ||||
|     public function filters(): array | ||||
|     { | ||||
							
								
								
									
										45
									
								
								app/JsonApi/V2/Accounts/AccountSingleQuery.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/JsonApi/V2/Accounts/AccountSingleQuery.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V2\Accounts; | ||||
| 
 | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use LaravelJsonApi\Laravel\Http\Requests\ResourceQuery; | ||||
| use LaravelJsonApi\Validation\Rule as JsonApiRule; | ||||
| 
 | ||||
| class AccountSingleQuery extends ResourceQuery | ||||
| { | ||||
|     /** | ||||
|      * Get the validation rules that apply to the request query parameters. | ||||
|      */ | ||||
|     public function rules(): array | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
| 
 | ||||
|         return [ | ||||
|             'fields'    => [ | ||||
|                 'nullable', | ||||
|                 'array', | ||||
|                 JsonApiRule::fieldSets(), | ||||
|             ], | ||||
|             'filter'    => [ | ||||
|                 'nullable', | ||||
|                 'array', | ||||
|                 JsonApiRule::filter()->forget('id'), | ||||
|             ], | ||||
|             'include'   => [ | ||||
|                 'nullable', | ||||
|                 'string', | ||||
|                 JsonApiRule::includePaths(), | ||||
|             ], | ||||
|             'page'      => JsonApiRule::notSupported(), | ||||
|             'sort'      => JsonApiRule::notSupported(), | ||||
|             'withCount' => [ | ||||
|                 'nullable', | ||||
|                 'string', | ||||
|                 JsonApiRule::countable(), | ||||
|             ], | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										141
									
								
								app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								app/JsonApi/V2/Accounts/Capabilities/AccountQuery.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| <?php | ||||
| /* | ||||
|  * AccountQuery.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V2\Accounts\Capabilities; | ||||
| 
 | ||||
| use FireflyIII\Models\Account; | ||||
| use FireflyIII\Support\Http\Api\AccountFilter; | ||||
| use FireflyIII\Support\JsonApi\CollectsCustomParameters; | ||||
| use FireflyIII\Support\JsonApi\Concerns\UsergroupAware; | ||||
| use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; | ||||
| use FireflyIII\Support\JsonApi\ExpandsQuery; | ||||
| use FireflyIII\Support\JsonApi\FiltersPagination; | ||||
| use FireflyIII\Support\JsonApi\SortsCollection; | ||||
| use FireflyIII\Support\JsonApi\SortsQueryResults; | ||||
| use FireflyIII\Support\JsonApi\ValidateSortParameters; | ||||
| use Illuminate\Pagination\LengthAwarePaginator; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use LaravelJsonApi\Contracts\Pagination\Page; | ||||
| use LaravelJsonApi\Contracts\Store\HasPagination; | ||||
| use LaravelJsonApi\NonEloquent\Capabilities\QueryAll; | ||||
| 
 | ||||
| class AccountQuery extends QueryAll implements HasPagination | ||||
| { | ||||
|     use AccountFilter; | ||||
|     use CollectsCustomParameters; | ||||
|     use ExpandsQuery; | ||||
|     use FiltersPagination; | ||||
|     use SortsCollection; | ||||
|     use SortsQueryResults; | ||||
|     use UsergroupAware; | ||||
|     use ValidateSortParameters; | ||||
| 
 | ||||
|     // use PaginatesEnumerables;
 | ||||
| 
 | ||||
|     #[\Override]
 | ||||
|     /** | ||||
|      * This method returns all accounts, given a bunch of filters and sort fields, together with pagination. | ||||
|      * | ||||
|      * It is only used on the index, and nowhere else. | ||||
|      */ | ||||
|     public function get(): iterable | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
|         // collect sort options
 | ||||
|         $sort        = $this->queryParameters->sortFields(); | ||||
| 
 | ||||
|         // collect pagination based on the page
 | ||||
|         $pagination  = $this->filtersPagination($this->queryParameters->page()); | ||||
| 
 | ||||
|         // check if we need all accounts, regardless of pagination
 | ||||
|         // This is necessary when the user wants to sort on specific params.
 | ||||
|         $needsAll    = $this->needsFullDataset(Account::class, $sort); | ||||
| 
 | ||||
|         // params that were not recognised, may be my own custom stuff.
 | ||||
|         $otherParams = $this->getOtherParams($this->queryParameters->unrecognisedParameters()); | ||||
| 
 | ||||
|         // start the query
 | ||||
|         $query       = $this->userGroup->accounts(); | ||||
| 
 | ||||
|         // add sort and filter parameters to the query.
 | ||||
|         $query       = $this->addSortParams(Account::class, $query, $sort); | ||||
|         $query       = $this->addFilterParams(Account::class, $query, $this->queryParameters->filter()); | ||||
| 
 | ||||
|         // collect the result.
 | ||||
|         $collection  = $query->get(['accounts.*']); | ||||
|         // sort the data after the query, and return it right away.
 | ||||
|         $collection  = $this->sortCollection(Account::class, $collection, $sort); | ||||
| 
 | ||||
|         // if the entire collection needs to be enriched and sorted, do so now:
 | ||||
|         $totalCount  = $collection->count(); | ||||
|         Log::debug(sprintf('Total is %d', $totalCount)); | ||||
|         if ($needsAll) { | ||||
|             Log::debug('Needs the entire collection'); | ||||
|             // enrich the entire collection
 | ||||
|             $enrichment  = new AccountEnrichment(); | ||||
|             $enrichment->setStart($otherParams['start'] ?? null); | ||||
|             $enrichment->setEnd($otherParams['end'] ?? null); | ||||
|             $collection  = $enrichment->enrich($collection); | ||||
| 
 | ||||
|             // TODO sort the set based on post-query sort options:
 | ||||
|             $collection  = $this->postQuerySort(Account::class, $collection, $sort); | ||||
| 
 | ||||
|             // take the current page from the enriched set.
 | ||||
|             $currentPage = $collection->skip(($pagination['number'] - 1) * $pagination['size'])->take($pagination['size']); | ||||
|         } | ||||
|         if (!$needsAll) { | ||||
|             Log::debug('Needs only partial collection'); | ||||
|             // take from the collection the filtered page + page number:
 | ||||
|             $currentPage = $collection->skip(($pagination['number'] - 1) * $pagination['size'])->take($pagination['size']); | ||||
| 
 | ||||
|             // enrich only the current page.
 | ||||
|             $enrichment  = new AccountEnrichment(); | ||||
|             $enrichment->setStart($otherParams['start'] ?? null); | ||||
|             $enrichment->setEnd($otherParams['end'] ?? null); | ||||
|             $currentPage = $enrichment->enrich($currentPage); | ||||
|         } | ||||
|         // get current page?
 | ||||
|         Log::debug(sprintf('Skip %d, take %d', ($pagination['number'] - 1) * $pagination['size'], $pagination['size'])); | ||||
|         // $currentPage = $collection->skip(($pagination['number'] - 1) * $pagination['size'])->take($pagination['size']);
 | ||||
|         Log::debug(sprintf('New collection size: %d', $currentPage->count())); | ||||
| 
 | ||||
|         // TODO add filters after the query, if there are filters that cannot be applied to the database
 | ||||
|         // TODO same for sort things.
 | ||||
| 
 | ||||
|         return new LengthAwarePaginator($currentPage, $totalCount, $pagination['size'], $pagination['number']); | ||||
|     } | ||||
| 
 | ||||
|     #[\Override]
 | ||||
|     public function paginate(array $page): Page | ||||
|     { | ||||
|         exit('here weare'); | ||||
|         // TODO: Implement paginate() method.
 | ||||
|     } | ||||
| 
 | ||||
|     #[\Override]
 | ||||
|     public function getOrPaginate(?array $page): iterable | ||||
|     { | ||||
|         exit('here weare'); | ||||
|         // TODO: Implement getOrPaginate() method.
 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										60
									
								
								app/JsonApi/V2/Accounts/Capabilities/CrudAccount.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								app/JsonApi/V2/Accounts/Capabilities/CrudAccount.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| <?php | ||||
| /* | ||||
|  * CrudAccount.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V2\Accounts\Capabilities; | ||||
| 
 | ||||
| use FireflyIII\Models\Account; | ||||
| use FireflyIII\Support\JsonApi\CollectsCustomParameters; | ||||
| use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use LaravelJsonApi\NonEloquent\Capabilities\CrudResource; | ||||
| 
 | ||||
| class CrudAccount extends CrudResource | ||||
| { | ||||
|     use CollectsCustomParameters; | ||||
| 
 | ||||
|     /** | ||||
|      * Read the supplied site. | ||||
|      */ | ||||
|     public function read(Account $account): ?Account | ||||
|     { | ||||
|         $otherParams = $this->getOtherParams($this->request->query->all()); | ||||
| 
 | ||||
|         Log::debug(__METHOD__); | ||||
|         // enrich the collected data
 | ||||
|         $enrichment  = new AccountEnrichment(); | ||||
| 
 | ||||
|         // set start and date, if present.
 | ||||
|         $enrichment->setStart($otherParams['start'] ?? null); | ||||
|         $enrichment->setEnd($otherParams['end'] ?? null); | ||||
| 
 | ||||
|         return $enrichment->enrichSingle($account); | ||||
|     } | ||||
| 
 | ||||
|     public function create(array $validatedData): Account | ||||
|     { | ||||
|         var_dump($validatedData); | ||||
| 
 | ||||
|         exit; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,28 @@ | ||||
| <?php | ||||
| /* | ||||
|  * CrudAccountRelations.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V2\Accounts\Capabilities; | ||||
| 
 | ||||
| use LaravelJsonApi\NonEloquent\Capabilities\CrudRelations; | ||||
| 
 | ||||
| class CrudAccountRelations extends CrudRelations {} | ||||
							
								
								
									
										53
									
								
								app/JsonApi/V2/Server.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								app/JsonApi/V2/Server.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V2; | ||||
| 
 | ||||
| use FireflyIII\JsonApi\V2\Accounts\AccountSchema; | ||||
| use FireflyIII\JsonApi\V2\Users\UserSchema; | ||||
| use FireflyIII\Support\JsonApi\Concerns\UsergroupAware; | ||||
| use FireflyIII\Support\JsonApi\Concerns\UserGroupDetectable; | ||||
| use Illuminate\Support\Facades\Log; | ||||
| use LaravelJsonApi\Core\Server\Server as BaseServer; | ||||
| 
 | ||||
| /** | ||||
|  * Class Server | ||||
|  * | ||||
|  * This class serves as a generic class for the v2 API "server". | ||||
|  */ | ||||
| class Server extends BaseServer | ||||
| { | ||||
|     use UsergroupAware; | ||||
|     use UserGroupDetectable; | ||||
| 
 | ||||
|     /** | ||||
|      * The base URI namespace for this server. | ||||
|      */ | ||||
|     protected string $baseUri = '/api/v2'; | ||||
| 
 | ||||
|     /** | ||||
|      * Bootstrap the server when it is handling an HTTP request. | ||||
|      */ | ||||
|     public function serving(): void | ||||
|     { | ||||
|         Log::debug(__METHOD__); | ||||
|         // at this point the user may not actually have access to this user group.
 | ||||
|         $res = $this->detectUserGroup(); | ||||
|         $this->setUserGroup($res); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the server's list of schemas. | ||||
|      */ | ||||
|     protected function allSchemas(): array | ||||
|     { | ||||
|         // Log::debug(__METHOD__);
 | ||||
| 
 | ||||
|         return [ | ||||
|             AccountSchema::class, | ||||
|             UserSchema::class, | ||||
|             // AccountBalanceSchema::class,
 | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -2,7 +2,7 @@ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V3\Users; | ||||
| namespace FireflyIII\JsonApi\V2\Users; | ||||
| 
 | ||||
| use FireflyIII\Models\User; | ||||
| use Illuminate\Http\Request; | ||||
| @@ -2,7 +2,7 @@ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V3\Users; | ||||
| namespace FireflyIII\JsonApi\V2\Users; | ||||
| 
 | ||||
| use FireflyIII\User; | ||||
| use LaravelJsonApi\Eloquent\Contracts\Paginator; | ||||
| @@ -1,73 +0,0 @@ | ||||
| <?php | ||||
| /* | ||||
|  * AccountQuery.php | ||||
|  * Copyright (c) 2024 james@firefly-iii.org. | ||||
|  * | ||||
|  * This file is part of Firefly III (https://github.com/firefly-iii). | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see https://www.gnu.org/licenses/. | ||||
|  */ | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V3\Accounts\Capabilities; | ||||
| 
 | ||||
| use FireflyIII\Support\JsonApi\Concerns\UsergroupAware; | ||||
| use FireflyIII\Support\JsonApi\Enrichments\AccountEnrichment; | ||||
| use FireflyIII\Support\JsonApi\ExpandsQuery; | ||||
| use FireflyIII\Support\JsonApi\FiltersPagination; | ||||
| use FireflyIII\Support\JsonApi\SortsCollection; | ||||
| use FireflyIII\Support\JsonApi\ValidateSortParameters; | ||||
| use LaravelJsonApi\Contracts\Store\HasPagination; | ||||
| use LaravelJsonApi\NonEloquent\Capabilities\QueryAll; | ||||
| use LaravelJsonApi\NonEloquent\Concerns\PaginatesEnumerables; | ||||
| 
 | ||||
| class AccountQuery extends QueryAll implements HasPagination | ||||
| { | ||||
|     use ExpandsQuery; | ||||
|     use FiltersPagination; | ||||
|     use PaginatesEnumerables; | ||||
|     use SortsCollection; | ||||
|     use UsergroupAware; | ||||
|     use ValidateSortParameters; | ||||
| 
 | ||||
|     #[\Override]
 | ||||
|     public function get(): iterable | ||||
|     { | ||||
|         $filters    = $this->queryParameters->filter(); | ||||
|         $sort       = $this->queryParameters->sortFields(); | ||||
|         $pagination = $this->filtersPagination($this->queryParameters->page()); | ||||
|         $needsAll   = $this->validateParams('account', $sort); | ||||
|         $query      = $this->userGroup->accounts(); | ||||
| 
 | ||||
|         if (!$needsAll) { | ||||
|             $query = $this->addPagination($query, $pagination); | ||||
|         } | ||||
|         $query      = $this->addSortParams($query, $sort); | ||||
|         $query      = $this->addFilterParams('account', $query, $filters); | ||||
| 
 | ||||
|         $collection = $query->get(['accounts.*']); | ||||
| 
 | ||||
|         // enrich data
 | ||||
|         $enrichment = new AccountEnrichment(); | ||||
|         $collection = $enrichment->enrich($collection); | ||||
| 
 | ||||
|         // add filters after the query
 | ||||
| 
 | ||||
|         // add sort after the query
 | ||||
|         return $this->sortCollection($collection, $sort); | ||||
|         //        var_dump($filters->value('name'));
 | ||||
|         //        exit;
 | ||||
|     } | ||||
| } | ||||
| @@ -1,38 +0,0 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\JsonApi\V3; | ||||
| 
 | ||||
| use FireflyIII\JsonApi\V3\Accounts\AccountSchema; | ||||
| use FireflyIII\JsonApi\V3\AccountBalances\AccountBalanceSchema; | ||||
| use FireflyIII\JsonApi\V3\Users\UserSchema; | ||||
| use LaravelJsonApi\Core\Server\Server as BaseServer; | ||||
| 
 | ||||
| class Server extends BaseServer | ||||
| { | ||||
|     /** | ||||
|      * The base URI namespace for this server. | ||||
|      */ | ||||
|     protected string $baseUri = '/api/v3'; | ||||
| 
 | ||||
|     /** | ||||
|      * Bootstrap the server when it is handling an HTTP request. | ||||
|      */ | ||||
|     public function serving(): void | ||||
|     { | ||||
|         // no-op
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the server's list of schemas. | ||||
|      */ | ||||
|     protected function allSchemas(): array | ||||
|     { | ||||
|         return [ | ||||
|             AccountSchema::class, | ||||
|             UserSchema::class, | ||||
|             AccountBalanceSchema::class, | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| @@ -23,14 +23,12 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Models; | ||||
| 
 | ||||
| use Carbon\Carbon; | ||||
| use Eloquent; | ||||
| use FireflyIII\Support\Models\ReturnsIntegerIdTrait; | ||||
| use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; | ||||
| use FireflyIII\User; | ||||
| use GeneaLabs\LaravelModelCaching\Traits\Cachable; | ||||
| use Illuminate\Database\Eloquent\Builder as EloquentBuilder; | ||||
| use Illuminate\Database\Eloquent\Casts\Attribute; | ||||
| use Illuminate\Database\Eloquent\Collection; | ||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| @@ -38,84 +36,14 @@ use Illuminate\Database\Eloquent\Relations\HasMany; | ||||
| use Illuminate\Database\Eloquent\Relations\MorphMany; | ||||
| use Illuminate\Database\Eloquent\Relations\MorphToMany; | ||||
| use Illuminate\Database\Eloquent\SoftDeletes; | ||||
| use Illuminate\Database\Query\Builder; | ||||
| use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | ||||
| 
 | ||||
| /** | ||||
|  * Class Account | ||||
|  * | ||||
|  * @property int                      $id | ||||
|  * @property null|Carbon              $created_at | ||||
|  * @property null|Carbon              $updated_at | ||||
|  * @property null|Carbon              $deleted_at | ||||
|  * @property int                      $user_id | ||||
|  * @property int                      $account_type_id | ||||
|  * @property string                   $name | ||||
|  * @property string                   $virtual_balance | ||||
|  * @property null|string              $iban | ||||
|  * @property bool                     $active | ||||
|  * @property bool                     $encrypted | ||||
|  * @property int                      $order | ||||
|  * @property AccountMeta[]|Collection $accountMeta | ||||
|  * @property null|int                 $account_meta_count | ||||
|  * @property AccountType              $accountType | ||||
|  * @property Attachment[]|Collection  $attachments | ||||
|  * @property null|int                 $attachments_count | ||||
|  * @property string                   $account_number | ||||
|  * @property string                   $edit_name | ||||
|  * @property Collection|Location[]    $locations | ||||
|  * @property null|int                 $locations_count | ||||
|  * @property Collection|Note[]        $notes | ||||
|  * @property null|int                 $notes_count | ||||
|  * @property Collection|ObjectGroup[] $objectGroups | ||||
|  * @property null|int                 $object_groups_count | ||||
|  * @property Collection|PiggyBank[]   $piggyBanks | ||||
|  * @property null|int                 $piggy_banks_count | ||||
|  * @property Collection|Transaction[] $transactions | ||||
|  * @property null|int                 $transactions_count | ||||
|  * @property User                     $user | ||||
|  * | ||||
|  * @method static EloquentBuilder|Account accountTypeIn($types) | ||||
|  * @method static EloquentBuilder|Account newModelQuery() | ||||
|  * @method static EloquentBuilder|Account newQuery() | ||||
|  * @method static Builder|Account         onlyTrashed() | ||||
|  * @method static EloquentBuilder|Account query() | ||||
|  * @method static EloquentBuilder|Account whereAccountTypeId($value) | ||||
|  * @method static EloquentBuilder|Account whereActive($value) | ||||
|  * @method static EloquentBuilder|Account whereCreatedAt($value) | ||||
|  * @method static EloquentBuilder|Account whereDeletedAt($value) | ||||
|  * @method static EloquentBuilder|Account whereEncrypted($value) | ||||
|  * @method static EloquentBuilder|Account whereIban($value) | ||||
|  * @method static EloquentBuilder|Account whereId($value) | ||||
|  * @method static EloquentBuilder|Account whereName($value) | ||||
|  * @method static EloquentBuilder|Account whereOrder($value) | ||||
|  * @method static EloquentBuilder|Account whereUpdatedAt($value) | ||||
|  * @method static EloquentBuilder|Account whereUserId($value) | ||||
|  * @method static EloquentBuilder|Account whereVirtualBalance($value) | ||||
|  * @method static Builder|Account         withTrashed() | ||||
|  * @method static Builder|Account         withoutTrashed() | ||||
|  * | ||||
|  * @property Carbon   $lastActivityDate | ||||
|  * @property string   $startBalance | ||||
|  * @property string   $endBalance | ||||
|  * @property string   $difference | ||||
|  * @property string   $interest | ||||
|  * @property string   $interestPeriod | ||||
|  * @property string   $accountTypeString | ||||
|  * @property Location $location | ||||
|  * @property string   $liability_direction | ||||
|  * @property string   $current_debt | ||||
|  * @property int      $user_group_id | ||||
|  * | ||||
|  * @method static EloquentBuilder|Account whereUserGroupId($value) | ||||
|  * | ||||
|  * @property null|UserGroup $userGroup | ||||
|  * @property mixed          $account_id | ||||
|  * | ||||
|  * @mixin Eloquent | ||||
|  * @mixin IdeHelperAccount | ||||
|  */ | ||||
| class Account extends Model | ||||
| { | ||||
|     use Cachable; | ||||
|     use HasFactory; | ||||
|     use ReturnsIntegerIdTrait; | ||||
|     use ReturnsIntegerUserIdTrait; | ||||
| @@ -123,12 +51,13 @@ class Account extends Model | ||||
| 
 | ||||
|     protected $casts | ||||
|                                      = [ | ||||
|             'created_at' => 'datetime', | ||||
|             'updated_at' => 'datetime', | ||||
|             'user_id'    => 'integer', | ||||
|             'deleted_at' => 'datetime', | ||||
|             'active'     => 'boolean', | ||||
|             'encrypted'  => 'boolean', | ||||
|             'created_at'      => 'datetime', | ||||
|             'updated_at'      => 'datetime', | ||||
|             'user_id'         => 'integer', | ||||
|             'deleted_at'      => 'datetime', | ||||
|             'active'          => 'boolean', | ||||
|             'encrypted'       => 'boolean', | ||||
|             'virtual_balance' => 'string', | ||||
|         ]; | ||||
| 
 | ||||
|     protected $fillable              = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban']; | ||||
| @@ -144,7 +73,7 @@ class Account extends Model | ||||
|     public static function routeBinder(string $value): self | ||||
|     { | ||||
|         if (auth()->check()) { | ||||
|             $accountId = (int)$value; | ||||
|             $accountId = (int) $value; | ||||
| 
 | ||||
|             /** @var User $user */ | ||||
|             $user      = auth()->user(); | ||||
| @@ -246,7 +175,7 @@ class Account extends Model | ||||
| 
 | ||||
|     public function setVirtualBalanceAttribute(mixed $value): void | ||||
|     { | ||||
|         $value                               = (string)$value; | ||||
|         $value                               = (string) $value; | ||||
|         if ('' === $value) { | ||||
|             $value = null; | ||||
|         } | ||||
| @@ -266,7 +195,7 @@ class Account extends Model | ||||
|     protected function accountId(): Attribute | ||||
|     { | ||||
|         return Attribute::make( | ||||
|             get: static fn ($value) => (int)$value, | ||||
|             get: static fn ($value) => (int) $value, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| @@ -276,21 +205,21 @@ class Account extends Model | ||||
|     protected function accountTypeId(): Attribute | ||||
|     { | ||||
|         return Attribute::make( | ||||
|             get: static fn ($value) => (int)$value, | ||||
|             get: static fn ($value) => (int) $value, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     protected function iban(): Attribute | ||||
|     { | ||||
|         return Attribute::make( | ||||
|             get: static fn ($value) => null === $value ? null : trim(str_replace(' ', '', (string)$value)), | ||||
|             get: static fn ($value) => null === $value ? null : trim(str_replace(' ', '', (string) $value)), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     protected function order(): Attribute | ||||
|     { | ||||
|         return Attribute::make( | ||||
|             get: static fn ($value) => (int)$value, | ||||
|             get: static fn ($value) => (int) $value, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| @@ -300,7 +229,7 @@ class Account extends Model | ||||
|     protected function virtualBalance(): Attribute | ||||
|     { | ||||
|         return Attribute::make( | ||||
|             get: static fn ($value) => (string)$value, | ||||
|             get: static fn ($value) => (string) $value, | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,14 +4,26 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Models; | ||||
| 
 | ||||
| use FireflyIII\Casts\SeparateTimezoneCaster; | ||||
| use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| 
 | ||||
| /** | ||||
|  * @mixin IdeHelperAccountBalance | ||||
|  */ | ||||
| class AccountBalance extends Model | ||||
| { | ||||
|     use HasFactory; | ||||
|     protected $fillable = ['account_id', 'title', 'transaction_currency_id', 'balance']; | ||||
|     protected $fillable = ['account_id', 'title', 'transaction_currency_id', 'balance', 'date', 'date_tz']; | ||||
| 
 | ||||
|     protected function casts(): array | ||||
|     { | ||||
|         return [ | ||||
|             'date'    => SeparateTimezoneCaster::class, | ||||
|             'balance' => 'string', | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     public function account(): BelongsTo | ||||
|     { | ||||
|   | ||||
| @@ -23,35 +23,12 @@ declare(strict_types=1); | ||||
| 
 | ||||
| namespace FireflyIII\Models; | ||||
| 
 | ||||
| use Carbon\Carbon; | ||||
| use Eloquent; | ||||
| use FireflyIII\Support\Models\ReturnsIntegerIdTrait; | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| 
 | ||||
| /** | ||||
|  * Class AccountMeta | ||||
|  * | ||||
|  * @property int         $id | ||||
|  * @property null|Carbon $created_at | ||||
|  * @property null|Carbon $updated_at | ||||
|  * @property int         $account_id | ||||
|  * @property string      $name | ||||
|  * @property mixed       $data | ||||
|  * @property Account     $account | ||||
|  * | ||||
|  * @method static Builder|AccountMeta newModelQuery() | ||||
|  * @method static Builder|AccountMeta newQuery() | ||||
|  * @method static Builder|AccountMeta query() | ||||
|  * @method static Builder|AccountMeta whereAccountId($value) | ||||
|  * @method static Builder|AccountMeta whereCreatedAt($value) | ||||
|  * @method static Builder|AccountMeta whereData($value) | ||||
|  * @method static Builder|AccountMeta whereId($value) | ||||
|  * @method static Builder|AccountMeta whereName($value) | ||||
|  * @method static Builder|AccountMeta whereUpdatedAt($value) | ||||
|  * | ||||
|  * @mixin Eloquent | ||||
|  * @mixin IdeHelperAccountMeta | ||||
|  */ | ||||
| class AccountMeta extends Model | ||||
| { | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user