Compare commits
1397 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0666880786 | |||
| e0113388d2 | |||
| 384f28ecae | |||
| d7596c208c | |||
| 6d1ab5870f | |||
| 73dbfa38ec | |||
| 4adb068699 | |||
| 3e4c738ab1 | |||
| cd92e4fcd8 | |||
| fba191f22c | |||
| 8874c8827a | |||
| 34529a8ed9 | |||
| 8fd44acae2 | |||
| 4dd540ffc7 | |||
| 9c20116c9b | |||
| 926410d58f | |||
| c64a107ad9 | |||
| 5dcd25946b | |||
| d12865bbcb | |||
| 3fed415437 | |||
| a03a13d3b9 | |||
| e625951831 | |||
| f533c68c9b | |||
| 2420746f1b | |||
| 2dfd00dd2e | |||
| e043bb48e9 | |||
| 9aaef9d957 | |||
| 87449aae67 | |||
| 191d064fe1 | |||
| dcb3e2feaf | |||
| da9a423e59 | |||
| 3be4510e63 | |||
| a6bb5a239b | |||
| a5fb42609b | |||
| 40c2c1718e | |||
| aeecfd63da | |||
| 1da1e6da9b | |||
| bf9f23ebdd | |||
| 7beb4f8d83 | |||
| 991b859643 | |||
| 9bd74e08db | |||
| cfdf65025d | |||
| bf471e654c | |||
| 5feee1cf55 | |||
| 6af6fc017b | |||
| 8ee9a792fc | |||
| 9ef1c5928f | |||
| 64c933e365 | |||
| 32055f3d6e | |||
| 804acae44b | |||
| 6b3f37c00e | |||
| 4bdc4878cd | |||
| 625d2bc128 | |||
| 0af5bb1b4b | |||
| 7a0d59357c | |||
| adbb46d02c | |||
| ac3884179e | |||
| 7cf1af188d | |||
| 21b695e4ef | |||
| fbda66defc | |||
| beb4435ee0 | |||
| 6a4c78c1cb | |||
| dc7f5d8e66 | |||
| 872a4c2f2c | |||
| 8a5204bc4c | |||
| acbf6a1742 | |||
| b2d3d56a85 | |||
| 8b83fa640f | |||
| 5a95abed73 | |||
| ca1d689c3a | |||
| 0c750789b1 | |||
| ccb6c19c72 | |||
| 011581f09d | |||
| 110dd7b8b3 | |||
| f9bdd78df8 | |||
| 241b582923 | |||
| 348a089e18 | |||
| 7528fb3871 | |||
| e76f849bde | |||
| 770658ffed | |||
| 348bcd64ba | |||
| 23ec5a66a7 | |||
| 210da100ea | |||
| 91aec9fc1d | |||
| 155565b946 | |||
| 5157430104 | |||
| 2eb88617b3 | |||
| 5e4e0b8d3c | |||
| 5f1cb260ae | |||
| d854fc9fa6 | |||
| d455d0c73f | |||
| ae15b8fb5e | |||
| d686aee9d4 | |||
| 6e667a5024 | |||
| 5610ac9b71 | |||
| f9bb35ae68 | |||
| 768f6d7c71 | |||
| ea1455a50c | |||
| cd6a157f1d | |||
| 447d54b61b | |||
| f721220b1a | |||
| 95ed44d2ec | |||
| 5ad74492e1 | |||
| b191896fb1 | |||
| 314636e298 | |||
| 6e4d25fadd | |||
| d401cb4e4c | |||
| 2f7c892f67 | |||
| 1f33f41965 | |||
| 7e6c64364f | |||
| 9916b95703 | |||
| cba0bf3d20 | |||
| 82b043adf6 | |||
| bfcf607468 | |||
| 6db74ec68f | |||
| 02cb7c1fd5 | |||
| 8b06f2c02c | |||
| 9644729f68 | |||
| c0f3d5e765 | |||
| e766625aa8 | |||
| ad97a71cb6 | |||
| d857231178 | |||
| 4976ebe89e | |||
| 571ccc08d4 | |||
| 63f73483a1 | |||
| 60e4d3e7c0 | |||
| 063b5e19a2 | |||
| 9899e5913f | |||
| 6730e8a48b | |||
| fe833862f3 | |||
| f660c9aedf | |||
| 58cd736ab9 | |||
| 693d952910 | |||
| 8ffb72df30 | |||
| 2ae4094c64 | |||
| 3c089c8628 | |||
| a2f7bef58d | |||
| cc2e64a4cd | |||
| a2eb3d7b8c | |||
| 214f5cec96 | |||
| d56b95d9f2 | |||
| 7f4595a374 | |||
| c3b5191ca4 | |||
| de8f9114ab | |||
| 960583c052 | |||
| 32baf94251 | |||
| e290bc0b12 | |||
| 54bf5d98ae | |||
| d00b77137e | |||
| 293ca499cc | |||
| 5825f8a8f9 | |||
| 70f8458d9c | |||
| d8dc5d107e | |||
| 47d7064a72 | |||
| 6874bee0ad | |||
| 18f285be91 | |||
| 9be5d5b102 | |||
| 25782c7696 | |||
| 20ae316c63 | |||
| 8fd166ef2b | |||
| af8ce3a34e | |||
| e5956abb85 | |||
| 7024487528 | |||
| a938905946 | |||
| 0250bfcd12 | |||
| 6d7e1f0952 | |||
| a098b20221 | |||
| 4ab21cb80b | |||
| f9e3953f68 | |||
| 313a2874cd | |||
| 60ae130612 | |||
| 22084a3d77 | |||
| ce35f68584 | |||
| d0ff17ed40 | |||
| 30cfea4ac4 | |||
| f83d0a34c3 | |||
| f2fd948c7b | |||
| 1b0a20c001 | |||
| 9fa4f40ccf | |||
| 71e39fa4fe | |||
| d98dfecef3 | |||
| adfd7efae6 | |||
| 0d75fa4929 | |||
| 43c8613e94 | |||
| ee1e3bbaf9 | |||
| 1d03f557ce | |||
| 939d482cfb | |||
| 044ffbff13 | |||
| 37130f00a0 | |||
| c40070ceb0 | |||
| 76d9b3cf9c | |||
| 532321ddeb | |||
| 40b2613580 | |||
| e298ebec4b | |||
| 4073c60182 | |||
| 2553496ce6 | |||
| 67c0ac45bd | |||
| 26f3c5630a | |||
| f92f21764c | |||
| a8631a6ce9 | |||
| 4045e33453 | |||
| 8c79eb1ea4 | |||
| 8885db0ae5 | |||
| a8d210a22a | |||
| 82912197eb | |||
| 33cac3a551 | |||
| ca89fd021d | |||
| ad4e7c0beb | |||
| 739e2b0ebc | |||
| 738c6791a4 | |||
| 9bfa1a9225 | |||
| 7e3cc0480e | |||
| 5365640ca8 | |||
| fe0570b0c4 | |||
| cca76ec4e0 | |||
| c226fd7936 | |||
| f65143edf3 | |||
| 483b717825 | |||
| b8a53f793a | |||
| 0210f9642a | |||
| b6af13767d | |||
| 73ec521f41 | |||
| ea4ff23eee | |||
| d4131bbcb0 | |||
| 237ef71ad0 | |||
| 918f3fdd70 | |||
| 98bbf8c1ba | |||
| 613b82a85a | |||
| f72cefeaef | |||
| 316e7ec876 | |||
| e0ad00d857 | |||
| 108998be43 | |||
| 5b48d2d3eb | |||
| 66c3aa2011 | |||
| f7d0723516 | |||
| 52588323a0 | |||
| 43451f99f1 | |||
| 93139ee7d0 | |||
| 4ee86fccfa | |||
| cd1786c8ac | |||
| 6a346bd01d | |||
| b0b770eabe | |||
| 21ef8c5fe6 | |||
| db6ecd6b68 | |||
| cf54ede1ba | |||
| f8493d3300 | |||
| fad66fe813 | |||
| 03c47cb625 | |||
| 21f2b22720 | |||
| e4142bc0a1 | |||
| cba4d92f08 | |||
| 0836168771 | |||
| 565f19f51b | |||
| b7de48a8e8 | |||
| 402074a9b0 | |||
| c5904fdda4 | |||
| 70fe7dfd62 | |||
| c3afa55f89 | |||
| 3d9c5c01d2 | |||
| 9422429df0 | |||
| 4ecb8bf898 | |||
| 71dd03c914 | |||
| 9691f62ac3 | |||
| 374fcf2f9c | |||
| 940d407c77 | |||
| 9b2c988ddc | |||
| 6d25fb36a0 | |||
| 2fa287389b | |||
| 5b7010eebe | |||
| 32977a09f1 | |||
| b222a0996f | |||
| 267dd01228 | |||
| f07b129826 | |||
| aae417a5d9 | |||
| 68aeaa54b9 | |||
| 8be18a2d11 | |||
| cf35c20460 | |||
| e51a8d74cb | |||
| be6a872eda | |||
| 34392267f1 | |||
| b960228799 | |||
| bb8f6c9941 | |||
| e6771aa5f6 | |||
| 3f52538773 | |||
| f66de9054d | |||
| b109e8da68 | |||
| d14bb881bb | |||
| eedfdd14e8 | |||
| 14c28a5aab | |||
| 975ed83753 | |||
| df4e9c8ed0 | |||
| cd50307fdf | |||
| feee057e2b | |||
| 6581acd083 | |||
| f3824117b3 | |||
| f3736da8a6 | |||
| 198b746ce3 | |||
| a9ded55e26 | |||
| b50d56f884 | |||
| 207794e168 | |||
| 831e589b3d | |||
| 61e6f5aeb5 | |||
| a3477ac57d | |||
| 9f08d3e9d6 | |||
| d0f65e5fd9 | |||
| 5f20571958 | |||
| 4634597b7f | |||
| ca44c8e9c7 | |||
| 2551b024f0 | |||
| e652320a3e | |||
| c1f13b146d | |||
| 36f3d040dd | |||
| 5ec3ac222b | |||
| 739f94a41b | |||
| 678ddfaf11 | |||
| 2cda441421 | |||
| e518b8bef5 | |||
| ae998c33bd | |||
| dc2def1af7 | |||
| 9570857f8a | |||
| 6df100c2c2 | |||
| 0b46188034 | |||
| eb8d958fa1 | |||
| 7e74ddc54f | |||
| 90ac4bc071 | |||
| c59b655200 | |||
| a1c7814f5a | |||
| db713e4c41 | |||
| 33d7f99c5c | |||
| 487fed21a1 | |||
| 8fed65f5be | |||
| d2f57cb558 | |||
| 8b905b6cd3 | |||
| e5389af306 | |||
| d524178d3b | |||
| 6f7198690d | |||
| d52b73f738 | |||
| 785aadc288 | |||
| 42175f98e4 | |||
| 62975b2957 | |||
| dc433f5a82 | |||
| 1af9256658 | |||
| a3696af196 | |||
| 20913c283e | |||
| f9df70e710 | |||
| c0c878c20b | |||
| abc19ee574 | |||
| 3b97d17aed | |||
| 06ceb371e3 | |||
| d78666e0d2 | |||
| cde19542a6 | |||
| 263e553a0f | |||
| a129d54e06 | |||
| 09f9cbf3df | |||
| 14cb5730b5 | |||
| 64a0064f3c | |||
| 7c9a605450 | |||
| 7a9d0af314 | |||
| e2e3bf9a71 | |||
| 7752c85304 | |||
| 51fcd5aca9 | |||
| ad9391ad9e | |||
| 0a97096c61 | |||
| fd5b77d919 | |||
| f118f572bc | |||
| c08b43991a | |||
| f58f2071ed | |||
| 879df57fa0 | |||
| 46d0fa2c15 | |||
| f242cd1454 | |||
| ba04f1c7f1 | |||
| df45635fd5 | |||
| 260f13bf3f | |||
| bdd0ced5d2 | |||
| f3a729e283 | |||
| af345e8721 | |||
| 99670c7b3d | |||
| 01dffd98f2 | |||
| 137b989064 | |||
| 19eaca9bf4 | |||
| 83c82767c3 | |||
| 838be733ff | |||
| 6a49e812fb | |||
| 95ce8645f2 | |||
| b707bf70b8 | |||
| bc7de312c1 | |||
| 9c851a1662 | |||
| 41cc7efe5b | |||
| 22d141cd20 | |||
| 768c8f6247 | |||
| 478079ce19 | |||
| d472809175 | |||
| 37ed7f3527 | |||
| 1d9b011a68 | |||
| c6e8ce5ad4 | |||
| 63223a1150 | |||
| 580e2858a5 | |||
| f298a5d0b2 | |||
| 1e9e60640c | |||
| c36096cb45 | |||
| cf6bf2ad28 | |||
| 09b62d5112 | |||
| c12eacc4c1 | |||
| b5289212b1 | |||
| 856e68f9ee | |||
| 9eb7e2ce0f | |||
| a0777dc452 | |||
| 7ecf2ea11d | |||
| 4af7fde960 | |||
| 838a80fa55 | |||
| 36a72d0cb9 | |||
| ea00d2d869 | |||
| 1b2f96fe5b | |||
| 8323627e9a | |||
| 73a2ece6a1 | |||
| 3fc84a8453 | |||
| d2480e9637 | |||
| fb95614171 | |||
| 97e497cced | |||
| 0857e28931 | |||
| b1226fb091 | |||
| a68c797cb6 | |||
| f29a1a1e80 | |||
| bea7442ecf | |||
| fc09c29769 | |||
| 5c997b04c3 | |||
| 9a7e0e8b2a | |||
| ff519194c9 | |||
| b89d9bd6b5 | |||
| 183ab82f5a | |||
| 8be9e1ec55 | |||
| a4ae94f5e9 | |||
| be981741ce | |||
| 9491895991 | |||
| 038c374dcd | |||
| ccc2ed09b8 | |||
| 4a570c6699 | |||
| 8bccde26d2 | |||
| f924f60de3 | |||
| 3d646039be | |||
| da064ff76f | |||
| 4f17ebabee | |||
| 0cd88f2ec9 | |||
| 77d5e1997d | |||
| 2e688d7671 | |||
| 3a5e514691 | |||
| 5e7e920687 | |||
| adefd7d3c5 | |||
| e2a1e497fc | |||
| bd9dc3dfe3 | |||
| 58a0be2cef | |||
| fea97768d9 | |||
| 41865d8cde | |||
| b2df17cf93 | |||
| ec0649f65a | |||
| 01cc896972 | |||
| 522e9be624 | |||
| 856f4687e7 | |||
| ca4f787131 | |||
| d518d3671b | |||
| 9bcdef9107 | |||
| 8854f60f4a | |||
| 760f51352b | |||
| 11860495c6 | |||
| 320420ef29 | |||
| 157b2a60bb | |||
| 1299b20fe1 | |||
| b69109a5d4 | |||
| 3d6e83a8cf | |||
| abe076dc68 | |||
| cf5b6a6b62 | |||
| ac115642cd | |||
| 41af71ebf3 | |||
| 85ed285c79 | |||
| 906f6e5717 | |||
| 4d2933797d | |||
| e237084ec4 | |||
| 6e5148c48a | |||
| 9dd9d4e33a | |||
| 306de81763 | |||
| 533b51aed4 | |||
| 7576428284 | |||
| ab24606bdf | |||
| 204046620c | |||
| f3d234877b | |||
| 81f346fcc9 | |||
| 3033d3f09b | |||
| 5c7b674b60 | |||
| 89ccbbb2ec | |||
| 899c9dcb74 | |||
| 74d00c8b89 | |||
| f8bfd6869c | |||
| 2aaafe850f | |||
| 4d7f86b5da | |||
| 9eaab5176a | |||
| 74661e676c | |||
| 7f3db957a4 | |||
| 0f7ed3d4c7 | |||
| 5f2a2728fc | |||
| 429faf405c | |||
| 6a5f15bbc5 | |||
| 375454fc5d | |||
| 41b92c9ee1 | |||
| 6293fb98d6 | |||
| 306321258d | |||
| 03e85ea32e | |||
| 551e2a6b47 | |||
| 0d4214fe46 | |||
| c8d0fb019e | |||
| 9867eab381 | |||
| 252c5a34b3 | |||
| ce5e7d79e8 | |||
| e8946bb4fe | |||
| d79a5cadd4 | |||
| 4e3cee4dc8 | |||
| 61922e0558 | |||
| 300cb53f3a | |||
| 0a43c781e0 | |||
| 87c6b9e5d5 | |||
| 3f6a8eea5a | |||
| 1b8142d4c3 | |||
| 28113d50ca | |||
| 0851d2ce7a | |||
| b32f627bda | |||
| e88153b917 | |||
| 893a260ed8 | |||
| e93ff18b4c | |||
| 048a75ff67 | |||
| 911bdb3a51 | |||
| b87019e200 | |||
| e413ca6643 | |||
| 642ddf5a6c | |||
| 42c62ea59b | |||
| d19b7ef657 | |||
| 6fbc892acf | |||
| 38d62d3a8f | |||
| a44d3aa4a3 | |||
| 248d288803 | |||
| 564956d151 | |||
| 75888d2f38 | |||
| 3b9f705a53 | |||
| e24d695eb3 | |||
| 55d68f1143 | |||
| 58e006cdde | |||
| 48811450a8 | |||
| 8aa2f12a8b | |||
| a13c7d9465 | |||
| 3dd82aa61e | |||
| e67a1b67ed | |||
| 463649ffa8 | |||
| e037f2fddb | |||
| 6aef2939f9 | |||
| 0b6486a03b | |||
| fb39f0915f | |||
| a80178494a | |||
| 7bdb61c05e | |||
| 47403e2e35 | |||
| a7d86dff4b | |||
| a1ef2886dc | |||
| 2f8ec94169 | |||
| 1c6c445a59 | |||
| f1cfb01451 | |||
| 8921ed42ad | |||
| 0836d52065 | |||
| 4f4b362c05 | |||
| 63db221a2a | |||
| 31c4206b2e | |||
| 13e0f0f06a | |||
| 153dd65a0d | |||
| 306fca8291 | |||
| ef253690d6 | |||
| f5377fe12b | |||
| c35cb98b32 | |||
| 009f090061 | |||
| 61c586c31d | |||
| a90cf7b8e3 | |||
| db4e5c001b | |||
| ead76d78a3 | |||
| 02cf4f4bb8 | |||
| f9040b3afb | |||
| e47155006b | |||
| a07213fa5f | |||
| d74d89e9d4 | |||
| fc2c3ce644 | |||
| 7745a92490 | |||
| 92c0cf297c | |||
| c6accac161 | |||
| 58b4a9b965 | |||
| d77022a167 | |||
| 1c77a197ab | |||
| fa1354a3b9 | |||
| fc5c159d00 | |||
| d4aa273661 | |||
| ff1c5e9e20 | |||
| eff9ae7225 | |||
| 146bdf01f1 | |||
| 487201d8e7 | |||
| 788d72afc4 | |||
| f5da342192 | |||
| 2ce82fe605 | |||
| 291a54f070 | |||
| 14750fa131 | |||
| 26fde45450 | |||
| cef116b4d6 | |||
| 04324bd03b | |||
| 4a302e637b | |||
| da13823dbc | |||
| 9b6d1455a8 | |||
| 10845e1432 | |||
| 88dacb6a02 | |||
| d9cc84b50a | |||
| 9505fe8222 | |||
| 70eca7a7b2 | |||
| e9636e7e6f | |||
| 84dc7f796e | |||
| 16ccab49a5 | |||
| 3dfd6b6c57 | |||
| 5ac84f20d3 | |||
| 590b32c7ae | |||
| 9530c378d5 | |||
| 8fc2f85c37 | |||
| 88063f5ec7 | |||
| 9e6e8f580c | |||
| 586baeea14 | |||
| 3bf44c44a1 | |||
| f05cc3f045 | |||
| 0aced49759 | |||
| 45d87e5462 | |||
| 5b10212354 | |||
| 5324c4f681 | |||
| e315063e3f | |||
| ba35a08cca | |||
| e4a2fafb90 | |||
| 2c89644149 | |||
| 07d2a1e897 | |||
| 89f832bf22 | |||
| 6161cd8ea2 | |||
| 7b708ace1e | |||
| e0b542fac6 | |||
| 17f871b424 | |||
| 4b565d9dd0 | |||
| d1c2678cb1 | |||
| 1fbe13bb03 | |||
| 38b0dbd4d0 | |||
| 6d9bab52bd | |||
| 11aac106cd | |||
| 9a043cc95e | |||
| c527144719 | |||
| 73905a789e | |||
| d115005a14 | |||
| df2c381946 | |||
| 3b4539858c | |||
| 0403af91fa | |||
| b98bbd8d4c | |||
| 1722e43fa8 | |||
| 05af5fa0f6 | |||
| fdab5c42a6 | |||
| d62fce6b32 | |||
| 33b737296e | |||
| dfcb7f1445 | |||
| 14511dce70 | |||
| f0dc216994 | |||
| 023b5a977a | |||
| 2c08b5d80a | |||
| 890b9d8db0 | |||
| 7240f13c12 | |||
| 910fe5546c | |||
| 4d633853a8 | |||
| 1b0c9f77ab | |||
| d262c4bc51 | |||
| 625ee91bc0 | |||
| 05e296879f | |||
| 33140d4928 | |||
| f178d92273 | |||
| ffae105b38 | |||
| 7a65acc8ee | |||
| 5de892d3b3 | |||
| 180e4143a1 | |||
| f92b1168ac | |||
| c343ee4c5f | |||
| ee33e560e7 | |||
| 6589c8ed17 | |||
| 7a93eb59a1 | |||
| a18bd4d184 | |||
| 34821f2b4b | |||
| 16d46abb1a | |||
| 10259a9c99 | |||
| 937971e3dd | |||
| dd732d3044 | |||
| 77129b9f81 | |||
| 00bf9561da | |||
| 1fb820c930 | |||
| 76c1fc1669 | |||
| 64be5837f6 | |||
| 2b2af83ddc | |||
| 1817ec2aaa | |||
| 738a10f970 | |||
| 85a44b6657 | |||
| 869ebbe44e | |||
| b648cf730f | |||
| 046a901c40 | |||
| df478027d8 | |||
| 3eca6024cd | |||
| fc3f517a1b | |||
| f2e82acd4a | |||
| f9122b7450 | |||
| c6fdff53c3 | |||
| dfcb63c18a | |||
| 9afcdbf65d | |||
| 6c7e7b7df2 | |||
| 6fd0f10ee7 | |||
| 7889d562b4 | |||
| 5731e7e408 | |||
| e333503954 | |||
| fd85bb95d2 | |||
| b48660118b | |||
| e9de116400 | |||
| 6127ef9c72 | |||
| 21842d159d | |||
| 21e0598159 | |||
| 8e4f83f7fe | |||
| b394089a79 | |||
| 2faf1e65c3 | |||
| 5eafec9f03 | |||
| 668bc38d70 | |||
| 20108478b5 | |||
| fbd1a9d02f | |||
| 5b466f7979 | |||
| c1d7854eb7 | |||
| b642f091c3 | |||
| 19a2523089 | |||
| 1bc1d23934 | |||
| b1a0f979c9 | |||
| fa67206416 | |||
| ae891362c7 | |||
| 8d94a7d7bd | |||
| 4d450111e9 | |||
| 5d2bb9bea9 | |||
| dc04e6c5f8 | |||
| 7551032364 | |||
| 1a935d0758 | |||
| a696a1b0b4 | |||
| 25a553c8f4 | |||
| 9c4a472ac1 | |||
| fb7202ad34 | |||
| 1de62f442e | |||
| 257bf317d0 | |||
| 0fbfde71fa | |||
| 10ba796288 | |||
| 0249c6fdce | |||
| 7cabbf1ee7 | |||
| bcbf15df13 | |||
| 578e77b5c3 | |||
| 68ccfde3f3 | |||
| 9edc8eeab5 | |||
| b3d570527e | |||
| c1e5af36c4 | |||
| 1404698c5e | |||
| 7ece2673cc | |||
| 95ceb279a2 | |||
| 3a6611602f | |||
| 468cdebb38 | |||
| 9faca0fefe | |||
| 58c5eb8118 | |||
| ac2eca2529 | |||
| f1054af1f1 | |||
| 6864405e55 | |||
| 39027acc91 | |||
| 6e65991341 | |||
| 760bf67c32 | |||
| 9ece367ff1 | |||
| 8ff9c94936 | |||
| d401655b45 | |||
| 85b266cb79 | |||
| 39be4cf2be | |||
| 68477319e2 | |||
| 08d3cc11d9 | |||
| 082e67287a | |||
| 518fe07ea1 | |||
| d2e36316ea | |||
| 5362e98500 | |||
| 2839f0457a | |||
| a77f70f457 | |||
| 73036c7347 | |||
| b9dbac65cb | |||
| 8f49fe7adf | |||
| 52e968809e | |||
| 7192d5d5f5 | |||
| dc7d4714d9 | |||
| 9a714b92fb | |||
| ed57338919 | |||
| d696f7347f | |||
| d7aa35bf7c | |||
| d67eba2684 | |||
| 6779684834 | |||
| 9f594f8259 | |||
| 46bab9cd8f | |||
| c533eea6e3 | |||
| 6c187a0cc2 | |||
| 4a14f91c54 | |||
| bcadc1e1d0 | |||
| ceb4979949 | |||
| a913eb600d | |||
| bbdf8edce8 | |||
| 9abb977844 | |||
| b2d143c931 | |||
| 486b14e818 | |||
| 92a9280e8e | |||
| 8741baeb52 | |||
| db39305290 | |||
| c1152d7fe7 | |||
| dddf3f6e7c | |||
| e2510113d4 | |||
| 23fbea2b9b | |||
| 1fcef2654c | |||
| b2f98d25bc | |||
| 9bae43d200 | |||
| 2b4c690886 | |||
| 132264864c | |||
| 7f1297e6ae | |||
| 69a5ab9b83 | |||
| a49eb15cbe | |||
| ca5b56266d | |||
| 822ef61a0c | |||
| 363184e7fa | |||
| 5e877a8a8b | |||
| 4715350d7c | |||
| ce32d6dcec | |||
| d801c10e62 | |||
| 3b143de0ca | |||
| 0ac1d6bbce | |||
| 66761665da | |||
| 8236902325 | |||
| f4a2d21ca6 | |||
| 4fea9b5ef3 | |||
| 5a4c151cff | |||
| 6e067a091e | |||
| 40562ef5b2 | |||
| 1a04a17d0c | |||
| 6e89eaaa4e | |||
| 0ecdf6b037 | |||
| 62eb882d31 | |||
| 971779b408 | |||
| 726c21ee47 | |||
| 92ef1620b2 | |||
| e5589d8b7d | |||
| 14d4647b5a | |||
| b46717e8f0 | |||
| edfaf8993c | |||
| 95b4dd3d71 | |||
| 53ff803f38 | |||
| 6d5bd92f1b | |||
| 63ae29af74 | |||
| 7acc0b2021 | |||
| b5f998eb0e | |||
| dbe35eb3c4 | |||
| cf9d446035 | |||
| 6e768dda5d | |||
| 22d70ad987 | |||
| d0f0042073 | |||
| 31685d57b7 | |||
| f054a3fb0d | |||
| 935253d79d | |||
| 30feae269c | |||
| bb86f2b01e | |||
| 8247429780 | |||
| f8134aa08c | |||
| 863ea657ed | |||
| 77c5471656 | |||
| ba33994252 | |||
| c08cb0a8f3 | |||
| e25465ba5b | |||
| 1276f4c286 | |||
| b66d09634b | |||
| 3c127293bc | |||
| f79e64883f | |||
| 479a26b5c9 | |||
| 662a6cd2dd | |||
| 1ee63dec26 | |||
| 7f1186c316 | |||
| a35fd6759a | |||
| 8466c44a9a | |||
| 5750ae3a6b | |||
| ccab48d31e | |||
| eba68da269 | |||
| 83e56e4279 | |||
| cbb2267164 | |||
| 526317f142 | |||
| 3e68adf77a | |||
| bd58259a43 | |||
| df75d4c989 | |||
| 7e88d16197 | |||
| 3b7a884f4c | |||
| fc910fec05 | |||
| 2f797719fa | |||
| e30af4f4c6 | |||
| 1a85f9a047 | |||
| 7737128b6e | |||
| bd2a318618 | |||
| 441551f8d8 | |||
| 4b679732b1 | |||
| 676a9d42cd | |||
| b24a45e4bf | |||
| b2ae958992 | |||
| f2c8942b84 | |||
| 9523ff2db0 | |||
| 00a921e0e7 | |||
| d960bd978c | |||
| 83263717fd | |||
| bc2389968d | |||
| 2b49625106 | |||
| d375986c36 | |||
| ddd4c1e427 | |||
| 9848e18caf | |||
| d490107bce | |||
| 55c72a5570 | |||
| ef892af56f | |||
| 46ea00aef8 | |||
| bac2a36472 | |||
| 3156909cf6 | |||
| 956beb3850 | |||
| 7869268e2d | |||
| 8e94164976 | |||
| 9ef06d7a08 | |||
| fd7960643d | |||
| 72ee2c4939 | |||
| 01e5350060 | |||
| 478fc69ca5 | |||
| 98a5c584e0 | |||
| c34211ae59 | |||
| 30fbc5b50a | |||
| a2356ce977 | |||
| 34df179c4c | |||
| 20f3c21368 | |||
| 0c1c10a5f1 | |||
| 4c0eea3dae | |||
| 72c9ec3453 | |||
| 2602e970e1 | |||
| fecf9b43aa | |||
| 70e25d17ea | |||
| 2943f46829 | |||
| dfb667f0de | |||
| 90f7a9ec81 | |||
| 26a43e76a4 | |||
| d28e2513ee | |||
| 2ad0336a53 | |||
| d9405ea058 | |||
| f46132b95c | |||
| 1bfad8ba22 | |||
| 7893eae4ca | |||
| 06a3edc446 | |||
| 237bd8e6b7 | |||
| e15c200541 | |||
| 405c5d145b | |||
| 848a05beb8 | |||
| 96917675a0 | |||
| 751b3f2fc7 | |||
| c12807d040 | |||
| d8c42dcee6 | |||
| 2f073dc8eb | |||
| cfb50440d7 | |||
| 216e945c06 | |||
| 4174864b8c | |||
| 9e7cc42c78 | |||
| d6846adacf | |||
| a30900aebe | |||
| 9c0e0122c5 | |||
| 2dd59f36a0 | |||
| d5d4c5874b | |||
| d2107eccde | |||
| 377268359b | |||
| a726f7f1d5 | |||
| e252e554c4 | |||
| 85b3d698b2 | |||
| abfcae48e0 | |||
| 4ab88c19c9 | |||
| 7615857663 | |||
| 00045f4837 | |||
| 348c3ef725 | |||
| 7d3d4de51c | |||
| 0c5c7953b6 | |||
| 58049dd449 | |||
| 8413b5c9d5 | |||
| 101a9084b8 | |||
| 79c51379b8 | |||
| 7db116cb89 | |||
| 219168a378 | |||
| 958cfd7eba | |||
| 3fbd65ceca | |||
| 4bef98e3f4 | |||
| 610a0c3c92 | |||
| 9ae755af2a | |||
| c5900911d7 | |||
| 926787fffb | |||
| 1b5bd3f5d8 | |||
| 562fb14cea | |||
| b5104cef3c | |||
| a067eb2070 | |||
| d5b6120d45 | |||
| bba15ab759 | |||
| afb42e3589 | |||
| 59bf7d5a7a | |||
| 8e297fda48 | |||
| 1093143773 | |||
| 2ccc796b6f | |||
| 634002c471 | |||
| 5d41085dd4 | |||
| 5f2bc52e6b | |||
| 2d1cbaae73 | |||
| 2d4f11717c | |||
| be6cfefbec | |||
| 2e1aa59250 | |||
| 04398052d8 | |||
| 88db4a5020 | |||
| 6338fe68c9 | |||
| bf11678e20 | |||
| 4d0bda7238 | |||
| 2c4d5ab31d | |||
| 13629079a8 | |||
| 4d2cd9eff4 | |||
| 4a358f12f4 | |||
| a182ad7a0f | |||
| dda1b4e547 | |||
| 572aceb107 | |||
| 97440a776c | |||
| 8b23c591ff | |||
| 1d776ea766 | |||
| 3b34c6746e | |||
| 73aaba15a9 | |||
| dd7220a62b | |||
| 4a774d387c | |||
| 1a46723abb | |||
| 2052bc7353 | |||
| 6e9136d4ea | |||
| 08e5f3cf40 | |||
| 9af6b6a8bd | |||
| 9bc31c1111 | |||
| 6b887f26c5 | |||
| 97a33bde9a | |||
| 10597931be | |||
| 452f7a5d98 | |||
| 3b6e690046 | |||
| 233b7163a2 | |||
| 821a4dd5b4 | |||
| 2116b7fc7d | |||
| aed9191584 | |||
| 75633fc216 | |||
| efe41212b0 | |||
| cfca519c5a | |||
| 544ddbbddd | |||
| 3cbc2e07bb | |||
| cae82e9866 | |||
| c3ea3ac6cc | |||
| 0641cf4d62 | |||
| ebd859e4d2 | |||
| 2fe09b5f40 | |||
| 29d6ea3668 | |||
| d901459581 | |||
| 38897f7f3b | |||
| d13a82fc9f | |||
| b0d3d19694 | |||
| 512fb35fb4 | |||
| f61714db17 | |||
| 2a08678635 | |||
| 4e3ff06d7c | |||
| d9d7567f06 | |||
| ce84389980 | |||
| c78feffc41 | |||
| d3f490ee89 | |||
| 5195656bac | |||
| dfce7eeac7 | |||
| e8060925a3 | |||
| 0fe2ea1d44 | |||
| f461a02b1f | |||
| 6c9196fdb2 | |||
| e76ee0b093 | |||
| 498f1bf2dc | |||
| e3cd5edaf7 | |||
| b8d1d954b6 | |||
| 7c2687b753 | |||
| 96b3a522c3 | |||
| 8ec6fa0f52 | |||
| 0fb683bbac | |||
| bc9490a7e4 | |||
| a0642e85b4 | |||
| e6e76df8c2 | |||
| 9b506f12e3 | |||
| c176f72fd8 | |||
| 678ce4f139 | |||
| 1d2818ef61 | |||
| 6f56fffcbd | |||
| c25d0e1304 | |||
| 45dce17fc6 | |||
| e61f9eb242 | |||
| 8a37c9ec83 | |||
| 49bc91117a | |||
| 9f7177d847 | |||
| 4bd330ff59 | |||
| 4895066215 | |||
| f9e30b778d | |||
| 72cf250afe | |||
| efb99f090d | |||
| 661402d052 | |||
| 45a1c38418 | |||
| 7d0851a785 | |||
| 7f8c935a82 | |||
| 169bb09576 | |||
| b14a989141 | |||
| 74844260c8 | |||
| 970b4ce104 | |||
| 74abbe9cc0 | |||
| 7ac7fea803 | |||
| c28aec44b6 | |||
| da8b117055 | |||
| 2c87d47507 | |||
| 92c9f2ca2d | |||
| 8d34a5bd1a | |||
| 27b9035fd8 | |||
| 59b20dabbb | |||
| 1e6dcd8fa7 | |||
| e08bf1b18d | |||
| 645b7f4482 | |||
| 37a71f050f | |||
| 8c5b24689a | |||
| 37777f0d92 | |||
| c5191deb40 | |||
| 78afd40955 | |||
| 2c7249f12d | |||
| d9806e8ea9 | |||
| 373de97515 | |||
| 52a96ae480 | |||
| d2398400e2 | |||
| 50de79e2f7 | |||
| 8af367d67e | |||
| 31a94da8ff | |||
| a9d5bc5d60 | |||
| 344a0f7ff0 | |||
| 1bd6c1c451 | |||
| 1d2f862d98 | |||
| 2ecfdc2d4b | |||
| 503e1f6eff | |||
| 6516616bf4 | |||
| bcd063b8f3 | |||
| 459de7be6f | |||
| 2d12868849 | |||
| dfe921c231 | |||
| 17dddfd5d9 | |||
| 21cb6f505e | |||
| e5f0ca77a9 | |||
| 6e5c85be7e | |||
| 43d5f93c4e | |||
| 955a489e21 | |||
| 05ef2099d4 | |||
| af717a53b0 | |||
| 59c53a191d | |||
| 9ec7c897df | |||
| cfe7212a80 | |||
| c0dd692263 | |||
| 71dc18afdb | |||
| ebf0fbd5d5 | |||
| 7a75c3c961 | |||
| b7ebc55c17 | |||
| 3c97db414b | |||
| 988fc94710 | |||
| d33355d561 | |||
| bbc3aa5dd2 | |||
| 4afc962f79 | |||
| f22fe29b26 | |||
| 0f7b28a6cd | |||
| 1539574fba | |||
| 3e5f193964 | |||
| 40e1e8b559 | |||
| 4871811385 | |||
| bf46534d4d | |||
| 471486ce01 | |||
| 9590a39d4b | |||
| 51a4dd2a05 | |||
| c85f28f8ea | |||
| 973e8cc77a | |||
| e852253f93 | |||
| 4e5c041a81 | |||
| 250f6f0670 | |||
| 313d52ad14 | |||
| e1af7b22f0 | |||
| db2303bc16 | |||
| 6010aa1336 | |||
| f7e9dbf292 | |||
| d13222bfe3 | |||
| e8daec0d0d | |||
| 2bc4c3c1c7 | |||
| cb43e60ae9 | |||
| b69d55d114 | |||
| c4ce4d6258 | |||
| 01081ab9a7 | |||
| d2a400d08a | |||
| 5c1fe910b6 | |||
| 97a2f644f0 | |||
| 35e1caf817 | |||
| bccf6a4e05 | |||
| 7ef9b5be4e | |||
| 075509f97e | |||
| 56dfe8d88a | |||
| f2c5fdf9b5 | |||
| 48020c771e | |||
| cd7f060feb | |||
| c0bb2148c6 | |||
| 966025c04f | |||
| e9fdd976ce | |||
| d1b5847f4c | |||
| ad58272a21 | |||
| ec5f66d6f1 | |||
| 5e59538865 | |||
| 4f884b3321 | |||
| cc55430837 | |||
| 9e2fbbe494 | |||
| 322091248e | |||
| 6f241d8803 | |||
| 720c318186 | |||
| caeed2ab43 | |||
| 4a5ef3c1b2 | |||
| 2da413754e | |||
| 7375149ada | |||
| 2cfd1b76d5 | |||
| 7de4a806a2 | |||
| 9e21dc43e9 | |||
| b711f263eb | |||
| d61cb52afe | |||
| f0f20dc39f | |||
| 3beebe0a87 | |||
| 3de40cd5cf | |||
| 680ddad042 | |||
| d65234508f | |||
| 8c7bce4244 | |||
| 58b9b5a775 | |||
| 3c2b7a757c | |||
| 69fc1c199d | |||
| 8c5e597f43 | |||
| 551427b5a1 | |||
| 9905cfa01b | |||
| 981693093c | |||
| 69ef1bf275 | |||
| cd953b47fe | |||
| 0578268f2d | |||
| 2422940477 | |||
| c551828192 | |||
| 077dd51e8a | |||
| 1a06f66555 | |||
| 345c6226b8 | |||
| d2052bf8b1 | |||
| 2f90205a16 | |||
| e267c88043 | |||
| 11c9bf7704 | |||
| fb4730942f | |||
| 68b3fcaa43 | |||
| 45d8cc9e8e | |||
| e501eaf238 | |||
| c8395c6558 | |||
| 00b69bfbab | |||
| 4d5fe65988 | |||
| 9441825b8f | |||
| 48047cfea7 | |||
| b26ea22041 | |||
| 2bdde353b1 | |||
| cb18485c3a | |||
| 0e267ca2a6 | |||
| 085f93ba39 | |||
| afea99ae6f | |||
| 3cd9e4919a | |||
| e6b7914ccd | |||
| c7e74fe209 | |||
| b3f69c0c70 | |||
| 11848b1ded | |||
| d6b48962ce | |||
| 521f407714 | |||
| fd50868899 | |||
| 4f00a18599 | |||
| 767e775f31 | |||
| bedb66fdd5 | |||
| e3319a3bd4 | |||
| c74111e29b | |||
| bd2204156e | |||
| fe15eeeac7 | |||
| 280a1a9313 | |||
| 27105910df | |||
| f3d2616afb | |||
| 99cd5ddf3c | |||
| 02ebef1db8 | |||
| 28227ddc2e | |||
| b848261468 | |||
| f1ff31829c | |||
| 0736285e24 | |||
| 79bc345307 | |||
| a737711652 | |||
| b5e8b89ed5 | |||
| a51b45df90 | |||
| 5612055b85 | |||
| e3aff78e89 | |||
| 84a7576772 | |||
| 69cd14f471 | |||
| 1a2d223740 | |||
| 4ccbf86589 | |||
| 502b6586f1 | |||
| 4f57677e03 | |||
| 8c1494e17e | |||
| 178d06650a | |||
| 926ee94aae | |||
| 50a6209f11 | |||
| e82a467f42 | |||
| f0d508be6f | |||
| 82a3c606c7 | |||
| e2b0aa65f8 | |||
| 271344cfc1 | |||
| 72cafdb27a | |||
| 930675672b | |||
| 73f8731014 | |||
| 33addcd90c | |||
| b5ee03b142 | |||
| a0d4fa5b82 | |||
| 317bbba8c9 | |||
| cd58ccbb52 | |||
| da032f3518 | |||
| 487da37bea | |||
| 3da95cce76 | |||
| 9a7cf5c35e | |||
| a022a4e8e8 | |||
| b96ae74c14 | |||
| 7a420fb6f9 | |||
| 58d161210d | |||
| 41c7cf115d | |||
| f511553ff1 | |||
| 9e047650e8 | |||
| ba3bbfc097 | |||
| dcc2d1ee70 | |||
| f167f0a0f9 | |||
| 7758e20224 | |||
| a1cc2ee8c6 | |||
| bdf397ad0f | |||
| 4dcae92ab7 | |||
| a7f5192057 | |||
| 1d4fedf7c7 | |||
| 7496d0c347 | |||
| 24e6f2469f | |||
| fdad7065f7 | |||
| 7b16d3541d | |||
| 3c9c3c9c8c | |||
| f0413b7037 | |||
| 749f1a6a70 | |||
| 512a23ef64 | |||
| 23729c5b89 | |||
| f31ee10cf7 | |||
| 21d34b091b | |||
| fd19aa8b2e | |||
| 5af0b62e8e | |||
| 965cb33e34 | |||
| aca19b3427 | |||
| 238baf89be | |||
| 50218a1146 | |||
| d1280a8797 | |||
| ca9358cf1a | |||
| c3fa8f3131 | |||
| 4af110cd69 | |||
| 45d2d4306b | |||
| 385f01da8b | |||
| 85da160957 | |||
| ff15f318c4 | |||
| 5769e1b58f | |||
| 028a7c728a | |||
| 9ec3889f96 | |||
| 3aa6502996 | |||
| 847f8713c9 | |||
| 7405476f61 | |||
| 985903e99b | |||
| 347f4e5566 | |||
| 15d72afbd5 | |||
| 085964dc58 | |||
| 6c0a1265ab | |||
| 6ee0de8f62 | |||
| bc2613d6a3 | |||
| 655a499caa | |||
| bcf6f33965 | |||
| 7c04febb3b | |||
| de39bcf197 | |||
| 4e7779ee48 | |||
| 4b17896ff1 | |||
| caf20a5e61 | |||
| 896d42df43 | |||
| 1905be823d | |||
| 6347992b6e | |||
| b695d7c148 | |||
| 9b75cdc2ac | |||
| da80ed0130 | |||
| e8a23de848 | |||
| cb431a03df | |||
| 4979fb7326 | |||
| aaae51bb37 | |||
| 4d860e3764 | |||
| e73ba5c734 |
+1
-2
@@ -4,8 +4,7 @@ root = true
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
end_of_line = crlf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
|
||||
@@ -8,4 +8,9 @@
|
||||
*.tpl text eol=crlf
|
||||
*.html text eol=crlf
|
||||
|
||||
bin/command text eol=lf
|
||||
|
||||
.gitattributes text eol=crlf
|
||||
.gitignore text eol=crlf
|
||||
|
||||
*.png binary
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@ If you believe you have discovered a vulnerability in EspoCRM please contacts us
|
||||
|
||||
## Supported versions
|
||||
|
||||
For severe vulnerabilities we provide fixes for 2 minor versions (the second number in the version string) back from the current stable version. Separate patches or manual fix guidelines will be provided for more old versions.
|
||||
For severe vulnerabilities we provide fixes for 2 minor versions (the second number in the version string) back from the current stable version.
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'hotfix/**'
|
||||
- 'fix'
|
||||
paths:
|
||||
- '**.php'
|
||||
- '**.json'
|
||||
|
||||
@@ -24,11 +24,6 @@ Options -Indexes
|
||||
# Skip redirect for `client` dir.
|
||||
RewriteRule ^client/ - [L]
|
||||
|
||||
# {#dev}
|
||||
# Skip redirect for `node_modules` dir. Actual only for dev environment.
|
||||
RewriteRule ^node_modules/ - [L]
|
||||
# {/dev}
|
||||
|
||||
# Store base path.
|
||||
RewriteCond %{REQUEST_URI}::$1 ^(.*?/)(.*)::\2$
|
||||
RewriteRule ^(.*)$ - [E=BASE:%1]
|
||||
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
*
|
||||
!.gitignore
|
||||
!/codeStyles
|
||||
!/fileTemplates
|
||||
!/inspectionProfiles
|
||||
Generated
+11
@@ -0,0 +1,11 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<PHPCodeStyleSettings>
|
||||
<option name="GROUP_USE_WRAP" value="2" />
|
||||
</PHPCodeStyleSettings>
|
||||
<codeStyleSettings language="PHP">
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-${YEAR} Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
+1
@@ -0,0 +1 @@
|
||||
#parse("PHP File Header.php")
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="ES6ConvertVarToLetConst" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="JSIgnoredPromiseFromCall" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpDocMissingThrowsInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpDocSignatureIsNotCompleteInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpMissingFieldTypeInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpMissingParamTypeInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpMissingReturnTypeInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PhpStanGlobal" enabled="false" level="WEAK WARNING" enabled_by_default="false">
|
||||
<option name="config" value="$PROJECT_DIR$/phpstan.neon" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PhpSwitchStatementWitSingleBranchInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PsalmAdvanceCallableParamsInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="TrivialIfJS" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
||||
+49
-98
@@ -32,36 +32,41 @@
|
||||
const fs = require('fs');
|
||||
const cp = require('child_process');
|
||||
const path = require('path');
|
||||
const buildUtils = require('./js/build-utils');
|
||||
|
||||
module.exports = grunt => {
|
||||
|
||||
const pkg = grunt.file.readJSON('package.json');
|
||||
const bundleConfig = require('./frontend/bundle-config.json');
|
||||
const libs = require('./frontend/libs.json');
|
||||
|
||||
let jsFilesToBundle = getBundleLibList().concat(bundleConfig.jsFiles);
|
||||
let jsFilesToCopy = getCopyLibDataList();
|
||||
const originalLibDir = 'client/lib/original';
|
||||
|
||||
let libFilesToMinify = jsFilesToCopy
|
||||
let bundleJsFileList = buildUtils.getPreparedBundleLibList(libs).concat(originalLibDir + '/espo.js');
|
||||
let copyJsFileList = buildUtils.getCopyLibDataList(libs);
|
||||
|
||||
let minifyLibFileList = copyJsFileList
|
||||
.filter(item => item.minify)
|
||||
.reduce((map, item) => (
|
||||
map[item.dest] = item.dest,
|
||||
map
|
||||
), {});
|
||||
.map(item => {
|
||||
return {
|
||||
dest: item.dest,
|
||||
src: item.originalDest,
|
||||
};
|
||||
});
|
||||
|
||||
let currentPath = path.dirname(fs.realpathSync(__filename));
|
||||
|
||||
let themeList = [];
|
||||
|
||||
fs.readdirSync('application/Espo/Resources/metadata/themes').forEach(file => {
|
||||
themeList.push(file.substr(0, file.length - 5));
|
||||
themeList.push(file.substring(0, file.length - 5));
|
||||
});
|
||||
|
||||
let cssminFilesData = {};
|
||||
|
||||
let lessData = {};
|
||||
|
||||
themeList.forEach(theme => {
|
||||
let name = camelCaseToHyphen(theme);
|
||||
let name = buildUtils.camelCaseToHyphen(theme);
|
||||
|
||||
let files = {};
|
||||
|
||||
@@ -71,14 +76,12 @@ module.exports = grunt => {
|
||||
cssminFilesData['client/css/espo/'+name+'.css'] = 'client/css/espo/'+name+'.css';
|
||||
cssminFilesData['client/css/espo/'+name+'-iframe.css'] = 'client/css/espo/'+name+'-iframe.css';
|
||||
|
||||
let o = {
|
||||
lessData[theme] = {
|
||||
options: {
|
||||
yuicompress: true,
|
||||
},
|
||||
files: files,
|
||||
};
|
||||
|
||||
lessData[theme] = o;
|
||||
});
|
||||
|
||||
grunt.initConfig({
|
||||
@@ -118,7 +121,7 @@ module.exports = grunt => {
|
||||
'build/tmp/client/custom/modules/*',
|
||||
'!build/tmp/client/custom/modules/dummy.txt',
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
less: lessData,
|
||||
@@ -142,11 +145,11 @@ module.exports = grunt => {
|
||||
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
|
||||
},
|
||||
files: {
|
||||
'client/lib/espo.min.js': jsFilesToBundle,
|
||||
'client/lib/espo.min.js': bundleJsFileList,
|
||||
},
|
||||
},
|
||||
lib: {
|
||||
files: libFilesToMinify,
|
||||
files: minifyLibFileList,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -172,7 +175,7 @@ module.exports = grunt => {
|
||||
dest: 'build/tmp/client',
|
||||
},
|
||||
frontendLib: {
|
||||
files: jsFilesToCopy,
|
||||
files: copyJsFileList,
|
||||
},
|
||||
backend: {
|
||||
expand: true,
|
||||
@@ -246,15 +249,33 @@ module.exports = grunt => {
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
grunt.registerTask('espo-bundle', () => {
|
||||
const Bundler = require('./js/bundler');
|
||||
|
||||
let contents = (new Bundler()).bundle(bundleConfig.jsFiles);
|
||||
|
||||
if (!fs.existsSync(originalLibDir)) {
|
||||
fs.mkdirSync(originalLibDir);
|
||||
}
|
||||
|
||||
fs.writeFileSync(originalLibDir + '/espo.js', contents, 'utf8');
|
||||
});
|
||||
|
||||
grunt.registerTask('prepare-lib-original', () => {
|
||||
// Even though `npm ci` runs the same script, 'clean:start' deletes files.
|
||||
cp.execSync("node js/scripts/prepare-lib-original");
|
||||
});
|
||||
|
||||
grunt.registerTask('prepare-lib', () => {
|
||||
cp.execSync("node js/scripts/prepare-lib");
|
||||
});
|
||||
|
||||
grunt.registerTask('chmod-folders', () => {
|
||||
cp.execSync(
|
||||
"find . -type d -exec chmod 755 {} +",
|
||||
{
|
||||
cwd: 'build/EspoCRM-' + pkg.version,
|
||||
}
|
||||
{cwd: 'build/EspoCRM-' + pkg.version}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -360,7 +381,7 @@ module.exports = grunt => {
|
||||
});
|
||||
|
||||
grunt.registerTask('upgrade', () => {
|
||||
cp.execSync("node diff --all --vendor", {stdio: 'inherit'});
|
||||
cp.execSync("node diff --closest", {stdio: 'inherit'});
|
||||
});
|
||||
|
||||
grunt.registerTask('unit-tests-run', () => {
|
||||
@@ -375,7 +396,7 @@ module.exports = grunt => {
|
||||
cp.execSync("composer run-script setConfigParams", {stdio: 'ignore'});
|
||||
});
|
||||
|
||||
grunt.registerTask('zip', () => {
|
||||
grunt.registerTask('zip', function () { // Don't change to arrow-function.
|
||||
const archiver = require('archiver');
|
||||
|
||||
let resolve = this.async();
|
||||
@@ -404,8 +425,9 @@ module.exports = grunt => {
|
||||
|
||||
archive
|
||||
.directory(currentPath + '/build/' + folder, folder)
|
||||
.pipe(zipOutput)
|
||||
.finalize();
|
||||
.pipe(zipOutput);
|
||||
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
grunt.registerTask('npm-install', () => {
|
||||
@@ -423,8 +445,11 @@ module.exports = grunt => {
|
||||
grunt.registerTask('internal', [
|
||||
'less',
|
||||
'cssmin',
|
||||
'espo-bundle',
|
||||
'prepare-lib-original',
|
||||
'uglify:bundle',
|
||||
'copy:frontendLib',
|
||||
'prepare-lib',
|
||||
'uglify:lib',
|
||||
]);
|
||||
|
||||
@@ -487,77 +512,3 @@ module.exports = grunt => {
|
||||
'offline',
|
||||
]);
|
||||
};
|
||||
|
||||
function getBundleLibList() {
|
||||
const libs = require('./frontend/libs.json');
|
||||
|
||||
let list = [];
|
||||
|
||||
libs.forEach(item => {
|
||||
if (!item.bundle) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.files) {
|
||||
item.files.forEach(item => {
|
||||
list.push(item.src);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.src) {
|
||||
throw new Error("No lib src.");
|
||||
}
|
||||
|
||||
list.push(item.src);
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function getCopyLibDataList() {
|
||||
const libs = require('./frontend/libs.json');
|
||||
|
||||
let list = [];
|
||||
|
||||
libs.forEach(item => {
|
||||
if (item.bundle) {
|
||||
return;
|
||||
}
|
||||
|
||||
let minify = item.minify;
|
||||
|
||||
if (item.files) {
|
||||
item.files.forEach(item => {
|
||||
list.push({
|
||||
src: item.src,
|
||||
dest: item.dest || 'client/lib/' + item.src.split('/').pop(),
|
||||
minify: minify,
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.src) {
|
||||
throw new Error("No lib src.");
|
||||
}
|
||||
|
||||
list.push({
|
||||
src: item.src,
|
||||
dest: item.dest || 'client/lib/' + item.src.split('/').pop(),
|
||||
minify: minify,
|
||||
});
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function camelCaseToHyphen(string){
|
||||
if (string === null) {
|
||||
return string;
|
||||
}
|
||||
|
||||
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ class AccessChecker implements AccessEntityCREDSChecker
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $assignedUserIdList */
|
||||
$assignedUserIdList = $entity->getLinkMultipleIdList('assignedUsers');
|
||||
|
||||
if (
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\Acl\Note;
|
||||
|
||||
use Espo\Entities\Note;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
@@ -77,6 +78,9 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$parentId = $entity->get('parentId');
|
||||
@@ -95,6 +99,62 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$parentId = $entity->getParentId();
|
||||
$parentType = $entity->getParentType();
|
||||
|
||||
if ($parentId && $parentType) {
|
||||
$parent = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
if (!$parent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->aclManager->checkEntityStream($user, $parent);
|
||||
}
|
||||
|
||||
if ($entity->getType() !== Note::TYPE_POST) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($entity->getCreatedById() === $user->getId()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($entity->getTargetType() === Note::TARGET_ALL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($entity->getTargetType() === Note::TARGET_TEAMS) {
|
||||
$targetTeamIdList = $entity->getLinkMultipleIdList('teams') ?? [];
|
||||
|
||||
foreach ($user->getTeamIdList() as $teamId) {
|
||||
if (in_array($teamId, $targetTeamIdList)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($entity->getTargetType() === Note::TARGET_USERS) {
|
||||
return in_array($user->getId(), $entity->getLinkMultipleIdList('users') ?? []);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
@@ -106,7 +166,7 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
}
|
||||
|
||||
if (!$this->aclManager->checkOwnershipOwn($user, $entity)) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
$createdAt = $entity->get('createdAt');
|
||||
@@ -134,6 +194,9 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if ($user->isAdmin()) {
|
||||
@@ -145,7 +208,7 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
}
|
||||
|
||||
if (!$this->aclManager->checkOwnershipOwn($user, $entity)) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
$createdAt = $entity->get('createdAt');
|
||||
|
||||
@@ -29,22 +29,24 @@
|
||||
|
||||
namespace Espo\Classes\Acl\Note;
|
||||
|
||||
use Espo\Entities\Note;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\{
|
||||
Acl\OwnershipOwnChecker,
|
||||
};
|
||||
use Espo\Core\Acl\OwnershipOwnChecker;
|
||||
|
||||
/**
|
||||
* @implements OwnershipOwnChecker<\Espo\Entities\Note>
|
||||
*/
|
||||
class OwnershipChecker implements OwnershipOwnChecker
|
||||
{
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkOwn(User $user, Entity $entity): bool
|
||||
{
|
||||
if ($entity->get('type') === 'Post' && $user->getId() === $entity->get('createdById')) {
|
||||
if ($entity->getType() === Note::TYPE_POST && $user->getId() === $entity->getCreatedById()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class OwnershipChecker implements OwnershipOwnChecker
|
||||
{
|
||||
public function checkOwn(User $user, Entity $entity): bool
|
||||
{
|
||||
/** @var string[] */
|
||||
/** @var string[] $userTeamIdList */
|
||||
$userTeamIdList = $user->getLinkMultipleIdList('teams');
|
||||
|
||||
return in_array($entity->getId(), $userTeamIdList);
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Classes\AclPortal\Note;
|
||||
|
||||
use Espo\Entities\Note;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
@@ -77,16 +78,19 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$parentId = $entity->get('parentId');
|
||||
$parentType = $entity->get('parentType');
|
||||
$parentId = $entity->getParentId();
|
||||
$parentType = $entity->getParentType();
|
||||
|
||||
if (!$parentId || !$parentType) {
|
||||
return $this->defaultAccessChecker->checkEntityCreate($user, $entity, $data);
|
||||
}
|
||||
|
||||
$parent = $this->entityManager->getEntity($parentType, $parentId);
|
||||
$parent = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
if ($parent && $this->aclManager->checkEntityStream($user, $parent)) {
|
||||
return true;
|
||||
@@ -95,31 +99,42 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
return $this->defaultAccessChecker->checkEntityCreate($user, $entity, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if ($entity->get('type') !== 'Post') {
|
||||
return false;
|
||||
}
|
||||
$parentId = $entity->getParentId();
|
||||
$parentType = $entity->getParentType();
|
||||
|
||||
if ($entity->get('type') === 'Post' && $entity->get('targetType')) {
|
||||
return false;
|
||||
}
|
||||
if ($parentId && $parentType) {
|
||||
$parent = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
if (!$entity->get('parentId') || !$entity->get('parentType')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parent = $this->entityManager->getEntity($entity->get('parentType'), $entity->get('parentId'));
|
||||
|
||||
if ($parent) {
|
||||
if ($this->aclManager->checkEntityStream($user, $parent)) {
|
||||
return true;
|
||||
if (!$parent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->aclManager->checkEntityStream($user, $parent);
|
||||
}
|
||||
|
||||
if ($entity->getType() !== Note::TYPE_POST) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($entity->getCreatedById() === $user->getId()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($entity->getTargetType() === Note::TARGET_PORTALS) {
|
||||
return in_array($user->getPortalId(), $entity->getLinkMultipleIdList('portals') ?? []);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if (!$this->defaultAccessChecker->checkEntityEdit($user, $entity, $data)) {
|
||||
@@ -127,7 +142,7 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
}
|
||||
|
||||
if (!$this->aclManager->checkOwnershipOwn($user, $entity)) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
$createdAt = $entity->get('createdAt');
|
||||
@@ -155,6 +170,9 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
if (!$this->defaultAccessChecker->checkEntityDelete($user, $entity, $data)) {
|
||||
@@ -162,7 +180,7 @@ class AccessChecker implements AccessEntityCREDChecker
|
||||
}
|
||||
|
||||
if (!$this->aclManager->checkOwnershipOwn($user, $entity)) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
$createdAt = $entity->get('createdAt');
|
||||
|
||||
@@ -29,22 +29,24 @@
|
||||
|
||||
namespace Espo\Classes\AclPortal\Note;
|
||||
|
||||
use Espo\Entities\Note;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\{
|
||||
Acl\OwnershipOwnChecker,
|
||||
};
|
||||
use Espo\Core\Acl\OwnershipOwnChecker;
|
||||
|
||||
/**
|
||||
* @implements OwnershipOwnChecker<\Espo\Entities\Note>
|
||||
*/
|
||||
class OwnershipChecker implements OwnershipOwnChecker
|
||||
{
|
||||
/**
|
||||
* @param Note $entity
|
||||
*/
|
||||
public function checkOwn(User $user, Entity $entity): bool
|
||||
{
|
||||
if ($entity->get('type') === 'Post' && $user->getId() === $entity->get('createdById')) {
|
||||
if ($entity->getType() === Note::TYPE_POST && $user->getId() === $entity->getCreatedById()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class Container
|
||||
'user',
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $fileList */
|
||||
$fileList = scandir('application/Espo/Core/Loaders');
|
||||
|
||||
if (file_exists('custom/Espo/Custom/Core/Loaders')) {
|
||||
|
||||
@@ -34,20 +34,24 @@ use Espo\Core\{
|
||||
Select\SelectBuilderFactory,
|
||||
ORM\EntityManager,
|
||||
};
|
||||
use Espo\Tools\App\AppParam;
|
||||
|
||||
/**
|
||||
* Returns a list of entity types for which a PDF template exists.
|
||||
*/
|
||||
class TemplateEntityTypeList
|
||||
class TemplateEntityTypeList implements AppParam
|
||||
{
|
||||
private $acl;
|
||||
private Acl $acl;
|
||||
|
||||
private $selectBuilderFactory;
|
||||
private SelectBuilderFactory $selectBuilderFactory;
|
||||
|
||||
private $entityManager;
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(Acl $acl, SelectBuilderFactory $selectBuilderFactory, EntityManager $entityManager)
|
||||
{
|
||||
public function __construct(
|
||||
Acl $acl,
|
||||
SelectBuilderFactory $selectBuilderFactory,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->acl = $acl;
|
||||
$this->selectBuilderFactory = $selectBuilderFactory;
|
||||
$this->entityManager = $entityManager;
|
||||
|
||||
@@ -83,14 +83,21 @@ class Import implements Command
|
||||
$resultId = $result->getId();
|
||||
$countCreated = $result->getCountCreated();
|
||||
$countUpdated = $result->getCountUpdated();
|
||||
$countError = $result->getCountError();
|
||||
$countDuplicate = $result->getCountDuplicate();
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$io->writeLine("Error occurred: ". $e->getMessage() . "");
|
||||
$io->writeLine("Error occurred: " . $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$io->writeLine("Finished. Import ID: {$resultId}. Created: {$countCreated}. Updated: {$countUpdated}.");
|
||||
$io->writeLine("Finished.");
|
||||
$io->writeLine(" Import ID: {$resultId}");
|
||||
$io->writeLine(" Created: {$countCreated}");
|
||||
$io->writeLine(" Updated: {$countUpdated}");
|
||||
$io->writeLine(" Duplicates: {$countDuplicate}");
|
||||
$io->writeLine(" Errors: {$countError}");
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -102,7 +109,7 @@ class Import implements Command
|
||||
$this->service->revert($id);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$io->writeLine("Error occurred: " . $e->getMessage() . "");
|
||||
$io->writeLine("Error occurred: " . $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -119,15 +126,21 @@ class Import implements Command
|
||||
$result = $this->service->importById($id, true, $forceResume);
|
||||
}
|
||||
catch (Throwable $e) {
|
||||
$io->writeLine("Error occurred: " . $e->getMessage() . "");
|
||||
$io->writeLine("Error occurred: " . $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$countCreated = $result->getCountCreated();
|
||||
$countUpdated = $result->getCountUpdated();
|
||||
$countError = $result->getCountError();
|
||||
$countDuplicate = $result->getCountDuplicate();
|
||||
|
||||
$io->writeLine("Finished. Created: {$countCreated}. Updated: {$countUpdated}.");
|
||||
$io->writeLine("Finished.");
|
||||
$io->writeLine(" Created: {$countCreated}");
|
||||
$io->writeLine(" Updated: {$countUpdated}");
|
||||
$io->writeLine(" Duplicates: {$countDuplicate}");
|
||||
$io->writeLine(" Errors: {$countError}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class LinkMultiple implements FieldDuplicator
|
||||
->getRelation($relationDefs->getForeignRelationName())
|
||||
->getType();
|
||||
|
||||
if ($foreignRelationType !== Entity::HAS_MANY) {
|
||||
if ($foreignRelationType !== Entity::MANY_MANY) {
|
||||
$valueMap->{$field . 'Ids'} = [];
|
||||
$valueMap->{$field . 'Names'} = (object) [];
|
||||
$valueMap->{$field . 'Columns'} = (object) [];
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
namespace Espo\Classes\FieldProcessing\Email;
|
||||
|
||||
use Espo\Modules\Crm\Entities\Call;
|
||||
use Espo\Modules\Crm\Entities\Meeting;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Repositories\EmailAddress as EmailAddressRepository;
|
||||
@@ -44,7 +46,6 @@ use Espo\Core\{
|
||||
};
|
||||
|
||||
use ICal\ICal;
|
||||
use ICal\Event;
|
||||
|
||||
use Throwable;
|
||||
use stdClass;
|
||||
@@ -85,7 +86,7 @@ class IcsDataLoader implements Loader
|
||||
|
||||
$ical->initString($icsContents);
|
||||
|
||||
/* @var $event Event */
|
||||
/* @var \ICal\Event|null $event */
|
||||
$event = $ical->events()[0] ?? null;
|
||||
|
||||
if ($event === null) {
|
||||
@@ -121,11 +122,14 @@ class IcsDataLoader implements Loader
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->eventAlreadyExists($espoEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var EmailAddressRepository $emailAddressRepository */
|
||||
$emailAddressRepository = $this->entityManager->getRepository(EmailAddress::ENTITY_TYPE);
|
||||
|
||||
$attendeeEmailAddressList = $espoEvent->getAttendeeEmailAddressList();
|
||||
|
||||
$organizerEmailAddress = $espoEvent->getOrganizerEmailAddress();
|
||||
|
||||
if ($organizerEmailAddress) {
|
||||
@@ -167,7 +171,6 @@ class IcsDataLoader implements Loader
|
||||
$this->loadCreatedEvent($entity, $espoEvent, $eventData);
|
||||
|
||||
$entity->set('icsEventData', $eventData);
|
||||
|
||||
$entity->set('icsEventDateStart', $espoEvent->getDateStart());
|
||||
|
||||
if ($espoEvent->isAllDay()) {
|
||||
@@ -209,4 +212,35 @@ class IcsDataLoader implements Loader
|
||||
'name' => $createdEvent->get('name'),
|
||||
];
|
||||
}
|
||||
|
||||
private function eventAlreadyExists(EspoEvent $espoEvent): bool
|
||||
{
|
||||
$id = $espoEvent->getUid();
|
||||
|
||||
if (!$id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$found1 = $this->entityManager
|
||||
->getRDBRepository(Meeting::ENTITY_TYPE)
|
||||
->select(['id'])
|
||||
->where(['id' => $id])
|
||||
->findOne();
|
||||
|
||||
if ($found1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$found2 = $this->entityManager
|
||||
->getRDBRepository(Call::ENTITY_TYPE)
|
||||
->select(['id'])
|
||||
->where(['id' => $id])
|
||||
->findOne();
|
||||
|
||||
if ($found2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ class StringDataLoader implements Loader
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var ?string */
|
||||
/** @var ?string $fromEmailAddressId */
|
||||
$fromEmailAddressId = $entity->get('fromEmailAddressId');
|
||||
|
||||
if (!$fromEmailAddressId) {
|
||||
|
||||
@@ -29,12 +29,27 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use StdClass;
|
||||
use stdClass;
|
||||
|
||||
class ArrayType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private Defs $defs;
|
||||
|
||||
private const DEFAULT_MAX_LENGTH = 100;
|
||||
|
||||
public function __construct(Metadata $metadata, Defs $defs)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->defs = $defs;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
return $this->isNotEmpty($entity, $field);
|
||||
@@ -55,9 +70,92 @@ class ArrayType
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rawCheckArray(StdClass $data, string $field): bool
|
||||
public function checkArrayOfString(Entity $entity, string $field): bool
|
||||
{
|
||||
if (isset($data->$field) && $data->$field !== null && !is_array($data->$field)) {
|
||||
/** @var ?mixed[] $list */
|
||||
$list = $entity->get($field);
|
||||
|
||||
if ($list === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($list as $item) {
|
||||
if (!is_string($item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
if (!$entity->has($field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var ?string[] $value */
|
||||
$value = $entity->get($field);
|
||||
|
||||
if ($value === null || $value === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$fieldDefs = $this->defs
|
||||
->getEntity($entity->getEntityType())
|
||||
->getField($field);
|
||||
|
||||
if ($fieldDefs->getParam('allowCustomOptions')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$optionList = $this->getOptionList($entity->getEntityType(), $field);
|
||||
|
||||
if ($optionList === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($value as $item) {
|
||||
if (!in_array($item, $optionList)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
*/
|
||||
private function getOptionList(string $entityType, string $field): ?array
|
||||
{
|
||||
$fieldDefs = $this->defs
|
||||
->getEntity($entityType)
|
||||
->getField($field);
|
||||
|
||||
/** @var ?string $path */
|
||||
$path = $fieldDefs->getParam('optionsPath');
|
||||
|
||||
/** @var string[]|null|false $optionList */
|
||||
$optionList = $path ?
|
||||
$this->metadata->get($path) :
|
||||
$fieldDefs->getParam('options');
|
||||
|
||||
if ($optionList === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For bc.
|
||||
if ($optionList === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $optionList;
|
||||
}
|
||||
|
||||
public function rawCheckArray(stdClass $data, string $field): bool
|
||||
{
|
||||
if (isset($data->$field) && !is_array($data->$field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -82,4 +180,73 @@ class ArrayType
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field, ?int $validationValue): bool
|
||||
{
|
||||
$maxLength = $validationValue ?? self::DEFAULT_MAX_LENGTH;
|
||||
|
||||
/** @var string[] $value */
|
||||
$value = $entity->get($field) ?? [];
|
||||
|
||||
foreach ($value as $item) {
|
||||
if (mb_strlen($item) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkPattern(Entity $entity, string $field, ?string $validationValue): bool
|
||||
{
|
||||
if (!$validationValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$pattern = $validationValue;
|
||||
|
||||
if ($validationValue[0] === '$') {
|
||||
$patternName = substr($validationValue, 1);
|
||||
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', $patternName, 'pattern']) ??
|
||||
$pattern;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
/** @var string[] $value */
|
||||
$value = $entity->get($field) ?? [];
|
||||
|
||||
foreach ($value as $item) {
|
||||
if ($item === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!preg_match($preparedPattern, $item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkNoEmptyString(Entity $entity, string $field, ?bool $validationValue): bool
|
||||
{
|
||||
if (!$validationValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var string[] $value */
|
||||
$value = $entity->get($field) ?? [];
|
||||
|
||||
$optionList = $this->getOptionList($entity->getEntityType(), $field) ?? [];
|
||||
|
||||
foreach ($value as $item) {
|
||||
if ($item === '' && !in_array($item, $optionList)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators\Attachment;
|
||||
|
||||
use Espo\Classes\FieldValidators\LinkParentType;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class Related extends LinkParentType
|
||||
{
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
$typeValue = $entity->get($field . 'Type');
|
||||
|
||||
if ($typeValue === 'TemplateManager') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::checkValid($entity, $field);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,4 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
class ChecklistType extends ArrayType
|
||||
{
|
||||
|
||||
}
|
||||
class ChecklistType extends ArrayType {}
|
||||
|
||||
@@ -29,10 +29,21 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class EmailType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private const DEFAULT_MAX_LENGTH = 255;
|
||||
|
||||
public function __construct(Metadata $metadata)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
}
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
if ($this->isNotEmpty($entity, $field)) {
|
||||
@@ -71,6 +82,10 @@ class EmailType
|
||||
}
|
||||
|
||||
foreach ($dataList as $item) {
|
||||
if (!$item instanceof stdClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($item->emailAddress)) {
|
||||
continue;
|
||||
}
|
||||
@@ -85,6 +100,36 @@ class EmailType
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field): bool
|
||||
{
|
||||
/** @var ?string $value */
|
||||
$value = $entity->get($field);
|
||||
|
||||
/** @var int $maxLength */
|
||||
$maxLength = $this->metadata->get(['entityDefs', 'EmailAddress', 'fields', 'name', 'maxLength']) ??
|
||||
self::DEFAULT_MAX_LENGTH;
|
||||
|
||||
if ($value && mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dataList = $entity->get($field . 'Data');
|
||||
|
||||
if (!is_array($dataList)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($dataList as $item) {
|
||||
$value = $item->emailAddress;
|
||||
|
||||
if ($value && mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function isNotEmpty(Entity $entity, string $field): bool
|
||||
{
|
||||
return $entity->has($field) && $entity->get($field) !== '' && $entity->get($field) !== null;
|
||||
|
||||
@@ -29,15 +29,90 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class EnumType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private Defs $defs;
|
||||
|
||||
private const DEFAULT_MAX_LENGTH = 255;
|
||||
|
||||
public function __construct(Metadata $metadata, Defs $defs)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->defs = $defs;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
return $this->isNotEmpty($entity, $field);
|
||||
}
|
||||
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
if (!$entity->has($field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$fieldDefs = $this->defs
|
||||
->getEntity($entity->getEntityType())
|
||||
->getField($field);
|
||||
|
||||
/** @var ?string $path */
|
||||
$path = $fieldDefs->getParam('optionsPath');
|
||||
|
||||
/** @var string[]|null|false $optionList */
|
||||
$optionList = $path ?
|
||||
$this->metadata->get($path) :
|
||||
$fieldDefs->getParam('options');
|
||||
|
||||
if ($optionList === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For bc.
|
||||
if ($optionList === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$optionList = array_map(
|
||||
fn ($item) => $item === '' ? null : $item,
|
||||
$optionList
|
||||
);
|
||||
|
||||
$value = $entity->get($field);
|
||||
|
||||
// For bc.
|
||||
// @todo Remove in v8.0.
|
||||
if ($value === '') {
|
||||
$value = null;
|
||||
}
|
||||
|
||||
return in_array($value, $optionList);
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field, ?int $validationValue): bool
|
||||
{
|
||||
if (!$this->isNotEmpty($entity, $field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$value = $entity->get($field);
|
||||
|
||||
$maxLength = $validationValue ?? self::DEFAULT_MAX_LENGTH;
|
||||
|
||||
if (mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function isNotEmpty(Entity $entity, string $field): bool
|
||||
{
|
||||
return $entity->has($field) && $entity->get($field) !== null;
|
||||
|
||||
@@ -37,7 +37,7 @@ class JsonArrayType
|
||||
{
|
||||
public function rawCheckArray(StdClass $data, string $field): bool
|
||||
{
|
||||
if (isset($data->$field) && $data->$field !== null && !is_array($data->$field)) {
|
||||
if (isset($data->$field) && !is_array($data->$field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,20 +29,240 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class LinkMultipleType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
private Defs $defs;
|
||||
|
||||
private const COLUMN_TYPE_ENUM = 'enum';
|
||||
private const COLUMN_TYPE_VARCHAR = 'varchar';
|
||||
private const COLUMN_TYPE_BOOL = 'bool';
|
||||
|
||||
public function __construct(Metadata $metadata, Defs $defs)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->defs = $defs;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $idList */
|
||||
$idList = $entity->getLinkMultipleIdList($field);
|
||||
|
||||
return count($idList) > 0;
|
||||
}
|
||||
|
||||
public function checkPattern(Entity $entity, string $field): bool
|
||||
{
|
||||
/** @var ?mixed[] $idList */
|
||||
$idList = $entity->get($field . 'Ids');
|
||||
|
||||
if ($idList === null || $idList === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', 'id', 'pattern']);
|
||||
|
||||
if (!$pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
foreach ($idList as $id) {
|
||||
if (!is_string($id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!preg_match($preparedPattern, $id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkColumnsValid(Entity $entity, string $field): bool
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$entity->has($field . 'Columns')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var ?stdClass $columnsData */
|
||||
$columnsData = $entity->get($field . 'Columns');
|
||||
|
||||
if ($columnsData === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$entityDefs = $this->defs->getEntity($entity->getEntityType());
|
||||
$fieldDefs = $entityDefs->getField($field);
|
||||
|
||||
if ($fieldDefs->isNotStorable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var ?array<string,string> $columnsMap */
|
||||
$columnsMap = $fieldDefs->getParam('columns');
|
||||
|
||||
if ($columnsMap === null || $columnsMap === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$entityDefs->hasRelation($field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$relationDefs = $entityDefs->getRelation($field);
|
||||
|
||||
if (!$relationDefs->hasForeignEntityType()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$foreignEntityType = $relationDefs->getForeignEntityType();
|
||||
|
||||
foreach (array_keys(get_object_vars($columnsData)) as $id) {
|
||||
$itemData = $columnsData->$id;
|
||||
|
||||
if (!$itemData instanceof stdClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($columnsMap as $column => $foreignField) {
|
||||
if (!property_exists($itemData, $column)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $itemData->$column;
|
||||
|
||||
$result = $this->checkColumnValue($foreignEntityType, $foreignField, $value);
|
||||
|
||||
if (!$result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function checkColumnValue(string $entityType, string $field, $value): bool
|
||||
{
|
||||
$fieldDefs = $this->defs
|
||||
->getEntity($entityType)
|
||||
->getField($field);
|
||||
|
||||
$type = $fieldDefs->getType();
|
||||
|
||||
if ($type === self::COLUMN_TYPE_VARCHAR) {
|
||||
return $this->checkColumnValueVarchar($fieldDefs, $value);
|
||||
}
|
||||
|
||||
if ($type === self::COLUMN_TYPE_ENUM) {
|
||||
return $this->checkColumnValueEnum($fieldDefs, $value);
|
||||
}
|
||||
|
||||
if ($type === self::COLUMN_TYPE_BOOL) {
|
||||
return is_bool($value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function checkColumnValueVarchar(Defs\FieldDefs $fieldDefs, $value): bool
|
||||
{
|
||||
if ($value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$maxLength = $fieldDefs->getParam('maxLength');
|
||||
$pattern = $fieldDefs->getParam('pattern');
|
||||
|
||||
if ($maxLength && mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($pattern) {
|
||||
if ($pattern[0] === '$') {
|
||||
$patternName = substr($pattern, 1);
|
||||
|
||||
$pattern = $this->metadata
|
||||
->get(['app', 'regExpPatterns', $patternName, 'pattern']) ??
|
||||
$pattern;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
if (!preg_match($preparedPattern, $value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function checkColumnValueEnum(Defs\FieldDefs $fieldDefs, $value): bool
|
||||
{
|
||||
if (!is_string($value) && $value !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var ?string $path */
|
||||
$path = $fieldDefs->getParam('optionsPath');
|
||||
|
||||
/** @var string[]|null|false $optionList */
|
||||
$optionList = $path ?
|
||||
$this->metadata->get($path) :
|
||||
$fieldDefs->getParam('options');
|
||||
|
||||
if ($optionList === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For bc.
|
||||
if ($optionList === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$optionList = array_map(
|
||||
fn ($item) => $item === '' ? null : $item,
|
||||
$optionList
|
||||
);
|
||||
|
||||
// For bc.
|
||||
// @todo Remove in v8.0.
|
||||
if ($value === '') {
|
||||
$value = null;
|
||||
}
|
||||
|
||||
return in_array($value, $optionList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,21 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class LinkParentType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
private Defs $defs;
|
||||
|
||||
public function __construct(Metadata $metadata, Defs $defs)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->defs = $defs;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
$idAttribute = $field . 'Id';
|
||||
@@ -52,4 +63,46 @@ class LinkParentType
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkPattern(Entity $entity, string $field): bool
|
||||
{
|
||||
/** @var ?string $idValue */
|
||||
$idValue = $entity->get($field . 'Id');
|
||||
|
||||
if ($idValue === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', 'id', 'pattern']);
|
||||
|
||||
if (!$pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
return (bool) preg_match($preparedPattern, $idValue);
|
||||
}
|
||||
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
/** @var ?string $typeValue */
|
||||
$typeValue = $entity->get($field . 'Type');
|
||||
|
||||
if ($typeValue === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var ?string[] $entityTypeList */
|
||||
$entityTypeList = $this->defs
|
||||
->getEntity($entity->getEntityType())
|
||||
->getField($field)
|
||||
->getParam('entityList');
|
||||
|
||||
if ($entityTypeList !== null) {
|
||||
return in_array($typeValue, $entityTypeList);
|
||||
}
|
||||
|
||||
return (bool) $this->metadata->get(['entityDefs', $typeValue]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,18 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class LinkType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
public function __construct(Metadata $metadata)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
$idAttribute = $field . 'Id';
|
||||
@@ -43,4 +51,23 @@ class LinkType
|
||||
|
||||
return $entity->get($idAttribute) !== null && $entity->get($idAttribute) !== '';
|
||||
}
|
||||
|
||||
public function checkPattern(Entity $entity, string $field): bool
|
||||
{
|
||||
$idValue = $entity->get($field . 'Id');
|
||||
|
||||
if ($idValue === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', 'id', 'pattern']);
|
||||
|
||||
if (!$pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
return (bool) preg_match($preparedPattern, $idValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,12 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class MultiEnumType extends ArrayType
|
||||
{
|
||||
|
||||
public function checkNoEmptyString(Entity $entity, string $field, ?bool $validationValue): bool
|
||||
{
|
||||
return parent::checkNoEmptyString($entity, $field, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class PasswordType
|
||||
{
|
||||
private const DEFAULT_MAX_LENGTH = 255;
|
||||
|
||||
public function rawCheckValid(stdClass $data, string $field): bool
|
||||
{
|
||||
$value = $data->$field ?? null;
|
||||
|
||||
if ($value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return is_string($value);
|
||||
}
|
||||
|
||||
public function rawCheckMaxLength(stdClass $data, string $field, ?int $validationValue): bool
|
||||
{
|
||||
$value = $data->$field ?? null;
|
||||
|
||||
if (!is_string($value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$maxLength = $validationValue ?? self::DEFAULT_MAX_LENGTH;
|
||||
|
||||
if (mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,26 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class PhoneType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private Defs $defs;
|
||||
|
||||
private const DEFAULT_MAX_LENGTH = 36;
|
||||
|
||||
public function __construct(Metadata $metadata, Defs $defs)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->defs = $defs;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
if ($this->isNotEmpty($entity, $field)) {
|
||||
@@ -54,6 +70,132 @@ class PhoneType
|
||||
return false;
|
||||
}
|
||||
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
if ($this->isNotEmpty($entity, $field)) {
|
||||
$number = $entity->get($field);
|
||||
|
||||
if (!$this->isValidNumber($number)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$dataList = $entity->get($field . 'Data');
|
||||
|
||||
if (!is_array($dataList)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($dataList as $item) {
|
||||
if (!$item instanceof stdClass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$number = $item->phoneNumber ?? null;
|
||||
$type = $item->type ?? null;
|
||||
|
||||
if (!$number) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isValidNumber($number)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isValidType($entity->getEntityType(), $field, $type)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field): bool
|
||||
{
|
||||
/** @var ?string $value */
|
||||
$value = $entity->get($field);
|
||||
|
||||
/** @var int $maxLength */
|
||||
$maxLength = $this->metadata->get(['entityDefs', 'PhoneNumber', 'fields', 'name', 'maxLength']) ??
|
||||
self::DEFAULT_MAX_LENGTH;
|
||||
|
||||
if ($value && mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dataList = $entity->get($field . 'Data');
|
||||
|
||||
if (!is_array($dataList)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($dataList as $item) {
|
||||
$value = $item->phoneNumber;
|
||||
|
||||
if ($value && mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $type
|
||||
*/
|
||||
private function isValidType(string $entityType, string $field, $type): bool
|
||||
{
|
||||
if ($type === null) {
|
||||
// Will be stored with a default type.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_string($type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var string[]|null|false $typeList */
|
||||
$typeList = $this->defs
|
||||
->getEntity($entityType)
|
||||
->getField($field)
|
||||
->getParam('typeList');
|
||||
|
||||
if ($typeList === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For bc.
|
||||
if ($typeList === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($type, $typeList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $number
|
||||
*/
|
||||
private function isValidNumber($number): bool
|
||||
{
|
||||
if (!is_string($number)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($number === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', 'phoneNumberLoose', 'pattern']);
|
||||
|
||||
if (!$pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
return (bool) preg_match($preparedPattern, $number);
|
||||
}
|
||||
|
||||
protected function isNotEmpty(Entity $entity, string $field): bool
|
||||
{
|
||||
return $entity->has($field) && $entity->get($field) !== '' && $entity->get($field) !== null;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class TextType
|
||||
{
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
return $this->isNotEmpty($entity, $field);
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field, int $validationValue): bool
|
||||
{
|
||||
if (!$this->isNotEmpty($entity, $field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$value = $entity->get($field);
|
||||
|
||||
if (mb_strlen($value) > $validationValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function isNotEmpty(Entity $entity, string $field): bool
|
||||
{
|
||||
return
|
||||
$entity->has($field) &&
|
||||
$entity->get($field) !== '' &&
|
||||
$entity->get($field) !== null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class UrlType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private VarcharType $varcharType;
|
||||
|
||||
public function __construct(Metadata $metadata, VarcharType $varcharType)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
$this->varcharType = $varcharType;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
return $this->varcharType->checkRequired($entity, $field);
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field, ?int $validationValue): bool
|
||||
{
|
||||
return $this->varcharType->checkMaxLength($entity, $field, $validationValue);
|
||||
}
|
||||
|
||||
public function checkValid(Entity $entity, string $field): bool
|
||||
{
|
||||
$value = $entity->get($field);
|
||||
|
||||
if ($value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var string $pattern */
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', 'uriOptionalProtocol', 'pattern']);
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
return (bool) preg_match($preparedPattern, $value);
|
||||
}
|
||||
}
|
||||
@@ -29,30 +29,68 @@
|
||||
|
||||
namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class VarcharType
|
||||
{
|
||||
private Metadata $metadata;
|
||||
|
||||
private const DEFAULT_MAX_LENGTH = 255;
|
||||
|
||||
public function __construct(Metadata $metadata)
|
||||
{
|
||||
$this->metadata = $metadata;
|
||||
}
|
||||
|
||||
public function checkRequired(Entity $entity, string $field): bool
|
||||
{
|
||||
return $this->isNotEmpty($entity, $field);
|
||||
}
|
||||
|
||||
public function checkMaxLength(Entity $entity, string $field, int $validationValue): bool
|
||||
public function checkMaxLength(Entity $entity, string $field, ?int $validationValue): bool
|
||||
{
|
||||
if ($this->isNotEmpty($entity, $field)) {
|
||||
$value = $entity->get($field);
|
||||
if (!$this->isNotEmpty($entity, $field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mb_strlen($value) > $validationValue) {
|
||||
return false;
|
||||
}
|
||||
$value = $entity->get($field);
|
||||
|
||||
$maxLength = $validationValue ?? self::DEFAULT_MAX_LENGTH;
|
||||
|
||||
if (mb_strlen($value) > $maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkPattern(Entity $entity, string $field, ?string $validationValue): bool
|
||||
{
|
||||
if (!$this->isNotEmpty($entity, $field) || !$validationValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$value = $entity->get($field);
|
||||
$pattern = $validationValue;
|
||||
|
||||
if ($validationValue[0] === '$') {
|
||||
$patternName = substr($validationValue, 1);
|
||||
|
||||
$pattern = $this->metadata->get(['app', 'regExpPatterns', $patternName, 'pattern']) ??
|
||||
$pattern;
|
||||
}
|
||||
|
||||
$preparedPattern = '/^' . $pattern . '$/';
|
||||
|
||||
return (bool) preg_match($preparedPattern, $value);
|
||||
}
|
||||
|
||||
protected function isNotEmpty(Entity $entity, string $field): bool
|
||||
{
|
||||
return $entity->has($field) && $entity->get($field) !== '' && $entity->get($field) !== null;
|
||||
return
|
||||
$entity->has($field) &&
|
||||
$entity->get($field) !== '' &&
|
||||
$entity->get($field) !== null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
namespace Espo\Classes\Jobs;
|
||||
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\ORM\Repository\RDBRepository;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
|
||||
@@ -134,7 +135,7 @@ class Cleanup implements JobDataLess
|
||||
|
||||
foreach ($items as $name => $item) {
|
||||
try {
|
||||
/** @var class-string<\Espo\Core\Cleanup\Cleanup> */
|
||||
/** @var class-string<\Espo\Core\Cleanup\Cleanup> $className */
|
||||
$className = $item['className'];
|
||||
|
||||
$obj = $injectableFactory->create($className);
|
||||
@@ -312,11 +313,15 @@ class Cleanup implements JobDataLess
|
||||
$datetime->modify($period);
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository('Attachment')
|
||||
->getRDBRepository(Attachment::ENTITY_TYPE)
|
||||
->where([
|
||||
'OR' => [
|
||||
[
|
||||
'role' => ['Export File', 'Mail Merge', 'Mass Pdf']
|
||||
'role' => [
|
||||
Attachment::ROLE_EXPORT_FILE,
|
||||
'Mail Merge',
|
||||
'Mass Pdf',
|
||||
]
|
||||
]
|
||||
],
|
||||
'createdAt<' => $datetime->format('Y-m-d H:i:s'),
|
||||
@@ -357,7 +362,7 @@ class Cleanup implements JobDataLess
|
||||
|
||||
$datetimeFrom->modify($fromPeriod);
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $scopeList */
|
||||
$scopeList = array_keys($this->metadata->get(['scopes']));
|
||||
|
||||
foreach ($scopeList as $scope) {
|
||||
@@ -450,6 +455,19 @@ class Cleanup implements JobDataLess
|
||||
}
|
||||
}
|
||||
|
||||
$isBeingUploadedCollection = $this->entityManager
|
||||
->getRDBRepository('Attachment')
|
||||
->sth()
|
||||
->where([
|
||||
'isBeingUploaded' => true,
|
||||
'createdAt<' => $datetime->format('Y-m-d H:i:s'),
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($isBeingUploadedCollection as $e) {
|
||||
$this->entityManager->removeEntity($e);
|
||||
}
|
||||
|
||||
$delete = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
@@ -554,7 +572,7 @@ class Cleanup implements JobDataLess
|
||||
$fileManager = $this->fileManager;
|
||||
|
||||
if ($fileManager->exists($path)) {
|
||||
/** @var string[] */
|
||||
/** @var string[] $fileList */
|
||||
$fileList = $fileManager->getFileList($path, false, '', false);
|
||||
|
||||
foreach ($fileList as $dirName) {
|
||||
@@ -713,7 +731,7 @@ class Cleanup implements JobDataLess
|
||||
|
||||
$datetime = new DateTime($period);
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $scopeList */
|
||||
$scopeList = array_keys($this->metadata->get(['scopes']));
|
||||
|
||||
foreach ($scopeList as $scope) {
|
||||
|
||||
@@ -29,40 +29,54 @@
|
||||
|
||||
namespace Espo\Classes\MassAction\User;
|
||||
|
||||
use Espo\Core\{
|
||||
MassAction\Actions\MassUpdate as MassUpdateOriginal,
|
||||
MassAction\QueryBuilder,
|
||||
MassAction\Params,
|
||||
MassAction\Result,
|
||||
MassAction\Data,
|
||||
MassAction\MassAction,
|
||||
Utils\File\Manager as FileManager,
|
||||
DataManager,
|
||||
Acl,
|
||||
ORM\EntityManager,
|
||||
Exceptions\Forbidden,
|
||||
};
|
||||
use Espo\Core\MassAction\Actions\MassUpdate as MassUpdateOriginal;
|
||||
use Espo\Core\MassAction\QueryBuilder;
|
||||
use Espo\Core\MassAction\Params;
|
||||
use Espo\Core\MassAction\Result;
|
||||
use Espo\Core\MassAction\Data;
|
||||
use Espo\Core\MassAction\MassAction;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
use Espo\Core\DataManager;
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
|
||||
use Espo\{
|
||||
Entities\User,
|
||||
ORM\Entity,
|
||||
};
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Tools\MassUpdate\Data as MassUpdateData;
|
||||
|
||||
class MassUpdate implements MassAction
|
||||
{
|
||||
private $massUpdateOriginal;
|
||||
private MassUpdateOriginal $massUpdateOriginal;
|
||||
|
||||
private $queryBuilder;
|
||||
private QueryBuilder $queryBuilder;
|
||||
|
||||
private $entityManager;
|
||||
private EntityManager $entityManager;
|
||||
|
||||
private $acl;
|
||||
private Acl $acl;
|
||||
|
||||
private $user;
|
||||
private User $user;
|
||||
|
||||
private $fileManager;
|
||||
private FileManager $fileManager;
|
||||
|
||||
private $dataManager;
|
||||
private DataManager $dataManager;
|
||||
|
||||
private const PERMISSION = 'massUpdatePermission';
|
||||
|
||||
private const SYSTEM_USER_ID = 'system';
|
||||
|
||||
/** @var string[] */
|
||||
private array $notAllowedAttributeList = [
|
||||
'type',
|
||||
'password',
|
||||
'emailAddress',
|
||||
'isAdmin',
|
||||
'isSuperAdmin',
|
||||
'isPortalUser',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
MassUpdateOriginal $massUpdateOriginal,
|
||||
@@ -86,48 +100,54 @@ class MassUpdate implements MassAction
|
||||
{
|
||||
$entityType = $params->getEntityType();
|
||||
|
||||
if (!$this->acl->check($entityType, 'edit')) {
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden("Only admin can mass-update users.");
|
||||
}
|
||||
|
||||
if (!$this->acl->check($entityType, Table::ACTION_EDIT)) {
|
||||
throw new Forbidden("No edit access for '{$entityType}'.");
|
||||
}
|
||||
|
||||
if ($this->acl->get('massUpdatePermission') !== 'yes') {
|
||||
if ($this->acl->get(self::PERMISSION) !== Table::LEVEL_YES) {
|
||||
throw new Forbidden("No mass-update permission.");
|
||||
}
|
||||
|
||||
if (
|
||||
$data->has('type') ||
|
||||
$data->has('password') ||
|
||||
$data->has('emailAddress') ||
|
||||
$data->has('isAdmin') ||
|
||||
$data->has('isSuperAdmin') ||
|
||||
$data->has('isPortalUser')
|
||||
) {
|
||||
throw new Forbidden("Not allowed fields.");
|
||||
}
|
||||
$massUpdateData = MassUpdateData::fromMassActionData($data);
|
||||
|
||||
$this->checkAccess($massUpdateData);
|
||||
|
||||
$query = $this->queryBuilder->build($params);
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository('User')
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->clone($query)
|
||||
->sth()
|
||||
->select(['id'])
|
||||
->find();
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
$this->checkEntity($entity, $data);
|
||||
$this->checkEntity($entity, $massUpdateData);
|
||||
}
|
||||
|
||||
$result = $this->massUpdateOriginal->process($params, $data);
|
||||
|
||||
$this->afterProcess($result, $data);
|
||||
$this->afterProcess($result, $massUpdateData);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function checkEntity(Entity $entity, Data $data): void
|
||||
private function checkAccess(MassUpdateData $data): void
|
||||
{
|
||||
if ($entity->getId() === 'system') {
|
||||
foreach ($this->notAllowedAttributeList as $attribute) {
|
||||
if ($data->has($attribute)) {
|
||||
throw new Forbidden("Attribute '{$attribute}' not allowed for mass-update.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkEntity(Entity $entity, MassUpdateData $data): void
|
||||
{
|
||||
if ($entity->getId() === self::SYSTEM_USER_ID) {
|
||||
throw new Forbidden("Can't update 'system' user.");
|
||||
}
|
||||
|
||||
@@ -138,9 +158,9 @@ class MassUpdate implements MassAction
|
||||
}
|
||||
}
|
||||
|
||||
protected function afterProcess(Result $result, Data $dataWrapped): void
|
||||
private function afterProcess(Result $result, MassUpdateData $dataWrapped): void
|
||||
{
|
||||
$data = $dataWrapped->getRaw();
|
||||
$data = $dataWrapped->getValues();
|
||||
|
||||
if (
|
||||
property_exists($data, 'rolesIds') ||
|
||||
@@ -168,12 +188,12 @@ class MassUpdate implements MassAction
|
||||
}
|
||||
}
|
||||
|
||||
protected function clearRoleCache(string $id): void
|
||||
private function clearRoleCache(string $id): void
|
||||
{
|
||||
$this->fileManager->removeFile('data/cache/application/acl/' . $id . '.php');
|
||||
}
|
||||
|
||||
protected function clearPortalRolesCache(): void
|
||||
private function clearPortalRolesCache(): void
|
||||
{
|
||||
$this->fileManager->removeInDir('data/cache/application/aclPortal');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\RecordHooks\Event;
|
||||
|
||||
use Espo\Core\Record\Hook\UpdateHook;
|
||||
use Espo\Core\Record\UpdateParams;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Field\Date;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Defs as OrmDefs;
|
||||
|
||||
/**
|
||||
* @implements UpdateHook<CoreEntity>
|
||||
*/
|
||||
class BeforeUpdatePreserveDuration implements UpdateHook
|
||||
{
|
||||
private OrmDefs $ormDefs;
|
||||
|
||||
public function __construct(OrmDefs $ormDefs)
|
||||
{
|
||||
$this->ormDefs = $ormDefs;
|
||||
}
|
||||
|
||||
public function process(Entity $entity, UpdateParams $params): void
|
||||
{
|
||||
/** @var CoreEntity $entity */
|
||||
|
||||
if (!$entity->isAttributeChanged('dateStart') && !$entity->isAttributeChanged('dateStartDate')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($entity->isAttributeWritten('dateEnd') || $entity->isAttributeWritten('dateEndDate')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$preserveDurationDisabled = $this->ormDefs
|
||||
->getEntity($entity->getEntityType())
|
||||
->getField('dateEnd')
|
||||
->getParam('preserveDurationDisabled');
|
||||
|
||||
if ($preserveDurationDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processDateTime($entity);
|
||||
$this->processDate($entity);
|
||||
}
|
||||
|
||||
private function processDateTime(Entity $entity): void
|
||||
{
|
||||
$dateStartFetchedString = $entity->getFetched('dateStart');
|
||||
$dateStartString = $entity->get('dateStart');
|
||||
$dateEndString = $entity->get('dateEnd');
|
||||
|
||||
if (!$dateStartFetchedString || !$dateStartString || !$dateEndString) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateStartFetched = DateTime::fromString($dateStartFetchedString);
|
||||
$dateStart = DateTime::fromString($dateStartString);
|
||||
$dateEnd = DateTime::fromString($dateEndString);
|
||||
|
||||
$diff = $dateStartFetched->diff($dateEnd);
|
||||
|
||||
$dateEndModified = $dateStart->add($diff);
|
||||
|
||||
$entity->set('dateEnd', $dateEndModified->getString());
|
||||
}
|
||||
|
||||
private function processDate(Entity $entity): void
|
||||
{
|
||||
$dateStartFetchedString = $entity->getFetched('dateStartDate');
|
||||
$dateStartString = $entity->get('dateStartDate');
|
||||
$dateEndString = $entity->get('dateEndDate');
|
||||
|
||||
if (!$dateStartFetchedString || !$dateStartString || !$dateEndString) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateStartFetched = Date::fromString($dateStartFetchedString);
|
||||
$dateStart = Date::fromString($dateStartString);
|
||||
$dateEnd = Date::fromString($dateEndString);
|
||||
|
||||
$diff = $dateStartFetched->diff($dateEnd);
|
||||
|
||||
$dateEndModified = $dateStart->add($diff);
|
||||
|
||||
$entity->set('dateEndDate', $dateEndModified->getString());
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class PortalOnlyAccount implements Filter
|
||||
'emailUser.userId' => $this->user->getId(),
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $accountIdList */
|
||||
$accountIdList = $this->user->getLinkMultipleIdList('accounts');
|
||||
|
||||
if (count($accountIdList)) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Select\ImportError\OrderItemConverters;
|
||||
|
||||
use Espo\Core\Select\Order\ItemConverter;
|
||||
use Espo\Core\Select\Order\Item;
|
||||
|
||||
use Espo\ORM\Query\Part\OrderList;
|
||||
use Espo\ORM\Query\Part\Order;
|
||||
use Espo\ORM\Query\Part\Expression as Expr;
|
||||
|
||||
class ExportLineNumber implements ItemConverter
|
||||
{
|
||||
public function convert(Item $item): OrderList
|
||||
{
|
||||
return OrderList::create([
|
||||
Order
|
||||
::create(Expr::column('exportRowIndex'))
|
||||
->withDirection($item->getOrder())
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Classes\Select\ImportError\OrderItemConverters;
|
||||
|
||||
use Espo\Core\Select\Order\ItemConverter;
|
||||
use Espo\Core\Select\Order\Item;
|
||||
|
||||
use Espo\ORM\Query\Part\OrderList;
|
||||
use Espo\ORM\Query\Part\Order;
|
||||
use Espo\ORM\Query\Part\Expression as Expr;
|
||||
|
||||
class LineNumber implements ItemConverter
|
||||
{
|
||||
public function convert(Item $item): OrderList
|
||||
{
|
||||
return OrderList::create([
|
||||
Order
|
||||
::create(Expr::column('rowIndex'))
|
||||
->withDirection($item->getOrder())
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ class OnlyMyTeam implements Filter
|
||||
|
||||
public function apply(SelectBuilder $queryBuilder, OrGroupBuilder $orGroupBuilder): void
|
||||
{
|
||||
/** @var string[] */
|
||||
/** @var string[] $teamIdList */
|
||||
$teamIdList = $this->user->getLinkMultipleIdList('teams');
|
||||
|
||||
if (count($teamIdList) === 0) {
|
||||
|
||||
@@ -106,14 +106,16 @@ class Admin
|
||||
|
||||
|
||||
/**
|
||||
* @todo Use Request.
|
||||
*
|
||||
* @param array<string,mixed> $params
|
||||
* @param string $data
|
||||
* @return array{
|
||||
* id: string,
|
||||
* version: string,
|
||||
* }
|
||||
* @throws Forbidden
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
* @todo Use Request.
|
||||
*
|
||||
*/
|
||||
public function postActionUploadUpgradePackage($params, $data): array
|
||||
{
|
||||
@@ -134,6 +136,10 @@ class Admin
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
*/
|
||||
public function postActionRunUpgrade(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
@@ -52,6 +52,11 @@ class Attachment extends RecordBase
|
||||
return parent::getActionList($request, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
*/
|
||||
public function postActionGetAttachmentFromImageUrl(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -69,6 +74,12 @@ class Attachment extends RecordBase
|
||||
->getValueMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
* @throws \Espo\Core\Exceptions\NotFound
|
||||
*/
|
||||
public function postActionGetCopiedAttachment(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -86,6 +97,11 @@ class Attachment extends RecordBase
|
||||
->getValueMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws \Espo\Core\Exceptions\NotFound
|
||||
*/
|
||||
public function getActionFile(Request $request, Response $response): void
|
||||
{
|
||||
$id = $request->getRouteParam('id');
|
||||
@@ -103,6 +119,26 @@ class Attachment extends RecordBase
|
||||
->setBody($fileData->stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
* @throws \Espo\Core\Exceptions\NotFound
|
||||
*/
|
||||
public function postActionChunk(Request $request, Response $response): void
|
||||
{
|
||||
$id = $request->getRouteParam('id');
|
||||
$body = $request->getBodyContents();
|
||||
|
||||
if (!$id || !$body) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->getAttachmentService()->uploadChunk($id, $body);
|
||||
|
||||
$response->writeBody('true');
|
||||
}
|
||||
|
||||
private function getAttachmentService(): Service
|
||||
{
|
||||
/** @var Service */
|
||||
|
||||
@@ -29,22 +29,28 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
|
||||
use Espo\Core\Controllers\Record;
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
use Espo\Entities\Email as EmailEntity;
|
||||
use Espo\Services\Email as Service;
|
||||
use Espo\Services\EmailTemplate as EmailTemplateService;
|
||||
|
||||
use Espo\Core\Utils\Crypt;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class Email extends Record
|
||||
{
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionGetCopiedAttachments(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -61,89 +67,73 @@ class Email extends Record
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Move to service.
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionSendTestEmail(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (!$this->acl->checkScope('Email')) {
|
||||
if (!$this->acl->checkScope(EmailEntity::ENTITY_TYPE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (is_null($data->password)) {
|
||||
if ($data->type == 'preferences') {
|
||||
if (!$this->user->isAdmin() && $data->id !== $this->user->id) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
$data = get_object_vars($request->getParsedBody());
|
||||
|
||||
$preferences = $this->getEntityManager()->getEntity('Preferences', $data->id);
|
||||
$allowedParamList = [
|
||||
'type',
|
||||
'id',
|
||||
'username',
|
||||
'password',
|
||||
'auth',
|
||||
'authMechanism',
|
||||
'userId',
|
||||
'fromAddress',
|
||||
'fromName',
|
||||
'server',
|
||||
'port',
|
||||
'security',
|
||||
'emailAddress',
|
||||
];
|
||||
|
||||
if (!$preferences) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (is_null($data->password)) {
|
||||
$data->password = $this->getCrypt()->decrypt($preferences->get('smtpPassword'));
|
||||
}
|
||||
}
|
||||
else if ($data->type == 'emailAccount') {
|
||||
if (!$this->acl->checkScope('EmailAccount')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (!empty($data->id)) {
|
||||
$emailAccount = $this->getEntityManager()
|
||||
->getEntity('EmailAccount', $data->id);
|
||||
|
||||
if (!$emailAccount) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
if ($emailAccount->get('assignedUserId') !== $this->user->id) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($data->password)) {
|
||||
$data->password = $this->getCrypt()->decrypt($emailAccount->get('smtpPassword'));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ($data->type == 'inboundEmail') {
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (!empty($data->id)) {
|
||||
$emailAccount = $this->getEntityManager()->getEntity('InboundEmail', $data->id);
|
||||
|
||||
if (!$emailAccount) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (is_null($data->password)) {
|
||||
$data->password = $this->getCrypt()->decrypt($emailAccount->get('smtpPassword'));
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (is_null($data->password)) {
|
||||
$data->password = $this->getConfig()->get('smtpPassword');
|
||||
}
|
||||
foreach (array_keys($data) as $key) {
|
||||
if (!in_array($key, $allowedParamList)) {
|
||||
throw new BadRequest("Not allowed parameter `{$key}`.");
|
||||
}
|
||||
}
|
||||
|
||||
$this->getEmailService()->sendTestEmail(get_object_vars($data));
|
||||
$emailAddress = $data['emailAddress'] ?? null;
|
||||
|
||||
if (!is_string($emailAddress)) {
|
||||
throw new BadRequest("No email address.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array{
|
||||
* type?: ?string,
|
||||
* id?: ?string,
|
||||
* username?: ?string,
|
||||
* password?: ?string,
|
||||
* auth?: bool,
|
||||
* authMechanism?: ?string,
|
||||
* userId?: ?string,
|
||||
* fromAddress?: ?string,
|
||||
* fromName?: ?string,
|
||||
* server: string,
|
||||
* port: int,
|
||||
* security: string,
|
||||
* emailAddress: string,
|
||||
* } $data
|
||||
*/
|
||||
|
||||
$this->getEmailService()->sendTestEmail($data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMarkAsRead(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -165,6 +155,9 @@ class Email extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMarkAsNotRead(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -193,6 +186,9 @@ class Email extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMarkAsImportant(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -214,6 +210,9 @@ class Email extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMarkAsNotImportant(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -235,6 +234,9 @@ class Email extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMoveToTrash(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -256,6 +258,9 @@ class Email extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionRetrieveFromTrash(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -282,6 +287,9 @@ class Email extends Record
|
||||
return $this->getEmailService()->getFoldersNotReadCounts();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function postActionMoveToFolder(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -305,9 +313,12 @@ class Email extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function getActionGetInsertFieldData(Request $request): stdClass
|
||||
{
|
||||
if (!$this->acl->checkScope('Email', 'create')) {
|
||||
if (!$this->acl->checkScope(EmailEntity::ENTITY_TYPE, Table::ACTION_CREATE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -329,10 +340,4 @@ class Email extends Record
|
||||
/** @var EmailTemplateService */
|
||||
return $this->getServiceFactory()->create('EmailTemplate');
|
||||
}
|
||||
|
||||
private function getCrypt(): Crypt
|
||||
{
|
||||
/** @var Crypt */
|
||||
return $this->getContainer()->get('crypt');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Mail\Account\PersonalAccount\Service;
|
||||
use Espo\Core\Mail\Account\Storage\Params as StorageParams;
|
||||
|
||||
@@ -44,6 +46,8 @@ class EmailAccount extends Record
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionGetFolders(Request $request): array
|
||||
{
|
||||
@@ -63,6 +67,10 @@ class EmailAccount extends Record
|
||||
return $this->getEmailAccountService()->getFolderList($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function postActionTestConnection(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
@@ -45,6 +45,8 @@ class EmailAddress extends RecordBase
|
||||
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function actionSearchInAddressBook(Request $request): array
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ class ExternalAccount extends RecordBase
|
||||
$entity->get('enabled') &&
|
||||
$this->metadata->get('integrations.' . $entity->getId() .'.allowUserAccounts')
|
||||
) {
|
||||
/** @var string */
|
||||
/** @var string $id */
|
||||
$id = $entity->getId();
|
||||
|
||||
$userAccountAclScope = $this->metadata
|
||||
@@ -118,9 +118,13 @@ class ExternalAccount extends RecordBase
|
||||
|
||||
public function getActionRead(Request $request, Response $response): stdClass
|
||||
{
|
||||
/** @var string */
|
||||
/** @var string $id */
|
||||
$id = $request->getRouteParam('id');
|
||||
|
||||
if ($id === '') {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
return $this->getRecordService()
|
||||
->read($id, ReadParams::create())
|
||||
->getValueMap();
|
||||
@@ -128,7 +132,7 @@ class ExternalAccount extends RecordBase
|
||||
|
||||
public function putActionUpdate(Request $request, Response $response): stdClass
|
||||
{
|
||||
/** @var string */
|
||||
/** @var string $id */
|
||||
$id = $request->getRouteParam('id');
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
@@ -35,12 +35,12 @@ use Espo\{
|
||||
};
|
||||
|
||||
use Espo\Core\{
|
||||
Exceptions\Conflict,
|
||||
Exceptions\Error,
|
||||
Exceptions\Forbidden,
|
||||
Exceptions\BadRequest,
|
||||
Api\Request,
|
||||
DataManager,
|
||||
};
|
||||
DataManager};
|
||||
|
||||
class FieldManager
|
||||
{
|
||||
@@ -50,6 +50,9 @@ class FieldManager
|
||||
|
||||
private $fieldManagerTool;
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function __construct(User $user, DataManager $dataManager, FieldManagerTool $fieldManagerTool)
|
||||
{
|
||||
$this->user = $user;
|
||||
@@ -59,6 +62,9 @@ class FieldManager
|
||||
$this->checkControllerAccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
protected function checkControllerAccess(): void
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
@@ -68,6 +74,8 @@ class FieldManager
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function getActionRead(Request $request): array
|
||||
{
|
||||
@@ -83,6 +91,9 @@ class FieldManager
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @throws BadRequest
|
||||
* @throws Conflict
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionCreate(Request $request): array
|
||||
{
|
||||
@@ -113,6 +124,8 @@ class FieldManager
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function patchActionUpdate(Request $request): array
|
||||
{
|
||||
@@ -121,6 +134,8 @@ class FieldManager
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function putActionUpdate(Request $request): array
|
||||
{
|
||||
@@ -146,6 +161,10 @@ class FieldManager
|
||||
return $fieldManagerTool->read($scope, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function deleteActionDelete(Request $request): bool
|
||||
{
|
||||
$scope = $request->getRouteParam('scope');
|
||||
@@ -162,6 +181,10 @@ class FieldManager
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionResetToDefault(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -173,7 +196,6 @@ class FieldManager
|
||||
$this->fieldManagerTool->resetToDefault($data->scope, $data->name);
|
||||
|
||||
$this->dataManager->clearCache();
|
||||
|
||||
$this->dataManager->rebuildMetadata();
|
||||
|
||||
return true;
|
||||
|
||||
@@ -144,6 +144,25 @@ class Import extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws \Espo\Core\Exceptions\NotFound
|
||||
*/
|
||||
public function postActionExportErrors(Request $request): stdClass
|
||||
{
|
||||
$id = $request->getParsedBody()->id ?? null;
|
||||
|
||||
if (!$id) {
|
||||
throw new BadRequest("No `id`.");
|
||||
}
|
||||
|
||||
$attachmentId = $this->getImportService()->exportErrors($id);
|
||||
|
||||
return (object) [
|
||||
'attachmentId' => $attachmentId,
|
||||
];
|
||||
}
|
||||
|
||||
public function putActionUpdate(Request $request, Response $response): stdClass
|
||||
{
|
||||
throw new Forbidden();
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Controllers\Record;
|
||||
|
||||
class ImportError extends Record
|
||||
{
|
||||
}
|
||||
@@ -44,6 +44,7 @@ class InboundEmail extends Record
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
*/
|
||||
public function postActionGetFolders(Request $request): array
|
||||
{
|
||||
|
||||
@@ -56,7 +56,7 @@ class Integration
|
||||
|
||||
public function getActionRead(Request $request): stdClass
|
||||
{
|
||||
/** @var string */
|
||||
/** @var string $id */
|
||||
$id = $request->getRouteParam('id');
|
||||
|
||||
$entity = $this->service->read($id);
|
||||
@@ -66,7 +66,7 @@ class Integration
|
||||
|
||||
public function putActionUpdate(Request $request): stdClass
|
||||
{
|
||||
/** @var string */
|
||||
/** @var string $id */
|
||||
$id = $request->getRouteParam('id');
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class Kanban
|
||||
|
||||
public function getActionGetData(Request $request): stdClass
|
||||
{
|
||||
/** @var string */
|
||||
/** @var string $entityType */
|
||||
$entityType = $request->getRouteParam('entityType');
|
||||
|
||||
$searchParams = $this->searchParamsFetcher->fetch($request);
|
||||
|
||||
@@ -29,10 +29,12 @@
|
||||
|
||||
namespace Espo\Controllers;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Services\Layout as Service;
|
||||
use Espo\Entities\User;
|
||||
|
||||
@@ -50,6 +52,10 @@ class Layout
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function getActionRead(Request $request)
|
||||
{
|
||||
@@ -67,6 +73,10 @@ class Layout
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function putActionUpdate(Request $request)
|
||||
{
|
||||
@@ -95,6 +105,10 @@ class Layout
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionResetToDefault(Request $request)
|
||||
{
|
||||
@@ -113,6 +127,10 @@ class Layout
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function getActionGetOriginal(Request $request)
|
||||
{
|
||||
|
||||
@@ -99,6 +99,7 @@ class LeadCapture extends Record
|
||||
|
||||
/**
|
||||
* @return stdClass[]
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function getActionSmtpAccountDataList(): array
|
||||
{
|
||||
|
||||
@@ -119,6 +119,7 @@ class MassAction
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function prepareMassActionParams(stdClass $data): array
|
||||
{
|
||||
@@ -149,6 +150,9 @@ class MassAction
|
||||
throw new BadRequest("Bad search params for mass action.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
private function convertResult(ServiceResult $serviceResult): stdClass
|
||||
{
|
||||
if (!$serviceResult->hasResult()) {
|
||||
|
||||
@@ -49,6 +49,7 @@ class Metadata extends Base
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function getActionGet(Request $request)
|
||||
{
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
namespace Espo\Core;
|
||||
|
||||
use Espo\Core\{
|
||||
Acl\GlobalRestriction,
|
||||
Acl\Table,
|
||||
Acl\Exceptions\NotImplemented,
|
||||
};
|
||||
Acl\Exceptions\NotImplemented};
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
@@ -283,7 +283,7 @@ class Acl
|
||||
/**
|
||||
* Get a restricted field list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param string|string[] $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedFieldList(string $scope, $type): array
|
||||
@@ -294,7 +294,7 @@ class Acl
|
||||
/**
|
||||
* Get a restricted attribute list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param string|string[] $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedAttributeList(string $scope, $type): array
|
||||
@@ -305,7 +305,7 @@ class Acl
|
||||
/**
|
||||
* Get a restricted link list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param string|string[] $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedLinkList(string $scope, $type): array
|
||||
|
||||
@@ -85,7 +85,7 @@ class AccessCheckerFactory
|
||||
*/
|
||||
private function getClassName(string $scope): string
|
||||
{
|
||||
/** @var ?class-string<AccessChecker> */
|
||||
/** @var ?class-string<AccessChecker> $className1 */
|
||||
$className1 = $this->metadata->get(['aclDefs', $scope, 'accessCheckerClassName']);
|
||||
|
||||
if ($className1) {
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Acl\AccessChecker\AccessCheckers;
|
||||
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Core\Utils\Metadata;
|
||||
|
||||
use Espo\Core\Acl\DefaultAccessChecker;
|
||||
use Espo\Core\Acl\Traits\DefaultAccessCheckerDependency;
|
||||
use Espo\Core\Acl\AccessEntityCreateChecker;
|
||||
use Espo\Core\Acl\AccessEntityReadChecker;
|
||||
use Espo\Core\Acl\AccessEntityEditChecker;
|
||||
use Espo\Core\Acl\AccessEntityDeleteChecker;
|
||||
use Espo\Core\Acl\AccessEntityStreamChecker;
|
||||
use Espo\Core\Acl\ScopeData;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Access is determined by access to a foreign entity.
|
||||
*
|
||||
* @implements AccessEntityCreateChecker<Entity>
|
||||
* @implements AccessEntityReadChecker<Entity>
|
||||
* @implements AccessEntityEditChecker<Entity>
|
||||
* @implements AccessEntityDeleteChecker<Entity>
|
||||
* @implements AccessEntityStreamChecker<Entity>
|
||||
*/
|
||||
class Foreign implements
|
||||
|
||||
AccessEntityCreateChecker,
|
||||
AccessEntityReadChecker,
|
||||
AccessEntityEditChecker,
|
||||
AccessEntityDeleteChecker,
|
||||
AccessEntityStreamChecker
|
||||
{
|
||||
use DefaultAccessCheckerDependency;
|
||||
|
||||
private Metadata $metadata;
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
DefaultAccessChecker $defaultAccessChecker,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->defaultAccessChecker = $defaultAccessChecker;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
private function getForeignEntity(Entity $entity): ?Entity
|
||||
{
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$link = $this->metadata->get(['aclDefs', $entityType, 'link']);
|
||||
|
||||
if (!$link) {
|
||||
throw new LogicException("No `link` in aclDefs for {$entityType}.");
|
||||
}
|
||||
|
||||
if ($entity->isNew()) {
|
||||
$foreignEntityType = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType)
|
||||
->getRelation($link)
|
||||
->getForeignEntityType();
|
||||
|
||||
/** @var ?string $id */
|
||||
$id = $entity->get($link . 'Id');
|
||||
|
||||
if (!$id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->entityManager->getEntityById($foreignEntityType, $id);
|
||||
}
|
||||
|
||||
return $this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->getRelation($entity, $link)
|
||||
->findOne();
|
||||
}
|
||||
|
||||
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityCreate($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityRead($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityEdit($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityDelete($user, $entity, $data);
|
||||
}
|
||||
|
||||
public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool
|
||||
{
|
||||
$foreign = $this->getForeignEntity($entity);
|
||||
|
||||
if (!$foreign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->defaultAccessChecker->checkEntityStream($user, $entity, $data);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class AssignmentCheckerFactory
|
||||
*/
|
||||
private function getClassName(string $scope): string
|
||||
{
|
||||
/** @var ?class-string<AssignmentChecker<\Espo\ORM\Entity>> */
|
||||
/** @var ?class-string<AssignmentChecker<\Espo\ORM\Entity>> $className */
|
||||
$className = $this->metadata->get(['aclDefs', $scope, 'assignmentCheckerClassName']);
|
||||
|
||||
if ($className) {
|
||||
|
||||
@@ -228,7 +228,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $userTeamIdList */
|
||||
$userTeamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS);
|
||||
|
||||
foreach ($newIdList as $id) {
|
||||
@@ -331,7 +331,7 @@ class DefaultAssignmentChecker implements AssignmentChecker
|
||||
|
||||
private function isPermittedAssignedUsersLevelNo(User $user, CoreEntity $entity): bool
|
||||
{
|
||||
/** @var string[] */
|
||||
/** @var string[] $userIdList */
|
||||
$userIdList = $entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS);
|
||||
|
||||
$fetchedAssignedUserIdList = $entity->getFetched(self::ATTR_ASSIGNED_USERS_IDS);
|
||||
@@ -351,12 +351,12 @@ class DefaultAssignmentChecker implements AssignmentChecker
|
||||
|
||||
private function isPermittedAssignedUsersLevelTeam(User $user, CoreEntity $entity): bool
|
||||
{
|
||||
/** @var string[] */
|
||||
/** @var string[] $userIdList */
|
||||
$userIdList = $entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS);
|
||||
|
||||
$fetchedAssignedUserIdList = $entity->getFetched(self::ATTR_ASSIGNED_USERS_IDS);
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $teamIdList */
|
||||
$teamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS);
|
||||
|
||||
foreach ($userIdList as $userId) {
|
||||
|
||||
@@ -85,7 +85,7 @@ class DefaultOwnershipChecker implements OwnershipOwnChecker, OwnershipTeamCheck
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[] $userTeamIdList */
|
||||
$userTeamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS);
|
||||
|
||||
if (
|
||||
|
||||
+68
-42
@@ -33,7 +33,6 @@ use Espo\Core\{
|
||||
Utils\Metadata,
|
||||
Utils\DataCache,
|
||||
Utils\FieldUtil,
|
||||
Utils\Log,
|
||||
Utils\Config,
|
||||
};
|
||||
|
||||
@@ -43,68 +42,75 @@ use stdClass;
|
||||
* Lists of restricted fields can be obtained from here. Restricted fields
|
||||
* are specified in metadata > entityAcl.
|
||||
*/
|
||||
class GlobalRestricton
|
||||
class GlobalRestriction
|
||||
{
|
||||
/** Totally forbidden. */
|
||||
public const TYPE_FORBIDDEN = 'forbidden';
|
||||
|
||||
/** Reading forbidden, writing allowed. */
|
||||
public const TYPE_INTERNAL = 'internal';
|
||||
|
||||
/** Forbidden for non-admin users. */
|
||||
public const TYPE_ONLY_ADMIN = 'onlyAdmin';
|
||||
|
||||
/** Read-only for all users. */
|
||||
public const TYPE_READ_ONLY = 'readOnly';
|
||||
|
||||
/** Read-only for non-admin users. */
|
||||
public const TYPE_NON_ADMIN_READ_ONLY = 'nonAdminReadOnly';
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
* @var array<int,self::TYPE_*>
|
||||
*/
|
||||
protected $fieldTypeList = [
|
||||
'forbidden', // totally forbidden
|
||||
'internal', // reading forbidden, writing allowed
|
||||
'onlyAdmin', // forbidden for non admin users
|
||||
'readOnly', // read-only for all users
|
||||
'nonAdminReadOnly' // read-only for non-admin users
|
||||
private $fieldTypeList = [
|
||||
self::TYPE_FORBIDDEN,
|
||||
self::TYPE_INTERNAL,
|
||||
self::TYPE_ONLY_ADMIN,
|
||||
self::TYPE_READ_ONLY,
|
||||
self::TYPE_NON_ADMIN_READ_ONLY,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
* @var array<int,self::TYPE_*>
|
||||
*/
|
||||
protected $linkTypeList = [
|
||||
'forbidden', // totally forbidden
|
||||
'internal', // reading forbidden, writing allowed
|
||||
'onlyAdmin', // forbidden for non admin users
|
||||
'readOnly', // read-only for all users
|
||||
'nonAdminReadOnly' // read-only for non-admin users
|
||||
private $linkTypeList = [
|
||||
self::TYPE_FORBIDDEN,
|
||||
self::TYPE_INTERNAL,
|
||||
self::TYPE_ONLY_ADMIN,
|
||||
self::TYPE_READ_ONLY,
|
||||
self::TYPE_NON_ADMIN_READ_ONLY,
|
||||
];
|
||||
|
||||
/**
|
||||
* Types that should also be taken from entityDefs.
|
||||
* @var array<int,self::TYPE_*>
|
||||
*/
|
||||
private array $entityDefsTypeList = [
|
||||
self::TYPE_READ_ONLY,
|
||||
];
|
||||
|
||||
private ?stdClass $data = null;
|
||||
|
||||
protected string $cacheKey = 'entityAcl';
|
||||
private string $cacheKey = 'entityAcl';
|
||||
|
||||
private Metadata $metadata;
|
||||
|
||||
private DataCache $dataCache;
|
||||
|
||||
private FieldUtil $fieldUtil;
|
||||
|
||||
private Log $log;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
DataCache $dataCache,
|
||||
FieldUtil $fieldUtil,
|
||||
Log $log,
|
||||
Config $config
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->dataCache = $dataCache;
|
||||
$this->fieldUtil = $fieldUtil;
|
||||
$this->log = $log;
|
||||
|
||||
$useCache = $config->get('useCache');
|
||||
|
||||
if ($useCache && $this->dataCache->has($this->cacheKey)) {
|
||||
/** @var stdClass */
|
||||
/** @var stdClass $cachedData */
|
||||
$cachedData = $this->dataCache->get($this->cacheKey);
|
||||
|
||||
$this->data = $cachedData;
|
||||
@@ -121,25 +127,25 @@ class GlobalRestricton
|
||||
}
|
||||
}
|
||||
|
||||
protected function storeCacheFile(): void
|
||||
private function storeCacheFile(): void
|
||||
{
|
||||
assert($this->data !== null);
|
||||
|
||||
$this->dataCache->store($this->cacheKey, $this->data);
|
||||
}
|
||||
|
||||
protected function buildData(): void
|
||||
private function buildData(): void
|
||||
{
|
||||
/** @var string[] */
|
||||
$scopeList = array_keys($this->metadata->get(['entityDefs'], []));
|
||||
/** @var string[] $scopeList */
|
||||
$scopeList = array_keys($this->metadata->get(['entityDefs']) ?? []);
|
||||
|
||||
$data = (object) [];
|
||||
|
||||
foreach ($scopeList as $scope) {
|
||||
/** @var string[] */
|
||||
$fieldList = array_keys($this->metadata->get(['entityDefs', $scope, 'fields'], []));
|
||||
/** @var string[] */
|
||||
$linkList = array_keys($this->metadata->get(['entityDefs', $scope, 'links'], []));
|
||||
/** @var string[] $fieldList */
|
||||
$fieldList = array_keys($this->metadata->get(['entityDefs', $scope, 'fields']) ?? []);
|
||||
/** @var string[] $linkList */
|
||||
$linkList = array_keys($this->metadata->get(['entityDefs', $scope, 'links']) ?? []);
|
||||
|
||||
$isNotEmpty = false;
|
||||
|
||||
@@ -154,16 +160,24 @@ class GlobalRestricton
|
||||
$resultAttributeList = [];
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
if ($this->metadata->get(['entityAcl', $scope, 'fields', $field, $type])) {
|
||||
$isNotEmpty = true;
|
||||
$value = $this->metadata->get(['entityAcl', $scope, 'fields', $field, $type]);
|
||||
|
||||
$resultFieldList[] = $field;
|
||||
if (!$value && in_array($type, $this->entityDefsTypeList)) {
|
||||
$value = $this->metadata->get(['entityDefs', $scope, 'fields', $field, $type]);
|
||||
}
|
||||
|
||||
$fieldAttributeList = $this->fieldUtil->getAttributeList($scope, $field);
|
||||
if (!$value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($fieldAttributeList as $attribute) {
|
||||
$resultAttributeList[] = $attribute;
|
||||
}
|
||||
$isNotEmpty = true;
|
||||
|
||||
$resultFieldList[] = $field;
|
||||
|
||||
$fieldAttributeList = $this->fieldUtil->getAttributeList($scope, $field);
|
||||
|
||||
foreach ($fieldAttributeList as $attribute) {
|
||||
$resultAttributeList[] = $attribute;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,12 +189,21 @@ class GlobalRestricton
|
||||
$resultLinkList = [];
|
||||
|
||||
foreach ($linkList as $link) {
|
||||
if ($this->metadata->get(['entityAcl', $scope, 'links', $link, $type])) {
|
||||
$isNotEmpty = true;
|
||||
$value = $this->metadata->get(['entityAcl', $scope, 'links', $link, $type]);
|
||||
|
||||
$resultLinkList[] = $link;
|
||||
if (!$value && in_array($type, $this->entityDefsTypeList)) {
|
||||
$value = $this->metadata->get(['entityDefs', $scope, 'links', $link, $type]);
|
||||
}
|
||||
|
||||
if (!$value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isNotEmpty = true;
|
||||
|
||||
$resultLinkList[] = $link;
|
||||
}
|
||||
|
||||
$scopeData->links->$type = $resultLinkList;
|
||||
}
|
||||
|
||||
@@ -193,6 +216,7 @@ class GlobalRestricton
|
||||
}
|
||||
|
||||
/**
|
||||
* @param self::TYPE_* $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedFieldList(string $scope, string $type): array
|
||||
@@ -215,6 +239,7 @@ class GlobalRestricton
|
||||
}
|
||||
|
||||
/**
|
||||
* @param self::TYPE_* $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedAttributeList(string $scope, string $type): array
|
||||
@@ -237,6 +262,7 @@ class GlobalRestricton
|
||||
}
|
||||
|
||||
/**
|
||||
* @param self::TYPE_* $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedLinkList(string $scope, string $type): array
|
||||
@@ -95,7 +95,7 @@ class Map
|
||||
$this->cacheKey = $cacheKeyProvider->get();
|
||||
|
||||
if ($this->config->get('useCache') && $this->dataCache->has($this->cacheKey)) {
|
||||
/** @var stdClass */
|
||||
/** @var stdClass $cachedData */
|
||||
$cachedData = $this->dataCache->get($this->cacheKey);
|
||||
|
||||
$this->data = $cachedData;
|
||||
|
||||
@@ -80,7 +80,7 @@ class OwnershipCheckerFactory
|
||||
*/
|
||||
private function getClassName(string $scope): string
|
||||
{
|
||||
/** @var ?class-string<OwnershipChecker> */
|
||||
/** @var ?class-string<OwnershipChecker> $className */
|
||||
$className = $this->metadata->get(['aclDefs', $scope, 'ownershipCheckerClassName']);
|
||||
|
||||
if ($className) {
|
||||
|
||||
@@ -53,7 +53,7 @@ class DefaultRoleListProvider implements RoleListProvider
|
||||
{
|
||||
$roleList = [];
|
||||
|
||||
/** @var iterable<RoleEntity> */
|
||||
/** @var iterable<RoleEntity> $userRoleList */
|
||||
$userRoleList = $this->entityManager
|
||||
->getRDBRepository('User')
|
||||
->getRelation($this->user, 'roles')
|
||||
@@ -63,14 +63,14 @@ class DefaultRoleListProvider implements RoleListProvider
|
||||
$roleList[] = $role;
|
||||
}
|
||||
|
||||
/** @var iterable<\Espo\Entities\Team> */
|
||||
/** @var iterable<\Espo\Entities\Team> $teamList */
|
||||
$teamList = $this->entityManager
|
||||
->getRDBRepository('User')
|
||||
->getRelation($this->user, 'teams')
|
||||
->find();
|
||||
|
||||
foreach ($teamList as $team) {
|
||||
/** @var iterable<RoleEntity> */
|
||||
/** @var iterable<RoleEntity> $teamRoleList */
|
||||
$teamRoleList = $this->entityManager
|
||||
->getRDBRepository('Team')
|
||||
->getRelation($team, 'roles')
|
||||
|
||||
@@ -146,7 +146,7 @@ class DefaultTable implements Table
|
||||
$this->cacheKey = $cacheKeyProvider->get();
|
||||
|
||||
if ($config->get('useCache') && $dataCache->has($this->cacheKey)) {
|
||||
/** @var stdClass */
|
||||
/** @var stdClass $cachedData */
|
||||
$cachedData = $dataCache->get($this->cacheKey);
|
||||
|
||||
$this->data = $cachedData;
|
||||
@@ -248,6 +248,7 @@ class DefaultTable implements Table
|
||||
$fieldTable = (object) [];
|
||||
|
||||
$this->applyHighest($aclTable, $fieldTable);
|
||||
$this->applyDisabled($aclTable, $fieldTable);
|
||||
$this->applyAdminMandatory($aclTable, $fieldTable);
|
||||
}
|
||||
|
||||
@@ -526,10 +527,6 @@ class DefaultTable implements Table
|
||||
|
||||
protected function applyDisabled(stdClass &$table, stdClass &$fieldTable): void
|
||||
{
|
||||
if ($this->user->isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->getScopeList() as $scope) {
|
||||
if ($this->metadata->get(['scopes', $scope, 'disabled'])) {
|
||||
$table->$scope = false;
|
||||
|
||||
@@ -37,7 +37,7 @@ use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Core\{
|
||||
Acl,
|
||||
Acl\GlobalRestricton,
|
||||
Acl\GlobalRestriction,
|
||||
Acl\OwnerUserFieldProvider,
|
||||
Acl\Table\TableFactory,
|
||||
Acl\Table,
|
||||
@@ -140,9 +140,9 @@ class AclManager
|
||||
private $mapFactory;
|
||||
|
||||
/**
|
||||
* @var GlobalRestricton
|
||||
* @var GlobalRestriction
|
||||
*/
|
||||
protected $globalRestricton;
|
||||
protected $globalRestriction;
|
||||
|
||||
/**
|
||||
* @var OwnerUserFieldProvider
|
||||
@@ -159,7 +159,7 @@ class AclManager
|
||||
OwnershipCheckerFactory $ownershipCheckerFactory,
|
||||
TableFactory $tableFactory,
|
||||
MapFactory $mapFactory,
|
||||
GlobalRestricton $globalRestricton,
|
||||
GlobalRestriction $globalRestriction,
|
||||
OwnerUserFieldProvider $ownerUserFieldProvider,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
@@ -167,7 +167,7 @@ class AclManager
|
||||
$this->ownershipCheckerFactory = $ownershipCheckerFactory;
|
||||
$this->tableFactory = $tableFactory;
|
||||
$this->mapFactory = $mapFactory;
|
||||
$this->globalRestricton = $globalRestricton;
|
||||
$this->globalRestriction = $globalRestriction;
|
||||
$this->ownerUserFieldProvider = $ownerUserFieldProvider;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
@@ -487,27 +487,27 @@ class AclManager
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @return array<int,GlobalRestriction::TYPE_*>
|
||||
*/
|
||||
protected function getGlobalRestrictionTypeList(User $user, string $action = Table::ACTION_READ): array
|
||||
{
|
||||
$typeList = [
|
||||
GlobalRestricton::TYPE_FORBIDDEN,
|
||||
GlobalRestriction::TYPE_FORBIDDEN,
|
||||
];
|
||||
|
||||
if ($action === Table::ACTION_READ) {
|
||||
$typeList[] = GlobalRestricton::TYPE_INTERNAL;
|
||||
$typeList[] = GlobalRestriction::TYPE_INTERNAL;
|
||||
}
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$typeList[] = GlobalRestricton::TYPE_ONLY_ADMIN;
|
||||
$typeList[] = GlobalRestriction::TYPE_ONLY_ADMIN;
|
||||
}
|
||||
|
||||
if ($action === Table::ACTION_EDIT) {
|
||||
$typeList[] = GlobalRestricton::TYPE_READ_ONLY;
|
||||
$typeList[] = GlobalRestriction::TYPE_READ_ONLY;
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
$typeList[] = GlobalRestricton::TYPE_NON_ADMIN_READ_ONLY;
|
||||
$typeList[] = GlobalRestriction::TYPE_NON_ADMIN_READ_ONLY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,7 +596,7 @@ class AclManager
|
||||
*/
|
||||
public function checkUserPermission(User $user, $target, string $permissionType = 'user'): bool
|
||||
{
|
||||
$permission = $this->get($user, $permissionType);
|
||||
$permission = $this->getPermissionLevel($user, $permissionType);
|
||||
|
||||
if (is_object($target)) {
|
||||
$userId = $target->getId();
|
||||
@@ -618,11 +618,11 @@ class AclManager
|
||||
}
|
||||
|
||||
if ($permission === Table::LEVEL_TEAM) {
|
||||
/** @var string[] */
|
||||
/** @var string[] $teamIdList */
|
||||
$teamIdList = $user->getLinkMultipleIdList('teams');
|
||||
|
||||
/** @var \Espo\Repositories\User $userRepository */
|
||||
$userRepository = $this->entityManager->getRepository('User');
|
||||
$userRepository = $this->entityManager->getRepository(User::ENTITY_TYPE);
|
||||
|
||||
if (!$userRepository->checkBelongsToAnyOfTeams($userId, $teamIdList)) {
|
||||
return false;
|
||||
@@ -658,7 +658,7 @@ class AclManager
|
||||
/**
|
||||
* Get a restricted field list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param string|string[] $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedFieldList(string $scope, $type): array
|
||||
@@ -671,20 +671,20 @@ class AclManager
|
||||
foreach ($typeList as $type) {
|
||||
$list = array_merge(
|
||||
$list,
|
||||
$this->globalRestricton->getScopeRestrictedFieldList($scope, $type)
|
||||
$this->globalRestriction->getScopeRestrictedFieldList($scope, $type)
|
||||
);
|
||||
}
|
||||
|
||||
return array_unique($list);
|
||||
}
|
||||
|
||||
return $this->globalRestricton->getScopeRestrictedFieldList($scope, $type);
|
||||
return $this->globalRestriction->getScopeRestrictedFieldList($scope, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a restricted attribute list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param string|string[] $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedAttributeList(string $scope, $type): array
|
||||
@@ -697,20 +697,20 @@ class AclManager
|
||||
foreach ($typeList as $type) {
|
||||
$list = array_merge(
|
||||
$list,
|
||||
$this->globalRestricton->getScopeRestrictedAttributeList($scope, $type)
|
||||
$this->globalRestriction->getScopeRestrictedAttributeList($scope, $type)
|
||||
);
|
||||
}
|
||||
|
||||
return array_unique($list);
|
||||
}
|
||||
|
||||
return $this->globalRestricton->getScopeRestrictedAttributeList($scope, $type);
|
||||
return $this->globalRestriction->getScopeRestrictedAttributeList($scope, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a restricted link list for a specific scope by a restriction type.
|
||||
*
|
||||
* @param string|string[] $type
|
||||
* @param GlobalRestriction::TYPE_*|array<int,GlobalRestriction::TYPE_*> $type
|
||||
* @return string[]
|
||||
*/
|
||||
public function getScopeRestrictedLinkList(string $scope, $type): array
|
||||
@@ -723,19 +723,19 @@ class AclManager
|
||||
foreach ($typeList as $type) {
|
||||
$list = array_merge(
|
||||
$list,
|
||||
$this->globalRestricton->getScopeRestrictedLinkList($scope, $type)
|
||||
$this->globalRestriction->getScopeRestrictedLinkList($scope, $type)
|
||||
);
|
||||
}
|
||||
|
||||
return array_unique($list);
|
||||
}
|
||||
|
||||
return $this->globalRestricton->getScopeRestrictedLinkList($scope, $type);
|
||||
return $this->globalRestriction->getScopeRestrictedLinkList($scope, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity field that stores an owner-user (or multiple users).
|
||||
* Must be link or linkMulitple field. NULL means no owner.
|
||||
* Must be a link or linkMultiple field. NULL means no owner.
|
||||
*/
|
||||
public function getReadOwnerUserField(string $entityType): ?string
|
||||
{
|
||||
|
||||
@@ -65,8 +65,10 @@ class ActionFactory
|
||||
|
||||
/**
|
||||
* @param array<string,object> $with
|
||||
* @deprecated
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @todo Remove.
|
||||
* @deprecated
|
||||
*/
|
||||
public function createWith(string $action, ?string $entityType, array $with): Action
|
||||
{
|
||||
|
||||
@@ -209,7 +209,7 @@ class Merger
|
||||
{
|
||||
$list = [];
|
||||
|
||||
/** @var iterable<PhoneNumber> */
|
||||
/** @var iterable<PhoneNumber> $collection */
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, 'phoneNumbers')
|
||||
@@ -229,7 +229,7 @@ class Merger
|
||||
{
|
||||
$list = [];
|
||||
|
||||
/** @var iterable<EmailAddress> */
|
||||
/** @var iterable<EmailAddress> $collection */
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entity->getEntityType())
|
||||
->getRelation($entity, 'emailAddresses')
|
||||
|
||||
@@ -135,6 +135,7 @@ class ActionProcessor
|
||||
|
||||
/**
|
||||
* @param mixed $result
|
||||
* @throws \JsonException
|
||||
*/
|
||||
private function handleResult(Response $response, $result): void
|
||||
{
|
||||
@@ -177,7 +178,7 @@ class ActionProcessor
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var class-string */
|
||||
/** @var class-string $className */
|
||||
$className = $type->getName();
|
||||
|
||||
$firstParamClass = new ReflectionClass($className);
|
||||
|
||||
@@ -33,8 +33,6 @@ use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\ServiceUnavailable;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Authentication\Authentication;
|
||||
use Espo\Core\Authentication\AuthenticationData;
|
||||
use Espo\Core\Authentication\Result;
|
||||
@@ -69,6 +67,10 @@ class Auth
|
||||
$this->isEntryPoint = $isEntryPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Exception
|
||||
*/
|
||||
public function process(Request $request, Response $response): AuthResult
|
||||
{
|
||||
$username = null;
|
||||
@@ -87,6 +89,18 @@ class Auth
|
||||
|
||||
$hasAuthData = (bool) ($username || $authenticationMethod);
|
||||
|
||||
if (!$hasAuthData) {
|
||||
$password = $this->obtainTokenFromCookies($request);
|
||||
|
||||
if ($password) {
|
||||
$authenticationData = AuthenticationData::create()
|
||||
->withPassword($password)
|
||||
->withByTokenOnly(true);
|
||||
|
||||
$hasAuthData = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->authRequired && !$this->isEntryPoint && $hasAuthData) {
|
||||
$authResult = $this->processAuthNotRequired(
|
||||
$authenticationData,
|
||||
@@ -118,6 +132,9 @@ class Auth
|
||||
return AuthResult::createNotResolved();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function processAuthNotRequired(
|
||||
AuthenticationData $data,
|
||||
Request $request,
|
||||
@@ -140,6 +157,9 @@ class Auth
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function processWithAuthData(
|
||||
AuthenticationData $data,
|
||||
Request $request,
|
||||
@@ -178,7 +198,7 @@ class Auth
|
||||
*/
|
||||
protected function decodeAuthorizationString(string $string): array
|
||||
{
|
||||
/** @var string */
|
||||
/** @var string $stringDecoded */
|
||||
$stringDecoded = base64_decode($string);
|
||||
|
||||
if (strpos($stringDecoded, ':') === false) {
|
||||
@@ -205,6 +225,9 @@ class Auth
|
||||
$response->writeBody(Json::encode($bodyData));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function handleException(Response $response, Exception $e): void
|
||||
{
|
||||
if (
|
||||
@@ -269,6 +292,7 @@ class Auth
|
||||
|
||||
/**
|
||||
* @return array{?string,?string}
|
||||
* @throws BadRequest
|
||||
*/
|
||||
protected function obtainUsernamePasswordFromRequest(Request $request): array
|
||||
{
|
||||
@@ -290,17 +314,6 @@ class Auth
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
if (
|
||||
$request->getCookieParam('auth-username') &&
|
||||
$request->getCookieParam('auth-token')
|
||||
) {
|
||||
|
||||
$username = $request->getCookieParam('auth-username');
|
||||
$password = $request->getCookieParam('auth-token');
|
||||
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
$cgiAuthString = $request->getHeader('Http-Espo-Cgi-Auth') ??
|
||||
$request->getHeader('Redirect-Http-Espo-Cgi-Auth');
|
||||
|
||||
@@ -312,4 +325,9 @@ class Auth
|
||||
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
private function obtainTokenFromCookies(Request $request): ?string
|
||||
{
|
||||
return $request->getCookieParam('auth-token');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,12 @@
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\{
|
||||
Exceptions\Error,
|
||||
Authentication\Authentication,
|
||||
Utils\Log,
|
||||
};
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Builds Auth instance.
|
||||
*/
|
||||
@@ -77,7 +78,7 @@ class AuthBuilder
|
||||
public function build(): Auth
|
||||
{
|
||||
if (!$this->authentication) {
|
||||
throw new Error("Authentication is not set.");
|
||||
throw new RuntimeException("Authentication is not set.");
|
||||
}
|
||||
|
||||
return new Auth($this->log, $this->authentication, $this->authRequired, $this->isEntryPoint);
|
||||
|
||||
@@ -34,6 +34,7 @@ use Espo\Core\Exceptions\HasBody;
|
||||
use Espo\Core\{
|
||||
Api\Request,
|
||||
Api\Response,
|
||||
Exceptions\HasLogMessage,
|
||||
Utils\Log,
|
||||
Utils\Config,
|
||||
};
|
||||
@@ -121,14 +122,16 @@ class ErrorOutput
|
||||
$message = $exception->getMessage();
|
||||
$statusCode = $exception->getCode();
|
||||
|
||||
if ($exception instanceof HasLogMessage) {
|
||||
$message = $exception->getLogMessage();
|
||||
}
|
||||
|
||||
if ($route) {
|
||||
$this->processRoute($route, $request, $exception);
|
||||
}
|
||||
|
||||
$logLevel = 'error';
|
||||
|
||||
$messageLineFile = null;
|
||||
|
||||
$messageLineFile =
|
||||
'line: ' . $exception->getLine() . ', ' .
|
||||
'file: ' . $exception->getFile();
|
||||
@@ -176,10 +179,10 @@ class ErrorOutput
|
||||
}
|
||||
|
||||
if ($toPrintBody) {
|
||||
$codeDesription = $this->getCodeDescription($statusCode);
|
||||
$codeDescription = $this->getCodeDescription($statusCode);
|
||||
|
||||
$statusText = isset($codeDesription) ?
|
||||
$statusCode . ' '. $codeDesription :
|
||||
$statusText = isset($codeDescription) ?
|
||||
$statusCode . ' '. $codeDescription :
|
||||
'HTTP ' . $statusCode;
|
||||
|
||||
if ($message) {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Authentication\AuthenticationFactory;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Log;
|
||||
@@ -37,6 +37,7 @@ use Espo\Core\ApplicationUser;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Processes requests. Handles authentication. Obtains a controller name, action, body from a request.
|
||||
@@ -113,6 +114,10 @@ class RequestProcessor
|
||||
ob_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Espo\Core\Exceptions\NotFound
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function proceed(Request $request, Response $response): void
|
||||
{
|
||||
$this->beforeProceed($response);
|
||||
@@ -129,7 +134,7 @@ class RequestProcessor
|
||||
$actionName = $crudList[$httpMethod] ?? null;
|
||||
|
||||
if (!$actionName) {
|
||||
throw new Error("No action for method {$httpMethod}.");
|
||||
throw new BadRequest("No action for method {$httpMethod}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +148,7 @@ class RequestProcessor
|
||||
$controllerName = $request->getRouteParam('controller');
|
||||
|
||||
if (!$controllerName) {
|
||||
throw new Error("Route doesn't have specified controller.");
|
||||
throw new LogicException("Route doesn't have specified controller.");
|
||||
}
|
||||
|
||||
return ucfirst($controllerName);
|
||||
@@ -174,6 +179,7 @@ class RequestProcessor
|
||||
private function afterProceed(Response $response): void
|
||||
{
|
||||
$response
|
||||
->setHeader('X-App-Timestamp', (string) ($this->config->get('appTimestamp') ?? '0'))
|
||||
->setHeader('Expires', '0')
|
||||
->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT')
|
||||
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0')
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
|
||||
namespace Espo\Core\Api;
|
||||
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Psr\Http\Message\{
|
||||
ServerRequestInterface as Psr7Request,
|
||||
UriInterface,
|
||||
@@ -181,30 +184,44 @@ class RequestWrapper implements ApiRequest
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function getParsedBody(): stdClass
|
||||
{
|
||||
if ($this->parsedBody === null) {
|
||||
$this->initParsedBody();
|
||||
}
|
||||
|
||||
assert($this->parsedBody !== null);
|
||||
if ($this->parsedBody === null) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
return Util::cloneObject($this->parsedBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function initParsedBody(): void
|
||||
{
|
||||
$contents = $this->getBodyContents();
|
||||
|
||||
if ($this->getContentType() === 'application/json' && $contents) {
|
||||
$this->parsedBody = json_decode($contents);
|
||||
$parsedBody = Json::decode($contents);
|
||||
|
||||
if (is_array($this->parsedBody)) {
|
||||
$this->parsedBody = (object) [
|
||||
'list' => $this->parsedBody,
|
||||
if (is_array($parsedBody)) {
|
||||
$parsedBody = (object) [
|
||||
'list' => $parsedBody,
|
||||
];
|
||||
}
|
||||
|
||||
if (!$parsedBody instanceof stdClass) {
|
||||
throw new BadRequest("Body is not a JSON object.");
|
||||
}
|
||||
|
||||
$this->parsedBody = $parsedBody;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class Application
|
||||
|
||||
protected function initContainer(): void
|
||||
{
|
||||
/** @var Container */
|
||||
/** @var Container $container */
|
||||
$container = (new ContainerBuilder())->build();
|
||||
|
||||
$this->container = $container;
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Espo\Core\ApplicationRunners;
|
||||
use Espo\Core\{
|
||||
Application\Runner,
|
||||
DataManager,
|
||||
Exceptions\Error,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -48,6 +49,9 @@ class ClearCache implements Runner
|
||||
$this->dataManager = $dataManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->dataManager->clearCache();
|
||||
|
||||
@@ -42,7 +42,7 @@ class Command implements Runner
|
||||
use Cli;
|
||||
use SetupSystemUser;
|
||||
|
||||
private $commandManager;
|
||||
private ConsoleCommandManager $commandManager;
|
||||
|
||||
public function __construct(ConsoleCommandManager $commandManager)
|
||||
{
|
||||
@@ -52,10 +52,14 @@ class Command implements Runner
|
||||
public function run(): void
|
||||
{
|
||||
try {
|
||||
$this->commandManager->run($_SERVER['argv']);
|
||||
$exitStatus = $this->commandManager->run($_SERVER['argv']);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
exit($exitStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,10 @@
|
||||
|
||||
namespace Espo\Core\ApplicationRunners;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Core\{
|
||||
Application\RunnerParameterized,
|
||||
Application\Runner\Params,
|
||||
Exceptions\BadRequest,
|
||||
Exceptions\NotFound,
|
||||
Utils\ClientManager,
|
||||
Utils\Config,
|
||||
@@ -42,8 +41,7 @@ use Espo\Core\{
|
||||
Portal\Utils\Url,
|
||||
Api\ErrorOutput,
|
||||
Api\RequestWrapper,
|
||||
Api\ResponseWrapper,
|
||||
};
|
||||
Api\ResponseWrapper};
|
||||
|
||||
use Slim\{
|
||||
ResponseEmitter,
|
||||
@@ -89,7 +87,7 @@ class PortalClient implements RunnerParameterized
|
||||
$responseWrapped = new ResponseWrapper(new Response());
|
||||
|
||||
if ($requestWrapped->getMethod() !== 'GET') {
|
||||
throw new Error("Only GET request is allowed.");
|
||||
throw new BadRequest("Only GET request is allowed.");
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -33,12 +33,14 @@ use Espo\Core\Exceptions\Error;
|
||||
use Espo\Entities\Portal as PortalEntity;
|
||||
use Espo\Entities\User as UserEntity;
|
||||
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Provides information about an application, current user, portal.
|
||||
*/
|
||||
class ApplicationState
|
||||
{
|
||||
private $container;
|
||||
private Container $container;
|
||||
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
@@ -59,7 +61,7 @@ class ApplicationState
|
||||
public function getPortalId(): string
|
||||
{
|
||||
if (!$this->isPortal()) {
|
||||
throw new Error("Can't get portal ID for non-portal application.");
|
||||
throw new LogicException("Can't get portal ID for non-portal application.");
|
||||
}
|
||||
|
||||
return $this->getPortal()->getId();
|
||||
@@ -71,7 +73,7 @@ class ApplicationState
|
||||
public function getPortal(): PortalEntity
|
||||
{
|
||||
if (!$this->isPortal()) {
|
||||
throw new Error("Can't get portal for non-portal application.");
|
||||
throw new LogicException("Can't get portal for non-portal application.");
|
||||
}
|
||||
|
||||
/** @var PortalEntity */
|
||||
@@ -92,7 +94,7 @@ class ApplicationState
|
||||
public function getUser(): UserEntity
|
||||
{
|
||||
if (!$this->hasUser()) {
|
||||
throw new Error("User is not yet available.");
|
||||
throw new LogicException("User is not yet available.");
|
||||
}
|
||||
|
||||
/** @var UserEntity */
|
||||
|
||||
@@ -29,10 +29,6 @@
|
||||
|
||||
namespace Espo\Core;
|
||||
|
||||
use Espo\Core\Exceptions\{
|
||||
Error,
|
||||
};
|
||||
|
||||
use Espo\Entities\{
|
||||
User,
|
||||
};
|
||||
@@ -41,6 +37,8 @@ use Espo\Core\{
|
||||
ORM\EntityManagerProxy,
|
||||
};
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Setting a current user for the application.
|
||||
*/
|
||||
@@ -57,14 +55,14 @@ class ApplicationUser
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the system user as a current user. The system user is used when no user is logged in.
|
||||
* Set up the system user as a current user. The system user is used when no user is logged in.
|
||||
*/
|
||||
public function setupSystemUser(): void
|
||||
{
|
||||
$user = $this->entityManagerProxy->getEntity('User', 'system');
|
||||
|
||||
if (!$user) {
|
||||
throw new Error("System user is not found.");
|
||||
throw new RuntimeException("System user is not found.");
|
||||
}
|
||||
|
||||
$user->set('ipAddress', $_SERVER['REMOTE_ADDR'] ?? null);
|
||||
|
||||
@@ -50,7 +50,7 @@ class EspoManager implements Manager
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
|
||||
/** @var RDBRepository<AuthTokenEntity> */
|
||||
/** @var RDBRepository<AuthTokenEntity> $repository */
|
||||
$repository = $entityManager->getRDBRepository(AuthTokenEntity::ENTITY_TYPE);
|
||||
|
||||
$this->repository = $repository;
|
||||
@@ -58,7 +58,7 @@ class EspoManager implements Manager
|
||||
|
||||
public function get(string $token): ?AuthToken
|
||||
{
|
||||
/** @var ?AuthTokenEntity */
|
||||
/** @var ?AuthTokenEntity $authToken */
|
||||
$authToken = $this->entityManager
|
||||
->getRDBRepository(AuthTokenEntity::ENTITY_TYPE)
|
||||
->select([
|
||||
@@ -83,7 +83,7 @@ class EspoManager implements Manager
|
||||
|
||||
public function create(Data $data): AuthToken
|
||||
{
|
||||
/** @var AuthTokenEntity */
|
||||
/** @var AuthTokenEntity $authToken */
|
||||
$authToken = $this->repository->getNew();
|
||||
|
||||
$authToken->set([
|
||||
@@ -169,7 +169,7 @@ class EspoManager implements Manager
|
||||
}
|
||||
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
/** @var string */
|
||||
/** @var string $randomValue */
|
||||
$randomValue = openssl_random_pseudo_bytes($length);
|
||||
|
||||
return bin2hex($randomValue);
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
|
||||
namespace Espo\Core\Authentication;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ServiceUnavailable;
|
||||
|
||||
use Espo\Repositories\UserData as UserDataRepository;
|
||||
@@ -43,9 +42,7 @@ use Espo\Entities\{
|
||||
};
|
||||
|
||||
use Espo\Core\Authentication\{
|
||||
Result,
|
||||
Result\FailReason,
|
||||
LoginFactory,
|
||||
TwoFactor\LoginFactory as TwoFactorLoginFactory,
|
||||
AuthToken\Manager as AuthTokenManager,
|
||||
AuthToken\Data as AuthTokenData,
|
||||
@@ -124,7 +121,6 @@ class Authentication
|
||||
*
|
||||
* Warning: This method can change the state of the object (by setting the `portal` prop.).
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws ServiceUnavailable
|
||||
*/
|
||||
public function login(AuthenticationData $data, Request $request, Response $response): Result
|
||||
@@ -132,14 +128,14 @@ class Authentication
|
||||
$username = $data->getUsername();
|
||||
$password = $data->getPassword();
|
||||
$authenticationMethod = $data->getMethod();
|
||||
$byTokenOnly = $data->byTokenOnly();
|
||||
|
||||
if (
|
||||
$authenticationMethod &&
|
||||
!$this->configDataProvider->authenticationMethodIsApi($authenticationMethod)
|
||||
) {
|
||||
$this->log->warning(
|
||||
"AUTH: Trying to use not allowed authentication method '{$authenticationMethod}'."
|
||||
);
|
||||
$this->log
|
||||
->warning("AUTH: Trying to use not allowed authentication method '{$authenticationMethod}'.");
|
||||
|
||||
return $this->processFail(
|
||||
Result::fail(FailReason::METHOD_NOT_ALLOWED),
|
||||
@@ -184,9 +180,13 @@ class Authentication
|
||||
}
|
||||
}
|
||||
|
||||
$isByTokenOnly = !$authenticationMethod && $request->getHeader('Espo-Authorization-By-Token') === 'true';
|
||||
$byTokenAndUsername = $request->getHeader('Espo-Authorization-By-Token') === 'true';
|
||||
|
||||
if ($isByTokenOnly && !$authToken) {
|
||||
if ($authenticationMethod && $byTokenAndUsername) {
|
||||
return Result::fail(FailReason::DISCREPANT_DATA);
|
||||
}
|
||||
|
||||
if (($byTokenAndUsername || $byTokenOnly) && !$authToken) {
|
||||
if ($username) {
|
||||
$this->log->info(
|
||||
"AUTH: Trying to login as user '{$username}' by token but token is not found."
|
||||
@@ -200,6 +200,20 @@ class Authentication
|
||||
);
|
||||
}
|
||||
|
||||
if ($byTokenOnly) {
|
||||
assert($authToken !== null);
|
||||
|
||||
$username = $this->getUsernameByAuthToken($authToken);
|
||||
|
||||
if (!$username) {
|
||||
return $this->processFail(
|
||||
Result::fail(FailReason::USER_NOT_FOUND),
|
||||
$data,
|
||||
$request
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$authenticationMethod) {
|
||||
$authenticationMethod = $this->configDataProvider->getDefaultAuthenticationMethod();
|
||||
}
|
||||
@@ -354,7 +368,7 @@ class Authentication
|
||||
private function processAuthTokenCheck(AuthToken $authToken): bool
|
||||
{
|
||||
if ($this->allowAnyAccess && $authToken->getPortalId() && !$this->isPortal()) {
|
||||
/** @var ?Portal */
|
||||
/** @var ?Portal $portal */
|
||||
$portal = $this->entityManager->getEntity('Portal', $authToken->getPortalId());
|
||||
|
||||
if ($portal) {
|
||||
@@ -504,7 +518,7 @@ class Authentication
|
||||
);
|
||||
|
||||
if ($createSecret) {
|
||||
$this->setSecretInCookie($authToken->getSecret(), $response);
|
||||
$this->setSecretInCookie($authToken->getSecret(), $response, $request);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -563,7 +577,7 @@ class Authentication
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var AuthLogRecord */
|
||||
/** @var AuthLogRecord $authLogRecord */
|
||||
$authLogRecord = $this->entityManager->getNewEntity('AuthLogRecord');
|
||||
|
||||
$requestUrl =
|
||||
@@ -612,7 +626,7 @@ class Authentication
|
||||
$this->entityManager->saveEntity($authLogRecord);
|
||||
}
|
||||
|
||||
private function setSecretInCookie(?string $secret, Response $response): void
|
||||
private function setSecretInCookie(?string $secret, Response $response, ?Request $request = null): void
|
||||
{
|
||||
$time = $secret ? strtotime('+1000 days') : 1;
|
||||
|
||||
@@ -625,9 +639,36 @@ class Authentication
|
||||
'; HttpOnly' .
|
||||
'; SameSite=Lax';
|
||||
|
||||
if ($request && self::isSecureRequest($request)) {
|
||||
$headerValue .= "; Secure";
|
||||
}
|
||||
|
||||
$response->addHeader('Set-Cookie', $headerValue);
|
||||
}
|
||||
|
||||
private static function isSecureRequest(Request $request): bool
|
||||
{
|
||||
$https = $request->getServerParam('HTTPS');
|
||||
|
||||
if ($https === 'on') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$scheme = $request->getServerParam('REQUEST_SCHEME');
|
||||
|
||||
if ($scheme === 'https') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$forwardedProto = $request->getServerParam('HTTP_X_FORWARDED_PROTO');
|
||||
|
||||
if ($forwardedProto === 'https') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function processFail(Result $result, AuthenticationData $data, Request $request): Result
|
||||
{
|
||||
$this->hookManager->processOnFail($result, $data, $request);
|
||||
@@ -669,4 +710,20 @@ class Authentication
|
||||
/** @var UserDataRepository */
|
||||
return $this->entityManager->getRepository(UserData::ENTITY_TYPE);
|
||||
}
|
||||
|
||||
private function getUsernameByAuthToken(AuthToken $authToken): ?string
|
||||
{
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->select(['userName'])
|
||||
->where(['id' => $authToken->getUserId()])
|
||||
->findOne();
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user->getUserName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,10 @@ namespace Espo\Core\Authentication;
|
||||
|
||||
class AuthenticationData
|
||||
{
|
||||
private $username;
|
||||
|
||||
private $password;
|
||||
|
||||
private $method;
|
||||
private ?string $username;
|
||||
private ?string $password;
|
||||
private ?string $method;
|
||||
private bool $byTokenOnly = false;
|
||||
|
||||
public function __construct(
|
||||
?string $username = null,
|
||||
@@ -52,21 +51,38 @@ class AuthenticationData
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* A username.
|
||||
*/
|
||||
public function getUsername(): ?string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* A password or auth-token.
|
||||
*/
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* A method.
|
||||
*/
|
||||
public function getMethod(): ?string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate by auth-token only. No username check.
|
||||
*/
|
||||
public function byTokenOnly(): bool
|
||||
{
|
||||
return $this->byTokenOnly;
|
||||
}
|
||||
|
||||
public function withUsername(?string $username): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
@@ -90,4 +106,12 @@ class AuthenticationData
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withByTokenOnly(bool $byTokenOnly): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->byTokenOnly = $byTokenOnly;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class UserFinder
|
||||
|
||||
public function find(string $username, string $hash): ?User
|
||||
{
|
||||
/** @var ?User */
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->where([
|
||||
@@ -59,7 +59,7 @@ class UserFinder
|
||||
|
||||
public function findApiHmac(string $apiKey): ?User
|
||||
{
|
||||
/** @var ?User */
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->where([
|
||||
@@ -74,7 +74,7 @@ class UserFinder
|
||||
|
||||
public function findApiApiKey(string $apiKey): ?User
|
||||
{
|
||||
/** @var ?User */
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->where([
|
||||
|
||||
@@ -33,6 +33,7 @@ class ClientFactory
|
||||
{
|
||||
/**
|
||||
* @param array<string,mixed> $options
|
||||
* @throws \Laminas\Ldap\Exception\LdapException
|
||||
*/
|
||||
public function create(array $options): Client
|
||||
{
|
||||
|
||||
@@ -52,14 +52,14 @@ class LoginFactory
|
||||
|
||||
public function create(string $method, bool $isPortal = false): Login
|
||||
{
|
||||
/** @var class-string<Login> */
|
||||
/** @var class-string<Login> $className */
|
||||
$className = $this->metadata->get(['authenticationMethods', $method, 'implementationClassName']);
|
||||
|
||||
if (!$className) {
|
||||
$sanitizedName = preg_replace('/[^a-zA-Z0-9]+/', '', $method);
|
||||
|
||||
if (!class_exists($className)) {
|
||||
/** @var class-string<Login> */
|
||||
/** @var class-string<Login> $className */
|
||||
$className = "Espo\\Core\\Authentication\\Logins\\" . $sanitizedName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,9 +43,10 @@ use RuntimeException;
|
||||
|
||||
class Espo implements Login
|
||||
{
|
||||
private $userFinder;
|
||||
public const NAME = 'Espo';
|
||||
|
||||
private $passwordHash;
|
||||
private UserFinder $userFinder;
|
||||
private PasswordHash $passwordHash;
|
||||
|
||||
public function __construct(UserFinder $userFinder, PasswordHash $passwordHash)
|
||||
{
|
||||
@@ -81,7 +82,7 @@ class Espo implements Login
|
||||
return Result::fail(FailReason::WRONG_CREDENTIALS);
|
||||
}
|
||||
|
||||
if ($authToken && $user->id !== $authToken->getUserId()) {
|
||||
if ($authToken && $user->getId() !== $authToken->getUserId()) {
|
||||
return Result::fail(FailReason::USER_TOKEN_MISMATCH);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,10 +36,11 @@ use Espo\Core\{
|
||||
Authentication\Login\Data,
|
||||
Authentication\Result,
|
||||
Authentication\Helper\UserFinder,
|
||||
Exceptions\Error,
|
||||
Authentication\Result\FailReason,
|
||||
};
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class Hmac implements Login
|
||||
{
|
||||
private $userFinder;
|
||||
@@ -71,7 +72,7 @@ class Hmac implements Login
|
||||
$secretKey = $this->apiKeyUtil->getSecretKeyForUserId($user->getId());
|
||||
|
||||
if (!$secretKey) {
|
||||
throw new Error("No secret key for API user '" . $user->getId() . "'.");
|
||||
throw new RuntimeException("No secret key for API user '" . $user->getId() . "'.");
|
||||
}
|
||||
|
||||
$string = $request->getMethod() . ' ' . $request->getResourcePath();
|
||||
|
||||
@@ -408,6 +408,8 @@ class LDAP implements Login
|
||||
|
||||
/**
|
||||
* Find LDAP user DN by his username.
|
||||
*
|
||||
* @throws \Laminas\Ldap\Exception\LdapException
|
||||
*/
|
||||
private function findLdapUserDnByUsername(string $username): ?string
|
||||
{
|
||||
@@ -426,7 +428,7 @@ class LDAP implements Login
|
||||
'(' . $options['userNameAttribute'] . '=' . $username . ')' .
|
||||
$loginFilterString . ')';
|
||||
|
||||
/** @var array<int,array{dn: string}> */
|
||||
/** @var array<int,array{dn: string}> $result */
|
||||
$result = $ldapClient->search($searchString, null, Client::SEARCH_SCOPE_SUB);
|
||||
|
||||
$this->log->debug('LDAP: user search string: "' . $searchString . '"');
|
||||
|
||||
@@ -37,7 +37,7 @@ class Data
|
||||
{
|
||||
private ?string $message = null;
|
||||
|
||||
private ?string$token = null;
|
||||
private ?string $token = null;
|
||||
|
||||
private ?string $view = null;
|
||||
|
||||
|
||||
@@ -50,4 +50,6 @@ class FailReason
|
||||
public const HASH_NOT_MATCHED = 'Hash not matched';
|
||||
|
||||
public const METHOD_NOT_ALLOWED = 'Not allowed authentication method';
|
||||
|
||||
public const DISCREPANT_DATA = 'Discrepant authentication data';
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user