diff --git a/app/Api/V1/Controllers/AccountController.php b/app/Api/V1/Controllers/AccountController.php index d5e0d4d255..f7bd0580f2 100644 --- a/app/Api/V1/Controllers/AccountController.php +++ b/app/Api/V1/Controllers/AccountController.php @@ -30,6 +30,7 @@ use FireflyIII\Models\AccountType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Transformers\AccountTransformer; +use FireflyIII\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Pagination\LengthAwarePaginator; @@ -57,12 +58,14 @@ class AccountController extends Controller parent::__construct(); $this->middleware( function ($request, $next) { + /** @var User $user */ + $user = auth()->user(); // @var AccountRepositoryInterface repository $this->repository = app(AccountRepositoryInterface::class); - $this->repository->setUser(auth()->user()); + $this->repository->setUser($user); $this->currencyRepository = app(CurrencyRepositoryInterface::class); - $this->currencyRepository->setUser(auth()->user()); + $this->currencyRepository->setUser($user); return $next($request); } @@ -93,7 +96,7 @@ class AccountController extends Controller public function index(Request $request): JsonResponse { // create some objects: - $manager = new Manager(); + $manager = new Manager; $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; // read type from URI @@ -129,7 +132,7 @@ class AccountController extends Controller */ public function show(Request $request, Account $account): JsonResponse { - $manager = new Manager(); + $manager = new Manager; // add include parameter: $include = $request->get('include') ?? ''; @@ -156,7 +159,7 @@ class AccountController extends Controller $data['currency_id'] = null === $currency ? 0 : $currency->id; } $account = $this->repository->store($data); - $manager = new Manager(); + $manager = new Manager; $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; $manager->setSerializer(new JsonApiSerializer($baseUrl)); @@ -184,7 +187,7 @@ class AccountController extends Controller // set correct type: $data['type'] = config('firefly.shortNamesByFullName.' . $account->accountType->type); $this->repository->update($account, $data); - $manager = new Manager(); + $manager = new Manager; $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; $manager->setSerializer(new JsonApiSerializer($baseUrl)); diff --git a/app/Api/V1/Controllers/AttachmentController.php b/app/Api/V1/Controllers/AttachmentController.php new file mode 100644 index 0000000000..5c67f0e387 --- /dev/null +++ b/app/Api/V1/Controllers/AttachmentController.php @@ -0,0 +1,227 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V1\Controllers; + +use FireflyIII\Api\V1\Requests\AttachmentRequest; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; +use FireflyIII\Models\Attachment; +use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface; +use FireflyIII\Transformers\AttachmentTransformer; +use FireflyIII\User; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use Illuminate\Http\Response as LaravelResponse; +use Illuminate\Pagination\LengthAwarePaginator; +use League\Fractal\Manager; +use League\Fractal\Pagination\IlluminatePaginatorAdapter; +use League\Fractal\Resource\Collection as FractalCollection; +use League\Fractal\Resource\Item; +use League\Fractal\Serializer\JsonApiSerializer; + +/** + * Class AttachmentController + */ +class AttachmentController extends Controller +{ + /** @var AttachmentRepositoryInterface */ + private $repository; + + /** + * AccountController constructor. + */ + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + /** @var User $user */ + $user = auth()->user(); + $this->repository = app(AttachmentRepositoryInterface::class); + $this->repository->setUser($user); + + return $next($request); + } + ); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * + * @return \Illuminate\Http\Response + */ + public function destroy($id) + { + // + } + + /** + * @param Attachment $attachment + * + * @return LaravelResponse + * @throws FireflyException + */ + public function download(Attachment $attachment): LaravelResponse + { + if ($attachment->uploaded === false) { + throw new FireflyException('No file has been uploaded for this attachment (yet).'); + } + if ($this->repository->exists($attachment)) { + $content = $this->repository->getContent($attachment); + $quoted = sprintf('"%s"', addcslashes(basename($attachment->filename), '"\\')); + + /** @var LaravelResponse $response */ + $response = response($content, 200); + $response + ->header('Content-Description', 'File Transfer') + ->header('Content-Type', 'application/octet-stream') + ->header('Content-Disposition', 'attachment; filename=' . $quoted) + ->header('Content-Transfer-Encoding', 'binary') + ->header('Connection', 'Keep-Alive') + ->header('Expires', '0') + ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->header('Pragma', 'public') + ->header('Content-Length', \strlen($content)); + + return $response; + } + throw new FireflyException('Could not find the indicated attachment. The file is no longer there.'); + } + + /** + * Display a listing of the resource. + * + * @param Request $request + * + * @return JsonResponse + */ + public function index(Request $request): JsonResponse + { + // create some objects: + $manager = new Manager; + $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; + + // types to get, page size: + $pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data; + + // get list of accounts. Count it and split it. + $collection = $this->repository->get(); + $count = $collection->count(); + $attachments = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize); + + // make paginator: + $paginator = new LengthAwarePaginator($attachments, $count, $pageSize, $this->parameters->get('page')); + $paginator->setPath(route('api.v1.attachments.index') . $this->buildParams()); + + // present to user. + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + $resource = new FractalCollection($attachments, new AttachmentTransformer($this->parameters), 'attachments'); + $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); + + return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); + } + + /** + * Display the specified resource. + * + * @param Request $request + * @param Attachment $attachment + * + * @return JsonResponse + */ + public function show(Request $request, Attachment $attachment): JsonResponse + { + $manager = new Manager; + + // add include parameter: + $include = $request->get('include') ?? ''; + $manager->parseIncludes($include); + + $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + $resource = new Item($attachment, new AttachmentTransformer($this->parameters), 'attachments'); + + return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * + * @return \Illuminate\Http\Response + * @throws FireflyException + */ + public function store(AttachmentRequest $request): JsonResponse + { + $data = $request->getAll(); + $attachment = $this->repository->store($data); + $manager = new Manager; + $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + $resource = new Item($attachment, new AttachmentTransformer($this->parameters), 'attachments'); + + return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); + } + + /** + * Update the specified resource in storage. + * + * @param AttachmentRequest $request + * @param Attachment $attachment + * + * @return JsonResponse + */ + public function update(AttachmentRequest $request, Attachment $attachment): JsonResponse + { + $data = $request->getAll(); + $this->repository->update($attachment, $data); + $manager = new Manager; + $baseUrl = $request->getSchemeAndHttpHost() . '/api/v1'; + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + + $resource = new Item($attachment, new AttachmentTransformer($this->parameters), 'attachments'); + + return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json'); + } + + /** + * @param Request $request + * @param Attachment $attachment + * + * @return LaravelResponse + */ + public function upload(Request $request, Attachment $attachment): LaravelResponse + { + /** @var AttachmentHelperInterface $helper */ + $helper = app(AttachmentHelperInterface::class); + $body = $request->getContent(); + $helper->saveAttachmentFromApi($attachment, $body); + + return response('', 200); + } + +} \ No newline at end of file diff --git a/app/Api/V1/Requests/AttachmentRequest.php b/app/Api/V1/Requests/AttachmentRequest.php new file mode 100644 index 0000000000..30da4d2683 --- /dev/null +++ b/app/Api/V1/Requests/AttachmentRequest.php @@ -0,0 +1,92 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Api\V1\Requests; + +use FireflyIII\Models\Bill; +use FireflyIII\Models\ImportJob; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Rules\IsBase64; +use FireflyIII\Rules\IsValidAttachmentModel; + +/** + * Class AttachmentRequest + */ +class AttachmentRequest extends Request +{ + /** + * @return bool + */ + public function authorize(): bool + { + // Only allow authenticated users + return auth()->check(); + } + + /** + * @return array + */ + public function getAll(): array + { + return [ + 'filename' => $this->string('filename'), + 'title' => $this->string('title'), + 'notes' => $this->string('notes'), + 'model' => $this->string('model'), + 'model_id' => $this->integer('model_id'), + ]; + } + + /** + * @return array + */ + public function rules(): array + { + $models = implode( + ',', [ + Bill::class, + ImportJob::class, + TransactionJournal::class, + ] + ); + $model = $this->string('model'); + $rules = [ + 'filename' => 'required|between:1,255', + 'title' => 'between:1,255', + 'notes' => 'between:1,65000', + 'model' => sprintf('required|in:%s', $models), + 'model_id' => ['required', 'numeric', new IsValidAttachmentModel($model)], + ]; + switch ($this->method()) { + default: + break; + case 'PUT': + case 'PATCH': + unset($rules['model'], $rules['model_id']); + $rules['filename'] = 'between:1,255'; + break; + } + + return $rules; + } +} \ No newline at end of file diff --git a/app/Factory/AttachmentFactory.php b/app/Factory/AttachmentFactory.php new file mode 100644 index 0000000000..b728625a4c --- /dev/null +++ b/app/Factory/AttachmentFactory.php @@ -0,0 +1,79 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Factory; + +use FireflyIII\Models\Attachment; +use FireflyIII\Models\Note; +use FireflyIII\User; + +/** + * Class AttachmentFactory + */ +class AttachmentFactory +{ + /** @var User */ + private $user; + + /** + * @param array $data + * + * @return Attachment|null + */ + public function create(array $data): ?Attachment + { + // create attachment: + $attachment = Attachment::create( + [ + 'user_id' => $this->user->id, + 'attachable_id' => $data['model_id'], + 'attachable_type' => $data['model'], + 'md5' => '', + 'filename' => $data['filename'], + 'title' => '' === $data['title'] ? null : $data['title'], + 'description' => null, + 'mime' => '', + 'size' => 0, + 'uploaded' => 0, + ] + ); + $notes = (string)($data['notes'] ?? ''); + if ('' !== $notes) { + $note = new Note; + $note->noteable()->associate($attachment); + $note->text = $notes; + $note->save(); + } + + return $attachment; + } + + /** + * @param User $user + */ + public function setUser(User $user): void + { + $this->user = $user; + } + +} \ No newline at end of file diff --git a/app/Helpers/Attachments/AttachmentHelper.php b/app/Helpers/Attachments/AttachmentHelper.php index 72a00bab0c..b6d950042f 100644 --- a/app/Helpers/Attachments/AttachmentHelper.php +++ b/app/Helpers/Attachments/AttachmentHelper.php @@ -122,6 +122,46 @@ class AttachmentHelper implements AttachmentHelperInterface return $this->messages; } + /** + * Uploads a file as a string. + * + * @param Attachment $attachment + * @param string $content + * + * @return bool + */ + public function saveAttachmentFromApi(Attachment $attachment, string $content): bool + { + $resource = tmpfile(); + if (false === $resource) { + Log::error('Cannot create temp-file for file upload.'); + + return false; + } + $path = stream_get_meta_data($resource)['uri']; + fwrite($resource, $content); + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $path); + $allowedMime = config('firefly.allowedMimes'); + if (!\in_array($mime, $allowedMime, true)) { + Log::error(sprintf('Mime type %s is not allowed for API file upload.', $mime)); + + return false; + } + // is allowed? Save the file! + $encrypted = Crypt::encrypt($content); + $this->uploadDisk->put($attachment->fileName(), $encrypted); + + // update attachment. + $attachment->md5 = md5_file($path); + $attachment->mime = $mime; + $attachment->size = \strlen($content); + $attachment->uploaded = 1; + $attachment->save(); + + return true; + } + /** * @param Model $model * @param array|null $files @@ -232,7 +272,7 @@ class AttachmentHelper implements AttachmentHelperInterface Log::debug(sprintf('Name is %s, and mime is %s', $name, $mime)); Log::debug('Valid mimes are', $this->allowedMimes); - if (!\in_array($mime, $this->allowedMimes)) { + if (!\in_array($mime, $this->allowedMimes, true)) { $msg = (string)trans('validation.file_invalid_mime', ['name' => $name, 'mime' => $mime]); $this->errors->add('attachments', $msg); Log::error($msg); diff --git a/app/Helpers/Attachments/AttachmentHelperInterface.php b/app/Helpers/Attachments/AttachmentHelperInterface.php index 03fd095727..8f34041faa 100644 --- a/app/Helpers/Attachments/AttachmentHelperInterface.php +++ b/app/Helpers/Attachments/AttachmentHelperInterface.php @@ -61,6 +61,16 @@ interface AttachmentHelperInterface */ public function getMessages(): MessageBag; + /** + * Uploads a file as a string. + * + * @param Attachment $attachment + * @param string $content + * + * @return bool + */ + public function saveAttachmentFromApi(Attachment $attachment, string $content): bool; + /** * @param Model $model * @param null|array $files diff --git a/app/Models/Account.php b/app/Models/Account.php index c29fe3c988..3c2c4f761f 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -44,6 +44,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * @property AccountType $accountType * @property bool $active * @property string $virtual_balance + * @property User $user */ class Account extends Model { diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php index 044283cbdb..c0d71a8fa7 100644 --- a/app/Models/Attachment.php +++ b/app/Models/Attachment.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace FireflyIII\Models; +use Carbon\Carbon; use Crypt; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; @@ -32,6 +33,20 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class Attachment. + * + * @property int $id + * @property Carbon $created_at + * @property Carbon $updated_at + * @property string $attachable_type + * @property string $md5 + * @property string $filename + * @property string $title + * @property string $description + * @property string $notes + * @property string $mime + * @property int $size + * @property User $user + * @property bool $uploaded */ class Attachment extends Model { @@ -100,9 +115,9 @@ class Attachment extends Model * @return null|string * @throws \Illuminate\Contracts\Encryption\DecryptException */ - public function getDescriptionAttribute($value) + public function getDescriptionAttribute($value): ?string { - if (null === $value || 0 === \strlen($value)) { + if (null === $value || '' === $value) { return null; } @@ -116,9 +131,9 @@ class Attachment extends Model * @return null|string * @throws \Illuminate\Contracts\Encryption\DecryptException */ - public function getFilenameAttribute($value) + public function getFilenameAttribute($value): ?string { - if (null === $value || 0 === \strlen($value)) { + if (null === $value || '' === $value) { return null; } @@ -132,9 +147,9 @@ class Attachment extends Model * @return null|string * @throws \Illuminate\Contracts\Encryption\DecryptException */ - public function getMimeAttribute($value) + public function getMimeAttribute($value): ?string { - if (null === $value || 0 === \strlen($value)) { + if (null === $value || '' === $value) { return null; } @@ -148,9 +163,9 @@ class Attachment extends Model * @return null|string * @throws \Illuminate\Contracts\Encryption\DecryptException */ - public function getTitleAttribute($value) + public function getTitleAttribute($value): ?string { - if (null === $value || 0 === \strlen($value)) { + if (null === $value || '' === $value) { return null; } @@ -169,13 +184,14 @@ class Attachment extends Model /** * @codeCoverageIgnore * - * @param string $value - * - * @throws \Illuminate\Contracts\Encryption\EncryptException + * @param string|null $value */ - public function setDescriptionAttribute(string $value) + public function setDescriptionAttribute(string $value = null): void { - $this->attributes['description'] = Crypt::encrypt($value); + if (null !== $value) { + $this->attributes['description'] = Crypt::encrypt($value); + } + } /** @@ -185,7 +201,7 @@ class Attachment extends Model * * @throws \Illuminate\Contracts\Encryption\EncryptException */ - public function setFilenameAttribute(string $value) + public function setFilenameAttribute(string $value): void { $this->attributes['filename'] = Crypt::encrypt($value); } @@ -197,7 +213,7 @@ class Attachment extends Model * * @throws \Illuminate\Contracts\Encryption\EncryptException */ - public function setMimeAttribute(string $value) + public function setMimeAttribute(string $value): void { $this->attributes['mime'] = Crypt::encrypt($value); } @@ -209,9 +225,11 @@ class Attachment extends Model * * @throws \Illuminate\Contracts\Encryption\EncryptException */ - public function setTitleAttribute(string $value) + public function setTitleAttribute(string $value = null): void { - $this->attributes['title'] = Crypt::encrypt($value); + if (null !== $value) { + $this->attributes['title'] = Crypt::encrypt($value); + } } /** diff --git a/app/Models/Bill.php b/app/Models/Bill.php index 3da9e56add..81da6f180d 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -22,23 +22,34 @@ declare(strict_types=1); namespace FireflyIII\Models; +use Carbon\Carbon; use Crypt; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Collection; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class Bill. * - * @property bool $active - * @property int $transaction_currency_id - * @property string $amount_min - * @property string $amount_max - * @property int $id - * @property string $name + * @property bool $active + * @property int $transaction_currency_id + * @property string $amount_min + * @property string $amount_max + * @property int $id + * @property string $name + * @property Collection $notes + * @property TransactionCurrency $transactionCurrency + * @property Carbon $created_at + * @property Carbon $updated_at + * @property Carbon $date + * @property string $repeat_freq + * @property int $skip + * @property bool $automatch + * @property User $user */ class Bill extends Model { diff --git a/app/Models/Note.php b/app/Models/Note.php index f2cc1ddb20..316b65089e 100644 --- a/app/Models/Note.php +++ b/app/Models/Note.php @@ -22,12 +22,17 @@ declare(strict_types=1); namespace FireflyIII\Models; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; /** * Class Note. * + * @property int $id + * @property Carbon $created_at + * @property Carbon $updated_at * @property string $text + * @property string $title */ class Note extends Model { diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index 92819a5d28..82721a243a 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -38,6 +38,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * @property string $targetamount * @property int $id * @property string $name + * @property Account $account * */ class PiggyBank extends Model diff --git a/app/Models/Rule.php b/app/Models/Rule.php index c3c4acd811..4d368acec2 100644 --- a/app/Models/Rule.php +++ b/app/Models/Rule.php @@ -22,19 +22,30 @@ declare(strict_types=1); namespace FireflyIII\Models; +use Carbon\Carbon; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Collection; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class Rule. * - * @property bool $stop_processing - * @property int $id - * @property \Illuminate\Support\Collection $ruleTriggers - * @property bool $active - * @property bool $strict + * @property bool $stop_processing + * @property int $id + * @property Collection $ruleTriggers + * @property Collection $ruleActions + * @property bool $active + * @property bool $strict + * @property User $user + * @property Carbon $created_at + * @property Carbon $updated_at + * @property string $title + * @property string $text + * @property int $order + * @property RuleGroup $ruleGroup */ class Rule extends Model { @@ -78,9 +89,9 @@ class Rule extends Model /** * @codeCoverageIgnore - * @return \Illuminate\Database\Eloquent\Relations\HasMany + * @return HasMany */ - public function ruleActions() + public function ruleActions(): HasMany { return $this->hasMany(RuleAction::class); } diff --git a/app/Models/RuleAction.php b/app/Models/RuleAction.php index da9e6c048a..7ca38fcc39 100644 --- a/app/Models/RuleAction.php +++ b/app/Models/RuleAction.php @@ -22,10 +22,20 @@ declare(strict_types=1); namespace FireflyIII\Models; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; /** * Class RuleAction. + * + * @property string $action_value + * @property string $action_type + * @property int $id + * @property Carbon $created_at + * @property Carbon $updated_at + * @property int $order + * @property bool $active + * @property bool $stop_processing */ class RuleAction extends Model { diff --git a/app/Models/RuleGroup.php b/app/Models/RuleGroup.php index 149b12fc4d..8bbbc74e8b 100644 --- a/app/Models/RuleGroup.php +++ b/app/Models/RuleGroup.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace FireflyIII\Models; +use Carbon\Carbon; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -30,7 +31,14 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Class RuleGroup. * - * @property bool $active + * @property bool $active + * @property User $user + * @property Carbon $created_at + * @property Carbon $updated_at + * @property string $title + * @property string $text + * @property int $id + * @property int $order */ class RuleGroup extends Model { diff --git a/app/Models/RuleTrigger.php b/app/Models/RuleTrigger.php index 58265d0d1c..b030f9d836 100644 --- a/app/Models/RuleTrigger.php +++ b/app/Models/RuleTrigger.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace FireflyIII\Models; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Model; /** @@ -29,6 +30,12 @@ use Illuminate\Database\Eloquent\Model; * * @property string $trigger_value * @property string $trigger_type + * @property int $id + * @property Carbon $created_at + * @property Carbon $updated_at + * @property int $order + * @property bool $active + * @property bool $stop_processing */ class RuleTrigger extends Model { diff --git a/app/Repositories/Attachment/AttachmentRepository.php b/app/Repositories/Attachment/AttachmentRepository.php index 36d38ade9d..d5eac20346 100644 --- a/app/Repositories/Attachment/AttachmentRepository.php +++ b/app/Repositories/Attachment/AttachmentRepository.php @@ -25,6 +25,8 @@ namespace FireflyIII\Repositories\Attachment; use Carbon\Carbon; use Crypt; use Exception; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\AttachmentFactory; use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; use FireflyIII\Models\Attachment; use FireflyIII\Models\Note; @@ -179,11 +181,30 @@ class AttachmentRepository implements AttachmentRepositoryInterface /** * @param User $user */ - public function setUser(User $user) + public function setUser(User $user): void { $this->user = $user; } + /** + * @param array $data + * + * @return Attachment + * @throws FireflyException + */ + public function store(array $data): Attachment + { + /** @var AttachmentFactory $factory */ + $factory = app(AttachmentFactory::class); + $factory->setUser($this->user); + $result = $factory->create($data); + if (null === $result) { + throw new FireflyException('Could not store attachment.'); + } + + return $result; + } + /** * @param Attachment $attachment * @param array $data @@ -193,8 +214,13 @@ class AttachmentRepository implements AttachmentRepositoryInterface public function update(Attachment $attachment, array $data): Attachment { $attachment->title = $data['title']; + + // update filename, if present and different: + if (isset($data['filename']) && '' !== $data['filename'] && $data['filename'] !== $attachment->filename) { + $attachment->filename = $data['filename']; + } $attachment->save(); - $this->updateNote($attachment, $data['notes']); + $this->updateNote($attachment, $data['notes'] ?? ''); return $attachment; } @@ -207,7 +233,7 @@ class AttachmentRepository implements AttachmentRepositoryInterface */ public function updateNote(Attachment $attachment, string $note): bool { - if (0 === \strlen($note)) { + if ('' === $note) { $dbNote = $attachment->notes()->first(); if (null !== $dbNote) { $dbNote->delete(); diff --git a/app/Repositories/Attachment/AttachmentRepositoryInterface.php b/app/Repositories/Attachment/AttachmentRepositoryInterface.php index 0a2b63b537..30caf2c253 100644 --- a/app/Repositories/Attachment/AttachmentRepositoryInterface.php +++ b/app/Repositories/Attachment/AttachmentRepositoryInterface.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Repositories\Attachment; use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Attachment; use FireflyIII\User; use Illuminate\Support\Collection; @@ -95,6 +96,14 @@ interface AttachmentRepositoryInterface */ public function setUser(User $user); + /** + * @param array $data + * + * @return Attachment + * @throws FireflyException + */ + public function store(array $data): Attachment; + /** * @param Attachment $attachment * @param array $attachmentData diff --git a/app/Rules/IsValidAttachmentModel.php b/app/Rules/IsValidAttachmentModel.php new file mode 100644 index 0000000000..db2959c7c8 --- /dev/null +++ b/app/Rules/IsValidAttachmentModel.php @@ -0,0 +1,88 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Rules; + + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use Illuminate\Contracts\Validation\Rule; + +/** + * Class IsValidAttachmentModel + */ +class IsValidAttachmentModel implements Rule +{ + /** @var string */ + private $model; + + /** + * IsValidAttachmentModel constructor. + * + * @param string $model + */ + public function __construct(string $model) + { + $this->model = $model; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message(): string + { + return trans('validation.model_id_invalid'); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * + * @return bool + * @throws FireflyException + */ + public function passes($attribute, $value): bool + { + if (!auth()->check()) { + return false; + } + $user = auth()->user(); + switch ($this->model) { + default: + throw new FireflyException(sprintf('Model "%s" cannot be validated.', $this->model)); + case TransactionJournal::class: + /** @var JournalRepositoryInterface $repository */ + $repository = app(JournalRepositoryInterface::class); + $repository->setUser($user); + $result = $repository->findNull((int)$value); + + return null !== $result; + break; + } + } +} \ No newline at end of file diff --git a/app/Transformers/AccountTransformer.php b/app/Transformers/AccountTransformer.php index 099398fd8c..c238a28dd8 100644 --- a/app/Transformers/AccountTransformer.php +++ b/app/Transformers/AccountTransformer.php @@ -153,7 +153,7 @@ class AccountTransformer extends TransformerAbstract } $currencyId = (int)$this->repository->getMetaValue($account, 'currency_id'); $currencyCode = null; - $currencySymbol = 'x'; + $currencySymbol = null; $decimalPlaces = 2; if ($currencyId > 0) { $currency = TransactionCurrency::find($currencyId); diff --git a/app/Transformers/AttachmentTransformer.php b/app/Transformers/AttachmentTransformer.php index 0db1993352..81ba473173 100644 --- a/app/Transformers/AttachmentTransformer.php +++ b/app/Transformers/AttachmentTransformer.php @@ -28,6 +28,7 @@ use FireflyIII\Models\Attachment; use League\Fractal\Resource\Item; use League\Fractal\TransformerAbstract; use Symfony\Component\HttpFoundation\ParameterBag; +use League\Fractal\Resource\Collection as FractalCollection; /** * Class AttachmentTransformer @@ -39,13 +40,13 @@ class AttachmentTransformer extends TransformerAbstract * * @var array */ - protected $availableIncludes = ['user']; + protected $availableIncludes = ['user','notes']; /** * List of resources to automatically include * * @var array */ - protected $defaultIncludes = ['user']; + protected $defaultIncludes = ['user','notes']; /** @var ParameterBag */ protected $parameters; @@ -76,6 +77,20 @@ class AttachmentTransformer extends TransformerAbstract return $this->item($attachment->user, new UserTransformer($this->parameters), 'user'); } + /** + * Attach the notes. + * + * @codeCoverageIgnore + * + * @param Attachment $attachment + * + * @return FractalCollection + */ + public function includeNotes(Attachment $attachment): FractalCollection + { + return $this->collection($attachment->notes, new NoteTransformer($this->parameters), 'notes'); + } + /** * Transform attachment. * @@ -92,11 +107,11 @@ class AttachmentTransformer extends TransformerAbstract 'attachable_type' => $attachment->attachable_type, 'md5' => $attachment->md5, 'filename' => $attachment->filename, + 'download_uri' => route('api.v1.attachments.download', [$attachment->id]), + 'upload_uri' => route('api.v1.attachments.upload', [$attachment->id]), 'title' => $attachment->title, - 'description' => $attachment->description, - 'notes' => $attachment->notes, 'mime' => $attachment->mime, - 'size' => $attachment->size, + 'size' => (int)$attachment->size, 'links' => [ [ 'rel' => 'self', diff --git a/app/Transformers/BillTransformer.php b/app/Transformers/BillTransformer.php index 78a17fe43d..86c7516833 100644 --- a/app/Transformers/BillTransformer.php +++ b/app/Transformers/BillTransformer.php @@ -26,7 +26,6 @@ namespace FireflyIII\Transformers; use Carbon\Carbon; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Models\Bill; -use FireflyIII\Models\Note; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use Illuminate\Support\Collection; use League\Fractal\Resource\Collection as FractalCollection; @@ -45,13 +44,13 @@ class BillTransformer extends TransformerAbstract * * @var array */ - protected $availableIncludes = ['attachments', 'transactions', 'user']; + protected $availableIncludes = ['attachments', 'transactions', 'user', 'notes', 'rules']; /** * List of resources to automatically include * * @var array */ - protected $defaultIncludes = []; + protected $defaultIncludes = ['notes', 'rules']; /** @var ParameterBag */ protected $parameters; @@ -83,6 +82,40 @@ class BillTransformer extends TransformerAbstract return $this->collection($attachments, new AttachmentTransformer($this->parameters), 'attachments'); } + /** + * Attach the notes. + * + * @codeCoverageIgnore + * + * @param Bill $bill + * + * @return FractalCollection + */ + public function includeNotes(Bill $bill): FractalCollection + { + return $this->collection($bill->notes, new NoteTransformer($this->parameters), 'notes'); + } + + /** + * Attach the rules. + * + * @codeCoverageIgnore + * + * @param Bill $bill + * + * @return FractalCollection + */ + public function includeRules(Bill $bill): FractalCollection + { + /** @var BillRepositoryInterface $repository */ + $repository = app(BillRepositoryInterface::class); + $repository->setUser($bill->user); + // add info about rules: + $rules = $repository->getRulesForBill($bill); + + return $this->collection($rules, new RuleTransformer($this->parameters), 'rules'); + } + /** * Include any transactions. * @@ -141,19 +174,17 @@ class BillTransformer extends TransformerAbstract 'name' => $bill->name, 'currency_id' => $bill->transaction_currency_id, 'currency_code' => $bill->transactionCurrency->code, - 'match' => explode(',', $bill->match), 'amount_min' => round($bill->amount_min, 2), 'amount_max' => round($bill->amount_max, 2), 'date' => $bill->date->format('Y-m-d'), 'repeat_freq' => $bill->repeat_freq, 'skip' => (int)$bill->skip, - 'automatch' => (int)$bill->automatch === 1, - 'active' => (int)$bill->active === 1, + 'automatch' => $bill->automatch, + 'active' => $bill->active, 'attachments_count' => $bill->attachments()->count(), 'pay_dates' => $payDates, 'paid_dates' => $paidData['paid_dates'], 'next_expected_match' => $paidData['next_expected_match'], - 'notes' => null, 'links' => [ [ 'rel' => 'self', @@ -161,11 +192,6 @@ class BillTransformer extends TransformerAbstract ], ], ]; - /** @var Note $note */ - $note = $bill->notes()->first(); - if (null !== $note) { - $data['notes'] = $note->text; - } return $data; diff --git a/app/Transformers/NoteTransformer.php b/app/Transformers/NoteTransformer.php new file mode 100644 index 0000000000..9cf804c45a --- /dev/null +++ b/app/Transformers/NoteTransformer.php @@ -0,0 +1,92 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Transformers; + + +use FireflyIII\Models\Note; +use League\CommonMark\CommonMarkConverter; +use League\Fractal\TransformerAbstract; +use Symfony\Component\HttpFoundation\ParameterBag; + +/** + * Class NoteTransformer + */ +class NoteTransformer extends TransformerAbstract +{ + /** + * List of resources possible to include + * + * @var array + */ + protected $availableIncludes = []; + /** + * List of resources to automatically include + * + * @var array + */ + protected $defaultIncludes = []; + + /** @var ParameterBag */ + protected $parameters; + + /** + * CurrencyTransformer constructor. + * + * @codeCoverageIgnore + * + * @param ParameterBag $parameters + */ + public function __construct(ParameterBag $parameters) + { + $this->parameters = $parameters; + } + + /** + * Transform the note. + * + * @param Note $note + * + * @return array + */ + public function transform(Note $note): array + { + $converter = new CommonMarkConverter; + $data = [ + 'id' => (int)$note->id, + 'updated_at' => $note->updated_at->toAtomString(), + 'created_at' => $note->created_at->toAtomString(), + 'title' => $note->title, + 'text' => $note->text, + 'markdown' => $converter->convertToHtml($note->text), + 'links' => [ + [ + 'rel' => 'self', + 'uri' => '/notes/' . $note->id, + ], + ], + ]; + + return $data; + } +} \ No newline at end of file diff --git a/app/Transformers/RuleActionTransformer.php b/app/Transformers/RuleActionTransformer.php new file mode 100644 index 0000000000..a0a8401716 --- /dev/null +++ b/app/Transformers/RuleActionTransformer.php @@ -0,0 +1,93 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Transformers; + + +use FireflyIII\Models\RuleAction; +use FireflyIII\Models\RuleTrigger; +use League\Fractal\TransformerAbstract; +use Symfony\Component\HttpFoundation\ParameterBag; + +/** + * Class RuleActionTransformer + */ +class RuleActionTransformer extends TransformerAbstract +{ + /** + * List of resources possible to include + * + * @var array + */ + protected $availableIncludes = []; + /** + * List of resources to automatically include + * + * @var array + */ + protected $defaultIncludes = []; + + /** @var ParameterBag */ + protected $parameters; + + /** + * CurrencyTransformer constructor. + * + * @codeCoverageIgnore + * + * @param ParameterBag $parameters + */ + public function __construct(ParameterBag $parameters) + { + $this->parameters = $parameters; + } + + /** + * Transform the rule action. + * + * @param RuleAction $ruleAction + * + * @return array + */ + public function transform(RuleAction $ruleAction): array + { + $data = [ + 'id' => (int)$ruleAction->id, + 'updated_at' => $ruleAction->updated_at->toAtomString(), + 'created_at' => $ruleAction->created_at->toAtomString(), + 'action_type' => $ruleAction->action_type, + 'action_value' => $ruleAction->action_value, + 'order' => $ruleAction->order, + 'active' => $ruleAction->active, + 'stop_processing' => $ruleAction->stop_processing, + 'links' => [ + [ + 'rel' => 'self', + 'uri' => '/rule_triggers/' . $ruleAction->id, + ], + ], + ]; + + return $data; + } +} \ No newline at end of file diff --git a/app/Transformers/RuleGroupTransformer.php b/app/Transformers/RuleGroupTransformer.php new file mode 100644 index 0000000000..5cbd579a54 --- /dev/null +++ b/app/Transformers/RuleGroupTransformer.php @@ -0,0 +1,110 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Transformers; + + +use FireflyIII\Models\RuleGroup; +use League\Fractal\Resource\Item; +use League\Fractal\TransformerAbstract; +use Symfony\Component\HttpFoundation\ParameterBag; + +/** + * Class RuleGroupTransformer + */ +class RuleGroupTransformer extends TransformerAbstract +{ + /** + * List of resources possible to include + * + * @var array + */ + protected $availableIncludes = ['user']; + /** + * List of resources to automatically include + * + * @var array + */ + protected $defaultIncludes = ['user']; + + /** @var ParameterBag */ + protected $parameters; + + /** + * CurrencyTransformer constructor. + * + * @codeCoverageIgnore + * + * @param ParameterBag $parameters + */ + public function __construct(ParameterBag $parameters) + { + $this->parameters = $parameters; + } + + + /** + * Include the user. + * + * @param RuleGroup $ruleGroup + * + * @codeCoverageIgnore + * @return Item + */ + public function includeUser(RuleGroup $ruleGroup): Item + { + return $this->item($ruleGroup->user, new UserTransformer($this->parameters), 'users'); + } + + /** + * Transform the rule group + * + * @param RuleGroup $ruleGroup + * + * @return array + */ + public function transform(RuleGroup $ruleGroup): array + { + $data = [ + 'id' => (int)$ruleGroup->id, + 'updated_at' => $ruleGroup->updated_at->toAtomString(), + 'created_at' => $ruleGroup->created_at->toAtomString(), + 'title' => $ruleGroup->title, + 'text' => $ruleGroup->text, + 'order' => $ruleGroup->order, + 'active' => $ruleGroup->active, + 'links' => [ + [ + 'rel' => 'self', + 'uri' => '/rule_groups/' . $ruleGroup->id, + ], + ], + ]; + + return $data; + } + + +} + + diff --git a/app/Transformers/RuleTransformer.php b/app/Transformers/RuleTransformer.php new file mode 100644 index 0000000000..35db378eeb --- /dev/null +++ b/app/Transformers/RuleTransformer.php @@ -0,0 +1,140 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Transformers; + + +use FireflyIII\Models\Rule; +use League\Fractal\Resource\Item; +use League\Fractal\TransformerAbstract; +use Symfony\Component\HttpFoundation\ParameterBag; +use League\Fractal\Resource\Collection as FractalCollection; + +/** + * Class RuleTransformer + */ +class RuleTransformer extends TransformerAbstract +{ + /** + * List of resources possible to include + * + * @var array + */ + protected $availableIncludes = ['rule_group', 'rule_triggers', 'rule_actions', 'user']; + /** + * List of resources to automatically include + * + * @var array + */ + protected $defaultIncludes = ['rule_group', 'rule_triggers', 'rule_actions']; + + /** @var ParameterBag */ + protected $parameters; + + /** + * CurrencyTransformer constructor. + * + * @codeCoverageIgnore + * + * @param ParameterBag $parameters + */ + public function __construct(ParameterBag $parameters) + { + $this->parameters = $parameters; + } + + /** + * @param Rule $rule + * + * @return FractalCollection + */ + public function includeRuleTriggers(Rule $rule): FractalCollection { + return $this->collection($rule->ruleTriggers, new RuleTriggerTransformer($this->parameters), 'rule_triggers'); + } + + /** + * @param Rule $rule + * + * @return FractalCollection + */ + public function includeRuleActions(Rule $rule): FractalCollection { + return $this->collection($rule->ruleActions, new RuleActionTransformer($this->parameters), 'rule_actions'); + } + + /** + * Include the user. + * + * @param Rule $rule + * + * @codeCoverageIgnore + * @return Item + */ + public function includeUser(Rule $rule): Item + { + return $this->item($rule->user, new UserTransformer($this->parameters), 'users'); + } + + + /** + * Include the rule group. + * + * @param Rule $rule + * + * @codeCoverageIgnore + * @return Item + */ + public function includeRuleGroup(Rule $rule): Item + { + return $this->item($rule->ruleGroup, new RuleGroupTransformer($this->parameters), 'rule_groups'); + } + + /** + * Transform the rule. + * + * @param Rule $rule + * + * @return array + */ + public function transform(Rule $rule): array + { + $data = [ + 'id' => (int)$rule->id, + 'updated_at' => $rule->updated_at->toAtomString(), + 'created_at' => $rule->created_at->toAtomString(), + 'title' => $rule->title, + 'text' => $rule->text, + 'order' => (int)$rule->order, + 'active' => $rule->active, + 'stop_processing' => $rule->stop_processing, + 'strict' => $rule->strict, + 'links' => [ + [ + 'rel' => 'self', + 'uri' => '/rules/' . $rule->id, + ], + ], + ]; + + return $data; + } +} \ No newline at end of file diff --git a/app/Transformers/RuleTriggerTransformer.php b/app/Transformers/RuleTriggerTransformer.php new file mode 100644 index 0000000000..6c51c84bb3 --- /dev/null +++ b/app/Transformers/RuleTriggerTransformer.php @@ -0,0 +1,92 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Transformers; + + +use FireflyIII\Models\RuleTrigger; +use League\Fractal\TransformerAbstract; +use Symfony\Component\HttpFoundation\ParameterBag; + +/** + * Class RuleTriggerTransformer + */ +class RuleTriggerTransformer extends TransformerAbstract +{ + /** + * List of resources possible to include + * + * @var array + */ + protected $availableIncludes = []; + /** + * List of resources to automatically include + * + * @var array + */ + protected $defaultIncludes = []; + + /** @var ParameterBag */ + protected $parameters; + + /** + * CurrencyTransformer constructor. + * + * @codeCoverageIgnore + * + * @param ParameterBag $parameters + */ + public function __construct(ParameterBag $parameters) + { + $this->parameters = $parameters; + } + + /** + * Transform the rule trigger. + * + * @param RuleTrigger $ruleTrigger + * + * @return array + */ + public function transform(RuleTrigger $ruleTrigger): array + { + $data = [ + 'id' => (int)$ruleTrigger->id, + 'updated_at' => $ruleTrigger->updated_at->toAtomString(), + 'created_at' => $ruleTrigger->created_at->toAtomString(), + 'trigger_type' => $ruleTrigger->trigger_type, + 'trigger_value' => $ruleTrigger->trigger_value, + 'order' => $ruleTrigger->order, + 'active' => $ruleTrigger->active, + 'stop_processing' => $ruleTrigger->stop_processing, + 'links' => [ + [ + 'rel' => 'self', + 'uri' => '/rule_triggers/' . $ruleTrigger->id, + ], + ], + ]; + + return $data; + } +} \ No newline at end of file diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index d8764ef5a9..66df306ae7 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -44,6 +44,8 @@ return [ 'belongs_to_user' => 'The value of :attribute is unknown', 'accepted' => 'The :attribute must be accepted.', 'bic' => 'This is not a valid BIC.', + 'base64' => 'This is not valid base64 encoded data.', + 'model_id_invalid' => 'The given ID seems invalid for this model.', 'more' => ':attribute must be larger than zero.', 'active_url' => 'The :attribute is not a valid URL.', 'after' => 'The :attribute must be a date after :date.', diff --git a/routes/api.php b/routes/api.php index eeca33c0f6..6db99723e3 100644 --- a/routes/api.php +++ b/routes/api.php @@ -43,6 +43,21 @@ Route::group( } ); +Route::group( + ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'attachments', 'as' => 'api.v1.attachments.'], + function () { + + // Accounts API routes: + Route::get('', ['uses' => 'AttachmentController@index', 'as' => 'index']); + Route::post('', ['uses' => 'AttachmentController@store', 'as' => 'store']); + Route::get('{attachment}', ['uses' => 'AttachmentController@show', 'as' => 'show']); + Route::get('{attachment}/download', ['uses' => 'AttachmentController@download', 'as' => 'download']); + Route::post('{attachment}/upload', ['uses' => 'AttachmentController@upload', 'as' => 'upload']); + Route::put('{attachment}', ['uses' => 'AttachmentController@update', 'as' => 'update']); + Route::delete('{attachment}', ['uses' => 'AttachmentController@delete', 'as' => 'delete']); + } +); + Route::group( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'bills', 'as' => 'api.v1.bills.'], function () {